This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / tools / turbomode / turbomode.py
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()