Input relay: release keys the desktop has down that no keyboard holds

After a gaze calibration, Meta stayed down in KWin: typing opened the overview,
and clicks on the desktop did other things. A key can be left down there when
its device vanishes with it held (release_held only let go of it on the relay's
own devices; the Z3's keyboard dropped out twice right then) or a release goes
astray. The relay now remembers which keys it told ft-screens went down, and
once a second releases any no device holds (EVIOCGKEY), and the key
combinations forget a Meta or modifier no device holds. A relay that starts
releases the modifiers on the desktop, for keys an earlier one left down.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-01 11:49:51 -06:00
1 parent da3e3d66a7
commit 2ae10a3b65
1 file changed
+38 -1
+38 -1
View File
@@ -790,13 +790,16 @@ def main():
return True
screens_down = set() # keys the desktop was told went down and not yet up (see reconcile_desktop_keys)
def to_screens(code, value):
"""A key for the desktop screens (ft-screens decides whether it types)."""
if value in (0, 1) and code < BTN_MISC:
try:
screens_sock.sendto(f"key {code} {value}".encode(), SCREENS)
except OSError:
pass # ft-screens not running
return # ft-screens not running
(screens_down.add if value else screens_down.discard)(code)
nodes = {} # fd -> Node
# Nodes already probed (rejected or open): path -> inode. A device that disconnects and
# reconnects between two scans often gets the same event numbers back, so the path alone
@@ -878,6 +881,35 @@ def main():
mouse.sync()
keyboard.sync()
def physically_down():
"""The keys down on every device read here, as the kernel has them (EVIOCGKEY)."""
down = set()
for node in nodes.values():
buf = bytearray((KEY_MAX + 8) // 8)
try:
fcntl.ioctl(node.fd, EVIOCGKEY, buf)
except OSError:
continue
down.update(i * 8 + bit for i, b in enumerate(buf) if b for bit in range(8) if b >> bit & 1)
return down
def reconcile_desktop_keys():
"""A key the desktop has down that no device holds comes up there, and the key
combinations forget a Meta or modifier no device holds. A key can be left down when
its device vanishes with it held (release_held only lets go of it here) or a release
goes astray: on 2026-10-01, after a calibration, Meta stayed down in KWin, so typing
opened the overview and clicks on the desktop did other things."""
if not screens_down and not held_meta and not held_modifiers:
return
down = physically_down()
for code in sorted(screens_down - down):
to_screens(code, 0)
log(f"key {code} released on the desktop: no keyboard holds it")
for code in [c for c in held_meta if c not in down]:
held_meta.discard(code)
meta_hidden.discard(code)
held_modifiers.intersection_update({MODIFIERS[c] for c in down if c in MODIFIERS})
def keys_down(node):
buf = bytearray((KEY_MAX + 8) // 8)
try:
@@ -1000,11 +1032,16 @@ def main():
vr_bind(time.monotonic()) # a helper that's already running keeps its buttons in step
waiting = False # a keyboard's grab waits for its keys to come up
# A relay that went away with a key down left it down on the desktop, where this one
# never sent it: modifiers come up there now (a release of a key that isn't down is nothing).
for code in sorted(MODIFIERS):
to_screens(code, 0)
while True:
now = time.monotonic()
pointer = state["pointer"]
if now >= next_scan:
next_scan = now + 1.0
reconcile_desktop_keys()
current = {}
for name in os.listdir("/dev/input"):
if name.startswith("event"):