#!/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)
