Split _ui_flows.py into sick-day and lockout flow mixins

_ui_flows.py was 370 lines. The sick-day flow and the lockout/phone-penalty
flow are both self-contained, so each moves to a sibling module:

  _ui_flows_sick.py     SickDayFlowMixin   (justification, countdown, debt)
  _ui_flows_lockout.py  LockoutFlowMixin   (lockout timer, no-phone penalty)

UIFlowsMixin composes both rather than exposing them as separate mixins, so
neither ScreenLocker nor scripts/verify_screen_fits.py changes its bases --
and ScreenLocker's ancestor count (already at pylint's max-parents limit)
stays where it was.

Pure move: 975 tests still pass at 100% branch coverage, pylint holds at
8.70/10, and `screen_lock --status` renders the real week unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
This commit is contained in:
Krzysztof Rudnicki 2026-08-15 19:56:16 +02:00
parent 3476dd3afd
commit 0714c2264d
4 changed files with 219 additions and 181 deletions

View File

@ -19,8 +19,8 @@ churn, not size alone — refactoring pays where code is read and changed often
<!-- BEGIN GENERATED VIOLATIONS -->
- **61 files** currently exceed 250 lines.
- **9,084 lines** over the cap in total (the work left to do); longest file is **897** lines.
- **41 files** currently exceed 250 lines.
- **6,753 lines** over the cap in total (the work left to do); longest file is **897** lines.
ROI = lines x commits in the last year. Work top-down; a long file nobody edits
has near-zero payoff and should not be first.
@ -33,7 +33,6 @@ has near-zero payoff and should not be first.
| 323 | 22 | code | `screen_locker/tests/conftest.py` |
| 480 | 12 | code | `stronglift_replacement/workout_app/test/screens/settings_screen_test.dart` |
| 633 | 9 | code | `stronglift_replacement/workout_app/lib/screens/workout_screen.dart` |
| 343 | 16 | code | `screen_locker/tests/test_init_and_log.py` |
| 740 | 7 | code | `stronglift_replacement/workout_app/test/services/workout_sync_service_test.dart` |
| 370 | 14 | code | `screen_locker/_ui_flows.py` |
| 380 | 13 | code | `screen_locker/_phone_verification.py` |
@ -42,8 +41,9 @@ has near-zero payoff and should not be first.
| 357 | 9 | code | `screen_locker/_manual_workout_dialog.py` |
| 384 | 8 | code | `screen_locker/_shutdown.py` |
| 383 | 8 | code | `stronglift_replacement/workout_app/lib/screens/home_screen.dart` |
| 396 | 7 | code | `screen_locker/status_view.py` |
_(46 further files over 250 lines not listed — re-run `python3 scripts/refresh_refactor_todo.py` for the current set.)_
_(26 further files over 250 lines not listed — re-run `python3 scripts/refresh_refactor_todo.py` for the current set.)_
<!-- END GENERATED VIOLATIONS -->

View File

