diff --git a/wake_alarm/_alarm.py b/wake_alarm/_alarm.py index e5ab008..8efb61b 100644 --- a/wake_alarm/_alarm.py +++ b/wake_alarm/_alarm.py @@ -21,6 +21,7 @@ import tkinter as tk from gatelock import GateRoot, LockConfig, LockWindow from wake_alarm._alarm_display import _restore_display, _wake_display +from wake_alarm._alarm_view import _build_alarm_view from wake_alarm._audio import ( _beep_loud, _beep_medium, @@ -68,20 +69,6 @@ def _is_alarm_day() -> bool: return datetime.now(tz=timezone.utc).weekday() in ALARM_DAYS -@dataclass -class _AlarmView: - """The Tk widgets that make up the alarm's dismiss-challenge screen.""" - - container: tk.Frame - title_label: tk.Label - round_label: tk.Label - info_label: tk.Label - code_label: tk.Label - entry: tk.Entry - status_label: tk.Label - timer_label: tk.Label - - @dataclass class _AlarmProgress: """Mutable dismiss-challenge progress state.""" @@ -121,11 +108,14 @@ class WakeAlarm: self.root.title("Wake Alarm" + (" [DEMO]" if demo_mode else "")) # mode="soft": no watchdog exists yet for a clean, silent lock # failure, so hard-lock (overrideredirect + grab) is deferred. - self._lock = LockWindow(self.root, LockConfig(mode="soft"), hooks=self) + config = LockConfig(mode="soft") + self._lock = LockWindow(self.root, config, hooks=self) self._lock.setup() self._progress = _AlarmProgress(current_challenge=_make_challenge()) - self._view = self._build_ui() + self._view = _build_alarm_view( + self.root, self._progress.current_challenge, config, self._on_submit + ) self._update_timer() if self._progress.current_challenge.kind == "flash": self._start_flash_countdown() @@ -159,92 +149,6 @@ class WakeAlarm: _restore_display() turn_off_plug() - def _build_ui(self) -> _AlarmView: - """Build the dismiss-challenge UI and return its widgets as a view.""" - challenge = self._progress.current_challenge - - container = tk.Frame(self.root, bg="#1a1a1a") - container.place(relx=0.5, rely=0.5, anchor="center") - - title_label = tk.Label( - container, - text="WAKE UP!", - font=("Arial", 48, "bold"), - fg="#ff4444", - bg="#1a1a1a", - ) - title_label.pack(pady=20) - - round_label = tk.Label( - container, - text=f"Round 1 / {DISMISS_ROUNDS_REQUIRED}", - font=("Arial", 24, "bold"), - fg="#ffaa00", - bg="#1a1a1a", - ) - round_label.pack(pady=5) - - info_label = tk.Label( - container, - text=challenge.hint, - font=("Arial", 18), - fg="white", - bg="#1a1a1a", - ) - info_label.pack(pady=10) - - # Non-flash types use a smaller font because their display text is wider. - code_font_size = ( - 48 if challenge.kind in ("math", "sort", "sequence", "reverse") else 72 - ) - code_label = tk.Label( - container, - text=challenge.display, - font=("Courier", code_font_size, "bold"), - fg="#00ff00", - bg="#1a1a1a", - ) - code_label.pack(pady=30) - - entry = tk.Entry( - container, - font=("Courier", 36), - justify="center", - width=12, - ) - entry.pack(pady=10) - entry.focus_set() - entry.bind("", self._on_submit) - - status_label = tk.Label( - container, - text="", - font=("Arial", 18), - fg="#ff4444", - bg="#1a1a1a", - ) - status_label.pack(pady=10) - - timer_label = tk.Label( - container, - text="", - font=("Arial", 14), - fg="#aaaaaa", - bg="#1a1a1a", - ) - timer_label.pack(pady=5) - - return _AlarmView( - container=container, - title_label=title_label, - round_label=round_label, - info_label=info_label, - code_label=code_label, - entry=entry, - status_label=status_label, - timer_label=timer_label, - ) - def _on_submit(self, _event: object = None) -> None: """Handle challenge submission. @@ -260,7 +164,7 @@ class WakeAlarm: if self._progress.current_challenge.kind == "flash": self._view.code_label.configure( text=self._progress.current_challenge.display, - fg="#00ff00", + fg=self._view.config.success, ) self._start_flash_countdown() return @@ -271,7 +175,7 @@ class WakeAlarm: self._progress.current_challenge = _make_challenge() self._view.code_label.configure( text=self._progress.current_challenge.display, - fg="#00ff00", + fg=self._view.config.success, ) self._view.info_label.configure(text=self._progress.current_challenge.hint) self._view.entry.delete(0, tk.END) @@ -302,7 +206,7 @@ class WakeAlarm: self.root.after(1000, self._flash_tick) else: hidden = "?" * len(self._progress.current_challenge.display) - self._view.code_label.configure(text=hidden, fg="#555555") + self._view.code_label.configure(text=hidden, fg=self._view.config.muted) self._view.status_label.configure(text="Now type the code from memory!") def _dismiss_alarm(self, *, earned_skip: bool) -> None: @@ -321,14 +225,15 @@ class WakeAlarm: if earned_skip else "Alarm dismissed. No workout skip." ) - color = "#00ff00" if earned_skip else "#ffaa00" + cfg = self._view.config + color = cfg.success if earned_skip else cfg.warning tk.Label( self._view.container, text=msg, font=("Arial", 36, "bold"), fg=color, - bg="#1a1a1a", + bg=cfg.bg, ).pack(pady=30) self.root.after(3000, self._lock.close) @@ -344,7 +249,7 @@ class WakeAlarm: self._progress.current_challenge = _make_challenge() self._view.code_label.configure( text=self._progress.current_challenge.display, - fg="#00ff00", + fg=self._view.config.success, ) self._view.info_label.configure(text=self._progress.current_challenge.hint) self._view.entry.delete(0, tk.END) @@ -406,7 +311,8 @@ class WakeAlarm: """Alternate background colour every 750 ms (below seizure-risk 3 Hz).""" if not self._active: return - self.root.configure(bg="#ff0000" if self._progress.flash_on else "#1a1a1a") + cfg = self._view.config + self.root.configure(bg=cfg.danger if self._progress.flash_on else cfg.bg) self._progress.flash_on = not self._progress.flash_on self.root.after(750, self._flash_step) diff --git a/wake_alarm/_alarm_view.py b/wake_alarm/_alarm_view.py new file mode 100644 index 0000000..c64efca --- /dev/null +++ b/wake_alarm/_alarm_view.py @@ -0,0 +1,122 @@ +"""Widget construction for the wake alarm's dismiss-challenge screen.""" + +from __future__ import annotations + +from dataclasses import dataclass +import tkinter as tk +from typing import TYPE_CHECKING + +from wake_alarm._constants import DISMISS_ROUNDS_REQUIRED + +if TYPE_CHECKING: + from collections.abc import Callable + + from gatelock import LockConfig + + from wake_alarm._challenges import _Challenge + + +@dataclass +class _AlarmView: + """The Tk widgets that make up the alarm's dismiss-challenge screen.""" + + container: tk.Frame + title_label: tk.Label + round_label: tk.Label + info_label: tk.Label + code_label: tk.Label + entry: tk.Entry + status_label: tk.Label + timer_label: tk.Label + config: LockConfig + + +def _build_alarm_view( + root: tk.Tk, + challenge: _Challenge, + config: LockConfig, + on_submit: Callable[[object], None], +) -> _AlarmView: + """Build the dismiss-challenge UI and return its widgets as a view.""" + container = tk.Frame(root, bg=config.bg) + container.place(relx=0.5, rely=0.5, anchor="center") + + title_label = tk.Label( + container, + text="WAKE UP!", + font=("Arial", 48, "bold"), + fg=config.danger, + bg=config.bg, + ) + title_label.pack(pady=20) + + round_label = tk.Label( + container, + text=f"Round 1 / {DISMISS_ROUNDS_REQUIRED}", + font=("Arial", 24, "bold"), + fg=config.warning, + bg=config.bg, + ) + round_label.pack(pady=5) + + info_label = tk.Label( + container, + text=challenge.hint, + font=("Arial", 18), + fg=config.fg, + bg=config.bg, + ) + info_label.pack(pady=10) + + # Non-flash types use a smaller font because their display text is wider. + code_font_size = ( + 48 if challenge.kind in ("math", "sort", "sequence", "reverse") else 72 + ) + code_label = tk.Label( + container, + text=challenge.display, + font=("Courier", code_font_size, "bold"), + fg=config.success, + bg=config.bg, + ) + code_label.pack(pady=30) + + entry = tk.Entry( + container, + font=("Courier", 36), + justify="center", + width=12, + ) + entry.pack(pady=10) + entry.focus_set() + entry.bind("", on_submit) + + status_label = tk.Label( + container, + text="", + font=("Arial", 18), + fg=config.danger, + bg=config.bg, + ) + status_label.pack(pady=10) + + timer_label = tk.Label( + container, + text="", + font=("Arial", 14), + fg=config.muted, + bg=config.bg, + ) + timer_label.pack(pady=5) + + return _AlarmView( + container=container, + title_label=title_label, + round_label=round_label, + info_label=info_label, + code_label=code_label, + entry=entry, + status_label=status_label, + timer_label=timer_label, + config=config, + ) diff --git a/wake_alarm/tests/test_alarm.py b/wake_alarm/tests/test_alarm.py index 677232e..06f8540 100644 --- a/wake_alarm/tests/test_alarm.py +++ b/wake_alarm/tests/test_alarm.py @@ -60,6 +60,7 @@ def _block_real_tk() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, @@ -74,6 +75,7 @@ def mock_tk_module() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, diff --git a/wake_alarm/tests/test_alarm_part2.py b/wake_alarm/tests/test_alarm_part2.py index a88d8fa..bea6d8c 100644 --- a/wake_alarm/tests/test_alarm_part2.py +++ b/wake_alarm/tests/test_alarm_part2.py @@ -42,6 +42,7 @@ def _block_real_tk() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, @@ -75,6 +76,7 @@ def mock_tk_module() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, @@ -397,7 +399,7 @@ class TestScreenFlash: alarm._progress.flash_on = False alarm._flash_step() - mock_root.configure.assert_called_once_with(bg="#1a1a1a") + mock_root.configure.assert_called_once_with(bg=alarm._view.config.bg) assert alarm._progress.flash_on is True mock_root.after.assert_called_with(750, alarm._flash_step) alarm._stop_beep.set() @@ -415,7 +417,7 @@ class TestScreenFlash: alarm._progress.flash_on = True alarm._flash_step() - mock_root.configure.assert_called_once_with(bg="#ff0000") + mock_root.configure.assert_called_once_with(bg=alarm._view.config.danger) assert alarm._progress.flash_on is False mock_root.after.assert_called_with(750, alarm._flash_step) alarm._stop_beep.set() diff --git a/wake_alarm/tests/test_alarm_part3.py b/wake_alarm/tests/test_alarm_part3.py index 86484bd..77cc035 100644 --- a/wake_alarm/tests/test_alarm_part3.py +++ b/wake_alarm/tests/test_alarm_part3.py @@ -41,6 +41,7 @@ def _block_real_tk() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, @@ -74,6 +75,7 @@ def mock_tk_module() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, diff --git a/wake_alarm/tests/test_alarm_part4.py b/wake_alarm/tests/test_alarm_part4.py index 10857db..cce2435 100644 --- a/wake_alarm/tests/test_alarm_part4.py +++ b/wake_alarm/tests/test_alarm_part4.py @@ -40,6 +40,7 @@ def _block_real_tk() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value, @@ -73,6 +74,7 @@ def mock_tk_module() -> Generator[MagicMock]: mock = _make_mock_tk() with ( patch("wake_alarm._alarm.tk", mock), + patch("wake_alarm._alarm_view.tk", mock), patch( "wake_alarm._alarm.GateRoot", return_value=mock.Tk.return_value,