On 2026-08-03 the screen locker's lock screen scrolled to mid-content and back with nobody touching the keyboard, on a 1366x768 panel. Measured under Xvfb with the real app: one programmatic focus_set() produced three scrolls, frac 0.000 -> 0.436 -> 0.145. Two causes, both here: * ScrollableSurface followed every <FocusIn>. The apps focus a widget on every repaint (and gatelock re-asserts focus from its recovery tick), and Tk delivers <FocusIn> to the focused widget's whole ancestor chain when the toplevel takes X focus -- so each repaint aimed a scroll at containers taller than the screen. Now a focus move scrolls only when it followed a real keypress, or a click on the widget that took focus, and only for a leaf. Tab onto a clipped field still reveals it; nothing else moves. * finalize()'s reset-to-top cancelled the centring, because centring moved the canvas item down and the region's top edge with it. The region is now pinned to the viewport whenever the content fits, so the view cannot move at all, the scrollbar is taken down, and centring survives the repaint. Content that does not fit is a layout defect, not a mode to live in, so finalize() reports it at warning and gatelock.measure_fit() lets each app gate on it before shipping (scripts/verify_screen_fits.py in all four). Also: LockConfig's type/space scales now compact below 768px (_density), which is where a screen has no room to be at design-system size and no scrollbar to make up for it. 768 and up is verified to fit at full size, so it is never compacted. Tests: _scrollable had none; the regression is now stated as code (programmatic focus must not move yview; Tab must). 323 pass, coverage 95.38% -> 99.44%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsXnTHyuuCUZsqetTaajcF |
||
|---|---|---|
| .. | ||
| gatelock | ||
| .pre-commit-config.yaml | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
| requirements.txt | ||
gatelock
Shared fullscreen lock-window + HMAC log-integrity backend, extracted from three personal automation tools that each independently implemented "lock the screen with a blocking overlay until a condition is met": diet_guard, wake_alarm, and screen-locker.
Why
All three reimplemented the same tkinter fullscreen mechanics (overrideredirect,
input grab, VT-switch disable) at different levels of maturity, and the
HMAC-signed-state module had already been hand-copied between two of them
because they live in separate repos. gatelock is the one place that logic
now lives.
Install
pip install "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.1#subdirectory=gatelock"
Usage
import tkinter as tk
from gatelock import GateRoot, LockConfig, LockWindow
class MyGate:
def __init__(self, *, demo_mode: bool) -> None:
self.root = GateRoot()
self.root.on_callback_error = self._handle_callback_error
config = LockConfig(mode="soft" if demo_mode else "hard")
self._lock = LockWindow(self.root, config, hooks=self)
self._lock.setup()
# ... build your widgets ...
self._lock.grab_input()
def on_focus_ready(self) -> None:
self.my_entry.focus_force()
def on_callback_error(self) -> None:
self.close()
def on_close(self) -> None:
... # release any hardware/state held while locked
def close(self) -> None:
self._lock.close()
def run(self) -> None:
self._lock.run()
LockConfig's mode preset bundles the common combination ("soft" = topmost
only, typeable, WM-escapable; "hard" = overrideredirect + global grab +
VT-disable). Each axis (overrideredirect, grab, disable_vt,
grab_retry_ms) can be set explicitly to reproduce a consumer's exact prior
behavior where it diverges from the preset.
gatelock.log_integrity ports the HMAC-signed state module used by all three
projects; DEFAULT_HMAC_KEY_FILE (/etc/workout-locker/hmac.key) is
unchanged so existing signed history keeps verifying.
Development
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest
pre-commit install && pre-commit install --hook-type pre-push