Private
Public Access
0
0

conductor(plan): theme + syntax modularization - 7-task plan

This commit is contained in:
2026-06-04 22:20:58 -04:00
parent e86dacde8a
commit cd24c43f8f
2 changed files with 1219 additions and 0 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,105 @@
# Theme & Syntax Highlighting Modularization
## Problem
The current theming system in `src/theme_2.py` has three limitations:
1. **Themes are hardcoded as a Python dict.** Users cannot author new themes without editing Python source and recompiling. This is inconsistent with the rest of the project (presets, personas, tool_presets, context_presets, bias profiles, workspace profiles all use TOML).
2. **Syntax highlighting is hardcoded.** The `MarkdownRenderer._lang_map` in `src/markdown_helper.py` uses `imgui-bundle`'s `imgui_color_text_edit` language definitions whose token colors are baked into the C++ library. There is no way to align syntax token colors with the active UI theme.
3. **No way to bundle new themes with a release or share them between projects.**
## Goals
- **TOML-based theme authoring.** Themes live in `themes/<name>.toml` (global) and `<project>/project_themes.toml` (project override). Schema mirrors the existing `_PALETTES` dict shape.
- **Authoring without recompiling.** Drop a new `.toml` file in `themes/` and it appears in the palette selector after the next load (or hot-reload, future).
- **Syntax palette mapping.** Each theme TOML declares a `syntax_palette` field that maps to one of the four built-in `imgui_color_text_edit` palettes (`dark`, `light`, `mariana`, `retro_blue`). The renderer calls `editor.set_default_palette(...)` whenever the active theme changes.
- **Scope-based merging** matches the existing pattern: project themes override global themes with the same name.
## Constraints
- `imgui-bundle` only ships 4 built-in syntax palettes and exposes no API to define new ones or override individual token colors. This is a hard upstream limit. The plan accepts the limit and works around it via palette mapping.
- We do NOT attempt to wrap or shadow `imgui_color_text_edit`. The C++ library owns the per-language token regexes and default token colors. We pick the closest of the 4 palettes for each theme and let users override the mapping per theme.
## Out of scope
- Defining new `imgui_color_text_edit` palettes or overriding token colors per language (blocked by upstream API).
- Hot-reload of theme changes (the user can re-apply from the selector).
- Per-language color customization (e.g., Python `keyword` color distinct from C `keyword`).
## File structure
| File | Action | Responsibility |
|---|---|---|
| `src/theme_2.py` | Modify | Replace hardcoded `_PALETTES` dict with a load-from-TOML pipeline. Keep `apply()` public API. Expose new helpers `get_syntax_palette_for_theme(name)` and `apply_syntax_palette(palette_id)`. |
| `src/paths.py` | Modify | Add `get_global_themes_path()` returning `<root>/themes/` (directory) and `get_project_themes_path(project_root)` returning `<project>/project_themes.toml` (file). Override `get_global_themes_path()` via the `SLOP_GLOBAL_THEMES` env var. |
| `src/theme_models.py` | Create | `ThemePalette` dataclass + `ThemeFile` schema; `from_dict()` / `to_dict()` round-trip; imgui.Col_ key normalization; loaders for both per-file (`themes/*.toml`) and bundled (`project_themes.toml`) layouts. |
| `themes/solarized_dark.toml` | Create | Authoring artifact. RGB triples in standard 0-255 form. |
| `themes/solarized_light.toml` | Create | Same. |
| `themes/gruvbox_dark.toml` | Create | Same. |
| `themes/moss.toml` | Create | Same. |
| `tests/test_theme_models.py` | Create | Round-trip + validation tests for `ThemePalette` and `ThemeFile` (both per-file and bundled layouts). |
| `tests/test_theme.py` | Modify | Add tests for the 4 new palettes, TOML loading, scope merge, and syntax palette mapping. |
| `tests/fixtures/themes/minimal.toml` | Create | Minimal valid TOML fixture for loader tests. |
| `tests/fixtures/themes/missing_required.toml` | Create | TOML missing required keys — should raise a clear error. |
| `tests/fixtures/themes/bundled_project.toml` | Create | Multi-theme project override fixture (bundled format). |
| `docs/guide_themes.md` | Create | Authoring guide: schema, file locations, scope rules, syntax palette mapping, env vars. |
## Theme TOML schema (reference, not implementation in this plan)
```toml
# theme name (informational)
name = "Solarized Dark"
# optional: which built-in imgui_color_text_edit palette to use
# one of: dark | light | mariana | retro_blue
syntax_palette = "dark"
# which imgui style colors this theme overrides
# any key not listed falls back to the base imgui dark/light defaults
[colors]
window_bg = [ 0, 43, 54] # 0x002b36 base03
child_bg = [ 7, 54, 66] # 0x073642 base02
text = [147, 161, 161] # 0x93a1a1 base1
text_disabled = [ 88, 110, 117] # 0x586e75 base01
button_hovered = [ 38, 139, 210] # 0x268bd2 blue
check_mark = [ 38, 139, 210]
slider_grab = [ 38, 139, 210]
tab_selected = [ 88, 110, 117]
tab_hovered = [ 38, 139, 210]
# ... remaining colors omitted
```
Values are 3-element RGB arrays (0-255) for the body and the syntax palette is a string identifier.
## Syntax palette mapping (built-in only)
| Theme | Syntax palette |
|---|---|
| Solarized Dark | `dark` (closest dark base) |
| Solarized Light | `light` |
| Gruvbox Dark | `retro_blue` (warm retro feel) |
| Moss | `mariana` (deep blue-green base) |
| 10x Dark | `dark` |
| Nord Dark | `dark` |
| Monokai | `dark` |
| Binks | `light` |
| ImGui Dark | `dark` |
| NERV | `dark` (NERV's own custom palette via `theme_nerv.apply_nerv()`) |
The mapping lives in `src/theme_2.py` as a small dict and is overridable per theme via the TOML `syntax_palette` field.
## Public API
Existing `src.theme_2` callsites must continue to work. New surface:
- `theme.get_palette_names() -> list[str]` — already exists, now also returns TOML-loaded themes
- `theme.apply(name) -> None` — already exists, applies the named theme (built-in OR TOML)
- `theme.get_syntax_palette_for_theme(name) -> PaletteId` — new
- `theme.apply_syntax_palette(palette_id) -> None` — new, calls `editor.set_default_palette(palette_id)`
- `theme.load_themes_from_disk() -> None` — new, public for hot-reload