docs(themes): add authoring guide for TOML theme system
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
# Themes — Authoring Guide
|
||||
|
||||
## File Layout
|
||||
|
||||
- Global themes: `themes.toml` (single multi-theme file) OR `themes/<name>.toml` (one file per theme)
|
||||
- Project-specific overrides: `<project>/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).
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user