mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-15 01:50:58 +01:00
f0be8c5919
8 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| f0be8c5919 |
Make lock surfaces keyboard-operable and safe on a 768px screen
gatelock v0.3.0. Every change here works around a Tk *default*, not an application mistake -- which is why they belong in the shared backend rather than being fixed four times. On a lock surface these are not accessibility niceties. The lockers hold a global input grab with VT switching disabled, so a control the keyboard cannot reach, or a button clipped off the bottom of the screen, is a bricked machine. LockConfig gains a type scale, a spacing scale and a focus-ring token: - font() applies Tk's sign convention. Tk encodes the unit in the SIGN of the font size -- positive is points, negative is pixels -- so passing the design system's px values straight through rendered every string ~30% oversized (measured: body at 16pt has a 26px linespace where 16px gives 20). That alone pushed diet_guard's meal gate to 866px of content inside a 739px pane on a 1366x768 screen. - focus_kwargs() sets highlightcolor, the *focused* ring. Tk's default is a 1px BLACK ring, invisible against the palette's #211D1B background. Note highlightbackground is the UNfocused ring: setting only that one inverts the affordance, which is what diet_guard was doing. _keyboard.py collects three Tk defaults that each make a form unusable: - tk.Text traps Tab (inserts a literal tab and refocuses itself) and no-ops Shift-Tab; the only exits are the undocumented Ctrl+Tab / Ctrl+Shift+Tab. - tk.Button binds <space> on X11 but never <Return>, so the key a user reaches for to submit does nothing. - Escape needs binding at the toplevel to work wherever focus happens to sit. _scrollable.py adds ScrollableSurface, a viewport that content can actually be reached in: - A place()-centred frame takes its *requested* size and is clipped SYMMETRICALLY, so overflow removes the heading and the submit button at the same time, with no scrollbar and no hint anything is missing. - A bare tk.Canvas has no class key bindings, so ::tk::FocusOK rejects it as a focus stop and the only way to scroll is dragging the thumb with a pointer. - Canvas clipping does not unmap children, so scrolled-out fields stay in the tab ring while Tk never scrolls to follow focus -- Tab lands on fields the user can neither see nor reveal. - A Canvas holds content in a canvas *item*, so it never propagates the content's requested size; a toplevel told to size itself has nothing to grow to. _request_content_size() fixes that, clamped to the display. center_when_fits keeps short screens looking exactly as they did, so the viewport can replace a centred frame without a visual change. A scrollable dialog Toplevel is deliberately NOT the mechanism: on an override-redirect lock surface a second toplevel steals the Tk grab, which the recovery tick then kills -- the 2026-07-26 failure where a frozen sport selector logged a walk as table tennis. unified-design-system gains the two rule families this implements (pointer-free operability, 1366x768 usable / 1024x600 graceful), a focus-ring token, and the px/pt sign convention written down so it cannot recur. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 206740889c |
gatelock 0.2.1: the grab predicate and recovery-loop fixes
Some checks failed
crdt-sync pre-commit / pre-commit (push) Has been cancelled
crdt-sync tests / test (3.10) (push) Has been cancelled
crdt-sync tests / test (3.11) (push) Has been cancelled
crdt-sync tests / test (3.12) (push) Has been cancelled
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
Releases the two fixes from the previous commit so the four consumers (screen-locker, diet-guard, wake-alarm, testsAndMisc) can pin them: holds_grab no longer steals the grab back off our own posted menus, and a raising tick no longer permanently stops the recovery loop. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
|||
| 39e52d07c2 |
Stop the recovery tick stealing our own grab, and fix pre-commit for the monorepo
`_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> |
|||
| 7327c287f4 |
gatelock: extract the shared escape-hatch core
Every locker here already had a bounded way out -- screen-locker's sick mode, diet-guard's log-a-meal -- except the wake alarm, which had only the challenge itself. Rather than copy sick mode a third time, the mechanism moves into gatelock so all three consumers share one implementation and one set of budget rules. `_escape.py` owns the parts that are policy-free: rolling 7/30/90-day budgets, the justification draft and its validation, the escalating lockout that doubles with each recent use, HMAC-signed history entries, and the "last ten, played back to you" summary. App-specific policy (screen-locker's commitment penalty and workout debt) stays in the consumer. Entries that fail HMAC verification are KEPT rather than dropped. Dropping them looks safer but is not: `verify_entry_hmac` also returns False when the key is merely unreadable, so "discard what you cannot verify" would turn a chmod on the key file into an unlimited escape hatch. They stay in the budget count and are reported through a `tampered` counter instead. 278 tests, 100% branch coverage, no lint suppressions. |
|||
| a9b65812d7 |
gatelock v0.2.0: per-output locking, monotonic recovery, N-app arbitration
On 2026-07-25 the PC appeared frozen and needed an SSH rescue. Three defects
stacked:
1. DP-0 came up `connected` with a valid EDID and no mode/CRTC -- physically
dark, while X still reserved its 3840px slice of the root window.
2. diet-guard-gate and workout-locker each mapped an override-redirect window
spanning the WHOLE root (6400x1440+0+0), centring their controls on the
primary output -- the dark one. The escape UI was invisible.
3. They deadlocked on the X grab, and the loser logged "another app -- e.g. a
fullscreen game -- holds it". No game was running; the holder was the other
locker. That guess sent the diagnosis the wrong way for the whole outage.
The library had no concept of an individual output: setup() sized the window
from winfo_screenwidth(), the union bounding box of every output.
What changes:
* _outputs.py An output is LIVE only if connected AND has a mode/CRTC.
RandR (crtc==0 or mode==0) and an xrandr --query parser derive
that predicate independently; verified to agree exactly on the
real DP-0/HDMI-0 layout. Fails closed three distinct ways.
* _surfaces.py One window per live output at its own geometry, and the only
module allowed to create/move/destroy windows. Explicit
geometry replaces attributes(fullscreen=True), which an
override-redirect window is invisible to anyway.
* _detect.py RandR events + Tk <Configure> + xrandr polling, coalesced. The
RandR thread runs on a private X connection and may only call
queue.put -- Tk is not thread-safe.
* _recovery.py Re-asserts coverage, placement, grab and VT on a timer. Proven
monotonic: it may only ADD coverage or RE-ACQUIRE the grab. An
ast test asserts the file contains none of grab_release /
restore_vt_switching / close / destroy / quit / withdraw, while
disable_vt_switching and grab_set_global ARE present. Yanking
every cable therefore does not unlock anything.
* _arbiter.py N-app priority: wake_alarm > screen_locker > diet_guard.
Liveness is proved by flock-on-death -- no heartbeat, no PID
scan, no clock. A two-axis strength check stops rank ever
weakening the lock: an app stands down only for an incumbent
at least as strong on BOTH grab kind and VT disabling.
* _guards.py wait_for_x_server replaces wait_for_display and gates ONLY on
an X server existing. Output count must never gate arming:
zero live outputs means lock without showing, not decline to
lock. That inversion is what left the machine unlocked.
* _window.py Arms first, renders second. Names the real grab holder instead
of blaming a fullscreen game. Releases the arbiter on close so
the alarm -> workout handoff cannot strand the next app.
Two bugs found by running it, not reading it:
* acquire_holder() opened the holder file "w", which truncates at open() --
before the lock attempt. A losing contender erased the incumbent's claim, so
the app that most needed to know who held the screen found an empty file.
* tk.Toplevel over a MagicMock sends real tkinter into infinite mock recursion;
the suite hung instead of failing, and an unpatched setup() would have run
setxkbmap against the developer's own machine. conftest now has an autouse
hermetic fixture; the suite went from >120s to 0.7s.
235 tests, 100% branch coverage, ruff (select=ALL) and mypy --strict clean.
No suppressions added: Protocols replace Any for python-xlib, public attribute
access replaces ._data, and type-only Protocols live under TYPE_CHECKING.
Consumers still pin gatelock-v0.1.1 and are unmigrated; nothing ships until a
tag and a reinstall into system python.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
|||
| 63badeaed5 |
gatelock: warm-tint the unified-design-system palette, add on-fill token
Mirrors the palette rework already applied to diet-guard/dufs-cloud/ screen-locker. Adds `on_fill` (dark) for text drawn on a filled accent/ success/warning/danger surface -- near-white text under-contrasts on all four (measured 2.2-3.6:1, below the 4.5:1 floor); dark text passes 5.8-9.7:1 on every one of them. Bumps to 0.1.1 since this is a real release: the 3 consumer repos pin gatelock to a git tag, not editable. |
|||
| d69598880a |
Docs: correct stale 'not yet consumed' notes and old install URLs
Some checks failed
crdt-sync pre-commit / pre-commit (push) Waiting to run
crdt-sync tests / test (3.10) (push) Waiting to run
crdt-sync tests / test (3.11) (push) Waiting to run
crdt-sync tests / test (3.12) (push) Waiting to run
gatelock pre-commit / pre-commit (push) Has been cancelled
gatelock tests / test (3.10) (push) Has been cancelled
gatelock tests / test (3.11) (push) Has been cancelled
gatelock tests / test (3.12) (push) Has been cancelled
Both CRDT libs are now consumed (crdt_sync_dart: diet_guard app, workout_app, phone_app, todo; crdt-sync: diet-guard, screen-locker). Reconcile the sub-package READMEs' old standalone git URLs to the monorepo-subdirectory form, and bump the crdt_sync examples to v0.2.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019KwUCPESvE5CjCtmUFz8xQ |
|||
| 5f7553de91 |
Initial gatelock: shared lock-window + HMAC log-integrity backend
Extracted from wake_alarm, screen-locker, and diet_guard, which each independently implemented fullscreen-lock, input-grab, VT-disable, and HMAC-signed-state mechanics at different levels of maturity (the HMAC module was already a hand-copied duplicate between two of them). LockConfig exposes overrideredirect/grab/disable_vt as independent axes so one LockWindow can reproduce all three projects' exact prior behavior, plus screen-locker's confirmed upgrade to retry-forever grab. |