@ -6,18 +6,19 @@ from concurrent.futures import ThreadPoolExecutor # pylint: disable=no-name-in-
from typing import TYPE_CHECKING
from screen_locker import _manual_workout, _sick_tracker
from screen_locker._constants import (
NO_PHONE_EXTRA_LOCKOUT_SECONDS,
PHONE_PENALTY_DELAY_DEMO,
PHONE_PENALTY_DELAY_PRODUCTION,
)
from screen_locker._ui_flows_lockout import LockoutFlowMixin
from screen_locker._ui_flows_sick import SickDayFlowMixin
if TYPE_CHECKING:
from collections.abc import Callable
class UIFlowsMixin:
"""Mixin providing UI flow logic for the screen locker."""
class UIFlowsMixin(SickDayFlowMixin, LockoutFlowMixin):
"""Mixin providing UI flow logic for the screen locker.
The sick-day and lockout/penalty flows live in sibling modules and are
composed in here, so ``ScreenLocker``'s own mixin list stays as it was.
"""
def paint_phone_check(self) -> None:
"""Paint the "checking phone" screen, without starting the check.
@ -198,173 +199,3 @@ class UIFlowsMixin:
self._runnerup_on_failure()
else:
self.root.after(500, self._poll_runnerup_fallback)
def ask_if_sick(self) -> None:
"""Display the structured sick-day justification dialog."""
self._show_sick_justification()
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
if self._adjust_shutdown_time_earlier():
return (
"Shutdown time moved 1.5 hours earlier ✓\n(Will revert tomorrow)"
), self._colors.success
return (
"Could not adjust shutdown time (check permissions)",
self._colors.danger,
)
def _proceed_to_sick_countdown(self) -> None:
"""Start the (escalated) sick day countdown after justification."""
history = getattr(
self,
"_sick_history_cache",
None,
)
if history is None:
history = _sick_tracker.load_history()
self._sick_history_cache = history
countdown = _sick_tracker.compute_lockout_seconds(history)
self.clear_container()
status_text, status_color = self._get_sick_day_status()
self._show_sick_day_ui(status_text, status_color, countdown)
self.sick_remaining_time = countdown
self._update_sick_countdown()
def _show_sick_day_ui(
self,
status_text: str,
status_color: str,
countdown: int,
) -> None:
"""Display sick day UI labels and countdown."""
self._label("Sick Day Mode", color=self._colors.warning, pad="md")
self._text(status_text, color=status_color)
minutes = countdown // 60
self._text(
f"Please wait ~{minutes} min before unlocking...",
role="title",
pad="md",
)
self.sick_countdown_label = self._label(
str(countdown),
role="display",
scale=2.5,
pad="lg",
)
def _update_sick_countdown(self) -> None:
"""Update the sick day countdown timer."""
if self.sick_remaining_time > 0:
self.sick_countdown_label.config(text=str(self.sick_remaining_time))
self.sick_remaining_time -= 1
self.root.after(1000, self._update_sick_countdown)
else:
self._finalize_sick_day()
def _finalize_sick_day(self) -> None:
"""Persist sick-day history and unlock the screen."""
history = getattr(self, "_sick_history_cache", None)
if history is None:
history = _sick_tracker.load_history()
if _sick_tracker.had_commitment_for_today(history):
_sick_tracker.mark_commitment_broken(history)
self.workout_data["broke_commitment"] = "true"
new_debt = _sick_tracker.add_sick_day(history)
_sick_tracker.save_history(history)
self.workout_data["type"] = "sick_day"
self.workout_data["note"] = "Sick day - shutdown moved earlier"
self.workout_data["debt"] = str(new_debt)
self.unlock_screen()
# ------------------------------------------------------------------
# Lockout flow
# ------------------------------------------------------------------
def lockout(self) -> None:
"""Display lockout screen with countdown timer."""
self.clear_container()
self.lockout_label = self._label(
f"Go work out!\nLocked for {self.lockout_time} seconds",
role="display",
scale=1.5,
color=self._colors.danger,
pad="lg",
)
self.countdown_label = self._label(
str(self.lockout_time),
role="display",
scale=3.75,
pad="lg",
)
self.remaining_time = self.lockout_time
self.update_lockout_countdown()
def update_lockout_countdown(self) -> None:
"""Update the lockout countdown timer display."""
if self.remaining_time > 0:
self.countdown_label.config(text=str(self.remaining_time))
self.remaining_time -= 1
self.root.after(1000, self.update_lockout_countdown)
else:
self._start_phone_check()
# ------------------------------------------------------------------
# Phone penalty
# ------------------------------------------------------------------
def _show_phone_penalty(
self, message: str, *, on_done: Callable[[], None] | None = None
) -> None:
"""Show penalty countdown when phone verification is unavailable."""
self.clear_container()
self._phone_penalty_done_fn: Callable[[], None] = (
on_done
if on_done is not None
else lambda: self._show_retry_and_sick(message)
)
base_delay = (
PHONE_PENALTY_DELAY_DEMO
if self.demo_mode
else PHONE_PENALTY_DELAY_PRODUCTION
)
# Disconnecting the phone shouldn't be a fast path into sick mode.
delay = (
base_delay
if self.demo_mode
else base_delay + NO_PHONE_EXTRA_LOCKOUT_SECONDS
)
self._label(
"Cannot Verify Workout",
role="display",
color=self._colors.warning,
pad="md",
)
self._text(message, color=self._colors.warning)
self._text(
"Connect phone via ADB to skip this wait,\n"
"or wait for the penalty timer.\n\n"
"Note: Phone must be rooted and StrongLifts installed.",
role="body",
)
self.phone_penalty_remaining = delay
self.phone_penalty_label = self._label(
str(delay),
role="display",
scale=2.5,
pad="md",
)
self._update_phone_penalty()
def _update_phone_penalty(self) -> None:
"""Update phone penalty countdown."""
if self.phone_penalty_remaining > 0:
self.phone_penalty_label.config(
text=str(self.phone_penalty_remaining),
)
self.phone_penalty_remaining -= 1
self.root.after(1000, self._update_phone_penalty)
else:
self._phone_penalty_done_fn()

View File

@ -0,0 +1,113 @@
"""Lockout countdown and phone-penalty flows for the locked screen.
Split out of :mod:`screen_locker._ui_flows` to keep every file under the
250-line cap. Composed back into ``UIFlowsMixin`` there, so the set of mixins
``ScreenLocker`` inherits is unchanged.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from screen_locker._constants import (
NO_PHONE_EXTRA_LOCKOUT_SECONDS,
PHONE_PENALTY_DELAY_DEMO,
PHONE_PENALTY_DELAY_PRODUCTION,
)
if TYPE_CHECKING:
from collections.abc import Callable
class LockoutFlowMixin:
"""Lockout countdown and the no-phone penalty timer."""
# ------------------------------------------------------------------
# Lockout flow
# ------------------------------------------------------------------
def lockout(self) -> None:
"""Display lockout screen with countdown timer."""
self.clear_container()
self.lockout_label = self._label(
f"Go work out!\nLocked for {self.lockout_time} seconds",
role="display",
scale=1.5,
color=self._colors.danger,
pad="lg",
)
self.countdown_label = self._label(
str(self.lockout_time),
role="display",
scale=3.75,
pad="lg",
)
self.remaining_time = self.lockout_time
self.update_lockout_countdown()
def update_lockout_countdown(self) -> None:
"""Update the lockout countdown timer display."""
if self.remaining_time > 0:
self.countdown_label.config(text=str(self.remaining_time))
self.remaining_time -= 1
self.root.after(1000, self.update_lockout_countdown)
else:
self._start_phone_check()
# ------------------------------------------------------------------
# Phone penalty
# ------------------------------------------------------------------
def _show_phone_penalty(
self, message: str, *, on_done: Callable[[], None] | None = None
) -> None:
"""Show penalty countdown when phone verification is unavailable."""
self.clear_container()
self._phone_penalty_done_fn: Callable[[], None] = (
on_done
if on_done is not None
else lambda: self._show_retry_and_sick(message)
)
base_delay = (
PHONE_PENALTY_DELAY_DEMO
if self.demo_mode
else PHONE_PENALTY_DELAY_PRODUCTION
)
# Disconnecting the phone shouldn't be a fast path into sick mode.
delay = (
base_delay
if self.demo_mode
else base_delay + NO_PHONE_EXTRA_LOCKOUT_SECONDS
)
self._label(
"Cannot Verify Workout",
role="display",
color=self._colors.warning,
pad="md",
)
self._text(message, color=self._colors.warning)
self._text(
"Connect phone via ADB to skip this wait,\n"
"or wait for the penalty timer.\n\n"
"Note: Phone must be rooted and StrongLifts installed.",
role="body",
)
self.phone_penalty_remaining = delay
self.phone_penalty_label = self._label(
str(delay),
role="display",
scale=2.5,
pad="md",
)
self._update_phone_penalty()
def _update_phone_penalty(self) -> None:
"""Update phone penalty countdown."""
if self.phone_penalty_remaining > 0:
self.phone_penalty_label.config(
text=str(self.phone_penalty_remaining),
)
self.phone_penalty_remaining -= 1
self.root.after(1000, self._update_phone_penalty)
else:
self._phone_penalty_done_fn()

