screen-locker/screen_locker/_ui_flows_sick.py
Krzysztof kuhy Rudnicki 8450644a56 Lock screen: "choose lockers…" opens sickdays' KeepPanel in the lock
Next to the one-tap "I'm sick" (unchanged: keeps nothing, unlocks), a small
secondary link lays sickdays 0.3.0's embeddable KeepPanel over every
surface, in-process so it gets input under the lock's grab. Closing the
overlay reveals the untouched screen underneath, so no countdown or phone
check is restarted or skipped.

After the panel's own write + sync: sick with the workout lock standing
down -> sick floor, decision, close (shared with the one-tap path, no second
mark or push); sick with the workout kept -> sick floor and a note, lock
stays; cleared / unchanged / cancel -> the panel just closes.

Tests stub KeepPanel and redirect resolve_paths and sync_quietly in every
sickdays.* module, sickdays.gui._panel included. The fit check measures the
panel on a throwaway pool. Pins bumped to sickdays-v0.3.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
2026-10-17 14:06:22 +02:00

103 lines
3.8 KiB
Python

"""The "I'm sick" button: one tap marks today sick and unlocks.
A sick day is a date in the shared ``sickdays`` pool (``_sick.py``). There is
no budget, countdown, justification, commitment or debt any more: the tap
writes today into the local pool, pushes it in the background (best
effort, so the phone sees it before the next sickdays-sync tick), raises
tonight's shutdown to the sick floor right
away (``_sick_floor``) and closes the lock. Every other gate reads the same
pool and stands down too.
The button is offered on every screen that can end in a lock or a prompt:
the retry screen, the no-phone penalty screen and both relaxed-day screens.
A refusal (a free day, an unreadable pool) is shown on screen and the lock
stays up.
Next to the button sits a small "choose lockers…" link: the same day, but
with some lockers kept (``_ui_flows_sick_panel``). The button itself still
keeps nothing.
Split out of :mod:`screen_locker._ui_flows` to keep every file under the
250-line cap. Composed back into ``UIFlowsMixin`` there.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from screen_locker._constants import SHUTDOWN_BASE_FILE
from screen_locker._credit_slot import locked_shutdown_state
from screen_locker._sick import mark_today_sick, push_in_background
if TYPE_CHECKING:
import tkinter as tk
class SickDayFlowMixin:
"""The one-tap sick-day button and what follows it."""
def _sick_button(self, frame: tk.Widget) -> None:
"""Pack the "I'm sick" button into ``frame``."""
self._button(
frame,
"I'm sick",
bg=self._colors.palette.warning,
command=self.mark_sick_and_unlock,
width=12,
).pack(side="left", padx=self._colors.space("sm"))
# The keep-aware alternative (``_ui_flows_sick_panel``), next to it.
self._choose_lockers_link(frame)
def mark_sick_and_unlock(self) -> None:
"""Mark today sick, lift tonight's shutdown to 20:00 and unlock."""
result = mark_today_sick()
if not result.ok:
self._show_sick_refused(result.message)
return
self._raise_sick_floor()
# Best effort and off the UI thread: the unlock never waits for it.
push_in_background()
self._unlock_sick_day(
f"Marked today sick from the lock screen. {result.message}"
)
def _raise_sick_floor(self) -> None:
"""Lift tonight's shutdown to the sick floor (a no-op unless sick)."""
# Now, not at the next 5-minute tick: while this window is up that
# tick stands down at the instance lock.
with locked_shutdown_state(SHUTDOWN_BASE_FILE):
self._apply_sick_floor()
def _unlock_sick_day(self, detail: str) -> None:
"""Record the sick-day unlock and close the lock.
Shared by the one-tap button and the keep panel; neither pushes here
(the panel starts its own sync).
"""
self._record_decision(locked=False, reason="sick_day_marked", detail=detail)
self.close()
def _show_sick_refused(self, message: str) -> None:
"""Say why today could not be marked sick; the lock stays up."""
self.clear_container()
self._label(
"Could not mark today as sick",
role="display",
color=self._colors.palette.danger,
pad="md",
)
self._text(message, color=self._colors.palette.warning)
frame = self._button_row()
back = (
self._start_relaxed_day_flow
if self._relaxed_day_mode
else self._start_phone_check
)
self._button(
frame,
"Back",
bg=self._colors.palette.accent,
command=back,
width=12,
).pack(side="left", padx=self._colors.space("sm"))