5.7 KiB
Theme & Syntax Highlighting Modularization
Problem
The current theming system in src/theme_2.py has three limitations:
-
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).
-
Syntax highlighting is hardcoded. The
MarkdownRenderer._lang_mapinsrc/markdown_helper.pyusesimgui-bundle'simgui_color_text_editlanguage definitions whose token colors are baked into the C++ library. There is no way to align syntax token colors with the active UI theme. -
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_PALETTESdict shape. -
Authoring without recompiling. Drop a new
.tomlfile inthemes/and it appears in the palette selector after the next load (or hot-reload, future). -
Syntax palette mapping. Each theme TOML declares a
syntax_palettefield that maps to one of the four built-inimgui_color_text_editpalettes (dark,light,mariana,retro_blue). The renderer callseditor.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-bundleonly 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_editpalettes 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
keywordcolor distinct from Ckeyword).
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() and get_project_themes_path(project_root). Defaults: themes.toml (global) and project_themes.toml (project). Override via SLOP_GLOBAL_THEMES env var. |
src/theme_models.py |
Create | Pydantic/dataclass schema for theme TOML files. ThemePalette has all imgui.Col_ keys, syntax_palette is a string (one of the 4 IDs). to_dict() / from_dict() round-trip. |
themes/solarized_dark.toml |
Create | Authoring artifact. RGB triples in standard #RRGGBB 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 tests for ThemePalette from/to TOML. |
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_keys.toml |
Create | TOML missing required keys — should raise a clear error. |
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)
# 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 themestheme.apply(name) -> None— already exists, applies the named theme (built-in OR TOML)theme.get_syntax_palette_for_theme(name) -> PaletteId— newtheme.apply_syntax_palette(palette_id) -> None— new, callseditor.set_default_palette(palette_id)theme.load_themes_from_disk() -> None— new, public for hot-reload