Persist accessible colour and motion preferences (issue #80)
Implement Amber/Nord/High Contrast theme presets with XDG user-scoped persistence and reduced motion toggle. Covers TPH-10 and preference integration with TPH-2. - preferences.py: safe load/save with XDG_CONFIG_HOME/fenris/preferences.json - themes.py: three Textual Theme objects with graph colour roles - TUI: t cycles presets, m toggles reduced motion, both persist across restart - Status composition receives reduced_motion from preferences - 56 new tests covering persistence, themes, TUI integration, CLI isolation - All 590 existing tests continue to pass
This commit is contained in:
+47
-6
@@ -59,6 +59,8 @@ from .status_composition import (
|
||||
render_status_tui,
|
||||
STATUS_POLL_INTERVAL_S,
|
||||
)
|
||||
from .preferences import load_preferences, save_preferences
|
||||
from .themes import THEMES, THEME_NAMES, get_theme, get_graph_colors
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -954,6 +956,8 @@ class FenrisTuiApp(App):
|
||||
Binding("r", "resume", "Resume", show=False),
|
||||
Binding("c", "collect", "Collect Now", show=False),
|
||||
Binding("d", "disclose", "Disclosures", show=False),
|
||||
Binding("t", "toggle_theme", "Theme", show=False),
|
||||
Binding("m", "toggle_motion", "Motion", show=False),
|
||||
Binding("q", "quit", "Quit", show=False),
|
||||
]
|
||||
|
||||
@@ -965,6 +969,11 @@ class FenrisTuiApp(App):
|
||||
refresh_interval_s: float = CADENCE_DEFAULT_S,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
# Load user preferences before super().__init__ so theme is available
|
||||
self._prefs = load_preferences()
|
||||
self._current_theme_name = self._prefs["theme"]
|
||||
self._reduced_motion = self._prefs["reduced_motion"]
|
||||
|
||||
super().__init__(**kwargs)
|
||||
self.store_path = store_path or Path("/var/lib/fenris/observations.db")
|
||||
self.config_path = config_path
|
||||
@@ -974,6 +983,10 @@ class FenrisTuiApp(App):
|
||||
self._conn: Optional[sqlite3.Connection] = None
|
||||
self._clock_now = datetime.now(timezone.utc)
|
||||
|
||||
# Register all Fenris themes
|
||||
for theme in THEMES.values():
|
||||
self.register_theme(theme)
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
with VerticalScroll(id="dashboard-scroll"):
|
||||
with Container(id="main-grid"):
|
||||
@@ -985,7 +998,11 @@ class FenrisTuiApp(App):
|
||||
yield Static("q QUIT TUI", id="quit-rail")
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Set border titles and render initial state."""
|
||||
"""Set border titles, apply theme, and render initial state."""
|
||||
# Apply loaded theme (issue #80)
|
||||
# Preference uses underscores (high_contrast); Textual theme uses dashes (fenris-high-contrast)
|
||||
self.theme = "fenris-%s" % self._current_theme_name.replace("_", "-")
|
||||
|
||||
self.query_one("#headline-band").border_title = "headline"
|
||||
self.query_one("#usage-history").border_title = "usage history"
|
||||
self.query_one("#drive-health").border_title = "drive"
|
||||
@@ -1057,7 +1074,7 @@ class FenrisTuiApp(App):
|
||||
self.query_one("#service-strip").update(
|
||||
"boot: disabled · timer: inactive · last collect: unknown · freshness: empty\n"
|
||||
"[bold]CONTINUITY[/bold] %s\n"
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
"p pause · r resume · c collect · t theme · m motion · d disclosures"
|
||||
% monitoring_continuity({"boot_enabled": False})
|
||||
)
|
||||
else:
|
||||
@@ -1069,7 +1086,7 @@ class FenrisTuiApp(App):
|
||||
self.query_one("#usage-history").set_data([])
|
||||
self.query_one("#drive-health").update("")
|
||||
self.query_one("#service-strip").update(
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
"p pause · r resume · c collect · t theme · m motion · d disclosures"
|
||||
)
|
||||
|
||||
def _render_all_regions(self, conn: sqlite3.Connection) -> None:
|
||||
@@ -1147,14 +1164,14 @@ class FenrisTuiApp(App):
|
||||
conn, svc, self._clock_now,
|
||||
store_fault=None,
|
||||
newer_schema=None,
|
||||
reduced_motion=False,
|
||||
reduced_motion=self._reduced_motion,
|
||||
)
|
||||
# Render using the shared composition with TUI styling
|
||||
status_text = render_status_tui(comp)
|
||||
# Add TUI-only actions
|
||||
self.query_one("#service-strip").update(
|
||||
"%s\n"
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
"p pause · r resume · c collect · t theme · m motion · d disclosures"
|
||||
% status_text
|
||||
)
|
||||
self._render_paused_banner(comp)
|
||||
@@ -1162,7 +1179,7 @@ class FenrisTuiApp(App):
|
||||
self._hide_paused_banner()
|
||||
self.query_one("#service-strip").update(
|
||||
"boot: unknown · timer: unknown · last collect: unknown · freshness: unknown\n"
|
||||
"p pause · r resume · c collect · d disclosures"
|
||||
"p pause · r resume · c collect · t theme · m motion · d disclosures"
|
||||
)
|
||||
|
||||
def _render_paused_banner(self, comp) -> None:
|
||||
@@ -1266,6 +1283,30 @@ class FenrisTuiApp(App):
|
||||
|
||||
# --- Actions ---
|
||||
|
||||
def action_toggle_theme(self) -> None:
|
||||
"""Cycle through Amber → Nord → High Contrast presets (issue #80)."""
|
||||
names = sorted(THEME_NAMES)
|
||||
try:
|
||||
idx = names.index(self._current_theme_name)
|
||||
except ValueError:
|
||||
idx = 0
|
||||
self._current_theme_name = names[(idx + 1) % len(names)]
|
||||
self.theme = "fenris-%s" % self._current_theme_name.replace("_", "-")
|
||||
save_preferences(
|
||||
theme=self._current_theme_name,
|
||||
reduced_motion=self._reduced_motion,
|
||||
)
|
||||
self._refresh()
|
||||
|
||||
def action_toggle_motion(self) -> None:
|
||||
"""Toggle reduced motion preference (issue #80)."""
|
||||
self._reduced_motion = not self._reduced_motion
|
||||
save_preferences(
|
||||
theme=self._current_theme_name,
|
||||
reduced_motion=self._reduced_motion,
|
||||
)
|
||||
self._refresh()
|
||||
|
||||
def action_pause(self) -> None:
|
||||
"""Pause monitoring — asks for confirmation (spec §7.4, LC-6)."""
|
||||
self.push_screen(ConfirmPause(), callback=self._pause_confirmed)
|
||||
|
||||
Reference in New Issue
Block a user