#!/usr/bin/env python3 """ openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr Turns Ctrl+Alt (hold) into Ctrl+` toggle pairs using raw evdev input. No key repeat issues — reads physical key state directly. Requires: evdev, xdotool May need to run as root or add user to 'input' group. Usage: sudo python3 openwhispr-ptt.py """ import subprocess import signal import sys import time try: import evdev from evdev import ecodes except ImportError: print("Error: evdev not installed.") print("Install with: pip3 install evdev --break-system-packages") sys.exit(1) WHISPR_HOTKEY = "ctrl+grave" KEY_LEFTCTRL = ecodes.KEY_LEFTCTRL KEY_RIGHTCTRL = ecodes.KEY_RIGHTCTRL KEY_LEFTALT = ecodes.KEY_LEFTALT KEY_RIGHTALT = ecodes.KEY_RIGHTALT ctrl_held = False alt_held = False is_recording = False def send_whispr_toggle(): """Send Ctrl+` to OpenWhispr via xdotool. Release all held modifiers first, then send the full keydown ctrl, key grave, keyup ctrl sequence cleanly. """ try: subprocess.run( ["xdotool", "keyup", "ctrl", "alt", "super"], timeout=2, capture_output=True, ) subprocess.run( ["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"], timeout=2, capture_output=True, ) except FileNotFoundError: print("Error: xdotool not installed.") sys.exit(1) except subprocess.TimeoutExpired: pass def find_keyboard(): """Find the keyboard device.""" devices = [evdev.InputDevice(path) for path in evdev.list_devices()] for dev in devices: caps = dev.capabilities(verbose=False) if ecodes.EV_KEY in caps: keys = caps[ecodes.EV_KEY] if ecodes.KEY_A in keys and ecodes.KEY_LEFTCTRL in keys: return dev return None def main(): global ctrl_held, alt_held, is_recording print("=" * 43) print(" OpenWhispr Push-to-Talk (evdev)") print("=" * 43) print(" Hold: Ctrl + Alt -> start dictation") print(" Release: Ctrl + Alt -> stop dictation") print(" Quit: Ctrl + C") print("=" * 43) kb = find_keyboard() if kb is None: print("") print("Error: No keyboard found.") print("Try: sudo python3 ~/openwhispr-ptt.py") print("Or: sudo usermod -aG input $USER (then log out/in)") sys.exit(1) print(" Keyboard: " + kb.name) print("=" * 43) print("Listening...\n") signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) for event in kb.read_loop(): if event.type != ecodes.EV_KEY: continue # value: 0=release, 1=press, 2=repeat — IGNORE repeats if event.value == 2: continue pressed = (event.value == 1) if event.code in (KEY_LEFTCTRL, KEY_RIGHTCTRL): ctrl_held = pressed elif event.code in (KEY_LEFTALT, KEY_RIGHTALT): alt_held = pressed # Start on both pressed if ctrl_held and alt_held and not is_recording: is_recording = True send_whispr_toggle() print("\033[32m● Recording...\033[0m", flush=True) # Stop when either released if is_recording and not (ctrl_held and alt_held): time.sleep(0.05) # Let key release propagate to X11 send_whispr_toggle() is_recording = False print("\033[0m○ Stopped", flush=True) if __name__ == "__main__": main()