From 9467769260188dae6ed2e1aaf006950f6e5672a1 Mon Sep 17 00:00:00 2001 From: Ed_ Date: Fri, 5 Jun 2026 18:50:10 -0400 Subject: [PATCH] docs(themes): rewrite authoring guide to match actual API + 8-shipped themes --- docs/guide_themes.md | 132 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 114 insertions(+), 18 deletions(-) diff --git a/docs/guide_themes.md b/docs/guide_themes.md index d8707474..440c6442 100644 --- a/docs/guide_themes.md +++ b/docs/guide_themes.md @@ -2,21 +2,21 @@ ## File Layout -- Global themes: `themes.toml` (single multi-theme file) OR `themes/.toml` (one file per theme) -- Project-specific overrides: `/project_themes.toml` +- **Global themes:** `themes/.toml` — one file per theme, in a directory at the project root. +- **Project-specific overrides:** `/project_themes.toml` — a single bundled TOML file with one `[themes.]` table per theme. +- **Override the global path** via the `SLOP_GLOBAL_THEMES` environment variable (must be a directory). Both layouts are scanned and merged; project themes with the same name as a global theme override it. -Override the global path via the `SLOP_GLOBAL_THEMES` env var. - ## Schema ```toml -# human-readable label +# human-readable label (optional) description = "Solarized Dark by Ethan Schoonover" # one of: dark | light | mariana | retro_blue # selects which built-in imgui_color_text_edit palette to apply +# to code blocks in markdown viewers syntax_palette = "dark" [colors] @@ -27,26 +27,122 @@ button_hovered = [ 38, 139, 210] # ... any imgui.Col_ key is accepted ``` -`[colors]` is required. Missing required section is a hard error (logged to stderr, theme skipped). +- **`syntax_palette`** is required for TOML-defined themes. Unknown values fall back to `"dark"`. +- **`[colors]`** is required. Missing it is a hard error (logged to stderr, theme skipped). +- **Color keys** are imgui `Col_` enum members in snake_case. The loader does best-effort mapping; unknown keys are silently ignored. -## Available Color Keys +### Common Color Keys -All keys are imgui `Col_` enum members in snake_case. The loader does best-effort mapping; unknown keys are silently ignored. Common ones: `window_bg`, `child_bg`, `popup_bg`, `border`, `frame_bg`, `title_bg`, `menu_bar_bg`, `scrollbar_bg`, `button`, `header`, `separator`, `tab`, `text`, `text_disabled`, `check_mark`, `slider_grab`, `table_header_bg`. +| Key | ImGui `Col_` | Use | +|---|---|---| +| `window_bg` | `WindowBg` | Panel/window background | +| `child_bg` | `ChildBg` | Nested child regions | +| `popup_bg` | `PopupBg` | Modal/popup backdrop | +| `border` | `Border` | Separator/border | +| `frame_bg` | `FrameBg` | Input field background | +| `title_bg` | `TitleBg` | Window title bar | +| `menu_bar_bg` | `MenuBarBg` | Top menu strip | +| `scrollbar_bg` | `ScrollbarBg` | Scrollbar track | +| `button` | `Button` | Standard button | +| `header` | `Header` | Collapsible section header | +| `separator` | `Separator` | Divider line | +| `tab` | `Tab` | Tab bar item | +| `text` | `Text` | Default text | +| `text_disabled` | `TextDisabled` | Greyed-out text | +| `check_mark` | `CheckMark` | Checkbox/radio check | +| `slider_grab` | `SliderGrab` | Slider thumb | +| `table_header_bg` | `TableHeaderBg` | Table column headers | +| `status_info` | (semantic) | Informational accent | +| `status_success` | (semantic) | Success/positive accent | +| `status_warning` | (semantic) | Warning accent | +| `status_error` | (semantic) | Error/negative accent | -## Syntax Palette Mapping +The `status_*` keys are **semantic** — they map to the theme's accent colors and are used by the `C_*` color helpers in `src/gui_2.py:80-92`. -`imgui-bundle` ships four built-in `imgui_color_text_edit` palettes and exposes no API to define new ones. We pick the closest match per theme: +## The 4-Syntax-Palette Upstream Limit -| UI Theme | Syntax Palette | +`imgui-bundle` ships **four** built-in `imgui_color_text_edit` palettes and exposes no API to define new ones: + +| Palette | Style | |---|---| -| Solarized Dark | `dark` | -| Solarized Light | `light` | -| Gruvbox Dark | `retro_blue` | -| Moss | `mariana` | -| (anything else) | `dark` | +| `dark` | Default dark; balanced contrast | +| `light` | Default light; balanced contrast | +| `mariana` | VS Code Mariana-inspired; muted blues | +| `retro_blue` | High-contrast blue-on-black retro CRT | -You can override the mapping per theme by setting the `syntax_palette` field in the TOML. +You select which one your theme uses by setting the `syntax_palette` field. The system picks the closest match for you when you omit the field (built-in non-TOML themes get `dark` by default). To get a different palette, set the field explicitly. + +This is a hard upstream limit; there is no way to define a 5th palette without forking imgui-bundle. If you find yourself wanting to, the answer is to pick the closest of the four and adjust your UI theme's colors to harmonize. + +## Public API (`src/theme_2.py`) + +The system exposes three functions for runtime use: + +| Function | Purpose | +|---|---| +| `load_themes_from_disk() -> None` | Re-scan the global themes directory and `/project_themes.toml`, re-parse, and refresh the palette registry. Call this after dropping a new `.toml` file into `themes/`. | +| `get_syntax_palette_for_theme(theme_name: str) -> str` | Return the syntax palette name (`dark`/`light`/`mariana`/`retro_blue`) associated with a UI theme. Returns `"dark"` for unknown themes. | +| `apply_syntax_palette(palette_name: str) -> None` | Set the active `imgui_color_text_edit` default palette. No-op for unknown names. | + +The `MarkdownRenderer.__init__` (`src/markdown_helper.py`) automatically calls `apply_syntax_palette(get_syntax_palette_for_theme(get_current_palette()))`, so code blocks in markdown viewers track the active theme. When the user switches themes, new `TextEditor` instances pick up the new palette; cached editors keep their previous palette until the next block renders. + +### Usage Examples + +```python +from src import theme_2 as theme + +# Re-scan disk (e.g. after dropping a new theme file) +theme.load_themes_from_disk() + +# Look up the syntax palette for a UI theme name +syntax = theme.get_syntax_palette_for_theme("solarized_dark") # "dark" + +# Force a specific syntax palette (e.g. for a one-off preview) +theme.apply_syntax_palette("mariana") +``` + +## The C_* Color-Callable Convention + +`src/gui_2.py:80-92` defines 13 module-level getter functions for semantic colors used throughout the GUI: + +```python +def C_LBL() -> imgui.ImVec4: return theme.get_color("text_disabled") +def C_VAL() -> imgui.ImVec4: return theme.get_color("text") +def C_OUT() -> imgui.ImVec4: return theme.get_color("status_info") +def C_IN() -> imgui.ImVec4: return theme.get_color("status_success") +# ... and 9 more (C_REQ, C_RES, C_TC, C_TR, C_TRS, C_KEY, C_NUM, C_TRM, C_SUB) +``` + +**These are callables, not `ImVec4` values.** They resample the current theme's color on each call, so theme switches take effect on the next render frame. + +**Correct usage** — call the function at the use site: +```python +imgui.text_colored(C_LBL(), "Completed:") +imgui.text_colored(C_VAL(), str(value)) +``` + +**Common bug** — storing the function in a dict keyed by name, then passing the function (not its result) to imgui: +```python +DIR_COLORS = {"request": C_OUT, "response": C_IN} +d_col_fn = DIR_COLORS.get(direction, C_VAL) # stores the function +imgui.text_colored(d_col_fn(), direction) # CORRECT: call it +``` + +The bug shipped in commit `7ea52cbb` (multi-themes track) at `src/gui_2.py:3705-3707` and was fixed in `1469ecac`. When writing tests that assert theme color usage, **patch `src.theme_2.imgui`** so `theme.get_color()` returns the mock's `ImVec4`, and assert with `C_LBL()` (called), not `C_LBL` (the function). ## Hot Reload -Theme TOMLs are loaded once at module init. To pick up a new file, call `theme.load_themes_from_disk()` (or restart the app). \ No newline at end of file +Theme TOMLs are loaded once at module init **and** can be reloaded on demand via `theme.load_themes_from_disk()`. The function is safe to call from the GUI thread; it mutates the global registry atomically. + +**Typical workflow** when authoring a new theme: +1. Drop a new file into `themes/`. +2. From the AI Settings panel's theme dropdown, the new theme is not yet visible — the registry is cached. +3. To see it without restarting, call `theme.load_themes_from_disk()` from a Python console hooked into the running process, OR add a "Refresh Themes" button that calls it, OR restart the app. + +`project_themes.toml` is scanned for every project load, so changes there are picked up automatically when you switch projects. + +## Cross-References + +- **[guide_gui_2.md](guide_gui_2.md#theme-color-callable-pattern)** — The C_* callables in detail; the DIR_COLORS bug history. +- **[guide_testing.md](guide_testing.md#known-gotchas-2026-06-05)** — How to test theme color usage without crashing `imgui.color()`. +- **[conductor/tracks.md](../../conductor/tracks.md)** — The `multi_themes_20260604` track entry (the 8 shipped themes and the API design).