forked from
lass.pds.lassul.us/superconfig
This repository has no description
4.5 kB
150 lines
1#!/usr/bin/env python3
2"""turbomode - Turbo/rapid-fire mode for gamepad buttons.
3
4When enabled, holding the specified button will rapidly press and release it
5instead of holding it down. All other inputs are forwarded transparently.
6"""
7
8import argparse
9import sys
10import threading
11import time
12
13import evdev
14from evdev import UInput, ecodes
15
16BUTTON_NAMES: dict[str, int] = {
17 "a": ecodes.BTN_SOUTH,
18 "b": ecodes.BTN_EAST,
19 "x": ecodes.BTN_NORTH,
20 "y": ecodes.BTN_WEST,
21 "lb": ecodes.BTN_TL,
22 "rb": ecodes.BTN_TR,
23 "lt": ecodes.BTN_TL2,
24 "rt": ecodes.BTN_TR2,
25 "start": ecodes.BTN_START,
26 "select": ecodes.BTN_SELECT,
27 "thumbl": ecodes.BTN_THUMBL,
28 "thumbr": ecodes.BTN_THUMBR,
29}
30
31
32def find_gamepad() -> evdev.InputDevice | None:
33 """Find the first gamepad-like device (has both EV_KEY and EV_ABS)."""
34 for path in evdev.list_devices():
35 dev = evdev.InputDevice(path)
36 caps = dev.capabilities()
37 if ecodes.EV_KEY in caps and ecodes.EV_ABS in caps:
38 return dev
39 return None
40
41
42def main() -> None:
43 parser = argparse.ArgumentParser(
44 description="Turbo mode: hold a gamepad button for rapid-fire pressing"
45 )
46 parser.add_argument(
47 "--button",
48 required=True,
49 help="button name ({}) or evdev code number".format(
50 "/".join(BUTTON_NAMES.keys())
51 ),
52 )
53 parser.add_argument(
54 "--device",
55 help="input device path (auto-detects gamepad if not specified)",
56 )
57 parser.add_argument(
58 "--rate",
59 type=int,
60 default=30,
61 help="presses per second (default: 30)",
62 )
63 args = parser.parse_args()
64
65 # Resolve button code
66 button_name = args.button.lower()
67 if button_name in BUTTON_NAMES:
68 turbo_button = BUTTON_NAMES[button_name]
69 else:
70 try:
71 turbo_button = int(args.button)
72 except ValueError:
73 print(f"Unknown button: {args.button}", file=sys.stderr)
74 print(
75 f"Known buttons: {', '.join(BUTTON_NAMES.keys())}",
76 file=sys.stderr,
77 )
78 sys.exit(1)
79
80 # Open device
81 if args.device:
82 dev = evdev.InputDevice(args.device)
83 else:
84 found = find_gamepad()
85 if found is None:
86 print("No gamepad found", file=sys.stderr)
87 sys.exit(1)
88 dev = found
89
90 print(f"Device: {dev.name} ({dev.path})")
91 print(f"Turbo button: {button_name} (code {turbo_button})")
92 print(f"Rate: {args.rate} presses/sec")
93
94 # Create virtual device cloning the real one's capabilities
95 ui = UInput.from_device(dev, name=f"{dev.name} (turbo)")
96
97 # Grab the real device so its events only go through our proxy
98 dev.grab()
99
100 turbo_active = threading.Event()
101 stop_event = threading.Event()
102
103 def turbo_loop() -> None:
104 # Each cycle is press + release, so we need 2x the interval
105 interval = 1.0 / (args.rate * 2)
106 while not stop_event.is_set():
107 turbo_active.wait(timeout=1.0)
108 if stop_event.is_set():
109 break
110 if not turbo_active.is_set():
111 continue
112 ui.write(ecodes.EV_KEY, turbo_button, 1)
113 ui.syn()
114 if stop_event.wait(timeout=interval):
115 break
116 if not turbo_active.is_set():
117 continue
118 ui.write(ecodes.EV_KEY, turbo_button, 0)
119 ui.syn()
120 if stop_event.wait(timeout=interval):
121 break
122
123 t = threading.Thread(target=turbo_loop, daemon=True)
124 t.start()
125
126 try:
127 for event in dev.read_loop():
128 if event.type == ecodes.EV_KEY and event.code == turbo_button:
129 if event.value == 1: # Pressed
130 turbo_active.set()
131 elif event.value == 0: # Released
132 turbo_active.clear()
133 # Ensure the button ends in released state
134 ui.write(ecodes.EV_KEY, turbo_button, 0)
135 ui.syn()
136 # Don't forward original turbo button events
137 else:
138 # Forward all other events as-is
139 ui.write_event(event)
140 except KeyboardInterrupt:
141 pass
142 finally:
143 stop_event.set()
144 turbo_active.set() # Wake the thread so it can exit
145 dev.ungrab()
146 ui.close()
147
148
149if __name__ == "__main__":
150 main()