Notice a mouse or keyboard that reconnects between device scans

The relay scanned /dev/input once a second and only probed paths it hadn't
seen. A Bluetooth mouse that disconnects and reconnects within that second
often gets the same event numbers back, so the relay never grabbed it
again and the mouse stopped working. Nodes are now tracked by inode, so
a re-created node is probed even in a known place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-26 19:56:43 -06:00
1 parent 54b1e5cd4b
commit fb2d62e1da
1 file changed
+21 -6
+21 -6
View File
@@ -466,7 +466,10 @@ def main():
except OSError: except OSError:
pass # ft-screens not running pass # ft-screens not running
nodes = {} # fd -> Node nodes = {} # fd -> Node
seen = set() # paths already probed (rejected or open) # 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
# would hide it; the re-created node has a new inode.
seen = {}
next_scan = 0.0 next_scan = 0.0
def role_of(node): def role_of(node):
@@ -503,7 +506,7 @@ def main():
release_held(node) release_held(node)
os.close(node.fd) os.close(node.fd)
del nodes[node.fd] del nodes[node.fd]
seen.discard(node.path) seen.pop(node.path, None)
log(f"released {node.name} ({node.path}): {reason}") log(f"released {node.name} ({node.path}): {reason}")
def reply(addr, obj): def reply(addr, obj):
@@ -557,11 +560,23 @@ def main():
pointer = state["pointer"] pointer = state["pointer"]
if now >= next_scan: if now >= next_scan:
next_scan = now + 1.0 next_scan = now + 1.0
paths = {f"/dev/input/{n}" for n in os.listdir("/dev/input") if n.startswith("event")} current = {}
seen &= paths | {n.path for n in nodes.values()} for name in os.listdir("/dev/input"):
if name.startswith("event"):
try:
current[f"/dev/input/{name}"] = os.stat(f"/dev/input/{name}").st_ino
except OSError:
pass
for path in [p for p in seen if p not in current]:
del seen[path]
added = False added = False
for path in sorted(paths - seen): for path, ino in sorted(current.items()):
seen.add(path) if seen.get(path) == ino:
continue
# New here: a new device, or one that came back in the same place.
for old in [n for n in nodes.values() if n.path == path]:
drop(old, "replaced by a new device node")
seen[path] = ino
node = probe(path) node = probe(path)
if node: if node:
nodes[node.fd] = node nodes[node.fd] = node