Private
Public Access
0
0

feat(theme): load themes from TOML and apply syntax palette mapping

This commit is contained in:
2026-06-04 22:59:59 -04:00
parent e2f698c4a3
commit e14b3c2ce0
3 changed files with 4283 additions and 8 deletions
+4166
View File
File diff suppressed because one or more lines are too long
+74 -3
View File
@@ -15,6 +15,8 @@ from src import imgui_scopes as imscope
import src.theme_nerv
from src.theme_nerv import DATA_GREEN
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
from src.paths import get_global_themes_path
from src.theme_models import ThemeFile, load_themes_from_dir, load_themes_from_toml
# ------------------------------------------------------------------ palettes
@@ -225,12 +227,33 @@ _PALETTES: dict[str, dict[int, tuple]] = {
},
}
def _hex(rgb: tuple[int, int, int]) -> tuple[float, float, float, float]:
return (rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0, 1.0)
_TOML_PALETTES: dict[str, ThemeFile] = {}
_TOML_COLOUR_CACHE: dict[str, dict[int, tuple[float, float, float, float]]] = {}
def _build_imgui_colour_dict(theme: ThemeFile) -> dict[int, tuple[float, float, float, float]]:
from imgui_bundle import imgui
out: dict[int, tuple[float, float, float, float]] = {}
palette_dict = theme.palette.to_dict()
for col_name, rgb in palette_dict.items():
if hasattr(imgui.Col_, col_name):
enum_val = getattr(imgui.Col_, col_name)
if isinstance(enum_val, int):
out[enum_val] = _hex(rgb)
return out
def get_palette_names() -> list[str]:
"""Returns a list of all available palettes, including hello_imgui built-ins."""
"""Returns a list of all available palettes, including hello_imgui built-ins
and TOML-loaded themes."""
names = list(_PALETTES.keys())
# Add hello_imgui themes
names.extend(sorted(_TOML_PALETTES.keys()))
hi_themes = [name for name in dir(hello_imgui.ImGuiTheme_) if not name.startswith('_') and name != 'count']
# Filter out int methods that leaked into dir() if any
hi_themes = [n for n in hi_themes if not hasattr(int, n)]
names.extend(sorted(hi_themes))
return names
@@ -292,6 +315,7 @@ def apply(palette_name: str) -> None:
_current_palette = palette_name
if palette_name == 'NERV':
src.theme_nerv.apply_nerv()
apply_syntax_palette(get_syntax_palette_for_theme(palette_name))
return
# 1. Apply base colors
@@ -301,6 +325,16 @@ def apply(palette_name: str) -> None:
style = imgui.get_style()
for col_enum, rgba in colours.items():
style.set_color_(col_enum, imgui.ImVec4(*rgba))
elif palette_name in _TOML_PALETTES:
colours = _TOML_COLOUR_CACHE.get(palette_name, {})
if not colours:
theme = _TOML_PALETTES[palette_name]
colours = _build_imgui_colour_dict(theme)
_TOML_COLOUR_CACHE[palette_name] = colours
imgui.style_colors_dark()
style = imgui.get_style()
for colenum, rgba in colours.items():
style.set_color_(colenum, imgui.ImVec4(*rgba))
elif hasattr(hello_imgui.ImGuiTheme_, palette_name):
theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name)
hello_imgui.apply_theme(theme_enum)
@@ -392,6 +426,43 @@ def apply_current() -> None:
apply(_current_palette)
set_scale(_current_scale)
def load_themes_from_disk() -> None:
"""Load all themes from the global themes directory. Each *.toml file
in the directory is one theme. Idempotent - safe to call repeatedly.
Broken entries are logged and skipped."""
global _TOML_PALETTES, _TOML_COLOUR_CACHE
themes_dir = get_global_themes_path()
loaded: dict[str, ThemeFile] = {}
if themes_dir.exists() and themes_dir.is_dir():
loaded.update(load_themes_from_dir(themes_dir, scope="global"))
_TOML_PALETTES = loaded
_TOML_COLOUR_CACHE = {name: _build_imgui_colour_dict(t) for name, t in loaded.items()}
def get_syntax_palette_for_theme(theme_name: str) -> str:
"""Return the syntax palette name (one of dark/light/mariana/retro_blue)
associated with the given UI theme. Falls back to 'dark' for unknown
themes and for non-TOML built-ins."""
if theme_name in _TOML_PALETTES:
return _TOML_PALETTES[theme_name].syntax_palette
return "dark"
def apply_syntax_palette(palette_name: str) -> None:
"""Set the default imgui_color_text_edit palette. palette_name must
be one of: dark, light, mariana, retro_blue. No-op for unknown names."""
from imgui_bundle import imgui_color_text_edit as ed
if not hasattr(ed.TextEditor, "PaletteId"):
return
palette_id = getattr(ed.TextEditor.PaletteId, palette_name, None)
if palette_id is None:
return
ed.TextEditor.set_default_palette(palette_id)
load_themes_from_disk()
def get_font_loading_params() -> tuple[str, float]:
"""Return (font_path, font_size) for use during hello_imgui font loading callback."""
return _current_font_path, _current_font_size
+43 -5
View File
@@ -1,19 +1,18 @@
import pytest
from unittest.mock import MagicMock
from src import theme_2 as theme
from src import paths as paths_mod
def test_theme_apply_sets_rounding_and_padding(monkeypatch):
# Mock imgui
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)
# Call apply with the default palette
theme.apply("ImGui Dark")
# Verify subtle rounding styles
assert mock_style.window_rounding == 6.0
assert mock_style.child_rounding == 4.0
assert mock_style.frame_rounding == 4.0
@@ -22,14 +21,53 @@ def test_theme_apply_sets_rounding_and_padding(monkeypatch):
assert mock_style.grab_rounding == 4.0
assert mock_style.tab_rounding == 4.0
# Verify borders
assert mock_style.window_border_size == 1.0
assert mock_style.frame_border_size == 1.0
assert mock_style.popup_border_size == 1.0
# Verify padding/spacing
assert mock_style.window_padding == (8.0, 8.0)
assert mock_style.frame_padding == (8.0, 4.0)
assert mock_style.item_spacing == (8.0, 4.0)
assert mock_style.item_inner_spacing == (4.0, 4.0)
assert mock_style.scrollbar_size == 14.0
def test_themes_load_from_toml(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]\n'
'window_bg = [0, 43, 54]\n'
'text = [147, 161, 161]\n'
)
(themes_dir / "broken.toml").write_text('syntax_palette = "dark"\n')
monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir)
theme.load_themes_from_disk()
names = theme.get_palette_names()
assert "solarized_dark" in names
assert "broken" not in names
def test_get_syntax_palette_for_theme(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'
)
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"
assert theme.get_syntax_palette_for_theme("ImGui Dark") == "dark"
def test_get_syntax_palette_for_unknown_theme_returns_default(tmp_path, monkeypatch):
from src import paths as paths_mod
themes_dir = tmp_path / "themes"
themes_dir.mkdir()
monkeypatch.setattr(theme, "get_global_themes_path", lambda: themes_dir)
theme.load_themes_from_disk()
assert theme.get_syntax_palette_for_theme("NonExistent") == "dark"