diff --git a/conductor/tracks/python_style_refactor_20260227/plan.md b/conductor/tracks/python_style_refactor_20260227/plan.md index a16c515..7ca2dcc 100644 --- a/conductor/tracks/python_style_refactor_20260227/plan.md +++ b/conductor/tracks/python_style_refactor_20260227/plan.md @@ -1,12 +1,12 @@ # Implementation Plan: AI-Optimized Python Style Refactor ## Phase 1: Research and Pilot Tooling -- [ ] Task: Conductor - Define and Test Style Transformation Logic. (Develop or adapt a tool to perform 1-space indentation and newline reduction safely). -- [ ] Task: Conductor - Run Style Pilot on a Representative Module (e.g., `theme.py`). -- [ ] Task: Conductor - User Manual Verification 'Phase 1: Pilot and Tooling' (Protocol in workflow.md) +- [x] Task: Conductor - Define and Test Style Transformation Logic. (Develop or adapt a tool to perform 1-space indentation and newline reduction safely). [c75b926] +- [x] Task: Conductor - Run Style Pilot on a Representative Module (e.g., `theme.py`). [13c15ed] +- [x] Task: Conductor - User Manual Verification 'Phase 1: Pilot and Tooling' (Protocol in workflow.md) [checkpoint: Phase1] ## Phase 2: Core Refactor - Indentation and Newlines -- [ ] Task: Conductor - Refactor Primary Engine Modules (`ai_client.py`, `aggregate.py`, `mcp_client.py`, `shell_runner.py`). +- [~] Task: Conductor - Refactor Primary Engine Modules (`ai_client.py`, `aggregate.py`, `mcp_client.py`, `shell_runner.py`). - [ ] Task: Conductor - Refactor Project & Session Management Modules (`project_manager.py`, `session_logger.py`). - [ ] Task: Conductor - Refactor UI Modules (`gui_2.py`, `gui_legacy.py`, `theme.py`, `theme_2.py`). - [ ] Task: Conductor - Refactor Remaining Utility and Support Modules (`events.py`, `file_cache.py`, `models.py`, `mma_prompts.py`). diff --git a/scripts/ai_style_formatter.py b/scripts/ai_style_formatter.py new file mode 100644 index 0000000..24bc2d9 --- /dev/null +++ b/scripts/ai_style_formatter.py @@ -0,0 +1,125 @@ +import tokenize +import io + +def format_code(source: str) -> str: + """ + Formats Python code to use exactly 1 space for indentation (including continuations), + max 1 blank line between top-level definitions, and 0 blank lines inside + function/method bodies. + + Args: + source: The Python source code to format. + + Returns: + The formatted source code. + """ + if not source: + return "" + + tokens = list(tokenize.generate_tokens(io.StringIO(source).readline)) + lines = source.splitlines(keepends=True) + num_lines = len(lines) + + block_level = 0 + paren_level = 0 + in_function_stack = [] + expecting_function_indent = False + + line_indent = {} + line_is_blank = {i: True for i in range(1, num_lines + 2)} + line_is_string_interior = {i: False for i in range(1, num_lines + 2)} + + line_seen = set() + pending_blank_lines = [] + + for tok in tokens: + t_type = tok.type + t_string = tok.string + start_line, _ = tok.start + end_line, _ = tok.end + + if t_type == tokenize.STRING: + for l in range(start_line + 1, end_line + 1): + line_is_string_interior[l] = True + + if t_type not in (tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT, tokenize.ENDMARKER): + for l in range(start_line, end_line + 1): + line_is_blank[l] = False + pending_blank_lines = [] # Real content seen, clear pending blanks + + # State updates that affect CURRENT line + if t_type == tokenize.INDENT: + block_level += 1 + if expecting_function_indent: + in_function_stack.append(block_level) + expecting_function_indent = False + elif t_type == tokenize.DEDENT: + block_level -= 1 + if in_function_stack and block_level < in_function_stack[-1]: + in_function_stack.pop() + # Retroactively update pending blank lines to the current (outer) level + for l in pending_blank_lines: + line_indent[l] = block_level + paren_level + + if t_string in (')', ']', '}'): + paren_level -= 1 + + if start_line not in line_seen: + line_indent[start_line] = block_level + paren_level + if t_type not in (tokenize.INDENT, tokenize.DEDENT): + line_seen.add(start_line) + if t_type in (tokenize.NL, tokenize.NEWLINE): + pending_blank_lines.append(start_line) + + # State updates that affect FUTURE lines/tokens + if t_type == tokenize.NAME and t_string == 'def': + expecting_function_indent = True + if t_string in ('(', '[', '{'): + paren_level += 1 + + output = [] + consecutive_blanks = 0 + + for i in range(1, num_lines + 1): + if line_is_string_interior[i]: + output.append(lines[i-1]) + continue + + if line_is_blank[i]: + indent = line_indent.get(i, 0) + if indent > 0: + continue + else: + if consecutive_blanks < 1: + output.append("\n") + consecutive_blanks += 1 + continue + + consecutive_blanks = 0 + original_line = lines[i-1] + indent = line_indent.get(i, 0) + stripped = original_line.lstrip() + + output.append(" " * indent + stripped) + if not stripped.endswith('\n') and i < num_lines: + output[-1] += '\n' + + if output and not output[-1].endswith('\n'): + output[-1] += '\n' + + return "".join(output) + +if __name__ == "__main__": + import sys + import os + if len(sys.argv) > 1: + file_path = sys.argv[1] + with open(file_path, "r", encoding="utf-8") as f: + content = f.read() + formatted = format_code(content) + if len(sys.argv) > 2 and sys.argv[2] == "--write": + with open(file_path, "w", encoding="utf-8") as f: + f.write(formatted) + else: + sys.stdout.reconfigure(encoding='utf-8') + sys.stdout.write(formatted) diff --git a/tests/test_ai_style_formatter.py b/tests/test_ai_style_formatter.py new file mode 100644 index 0000000..7e089f7 --- /dev/null +++ b/tests/test_ai_style_formatter.py @@ -0,0 +1,122 @@ +import pytest +import textwrap +from scripts.ai_style_formatter import format_code + +def test_basic_indentation(): + source = textwrap.dedent("""\ + def hello(): + print("world") + if True: + print("nested") + """) + expected = ( + "def hello():\n" + " print(\"world\")\n" + " if True:\n" + " print(\"nested\")\n" + ) + assert format_code(source) == expected + +def test_top_level_blank_lines(): + source = textwrap.dedent("""\ + def a(): + pass + + + def b(): + pass + """) + expected = ( + "def a():\n" + " pass\n" + "\n" + "def b():\n" + " pass\n" + ) + assert format_code(source) == expected + +def test_inner_blank_lines(): + source = textwrap.dedent("""\ + def a(): + print("start") + + print("end") + """) + expected = ( + "def a():\n" + " print(\"start\")\n" + " print(\"end\")\n" + ) + assert format_code(source) == expected + +def test_multiline_string_safety(): + source = textwrap.dedent("""\ + def a(): + ''' + This is a multiline + string that should + not be reformatted + inside. + ''' + pass + """) + # Note: the indentation of the ''' itself becomes 1 space. + # The content inside remains exactly as in source. + # textwrap.dedent will remove the common leading whitespace from the source. + # The source's ''' is at 4 spaces. Content is at 4 spaces. + # After dedent: + # def a(): + # ''' + # This is a... + + result = format_code(source) + assert " This is a multiline" in result + assert result.startswith("def a():\n '''") + +def test_continuation_indentation(): + source = textwrap.dedent("""\ + def long_func( + a, + b + ): + return ( + a + + b + ) + """) + expected = ( + "def long_func(\n" + " a,\n" + " b\n" + "):\n" + " return (\n" + " a +\n" + " b\n" + " )\n" + ) + assert format_code(source) == expected + +def test_multiple_top_level_definitions(): + source = textwrap.dedent("""\ + class MyClass: + def __init__(self): + self.x = 1 + + def method(self): + pass + + + def top_level(): + pass + """) + expected = ( + "class MyClass:\n" + " def __init__(self):\n" + " self.x = 1\n" + " def method(self):\n" + " pass\n" + "\n" + "def top_level():\n" + " pass\n" + ) + assert format_code(source) == expected diff --git a/theme.py b/theme.py index 09bf8ad..13c15ed 100644 --- a/theme.py +++ b/theme.py @@ -29,179 +29,175 @@ from pathlib import Path # Only keys that differ from DPG defaults need to be listed. _PALETTES: dict[str, dict] = { - - "DPG Default": {}, # empty = reset to DPG built-in defaults - - "10x Dark": { - # Window / frame chrome - "WindowBg": ( 34, 32, 28), - "ChildBg": ( 30, 28, 24), - "PopupBg": ( 35, 30, 20), - "Border": ( 60, 55, 50), - "BorderShadow": ( 0, 0, 0, 0), - "FrameBg": ( 45, 42, 38), - "FrameBgHovered": ( 60, 56, 50), - "FrameBgActive": ( 75, 70, 62), - # Title bars - "TitleBg": ( 40, 35, 25), - "TitleBgActive": ( 60, 45, 15), - "TitleBgCollapsed": ( 30, 27, 20), - # Menu bar - "MenuBarBg": ( 35, 30, 20), - # Scrollbar - "ScrollbarBg": ( 30, 28, 24), - "ScrollbarGrab": ( 80, 78, 72), - "ScrollbarGrabHovered": (100, 100, 92), - "ScrollbarGrabActive": (120, 118, 110), - # Check marks / radio buttons - "CheckMark": (194, 164, 74), - # Sliders - "SliderGrab": (126, 78, 14), - "SliderGrabActive": (194, 140, 30), - # Buttons - "Button": ( 83, 76, 60), - "ButtonHovered": (126, 78, 14), - "ButtonActive": (115, 90, 70), - # Headers (collapsing headers, selectables, listbox items) - "Header": ( 83, 76, 60), - "HeaderHovered": (126, 78, 14), - "HeaderActive": (115, 90, 70), - # Separator - "Separator": ( 70, 65, 55), - "SeparatorHovered": (126, 78, 14), - "SeparatorActive": (194, 164, 74), - # Resize grip - "ResizeGrip": ( 60, 55, 44), - "ResizeGripHovered": (126, 78, 14), - "ResizeGripActive": (194, 164, 74), - # Tab bar - "Tab": ( 83, 83, 70), - "TabHovered": (126, 77, 25), - "TabActive": (126, 77, 25), - "TabUnfocused": ( 60, 58, 50), - "TabUnfocusedActive": ( 90, 80, 55), - # Docking - "DockingPreview": (126, 78, 14, 180), - "DockingEmptyBg": ( 20, 20, 20), - # Text - "Text": (200, 200, 200), - "TextDisabled": (130, 130, 120), - # Input text cursor / selection - "TextSelectedBg": ( 59, 86, 142, 180), - # Plot / table lines - "TableHeaderBg": ( 55, 50, 38), - "TableBorderStrong": ( 70, 65, 55), - "TableBorderLight": ( 50, 47, 42), - "TableRowBg": ( 0, 0, 0, 0), - "TableRowBgAlt": ( 40, 38, 34, 40), - # Misc - "NavHighlight": (126, 78, 14), - "NavWindowingHighlight":(194, 164, 74, 180), - "NavWindowingDimBg": ( 20, 20, 20, 80), - "ModalWindowDimBg": ( 10, 10, 10, 100), - }, - - "Nord Dark": { - "WindowBg": ( 36, 41, 49), - "ChildBg": ( 30, 34, 42), - "PopupBg": ( 36, 41, 49), - "Border": ( 59, 66, 82), - "BorderShadow": ( 0, 0, 0, 0), - "FrameBg": ( 46, 52, 64), - "FrameBgHovered": ( 59, 66, 82), - "FrameBgActive": ( 67, 76, 94), - "TitleBg": ( 36, 41, 49), - "TitleBgActive": ( 59, 66, 82), - "TitleBgCollapsed": ( 30, 34, 42), - "MenuBarBg": ( 46, 52, 64), - "ScrollbarBg": ( 30, 34, 42), - "ScrollbarGrab": ( 76, 86, 106), - "ScrollbarGrabHovered": ( 94, 129, 172), - "ScrollbarGrabActive": (129, 161, 193), - "CheckMark": (136, 192, 208), - "SliderGrab": ( 94, 129, 172), - "SliderGrabActive": (129, 161, 193), - "Button": ( 59, 66, 82), - "ButtonHovered": ( 94, 129, 172), - "ButtonActive": (129, 161, 193), - "Header": ( 59, 66, 82), - "HeaderHovered": ( 94, 129, 172), - "HeaderActive": (129, 161, 193), - "Separator": ( 59, 66, 82), - "SeparatorHovered": ( 94, 129, 172), - "SeparatorActive": (136, 192, 208), - "ResizeGrip": ( 59, 66, 82), - "ResizeGripHovered": ( 94, 129, 172), - "ResizeGripActive": (136, 192, 208), - "Tab": ( 46, 52, 64), - "TabHovered": ( 94, 129, 172), - "TabActive": ( 76, 86, 106), - "TabUnfocused": ( 36, 41, 49), - "TabUnfocusedActive": ( 59, 66, 82), - "DockingPreview": ( 94, 129, 172, 180), - "DockingEmptyBg": ( 20, 22, 28), - "Text": (216, 222, 233), - "TextDisabled": (116, 128, 150), - "TextSelectedBg": ( 94, 129, 172, 180), - "TableHeaderBg": ( 59, 66, 82), - "TableBorderStrong": ( 76, 86, 106), - "TableBorderLight": ( 59, 66, 82), - "TableRowBg": ( 0, 0, 0, 0), - "TableRowBgAlt": ( 46, 52, 64, 40), - "NavHighlight": (136, 192, 208), - "ModalWindowDimBg": ( 10, 12, 16, 100), - }, - - "Monokai": { - "WindowBg": ( 39, 40, 34), - "ChildBg": ( 34, 35, 29), - "PopupBg": ( 39, 40, 34), - "Border": ( 60, 61, 52), - "BorderShadow": ( 0, 0, 0, 0), - "FrameBg": ( 50, 51, 44), - "FrameBgHovered": ( 65, 67, 56), - "FrameBgActive": ( 80, 82, 68), - "TitleBg": ( 39, 40, 34), - "TitleBgActive": ( 73, 72, 62), - "TitleBgCollapsed": ( 30, 31, 26), - "MenuBarBg": ( 50, 51, 44), - "ScrollbarBg": ( 34, 35, 29), - "ScrollbarGrab": ( 80, 80, 72), - "ScrollbarGrabHovered": (102, 217, 39), - "ScrollbarGrabActive": (166, 226, 46), - "CheckMark": (166, 226, 46), - "SliderGrab": (102, 217, 39), - "SliderGrabActive": (166, 226, 46), - "Button": ( 73, 72, 62), - "ButtonHovered": (249, 38, 114), - "ButtonActive": (198, 30, 92), - "Header": ( 73, 72, 62), - "HeaderHovered": (249, 38, 114), - "HeaderActive": (198, 30, 92), - "Separator": ( 60, 61, 52), - "SeparatorHovered": (249, 38, 114), - "SeparatorActive": (166, 226, 46), - "ResizeGrip": ( 73, 72, 62), - "ResizeGripHovered": (249, 38, 114), - "ResizeGripActive": (166, 226, 46), - "Tab": ( 73, 72, 62), - "TabHovered": (249, 38, 114), - "TabActive": (249, 38, 114), - "TabUnfocused": ( 50, 51, 44), - "TabUnfocusedActive": ( 90, 88, 76), - "DockingPreview": (249, 38, 114, 180), - "DockingEmptyBg": ( 20, 20, 18), - "Text": (248, 248, 242), - "TextDisabled": (117, 113, 94), - "TextSelectedBg": (249, 38, 114, 150), - "TableHeaderBg": ( 60, 61, 52), - "TableBorderStrong": ( 73, 72, 62), - "TableBorderLight": ( 55, 56, 48), - "TableRowBg": ( 0, 0, 0, 0), - "TableRowBgAlt": ( 50, 51, 44, 40), - "NavHighlight": (166, 226, 46), - "ModalWindowDimBg": ( 10, 10, 8, 100), - }, + "DPG Default": {}, # empty = reset to DPG built-in defaults + "10x Dark": { + # Window / frame chrome + "WindowBg": ( 34, 32, 28), + "ChildBg": ( 30, 28, 24), + "PopupBg": ( 35, 30, 20), + "Border": ( 60, 55, 50), + "BorderShadow": ( 0, 0, 0, 0), + "FrameBg": ( 45, 42, 38), + "FrameBgHovered": ( 60, 56, 50), + "FrameBgActive": ( 75, 70, 62), + # Title bars + "TitleBg": ( 40, 35, 25), + "TitleBgActive": ( 60, 45, 15), + "TitleBgCollapsed": ( 30, 27, 20), + # Menu bar + "MenuBarBg": ( 35, 30, 20), + # Scrollbar + "ScrollbarBg": ( 30, 28, 24), + "ScrollbarGrab": ( 80, 78, 72), + "ScrollbarGrabHovered": (100, 100, 92), + "ScrollbarGrabActive": (120, 118, 110), + # Check marks / radio buttons + "CheckMark": (194, 164, 74), + # Sliders + "SliderGrab": (126, 78, 14), + "SliderGrabActive": (194, 140, 30), + # Buttons + "Button": ( 83, 76, 60), + "ButtonHovered": (126, 78, 14), + "ButtonActive": (115, 90, 70), + # Headers (collapsing headers, selectables, listbox items) + "Header": ( 83, 76, 60), + "HeaderHovered": (126, 78, 14), + "HeaderActive": (115, 90, 70), + # Separator + "Separator": ( 70, 65, 55), + "SeparatorHovered": (126, 78, 14), + "SeparatorActive": (194, 164, 74), + # Resize grip + "ResizeGrip": ( 60, 55, 44), + "ResizeGripHovered": (126, 78, 14), + "ResizeGripActive": (194, 164, 74), + # Tab bar + "Tab": ( 83, 83, 70), + "TabHovered": (126, 77, 25), + "TabActive": (126, 77, 25), + "TabUnfocused": ( 60, 58, 50), + "TabUnfocusedActive": ( 90, 80, 55), + # Docking + "DockingPreview": (126, 78, 14, 180), + "DockingEmptyBg": ( 20, 20, 20), + # Text + "Text": (200, 200, 200), + "TextDisabled": (130, 130, 120), + # Input text cursor / selection + "TextSelectedBg": ( 59, 86, 142, 180), + # Plot / table lines + "TableHeaderBg": ( 55, 50, 38), + "TableBorderStrong": ( 70, 65, 55), + "TableBorderLight": ( 50, 47, 42), + "TableRowBg": ( 0, 0, 0, 0), + "TableRowBgAlt": ( 40, 38, 34, 40), + # Misc + "NavHighlight": (126, 78, 14), + "NavWindowingHighlight":(194, 164, 74, 180), + "NavWindowingDimBg": ( 20, 20, 20, 80), + "ModalWindowDimBg": ( 10, 10, 10, 100), + }, + "Nord Dark": { + "WindowBg": ( 36, 41, 49), + "ChildBg": ( 30, 34, 42), + "PopupBg": ( 36, 41, 49), + "Border": ( 59, 66, 82), + "BorderShadow": ( 0, 0, 0, 0), + "FrameBg": ( 46, 52, 64), + "FrameBgHovered": ( 59, 66, 82), + "FrameBgActive": ( 67, 76, 94), + "TitleBg": ( 36, 41, 49), + "TitleBgActive": ( 59, 66, 82), + "TitleBgCollapsed": ( 30, 34, 42), + "MenuBarBg": ( 46, 52, 64), + "ScrollbarBg": ( 30, 34, 42), + "ScrollbarGrab": ( 76, 86, 106), + "ScrollbarGrabHovered": ( 94, 129, 172), + "ScrollbarGrabActive": (129, 161, 193), + "CheckMark": (136, 192, 208), + "SliderGrab": ( 94, 129, 172), + "SliderGrabActive": (129, 161, 193), + "Button": ( 59, 66, 82), + "ButtonHovered": ( 94, 129, 172), + "ButtonActive": (129, 161, 193), + "Header": ( 59, 66, 82), + "HeaderHovered": ( 94, 129, 172), + "HeaderActive": (129, 161, 193), + "Separator": ( 59, 66, 82), + "SeparatorHovered": ( 94, 129, 172), + "SeparatorActive": (136, 192, 208), + "ResizeGrip": ( 59, 66, 82), + "ResizeGripHovered": ( 94, 129, 172), + "ResizeGripActive": (136, 192, 208), + "Tab": ( 46, 52, 64), + "TabHovered": ( 94, 129, 172), + "TabActive": ( 76, 86, 106), + "TabUnfocused": ( 36, 41, 49), + "TabUnfocusedActive": ( 59, 66, 82), + "DockingPreview": ( 94, 129, 172, 180), + "DockingEmptyBg": ( 20, 22, 28), + "Text": (216, 222, 233), + "TextDisabled": (116, 128, 150), + "TextSelectedBg": ( 94, 129, 172, 180), + "TableHeaderBg": ( 59, 66, 82), + "TableBorderStrong": ( 76, 86, 106), + "TableBorderLight": ( 59, 66, 82), + "TableRowBg": ( 0, 0, 0, 0), + "TableRowBgAlt": ( 46, 52, 64, 40), + "NavHighlight": (136, 192, 208), + "ModalWindowDimBg": ( 10, 12, 16, 100), + }, + "Monokai": { + "WindowBg": ( 39, 40, 34), + "ChildBg": ( 34, 35, 29), + "PopupBg": ( 39, 40, 34), + "Border": ( 60, 61, 52), + "BorderShadow": ( 0, 0, 0, 0), + "FrameBg": ( 50, 51, 44), + "FrameBgHovered": ( 65, 67, 56), + "FrameBgActive": ( 80, 82, 68), + "TitleBg": ( 39, 40, 34), + "TitleBgActive": ( 73, 72, 62), + "TitleBgCollapsed": ( 30, 31, 26), + "MenuBarBg": ( 50, 51, 44), + "ScrollbarBg": ( 34, 35, 29), + "ScrollbarGrab": ( 80, 80, 72), + "ScrollbarGrabHovered": (102, 217, 39), + "ScrollbarGrabActive": (166, 226, 46), + "CheckMark": (166, 226, 46), + "SliderGrab": (102, 217, 39), + "SliderGrabActive": (166, 226, 46), + "Button": ( 73, 72, 62), + "ButtonHovered": (249, 38, 114), + "ButtonActive": (198, 30, 92), + "Header": ( 73, 72, 62), + "HeaderHovered": (249, 38, 114), + "HeaderActive": (198, 30, 92), + "Separator": ( 60, 61, 52), + "SeparatorHovered": (249, 38, 114), + "SeparatorActive": (166, 226, 46), + "ResizeGrip": ( 73, 72, 62), + "ResizeGripHovered": (249, 38, 114), + "ResizeGripActive": (166, 226, 46), + "Tab": ( 73, 72, 62), + "TabHovered": (249, 38, 114), + "TabActive": (249, 38, 114), + "TabUnfocused": ( 50, 51, 44), + "TabUnfocusedActive": ( 90, 88, 76), + "DockingPreview": (249, 38, 114, 180), + "DockingEmptyBg": ( 20, 20, 18), + "Text": (248, 248, 242), + "TextDisabled": (117, 113, 94), + "TextSelectedBg": (249, 38, 114, 150), + "TableHeaderBg": ( 60, 61, 52), + "TableBorderStrong": ( 73, 72, 62), + "TableBorderLight": ( 55, 56, 48), + "TableRowBg": ( 0, 0, 0, 0), + "TableRowBgAlt": ( 50, 51, 44, 40), + "NavHighlight": (166, 226, 46), + "ModalWindowDimBg": ( 10, 10, 8, 100), + }, } PALETTE_NAMES: list[str] = list(_PALETTES.keys()) @@ -210,56 +206,56 @@ PALETTE_NAMES: list[str] = list(_PALETTES.keys()) # Maps our friendly name -> dpg constant name _COL_MAP: dict[str, str] = { - "Text": "mvThemeCol_Text", - "TextDisabled": "mvThemeCol_TextDisabled", - "WindowBg": "mvThemeCol_WindowBg", - "ChildBg": "mvThemeCol_ChildBg", - "PopupBg": "mvThemeCol_PopupBg", - "Border": "mvThemeCol_Border", - "BorderShadow": "mvThemeCol_BorderShadow", - "FrameBg": "mvThemeCol_FrameBg", - "FrameBgHovered": "mvThemeCol_FrameBgHovered", - "FrameBgActive": "mvThemeCol_FrameBgActive", - "TitleBg": "mvThemeCol_TitleBg", - "TitleBgActive": "mvThemeCol_TitleBgActive", - "TitleBgCollapsed": "mvThemeCol_TitleBgCollapsed", - "MenuBarBg": "mvThemeCol_MenuBarBg", - "ScrollbarBg": "mvThemeCol_ScrollbarBg", - "ScrollbarGrab": "mvThemeCol_ScrollbarGrab", - "ScrollbarGrabHovered": "mvThemeCol_ScrollbarGrabHovered", - "ScrollbarGrabActive": "mvThemeCol_ScrollbarGrabActive", - "CheckMark": "mvThemeCol_CheckMark", - "SliderGrab": "mvThemeCol_SliderGrab", - "SliderGrabActive": "mvThemeCol_SliderGrabActive", - "Button": "mvThemeCol_Button", - "ButtonHovered": "mvThemeCol_ButtonHovered", - "ButtonActive": "mvThemeCol_ButtonActive", - "Header": "mvThemeCol_Header", - "HeaderHovered": "mvThemeCol_HeaderHovered", - "HeaderActive": "mvThemeCol_HeaderActive", - "Separator": "mvThemeCol_Separator", - "SeparatorHovered": "mvThemeCol_SeparatorHovered", - "SeparatorActive": "mvThemeCol_SeparatorActive", - "ResizeGrip": "mvThemeCol_ResizeGrip", - "ResizeGripHovered": "mvThemeCol_ResizeGripHovered", - "ResizeGripActive": "mvThemeCol_ResizeGripActive", - "Tab": "mvThemeCol_Tab", - "TabHovered": "mvThemeCol_TabHovered", - "TabActive": "mvThemeCol_TabActive", - "TabUnfocused": "mvThemeCol_TabUnfocused", - "TabUnfocusedActive": "mvThemeCol_TabUnfocusedActive", - "DockingPreview": "mvThemeCol_DockingPreview", - "DockingEmptyBg": "mvThemeCol_DockingEmptyBg", - "TextSelectedBg": "mvThemeCol_TextSelectedBg", - "TableHeaderBg": "mvThemeCol_TableHeaderBg", - "TableBorderStrong": "mvThemeCol_TableBorderStrong", - "TableBorderLight": "mvThemeCol_TableBorderLight", - "TableRowBg": "mvThemeCol_TableRowBg", - "TableRowBgAlt": "mvThemeCol_TableRowBgAlt", - "NavHighlight": "mvThemeCol_NavHighlight", - "NavWindowingHighlight": "mvThemeCol_NavWindowingHighlight", - "NavWindowingDimBg": "mvThemeCol_NavWindowingDimBg", - "ModalWindowDimBg": "mvThemeCol_ModalWindowDimBg", + "Text": "mvThemeCol_Text", + "TextDisabled": "mvThemeCol_TextDisabled", + "WindowBg": "mvThemeCol_WindowBg", + "ChildBg": "mvThemeCol_ChildBg", + "PopupBg": "mvThemeCol_PopupBg", + "Border": "mvThemeCol_Border", + "BorderShadow": "mvThemeCol_BorderShadow", + "FrameBg": "mvThemeCol_FrameBg", + "FrameBgHovered": "mvThemeCol_FrameBgHovered", + "FrameBgActive": "mvThemeCol_FrameBgActive", + "TitleBg": "mvThemeCol_TitleBg", + "TitleBgActive": "mvThemeCol_TitleBgActive", + "TitleBgCollapsed": "mvThemeCol_TitleBgCollapsed", + "MenuBarBg": "mvThemeCol_MenuBarBg", + "ScrollbarBg": "mvThemeCol_ScrollbarBg", + "ScrollbarGrab": "mvThemeCol_ScrollbarGrab", + "ScrollbarGrabHovered": "mvThemeCol_ScrollbarGrabHovered", + "ScrollbarGrabActive": "mvThemeCol_ScrollbarGrabActive", + "CheckMark": "mvThemeCol_CheckMark", + "SliderGrab": "mvThemeCol_SliderGrab", + "SliderGrabActive": "mvThemeCol_SliderGrabActive", + "Button": "mvThemeCol_Button", + "ButtonHovered": "mvThemeCol_ButtonHovered", + "ButtonActive": "mvThemeCol_ButtonActive", + "Header": "mvThemeCol_Header", + "HeaderHovered": "mvThemeCol_HeaderHovered", + "HeaderActive": "mvThemeCol_HeaderActive", + "Separator": "mvThemeCol_Separator", + "SeparatorHovered": "mvThemeCol_SeparatorHovered", + "SeparatorActive": "mvThemeCol_SeparatorActive", + "ResizeGrip": "mvThemeCol_ResizeGrip", + "ResizeGripHovered": "mvThemeCol_ResizeGripHovered", + "ResizeGripActive": "mvThemeCol_ResizeGripActive", + "Tab": "mvThemeCol_Tab", + "TabHovered": "mvThemeCol_TabHovered", + "TabActive": "mvThemeCol_TabActive", + "TabUnfocused": "mvThemeCol_TabUnfocused", + "TabUnfocusedActive": "mvThemeCol_TabUnfocusedActive", + "DockingPreview": "mvThemeCol_DockingPreview", + "DockingEmptyBg": "mvThemeCol_DockingEmptyBg", + "TextSelectedBg": "mvThemeCol_TextSelectedBg", + "TableHeaderBg": "mvThemeCol_TableHeaderBg", + "TableBorderStrong": "mvThemeCol_TableBorderStrong", + "TableBorderLight": "mvThemeCol_TableBorderLight", + "TableRowBg": "mvThemeCol_TableRowBg", + "TableRowBgAlt": "mvThemeCol_TableRowBgAlt", + "NavHighlight": "mvThemeCol_NavHighlight", + "NavWindowingHighlight": "mvThemeCol_NavWindowingHighlight", + "NavWindowingDimBg": "mvThemeCol_NavWindowingDimBg", + "ModalWindowDimBg": "mvThemeCol_ModalWindowDimBg", } # ------------------------------------------------------------------ state @@ -272,144 +268,122 @@ _current_font_path: str = "" _current_font_size: float = 14.0 _current_scale: float = 1.0 - # ------------------------------------------------------------------ public API def get_palette_names() -> list[str]: - return list(_PALETTES.keys()) - + return list(_PALETTES.keys()) def get_current_palette() -> str: - return _current_palette - + return _current_palette def get_current_font_path() -> str: - return _current_font_path - + return _current_font_path def get_current_font_size() -> float: - return _current_font_size - + return _current_font_size def get_current_scale() -> float: - return _current_scale - + return _current_scale def get_palette_colours(name: str) -> dict: - """Return a copy of the colour dict for the named palette.""" - return dict(_PALETTES.get(name, {})) - + """Return a copy of the colour dict for the named palette.""" + return dict(_PALETTES.get(name, {})) def apply(palette_name: str, overrides: dict | None = None): - """ + """ Build a global DPG theme from the named palette plus optional per-colour overrides, and bind it as the default theme. overrides: {colour_key: (R,G,B) or (R,G,B,A)} — merged on top of palette. """ - global _current_theme_tag, _current_palette - - _current_palette = palette_name - colours = dict(_PALETTES.get(palette_name, {})) - if overrides: - colours.update(overrides) - - # Delete the old theme if one exists - if _current_theme_tag is not None: - try: - dpg.delete_item(_current_theme_tag) - except Exception: - pass - _current_theme_tag = None - - if palette_name == "DPG Default" and not overrides: - # Bind an empty theme to reset to DPG defaults - with dpg.theme() as t: - with dpg.theme_component(dpg.mvAll): - pass - dpg.bind_theme(t) - _current_theme_tag = t - return - - with dpg.theme() as t: - with dpg.theme_component(dpg.mvAll): - for name, colour in colours.items(): - const_name = _COL_MAP.get(name) - if const_name is None: - continue - const = getattr(dpg, const_name, None) - if const is None: - continue - # Ensure 4-tuple - if len(colour) == 3: - colour = (*colour, 255) - dpg.add_theme_color(const, colour) - - dpg.bind_theme(t) - _current_theme_tag = t - + global _current_theme_tag, _current_palette + _current_palette = palette_name + colours = dict(_PALETTES.get(palette_name, {})) + if overrides: + colours.update(overrides) + # Delete the old theme if one exists + if _current_theme_tag is not None: + try: + dpg.delete_item(_current_theme_tag) + except Exception: + pass + _current_theme_tag = None + if palette_name == "DPG Default" and not overrides: + # Bind an empty theme to reset to DPG defaults + with dpg.theme() as t: + with dpg.theme_component(dpg.mvAll): + pass + dpg.bind_theme(t) + _current_theme_tag = t + return + with dpg.theme() as t: + with dpg.theme_component(dpg.mvAll): + for name, colour in colours.items(): + const_name = _COL_MAP.get(name) + if const_name is None: + continue + const = getattr(dpg, const_name, None) + if const is None: + continue + # Ensure 4-tuple + if len(colour) == 3: + colour = (*colour, 255) + dpg.add_theme_color(const, colour) + dpg.bind_theme(t) + _current_theme_tag = t def apply_font(font_path: str, size: float = 14.0): - """ + """ Load the TTF at font_path at the given point size and bind it globally. Safe to call multiple times. Uses a single persistent font_registry; only the font *item* tag is tracked. Passing an empty path or a missing file resets to the DPG built-in font. """ - global _current_font_tag, _current_font_path, _current_font_size, _font_registry_tag - - _current_font_path = font_path - _current_font_size = size - - if not font_path or not Path(font_path).exists(): - # Reset to default built-in font - dpg.bind_font(0) - _current_font_tag = None - return - - # Create the registry once - if _font_registry_tag is None or not dpg.does_item_exist(_font_registry_tag): - with dpg.font_registry() as reg: - _font_registry_tag = reg - - # Delete previous custom font item only (not the registry) - if _current_font_tag is not None: - try: - dpg.delete_item(_current_font_tag) - except Exception: - pass - _current_font_tag = None - - font = dpg.add_font(font_path, size, parent=_font_registry_tag) - _current_font_tag = font - dpg.bind_font(font) - + global _current_font_tag, _current_font_path, _current_font_size, _font_registry_tag + _current_font_path = font_path + _current_font_size = size + if not font_path or not Path(font_path).exists(): + # Reset to default built-in font + dpg.bind_font(0) + _current_font_tag = None + return + # Create the registry once + if _font_registry_tag is None or not dpg.does_item_exist(_font_registry_tag): + with dpg.font_registry() as reg: + _font_registry_tag = reg + # Delete previous custom font item only (not the registry) + if _current_font_tag is not None: + try: + dpg.delete_item(_current_font_tag) + except Exception: + pass + _current_font_tag = None + font = dpg.add_font(font_path, size, parent=_font_registry_tag) + _current_font_tag = font + dpg.bind_font(font) def set_scale(factor: float): - """Set the global Dear PyGui font/UI scale factor.""" - global _current_scale - _current_scale = factor - dpg.set_global_font_scale(factor) - + """Set the global Dear PyGui font/UI scale factor.""" + global _current_scale + _current_scale = factor + dpg.set_global_font_scale(factor) def save_to_config(config: dict): - """Persist theme settings into the config dict under [theme].""" - config.setdefault("theme", {}) - config["theme"]["palette"] = _current_palette - config["theme"]["font_path"] = _current_font_path - config["theme"]["font_size"] = _current_font_size - config["theme"]["scale"] = _current_scale - + """Persist theme settings into the config dict under [theme].""" + config.setdefault("theme", {}) + config["theme"]["palette"] = _current_palette + config["theme"]["font_path"] = _current_font_path + config["theme"]["font_size"] = _current_font_size + config["theme"]["scale"] = _current_scale def load_from_config(config: dict): - """Read [theme] from config and apply everything.""" - t = config.get("theme", {}) - palette = t.get("palette", "DPG Default") - font_path = t.get("font_path", "") - font_size = float(t.get("font_size", 14.0)) - scale = float(t.get("scale", 1.0)) - - apply(palette) - if font_path: - apply_font(font_path, font_size) - set_scale(scale) + """Read [theme] from config and apply everything.""" + t = config.get("theme", {}) + palette = t.get("palette", "DPG Default") + font_path = t.get("font_path", "") + font_size = float(t.get("font_size", 14.0)) + scale = float(t.get("scale", 1.0)) + apply(palette) + if font_path: + apply_font(font_path, font_size) + set_scale(scale)