View File

@ -0,0 +1,94 @@
"""Sick-day flow for the screen locker's locked screen.
Split out of :mod:`screen_locker._ui_flows` to keep every file under the
250-line cap. Composed back into ``UIFlowsMixin`` there, so the set of mixins
``ScreenLocker`` inherits is unchanged.
"""
from __future__ import annotations
from screen_locker import _sick_tracker
class SickDayFlowMixin:
"""Sick-day justification, countdown and debt bookkeeping."""
def ask_if_sick(self) -> None:
"""Display the structured sick-day justification dialog."""
self._show_sick_justification()
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
if self._adjust_shutdown_time_earlier():
return (
"Shutdown time moved 1.5 hours earlier ✓\n(Will revert tomorrow)"
), self._colors.success
return (
"Could not adjust shutdown time (check permissions)",
self._colors.danger,
)
def _proceed_to_sick_countdown(self) -> None:
"""Start the (escalated) sick day countdown after justification."""
history = getattr(
self,
"_sick_history_cache",
None,
)
if history is None:
history = _sick_tracker.load_history()
self._sick_history_cache = history
countdown = _sick_tracker.compute_lockout_seconds(history)
self.clear_container()
status_text, status_color = self._get_sick_day_status()
self._show_sick_day_ui(status_text, status_color, countdown)
self.sick_remaining_time = countdown
self._update_sick_countdown()
def _show_sick_day_ui(
self,
status_text: str,
status_color: str,
countdown: int,
) -> None:
"""Display sick day UI labels and countdown."""
self._label("Sick Day Mode", color=self._colors.warning, pad="md")
self._text(status_text, color=status_color)
minutes = countdown // 60
self._text(
f"Please wait ~{minutes} min before unlocking...",
role="title",
pad="md",
)
self.sick_countdown_label = self._label(
str(countdown),
role="display",
scale=2.5,
pad="lg",
)
def _update_sick_countdown(self) -> None:
"""Update the sick day countdown timer."""
if self.sick_remaining_time > 0:
self.sick_countdown_label.config(text=str(self.sick_remaining_time))
self.sick_remaining_time -= 1
self.root.after(1000, self._update_sick_countdown)
else:
self._finalize_sick_day()
def _finalize_sick_day(self) -> None:
"""Persist sick-day history and unlock the screen."""
history = getattr(self, "_sick_history_cache", None)
if history is None:
history = _sick_tracker.load_history()
if _sick_tracker.had_commitment_for_today(history):
_sick_tracker.mark_commitment_broken(history)
self.workout_data["broke_commitment"] = "true"
new_debt = _sick_tracker.add_sick_day(history)
_sick_tracker.save_history(history)
self.workout_data["type"] = "sick_day"
self.workout_data["note"] = "Sick day - shutdown moved earlier"
self.workout_data["debt"] = str(new_debt)
self.unlock_screen()