`_holds_grab` asked `grab_current() is self._root`, but our own transient children take the grab for themselves: a posted Tk menu grabs the menu window, so the test read "the grab was lost" and `grab_set_global()` took it back a second later, off our own widget mid-interaction. That is what made screen-locker's sport selector unusable while locked on 2026-07-26. The predicate is now `grab_current() is not None` -- any window we own means the lock still has pointer and keyboard -- and it is public, `holds_grab`, so an embedder can ask the same question instead of re-deriving it. Anything that does not resolve against our widget map (a Tcl-created `ttk.Combobox` popdown, a stale name) raises `KeyError` and counts as lost: fail closed, because losing a popdown beats believing we hold a grab we do not. The blind spot for a second `tk.Tk()` reporting its root as "." is unchanged from the old test, and now written down. `_verify` and `_drain` scheduled the next tick *after* running it, so any exception ended all re-assertion of coverage, grab and VT for the rest of the lock -- the exact fail-open this module exists to prevent, in a file whose docstring is a monotonicity proof. The reschedule moves into `finally`, and the warnings only fire when the reason changes: these states can last the whole lock, and a 1Hz journal stream is not the alternative to silence. The pre-commit side: both subproject configs were written as if cwd were the subproject, but pre-commit runs hooks from the git root -- which is ~/utils, not gatelock. Every pushed run since at least 2026-07-11 has been red, and for compounding reasons: pylint and bandit could not find their config, bandit's tests exclusion never matched, the pytest hook ran with no config at all, and with no `files:` filter each config linted the *whole* monorepo, so gatelock's mypy was type-checking crdt-sync. All paths are now root-relative, each config is scoped to its own subproject, and the pytest hook goes through a shared `scripts/run_subproject_tests.sh` that supplies the missing `cd` and fails loudly if the subproject is gone. Scoping via `files:` has one sharp edge -- rename the directory and the regex matches nothing, so every hook passes on zero files, green and useless. An `always_run` scope-guard hook turns that silent pass into a failure. bandit 1.7.10 threw inside its AST visitor on Python 3.14 and *skipped* every file while still reporting zero issues; 1.9.4 actually scans. Codespell was also flagging a real typo the noise had buried. Housekeeping the above turned up: `gatelock/.github/` and `crdt-sync/.github/` were dead -- GitHub only reads the root `.github/workflows`, which already has all four jobs -- and the two subproject `.gitignore`s were byte-identical to each other and subsumed by the new root one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| 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