mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 21:27:13 +01:00
Six new modules, all composed or re-exported so no caller changes:
_shutdown.py (384) -> _shutdown_sick_state.py SickDayStateMixin
_wake_alarm.py WakeAlarmMixin
_phone_verification.py (380)-> _adb_transport.py AdbTransportMixin
_http_workout_fetch.py HttpWorkoutFetchMixin
_status_data.py (370) -> _status_types.py the 7 dataclasses
_status_projection.py shutdown projection
+ format_summary_line
Moving a name means moving where it is *read*, so 30-odd mock patch targets
move with it. Most importantly conftest.py's autouse _block_real_network
fixture: ruff correctly dropped the now-unused `socket` import from
_phone_verification, which silently removed the guard's target and would have
let subnet probes reach the real network. It now blocks socket in both modules
that actually own the probes, and both targets are asserted to exist.
976 tests pass at 100% branch coverage; pylint 8.70 -> 8.71; `screen_lock
--status` renders the real week unchanged. File-length gate: 41 -> 35.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
111 lines
3.9 KiB
Python
111 lines
3.9 KiB
Python
"""Shutdown projection and the one-line i3blocks summary.
|
|
|
|
Split out of :mod:`screen_locker._status_data` to keep every file under the
|
|
250-line cap. ``format_summary_line`` is re-exported from there, so
|
|
``status_view`` and the i3blocks CLI are unaffected.
|
|
|
|
Projects what time the PC will shut down on each remaining day of the week,
|
|
and renders the compact status line.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._extra_benefits import (
|
|
current_streak,
|
|
preview_bonus_if_week_ended_now,
|
|
weekly_shutdown_bonus_hours,
|
|
)
|
|
from screen_locker._shutdown import read_shutdown_config
|
|
from screen_locker._shutdown_base import get_base_hours
|
|
from screen_locker._status_types import ShutdownProjection, ShutdownProjectionDay
|
|
from screen_locker._weekly_check import count_weekly_workouts
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
from screen_locker._status_types import StatusSnapshot
|
|
|
|
_RELAXED_DAY_EXPLANATION = (
|
|
"Shutdown time and the screen lock are two separate systems: the lock "
|
|
"decides whether you must log a workout right now; shutdown time only "
|
|
"controls how late the PC stays on tonight."
|
|
)
|
|
_WEEKDAY_LABELS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
|
|
_MON_WED_WEEKDAYS = frozenset({0, 1, 2})
|
|
|
|
|
|
def _week_rows(
|
|
mon_wed_hour: int, thu_sun_hour: int, *, speculative: bool
|
|
) -> tuple[ShutdownProjectionDay, ...]:
|
|
"""Build 7 labeled rows, one per weekday, from a Mon-Wed/Thu-Sun band pair."""
|
|
return tuple(
|
|
ShutdownProjectionDay(
|
|
label=label,
|
|
hour=mon_wed_hour if weekday in _MON_WED_WEEKDAYS else thu_sun_hour,
|
|
speculative=speculative,
|
|
)
|
|
for weekday, label in enumerate(_WEEKDAY_LABELS)
|
|
)
|
|
|
|
|
|
def _shutdown_projection(
|
|
*,
|
|
shutdown_base_file: Path,
|
|
shutdown_config_file: Path,
|
|
extra_benefits_file: Path,
|
|
log_file: Path,
|
|
today_local: datetime,
|
|
) -> ShutdownProjection:
|
|
"""Build the shutdown-time projection: tonight, rest of week, next week."""
|
|
tonight = read_shutdown_config(shutdown_config_file)
|
|
base_mw, base_ts = get_base_hours(shutdown_base_file)
|
|
bonus = weekly_shutdown_bonus_hours(extra_benefits_file, today=today_local)
|
|
rest_of_week = _week_rows(base_mw + bonus, base_ts + bonus, speculative=False)
|
|
|
|
this_week_count = count_weekly_workouts(log_file, today=today_local)
|
|
streak = current_streak(extra_benefits_file)
|
|
_would_be_streak, would_be_bonus = preview_bonus_if_week_ended_now(
|
|
this_week_count, streak
|
|
)
|
|
next_week_preview = _week_rows(
|
|
base_mw + would_be_bonus, base_ts + would_be_bonus, speculative=True
|
|
)
|
|
|
|
return ShutdownProjection(
|
|
tonight=tonight,
|
|
rest_of_week=rest_of_week,
|
|
next_week_preview=next_week_preview,
|
|
explanation=_RELAXED_DAY_EXPLANATION,
|
|
)
|
|
|
|
|
|
def _tonight_hour(snapshot: StatusSnapshot) -> int | None:
|
|
"""Pick the shutdown hour band (Mon-Wed vs Thu-Sun) matching today.
|
|
|
|
Uses ``snapshot.generated_at`` (not a fresh ``datetime.now()``) so the
|
|
summary line is a pure function of the snapshot it was handed.
|
|
"""
|
|
if snapshot.shutdown.tonight is None:
|
|
return None
|
|
mon_wed_hour, thu_sun_hour, _morning = snapshot.shutdown.tonight
|
|
generated_weekday = (
|
|
datetime.fromisoformat(snapshot.generated_at).astimezone().weekday()
|
|
)
|
|
return mon_wed_hour if generated_weekday in _MON_WED_WEEKDAYS else thu_sun_hour
|
|
|
|
|
|
def format_summary_line(snapshot: StatusSnapshot) -> str:
|
|
"""Cheap one-liner for the i3blocks summary (e.g. before/after a click)."""
|
|
week = snapshot.week
|
|
mark = "✓" if week.remaining == 0 else "…"
|
|
tonight_hour = _tonight_hour(snapshot)
|
|
tonight_str = f"{tonight_hour:02d}:00" if tonight_hour is not None else "?"
|
|
sick = snapshot.sick_budget
|
|
return (
|
|
f"{mark} {week.counted_count}/{week.minimum} workouts · "
|
|
f"{tonight_str} tonight · sick {sick.used_7d}/{sick.budget_7d}"
|
|
)
|