#!/usr/bin/env python3 # Freeze/resume the focused window on the controller's Home/guide button (BTN_MODE). # Stdlib only: reads raw evdev events. Gamepads are readable by the seat user. import glob, os, select, signal, struct, subprocess, time EV_KEY, BTN_MODE = 1, 0x13c EVENT = struct.Struct('llHHi') # struct input_event LONG_BITS = struct.calcsize('l') * 8 CMD = [os.path.expanduser('~/.config/i3/scripts/freeze-toggle')] signal.signal(signal.SIGCHLD, signal.SIG_IGN) # auto-reap child processes def has_btn_mode(path): try: with open(f'/sys/class/input/{os.path.basename(path)}/device/capabilities/key') as f: words = f.read().split() except OSError: return False bits = 0 for w in words: bits = bits << LONG_BITS | int(w, 16) return bits >> BTN_MODE & 1 fds = {} # fd -> device path last = 0.0 rescan = 0.0 while True: if time.monotonic() >= rescan: # pick up hotplugged controllers for path in glob.glob('/dev/input/event*'): if path not in fds.values() and has_btn_mode(path): try: fds[os.open(path, os.O_RDONLY)] = path except OSError: pass rescan = time.monotonic() + 2 ready, _, _ = select.select(list(fds), [], [], 2) for fd in ready: try: data = os.read(fd, EVENT.size * 64) except OSError: # controller unplugged os.close(fd) del fds[fd] continue for sec, usec, type_, code, value in EVENT.iter_unpack(data): if type_ == EV_KEY and code == BTN_MODE and value == 1: now = sec + usec / 1e6 if now - last < 0.4: # GuliKit's Home button double-fires ~10ms apart continue last = now subprocess.Popen(CMD, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)