From abc4483859c7b448ae014889f04ca15ec8a8eb79 Mon Sep 17 00:00:00 2001 From: Gene Johnson Date: Mon, 16 Mar 2026 02:49:15 -0700 Subject: [PATCH] feat: create Theme class and registry with finalized color gradients --- engine/themes.py | 60 +++++++++++++++ tests/test_themes.py | 169 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 engine/themes.py create mode 100644 tests/test_themes.py diff --git a/engine/themes.py b/engine/themes.py new file mode 100644 index 0000000..a6d3432 --- /dev/null +++ b/engine/themes.py @@ -0,0 +1,60 @@ +""" +Theme definitions with color gradients for terminal rendering. + +This module is data-only and does not import config or render +to prevent circular dependencies. +""" + + +class Theme: + """Represents a color theme with two gradients.""" + + def __init__(self, name, main_gradient, message_gradient): + """Initialize a theme with name and color gradients. + + Args: + name: Theme identifier string + main_gradient: List of 12 ANSI 256-color codes for main gradient + message_gradient: List of 12 ANSI 256-color codes for message gradient + """ + self.name = name + self.main_gradient = main_gradient + self.message_gradient = message_gradient + + +# ─── GRADIENT DEFINITIONS ───────────────────────────────────────────────── +# Each gradient is 12 ANSI 256-color codes in sequence +# Format: [light...] → [medium...] → [dark...] → [black] + +_GREEN_MAIN = [231, 195, 123, 118, 82, 46, 40, 34, 28, 22, 22, 235] +_GREEN_MSG = [231, 225, 219, 213, 207, 201, 165, 161, 125, 89, 89, 235] + +_ORANGE_MAIN = [231, 215, 209, 208, 202, 166, 130, 94, 58, 94, 94, 235] +_ORANGE_MSG = [231, 195, 33, 27, 21, 21, 21, 18, 18, 18, 18, 235] + +_PURPLE_MAIN = [231, 225, 177, 171, 165, 135, 129, 93, 57, 57, 57, 235] +_PURPLE_MSG = [231, 226, 226, 220, 220, 184, 184, 178, 178, 172, 172, 235] + + +# ─── THEME REGISTRY ─────────────────────────────────────────────────────── + +THEME_REGISTRY = { + "green": Theme("green", _GREEN_MAIN, _GREEN_MSG), + "orange": Theme("orange", _ORANGE_MAIN, _ORANGE_MSG), + "purple": Theme("purple", _PURPLE_MAIN, _PURPLE_MSG), +} + + +def get_theme(theme_id): + """Retrieve a theme by ID. + + Args: + theme_id: Theme identifier string + + Returns: + Theme object matching the ID + + Raises: + KeyError: If theme_id is not in registry + """ + return THEME_REGISTRY[theme_id] diff --git a/tests/test_themes.py b/tests/test_themes.py new file mode 100644 index 0000000..f6bbdf3 --- /dev/null +++ b/tests/test_themes.py @@ -0,0 +1,169 @@ +""" +Tests for engine.themes module. +""" + +import pytest + +from engine import themes + + +class TestThemeConstruction: + """Tests for Theme class initialization.""" + + def test_theme_construction(self): + """Theme stores name and gradients correctly.""" + main_grad = ["color1", "color2", "color3"] + msg_grad = ["msg1", "msg2", "msg3"] + theme = themes.Theme("test_theme", main_grad, msg_grad) + + assert theme.name == "test_theme" + assert theme.main_gradient == main_grad + assert theme.message_gradient == msg_grad + + +class TestGradientLength: + """Tests for gradient length validation.""" + + def test_gradient_length_green(self): + """Green theme has exactly 12 colors in each gradient.""" + green = themes.THEME_REGISTRY["green"] + assert len(green.main_gradient) == 12 + assert len(green.message_gradient) == 12 + + def test_gradient_length_orange(self): + """Orange theme has exactly 12 colors in each gradient.""" + orange = themes.THEME_REGISTRY["orange"] + assert len(orange.main_gradient) == 12 + assert len(orange.message_gradient) == 12 + + def test_gradient_length_purple(self): + """Purple theme has exactly 12 colors in each gradient.""" + purple = themes.THEME_REGISTRY["purple"] + assert len(purple.main_gradient) == 12 + assert len(purple.message_gradient) == 12 + + +class TestThemeRegistry: + """Tests for THEME_REGISTRY dictionary.""" + + def test_theme_registry_has_three_themes(self): + """Registry contains exactly three themes: green, orange, purple.""" + assert len(themes.THEME_REGISTRY) == 3 + assert set(themes.THEME_REGISTRY.keys()) == {"green", "orange", "purple"} + + def test_registry_values_are_themes(self): + """All registry values are Theme instances.""" + for theme_id, theme in themes.THEME_REGISTRY.items(): + assert isinstance(theme, themes.Theme) + assert theme.name == theme_id + + +class TestGetTheme: + """Tests for get_theme function.""" + + def test_get_theme_valid_green(self): + """get_theme('green') returns correct green Theme.""" + green = themes.get_theme("green") + assert isinstance(green, themes.Theme) + assert green.name == "green" + + def test_get_theme_valid_orange(self): + """get_theme('orange') returns correct orange Theme.""" + orange = themes.get_theme("orange") + assert isinstance(orange, themes.Theme) + assert orange.name == "orange" + + def test_get_theme_valid_purple(self): + """get_theme('purple') returns correct purple Theme.""" + purple = themes.get_theme("purple") + assert isinstance(purple, themes.Theme) + assert purple.name == "purple" + + def test_get_theme_invalid(self): + """get_theme with invalid ID raises KeyError.""" + with pytest.raises(KeyError): + themes.get_theme("invalid_theme") + + def test_get_theme_invalid_none(self): + """get_theme with None raises KeyError.""" + with pytest.raises(KeyError): + themes.get_theme(None) + + +class TestGreenTheme: + """Tests for green theme specific values.""" + + def test_green_theme_unchanged(self): + """Green theme maintains original color sequence.""" + green = themes.get_theme("green") + + # Expected main gradient: 231→195→123→118→82→46→40→34→28→22→22(dim)→235 + expected_main = [231, 195, 123, 118, 82, 46, 40, 34, 28, 22, 22, 235] + # Expected msg gradient: 231→225→219→213→207→201→165→161→125→89→89(dim)→235 + expected_msg = [231, 225, 219, 213, 207, 201, 165, 161, 125, 89, 89, 235] + + assert green.main_gradient == expected_main + assert green.message_gradient == expected_msg + + def test_green_theme_name(self): + """Green theme has correct name.""" + green = themes.get_theme("green") + assert green.name == "green" + + +class TestOrangeTheme: + """Tests for orange theme specific values.""" + + def test_orange_theme_unchanged(self): + """Orange theme maintains original color sequence.""" + orange = themes.get_theme("orange") + + # Expected main gradient: 231→215→209→208→202→166→130→94→58→94→94(dim)→235 + expected_main = [231, 215, 209, 208, 202, 166, 130, 94, 58, 94, 94, 235] + # Expected msg gradient: 231→195→33→27→21→21→21→18→18→18→18(dim)→235 + expected_msg = [231, 195, 33, 27, 21, 21, 21, 18, 18, 18, 18, 235] + + assert orange.main_gradient == expected_main + assert orange.message_gradient == expected_msg + + def test_orange_theme_name(self): + """Orange theme has correct name.""" + orange = themes.get_theme("orange") + assert orange.name == "orange" + + +class TestPurpleTheme: + """Tests for purple theme specific values.""" + + def test_purple_theme_unchanged(self): + """Purple theme maintains original color sequence.""" + purple = themes.get_theme("purple") + + # Expected main gradient: 231→225→177→171→165→135→129→93→57→57→57(dim)→235 + expected_main = [231, 225, 177, 171, 165, 135, 129, 93, 57, 57, 57, 235] + # Expected msg gradient: 231→226→226→220→220→184→184→178→178→172→172(dim)→235 + expected_msg = [231, 226, 226, 220, 220, 184, 184, 178, 178, 172, 172, 235] + + assert purple.main_gradient == expected_main + assert purple.message_gradient == expected_msg + + def test_purple_theme_name(self): + """Purple theme has correct name.""" + purple = themes.get_theme("purple") + assert purple.name == "purple" + + +class TestThemeDataOnly: + """Tests to ensure themes module has no problematic imports.""" + + def test_themes_module_imports(self): + """themes module should be data-only without config/render imports.""" + import inspect + source = inspect.getsource(themes) + # Verify no imports of config or render (look for actual import statements) + lines = source.split('\n') + import_lines = [line for line in lines if line.strip().startswith('import ') or line.strip().startswith('from ')] + # Filter out empty and comment lines + import_lines = [line for line in import_lines if line.strip() and not line.strip().startswith('#')] + # Should have no import lines + assert len(import_lines) == 0, f"Found unexpected imports: {import_lines}"