diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index 4817742..2451fb3 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -2,52 +2,39 @@ """ openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr -Turns Ctrl+Alt (hold) into Ctrl+` toggle pairs: - - Press Ctrl+Alt → sends Ctrl+` to START dictation - - Release Ctrl+Alt → sends Ctrl+` to STOP dictation +Turns Ctrl+Alt (hold) into Ctrl+` toggle pairs using raw evdev input. +No key repeat issues — reads physical key state directly. -Requires: pynput, xdotool -Usage: python3 openwhispr-ptt.py +Requires: evdev, xdotool +May need to run as root or add user to 'input' group. +Usage: sudo python3 openwhispr-ptt.py """ import subprocess -import threading -import time import signal import sys try: - from pynput import keyboard + import evdev + from evdev import ecodes except ImportError: - print("Error: pynput not installed.") - print("Install with: pip3 install pynput") + print("Error: evdev not installed.") + print("Install with: pip3 install evdev --break-system-packages") sys.exit(1) -# ── Configuration ──────────────────────────────────────────────────────────── - -# The key combo YOU hold to talk -PTT_MODIFIER = keyboard.Key.ctrl_l -PTT_KEY = keyboard.Key.alt_l - -# Also accept right-hand variants -PTT_MODIFIER_ALT = keyboard.Key.ctrl_r -PTT_KEY_ALT = keyboard.Key.alt_r - -# The key combo OpenWhispr uses (Ctrl+`) -# We simulate this via xdotool for reliability with Electron apps WHISPR_HOTKEY = "ctrl+grave" -# ── State ──────────────────────────────────────────────────────────────────── +KEY_LEFTCTRL = ecodes.KEY_LEFTCTRL +KEY_RIGHTCTRL = ecodes.KEY_RIGHTCTRL +KEY_LEFTALT = ecodes.KEY_LEFTALT +KEY_RIGHTALT = ecodes.KEY_RIGHTALT -is_recording = False -press_time = 0 ctrl_held = False -super_held = False -lock = threading.Lock() +alt_held = False +is_recording = False def send_whispr_toggle(): - """Send Ctrl+` to OpenWhispr via xdotool.""" try: subprocess.run( ["xdotool", "key", WHISPR_HOTKEY], @@ -55,66 +42,75 @@ def send_whispr_toggle(): capture_output=True, ) except FileNotFoundError: - print("Error: xdotool not installed. Install with: sudo apt install xdotool") + print("Error: xdotool not installed.") sys.exit(1) except subprocess.TimeoutExpired: pass -def on_press(key): - global is_recording, press_time, ctrl_held, super_held - - with lock: - # Track modifier state — ignore key repeat (already True) - if key in (PTT_MODIFIER, PTT_MODIFIER_ALT): - ctrl_held = True - elif key in (PTT_KEY, PTT_KEY_ALT): - super_held = True - - # Start recording when both held and not already recording - if ctrl_held and super_held and not is_recording: - press_time = time.time() - is_recording = True - send_whispr_toggle() # START - print("\033[32m● Recording...\033[0m", flush=True) - - -def on_release(key): - global is_recording, press_time, ctrl_held, super_held - - with lock: - released_ptt = False - - if key in (PTT_MODIFIER, PTT_MODIFIER_ALT): - ctrl_held = False - released_ptt = True - elif key in (PTT_KEY, PTT_KEY_ALT): - super_held = False - released_ptt = True - - # Stop recording when either key is truly released - if is_recording and released_ptt: - elapsed_ms = (time.time() - press_time) * 1000 - send_whispr_toggle() # STOP - is_recording = False - print(f"\033[0m○ Stopped ({int(elapsed_ms)}ms)", flush=True) +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(): - print("═══════════════════════════════════════════") - print(" OpenWhispr Push-to-Talk") - print("═══════════════════════════════════════════") - print(" Hold: Ctrl + Alt → start dictation") - print(" Release: Ctrl + Alt → stop dictation") + 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("═══════════════════════════════════════════") + 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") - # Handle Ctrl+C gracefully signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) - with keyboard.Listener(on_press=on_press, on_release=on_release) as listener: - listener.join() + 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): + send_whispr_toggle() + is_recording = False + print("\033[0m○ Stopped", flush=True) if __name__ == "__main__":