Add dark/light theme support with real-time OS appearance detection
Config now uses top-level dark/light sections (each with running/completed
styles) and a theme option ("dark", "light", "auto"). The daemon resolves
the active theme at flash time via NSApp.effectiveAppearance().
Made-with: Cursor
This commit is contained in:
@@ -1,23 +1,42 @@
|
||||
from cursor_flasher.config import Config, StyleConfig, load_config
|
||||
from cursor_flasher.config import (
|
||||
Config,
|
||||
StyleConfig,
|
||||
ThemeStyles,
|
||||
load_config,
|
||||
)
|
||||
|
||||
|
||||
class TestDefaultConfig:
|
||||
def test_running_defaults(self):
|
||||
def test_dark_running_defaults(self):
|
||||
c = Config()
|
||||
assert c.running.color == "#FF9500"
|
||||
assert c.running.width == 4
|
||||
assert c.running.duration == 1.5
|
||||
assert c.running.opacity == 0.85
|
||||
assert c.running.pulse_speed == 1.5
|
||||
assert c.running.sound == "Glass"
|
||||
assert c.running.volume == 0.5
|
||||
assert c.dark.running.color == "#FF9500"
|
||||
assert c.dark.running.width == 4
|
||||
assert c.dark.running.duration == 1.5
|
||||
assert c.dark.running.opacity == 0.85
|
||||
assert c.dark.running.pulse_speed == 1.5
|
||||
assert c.dark.running.sound == "Glass"
|
||||
assert c.dark.running.volume == 0.5
|
||||
|
||||
def test_completed_defaults(self):
|
||||
def test_dark_completed_defaults(self):
|
||||
c = Config()
|
||||
assert c.completed.color == "#FF9500"
|
||||
assert c.completed.width == 4
|
||||
assert c.completed.sound == ""
|
||||
assert c.completed.volume == 0.0
|
||||
assert c.dark.completed.color == "#FF9500"
|
||||
assert c.dark.completed.width == 4
|
||||
assert c.dark.completed.sound == ""
|
||||
assert c.dark.completed.volume == 0.0
|
||||
|
||||
def test_light_running_defaults(self):
|
||||
c = Config()
|
||||
assert c.light.running.color == "#FF9500"
|
||||
assert c.light.running.sound == "Glass"
|
||||
|
||||
def test_light_completed_defaults(self):
|
||||
c = Config()
|
||||
assert c.light.completed.sound == ""
|
||||
assert c.light.completed.volume == 0.0
|
||||
|
||||
def test_theme_defaults_to_auto(self):
|
||||
c = Config()
|
||||
assert c.theme == "auto"
|
||||
|
||||
def test_has_approval_tools(self):
|
||||
c = Config()
|
||||
@@ -32,6 +51,41 @@ class TestDefaultConfig:
|
||||
assert c.flash_mode == "screen"
|
||||
|
||||
|
||||
class TestActiveStyles:
|
||||
def test_auto_returns_dark_when_system_dark(self):
|
||||
c = Config(theme="auto")
|
||||
dark_styles = ThemeStyles(running=StyleConfig(color="#111111"))
|
||||
light_styles = ThemeStyles(running=StyleConfig(color="#EEEEEE"))
|
||||
c.dark = dark_styles
|
||||
c.light = light_styles
|
||||
assert c.active_styles("dark").running.color == "#111111"
|
||||
|
||||
def test_auto_returns_light_when_system_light(self):
|
||||
c = Config(theme="auto")
|
||||
dark_styles = ThemeStyles(running=StyleConfig(color="#111111"))
|
||||
light_styles = ThemeStyles(running=StyleConfig(color="#EEEEEE"))
|
||||
c.dark = dark_styles
|
||||
c.light = light_styles
|
||||
assert c.active_styles("light").running.color == "#EEEEEE"
|
||||
|
||||
def test_explicit_dark_ignores_system(self):
|
||||
c = Config(theme="dark")
|
||||
c.dark = ThemeStyles(running=StyleConfig(color="#111111"))
|
||||
c.light = ThemeStyles(running=StyleConfig(color="#EEEEEE"))
|
||||
assert c.active_styles("light").running.color == "#111111"
|
||||
|
||||
def test_explicit_light_ignores_system(self):
|
||||
c = Config(theme="light")
|
||||
c.dark = ThemeStyles(running=StyleConfig(color="#111111"))
|
||||
c.light = ThemeStyles(running=StyleConfig(color="#EEEEEE"))
|
||||
assert c.active_styles("dark").running.color == "#EEEEEE"
|
||||
|
||||
def test_active_styles_includes_completed(self):
|
||||
c = Config(theme="dark")
|
||||
c.dark = ThemeStyles(completed=StyleConfig(color="#AA0000"))
|
||||
assert c.active_styles("light").completed.color == "#AA0000"
|
||||
|
||||
|
||||
class TestLoadConfig:
|
||||
def test_missing_file_returns_defaults(self, tmp_path):
|
||||
c = load_config(tmp_path / "nope.yaml")
|
||||
@@ -43,26 +97,80 @@ class TestLoadConfig:
|
||||
c = load_config(p)
|
||||
assert c == Config()
|
||||
|
||||
def test_loads_running_overrides(self, tmp_path):
|
||||
def test_loads_theme(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"running:\n color: '#00FF00'\n duration: 2.0\n sound: Ping\n"
|
||||
)
|
||||
p.write_text("theme: dark\n")
|
||||
c = load_config(p)
|
||||
assert c.running.color == "#00FF00"
|
||||
assert c.running.duration == 2.0
|
||||
assert c.running.sound == "Ping"
|
||||
assert c.running.width == 4
|
||||
assert c.theme == "dark"
|
||||
|
||||
def test_loads_completed_overrides(self, tmp_path):
|
||||
def test_invalid_theme_uses_default(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text("theme: neon\n")
|
||||
c = load_config(p)
|
||||
assert c.theme == "auto"
|
||||
|
||||
def test_loads_dark_running_overrides(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"completed:\n color: '#0000FF'\n sound: Hero\n volume: 0.8\n"
|
||||
"dark:\n"
|
||||
" running:\n"
|
||||
" color: '#00FF00'\n"
|
||||
" duration: 2.0\n"
|
||||
" sound: Ping\n"
|
||||
)
|
||||
c = load_config(p)
|
||||
assert c.completed.color == "#0000FF"
|
||||
assert c.completed.sound == "Hero"
|
||||
assert c.completed.volume == 0.8
|
||||
assert c.dark.running.color == "#00FF00"
|
||||
assert c.dark.running.duration == 2.0
|
||||
assert c.dark.running.sound == "Ping"
|
||||
assert c.dark.running.width == 4
|
||||
|
||||
def test_loads_dark_completed_overrides(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"dark:\n"
|
||||
" completed:\n"
|
||||
" color: '#0000FF'\n"
|
||||
" sound: Hero\n"
|
||||
" volume: 0.8\n"
|
||||
)
|
||||
c = load_config(p)
|
||||
assert c.dark.completed.color == "#0000FF"
|
||||
assert c.dark.completed.sound == "Hero"
|
||||
assert c.dark.completed.volume == 0.8
|
||||
|
||||
def test_loads_light_running_overrides(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"light:\n"
|
||||
" running:\n"
|
||||
" color: '#3B82F6'\n"
|
||||
" opacity: 0.9\n"
|
||||
)
|
||||
c = load_config(p)
|
||||
assert c.light.running.color == "#3B82F6"
|
||||
assert c.light.running.opacity == 0.9
|
||||
assert c.light.running.width == 4
|
||||
|
||||
def test_missing_dark_section_uses_defaults(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"light:\n"
|
||||
" running:\n"
|
||||
" color: '#FFFFFF'\n"
|
||||
)
|
||||
c = load_config(p)
|
||||
assert c.dark == ThemeStyles()
|
||||
|
||||
def test_missing_running_within_theme_uses_defaults(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"dark:\n"
|
||||
" completed:\n"
|
||||
" color: '#FF0000'\n"
|
||||
)
|
||||
c = load_config(p)
|
||||
assert c.dark.running == StyleConfig()
|
||||
assert c.dark.completed.color == "#FF0000"
|
||||
|
||||
def test_loads_flash_mode(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
@@ -86,16 +194,25 @@ class TestLoadConfig:
|
||||
def test_full_config(self, tmp_path):
|
||||
p = tmp_path / "config.yaml"
|
||||
p.write_text(
|
||||
"running:\n"
|
||||
" color: '#FF0000'\n"
|
||||
" width: 6\n"
|
||||
" opacity: 0.9\n"
|
||||
" pulse_speed: 2.0\n"
|
||||
" sound: Glass\n"
|
||||
" volume: 0.8\n"
|
||||
"completed:\n"
|
||||
" color: '#00FF00'\n"
|
||||
" sound: ''\n"
|
||||
"theme: light\n"
|
||||
"dark:\n"
|
||||
" running:\n"
|
||||
" color: '#FF0000'\n"
|
||||
" width: 6\n"
|
||||
" opacity: 0.9\n"
|
||||
" pulse_speed: 2.0\n"
|
||||
" sound: Glass\n"
|
||||
" volume: 0.8\n"
|
||||
" completed:\n"
|
||||
" color: '#00FF00'\n"
|
||||
" sound: ''\n"
|
||||
"light:\n"
|
||||
" running:\n"
|
||||
" color: '#3B82F6'\n"
|
||||
" width: 3\n"
|
||||
" completed:\n"
|
||||
" color: '#22C55E'\n"
|
||||
" duration: 2.0\n"
|
||||
"flash:\n"
|
||||
" mode: window\n"
|
||||
"general:\n"
|
||||
@@ -105,14 +222,19 @@ class TestLoadConfig:
|
||||
" - Shell\n"
|
||||
)
|
||||
c = load_config(p)
|
||||
assert c.running.color == "#FF0000"
|
||||
assert c.running.width == 6
|
||||
assert c.running.opacity == 0.9
|
||||
assert c.running.pulse_speed == 2.0
|
||||
assert c.running.sound == "Glass"
|
||||
assert c.running.volume == 0.8
|
||||
assert c.completed.color == "#00FF00"
|
||||
assert c.completed.sound == ""
|
||||
assert c.theme == "light"
|
||||
assert c.dark.running.color == "#FF0000"
|
||||
assert c.dark.running.width == 6
|
||||
assert c.dark.running.opacity == 0.9
|
||||
assert c.dark.running.pulse_speed == 2.0
|
||||
assert c.dark.running.sound == "Glass"
|
||||
assert c.dark.running.volume == 0.8
|
||||
assert c.dark.completed.color == "#00FF00"
|
||||
assert c.dark.completed.sound == ""
|
||||
assert c.light.running.color == "#3B82F6"
|
||||
assert c.light.running.width == 3
|
||||
assert c.light.completed.color == "#22C55E"
|
||||
assert c.light.completed.duration == 2.0
|
||||
assert c.flash_mode == "window"
|
||||
assert c.approval_delay == 1.0
|
||||
assert c.cooldown == 3.0
|
||||
|
||||
Reference in New Issue
Block a user