mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Real fixes: split over-long functions (lock-decision chain, manual sync, validation, phone verification, status), hoist a dead lazy import, context-manage Xvfb and temp dirs, add encodings, share the silent-failure argv loop. lock_grab_handoff read lock._recovery, which gatelock's LockWindow does not have (its public property is recovery); a contract test now checks every attribute the handoff reads against the real class. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
88 lines
3.7 KiB
Python
88 lines
3.7 KiB
Python
"""Handing the lock screen to the workout app, and taking it back.
|
|
|
|
The Tk locker cannot simply ``close()`` to let the app take over: ``close()``
|
|
also restores VT switching, which would hand the user Ctrl+Alt+F2 and an exit
|
|
from the very obligation the lock exists to enforce. Only the *grab* moves.
|
|
|
|
The recovery watchdog has to be stopped for the duration, and that is the part
|
|
that is easy to get wrong. It is built never to release -- it re-takes a grab
|
|
it finds missing, deliberately, so that pulling a cable is not an escape. Left
|
|
running it would treat the Flutter app exactly like an intruder and steal the
|
|
screen back within one verify tick, mid-workout. So the order is: stop the
|
|
watchdog, release the grab, run the app, re-take the grab, restart the
|
|
watchdog. The Tk window itself stays mapped underneath the whole time, which
|
|
is what keeps the screen covered if the app dies.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import logging
|
|
import tkinter as tk
|
|
from typing import TYPE_CHECKING
|
|
|
|
from screen_locker._workout_app import GrabHandoff, launch_workout_app
|
|
|
|
if TYPE_CHECKING:
|
|
from gatelock import LockWindow
|
|
|
|
__all__ = ["lock_grab_handoff", "start_workout"]
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def lock_grab_handoff(lock: LockWindow) -> GrabHandoff:
|
|
"""Build the release/reacquire pair for a live lock window.
|
|
|
|
Neither direction touches VT switching or the arbiter claim: this is a
|
|
loan of the grab, not a teardown.
|
|
"""
|
|
|
|
def release() -> None:
|
|
# Stop first: a watchdog tick between the release and the app's own
|
|
# grab would take the screen straight back off it.
|
|
lock.recovery.stop()
|
|
# Claim.grab_held false BEFORE the release (fail closed), so
|
|
# leisure-watch never reads "held" while the workout app has input.
|
|
# reacquire() -> grab_input() sets it true once the grab lands.
|
|
lock.report_grab_held(held=False)
|
|
with contextlib.suppress(tk.TclError):
|
|
lock.root.grab_release()
|
|
_logger.warning(
|
|
"Released the X grab to the workout app; the lock window stays "
|
|
"mapped and VT switching stays disabled."
|
|
)
|
|
|
|
def reacquire() -> None:
|
|
# Unconditional and idempotent: whatever happened to the app, this
|
|
# machine must not be left with nothing holding the screen.
|
|
lock.grab_input()
|
|
_logger.warning("Re-took the X grab from the workout app.")
|
|
|
|
return GrabHandoff(release=release, reacquire=reacquire)
|
|
|
|
|
|
def start_workout(lock: LockWindow) -> str:
|
|
"""Run the workout app as the lock surface; return a status for the UI.
|
|
|
|
Never raises: this is wired to a button on a screen the user cannot leave,
|
|
so a failure has to come back as a message rather than a traceback that
|
|
would leave the grab in an unknown state.
|
|
|
|
Blocks until the app exits, so it is only safe for a caller with no event
|
|
loop to starve. Behind a Tk button use :class:`WorkoutSession` instead:
|
|
blocking there freezes the loop, which makes every binding on the lock --
|
|
including the escapes -- inert until the app exits.
|
|
|
|
An earlier version of this docstring claimed the blocking was deliberate,
|
|
to stop the recovery watchdog ticking mid-workout. That was wrong:
|
|
``release`` stops the watchdog first, and ``stop()`` cancels its timers
|
|
outright, so there are no ticks to race with either way.
|
|
"""
|
|
result = launch_workout_app(lock_grab_handoff(lock))
|
|
if not result.launched:
|
|
_logger.error("Could not start the PC workout: %s", result.reason)
|
|
return f"Could not start the workout: {result.reason}"
|
|
_logger.warning("The workout app finished: %s", result.reason)
|
|
return "Workout app closed — checking whether it counted…"
|