From 465396675d537421992438c81cf77e1f251a993b Mon Sep 17 00:00:00 2001 From: Ed_ Date: Thu, 4 Jun 2026 23:16:21 -0400 Subject: [PATCH] docs(themes): add authoring guide for TOML theme system --- docs/guide_themes.md | 52 ++++++++++++++++++++++++ tests/test_theme.py | 95 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 docs/guide_themes.md diff --git a/docs/guide_themes.md b/docs/guide_themes.md new file mode 100644 index 00000000..d8707474 --- /dev/null +++ b/docs/guide_themes.md @@ -0,0 +1,52 @@ +# Themes — Authoring Guide + +## File Layout + +- Global themes: `themes.toml` (single multi-theme file) OR `themes/.toml` (one file per theme) +- Project-specific overrides: `/project_themes.toml` + +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 +description = "Solarized Dark by Ethan Schoonover" + +# one of: dark | light | mariana | retro_blue +# selects which built-in imgui_color_text_edit palette to apply +syntax_palette = "dark" + +[colors] +# RGB triples, 0-255 +window_bg = [ 0, 43, 54] +text = [147, 161, 161] +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). + +## Available 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`. + +## Syntax Palette Mapping + +`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: + +| UI Theme | Syntax Palette | +|---|---| +| Solarized Dark | `dark` | +| Solarized Light | `light` | +| Gruvbox Dark | `retro_blue` | +| Moss | `mariana` | +| (anything else) | `dark` | + +You can override the mapping per theme by setting the `syntax_palette` field in the TOML. + +## 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 diff --git a/tests/test_theme.py b/tests/test_theme.py index 6c401d83..e0b4bded 100644 --- a/tests/test_theme.py +++ b/tests/test_theme.py @@ -58,6 +58,12 @@ def test_get_syntax_palette_for_theme(tmp_path, monkeypatch): (themes_dir / "solarized_light.toml").write_text( 'syntax_palette = "light"\n[colors]\nwindow_bg = [253, 246, 227]\n' ) + from unittest.mock import MagicMock + mock_style = MagicMock() + mock_imgui = MagicMock() + mock_imgui.get_style.return_value = mock_style + mock_imgui.ImVec2.side_effect = lambda x, y: (x, y) + monkeypatch.setattr(theme, "imgui", mock_imgui) monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir) theme.load_themes_from_disk() assert theme.get_syntax_palette_for_theme("solarized_light") == "light" @@ -97,6 +103,95 @@ def test_solarized_light_uses_light_syntax_palette(tmp_path, monkeypatch): (themes_dir / "solarized_light.toml").write_text( 'syntax_palette = "light"\n[colors]\nwindow_bg = [253, 246, 227]\n' ) + from unittest.mock import MagicMock + mock_style = MagicMock() + mock_imgui = MagicMock() + mock_imgui.get_style.return_value = mock_style + mock_imgui.ImVec2.side_effect = lambda x, y: (x, y) + monkeypatch.setattr(theme, "imgui", mock_imgui) monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir) theme.load_themes_from_disk() assert theme.get_syntax_palette_for_theme("solarized_light") == "light" + + +def test_solarized_dark_apply_does_not_raise(tmp_path, monkeypatch): + from src import paths as paths_mod + + themes_dir = tmp_path / "themes" + themes_dir.mkdir() + (themes_dir / "solarized_dark.toml").write_text( + 'syntax_palette = "dark"\n[colors]\nwindow_bg = [0, 43, 54]\n' + ) + from unittest.mock import MagicMock + mock_style = MagicMock() + mock_imgui = MagicMock() + mock_imgui.get_style.return_value = mock_style + mock_imgui.ImVec2.side_effect = lambda x, y: (x, y) + monkeypatch.setattr(theme, "imgui", mock_imgui) + monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir) + theme.load_themes_from_disk() + try: + theme.apply("solarized_dark") + except Exception as e: + pytest.fail(f"apply(solarized_dark) raised: {e}") + assert theme.get_current_palette() == "solarized_dark" + + +def test_gruvbox_dark_apply_does_not_raise(tmp_path, monkeypatch): + from src import paths as paths_mod + + themes_dir = tmp_path / "themes" + themes_dir.mkdir() + (themes_dir / "gruvbox_dark.toml").write_text( + 'syntax_palette = "retro_blue"\n[colors]\nwindow_bg = [40, 40, 40]\n' + ) + from unittest.mock import MagicMock + mock_style = MagicMock() + mock_imgui = MagicMock() + mock_imgui.get_style.return_value = mock_style + mock_imgui.ImVec2.side_effect = lambda x, y: (x, y) + monkeypatch.setattr(theme, "imgui", mock_imgui) + monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir) + theme.load_themes_from_disk() + theme.apply("gruvbox_dark") + assert theme.get_current_palette() == "gruvbox_dark" + + +def test_moss_apply_does_not_raise(tmp_path, monkeypatch): + from src import paths as paths_mod + + themes_dir = tmp_path / "themes" + themes_dir.mkdir() + (themes_dir / "moss.toml").write_text( + 'syntax_palette = "mariana"\n[colors]\nwindow_bg = [40, 47, 49]\n' + ) + from unittest.mock import MagicMock + mock_style = MagicMock() + mock_imgui = MagicMock() + mock_imgui.get_style.return_value = mock_style + mock_imgui.ImVec2.side_effect = lambda x, y: (x, y) + monkeypatch.setattr(theme, "imgui", mock_imgui) + monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir) + theme.load_themes_from_disk() + theme.apply("moss") + assert theme.get_current_palette() == "moss" + + +def test_solarized_light_apply_does_not_raise(tmp_path, monkeypatch): + from src import paths as paths_mod + + themes_dir = tmp_path / "themes" + themes_dir.mkdir() + (themes_dir / "solarized_light.toml").write_text( + 'syntax_palette = "light"\n[colors]\nwindow_bg = [253, 246, 227]\n' + ) + from unittest.mock import MagicMock + mock_style = MagicMock() + mock_imgui = MagicMock() + mock_imgui.get_style.return_value = mock_style + mock_imgui.ImVec2.side_effect = lambda x, y: (x, y) + monkeypatch.setattr(theme, "imgui", mock_imgui) + monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir) + theme.load_themes_from_disk() + theme.apply("solarized_light") + assert theme.get_current_palette() == "solarized_light"