diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8f9981b..ef0050d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -98,7 +98,7 @@ repos: - types-requests>=2.31.0 - repo: https://github.com/pylint-dev/pylint - rev: v3.3.2 + rev: v4.0.8 hooks: # Two hooks, because the three checks below are not defects in a test # suite -- they are how pytest and unit-testing private mixins work -- @@ -126,7 +126,7 @@ repos: # direct URLs for one distribution, so any pin that drifts from # the manifest becomes an unresolvable hook environment rather # than merely an old one. - - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock + - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays - mcp>=2,<3 @@ -146,7 +146,7 @@ repos: # direct URLs for one distribution, so any pin that drifts from # the manifest becomes an unresolvable hook environment rather # than merely an old one. - - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock + - gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock - crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync - freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays - mcp>=2,<3 diff --git a/install_systemd.sh b/install_systemd.sh index dd60e0d..4f91585 100755 --- a/install_systemd.sh +++ b/install_systemd.sh @@ -53,7 +53,7 @@ ensure_runtime_deps() { echo "WARNING: could not install $1 automatically" >&2 } _pip_runtime_dep gatelock \ - "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.1#subdirectory=gatelock" + "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock" _pip_runtime_dep crdt_sync \ "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.0#subdirectory=crdt-sync" } diff --git a/pyproject.toml b/pyproject.toml index 2c29aea..2759e6e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.14" # crdt-sync: shared CRDT merge scheme + GitHub-Contents-API sync transport, # also used by diet_guard; here for phone-workout GitHub sync (Workstream C) dependencies = [ - "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock", + "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock", "crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync", "freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays", ] @@ -178,6 +178,14 @@ persistent = true jobs = 0 py-version = "3.14" ignore = [".venv", "__pycache__"] +# Test suites intentionally use patterns (protected-access, fixture-arg +# shadowing) that don't apply to source code; ruff's per-file-ignores carry +# the equivalent exemptions, so tests stay linted -- by ruff. This is the same +# source-only pylint scope wake-alarm / diet-guard / testsAndMisc use. It has +# to be `ignore-paths`, not `ignore = ["tests"]`: `ignore` only matches the +# basename of what pylint is *given*, and pre-commit hands it explicit file +# paths, so a directory name in `ignore` never fires under the hook. +ignore-paths = ["^(.*/)?tests/.*$"] ignore-patterns = [".*\\.pyi$"] [tool.pylint.messages_control] diff --git a/requirements.txt b/requirements.txt index a1b16eb..5fa5363 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,8 +5,8 @@ codespell==2.4.3 coverage==7.16.0 crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync freedays @ git+https://github.com/kuhyx/utils@freedays-v0.2.0#subdirectory=freedays -gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock -mcp==2.1.1 +gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.8.1#subdirectory=gatelock +mcp==2.2.0 mypy==2.3.1 pre-commit==4.6.2 pylint==4.0.8 diff --git a/screen_locker/_heat_skip.py b/screen_locker/_heat_skip.py index 6bdbd2c..09d637d 100644 --- a/screen_locker/_heat_skip.py +++ b/screen_locker/_heat_skip.py @@ -39,15 +39,15 @@ def build_heat_skip_content( *both* edges if it ever outgrows the display -- and it has to be in the fit check for that reason. """ - outer = tk.Frame(parent, bg=_COLORS.bg) + outer = tk.Frame(parent, bg=_COLORS.palette.bg) outer.place(relx=0.5, rely=0.5, anchor="center") tk.Label( outer, text="\u2600 Too hot to workout?", font=_COLORS.font("body", bold=True, family=_FONT), - bg=_COLORS.bg, - fg=_COLORS.warning, + bg=_COLORS.palette.bg, + fg=_COLORS.palette.warning, ).pack(pady=(0, _COLORS.space("sm"))) tk.Label( @@ -57,28 +57,28 @@ def build_heat_skip_content( f" (threshold: {HEAT_SKIP_TEMP_THRESHOLD}\u00b0C)" ), font=_COLORS.font("body", family=_FONT), - bg=_COLORS.bg, - fg=_COLORS.muted, + bg=_COLORS.palette.bg, + fg=_COLORS.palette.muted, ).pack(pady=_COLORS.space("xs")) tk.Label( outer, text="Skip today's workout due to extreme heat?", font=_COLORS.font("body", family=_FONT), - bg=_COLORS.bg, - fg=_COLORS.muted, + bg=_COLORS.palette.bg, + fg=_COLORS.palette.muted, ).pack(pady=(_COLORS.space("xs"), 0)) - btn_frame = tk.Frame(outer, bg=_COLORS.bg) + btn_frame = tk.Frame(outer, bg=_COLORS.palette.bg) btn_frame.pack(pady=_COLORS.space("lg")) skip_button = tk.Button( btn_frame, text="Skip workout", command=on_skip, - bg=_COLORS.warning, - fg=_COLORS.on_fill, - activebackground=_COLORS.warning, + bg=_COLORS.palette.warning, + fg=_COLORS.palette.on_fill, + activebackground=_COLORS.palette.warning, font=_COLORS.font("label", family=_FONT), padx=_COLORS.space("md"), pady=_COLORS.space("sm"), @@ -91,9 +91,9 @@ def build_heat_skip_content( btn_frame, text="No, I'll workout", command=on_decline, - bg=_COLORS.field_bg, - fg=_COLORS.fg, - activebackground=_COLORS.field_bg, + bg=_COLORS.palette.field_bg, + fg=_COLORS.palette.fg, + activebackground=_COLORS.palette.field_bg, font=_COLORS.font("label", family=_FONT), padx=_COLORS.space("md"), pady=_COLORS.space("sm"), @@ -131,7 +131,7 @@ class HeatSkipMixin: # This window is destroyed before the main GateRoot is created. root = tk.Tk() root.title("Extreme Heat") - root.configure(bg=_COLORS.bg) + root.configure(bg=_COLORS.palette.bg) root.attributes("-fullscreen", True) root.attributes("-topmost", True) root.grab_set() diff --git a/screen_locker/_manual_workout_dialog.py b/screen_locker/_manual_workout_dialog.py index fcecb5e..2b7e515 100644 --- a/screen_locker/_manual_workout_dialog.py +++ b/screen_locker/_manual_workout_dialog.py @@ -42,21 +42,21 @@ class ManualWorkoutDialogMixin( 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=self._colors.accent, pad="xs") + self._label("Log Manual Workout", color=self._colors.palette.accent, pad="xs") if _manual_workout.is_budget_exhausted(self.log_file): self._text( "Manual-workout budget exhausted for this window.", - color=self._colors.danger, + color=self._colors.palette.danger, ) self._text( _manual_workout.budget_summary(self.log_file), - color=self._colors.muted, + color=self._colors.palette.muted, ) row = self._button_row() self._button( row, "BACK", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self._on_manual_workout_cancelled, width=12, ).pack(side="left", padx=self._colors.space("sm")) @@ -64,7 +64,7 @@ class ManualWorkoutDialogMixin( self._text( _manual_workout.budget_summary(self.log_file), role="label", - color=self._colors.accent, + color=self._colors.palette.accent, pad="xs", ) self._build_manual_workout_form() @@ -106,8 +106,8 @@ class ManualWorkoutDialogMixin( # 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=self._colors.bg) - self._mw_other_frame = tk.Frame(form, bg=self._colors.bg) + self._mw_tt_frame = tk.Frame(form, bg=self._colors.palette.bg) + self._mw_other_frame = tk.Frame(form, bg=self._colors.palette.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") @@ -151,20 +151,20 @@ class ManualWorkoutDialogMixin( ) self._mw_error_label = self._text( - "", role="label", color=self._colors.danger, pad="xs" + "", role="label", color=self._colors.palette.danger, pad="xs" ) button_row = self._button_row() self._button( button_row, "SUBMIT", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._submit_manual_workout_form, width=12, ).pack(side="left", padx=self._colors.space("sm")) self._button( button_row, "BACK", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self._on_manual_workout_cancelled, width=12, ).pack(side="left", padx=self._colors.space("sm")) diff --git a/screen_locker/_manual_workout_sport_fields.py b/screen_locker/_manual_workout_sport_fields.py index ced1c29..d96ce03 100644 --- a/screen_locker/_manual_workout_sport_fields.py +++ b/screen_locker/_manual_workout_sport_fields.py @@ -48,13 +48,13 @@ class ManualWorkoutSportFieldsMixin: self._mw_sport_var = tk.StringVar( value=_manual_workout.SPORT_LABELS[_manual_workout.SPORT_TABLE_TENNIS] ) - row = tk.Frame(parent, bg=self._colors.bg) + row = tk.Frame(parent, bg=self._colors.palette.bg) tk.Label( row, text="Sport:", font=self._colors.font("body"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, ).pack(side="left", padx=self._colors.space("xs")) for label in _manual_workout.SPORT_LABELS.values(): tk.Radiobutton( @@ -65,14 +65,14 @@ class ManualWorkoutSportFieldsMixin: # Radiobutton's command takes no argument -- bind the label. command=lambda chosen=label: self._on_mw_sport_changed(chosen), font=self._colors.font("body"), - fg=self._colors.fg, - bg=self._colors.bg, - activeforeground=self._colors.fg, - activebackground=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, + activeforeground=self._colors.palette.fg, + activebackground=self._colors.palette.bg, # Without selectcolor the indicator is a white blob on the dark # overlay; the OptionMenu was the one unstyled widget in this # form, which likely added to it reading as dead. - selectcolor=self._colors.field_bg, + selectcolor=self._colors.palette.field_bg, highlightthickness=0, ).pack(side="left", padx=self._colors.space("xs")) self._mw_grid(parent, row, full=True) diff --git a/screen_locker/_manual_workout_widgets.py b/screen_locker/_manual_workout_widgets.py index f2ecffe..0b354d7 100644 --- a/screen_locker/_manual_workout_widgets.py +++ b/screen_locker/_manual_workout_widgets.py @@ -72,8 +72,8 @@ class ManualWorkoutFormWidgetsMixin: parent, text=title, font=self._colors.font("body", bold=True), - fg=self._colors.accent, - bg=self._colors.bg, + fg=self._colors.palette.accent, + bg=self._colors.palette.bg, anchor="w", ) self._mw_grid(parent, label, full=True) @@ -95,7 +95,7 @@ class ManualWorkoutFormWidgetsMixin: one field per form should ask. """ var = tk.StringVar() - cell = tk.Frame(parent, bg=self._colors.bg) + cell = tk.Frame(parent, bg=self._colors.palette.bg) # Label beside the field, not above it. Stacked, each of the ten cells # cost two lines plus the gap between them, and the form measured # 1320px against a 768px panel; side-by-side each cell is one line @@ -105,17 +105,17 @@ class ManualWorkoutFormWidgetsMixin: cell, text=label, font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, anchor="w", ).pack(side="left", padx=(0, self._colors.space("xs"))) entry = tk.Entry( cell, textvariable=var, font=self._colors.font("label"), - bg=self._colors.field_bg, - fg=self._colors.fg, - insertbackground=self._colors.fg, + bg=self._colors.palette.field_bg, + fg=self._colors.palette.fg, + insertbackground=self._colors.palette.fg, **self._colors.focus_kwargs(), ) entry.pack(side="left", fill="x", expand=True) @@ -130,13 +130,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=self._colors.bg) + row = tk.Frame(parent, bg=self._colors.palette.bg) tk.Label( row, text=label, font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, ).pack(side="left", padx=self._colors.space("xs")) tk.Spinbox( row, @@ -157,22 +157,22 @@ class ManualWorkoutFormWidgetsMixin: whole rows of a form that has to fit a 768px panel, and two of them sit side by side just as legibly. """ - cell = tk.Frame(parent, bg=self._colors.bg) + cell = tk.Frame(parent, bg=self._colors.palette.bg) tk.Label( cell, text=label, font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, anchor="w", ).pack(fill="x") text_widget = tk.Text( cell, height=2, font=self._colors.font("label"), - bg=self._colors.field_bg, - fg=self._colors.fg, - insertbackground=self._colors.fg, + bg=self._colors.palette.field_bg, + fg=self._colors.palette.fg, + insertbackground=self._colors.palette.fg, **self._colors.focus_kwargs(), ) text_widget.pack(pady=self._colors.space("xs"), fill="x") @@ -187,7 +187,7 @@ class ManualWorkoutFormWidgetsMixin: def _mw_rpe_row(self, parent: tk.Widget) -> None: """Add the RPE (rate of perceived exertion) spinbox row.""" - row = tk.Frame(parent, bg=self._colors.bg) + row = tk.Frame(parent, bg=self._colors.palette.bg) tk.Label( row, text=( @@ -195,8 +195,8 @@ class ManualWorkoutFormWidgetsMixin: f"({MANUAL_WORKOUT_RPE_MIN}-{MANUAL_WORKOUT_RPE_MAX}):" ), font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, ).pack(side="left", padx=self._colors.space("xs")) tk.Spinbox( row, @@ -234,7 +234,7 @@ class ManualWorkoutFormWidgetsMixin: Still built on the primary surface only -- an independently scrolled copy per monitor would show two different parts of one form. """ - form = tk.Frame(self.container.first, bg=self._colors.bg) + form = tk.Frame(self.container.first, bg=self._colors.palette.bg) # No outer gap: the budget line above and the first section heading # below already separate the grid, and on a 1024x600 panel this form # fits by single-digit pixels. diff --git a/screen_locker/_sick_commitment.py b/screen_locker/_sick_commitment.py index 3a2ac9e..315f23d 100644 --- a/screen_locker/_sick_commitment.py +++ b/screen_locker/_sick_commitment.py @@ -31,7 +31,7 @@ class SickCommitmentMixin: self._label( "Commit to working out tomorrow?", role="display", - color=self._colors.warning, + color=self._colors.palette.warning, pad="md", ) # ~80 chars at font_size 16 would render as one unbroken line past the @@ -46,20 +46,20 @@ class SickCommitmentMixin: self._commitment_remaining = COMMITMENT_PROMPT_TIMEOUT_SECONDS self._commitment_timer_label = self._text( f"Auto-skipping in {COMMITMENT_PROMPT_TIMEOUT_SECONDS}s", - color=self._colors.muted, + color=self._colors.palette.muted, ) row = self._button_row() self._button( row, "YES", - bg=self._colors.success, + bg=self._colors.palette.success, command=lambda: self._answer_commitment(commit=True), width=12, ).pack(side="left", padx=self._colors.space("sm")) self._button( row, "NO", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=lambda: self._answer_commitment(commit=False), width=12, ).pack(side="left", padx=self._colors.space("sm")) diff --git a/screen_locker/_sick_dialog.py b/screen_locker/_sick_dialog.py index 66aae7f..a7c56db 100644 --- a/screen_locker/_sick_dialog.py +++ b/screen_locker/_sick_dialog.py @@ -35,34 +35,39 @@ class SickDialogMixin(SickCommitmentMixin): history = _sick_tracker.load_history() self._sick_history_cache: SickHistory = history self.clear_container() - self._label("Sick Day Request", color=self._colors.warning, pad="sm") - self._text(_sick_tracker.budget_summary(history), color=self._colors.warning) + self._label("Sick Day Request", color=self._colors.palette.warning, pad="sm") + self._text( + _sick_tracker.budget_summary(history), color=self._colors.palette.warning + ) recent = _sick_tracker.format_recent_justifications(history) if recent: self._text( - "Recent sick days:", role="label", color=self._colors.muted, pad="sm" + "Recent sick days:", + role="label", + color=self._colors.palette.muted, + pad="sm", ) - self._text(recent, role="label", color=self._colors.muted, pad="sm") + self._text(recent, role="label", color=self._colors.palette.muted, pad="sm") had_commitment = _sick_tracker.had_commitment_for_today(history) if had_commitment: self._text( "⚠ Yesterday you committed to working out today.", role="body", - color=self._colors.danger, + color=self._colors.palette.danger, ) self._text( "Breaking the commitment costs 2 sick-budget days.", role="label", - color=self._colors.danger, + color=self._colors.palette.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 = self.container.child_frame(bg=self._colors.bg) + form = self.container.child_frame(bg=self._colors.palette.bg) form.pack(pady=self._colors.space("sm")) self._sick_symptom_var = tk.StringVar() @@ -70,7 +75,9 @@ class SickDialogMixin(SickCommitmentMixin): self._sick_severity_var = tk.IntVar(value=5) self._sick_text_widget = self._add_form_widgets(form) - self._sick_error_label = self._text("", color=self._colors.danger, pad="sm") + self._sick_error_label = self._text( + "", color=self._colors.palette.danger, pad="sm" + ) button_row = self._button_row() # Starts disabled during the forced-read delay -- field_bg (our @@ -79,7 +86,7 @@ class SickDialogMixin(SickCommitmentMixin): self._sick_submit_button = self._button( button_row, "SUBMIT", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self._submit_sick_justification, width=12, ) @@ -87,7 +94,7 @@ class SickDialogMixin(SickCommitmentMixin): self._button( button_row, "BACK", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self._start_phone_check, width=12, ).pack(side="left", padx=self._colors.space("sm")) @@ -110,14 +117,14 @@ class SickDialogMixin(SickCommitmentMixin): label="When did it start? (e.g. last night):", variable=self._sick_onset_var, ) - sev_row = parent.child_frame(bg=self._colors.bg) + sev_row = parent.child_frame(bg=self._colors.palette.bg) sev_row.pack(pady=self._colors.space("sm")) sev_row.child_widgets( tk.Label, text="Severity (1-10):", font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, ).pack(side="left", padx=self._colors.space("xs")) sev_row.child_widgets( tk.Spinbox, @@ -132,8 +139,8 @@ class SickDialogMixin(SickCommitmentMixin): tk.Label, text=(f"Describe how you feel (min {SICK_JUSTIFICATION_MIN_CHARS} chars):"), font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, ).pack(pady=self._colors.space("sm")) text_widgets = TextGroup( list( @@ -142,9 +149,9 @@ class SickDialogMixin(SickCommitmentMixin): width=60, height=6, font=self._colors.font("label"), - bg=self._colors.field_bg, - fg=self._colors.fg, - insertbackground=self._colors.fg, + bg=self._colors.palette.field_bg, + fg=self._colors.palette.fg, + insertbackground=self._colors.palette.fg, **self._colors.focus_kwargs(), ) ) diff --git a/screen_locker/_status_sections.py b/screen_locker/_status_sections.py index 52e3efa..e68804e 100644 --- a/screen_locker/_status_sections.py +++ b/screen_locker/_status_sections.py @@ -40,12 +40,16 @@ class StatusSectionsMixin: self._text( f"{mark} Today ({day.label}): {entry_str}", role="body", - color=self._colors.success if day.counted else self._colors.warning, + color=self._colors.palette.success + if day.counted + else self._colors.palette.warning, ) if day.source: # Secondary provenance note on an already-shown entry -- a # deliberately caption-sized exception to the 16px floor. - self._text(day.source, role="caption", color=self._colors.muted, pad="xs") + self._text( + day.source, role="caption", color=self._colors.palette.muted, pad="xs" + ) def _section_week(self, parent: tk.Widget, week: WeeklySummary) -> None: """Render this ISO week's per-day breakdown and totals.""" @@ -61,20 +65,20 @@ class StatusSectionsMixin: self._text( f"{mark} {day.label}: {entry_str}", role="body", - color=self._colors.muted, + color=self._colors.palette.muted, pad="xs", ) if week.remaining > 0: self._text( f"Need {week.remaining} more this week.", role="body", - color=self._colors.warning, + color=self._colors.palette.warning, ) elif week.extra > 0: self._text( f"{week.extra} above the weekly minimum!", role="body", - color=self._colors.success, + color=self._colors.palette.success, ) def _section_lock_explanation( @@ -86,13 +90,15 @@ class StatusSectionsMixin: self._text( expl.reason, role="body", - color=self._colors.danger if expl.fired else self._colors.success, + color=self._colors.palette.danger + if expl.fired + else self._colors.palette.success, ) if expl.auto_upgrade.would_attempt: self._text( f"Pending auto-upgrade: {expl.auto_upgrade.reason}", role="label", - color=self._colors.warning, + color=self._colors.palette.warning, ) def _section_sick_budget(self, parent: tk.Widget, sick: SickBudgetStatus) -> None: @@ -103,7 +109,9 @@ class StatusSectionsMixin: 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}", role="body", - color=self._colors.danger if sick.exhausted else self._colors.muted, + color=self._colors.palette.danger + if sick.exhausted + else self._colors.palette.muted, ) def _section_manual_workout_budget( @@ -116,7 +124,9 @@ class StatusSectionsMixin: f"{manual.used_7d}/{manual.budget_7d} week · " f"{manual.used_30d}/{manual.budget_30d} month", role="body", - color=self._colors.danger if manual.exhausted else self._colors.muted, + color=self._colors.palette.danger + if manual.exhausted + else self._colors.palette.muted, ) def _section_sync_backend(self, parent: tk.Widget) -> None: @@ -134,7 +144,9 @@ class StatusSectionsMixin: self._text( format_sync_line(status), role="body", - color=self._colors.muted if status.healthy else self._colors.danger, + color=self._colors.palette.muted + if status.healthy + else self._colors.palette.danger, ) def _section_shutdown( @@ -154,7 +166,7 @@ class StatusSectionsMixin: self._text( "Live shutdown config unavailable.", role="body", - color=self._colors.warning, + color=self._colors.palette.warning, ) # Rest-of-week/next-week/explanation are speculative annotations, not # the section's primary content -- a deliberate caption-sized @@ -164,7 +176,9 @@ class StatusSectionsMixin: f"{d.label} {d.hour:02d}:00" for d in shutdown.rest_of_week ) self._text( - f"Rest of week: {rest_line}", role="caption", color=self._colors.muted + f"Rest of week: {rest_line}", + role="caption", + color=self._colors.palette.muted, ) next_line = ", ".join( f"{d.label} {d.hour:02d}:00" for d in shutdown.next_week_preview @@ -172,6 +186,8 @@ class StatusSectionsMixin: self._text( f"Next week (speculative): {next_line}", role="caption", - color=self._colors.muted, + color=self._colors.palette.muted, + ) + self._text( + shutdown.explanation, role="caption", color=self._colors.palette.muted ) - self._text(shutdown.explanation, role="caption", color=self._colors.muted) diff --git a/screen_locker/_temperature_status_mixin.py b/screen_locker/_temperature_status_mixin.py index e9a97b1..ec6c20e 100644 --- a/screen_locker/_temperature_status_mixin.py +++ b/screen_locker/_temperature_status_mixin.py @@ -40,7 +40,9 @@ class TemperatureStatusMixin: result = self._temp_result if result is None: self._text( - "Checking Warsaw temperature…", role="caption", color=self._colors.muted + "Checking Warsaw temperature…", + role="caption", + color=self._colors.palette.muted, ) return if result.timed_out: @@ -48,14 +50,14 @@ class TemperatureStatusMixin: f"Warsaw temperature check timed out after ~" f"{int(HARD_TIMEOUT_SECONDS)}s — network may be down.", role="caption", - color=self._colors.warning, + color=self._colors.palette.warning, ) return if result.temp_celsius is None: self._text( "Warsaw temperature check failed (network/API error).", role="caption", - color=self._colors.warning, + color=self._colors.palette.warning, ) return hot = result.temp_celsius >= HEAT_SKIP_TEMP_THRESHOLD @@ -63,13 +65,13 @@ class TemperatureStatusMixin: f"Warsaw: {result.temp_celsius:.0f}°C (heat-skip threshold " f"{HEAT_SKIP_TEMP_THRESHOLD}°C)", role="caption", - color=self._colors.warning if hot else self._colors.muted, + color=self._colors.palette.warning if hot else self._colors.palette.muted, ) if hot: self._text( "Would trigger heat-skip today.", role="caption", - color=self._colors.danger, + color=self._colors.palette.danger, ) def _start_temperature_check(self) -> None: diff --git a/screen_locker/_ui_flows.py b/screen_locker/_ui_flows.py index e315950..e07e0e7 100644 --- a/screen_locker/_ui_flows.py +++ b/screen_locker/_ui_flows.py @@ -30,7 +30,10 @@ class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin): """ self.clear_container() self._label( - "Checking phone...", role="display", color=self._colors.warning, pad="lg" + "Checking phone...", + role="display", + color=self._colors.palette.warning, + pad="lg", ) self._text("Looking for today's workout in StrongLifts...", role="body") @@ -54,52 +57,58 @@ class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin): """Show TRY AGAIN and (if budget allows) I'm sick after a failed check.""" self.clear_container() self._label( - "No Workout Found", role="display", color=self._colors.danger, pad="md" + "No Workout Found", + role="display", + color=self._colors.palette.danger, + pad="md", ) - self._text(message, color=self._colors.warning) + self._text(message, color=self._colors.palette.warning) # Never accuse the user of skipping a workout the machine simply could # not see. On 2026-08-24 this screen said "No Workout Found" after a # 1h57m session, because Firebase was unreadable and the GitHub mirror # had been stale for nine days -- neither of which was shown here. diagnosis = explain_findings(collect_source_findings()) if diagnosis: - self._text(diagnosis, color=self._colors.muted) + self._text(diagnosis, color=self._colors.palette.muted) history = _sick_tracker.load_history() - self._text(_sick_tracker.budget_summary(history), color=self._colors.muted) self._text( - _manual_workout.budget_summary(self.log_file), color=self._colors.muted + _sick_tracker.budget_summary(history), color=self._colors.palette.muted + ) + self._text( + _manual_workout.budget_summary(self.log_file), + color=self._colors.palette.muted, ) frame = self._button_row() self._button( frame, "TRY AGAIN", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._start_phone_check, width=12, ).pack(side="left", padx=self._colors.space("sm")) if _sick_tracker.is_budget_exhausted(history): self._text( "Sick budget exhausted. No 'I'm sick' option available.", - color=self._colors.danger, + color=self._colors.palette.danger, ) else: self._button( frame, "I'm sick", - bg=self._colors.warning, + bg=self._colors.palette.warning, command=self.ask_if_sick, width=12, ).pack(side="left", padx=self._colors.space("sm")) if _manual_workout.is_budget_exhausted(self.log_file): self._text( "Manual-workout budget exhausted. No manual-log option available.", - color=self._colors.danger, + color=self._colors.palette.danger, ) else: self._button( frame, "Log Manual Workout", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._show_manual_workout_form, width=16, ).pack(side="left", padx=self._colors.space("sm")) @@ -112,11 +121,13 @@ class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin): "✓ Manual Workout Logged!", role="display", scale=1.3, - color=self._colors.success, + color=self._colors.palette.success, pad="lg", ) - self._text(entry.get("source", ""), role="subtitle", color=self._colors.success) - self._text("Unlocking...", role="body", color=self._colors.muted) + self._text( + entry.get("source", ""), role="subtitle", color=self._colors.palette.success + ) + self._text("Unlocking...", role="body", color=self._colors.palette.muted) unlock_delay = 1500 if self.demo_mode else 2000 self.root.after(unlock_delay, self.unlock_screen) @@ -134,11 +145,11 @@ class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin): "✓ Workout Verified!", role="display", scale=1.3, - color=self._colors.success, + color=self._colors.palette.success, pad="lg", ) - self._text(message, role="subtitle", color=self._colors.success) - self._text("Unlocking...", role="body", color=self._colors.muted) + self._text(message, role="subtitle", color=self._colors.palette.success) + self._text("Unlocking...", role="body", color=self._colors.palette.muted) unlock_delay = 1500 if self.demo_mode else 2000 self.root.after(unlock_delay, self.unlock_screen) elif status == "too_short": @@ -175,7 +186,10 @@ class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin): """ self.clear_container() self._label( - "Checking RunnerUp...", role="display", color=self._colors.warning, pad="lg" + "Checking RunnerUp...", + role="display", + color=self._colors.palette.warning, + pad="lg", ) self._text("Looking for today's run in RunnerUp...", role="body") executor = ThreadPoolExecutor(max_workers=1) @@ -196,11 +210,13 @@ class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin): "✓ Run Verified!", role="display", scale=1.3, - color=self._colors.success, + color=self._colors.palette.success, pad="lg", ) - self._text(message, role="subtitle", color=self._colors.success) - self._text("Unlocking...", role="body", color=self._colors.muted) + self._text(message, role="subtitle", color=self._colors.palette.success) + self._text( + "Unlocking...", role="body", color=self._colors.palette.muted + ) unlock_delay = 1500 if self.demo_mode else 2000 self.root.after(unlock_delay, self.unlock_screen) else: diff --git a/screen_locker/_ui_flows_lockout.py b/screen_locker/_ui_flows_lockout.py index 7bc0075..153dbde 100644 --- a/screen_locker/_ui_flows_lockout.py +++ b/screen_locker/_ui_flows_lockout.py @@ -33,7 +33,7 @@ class LockoutFlowMixin: f"Go work out!\nLocked for {self.lockout_time} seconds", role="display", scale=1.5, - color=self._colors.danger, + color=self._colors.palette.danger, pad="lg", ) self.countdown_label = self._label( @@ -82,10 +82,10 @@ class LockoutFlowMixin: self._label( "Cannot Verify Workout", role="display", - color=self._colors.warning, + color=self._colors.palette.warning, pad="md", ) - self._text(message, color=self._colors.warning) + self._text(message, color=self._colors.palette.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 662d8b4..bd029f7 100644 --- a/screen_locker/_ui_flows_relaxed.py +++ b/screen_locker/_ui_flows_relaxed.py @@ -24,7 +24,7 @@ class UIFlowsRelaxedMixin: self._label( "Verifying Workout", role="display", - color=self._colors.warning, + color=self._colors.palette.warning, pad="lg", ) self._text( @@ -61,15 +61,15 @@ class UIFlowsRelaxedMixin: "✓ Workout Verified!", role="display", scale=1.3, - color=self._colors.success, + color=self._colors.palette.success, pad="lg", ) - self._text(message, role="subtitle", color=self._colors.success) + self._text(message, role="subtitle", color=self._colors.palette.success) if adjusted: self._text( "Shutdown time moved later!", role="subtitle", - color=self._colors.warning, + color=self._colors.palette.warning, ) self.root.after(2000, self.close) else: @@ -81,22 +81,22 @@ class UIFlowsRelaxedMixin: self._label( "Workout Not Found", role="display", - color=self._colors.danger, + color=self._colors.palette.danger, pad="md", ) - self._text(message, color=self._colors.warning) + self._text(message, color=self._colors.palette.warning) frame = self._button_row() self._button( frame, "TRY AGAIN", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._start_verify_workout_check, width=12, ).pack(side="left", padx=self._colors.space("sm")) self._button( frame, "Close", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self.close, width=12, ).pack(side="left", padx=self._colors.space("sm")) @@ -116,28 +116,28 @@ class UIFlowsRelaxedMixin: self._label( "Optional Day (Tue / Wed / Thu)", role="title", - color=self._colors.warning, + color=self._colors.palette.warning, pad="md", ) self._text( f"Weekly workouts: {count} / {WEEKLY_WORKOUT_MINIMUM}\n" "No penalty for skipping today.", role="subtitle", - color=self._colors.muted, + color=self._colors.palette.muted, pad="sm", ) frame = self._button_row() self._button( frame, "Skip — No Penalty", - bg=self._colors.success, + bg=self._colors.palette.success, command=self._skip_relaxed_day, width=18, ).pack(side="left", padx=self._colors.space("sm")) self._button( frame, "Log Stronglift Workout", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._start_relaxed_phone_check, width=20, ).pack(side="left", padx=self._colors.space("sm")) @@ -158,7 +158,10 @@ class UIFlowsRelaxedMixin: """Run Stronglift check in relaxed mode (no screen grab, no sick option).""" self.clear_container() self._label( - "Checking phone...", role="display", color=self._colors.warning, pad="lg" + "Checking phone...", + role="display", + color=self._colors.palette.warning, + pad="lg", ) self._text("Looking for today's workout in StrongLifts...", role="body") executor = ThreadPoolExecutor(max_workers=1) @@ -192,21 +195,26 @@ class UIFlowsRelaxedMixin: """Show retry and skip-close when workout not found in relaxed mode.""" self.clear_container() self._label( - "No Workout Found", role="display", color=self._colors.danger, pad="md" + "No Workout Found", + role="display", + color=self._colors.palette.danger, + pad="md", + ) + self._text( + f"❌ {message}\n\nReason: {status}", color=self._colors.palette.warning ) - self._text(f"❌ {message}\n\nReason: {status}", color=self._colors.warning) frame = self._button_row() self._button( frame, "TRY AGAIN", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._start_relaxed_phone_check, width=12, ).pack(side="left", padx=self._colors.space("sm")) self._button( frame, "Close (Skip)", - bg=self._colors.success, + bg=self._colors.palette.success, command=self.close, width=14, ).pack(side="left", padx=self._colors.space("sm")) diff --git a/screen_locker/_ui_flows_sick.py b/screen_locker/_ui_flows_sick.py index 8dc5553..0c1c10d 100644 --- a/screen_locker/_ui_flows_sick.py +++ b/screen_locker/_ui_flows_sick.py @@ -20,14 +20,14 @@ class SickDayFlowMixin: 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", self._colors.warning + return "Shutdown time already adjusted today", self._colors.palette.warning if self._adjust_shutdown_time_earlier(): return ( "Shutdown time moved 1.5 hours earlier ✓\n(Will revert tomorrow)" - ), self._colors.success + ), self._colors.palette.success return ( "Could not adjust shutdown time (check permissions)", - self._colors.danger, + self._colors.palette.danger, ) def _proceed_to_sick_countdown(self) -> None: @@ -54,7 +54,7 @@ class SickDayFlowMixin: countdown: int, ) -> None: """Display sick day UI labels and countdown.""" - self._label("Sick Day Mode", color=self._colors.warning, pad="md") + self._label("Sick Day Mode", color=self._colors.palette.warning, pad="md") self._text(status_text, color=status_color) minutes = countdown // 60 self._text( diff --git a/screen_locker/_ui_form_fields.py b/screen_locker/_ui_form_fields.py index b62fd59..3cefb95 100644 --- a/screen_locker/_ui_form_fields.py +++ b/screen_locker/_ui_form_fields.py @@ -54,14 +54,14 @@ class UIFormFieldsMixin: nothing focused the user's typing goes nowhere and there is no other window on screen to make that obvious. """ - row = parent.child_frame(bg=self._colors.bg) + row = parent.child_frame(bg=self._colors.palette.bg) row.pack(pady=self._colors.space("sm"), fill="x") row.child_widgets( tk.Label, text=label, font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, anchor="w", ).pack(side="top", anchor="w") entries = row.child_widgets( @@ -69,9 +69,9 @@ class UIFormFieldsMixin: textvariable=variable, width=50, font=self._colors.font("label"), - bg=self._colors.field_bg, - fg=self._colors.fg, - insertbackground=self._colors.fg, + bg=self._colors.palette.field_bg, + fg=self._colors.palette.fg, + insertbackground=self._colors.palette.fg, **self._colors.focus_kwargs(), ) entries.pack(side="top", anchor="w", pady=self._colors.space("xs")) @@ -96,14 +96,14 @@ class UIFormFieldsMixin: not advertised anywhere. On a lock surface that means a user who tabs into it can never reach the submit button. """ - row = parent.child_frame(bg=self._colors.bg) + row = parent.child_frame(bg=self._colors.palette.bg) row.pack(pady=self._colors.space("sm"), fill="x") row.child_widgets( tk.Label, text=label, font=self._colors.font("label"), - fg=self._colors.fg, - bg=self._colors.bg, + fg=self._colors.palette.fg, + bg=self._colors.palette.bg, anchor="w", ).pack(side="top", anchor="w") boxes = row.child_widgets( @@ -112,9 +112,9 @@ class UIFormFieldsMixin: height=height, wrap="word", font=self._colors.font("label"), - bg=self._colors.field_bg, - fg=self._colors.fg, - insertbackground=self._colors.fg, + bg=self._colors.palette.field_bg, + fg=self._colors.palette.fg, + insertbackground=self._colors.palette.fg, **self._colors.focus_kwargs(), ) boxes.pack(side="top", anchor="w", pady=self._colors.space("xs")) diff --git a/screen_locker/_ui_widgets.py b/screen_locker/_ui_widgets.py index a3c3022..714b700 100644 --- a/screen_locker/_ui_widgets.py +++ b/screen_locker/_ui_widgets.py @@ -69,8 +69,8 @@ class UIWidgetsMixin(UIFormFieldsMixin): tk.Label, text=text, font=self._colors.font(role, bold=True, scale=scale), - fg=color or self._colors.fg, - bg=self._colors.bg, + fg=color or self._colors.palette.fg, + bg=self._colors.palette.bg, ) label.pack(pady=self._colors.space(pad)) return label @@ -92,8 +92,8 @@ class UIWidgetsMixin(UIFormFieldsMixin): tk.Label, text=text, font=self._colors.font(role, scale=scale), - fg=color or self._colors.fg, - bg=self._colors.bg, + fg=color or self._colors.palette.fg, + bg=self._colors.palette.bg, ) label.pack(pady=self._colors.space(pad)) return label @@ -109,12 +109,12 @@ class UIWidgetsMixin(UIFormFieldsMixin): the bug by omission. """ fills = { - self._colors.accent, - self._colors.success, - self._colors.warning, - self._colors.danger, + self._colors.palette.accent, + self._colors.palette.success, + self._colors.palette.warning, + self._colors.palette.danger, } - return self._colors.on_fill if bg in fills else self._colors.fg + return self._colors.palette.on_fill if bg in fills else self._colors.palette.fg def _button( self, @@ -174,6 +174,6 @@ class UIWidgetsMixin(UIFormFieldsMixin): on every screen: the pixels it spends are the ones the manual-workout form has left over on a 1024x600 panel. """ - frame = self.container.child_frame(bg=self._colors.bg) + frame = self.container.child_frame(bg=self._colors.palette.bg) frame.pack(pady=self._colors.space("sm")) return frame diff --git a/screen_locker/_unlock_view.py b/screen_locker/_unlock_view.py index 2626d1c..54794cc 100644 --- a/screen_locker/_unlock_view.py +++ b/screen_locker/_unlock_view.py @@ -31,35 +31,35 @@ class UnlockViewMixin: "Great job! 💪", role="display", scale=1.5, - color=self._colors.success, + color=self._colors.palette.success, pad="lg", ) if credit.shutdown_adjusted: self._text( "Shutdown time +2h later! 🎁", role="title", - color=self._colors.warning, + color=self._colors.palette.warning, ) if credit.extra_bonus_delta > 0: self._text( f"Extra workout today! +{credit.extra_bonus_delta}h tonight", role="subtitle", - color=self._colors.warning, + color=self._colors.palette.warning, ) if credit.new_debt is not None: self._text( f"Workout debt: {credit.new_debt}", role="subtitle", - color=self._colors.warning + color=self._colors.palette.warning if credit.new_debt > 0 - else self._colors.muted, + else self._colors.palette.muted, ) streak = current_streak(EXTRA_BENEFITS_FILE) if streak >= 1: self._text( f"🔥 {streak}-week streak (5+ workouts each)", role="label", - color=self._colors.muted, + color=self._colors.palette.muted, ) self._text("Screen Unlocked!", role="display", pad="md") if self.workout_data.get("type") in _COMMITTABLE: diff --git a/screen_locker/_window_setup.py b/screen_locker/_window_setup.py index afbcd5b..2ad299d 100644 --- a/screen_locker/_window_setup.py +++ b/screen_locker/_window_setup.py @@ -210,8 +210,8 @@ class WindowSetupMixin: self.root, text="✕ Close Demo", font=self._colors.font("caption"), - bg=self._colors.danger, - fg=self._colors.on_fill, + bg=self._colors.palette.danger, + fg=self._colors.palette.on_fill, command=self.close, cursor="hand2", ) @@ -238,7 +238,7 @@ class WindowSetupMixin: still does not fit scrolls, because ``_ensure_container`` wraps these windows in a viewport too. """ - self.root.configure(bg=self._colors.bg, cursor="arrow") + self.root.configure(bg=self._colors.palette.bg, cursor="arrow") self.root.protocol("WM_DELETE_WINDOW", self.close) # "" hands sizing back to the geometry manager instead of pinning it. self.root.geometry("") diff --git a/screen_locker/screen_lock.py b/screen_locker/screen_lock.py index a2f6c05..10cae1b 100755 --- a/screen_locker/screen_lock.py +++ b/screen_locker/screen_lock.py @@ -15,6 +15,7 @@ from typing import TYPE_CHECKING from gatelock import ( GateRoot, + GrabPolicy, LockConfig, LockWindow, ) @@ -184,14 +185,17 @@ class ScreenLocker( # 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, - # Opt in: a weaker holder (diet_guard, leetcode_guard) that already - # owns the grab must stand down rather than block enforcement - # forever -- the 2026-08-21 lockout was diet_guard holding it for - # hours. Safe in this direction only; gatelock never signals a - # holder that outranks us, so wake_alarm can still take the screen. - preempt_weaker_holder=not demo_mode, + grab=GrabPolicy( + kind="local" if demo_mode else "global", + disable_vt=not demo_mode, + # Opt in: a weaker holder (diet_guard, leetcode_guard) that + # already owns the grab must stand down rather than block + # enforcement forever -- the 2026-08-21 lockout was diet_guard + # holding it for hours. Safe in this direction only; gatelock + # never signals a holder that outranks us, so wake_alarm can + # still take the screen. + preempt_weaker_holder=not demo_mode, + ), ) # Built before setup(), which calls build_surface per live output. self.container = FrameGroup([]) diff --git a/screen_locker/status_view.py b/screen_locker/status_view.py index 2d73042..05441ec 100644 --- a/screen_locker/status_view.py +++ b/screen_locker/status_view.py @@ -103,7 +103,7 @@ class StatusWindow( # but it shares UIWidgetsMixin's factory with the locker, which now # builds through a group. Filling the window rather than centring, so # this is built here instead of via FrameGroup.single(). - frame = tk.Frame(root, bg=self._colors.bg) + frame = tk.Frame(root, bg=self._colors.palette.bg) frame.pack(fill="both", expand=True) self.container = FrameGroup([frame]) self._start_temperature_check() @@ -127,11 +127,15 @@ class StatusWindow( if self._phone_check_result is not None: status, message = self._phone_check_result color = ( - self._colors.success if status == "verified" else self._colors.warning + self._colors.palette.success + if status == "verified" + else self._colors.palette.warning ) self._text(f"Phone check ({status}): {message}", role="body", color=color) if self._credit_message is not None: - self._text(self._credit_message, role="body", color=self._colors.success) + self._text( + self._credit_message, role="body", color=self._colors.palette.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. @@ -139,7 +143,7 @@ class StatusWindow( self._button( frame, "Check Phone", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._on_check_phone_clicked, width=14, ).pack(side="left", padx=self._colors.space("sm")) @@ -147,21 +151,21 @@ class StatusWindow( self._button( frame, "Log Manual Workout", - bg=self._colors.accent, + bg=self._colors.palette.accent, command=self._show_manual_workout_form, width=16, ).pack(side="left", padx=self._colors.space("sm")) self._button( frame, "Refresh", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self._on_refresh_clicked, width=10, ).pack(side="left", padx=self._colors.space("sm")) self._button( frame, "Close", - bg=self._colors.field_bg, + bg=self._colors.palette.field_bg, command=self.root.destroy, width=8, ).pack(side="left", padx=self._colors.space("sm")) @@ -219,7 +223,7 @@ def main(argv: list[str] | None = None) -> None: root = tk.Tk() root.title("Workout Status") - root.configure(bg=_STATUS_COLORS.bg) + root.configure(bg=_STATUS_COLORS.palette.bg) root.minsize(560, 200) def refresh() -> None: diff --git a/screen_locker/tests/test_focus_and_scroll.py b/screen_locker/tests/test_focus_and_scroll.py index 6e76d12..a088f86 100644 --- a/screen_locker/tests/test_focus_and_scroll.py +++ b/screen_locker/tests/test_focus_and_scroll.py @@ -184,7 +184,7 @@ class TestMultiLineField: ) -> None: """Both halves fan out, like every other widget helper here.""" locker = create_locker(mock_tk, tmp_path) - parent = locker.container.child_frame(bg=locker._colors.bg) + parent = locker.container.child_frame(bg=locker._colors.palette.bg) mock_tk.Text.reset_mock() boxes = locker._add_label_text(parent, label="Why?", height=3) @@ -202,7 +202,7 @@ class TestMultiLineField: advertised way out -- on this lock, that means never reaching SUBMIT. """ locker = create_locker(mock_tk, tmp_path) - parent = locker.container.child_frame(bg=locker._colors.bg) + parent = locker.container.child_frame(bg=locker._colors.palette.bg) boxes = locker._add_label_text(parent, label="Why?") @@ -213,7 +213,7 @@ class TestMultiLineField: def test_paste_is_disabled(self, mock_tk: MagicMock, tmp_path: Path) -> None: """The justification has to be typed, like every other field here.""" locker = create_locker(mock_tk, tmp_path) - parent = locker.container.child_frame(bg=locker._colors.bg) + parent = locker.container.child_frame(bg=locker._colors.palette.bg) boxes = locker._add_label_text(parent, label="Why?") diff --git a/screen_locker/tests/test_manual_workout_status_view.py b/screen_locker/tests/test_manual_workout_status_view.py index ba08568..f004e6b 100644 --- a/screen_locker/tests/test_manual_workout_status_view.py +++ b/screen_locker/tests/test_manual_workout_status_view.py @@ -32,7 +32,7 @@ class TestSectionManualWorkoutBudget: for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == window._colors.danger for c in calls) + assert any(c.kwargs.get("fg") == window._colors.palette.danger for c in calls) def test_not_exhausted_uses_normal_color(self, mock_tk: MagicMock) -> None: snap = _snapshot( @@ -44,7 +44,7 @@ class TestSectionManualWorkoutBudget: for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == window._colors.muted for c in calls) + assert any(c.kwargs.get("fg") == window._colors.palette.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 b863e43..7edb5f6 100644 --- a/screen_locker/tests/test_status_view.py +++ b/screen_locker/tests/test_status_view.py @@ -168,7 +168,7 @@ class TestSectionSickBudget: for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == window._colors.danger for c in calls) + assert any(c.kwargs.get("fg") == window._colors.palette.danger for c in calls) def test_not_exhausted_uses_normal_color(self, mock_tk: MagicMock) -> None: """Not exhausted uses normal color.""" @@ -179,7 +179,7 @@ class TestSectionSickBudget: for c in mock_tk.Label.call_args_list if "week" in c.kwargs.get("text", "") ] - assert any(c.kwargs.get("fg") == window._colors.muted for c in calls) + assert any(c.kwargs.get("fg") == window._colors.palette.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 e4b4e8e..9be240a 100644 --- a/screen_locker/tests/test_status_view_part2.py +++ b/screen_locker/tests/test_status_view_part2.py @@ -41,7 +41,7 @@ class TestPhoneCheckResultRendering: if "Phone check" in c.kwargs.get("text", "") ] assert calls - assert calls[-1].kwargs.get("fg") == window._colors.success + assert calls[-1].kwargs.get("fg") == window._colors.palette.success def test_non_verified_result_shown_in_orange(self, mock_tk: MagicMock) -> None: """Non verified result shown in orange.""" @@ -54,7 +54,7 @@ class TestPhoneCheckResultRendering: if "Phone check" in c.kwargs.get("text", "") ] assert calls - assert calls[-1].kwargs.get("fg") == window._colors.warning + assert calls[-1].kwargs.get("fg") == window._colors.palette.warning class TestButtons: diff --git a/screen_locker/tests/test_surface_fanout.py b/screen_locker/tests/test_surface_fanout.py index 7b7df88..ae0b5eb 100644 --- a/screen_locker/tests/test_surface_fanout.py +++ b/screen_locker/tests/test_surface_fanout.py @@ -55,7 +55,9 @@ class TestLockerFansOut: row = locker._button_row() mock_tk.Button.reset_mock() - locker._button(row, "OK", bg=locker._colors.accent, command=lambda: None) + locker._button( + row, "OK", bg=locker._colors.palette.accent, command=lambda: None + ) assert mock_tk.Button.call_count == len(TWO_OUTPUTS) diff --git a/screen_locker/tests/test_ui_and_timers.py b/screen_locker/tests/test_ui_and_timers.py index 96eb5cb..bc69546 100644 --- a/screen_locker/tests/test_ui_and_timers.py +++ b/screen_locker/tests/test_ui_and_timers.py @@ -147,7 +147,7 @@ class TestGetSickDayStatus: ) text, color = locker._get_sick_day_status() assert "already adjusted" in text - assert color == locker._colors.warning + assert color == locker._colors.palette.warning def test_adjustment_success( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path @@ -162,7 +162,7 @@ class TestGetSickDayStatus: ) text, color = locker._get_sick_day_status() assert "earlier" in text - assert color == locker._colors.success + assert color == locker._colors.palette.success def test_adjustment_failure( self, mock_tk: MagicMock, mock_sys_exit: MagicMock, tmp_path: Path @@ -177,7 +177,7 @@ class TestGetSickDayStatus: ) text, color = locker._get_sick_day_status() assert "Could not adjust" in text - assert color == locker._colors.danger + assert color == locker._colors.palette.danger class TestShowRetryAndSick: diff --git a/screen_locker/tests/test_verify_workout.py b/screen_locker/tests/test_verify_workout.py index 535065b..b06a1ae 100644 --- a/screen_locker/tests/test_verify_workout.py +++ b/screen_locker/tests/test_verify_workout.py @@ -161,7 +161,7 @@ class TestSetupVerifyWindow: locker.root.minsize.assert_called_once() locker.root.maxsize.assert_called_once() locker.root.configure.assert_called_with( - bg=locker._colors.bg, + bg=locker._colors.palette.bg, cursor="arrow", ) locker.root.protocol.assert_called_with( diff --git a/scripts/_popup_form_check.py b/scripts/_popup_form_check.py index 78c8e4a..7985557 100644 --- a/scripts/_popup_form_check.py +++ b/scripts/_popup_form_check.py @@ -143,7 +143,7 @@ class FormHost(UIWidgetsMixin, ManualWorkoutDialogMixin): self.log_file = Path(self._log_dir.name) / "log.json" self.log_file.write_text("{}") self._colors = LockConfig() - self.container = FrameGroup.single(root, bg=self._colors.bg) + self.container = FrameGroup.single(root, bg=self._colors.palette.bg) def _on_manual_workout_saved(self, entry: dict[str, object]) -> None: """Unused here; the form is never submitted.""" diff --git a/scripts/demo_workout_handoff.py b/scripts/demo_workout_handoff.py index a3a8419..6d206bd 100644 --- a/scripts/demo_workout_handoff.py +++ b/scripts/demo_workout_handoff.py @@ -43,7 +43,7 @@ import tkinter as tk sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from gatelock import LockConfig, LockWindow +from gatelock import GrabPolicy, LockConfig, LockWindow from screen_locker._workout_app import ProcessHooks, workout_app_binary from screen_locker._workout_handoff import lock_grab_handoff @@ -130,7 +130,7 @@ def main() -> int: root = tk.Tk() root.title("Workout handoff demo") # Local grab + VT switching enabled: every escape hatch stays open. - config = LockConfig(mode="hard", grab="local", disable_vt=False) + config = LockConfig(mode="hard", grab=GrabPolicy(kind="local", disable_vt=False)) hooks = _DemoHooks() lock = LockWindow(root, config, hooks) hooks.lock = lock