utils/gatelock
Krzysztof kuhy Rudnicki d82107ab9a gatelock: bind the wheel per descendant in ScrollableSurface
Wheel-over-text did not scroll. Tk dispatches through *bindtags*, not the
parent chain -- a child's tags are (itself, its class, the toplevel, "all") --
so binding <Button-4>/<Button-5> on `content` was never consulted for an event
over a Label sitting on top of it. Content that fills its viewport covers that
frame completely, which is every screen this class exists for, so the mouse
wheel was dead exactly where a user points it.

Found while retiring leetcode-guard's hand-rolled _scrollable, which got this
right by accident: it used bind_all, which works but installs the handler on
every widget in the application, including other windows' scroll regions.
Binding per descendant inside the existing _track_focus walk keeps it scoped,
and reuses that walk's WeakSet so a repaint cannot stack duplicate handlers.

Verified both directions under xvfb (Button-5 scrolls down, Button-4 back up)
from an event generated on a Label, not on the canvas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U4Rj3s7K2qzrLNgJqqPhno
2026-08-15 14:12:30 +02:00
..
gatelock gatelock: bind the wheel per descendant in ScrollableSurface 2026-08-15 14:12:30 +02:00
.pre-commit-config.yaml Stop the recovery tick stealing our own grab, and fix pre-commit for the monorepo 2026-07-26 15:08:41 +02:00
LICENSE Initial gatelock: shared lock-window + HMAC log-integrity backend 2026-06-19 17:39:44 +02:00
pyproject.toml gatelock 0.4.1: re-focus a surface that comes back after a blip 2026-08-12 22:13:46 +02:00
README.md gatelock: warm-tint the unified-design-system palette, add on-fill token 2026-07-21 21:11:07 +02:00
requirements.txt Initial gatelock: shared lock-window + HMAC log-integrity backend 2026-06-19 17:39:44 +02:00

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