From 0c6f6957a8d46d8744b04357fa081aad909839aa Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Tue, 21 Jul 2026 21:45:59 +0200 Subject: [PATCH] Wire gatelock.LockConfig into every window, not just the lock overlay self._colors = LockConfig(...) is now built unconditionally in ScreenLocker.__init__ (previously only the fullscreen lock path built a local `config`), and every raw hex literal across status_view.py, _ui_flows.py, _ui_flows_relaxed.py, _sick_dialog.py, _manual_workout_dialog.py, _manual_workout_widgets.py, and _temperature_status_mixin.py now reads from it instead -- replacing ad hoc greens/oranges/reds (#00ff00, #0066cc, #ff4444, ...) with the shared success/accent/danger/warning/muted tokens. Also fixes the verify-window and status-window paths that still hardcoded #1a1a1a for the background. This was the prerequisite the on-fill fix (previous commit) actually needed: _window_setup.py/_ui_widgets.py/_heat_skip.py reference self._colors, which nothing in the codebase constructed outside the fullscreen lock path until this commit. Verified: 859/859 tests, 100% coverage. --- screen_locker/_manual_workout_dialog.py | 39 ++++---- screen_locker/_manual_workout_widgets.py | 66 +++++++------- screen_locker/_sick_dialog.py | 68 +++++++------- screen_locker/_temperature_status_mixin.py | 16 ++-- screen_locker/_ui_flows.py | 80 +++++++++++------ screen_locker/_ui_flows_relaxed.py | 38 ++++---- screen_locker/screen_lock.py | 30 ++++--- screen_locker/status_view.py | 89 +++++++++++++------ .../tests/test_manual_workout_status_view.py | 8 +- screen_locker/tests/test_status_view.py | 8 +- screen_locker/tests/test_status_view_part2.py | 4 +- screen_locker/tests/test_ui_and_timers.py | 6 +- screen_locker/tests/test_verify_workout.py | 2 +- 13 files changed, 271 insertions(+), 183 deletions(-) diff --git a/screen_locker/_manual_workout_dialog.py b/screen_locker/_manual_workout_dialog.py index feacfe5..45cde57 100644 --- a/screen_locker/_manual_workout_dialog.py +++ b/screen_locker/_manual_workout_dialog.py @@ -39,23 +39,28 @@ class ManualWorkoutDialogMixin(ManualWorkoutFormWidgetsMixin): def _show_manual_workout_form(self) -> None: """Render the manual-workout evidence form, or a budget-exhausted note.""" self.clear_container() - self._label("Log Manual Workout", color="#0088cc", pady=10) + self._label("Log Manual Workout", color=self._colors.accent, pady=10) if _manual_workout.is_budget_exhausted(self.log_file): self._text( "Manual-workout budget exhausted for this window.", - color="#ff4444", + color=self._colors.danger, + ) + self._text( + _manual_workout.budget_summary(self.log_file), + color=self._colors.muted, ) - self._text(_manual_workout.budget_summary(self.log_file), color="#888888") row = self._button_row() self._button( row, "BACK", - bg="#aa0000", + bg=self._colors.field_bg, command=self._on_manual_workout_cancelled, width=12, ).pack(side="left", padx=10) return - self._text(_manual_workout.budget_summary(self.log_file), color="#88ccff") + self._text( + _manual_workout.budget_summary(self.log_file), color=self._colors.accent + ) self._build_manual_workout_form() def _mw_scrollable_form(self) -> tk.Frame: @@ -67,11 +72,11 @@ class ManualWorkoutDialogMixin(ManualWorkoutFormWidgetsMixin): the inner form to the viewport so its two columns split the full width. Harmless on StatusWindow, whose container already fills its window. """ - outer = tk.Frame(self.container, bg="#1a1a1a") + outer = tk.Frame(self.container, bg=self._colors.bg) outer.pack(fill="both", expand=True, pady=10) - canvas = tk.Canvas(outer, bg="#1a1a1a", highlightthickness=0) + canvas = tk.Canvas(outer, bg=self._colors.bg, highlightthickness=0) scrollbar = tk.Scrollbar(outer, orient="vertical", command=canvas.yview) - form = tk.Frame(canvas, bg="#1a1a1a") + form = tk.Frame(canvas, bg=self._colors.bg) form.grid_columnconfigure(0, weight=1, uniform="mw") form.grid_columnconfigure(1, weight=1, uniform="mw") form.bind( @@ -129,8 +134,8 @@ class ManualWorkoutDialogMixin(ManualWorkoutFormWidgetsMixin): # Both sport sub-frames occupy the same full-width grid slot; only the # selected one is shown (see _on_mw_sport_changed). act_row = self._mw_next_full_row(form) - self._mw_tt_frame = tk.Frame(form, bg="#1a1a1a") - self._mw_other_frame = tk.Frame(form, bg="#1a1a1a") + self._mw_tt_frame = tk.Frame(form, bg=self._colors.bg) + self._mw_other_frame = tk.Frame(form, bg=self._colors.bg) for sport_frame in (self._mw_tt_frame, self._mw_other_frame): sport_frame.grid_columnconfigure(0, weight=1, uniform="mwact") sport_frame.grid_columnconfigure(1, weight=1, uniform="mwact") @@ -168,19 +173,19 @@ class ManualWorkoutDialogMixin(ManualWorkoutFormWidgetsMixin): f"(min {MANUAL_WORKOUT_REFLECTION_MIN_CHARS} chars):", ) - self._mw_error_label = self._text("", color="#ff4444", pady=5) + self._mw_error_label = self._text("", color=self._colors.danger, pady=8) button_row = self._button_row() self._button( button_row, "SUBMIT", - bg="#0066cc", + bg=self._colors.accent, command=self._submit_manual_workout_form, width=12, ).pack(side="left", padx=10) self._button( button_row, "BACK", - bg="#aa0000", + bg=self._colors.field_bg, command=self._on_manual_workout_cancelled, width=12, ).pack(side="left", padx=10) @@ -190,13 +195,13 @@ class ManualWorkoutDialogMixin(ManualWorkoutFormWidgetsMixin): self._mw_sport_var = tk.StringVar( value=_manual_workout.SPORT_LABELS[_manual_workout.SPORT_TABLE_TENNIS] ) - row = tk.Frame(parent, bg="#1a1a1a") + row = tk.Frame(parent, bg=self._colors.bg) tk.Label( row, text="Sport:", - font=("Arial", 16), - fg="white", - bg="#1a1a1a", + font=(self._colors.font_family, 16), + fg=self._colors.fg, + bg=self._colors.bg, ).pack(side="left", padx=5) tk.OptionMenu( row, diff --git a/screen_locker/_manual_workout_widgets.py b/screen_locker/_manual_workout_widgets.py index 5526cfa..c3f5a61 100644 --- a/screen_locker/_manual_workout_widgets.py +++ b/screen_locker/_manual_workout_widgets.py @@ -44,7 +44,7 @@ class ManualWorkoutFormWidgetsMixin: columnspan=2 if full else 1, sticky="ew", padx=12, - pady=6, + pady=8, ) counters[id(parent)] = idx + (2 if full else 1) @@ -67,9 +67,9 @@ class ManualWorkoutFormWidgetsMixin: label = tk.Label( parent, text=title, - font=("Arial", 20, "bold"), - fg="#88ccff", - bg="#1a1a1a", + font=(self._colors.font_family, 20, "bold"), + fg=self._colors.accent, + bg=self._colors.bg, anchor="w", ) self._mw_grid(parent, label, full=True) @@ -82,24 +82,24 @@ class ManualWorkoutFormWidgetsMixin: the two-column grid. Paste stays disabled via ``disable_paste``. """ var = tk.StringVar() - cell = tk.Frame(parent, bg="#1a1a1a") + cell = tk.Frame(parent, bg=self._colors.bg) tk.Label( cell, text=label, - font=("Arial", 16), - fg="white", - bg="#1a1a1a", + font=(self._colors.font_family, 16), + fg=self._colors.fg, + bg=self._colors.bg, anchor="w", ).pack(fill="x") entry = tk.Entry( cell, textvariable=var, - font=("Arial", 18), - bg="#2a2a2a", - fg="white", - insertbackground="white", + font=(self._colors.font_family, 18), + bg=self._colors.field_bg, + fg=self._colors.fg, + insertbackground=self._colors.fg, ) - entry.pack(fill="x", pady=2) + entry.pack(fill="x", pady=4) disable_paste(entry) self._mw_grid(parent, cell) return var @@ -109,13 +109,13 @@ class ManualWorkoutFormWidgetsMixin: ) -> tk.IntVar: """Add a half-width label + numeric Spinbox and return its IntVar.""" var = tk.IntVar(value=0) - row = tk.Frame(parent, bg="#1a1a1a") + row = tk.Frame(parent, bg=self._colors.bg) tk.Label( row, text=label, - font=("Arial", 16), - fg="white", - bg="#1a1a1a", + font=(self._colors.font_family, 16), + fg=self._colors.fg, + bg=self._colors.bg, ).pack(side="left", padx=5) tk.Spinbox( row, @@ -123,47 +123,47 @@ class ManualWorkoutFormWidgetsMixin: to=to, textvariable=var, width=4, - font=("Arial", 16), + font=(self._colors.font_family, 16), ).pack(side="left", padx=5) self._mw_grid(parent, row) return var def _mw_textbox(self, parent: tk.Widget, label: str) -> tk.Text: """Add a full-width label + multi-line Text cell and return the widget.""" - cell = tk.Frame(parent, bg="#1a1a1a") + cell = tk.Frame(parent, bg=self._colors.bg) tk.Label( cell, text=label, - font=("Arial", 16), - fg="white", - bg="#1a1a1a", + font=(self._colors.font_family, 16), + fg=self._colors.fg, + bg=self._colors.bg, anchor="w", - ).pack(fill="x", pady=(5, 0)) + ).pack(fill="x", pady=(8, 0)) text_widget = tk.Text( cell, height=3, - font=("Arial", 14), - bg="#2a2a2a", - fg="white", - insertbackground="white", + font=(self._colors.font_family, 14), + bg=self._colors.field_bg, + fg=self._colors.fg, + insertbackground=self._colors.fg, ) - text_widget.pack(pady=2, fill="x") + text_widget.pack(pady=4, fill="x") disable_paste(text_widget) self._mw_grid(parent, cell, full=True) return text_widget def _mw_rpe_row(self, parent: tk.Widget) -> None: """Add the RPE (rate of perceived exertion) spinbox row.""" - row = tk.Frame(parent, bg="#1a1a1a") + row = tk.Frame(parent, bg=self._colors.bg) tk.Label( row, text=( f"RPE — perceived exertion " f"({MANUAL_WORKOUT_RPE_MIN}-{MANUAL_WORKOUT_RPE_MAX}):" ), - font=("Arial", 16), - fg="white", - bg="#1a1a1a", + font=(self._colors.font_family, 16), + fg=self._colors.fg, + bg=self._colors.bg, ).pack(side="left", padx=5) tk.Spinbox( row, @@ -171,6 +171,6 @@ class ManualWorkoutFormWidgetsMixin: to=MANUAL_WORKOUT_RPE_MAX, textvariable=self._mw_rpe_var, width=4, - font=("Arial", 16), + font=(self._colors.font_family, 16), ).pack(side="left", padx=5) self._mw_grid(parent, row) diff --git a/screen_locker/_sick_dialog.py b/screen_locker/_sick_dialog.py index 7dcb162..eb13157 100644 --- a/screen_locker/_sick_dialog.py +++ b/screen_locker/_sick_dialog.py @@ -34,32 +34,34 @@ class SickDialogMixin: history = _sick_tracker.load_history() self._sick_history_cache: SickHistory = history self.clear_container() - self._label("Sick Day Request", color="#cc6600", pady=10) - self._text(_sick_tracker.budget_summary(history), color="#ffaa00") + self._label("Sick Day Request", color=self._colors.warning, pady=10) + self._text(_sick_tracker.budget_summary(history), color=self._colors.warning) recent = _sick_tracker.format_recent_justifications(history) if recent: - self._text("Recent sick days:", font_size=14, color="#888888", pady=5) - self._text(recent, font_size=14, color="#cccccc", pady=5) + self._text( + "Recent sick days:", font_size=14, color=self._colors.muted, pady=8 + ) + self._text(recent, font_size=14, color=self._colors.muted, pady=8) had_commitment = _sick_tracker.had_commitment_for_today(history) if had_commitment: self._text( "⚠ Yesterday you committed to working out today.", font_size=18, - color="#ff6666", + color=self._colors.danger, ) self._text( "Breaking the commitment costs 2 sick-budget days.", font_size=14, - color="#ff6666", + color=self._colors.danger, ) self._build_justification_form(had_commitment=had_commitment) def _build_justification_form(self, *, had_commitment: bool) -> None: """Add justification form fields and submit button to the container.""" - form = tk.Frame(self.container, bg="#1a1a1a") + form = tk.Frame(self.container, bg=self._colors.bg) form.pack(pady=10) self._sick_symptom_var = tk.StringVar() @@ -67,13 +69,16 @@ class SickDialogMixin: self._sick_severity_var = tk.IntVar(value=5) self._sick_text_widget = self._add_form_widgets(form) - self._sick_error_label = self._text("", color="#ff4444", pady=5) + self._sick_error_label = self._text("", color=self._colors.danger, pady=8) button_row = self._button_row() + # Starts disabled during the forced-read delay -- field_bg (our + # neutral/secondary token) signals "not yet actionable" better than + # the accent used for an immediately-clickable submit. self._sick_submit_button = self._button( button_row, "SUBMIT", - bg="#666666", + bg=self._colors.field_bg, command=self._submit_sick_justification, width=12, ) @@ -81,7 +86,7 @@ class SickDialogMixin: self._button( button_row, "BACK", - bg="#aa0000", + bg=self._colors.field_bg, command=self._start_phone_check, width=12, ).pack(side="left", padx=10) @@ -103,14 +108,14 @@ class SickDialogMixin: label="When did it start? (e.g. last night):", variable=self._sick_onset_var, ) - sev_row = tk.Frame(parent, bg="#1a1a1a") - sev_row.pack(pady=5) + sev_row = tk.Frame(parent, bg=self._colors.bg) + sev_row.pack(pady=8) tk.Label( sev_row, text="Severity (1-10):", - font=("Arial", 14), - fg="white", - bg="#1a1a1a", + font=(self._colors.font_family, 14), + fg=self._colors.fg, + bg=self._colors.bg, ).pack(side="left", padx=5) tk.Spinbox( sev_row, @@ -118,26 +123,26 @@ class SickDialogMixin: to=10, textvariable=self._sick_severity_var, width=4, - font=("Arial", 14), + font=(self._colors.font_family, 14), ).pack(side="left", padx=5) tk.Label( parent, text=(f"Describe how you feel (min {SICK_JUSTIFICATION_MIN_CHARS} chars):"), - font=("Arial", 14), - fg="white", - bg="#1a1a1a", - ).pack(pady=5) + font=(self._colors.font_family, 14), + fg=self._colors.fg, + bg=self._colors.bg, + ).pack(pady=8) text_widget = tk.Text( parent, width=60, height=6, - font=("Arial", 12), - bg="#2a2a2a", - fg="white", - insertbackground="white", + font=(self._colors.font_family, 14), + bg=self._colors.field_bg, + fg=self._colors.fg, + insertbackground=self._colors.fg, ) - text_widget.pack(pady=5) + text_widget.pack(pady=8) _disable_paste(text_widget) return text_widget @@ -199,32 +204,35 @@ class SickDialogMixin: self._label( "Commit to working out tomorrow?", font_size=32, - color="#ffaa00", + color=self._colors.warning, pady=20, ) - self._text( + # ~80 chars at font_size 16 would render as one unbroken line past the + # ~70-char readable range (rule 21) -- wrap it explicitly. + prompt = self._text( "If you say YES and skip via 'I'm sick' tomorrow, " "the sick day costs 2x normal.", font_size=16, ) + prompt.config(wraplength=560, justify="center") self._commitment_done_fn = on_done self._commitment_remaining = COMMITMENT_PROMPT_TIMEOUT_SECONDS self._commitment_timer_label = self._text( f"Auto-skipping in {COMMITMENT_PROMPT_TIMEOUT_SECONDS}s", - color="#888888", + color=self._colors.muted, ) row = self._button_row() self._button( row, "YES", - bg="#00aa00", + bg=self._colors.success, command=lambda: self._answer_commitment(commit=True), width=12, ).pack(side="left", padx=10) self._button( row, "NO", - bg="#aa0000", + bg=self._colors.field_bg, command=lambda: self._answer_commitment(commit=False), width=12, ).pack(side="left", padx=10) diff --git a/screen_locker/_temperature_status_mixin.py b/screen_locker/_temperature_status_mixin.py index bc2f0ad..455b244 100644 --- a/screen_locker/_temperature_status_mixin.py +++ b/screen_locker/_temperature_status_mixin.py @@ -40,21 +40,23 @@ class TemperatureStatusMixin: self._label("Warsaw Temperature", font_size=16, pady=8) result = self._temp_result if result is None: - self._text("Checking Warsaw temperature…", font_size=12, color="#888888") + self._text( + "Checking Warsaw temperature…", font_size=12, color=self._colors.muted + ) return if result.timed_out: self._text( f"Warsaw temperature check timed out after ~" f"{int(HARD_TIMEOUT_SECONDS)}s — network may be down.", font_size=12, - color="#ff8844", + color=self._colors.warning, ) return if result.temp_celsius is None: self._text( "Warsaw temperature check failed (network/API error).", font_size=12, - color="#ff8844", + color=self._colors.warning, ) return hot = result.temp_celsius >= HEAT_SKIP_TEMP_THRESHOLD @@ -62,10 +64,14 @@ class TemperatureStatusMixin: f"Warsaw: {result.temp_celsius:.0f}°C (heat-skip threshold " f"{HEAT_SKIP_TEMP_THRESHOLD}°C)", font_size=12, - color="#ff8844" if hot else "#cccccc", + color=self._colors.warning if hot else self._colors.muted, ) if hot: - self._text("Would trigger heat-skip today.", font_size=11, color="#ff4444") + self._text( + "Would trigger heat-skip today.", + font_size=11, + color=self._colors.danger, + ) def _start_temperature_check(self) -> None: """Submit a background Warsaw-temperature fetch and start polling it. diff --git a/screen_locker/_ui_flows.py b/screen_locker/_ui_flows.py index 8eec6da..07ae997 100644 --- a/screen_locker/_ui_flows.py +++ b/screen_locker/_ui_flows.py @@ -22,7 +22,9 @@ class UIFlowsMixin: def _start_phone_check(self) -> None: """Check phone for today's workout immediately at startup.""" self.clear_container() - self._label("Checking phone...", font_size=36, color="#ffaa00", pady=30) + self._label( + "Checking phone...", font_size=36, color=self._colors.warning, pady=30 + ) self._text("Looking for today's workout in StrongLifts...", font_size=18) executor = ThreadPoolExecutor(max_workers=1) self._phone_future = executor.submit(self._verify_phone_workout) @@ -40,42 +42,46 @@ class UIFlowsMixin: def _show_retry_and_sick(self, message: str) -> None: """Show TRY AGAIN and (if budget allows) I'm sick after a failed check.""" self.clear_container() - self._label("No Workout Found", font_size=36, color="#ff4444", pady=20) - self._text(message, color="#ffaa00") + self._label( + "No Workout Found", font_size=36, color=self._colors.danger, pady=20 + ) + self._text(message, color=self._colors.warning) history = _sick_tracker.load_history() - self._text(_sick_tracker.budget_summary(history), color="#888888") - self._text(_manual_workout.budget_summary(self.log_file), color="#888888") + self._text(_sick_tracker.budget_summary(history), color=self._colors.muted) + self._text( + _manual_workout.budget_summary(self.log_file), color=self._colors.muted + ) frame = self._button_row() self._button( frame, "TRY AGAIN", - bg="#0066cc", + bg=self._colors.accent, command=self._start_phone_check, width=12, ).pack(side="left", padx=10) if _sick_tracker.is_budget_exhausted(history): self._text( "Sick budget exhausted. No 'I'm sick' option available.", - color="#ff6666", + color=self._colors.danger, ) else: self._button( frame, "I'm sick", - bg="#cc6600", + bg=self._colors.warning, command=self.ask_if_sick, width=12, ).pack(side="left", padx=10) if _manual_workout.is_budget_exhausted(self.log_file): self._text( "Manual-workout budget exhausted. No manual-log option available.", - color="#ff6666", + color=self._colors.danger, ) else: self._button( frame, "Log Manual Workout", - bg="#0088cc", + bg=self._colors.accent, command=self._show_manual_workout_form, width=16, ).pack(side="left", padx=10) @@ -84,9 +90,14 @@ class UIFlowsMixin: """Show confirmation and unlock after a manual-workout entry is built.""" self.workout_data = entry self.clear_container() - self._label("✓ Manual Workout Logged!", font_size=42, color="#00cc44", pady=30) - self._text(entry.get("source", ""), font_size=20, color="#aaffaa") - self._text("Unlocking...", font_size=18, color="#888888") + self._label( + "✓ Manual Workout Logged!", + font_size=42, + color=self._colors.success, + pady=30, + ) + self._text(entry.get("source", ""), font_size=20, color=self._colors.success) + self._text("Unlocking...", font_size=18, color=self._colors.muted) unlock_delay = 1500 if self.demo_mode else 2000 self.root.after(unlock_delay, self.unlock_screen) @@ -100,9 +111,14 @@ class UIFlowsMixin: self.workout_data["type"] = "phone_verified" self.workout_data["source"] = message self.clear_container() - self._label("✓ Workout Verified!", font_size=42, color="#00cc44", pady=30) - self._text(message, font_size=20, color="#aaffaa") - self._text("Unlocking...", font_size=18, color="#888888") + self._label( + "✓ Workout Verified!", + font_size=42, + color=self._colors.success, + pady=30, + ) + self._text(message, font_size=20, color=self._colors.success) + self._text("Unlocking...", font_size=18, color=self._colors.muted) unlock_delay = 1500 if self.demo_mode else 2000 self.root.after(unlock_delay, self.unlock_screen) elif status == "too_short": @@ -138,7 +154,9 @@ class UIFlowsMixin: either unlocks (run verified) or calls ``on_failure``. """ self.clear_container() - self._label("Checking RunnerUp...", font_size=36, color="#ffaa00", pady=30) + self._label( + "Checking RunnerUp...", font_size=36, color=self._colors.warning, pady=30 + ) self._text("Looking for today's run in RunnerUp...", font_size=18) executor = ThreadPoolExecutor(max_workers=1) self._runnerup_future = executor.submit(self._verify_runnerup_workout) @@ -154,9 +172,14 @@ class UIFlowsMixin: self.workout_data["type"] = "runnerup_verified" self.workout_data["source"] = message self.clear_container() - self._label("✓ Run Verified!", font_size=42, color="#00cc44", pady=30) - self._text(message, font_size=20, color="#aaffaa") - self._text("Unlocking...", font_size=18, color="#888888") + self._label( + "✓ Run Verified!", + font_size=42, + color=self._colors.success, + pady=30, + ) + self._text(message, font_size=20, color=self._colors.success) + self._text("Unlocking...", font_size=18, color=self._colors.muted) unlock_delay = 1500 if self.demo_mode else 2000 self.root.after(unlock_delay, self.unlock_screen) else: @@ -171,12 +194,15 @@ class UIFlowsMixin: def _get_sick_day_status(self) -> tuple[str, str]: """Determine sick day status text and color.""" if self._sick_mode_used_today(): - return "Shutdown time already adjusted today", "#ffaa00" + return "Shutdown time already adjusted today", self._colors.warning if self._adjust_shutdown_time_earlier(): return ( "Shutdown time moved 1.5 hours earlier ✓\n(Will revert tomorrow)" - ), "#00aa00" - return "Could not adjust shutdown time (check permissions)", "#ff4444" + ), self._colors.success + return ( + "Could not adjust shutdown time (check permissions)", + self._colors.danger, + ) def _proceed_to_sick_countdown(self) -> None: """Start the (escalated) sick day countdown after justification.""" @@ -202,7 +228,7 @@ class UIFlowsMixin: countdown: int, ) -> None: """Display sick day UI labels and countdown.""" - self._label("Sick Day Mode", color="#cc6600", pady=20) + self._label("Sick Day Mode", color=self._colors.warning, pady=20) self._text(status_text, color=status_color) minutes = countdown // 60 self._text( @@ -250,7 +276,7 @@ class UIFlowsMixin: self.lockout_label = self._label( f"Go work out!\nLocked for {self.lockout_time} seconds", font_size=48, - color="#ff4444", + color=self._colors.danger, pady=30, ) self.countdown_label = self._label( @@ -298,10 +324,10 @@ class UIFlowsMixin: self._label( "Cannot Verify Workout", font_size=36, - color="#ff8800", + color=self._colors.warning, pady=20, ) - self._text(message, color="#ffaa00") + self._text(message, color=self._colors.warning) self._text( "Connect phone via ADB to skip this wait,\n" "or wait for the penalty timer.\n\n" diff --git a/screen_locker/_ui_flows_relaxed.py b/screen_locker/_ui_flows_relaxed.py index 511c0d5..d0d0185 100644 --- a/screen_locker/_ui_flows_relaxed.py +++ b/screen_locker/_ui_flows_relaxed.py @@ -23,7 +23,7 @@ class UIFlowsRelaxedMixin: self._label( "Verifying Workout", font_size=36, - color="#ffaa00", + color=self._colors.warning, pady=30, ) self._text( @@ -59,15 +59,15 @@ class UIFlowsRelaxedMixin: self._label( "✓ Workout Verified!", font_size=42, - color="#00cc44", + color=self._colors.success, pady=30, ) - self._text(message, font_size=20, color="#aaffaa") + self._text(message, font_size=20, color=self._colors.success) if adjusted: self._text( "Shutdown time moved later!", font_size=20, - color="#ffaa00", + color=self._colors.warning, ) self.root.after(2000, self.close) else: @@ -79,22 +79,22 @@ class UIFlowsRelaxedMixin: self._label( "Workout Not Found", font_size=36, - color="#ff4444", + color=self._colors.danger, pady=20, ) - self._text(message, color="#ffaa00") + self._text(message, color=self._colors.warning) frame = self._button_row() self._button( frame, "TRY AGAIN", - bg="#0066cc", + bg=self._colors.accent, command=self._start_verify_workout_check, width=12, ).pack(side="left", padx=10) self._button( frame, "Close", - bg="#aa0000", + bg=self._colors.field_bg, command=self.close, width=12, ).pack(side="left", padx=10) @@ -114,28 +114,28 @@ class UIFlowsRelaxedMixin: self._label( "Optional Day (Tue / Wed / Thu)", font_size=30, - color="#ffaa00", + color=self._colors.warning, pady=20, ) self._text( f"Weekly workouts: {count} / {WEEKLY_WORKOUT_MINIMUM}\n" "No penalty for skipping today.", font_size=20, - color="#aaaaaa", + color=self._colors.muted, pady=10, ) frame = self._button_row() self._button( frame, "Skip — No Penalty", - bg="#006600", + bg=self._colors.success, command=self.close, width=18, ).pack(side="left", padx=10) self._button( frame, "Log Stronglift Workout", - bg="#0066cc", + bg=self._colors.accent, command=self._start_relaxed_phone_check, width=20, ).pack(side="left", padx=10) @@ -143,7 +143,9 @@ class UIFlowsRelaxedMixin: def _start_relaxed_phone_check(self) -> None: """Run Stronglift check in relaxed mode (no screen grab, no sick option).""" self.clear_container() - self._label("Checking phone...", font_size=36, color="#ffaa00", pady=30) + self._label( + "Checking phone...", font_size=36, color=self._colors.warning, pady=30 + ) self._text("Looking for today's workout in StrongLifts...", font_size=18) executor = ThreadPoolExecutor(max_workers=1) self._phone_future = executor.submit(self._verify_phone_workout) @@ -175,20 +177,22 @@ class UIFlowsRelaxedMixin: def _show_relaxed_retry(self, message: str, status: str) -> None: """Show retry and skip-close when workout not found in relaxed mode.""" self.clear_container() - self._label("No Workout Found", font_size=36, color="#ff4444", pady=20) - self._text(f"❌ {message}\n\nReason: {status}", color="#ffaa00") + self._label( + "No Workout Found", font_size=36, color=self._colors.danger, pady=20 + ) + self._text(f"❌ {message}\n\nReason: {status}", color=self._colors.warning) frame = self._button_row() self._button( frame, "TRY AGAIN", - bg="#0066cc", + bg=self._colors.accent, command=self._start_relaxed_phone_check, width=12, ).pack(side="left", padx=10) self._button( frame, "Close (Skip)", - bg="#006600", + bg=self._colors.success, command=self.close, width=14, ).pack(side="left", padx=10) diff --git a/screen_locker/screen_lock.py b/screen_locker/screen_lock.py index 6802d71..0d80def 100755 --- a/screen_locker/screen_lock.py +++ b/screen_locker/screen_lock.py @@ -140,21 +140,25 @@ class ScreenLocker( self.demo_mode = demo_mode self.lockout_time = 10 if demo_mode else 1800 self._lock: LockWindow | None = None + # One shared token source for every window/widget this app creates, + # lock or not -- see the `unified-design-system` skill. `mode`/`grab`/ + # `disable_vt` only matter for the fullscreen lock window itself; the + # color/font fields apply everywhere, so this is built unconditionally. + self._colors = LockConfig( + mode="hard", + grab="local" if demo_mode else "global", + disable_vt=not demo_mode, + ) if verify_only: self._setup_verify_window() elif self._relaxed_day_mode: self._setup_relaxed_day_window() else: - config = LockConfig( - mode="hard", - grab="local" if demo_mode else "global", - disable_vt=not demo_mode, - ) - self._lock = LockWindow(self.root, config, hooks=self) + self._lock = LockWindow(self.root, self._colors, hooks=self) self._lock.setup() if demo_mode: self._setup_demo_close_button() - self.container = tk.Frame(self.root, bg="#1a1a1a") + self.container = tk.Frame(self.root, bg=self._colors.bg) self.container.place(relx=0.5, rely=0.5, anchor="center") self._phone_future: Future[tuple[str, str]] | None = None self._runnerup_future: Future[tuple[str, str]] | None = None @@ -311,31 +315,33 @@ class ScreenLocker( credit = self._apply_workout_credit() self.clear_container() - self._label("Great job! 💪", font_size=48, color="#00ff00", pady=30) + self._label("Great job! 💪", font_size=48, color=self._colors.success, pady=30) if credit.shutdown_adjusted: self._text( "Shutdown time +2h later! 🎁", font_size=24, - color="#ffaa00", + color=self._colors.warning, ) if credit.extra_bonus_delta > 0: self._text( f"Extra workout today! +{credit.extra_bonus_delta}h tonight", font_size=20, - color="#ffaa00", + color=self._colors.warning, ) if credit.new_debt is not None: self._text( f"Workout debt: {credit.new_debt}", font_size=20, - color="#ffaa00" if credit.new_debt > 0 else "#888888", + color=self._colors.warning + if credit.new_debt > 0 + else self._colors.muted, ) streak = current_streak(EXTRA_BENEFITS_FILE) if streak >= 1: self._text( f"🔥 {streak}-week streak (5+ workouts each)", font_size=14, - color="#888888", + color=self._colors.muted, ) self._text("Screen Unlocked!", font_size=36, pady=20) if self.workout_data.get("type") in ( diff --git a/screen_locker/status_view.py b/screen_locker/status_view.py index 64c6c61..1e358b0 100644 --- a/screen_locker/status_view.py +++ b/screen_locker/status_view.py @@ -28,6 +28,8 @@ import sys import tkinter as tk from typing import TYPE_CHECKING +from gatelock import LockConfig + from screen_locker import _manual_workout from screen_locker._manual_workout_dialog import ManualWorkoutDialogMixin from screen_locker._status_data import ( @@ -56,6 +58,7 @@ if TYPE_CHECKING: from screen_locker.screen_lock import ScreenLocker _DEFAULT_LOG_FILE = Path(__file__).resolve().parent / "workout_log.json" +_STATUS_COLORS = LockConfig() class StatusWindow( @@ -96,7 +99,8 @@ class StatusWindow( self._temp_future: Future[TemperatureCheck] | None = None self._temp_result: TemperatureCheck | None = None self._last_snapshot = snapshot - self.container = tk.Frame(root, bg="#1a1a1a") + self._colors = _STATUS_COLORS + self.container = tk.Frame(root, bg=self._colors.bg) self.container.pack(fill="both", expand=True) self._start_temperature_check() self.render(snapshot) @@ -117,15 +121,20 @@ class StatusWindow( self._section_shutdown(self.container, snapshot.shutdown) if self._phone_check_result is not None: status, message = self._phone_check_result - color = "#00cc44" if status == "verified" else "#ff8844" - self._text(f"Phone check ({status}): {message}", font_size=13, color=color) + color = ( + self._colors.success if status == "verified" else self._colors.warning + ) + self._text(f"Phone check ({status}): {message}", font_size=16, color=color) if self._credit_message is not None: - self._text(self._credit_message, font_size=13, color="#00cc44") + self._text(self._credit_message, font_size=16, color=self._colors.success) + # "Check Phone"/"Log Manual Workout" are the primary write actions + # (accent, high contrast per rule 3); "Refresh"/"Close" are secondary + # utility actions (muted) -- two tiers instead of four arbitrary hues. frame = self._button_row() self._button( frame, "Check Phone", - bg="#0066cc", + bg=self._colors.accent, command=self._on_check_phone_clicked, width=14, ).pack(side="left", padx=8) @@ -133,15 +142,23 @@ class StatusWindow( self._button( frame, "Log Manual Workout", - bg="#0088cc", + bg=self._colors.accent, command=self._show_manual_workout_form, width=16, ).pack(side="left", padx=8) self._button( - frame, "Refresh", bg="#006600", command=self._on_refresh_clicked, width=10 + frame, + "Refresh", + bg=self._colors.field_bg, + command=self._on_refresh_clicked, + width=10, ).pack(side="left", padx=8) self._button( - frame, "Close", bg="#aa0000", command=self.root.destroy, width=8 + frame, + "Close", + bg=self._colors.field_bg, + command=self.root.destroy, + width=8, ).pack(side="left", padx=8) def _section_today(self, parent: tk.Widget, day: DayStatus) -> None: @@ -154,10 +171,12 @@ class StatusWindow( self._text( f"{mark} Today ({day.label}): {entry_str}", font_size=18, - color="#aaffaa" if day.counted else "#ffaa00", + color=self._colors.success if day.counted else self._colors.warning, ) if day.source: - self._text(day.source, font_size=12, color="#888888", pady=2) + # Secondary provenance note on an already-shown entry -- a + # deliberately caption-sized exception to the 16px floor. + self._text(day.source, font_size=12, color=self._colors.muted, pady=4) def _section_week(self, parent: tk.Widget, week: WeeklySummary) -> None: """Render this ISO week's per-day breakdown and totals.""" @@ -172,17 +191,21 @@ class StatusWindow( ) self._text( f"{mark} {day.label}: {entry_str}", - font_size=12, - color="#cccccc", - pady=1, + font_size=16, + color=self._colors.muted, + pady=4, ) if week.remaining > 0: self._text( - f"Need {week.remaining} more this week.", font_size=13, color="#ffaa00" + f"Need {week.remaining} more this week.", + font_size=16, + color=self._colors.warning, ) elif week.extra > 0: self._text( - f"{week.extra} above the weekly minimum!", font_size=13, color="#00cc44" + f"{week.extra} above the weekly minimum!", + font_size=16, + color=self._colors.success, ) def _section_lock_explanation( @@ -192,13 +215,15 @@ class StatusWindow( del parent self._label("Why the lock did/didn't fire", font_size=16, pady=8) self._text( - expl.reason, font_size=13, color="#ff4444" if expl.fired else "#00cc44" + expl.reason, + font_size=16, + color=self._colors.danger if expl.fired else self._colors.success, ) if expl.auto_upgrade.would_attempt: self._text( f"Pending auto-upgrade: {expl.auto_upgrade.reason}", - font_size=11, - color="#ffaa00", + font_size=14, + color=self._colors.warning, ) def _section_sick_budget(self, parent: tk.Widget, sick: SickBudgetStatus) -> None: @@ -208,8 +233,8 @@ class StatusWindow( self._text( f"{sick.used_7d}/{sick.budget_7d} week · {sick.used_30d}/{sick.budget_30d} " f"month · {sick.used_90d}/{sick.budget_90d} quarter · debt {sick.debt}", - font_size=12, - color="#ff4444" if sick.exhausted else "#cccccc", + font_size=16, + color=self._colors.danger if sick.exhausted else self._colors.muted, ) def _section_manual_workout_budget( @@ -221,8 +246,8 @@ class StatusWindow( self._text( f"{manual.used_7d}/{manual.budget_7d} week · " f"{manual.used_30d}/{manual.budget_30d} month", - font_size=12, - color="#ff4444" if manual.exhausted else "#cccccc", + font_size=16, + color=self._colors.danger if manual.exhausted else self._colors.muted, ) def _section_shutdown( @@ -236,23 +261,31 @@ class StatusWindow( self._text( f"Live config — Mon-Wed {mon_wed_hour:02d}:00, " f"Thu-Sun {thu_sun_hour:02d}:00", - font_size=12, + font_size=16, ) else: self._text( - "Live shutdown config unavailable.", font_size=12, color="#ff8844" + "Live shutdown config unavailable.", + font_size=16, + color=self._colors.warning, ) + # Rest-of-week/next-week/explanation are speculative annotations, not + # the section's primary content -- a deliberate caption-sized + # exception to the 16px floor, all sharing one muted tone instead of + # three unrelated ad hoc grays. rest_line = ", ".join( f"{d.label} {d.hour:02d}:00" for d in shutdown.rest_of_week ) - self._text(f"Rest of week: {rest_line}", font_size=10, color="#cccccc") + self._text(f"Rest of week: {rest_line}", font_size=12, color=self._colors.muted) next_line = ", ".join( f"{d.label} {d.hour:02d}:00" for d in shutdown.next_week_preview ) self._text( - f"Next week (speculative): {next_line}", font_size=10, color="#888888" + f"Next week (speculative): {next_line}", + font_size=12, + color=self._colors.muted, ) - self._text(shutdown.explanation, font_size=10, color="#666666") + self._text(shutdown.explanation, font_size=12, color=self._colors.muted) def _on_refresh_clicked(self) -> None: """Clear any stale phone-check/credit/temperature results, re-check both.""" @@ -314,7 +347,7 @@ def main(argv: list[str] | None = None) -> None: root = tk.Tk() root.title("Workout Status") - root.configure(bg="#1a1a1a") + root.configure(bg=_STATUS_COLORS.bg) root.minsize(560, 200) def refresh() -> None: diff --git a/screen_locker/tests/test_manual_workout_status_view.py b/screen_locker/tests/test_manual_workout_status_view.py index 5eb8d7e..ba08568 100644 --- a/screen_locker/tests/test_manual_workout_status_view.py +++ b/screen_locker/tests/test_manual_workout_status_view.py @@ -26,25 +26,25 @@ class TestSectionManualWorkoutBudget: snap = _snapshot( manual_workout_budget=_manual_workout_budget(used_7d=2, exhausted=True) ) - _make_window(mock_tk, snap) + window = _make_window(mock_tk, snap) calls = [ c for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == "#ff4444" for c in calls) + assert any(c.kwargs.get("fg") == window._colors.danger for c in calls) def test_not_exhausted_uses_normal_color(self, mock_tk: MagicMock) -> None: snap = _snapshot( manual_workout_budget=_manual_workout_budget(used_7d=0, exhausted=False) ) - _make_window(mock_tk, snap) + window = _make_window(mock_tk, snap) calls = [ c for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == "#cccccc" for c in calls) + assert any(c.kwargs.get("fg") == window._colors.muted for c in calls) class TestManualWorkoutButtonVisibility: diff --git a/screen_locker/tests/test_status_view.py b/screen_locker/tests/test_status_view.py index abf5e47..1c0d300 100644 --- a/screen_locker/tests/test_status_view.py +++ b/screen_locker/tests/test_status_view.py @@ -145,23 +145,23 @@ class TestSectionLockExplanation: class TestSectionSickBudget: def test_exhausted_uses_warning_color(self, mock_tk: MagicMock) -> None: snap = _snapshot(sick_budget=_sick_budget(used_7d=1, exhausted=True)) - _make_window(mock_tk, snap) + window = _make_window(mock_tk, snap) calls = [ c for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == "#ff4444" for c in calls) + assert any(c.kwargs.get("fg") == window._colors.danger for c in calls) def test_not_exhausted_uses_normal_color(self, mock_tk: MagicMock) -> None: snap = _snapshot(sick_budget=_sick_budget(used_7d=0, exhausted=False)) - _make_window(mock_tk, snap) + window = _make_window(mock_tk, snap) calls = [ c for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == "#cccccc" for c in calls) + assert any(c.kwargs.get("fg") == window._colors.muted for c in calls) class TestSectionShutdown: diff --git a/screen_locker/tests/test_status_view_part2.py b/screen_locker/tests/test_status_view_part2.py index 0e6f64d..6016e86 100644 --- a/screen_locker/tests/test_status_view_part2.py +++ b/screen_locker/tests/test_status_view_part2.py @@ -46,7 +46,7 @@ class TestPhoneCheckResultRendering: if "Phone check" in c.kwargs.get("text", "") ] assert calls - assert calls[-1].kwargs.get("fg") == "#00cc44" + assert calls[-1].kwargs.get("fg") == window._colors.success def test_non_verified_result_shown_in_orange(self, mock_tk: MagicMock) -> None: window = _make_window(mock_tk, _snapshot()) @@ -58,7 +58,7 @@ class TestPhoneCheckResultRendering: if "Phone check" in c.kwargs.get("text", "") ] assert calls - assert calls[-1].kwargs.get("fg") == "#ff8844" + assert calls[-1].kwargs.get("fg") == window._colors.warning class TestButtons: diff --git a/screen_locker/tests/test_ui_and_timers.py b/screen_locker/tests/test_ui_and_timers.py index 3e385f1..ea9feca 100644 --- a/screen_locker/tests/test_ui_and_timers.py +++ b/screen_locker/tests/test_ui_and_timers.py @@ -144,7 +144,7 @@ class TestGetSickDayStatus: ) text, color = locker._get_sick_day_status() assert "already adjusted" in text - assert color == "#ffaa00" + assert color == locker._colors.warning def test_adjustment_success( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path @@ -159,7 +159,7 @@ class TestGetSickDayStatus: ) text, color = locker._get_sick_day_status() assert "earlier" in text - assert color == "#00aa00" + assert color == locker._colors.success def test_adjustment_failure( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path @@ -174,7 +174,7 @@ class TestGetSickDayStatus: ) text, color = locker._get_sick_day_status() assert "Could not adjust" in text - assert color == "#ff4444" + assert color == locker._colors.danger class TestShowRetryAndSick: diff --git a/screen_locker/tests/test_verify_workout.py b/screen_locker/tests/test_verify_workout.py index 59c844b..db2550c 100644 --- a/screen_locker/tests/test_verify_workout.py +++ b/screen_locker/tests/test_verify_workout.py @@ -151,7 +151,7 @@ class TestSetupVerifyWindow: ) locker.root.geometry.assert_called_with("600x400") locker.root.configure.assert_called_with( - bg="#1a1a1a", + bg=locker._colors.bg, cursor="arrow", ) locker.root.protocol.assert_called_with(