From 1d9c5a9b07d74245524825f1e676123570feb75d Mon Sep 17 00:00:00 2001 From: Ed_ Date: Sat, 21 Feb 2026 19:01:00 -0500 Subject: [PATCH] theme! --- config.toml | 56 ++++++- dpg_layout.ini | 303 ++++++++++++++++++++++++++---------- gui.py | 94 +++++++++++ theme.py | 415 +++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 787 insertions(+), 81 deletions(-) create mode 100644 theme.py diff --git a/config.toml b/config.toml index 6669f04..4848420 100644 --- a/config.toml +++ b/config.toml @@ -15,6 +15,7 @@ paths = [ "C:/projects/manual_slop/shell_runner.py", "C:/projects/manual_slop/session_logger.py", "C:/projects/manual_slop/file_cache.py", + "C:/projects/manual_slop/theme.py", ] [screenshots] @@ -23,9 +24,62 @@ paths = [] [discussion] history = [ - "User:\nAdd support for properly handling files that we have in config with anthropics api (your context will have documentation reguarding it). This will allow us to free up the main markdown file were sending as a message.\nWe also want to make sure the messaging of that giant markdown is chunked as anthropic does have an upper limit per messages.\nAI:\nGood. Now the big rewrite of `ai_client.py`. The strategy:\n\n- Add a new `_send_anthropic_files` path that uses `file_cache.get_file_id()` to upload files individually as `document`/`image` content blocks, keeping the context message lean\n- The fallback (and also for oversized content) uses chunked text blocks — split the markdown into ≤180k-char chunks (well under the ~200k token limit with headroom for overhead), each sent as a separate `text` content block with `cache_control` on the last one\n- `send()` now accepts an optional `file_items` parameter from aggregate\n- `reset_session()` also resets the file_cache client\nUser:\ncontinue. Also got this error when trying to run latest version of this program. However I can use an old version for now.\nAi:\nNow fix `gui.py` — patch the three 2-tuple unpack sites and thread `file_items` through to `send()`:\n[User]\ncontinue. My bad the error in comms was shown int he screenshot I linked. in the md context. Make sure to update MainContext.md when done.\nAI:\nfinished without sending a response...\nUser:\nSeems to be working...! \nMake a richer discussioon history editor, with structual support for the user/ai message nodes and buttons in the reponse and message panels for adding their entries to it on click.\nAI:\nThe existing format already uses `[User]\\n...` and `[AI]\\n...` prefixes. The new structured editor will parse those naturally and write them back in the same format. Now let me implement the full changes:\nUser:\nYou had failres in your last few called scripts. Git diff shows now changes to python files.\nI'll add the comms and tool logs to your context. \nWe should remove the script content from the tool log and just save the script name so that it doesn't bloat it. \nSame with omitting full message sent in the comms log, very expensive to send to you or read manually.\nVendor API:\nERROR: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'A maximum of 4 blocks with cache_control may be provided. Found 5.'}, 'request_id': 'req_011CYMxgMXvcZUVFNyFz1Lx8'}\nUser:\nI'm not able to send a message for a while now...\n(Message finally sent)\nAI:\nCompleted without a response message showing up.\nUser:\nRestarts custom prompting program.\nUser:\nOk lets try this again...\nLets do the discussion history improvmeents. I want a sort of visual editor where each entry is its own block. \nThere should be a role field and a content filed and I should have buttons for appending the current response or current message to it\n. I should be able to insert or remove anywhere as well.\nVendor API:\nERROR: Error code: 500 - {'type': 'error', 'error': {'type': 'api_error', 'message': 'Internal server error'}, 'request_id': 'req_011CYMyjgyfDn2b6JnPD6rvJ'}\nUser:\ncontinue...\nVendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYMyjXYsf951nBgTvQmkE'}, 'request_id': 'req_011CYMysoSaH2xPxVtZurjSc'}\nUser:\nreverting to older editor...\nOk I'm back on an older prompting editor. Lets get this working again... with latest..\nAI:\nGood. Now I'll rewrite the Discussion History panel in `gui.py`. The changes are:\n\n1. Add `_parse_history_entries()` — converts the raw TOML string array into `[{role, content}]` dicts\n2. Add `self.disc_entries` list of dicts to `App.__init__`\n3. Replace `_build_ui`'s Discussion window with a structured block editor inside a scrollable child window\n4. Add `_rebuild_disc_list()` — full redraw of the structured editor\n5. Add `_make_disc_*` callbacks for insert/remove/role-change/content-change\n6. Add `cb_append_message_to_history()` and `cb_append_response_to_history()`\n7. Add **→ History** buttons to Message and Response panels\n8. Update `_flush_to_config()` to serialise `disc_entries` back to the string array format\n9. Remove the old `cb_add_excerpt` / `cb_clear_discussion` / `cb_save_discussion` callbacks (replaced)\nUser:\nsounds good, continue!\nVendor API:\nERROR: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.10: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01BjaPJPVFoNnNoRCLvPA8qF. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}, 'request_id': 'req_011CYMzHG47TKTPp7iLcE5NP'}\nUser:\nreverting to an even older version..\nUser:\nOk lets see if we can get it to work with this version and get everything fixed on latest...\nAI:\nThe cache_control logic looks correct already (system=1, tools=1, context_last=1 = 3 total, under the limit of 4). The error seen previously was likely from an older version. Good.\n\nNow let me fix the **comms log** (omit full message from OUT/request) and **tool log** (omit script body from `session_logger`):\nAI:\nNo response\nUser:\nOnly see changes done to gui.py...\nrestarting...\nUser:\nIs everything fixed? not sure?\nAI:\nNow I have a clear picture. The files on disk match the context documents closely. The outstanding tasks from the discussion history are:\n\n1. **`session_logger.py`** — omit the script body from the tool-call log (just save script name/path, not contents)\n2. **`ai_client.py`** — omit full message from OUT/request comms log (already mostly done — Gemini still uses `summary` key; Anthropic uses `message` with a summary. The Gemini one sends `summary` but the renderer looks for `message`. Fix that too.)\n3. **`gui.py`** — Discussion History structured block editor (the big one that kept failing)\n\nLet me tackle all three cleanly now.\n\n**First: `session_logger.py`** — remove script body from toolcalls log, just reference the `.ps1` file:\nVendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYN4ecyVxDg7ujLUYWeD3'}, 'request_id': 'req_011CYN4kpavNrA5KYXnMqKLq'}\nUser (Background):\nConstraining the discussion history to one entry...\nVendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYN4ecyVxDg7ujLUYWeD3'}, 'request_id': 'req_011CYN4rxjETKA5B9ACMkm2T'}\nUser:\nSigh, here we go again.\nAI - Observed by user:\nDoes many actions (recorded in screenshot but no direct response from them).\nVendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYN4wQpEzM4W4KFA48HDz'}, 'request_id': 'req_011CYN59c31BxJsgDZ6Rzqby'}\nUser (Background):\nOk definitely not...\nUser:\nAttempt to fix these error with files were getting... maybe this file usage thing with anthropic was a bad idea...\nVendor API:\nERROR: Error code: 500 - {'type': 'error', 'error': {'type': 'api_error', 'message': 'Internal server error'}, 'request_id': 'req_011CYN5PqDQLs5d1B9QS8inZ'}\nUser (Background):\nWeird it got pretty far and gutted the file_cache.py file. Not sure why that error popped up for the service...\nrestarting prompting editor to latest instance...\nVendor API:\nERROR: send() takes from 2 to 3 positional arguments but 4 were given\nUser:\nok... maybe I cant use latest...\nI applied a fix to gui.py line 658 was using the files arg with the send call thats no longer used.\nAI:\nThe file has `\\r\\n` line endings mixed in. I'll write the replacement using a Python one-liner to avoid PowerShell line-ending issues:", + "User:\nAdd support for properly handling files that we have in config with anthropics api (your context will have documentation reguarding it). This will allow us to free up the main markdown file were sending as a message.\nWe also want to make sure the messaging of that giant markdown is chunked as anthropic does have an upper limit per messages.", + "AI:\nGood. Now the big rewrite of `ai_client.py`. The strategy:\n\n- Add a new `_send_anthropic_files` path that uses `file_cache.get_file_id()` to upload files individually as `document`/`image` content blocks, keeping the context message lean\n- The fallback (and also for oversized content) uses chunked text blocks — split the markdown into ≤180k-char chunks (well under the ~200k token limit with headroom for overhead), each sent as a separate `text` content block with `cache_control` on the last one\n- `send()` now accepts an optional `file_items` parameter from aggregate\n- `reset_session()` also resets the file_cache client", + "User:\ncontinue. Also got this error when trying to run latest version of this program. However I can use an old version for now.", + "AI:\nNow fix `gui.py` — patch the three 2-tuple unpack sites and thread `file_items` through to `send()`:", + "User:\ncontinue. My bad the error in comms was shown int he screenshot I linked. in the md context. Make sure to update MainContext.md when done.", + "AI:\nfinished without sending a response...", + "User:\nSeems to be working...! \nMake a richer discussioon history editor, with structual support for the user/ai message nodes and buttons in the reponse and message panels for adding their entries to it on click.", + "AI:\nThe existing format already uses `[User]\\n...` and `[AI]\\n...` prefixes. The new structured editor will parse those naturally and write them back in the same format. Now let me implement the full changes:", + "User:\nYou had failres in your last few called scripts. Git diff shows now changes to python files.\nI'll add the comms and tool logs to your context. \nWe should remove the script content from the tool log and just save the script name so that it doesn't bloat it. \nSame with omitting full message sent in the comms log, very expensive to send to you or read manually.", + "Vendor API:\nERROR: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'A maximum of 4 blocks with cache_control may be provided. Found 5.'}, 'request_id': 'req_011CYMxgMXvcZUVFNyFz1Lx8'}", + "User:\nI'm not able to send a message for a while now...\n(Message finally sent)", + "AI:\nCompleted without a response message showing up.", + "User:\nRestarts custom prompting program.", + "User:\nOk lets try this again...\nLets do the discussion history improvmeents. I want a sort of visual editor where each entry is its own block. \nThere should be a role field and a content filed and I should have buttons for appending the current response or current message to it\n. I should be able to insert or remove anywhere as well.", + "Vendor API:\nERROR: Error code: 500 - {'type': 'error', 'error': {'type': 'api_error', 'message': 'Internal server error'}, 'request_id': 'req_011CYMyjgyfDn2b6JnPD6rvJ'}", + "User:\ncontinue...", + "Vendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYMyjXYsf951nBgTvQmkE'}, 'request_id': 'req_011CYMysoSaH2xPxVtZurjSc'}", + "User:\nreverting to older editor...\nOk I'm back on an older prompting editor. Lets get this working again... with latest..", + "AI:\nGood. Now I'll rewrite the Discussion History panel in `gui.py`. The changes are:\n\n1. Add `_parse_history_entries()` — converts the raw TOML string array into `[{role, content}]` dicts\n2. Add `self.disc_entries` list of dicts to `App.__init__`\n3. Replace `_build_ui`'s Discussion window with a structured block editor inside a scrollable child window\n4. Add `_rebuild_disc_list()` — full redraw of the structured editor\n5. Add `_make_disc_*` callbacks for insert/remove/role-change/content-change\n6. Add `cb_append_message_to_history()` and `cb_append_response_to_history()`\n7. Add **→ History** buttons to Message and Response panels\n8. Update `_flush_to_config()` to serialise `disc_entries` back to the string array format\n9. Remove the old `cb_add_excerpt` / `cb_clear_discussion` / `cb_save_discussion` callbacks (replaced)", + "User:\nsounds good, continue!", + "Vendor API:\nERROR: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'messages.10: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01BjaPJPVFoNnNoRCLvPA8qF. Each `tool_use` block must have a corresponding `tool_result` block in the next message.'}, 'request_id': 'req_011CYMzHG47TKTPp7iLcE5NP'}", + "User:\nreverting to an even older version..", + "User:\nOk lets see if we can get it to work with this version and get everything fixed on latest...", + "AI:\nThe cache_control logic looks correct already (system=1, tools=1, context_last=1 = 3 total, under the limit of 4). The error seen previously was likely from an older version. Good.\n\nNow let me fix the **comms log** (omit full message from OUT/request) and **tool log** (omit script body from `session_logger`):", + "AI:\nNo response", + "User:\nOnly see changes done to gui.py...\nrestarting...", + "User:\nIs everything fixed? not sure?", + "AI:\nNow I have a clear picture. The files on disk match the context documents closely. The outstanding tasks from the discussion history are:\n\n1. **`session_logger.py`** — omit the script body from the tool-call log (just save script name/path, not contents)\n2. **`ai_client.py`** — omit full message from OUT/request comms log (already mostly done — Gemini still uses `summary` key; Anthropic uses `message` with a summary. The Gemini one sends `summary` but the renderer looks for `message`. Fix that too.)\n3. **`gui.py`** — Discussion History structured block editor (the big one that kept failing)\n\nLet me tackle all three cleanly now.\n\n**First: `session_logger.py`** — remove script body from toolcalls log, just reference the `.ps1` file:", + "Vendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYN4ecyVxDg7ujLUYWeD3'}, 'request_id': 'req_011CYN4kpavNrA5KYXnMqKLq'}\nUser (Background):\nConstraining the discussion history to one entry...", + "Vendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYN4ecyVxDg7ujLUYWeD3'}, 'request_id': 'req_011CYN4rxjETKA5B9ACMkm2T'}", + "User:\nSigh, here we go again.\nAI - Observed by user:\nDoes many actions (recorded in screenshot but no direct response from them).", + "Vendor API:\nERROR: Error code: 404 - {'type': 'error', 'error': {'type': 'not_found_error', 'message': 'File not found: file_011CYN4wQpEzM4W4KFA48HDz'}, 'request_id': 'req_011CYN59c31BxJsgDZ6Rzqby'}\nUser (Background):\nOk definitely not...", + "User:\nAttempt to fix these error with files were getting... maybe this file usage thing with anthropic was a bad idea...", + "Vendor API:\nERROR: Error code: 500 - {'type': 'error', 'error': {'type': 'api_error', 'message': 'Internal server error'}, 'request_id': 'req_011CYN5PqDQLs5d1B9QS8inZ'}\nUser (Background):\nWeird it got pretty far and gutted the file_cache.py file. Not sure why that error popped up for the service...\nrestarting prompting editor to latest instance...", + "Vendor API:\nERROR: send() takes from 2 to 3 positional arguments but 4 were given", + "User:\nok... maybe I cant use latest...\nI applied a fix to gui.py line 658 was using the files arg with the send call thats no longer used.", + "AI:\nThe file has `\\r\\n` line endings mixed in. I'll write the replacement using a Python one-liner to avoid PowerShell line-ending issues:", + "User:\nSure, if you want also enforce LF if you want on all files. I manually attempted so in vscode.", + "AI:\nDoes work... (Noted by user)\n- Reponse:\nNow clean up the temp patch scripts and do a quick sanity check:", + "User:\n", + "User:\nLets add some theming to this. Add options to change the ui dpi scale and the font used, also add the ability to change the palette I guess. We can reference my 10x editor settings for inspiration:\n```\n[Omitted as its already been utilized]\n\n```", + "AI:\nGood. Now I'll patch `gui.py` — adding the Theme window and wiring it up. I'll make targeted insertions rather than a full rewrite:\n\n1. Add `import theme` at the top\n2. Add `_build_theme_window()` and its callbacks to `App`\n3. Call `theme.load_from_config()` at startup (in `run()`)\n4. Call `theme.save_to_config()` in `_flush_to_config()`\n5. Add `win_theme` to `_build_ui()`", + "User:\nSure lets patch gui.py.", + "AI:\nGood. Now I have a clear picture. I'll do the full patch in one Python script to avoid PowerShell string escaping issues:\n(Finished the job)", + "User:\nOk so the implementation works but when switching fonts it tends to crash (FiraCode, MapleMono, etc).", + "AI:\nThe em-dash in the docstring differs. Let me match by a more robust regex:", + "User:\nOk it seems fixed now... (themeing for fonts, nice job). \nNow lets do those adjustments to discussion history:\nIt'd also be nice if we can collapse the discussion entries indvidual and with a collapse/expand all button.\nAlso there is no way right now for me to define new roles for the discussion other than by editing the source.\nShould at least be configurable frm the config.toml...", + "AI:\nGood, I have a clear picture. Here's the plan:\n\n**Changes needed:**\n1. **`config.toml`** — add `[discussion] roles = [...]` so roles are configurable\n2. **`gui.py`** — 4 targeted edits:\n - `DISC_ROLES` becomes a module-level fallback; the real list comes from config at load time and lives on `App`\n - `_parse_history_entries()` takes `roles` as a parameter\n - `_rebuild_disc_list()` — add collapse/expand per entry (using a `bool` `collapsed` flag in each dict), Collapse All / Expand All buttons in the toolbar\n - Add a \"Roles\" sub-section in the Discussion window for adding/removing role names from the config list, and wire it into `_flush_to_config()`\n\nI'll do this as a Python patch script to avoid PowerShell string escaping issues:", ] [ai] provider = "anthropic" model = "claude-sonnet-4-6" + +[theme] +palette = "10x Dark" +font_path = "C:/Users/Ed/AppData/Local/uv/cache/archive-v0/WSthkYsQ82b_ywV6DkiaJ/pygame_gui/data/FiraCode-Regular.ttf" +font_size = 18.0 +scale = 1.25 diff --git a/dpg_layout.ini b/dpg_layout.ini index a184d42..2f18d79 100644 --- a/dpg_layout.ini +++ b/dpg_layout.ini @@ -10,13 +10,13 @@ Collapsed=0 [Window][###22] Pos=0,0 -Size=599,652 +Size=374,239 Collapsed=0 -DockId=0x00000005,0 +DockId=0x0000002D,0 [Window][###30] Pos=0,654 -Size=599,793 +Size=374,696 Collapsed=0 DockId=0x00000027,0 @@ -39,10 +39,10 @@ Collapsed=0 DockId=0x0000000D,0 [Window][###105] -Pos=378,494 -Size=829,1643 +Pos=1853,0 +Size=890,602 Collapsed=0 -DockId=0x00000025,0 +DockId=0x0000002A,0 [Window][###103] Pos=1613,1330 @@ -103,7 +103,7 @@ DockId=0x00000014,0 Pos=2531,0 Size=1309,1690 Collapsed=0 -DockId=0x00000025,0 +DockId=0x00000029,0 [Window][###106] Pos=601,0 @@ -112,10 +112,10 @@ Collapsed=0 DockId=0x00000012,0 [Window][###100] -Pos=2687,0 -Size=1153,1412 +Pos=2687,1049 +Size=1153,1088 Collapsed=0 -DockId=0x00000024,0 +DockId=0x0000002C,0 [Window][###133] Pos=1306,785 @@ -143,10 +143,10 @@ Size=700,440 Collapsed=0 [Window][###118] -Pos=551,0 -Size=1060,2137 +Pos=376,0 +Size=942,2137 Collapsed=0 -DockId=0x00000025,0 +DockId=0x00000012,2 [Window][###78] Pos=0,1422 @@ -164,7 +164,7 @@ DockId=0x00000015,0 Pos=551,0 Size=1060,2137 Collapsed=0 -DockId=0x00000025,1 +DockId=0x00000029,1 [Window][###110] Pos=2438,0 @@ -173,10 +173,10 @@ Collapsed=0 DockId=0x00000016,0 [Window][###112] -Pos=601,0 -Size=922,2137 +Pos=376,0 +Size=942,2137 Collapsed=0 -DockId=0x00000012,2 +DockId=0x00000012,1 [Window][###145] Pos=1578,868 @@ -209,28 +209,28 @@ Size=700,440 Collapsed=0 [Window][###115] -Pos=601,0 -Size=922,2137 +Pos=351,0 +Size=645,1548 Collapsed=0 DockId=0x00000012,0 [Window][###75] Pos=0,1352 -Size=549,785 +Size=374,785 Collapsed=0 DockId=0x00000022,0 [Window][###85] -Pos=1525,0 -Size=1160,1412 +Pos=1320,0 +Size=1365,2137 Collapsed=0 -DockId=0x00000025,0 +DockId=0x00000029,0 [Window][###92] -Pos=601,0 -Size=922,2137 +Pos=376,0 +Size=942,2137 Collapsed=0 -DockId=0x00000012,2 +DockId=0x00000012,0 [Window][###107] Pos=1525,1414 @@ -239,10 +239,10 @@ Collapsed=0 DockId=0x0000001A,0 [Window][###109] -Pos=601,0 -Size=922,2137 +Pos=351,0 +Size=645,1548 Collapsed=0 -DockId=0x00000012,1 +DockId=0x00000012,2 [Window][###142] Pos=1578,868 @@ -291,26 +291,26 @@ Collapsed=0 DockId=0x00000026,0 [Window][###72] -Pos=0,1449 -Size=599,688 +Pos=0,1050 +Size=349,498 Collapsed=0 DockId=0x00000028,0 [Window][###82] -Pos=1525,0 -Size=2315,1244 +Pos=998,0 +Size=853,602 Collapsed=0 -DockId=0x00000025,0 +DockId=0x00000029,0 [Window][###89] -Pos=601,0 -Size=922,2137 +Pos=351,0 +Size=645,1548 Collapsed=0 DockId=0x00000012,1 [Window][###97] -Pos=1525,1869 -Size=2315,268 +Pos=998,604 +Size=1745,944 Collapsed=0 DockId=0x00000020,0 @@ -394,46 +394,189 @@ Pos=1578,868 Size=700,440 Collapsed=0 -[Docking][Data] -DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,2137 Split=X Selected=0x40484D8F - DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=599,1161 Split=Y Selected=0xEE087978 - DockNode ID=0x00000005 Parent=0x00000003 SizeRef=235,354 Selected=0xEE087978 - DockNode ID=0x00000006 Parent=0x00000003 SizeRef=235,805 Split=Y Selected=0x5F94F9BD - DockNode ID=0x00000009 Parent=0x00000006 SizeRef=235,453 Split=Y Selected=0x5F94F9BD - DockNode ID=0x0000001D Parent=0x00000009 SizeRef=364,766 Split=Y Selected=0x5F94F9BD - DockNode ID=0x00000021 Parent=0x0000001D SizeRef=549,696 Split=Y Selected=0x5F94F9BD - DockNode ID=0x00000027 Parent=0x00000021 SizeRef=549,793 Selected=0x5F94F9BD - DockNode ID=0x00000028 Parent=0x00000021 SizeRef=549,688 Selected=0xBEC5E8CB - DockNode ID=0x00000022 Parent=0x0000001D SizeRef=549,785 Selected=0x0CE534DB - DockNode ID=0x0000001E Parent=0x00000009 SizeRef=364,715 Selected=0xF475F06A - DockNode ID=0x0000000A Parent=0x00000006 SizeRef=235,350 Selected=0x80199DAE - DockNode ID=0x00000004 Parent=0x7C6B3D9B SizeRef=3239,1161 Split=X - DockNode ID=0x00000001 Parent=0x00000004 SizeRef=1060,1161 Split=Y Selected=0x40484D8F - DockNode ID=0x00000007 Parent=0x00000001 SizeRef=595,492 Selected=0xBA13FCDE - DockNode ID=0x00000008 Parent=0x00000001 SizeRef=595,1643 Split=X Selected=0x40484D8F - DockNode ID=0x0000000F Parent=0x00000008 SizeRef=922,2137 Split=Y Selected=0x07E8375F - DockNode ID=0x00000011 Parent=0x0000000F SizeRef=835,425 Selected=0x72F373AE - DockNode ID=0x00000012 Parent=0x0000000F SizeRef=835,1710 Selected=0x3934423A - DockNode ID=0x00000010 Parent=0x00000008 SizeRef=2315,2137 Split=Y Selected=0xCE7F911A - DockNode ID=0x00000013 Parent=0x00000010 SizeRef=1967,1690 Split=X Selected=0xCE7F911A - DockNode ID=0x00000017 Parent=0x00000013 SizeRef=1314,1749 Selected=0x4B454E0B - DockNode ID=0x00000018 Parent=0x00000013 SizeRef=1309,1749 Split=Y Selected=0x88A8C2FF - DockNode ID=0x00000019 Parent=0x00000018 SizeRef=2440,1412 Split=X Selected=0x88A8C2FF - DockNode ID=0x00000023 Parent=0x00000019 SizeRef=1160,737 Split=Y Selected=0x4F935A1E - DockNode ID=0x0000001F Parent=0x00000023 SizeRef=2315,1867 Split=Y Selected=0x4F935A1E - DockNode ID=0x00000025 Parent=0x0000001F SizeRef=2315,1244 CentralNode=1 Selected=0x4F935A1E - DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,621 Selected=0x7D28643F - DockNode ID=0x00000020 Parent=0x00000023 SizeRef=2315,268 Selected=0x4C2F06CB - DockNode ID=0x00000024 Parent=0x00000019 SizeRef=1153,737 Selected=0x88A8C2FF - DockNode ID=0x0000001A Parent=0x00000018 SizeRef=2440,723 Selected=0x3A881EEF - DockNode ID=0x00000014 Parent=0x00000010 SizeRef=1967,445 Selected=0xC36FF36B - DockNode ID=0x00000002 Parent=0x00000004 SizeRef=2227,1161 Split=X Selected=0x714F2F7B - DockNode ID=0x0000000B Parent=0x00000002 SizeRef=968,1161 Selected=0xC915D9DA - DockNode ID=0x0000000C Parent=0x00000002 SizeRef=1661,1161 Split=Y Selected=0x714F2F7B - DockNode ID=0x0000000D Parent=0x0000000C SizeRef=396,342 Selected=0x714F2F7B - DockNode ID=0x0000000E Parent=0x0000000C SizeRef=396,817 Split=Y Selected=0xCF08B82F - DockNode ID=0x0000001B Parent=0x0000000E SizeRef=2104,1328 Split=X Selected=0x43F4115A - DockNode ID=0x00000015 Parent=0x0000001B SizeRef=823,1328 Selected=0x052342BF - DockNode ID=0x00000016 Parent=0x0000001B SizeRef=1402,1328 Selected=0x43F4115A - DockNode ID=0x0000001C Parent=0x0000000E SizeRef=2104,807 Selected=0xCF08B82F +[Window][###1815] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###1915] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###2016] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###2121] +Pos=1029,573 +Size=1337,950 +Collapsed=0 + +[Window][###2233] +Pos=1029,573 +Size=1358,440 +Collapsed=0 + +[Window][###2927] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###3047] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###3168] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###3293] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###3455] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###4505] +Pos=726,419 +Size=1151,833 +Collapsed=0 + +[Window][###4645] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###4786] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###4934] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###5086] +Pos=1029,573 +Size=700,440 +Collapsed=0 + +[Window][###108] +Pos=2687,0 +Size=1153,1047 +Collapsed=0 +DockId=0x0000002B,0 + +[Window][###134] +Pos=0,241 +Size=374,411 +Collapsed=0 +DockId=0x0000002E,0 + +[Window][###1399] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###1499] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###1600] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###1705] +Pos=1578,868 +Size=1377,1021 +Collapsed=0 + +[Window][###1817] +Pos=1578,868 +Size=1366,1080 +Collapsed=0 + +[Window][###538] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###638] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###739] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###844] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Window][###956] +Pos=1578,868 +Size=700,440 +Collapsed=0 + +[Docking][Data] +DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,0 Size=3840,2137 Split=X Selected=0x40484D8F + DockNode ID=0x00000003 Parent=0x7C6B3D9B SizeRef=374,1161 Split=Y Selected=0xEE087978 + DockNode ID=0x00000005 Parent=0x00000003 SizeRef=235,354 Split=Y Selected=0xEE087978 + DockNode ID=0x0000002D Parent=0x00000005 SizeRef=374,239 Selected=0xEE087978 + DockNode ID=0x0000002E Parent=0x00000005 SizeRef=374,411 Selected=0xFBBC1691 + DockNode ID=0x00000006 Parent=0x00000003 SizeRef=235,805 Split=Y Selected=0x5F94F9BD + DockNode ID=0x00000009 Parent=0x00000006 SizeRef=235,453 Split=Y Selected=0x5F94F9BD + DockNode ID=0x0000001D Parent=0x00000009 SizeRef=364,766 Split=Y Selected=0x5F94F9BD + DockNode ID=0x00000021 Parent=0x0000001D SizeRef=549,696 Split=Y Selected=0x5F94F9BD + DockNode ID=0x00000027 Parent=0x00000021 SizeRef=549,793 Selected=0x5F94F9BD + DockNode ID=0x00000028 Parent=0x00000021 SizeRef=549,688 Selected=0xBEC5E8CB + DockNode ID=0x00000022 Parent=0x0000001D SizeRef=549,785 Selected=0x0CE534DB + DockNode ID=0x0000001E Parent=0x00000009 SizeRef=364,715 Selected=0xF475F06A + DockNode ID=0x0000000A Parent=0x00000006 SizeRef=235,350 Selected=0x80199DAE + DockNode ID=0x00000004 Parent=0x7C6B3D9B SizeRef=3464,1161 Split=X + DockNode ID=0x00000001 Parent=0x00000004 SizeRef=1060,1161 Split=Y Selected=0x40484D8F + DockNode ID=0x00000007 Parent=0x00000001 SizeRef=595,492 Selected=0xBA13FCDE + DockNode ID=0x00000008 Parent=0x00000001 SizeRef=595,1643 Split=X Selected=0x40484D8F + DockNode ID=0x0000000F Parent=0x00000008 SizeRef=942,2137 Split=Y Selected=0x07E8375F + DockNode ID=0x00000011 Parent=0x0000000F SizeRef=835,425 Selected=0x72F373AE + DockNode ID=0x00000012 Parent=0x0000000F SizeRef=835,1710 Selected=0x73845A9B + DockNode ID=0x00000010 Parent=0x00000008 SizeRef=2520,2137 Split=Y Selected=0xCE7F911A + DockNode ID=0x00000013 Parent=0x00000010 SizeRef=1967,1690 Split=X Selected=0xCE7F911A + DockNode ID=0x00000017 Parent=0x00000013 SizeRef=1314,1749 Selected=0x4B454E0B + DockNode ID=0x00000018 Parent=0x00000013 SizeRef=1309,1749 Split=Y Selected=0x88A8C2FF + DockNode ID=0x00000019 Parent=0x00000018 SizeRef=2440,1412 Split=X Selected=0x88A8C2FF + DockNode ID=0x00000023 Parent=0x00000019 SizeRef=1365,737 Split=Y Selected=0x4F935A1E + DockNode ID=0x0000001F Parent=0x00000023 SizeRef=2315,1191 Split=Y Selected=0x4F935A1E + DockNode ID=0x00000025 Parent=0x0000001F SizeRef=2315,1244 Split=X Selected=0x4F935A1E + DockNode ID=0x00000029 Parent=0x00000025 SizeRef=853,1867 CentralNode=1 Selected=0xFDB3860E + DockNode ID=0x0000002A Parent=0x00000025 SizeRef=890,1867 Selected=0x40484D8F + DockNode ID=0x00000026 Parent=0x0000001F SizeRef=2315,621 Selected=0x7D28643F + DockNode ID=0x00000020 Parent=0x00000023 SizeRef=2315,944 Selected=0x4C2F06CB + DockNode ID=0x00000024 Parent=0x00000019 SizeRef=1153,737 Split=Y Selected=0x88A8C2FF + DockNode ID=0x0000002B Parent=0x00000024 SizeRef=1153,1047 Selected=0xB8D8893E + DockNode ID=0x0000002C Parent=0x00000024 SizeRef=1153,1088 Selected=0x88A8C2FF + DockNode ID=0x0000001A Parent=0x00000018 SizeRef=2440,723 Selected=0x3A881EEF + DockNode ID=0x00000014 Parent=0x00000010 SizeRef=1967,445 Selected=0xC36FF36B + DockNode ID=0x00000002 Parent=0x00000004 SizeRef=2227,1161 Split=X Selected=0x714F2F7B + DockNode ID=0x0000000B Parent=0x00000002 SizeRef=968,1161 Selected=0xC915D9DA + DockNode ID=0x0000000C Parent=0x00000002 SizeRef=1661,1161 Split=Y Selected=0x714F2F7B + DockNode ID=0x0000000D Parent=0x0000000C SizeRef=396,342 Selected=0x714F2F7B + DockNode ID=0x0000000E Parent=0x0000000C SizeRef=396,817 Split=Y Selected=0xCF08B82F + DockNode ID=0x0000001B Parent=0x0000000E SizeRef=2104,1328 Split=X Selected=0x43F4115A + DockNode ID=0x00000015 Parent=0x0000001B SizeRef=823,1328 Selected=0x052342BF + DockNode ID=0x00000016 Parent=0x0000001B SizeRef=1402,1328 Selected=0x43F4115A + DockNode ID=0x0000001C Parent=0x0000000E SizeRef=2104,807 Selected=0xCF08B82F diff --git a/gui.py b/gui.py index c5b69d1..cb7398d 100644 --- a/gui.py +++ b/gui.py @@ -10,6 +10,7 @@ import ai_client from ai_client import ProviderError import shell_runner import session_logger +import theme CONFIG_PATH = Path("config.toml") PROVIDERS = ["gemini", "anthropic"] @@ -459,6 +460,7 @@ class App: "provider": self.current_provider, "model": self.current_model, } + theme.save_to_config(self.config) def _do_generate(self) -> tuple[str, Path, list]: self._flush_to_config() @@ -792,6 +794,93 @@ class App: self.disc_entries.append({"role": "AI", "content": resp}) self._rebuild_disc_list() + + # ------------------------------------------------------------ theme + + def cb_palette_changed(self, sender, app_data): + theme.apply(app_data, self._read_colour_overrides()) + self._update_status(f"palette: {app_data}") + + def cb_apply_font(self): + path = dpg.get_value("theme_font_path").strip() + size = dpg.get_value("theme_font_size") + theme.apply_font(path, size) + self._update_status(f"font applied: {path or '(default)'} @{size}px") + + def cb_browse_font(self): + root = hide_tk_root() + p = filedialog.askopenfilename( + title="Select Font", + filetypes=[("TrueType / OpenType", "*.ttf *.otf"), ("All", "*.*")], + ) + root.destroy() + if p: + dpg.set_value("theme_font_path", p) + self.cb_apply_font() + + def cb_scale_changed(self, sender, app_data): + theme.set_scale(round(app_data, 2)) + + def _read_colour_overrides(self) -> dict: + return {} + + def _build_theme_window(self): + t_cfg = self.config.get("theme", {}) + cur_palette = t_cfg.get("palette", "DPG Default") + cur_font_path = t_cfg.get("font_path", "") + cur_font_size = float(t_cfg.get("font_size", 14.0)) + cur_scale = float(t_cfg.get("scale", 1.0)) + + with dpg.window( + label="Theme", + tag="win_theme", + pos=(416, 516), + width=400, + height=280, + no_close=True, + ): + dpg.add_text("Palette") + dpg.add_combo( + tag="theme_palette", + items=theme.PALETTE_NAMES, + default_value=cur_palette, + width=-1, + callback=self.cb_palette_changed, + ) + dpg.add_separator() + dpg.add_text("Font") + with dpg.group(horizontal=True): + dpg.add_input_text( + tag="theme_font_path", + default_value=cur_font_path, + hint="Path to .ttf / .otf (blank = built-in)", + width=-148, + ) + dpg.add_button(label="Browse##font", callback=self.cb_browse_font) + with dpg.group(horizontal=True): + dpg.add_text("Size (px)") + dpg.add_input_float( + tag="theme_font_size", + default_value=cur_font_size, + min_value=8.0, + max_value=64.0, + step=1.0, + width=90, + format="%.0f", + ) + dpg.add_button(label="Apply Font", callback=self.cb_apply_font) + dpg.add_separator() + dpg.add_text("UI Scale (DPI)") + dpg.add_slider_float( + tag="theme_scale", + default_value=cur_scale, + min_value=0.5, + max_value=3.0, + width=-1, + callback=self.cb_scale_changed, + format="%.2f", + ) + def _build_ui(self): with dpg.window( @@ -999,6 +1088,8 @@ class App: with dpg.child_window(tag="comms_scroll", height=-1, border=False, horizontal_scrollbar=True): pass + self._build_theme_window() + def run(self): dpg.create_context() dpg.configure_app(docking=True, docking_space=True, init_file="dpg_layout.ini") @@ -1007,6 +1098,8 @@ class App: dpg.show_viewport() dpg.maximize_viewport() self._build_ui() + theme.load_from_config(self.config) + self._rebuild_disc_list() self._fetch_models(self.current_provider) while dpg.is_dearpygui_running(): @@ -1037,3 +1130,4 @@ if __name__ == "__main__": + diff --git a/theme.py b/theme.py new file mode 100644 index 0000000..09bf8ad --- /dev/null +++ b/theme.py @@ -0,0 +1,415 @@ +# theme.py +""" +Theming support for manual_slop GUI. + +Palettes +-------- +Each palette is a dict mapping semantic names to (R,G,B) or (R,G,B,A) tuples. +The names correspond to dpg theme colour / style constants. + +Font handling +------------- +Call apply_font(path, size) to load a TTF and bind it as the global default. +Call set_scale(factor) to set the global font scale (DPI scaling). + +Usage +----- + import theme + theme.apply("10x") # apply a named palette + theme.apply_font("C:/Windows/Fonts/CascadiaCode.ttf", 15) + theme.set_scale(1.25) +""" + +import dearpygui.dearpygui as dpg +from pathlib import Path + +# ------------------------------------------------------------------ palettes + +# Colour key names match the DPG mvThemeCol_* constants (string lookup below). +# 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), + }, +} + +PALETTE_NAMES: list[str] = list(_PALETTES.keys()) + +# ------------------------------------------------------------------ colour key -> mvThemeCol_* mapping + +# 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", +} + +# ------------------------------------------------------------------ state + +_current_theme_tag: str | None = None +_current_font_tag: str | None = None +_font_registry_tag: str | None = None +_current_palette: str = "DPG Default" +_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()) + + +def get_current_palette() -> str: + return _current_palette + + +def get_current_font_path() -> str: + return _current_font_path + + +def get_current_font_size() -> float: + return _current_font_size + + +def get_current_scale() -> float: + 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, {})) + + +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 + + +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) + + +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) + + +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 + + +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)