This repository has no description
0

Configure Feed

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

tools.turbomode: add gamepad turbo/rapid-fire tool

+184
+34
tools/turbomode/flake-module.nix
··· 1 + { 2 + perSystem = 3 + { pkgs, ... }: 4 + let 5 + python = pkgs.python3.withPackages (ps: [ ps.evdev ]); 6 + in 7 + { 8 + packages.turbomode = pkgs.stdenv.mkDerivation { 9 + pname = "turbomode"; 10 + version = "0.1.0"; 11 + 12 + src = ./.; 13 + 14 + nativeBuildInputs = [ pkgs.makeWrapper ]; 15 + 16 + installPhase = '' 17 + runHook preInstall 18 + mkdir -p $out/bin 19 + cp turbomode.py $out/bin/turbomode 20 + chmod +x $out/bin/turbomode 21 + wrapProgram $out/bin/turbomode \ 22 + --prefix PATH : ${python}/bin \ 23 + --set PYTHONPATH ${python}/${python.sitePackages} 24 + runHook postInstall 25 + ''; 26 + 27 + meta = { 28 + description = "Turbo/rapid-fire mode for gamepad buttons"; 29 + platforms = pkgs.lib.platforms.linux; 30 + mainProgram = "turbomode"; 31 + }; 32 + }; 33 + }; 34 + }
+150
tools/turbomode/turbomode.py
··· 1 + #!/usr/bin/env python3 2 + """turbomode - Turbo/rapid-fire mode for gamepad buttons. 3 + 4 + When enabled, holding the specified button will rapidly press and release it 5 + instead of holding it down. All other inputs are forwarded transparently. 6 + """ 7 + 8 + import argparse 9 + import sys 10 + import threading 11 + import time 12 + 13 + import evdev 14 + from evdev import UInput, ecodes 15 + 16 + BUTTON_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 + 32 + def 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 + 42 + def 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 + 149 + if __name__ == "__main__": 150 + main()