mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 18:33:57 +01:00
Every locker screen, the status window and the credit toast now show glyphs (screen_locker/_glyphs.py) and numbers, coloured by state. Sentences that carried a reason (verifier message, sync diagnosis, sick refusal, form rejection) go to the log at warning instead. New pre-commit hook text-free runs scripts/verify_text_free.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
6.9 KiB
Python
185 lines
6.9 KiB
Python
"""The individual rendered sections of the status window.
|
|
|
|
Split out of :mod:`screen_locker.status_view` to keep every file under the
|
|
250-line cap. Composed into ``StatusWindow`` there, alongside the other view
|
|
mixins, so the class's public surface is unchanged.
|
|
|
|
Every method here is display-only: it reads the snapshot it is handed and
|
|
paints widgets. Nothing in this module writes to disk or reaches the network.
|
|
|
|
textgate: no section has a heading or a sentence any more. Each is one or
|
|
two lines of glyphs (``_glyphs``) and numbers, coloured by state. Weekdays
|
|
are positions in a Mon..Sun row, not names.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker import _glyphs as g
|
|
from screen_locker._earned import hhmm
|
|
from screen_locker._sync_status import gather_sync_status
|
|
|
|
if TYPE_CHECKING:
|
|
import tkinter as tk
|
|
|
|
from screen_locker._compliance_state import LockExplanation
|
|
from screen_locker._status_data import (
|
|
DayStatus,
|
|
ManualWorkoutBudgetStatus,
|
|
ShutdownProjection,
|
|
WeeklySummary,
|
|
)
|
|
|
|
|
|
class StatusSectionsMixin:
|
|
"""Renders each section of the status window from a snapshot."""
|
|
|
|
def _day_cell(self, day: DayStatus) -> str:
|
|
"""One day as a glyph: its entries, sick, or an empty circle."""
|
|
if day.entry_types:
|
|
return g.entry_glyphs(day.entry_types)
|
|
return g.WEEK_SICK if day.is_sick_day else g.WEEK_EMPTY
|
|
|
|
def _section_today(self, parent: tk.Widget, day: DayStatus) -> None:
|
|
"""Render today's outcome (was "✓ Today (Wed Nov 11): phone_verified")."""
|
|
del parent
|
|
mark = g.OK if day.counted else (g.SICK if day.is_sick_day else g.FAIL)
|
|
self._label(
|
|
f"{mark} {self._day_cell(day)}",
|
|
role="title",
|
|
color=self._colors.palette.success
|
|
if day.counted
|
|
else self._colors.palette.warning,
|
|
)
|
|
|
|
def _section_week(self, parent: tk.Widget, week: WeeklySummary) -> None:
|
|
"""Render this ISO week: dots + count, then one cell per day.
|
|
|
|
Was "This Week: 2/4", a "Mon Nov 09: no entry" line per day and "Need
|
|
2 more this week." / "1 above the weekly minimum!".
|
|
"""
|
|
del parent
|
|
enough = week.remaining <= 0
|
|
self._text(
|
|
g.dots(week.counted_count, week.minimum),
|
|
role="title",
|
|
color=self._colors.palette.success
|
|
if enough
|
|
else self._colors.palette.warning,
|
|
pad="sm",
|
|
)
|
|
self._text(
|
|
" ".join(self._day_cell(day) for day in week.days),
|
|
role="body",
|
|
color=self._colors.palette.muted,
|
|
pad="xs",
|
|
)
|
|
|
|
def _section_streak(
|
|
self, parent: tk.Widget, streak: int, week_rewards: tuple[str, ...] = ()
|
|
) -> None:
|
|
"""Render the consecutive 5+-workout-week streak and this week's reward.
|
|
|
|
Display only, and exactly the stored values (``extra_benefits_state``):
|
|
a streak of 0 is shown as 0, never padded or rounded up. Nothing here
|
|
grants time or relaxes a lock. Was "🔥 N-week streak (5+ workouts a
|
|
week)" / "No streak yet: ..." and the reward sentences.
|
|
"""
|
|
del parent
|
|
self._text(
|
|
f"{g.STREAK * min(streak, 10)} {streak}" if streak else f"{g.STREAK} 0",
|
|
role="body",
|
|
color=self._colors.palette.success
|
|
if streak
|
|
else self._colors.palette.muted,
|
|
)
|
|
for message in week_rewards:
|
|
# A reset is news, not a celebration: it must not be painted green.
|
|
lost = message.startswith("Streak reset")
|
|
self._text(
|
|
f"{g.STREAK}{g.FAIL}" if lost else f"{g.STREAK}{g.GIFT}",
|
|
role="body",
|
|
color=self._colors.palette.warning
|
|
if lost
|
|
else self._colors.palette.success,
|
|
)
|
|
|
|
def _section_lock_explanation(
|
|
self, parent: tk.Widget, expl: LockExplanation
|
|
) -> None:
|
|
"""Render whether the lock fired today and the stage that decided it.
|
|
|
|
Was "Why the lock did/didn't fire" over the stage's sentence.
|
|
"""
|
|
del parent
|
|
square = g.LOCK if expl.fired else g.UNLOCK
|
|
self._text(
|
|
f"{square} {g.STAGE_GLYPH.get(expl.stage, '')}".rstrip(),
|
|
role="title",
|
|
color=self._colors.palette.danger
|
|
if expl.fired
|
|
else self._colors.palette.success,
|
|
)
|
|
if expl.auto_upgrade.would_attempt:
|
|
self._text(g.RETRY, role="label", color=self._colors.palette.warning)
|
|
|
|
def _section_manual_workout_budget(
|
|
self, parent: tk.Widget, manual: ManualWorkoutBudgetStatus
|
|
) -> None:
|
|
"""Render rolling manual-workout budget usage (7-day, then 30-day)."""
|
|
del parent
|
|
self._text(
|
|
f"{g.FORM} {manual.used_7d}/{manual.budget_7d} · "
|
|
f"{manual.used_30d}/{manual.budget_30d}",
|
|
role="body",
|
|
color=self._colors.palette.danger
|
|
if manual.exhausted
|
|
else self._colors.palette.muted,
|
|
)
|
|
|
|
def _section_sync_backend(self, parent: tk.Widget) -> None:
|
|
"""Render whether this machine's sync is healthy, and its peer count.
|
|
|
|
Read from local state only (see :mod:`screen_locker._sync_status`), so
|
|
opening or refreshing the window never makes a network call. Coloured
|
|
by ``healthy`` rather than by backend: "configured but never pushed"
|
|
looks fine everywhere else and produces no data, which is exactly the
|
|
state worth surfacing here.
|
|
"""
|
|
del parent
|
|
status = gather_sync_status()
|
|
self._text(
|
|
f"{g.SYNC} {g.OK if status.healthy else g.FAIL} {status.peer_count}",
|
|
role="body",
|
|
color=self._colors.palette.muted
|
|
if status.healthy
|
|
else self._colors.palette.danger,
|
|
)
|
|
|
|
def _section_shutdown(
|
|
self, parent: tk.Widget, shutdown: ShutdownProjection
|
|
) -> None:
|
|
"""Render tonight's live config, rest-of-week, and next-week preview.
|
|
|
|
Was "Live config — Mon-Wed 21:00, Thu-Sun 22:00", "Rest of week: Mon
|
|
21:00, ...", "Next week (speculative): ..." and an explanation. Now
|
|
the times only, in day order: live config bold, the rest muted.
|
|
"""
|
|
del parent
|
|
if shutdown.tonight is not None:
|
|
mon_wed, thu_sun, _morning = shutdown.tonight
|
|
self._text(f"{g.SHUTDOWN} {hhmm(mon_wed)} · {hhmm(thu_sun)}", role="title")
|
|
else:
|
|
self._text(
|
|
f"{g.SHUTDOWN} {g.FAIL}",
|
|
role="title",
|
|
color=self._colors.palette.warning,
|
|
)
|
|
for days in (shutdown.rest_of_week, shutdown.next_week_preview):
|
|
self._text(
|
|
" ".join(hhmm(d.minutes) for d in days),
|
|
role="caption",
|
|
color=self._colors.palette.muted,
|
|
)
|