mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 20:27:13 +01:00
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
Lending the X grab to the workout app stops recovery and releases it with the claim still live, so gatelock 0.11 held_grabs (leisure-watch C2) read "held" for the whole workout. release() now reports held=False before grab_release; reacquire()'s grab_input() sets it true once the grab lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dsh4Znwh2KnC7q6vbGpX1N
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…"
|