utils/gatelock/README.md
Krzysztof kuhy Rudnicki 97d043d424 gatelock 0.12.0: corner strip, 180 s wait
Add gatelock.corner_strip, the top-right always-on-top status strip for a
gate that is stood down but not done. Raise the morning-session wait from
30 s to 180 s: on 2026-11-07 a boot-time DNS race let the 30 s wait expire
and both lockers armed mid morning-session; wake-alarm's refresher now
retries 150 s in-process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZd3smuxDGmEgXkYP8
2026-11-07 08:09:40 +01:00

108 lines
4.5 KiB
Markdown

# 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](https://github.com/kuhyx/testsAndMisc/tree/main/python_pkg/diet_guard),
[wake_alarm](https://github.com/kuhyx/testsAndMisc/tree/main/python_pkg/wake_alarm),
and [screen-locker](https://github.com/kuhyx/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
```bash
pip install "gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.1.1#subdirectory=gatelock"
```
## Usage
```python
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). `overrideredirect` and the `grab=GrabPolicy(kind=..., disable_vt=...,
retry_ms=...)` axes can be set explicitly to reproduce a consumer's exact prior
behavior where it diverges from the preset. Colours, type scale and spacing are
the `palette`, `typography` and `spacing` records (`gatelock.LockPalette` etc.),
overridden as a unit.
`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.
`gatelock.morning_session.morning_skip` is the one reader of wake-alarm's
signed `morning_session.json` (the wake-early carrot: no lock until the signed
`exempt_until`, 11:00 for a morning completed in time). screen-locker,
leetcode-guard and book-guard all call it; the policy itself stays in
wake-alarm. The arming run waits up to 180 s for a freshly booted PC's
refresher (status paths never wait). Path and retry budget are arguments so
each consumer's tests keep redirecting their own constant.
`gatelock.corner_strip` is the top-right always-on-top status strip for a gate
that is stood down but not done (`build_corner_strip`, `CornerStrip`,
`StripContent`, `corner_rect`, `corner_geometry`). It never grabs input; its
tick ends when the owner's `supply` returns `None`.
`gatelock.night_lockdown.evening_lockdown_today` answers "has tonight's
evening night-lockdown already happened?" from night-lockdown's
`/var/lib/night-lockdown/locked_at_epoch` (local date today, local time
>= 12:00; the 04:01 morning re-lock does not count). Earner gates
(screen-locker's workout lock, leetcode-guard, book-guard) opt in and do not
re-arm for the rest of such a day; diet-guard does not. Missing or garbage
means "no lockdown" (garbage warns), so the gate locks as before.
`gatelock.held_grabs(claims_dir=None) -> tuple[Claim, ...]` answers "is a
locker's X grab ACTUALLY held right now?": live claims (flock-proved) whose
`grab_held` is true and whose declared grab is not `none`. `grab_held` turns
true only after `grab_set`/`grab_set_global` succeeded, follows each recovery
tick, and is cleared/restored around a deliberate release via
`LockWindow.report_grab_held(held=...)` (or `Arbiter.set_grab_held`) --
leetcode-guard's study mode. It is rewritten in place under the owner's flock.
A claim from a pre-0.11 writer has no key and reads as `grab != "none"`. Root
has no `XDG_RUNTIME_DIR`, so a root reader passes the user's dir
(`/run/user/1000/gatelock/claims`).
## Development
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest
pre-commit install && pre-commit install --hook-type pre-push
```