utils/gatelock/gatelock/_hooks.py
Krzysztof kuhy Rudnicki 940bf19d54 gatelock: split _window.py 571 -> 243 lines across four modules
_window.py was the repo's largest file and could not be read in one piece.
Split along the seams the file already marked out, keeping the public API
and every existing import path intact:

  _config.py  (215) — LockConfig, the design tokens and preset resolution
  _arming.py  (178) — the way up: VT-disable, backdrop, surfaces, input grab
  _preempt.py (118) — naming a blocked grab's holder, standing a weaker one down
  _hooks.py   (51)  — the LockWindowHooks Protocol, pure interface
  _window.py  (243) — construction, focus, and teardown

_arming takes an explicit ArmingCollaborators bundle rather than the window
itself, mirroring the existing RecoveryCollaborators; setup() returns whether
VT was disabled instead of mutating the window. That leaves no reach-back into
private members (ruff SLF001 clean) and makes the arming sequence testable
without a window.

LockConfig is re-exported from _window, so `from gatelock._window import
LockConfig` — which five sibling modules and the tests already do — still
resolves. Tests that patched moved symbols were retargeted to their new home;
the dead _maybe_preempt delegate, whose only callers were tests, is gone.

389 passed, 100% branch coverage — identical to before the split.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
2026-08-22 14:20:11 +02:00

52 lines
2.0 KiB
Python

"""The callback interface an app embedding :class:`~gatelock.LockWindow` fills.
Split out of :mod:`gatelock._window` so the pure *interface* -- what an app
must implement -- can be read without the window mechanics that call it. This
module deliberately imports nothing from the runtime half, so an app can type
against the protocol without pulling in Tk window machinery.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol
if TYPE_CHECKING:
import tkinter as tk
from gatelock._surfaces import SurfaceInfo
class LockWindowHooks(Protocol):
"""Callbacks :class:`LockWindow` invokes; the embedding app supplies all."""
def build_surface(self, parent: tk.Misc, surface: SurfaceInfo) -> None:
"""Build this app's widgets inside ``parent``, for one output.
Called once per live output, and again whenever an output comes back.
Tk variables should stay mastered on the root so every surface shows
the same state, and every surface's submit path should call the same
handler -- solving the lock on any monitor dismisses all of them.
"""
def teardown_surface(self, surface: SurfaceInfo) -> None:
"""Forget per-surface state; that output went dark and is closing."""
def on_focus_ready(self, surface: SurfaceInfo | None) -> None:
"""Called once the lock is mapped and (if applicable) grabbed.
Args:
surface: The surface that took initial focus, or None when no
output is live and there is therefore nothing to focus.
"""
def on_callback_error(self) -> None:
"""Called when a Tk callback raised (see :class:`~gatelock.GateRoot`)."""
def on_close(self) -> None:
"""Called once, from :meth:`LockWindow.close`, before VT is restored.
Runs on every exit path -- normal dismiss, SIGTERM, SIGINT -- not just
a clean close, so app-specific teardown (restoring hardware state,
etc.) can't be skipped by killing the process.
"""