utils/gatelock/scripts/prompt_grab_helper.py
Krzysztof kuhy Rudnicki 2d341dd41e
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
gatelock pre-commit / pre-commit (push) Waiting to run
gatelock tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
gatelock 0.13.0: locks stay clickable together
A keyring prompt (gcr-prompter) that grabbed input before the
leetcode lock armed left the lock visible but unclickable: every
click went to the prompt hidden under it. Each failed
grab_set_global also froze Tk ~1 s, so the lock barely repainted.

- Raise known credential prompts above the lock after 2 s of a
  blocked grab; games and other grabbers stay covered.
- Probe the grab on a side Xlib connection first so a blocked
  grab never stalls the Tk loop (worst gap 1.05 s -> 0.05 s).
- Log the Tk grab error and stop blaming our own claim.
- 1.5 s startup grace in wait_for_turn, and a weaker armed lock
  steps back (windowless, re-arms later) when a stronger one is
  live, so only one lock is ever on screen.
- Real-X checks: verify_prompt_yield, verify_grab_responsive,
  verify_all_lockers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWZbpxQZj41WpbQPuxB
2026-11-11 12:49:40 +01:00

74 lines
2.4 KiB
Python
Executable File

#!/usr/bin/env python3
# Copyright (c) 2026 Krzysztof Rudnicki
# pylint: disable=import-error
"""A second X client that holds the keyboard+pointer grab, like a credential prompt.
Started by ``verify_prompt_yield.py`` on a private display. Maps one window with
the given ``WM_CLASS`` (optionally inside an override-redirect frame, the way a
window manager reparents clients), takes ``XGrabKeyboard`` + ``XGrabPointer``,
prints ``READY``, then waits on stdin: ``release`` ungrabs and unmaps (prints
``RELEASED``), EOF exits.
Usage: ``prompt_grab_helper.py <wm_class> [--framed]``
"""
from __future__ import annotations
import sys
import time
from Xlib import X, display
OWNER_EVENTS = True
VIEWABLE_TRIES = 50
def main(argv: list[str]) -> int:
"""Grab, announce, wait for ``release``; returns a process exit status."""
wm_class = argv[0]
xdisplay = display.Display()
root = xdisplay.screen().root
depth = xdisplay.screen().root_depth
top = window = root.create_window(
10, 10, 400, 200, 0, depth, X.InputOutput, X.CopyFromParent
)
if "--framed" in argv:
top = root.create_window(
10, 10, 400, 200, 0, depth, X.InputOutput, X.CopyFromParent,
override_redirect=1,
) # fmt: skip
window.reparent(top, 0, 0)
window.set_wm_class(wm_class, wm_class.capitalize())
window.map()
top.map()
xdisplay.sync()
# A window manager maps asynchronously; grabbing a not-yet-viewable window
# fails with GrabNotViewable.
for _ in range(VIEWABLE_TRIES):
if window.get_attributes().map_state == X.IsViewable:
break
time.sleep(0.1)
modes = (X.GrabModeAsync, X.GrabModeAsync)
keyboard = window.grab_keyboard(OWNER_EVENTS, *modes, X.CurrentTime)
pointer = window.grab_pointer(
OWNER_EVENTS, X.ButtonPressMask, *modes, X.NONE, X.NONE, X.CurrentTime
)
if keyboard or pointer:
sys.stdout.write(f"grab failed keyboard={keyboard} pointer={pointer}\n")
return 2
sys.stdout.write("READY\n")
sys.stdout.flush()
for line in sys.stdin:
if line.strip() == "release":
xdisplay.ungrab_keyboard(X.CurrentTime)
xdisplay.ungrab_pointer(X.CurrentTime)
top.unmap()
xdisplay.sync()
sys.stdout.write("RELEASED\n")
sys.stdout.flush()
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))