mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:27:15 +01:00
Take gatelock 0.8.1: nested LockConfig
The grab axes are grab=GrabPolicy(kind=..., disable_vt=..., preempt_weaker_holder=...) and every colour read goes through config.palette; nothing about the lock's behaviour changed. Also carries the pylint hook on 4.0.8 with the source-only scope config; the gate stays at 8.0 until the remaining findings are cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
This commit is contained in:
parent
fc9720fafe
commit
9b39196671
@ -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
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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(),
|
||||
)
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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("")
|
||||
|
||||
@ -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([])
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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?")
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user