diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py new file mode 100644 index 0000000..635c683 --- /dev/null +++ b/openwhispr-ptt.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr + +Turns Ctrl+Super (hold) into Ctrl+` toggle pairs: + - Press Ctrl+Super → sends Ctrl+` to START dictation + - Release Ctrl+Super → sends Ctrl+` to STOP dictation + +Requires: pynput, xdotool +Usage: python3 openwhispr-ptt.py +""" + +import subprocess +import threading +import time +import signal +import sys + +try: + from pynput import keyboard +except ImportError: + print("Error: pynput not installed.") + print("Install with: pip3 install pynput") + sys.exit(1) + +# ── Configuration ──────────────────────────────────────────────────────────── + +# The key combo YOU hold to talk +PTT_MODIFIER = keyboard.Key.ctrl_l +PTT_KEY = keyboard.Key.cmd # Super/Windows key + +# The key combo OpenWhispr uses (Ctrl+`) +# We simulate this via xdotool for reliability with Electron apps +WHISPR_HOTKEY = "ctrl+grave" + +# Minimum hold time (ms) to avoid accidental triggers +MIN_HOLD_MS = 100 + +# ── State ──────────────────────────────────────────────────────────────────── + +pressed_keys = set() +is_recording = False +press_time = 0 +lock = threading.Lock() + + +def send_whispr_toggle(): + """Send Ctrl+` to OpenWhispr via xdotool.""" + try: + subprocess.run( + ["xdotool", "key", WHISPR_HOTKEY], + timeout=2, + capture_output=True, + ) + except FileNotFoundError: + print("Error: xdotool not installed. Install with: sudo apt install xdotool") + sys.exit(1) + except subprocess.TimeoutExpired: + pass + + +def on_press(key): + global is_recording, press_time + + with lock: + pressed_keys.add(key) + + # Check if both PTT keys are held + if PTT_MODIFIER in pressed_keys and PTT_KEY in pressed_keys: + if not is_recording: + press_time = time.time() + is_recording = True + send_whispr_toggle() # START + print("\033[32m● Recording...\033[0m") + + +def on_release(key): + global is_recording, press_time + + with lock: + # If either PTT key is released while recording + if is_recording and key in (PTT_MODIFIER, PTT_KEY): + elapsed_ms = (time.time() - press_time) * 1000 + if elapsed_ms >= MIN_HOLD_MS: + send_whispr_toggle() # STOP + print(f"\033[0m○ Stopped ({int(elapsed_ms)}ms)") + else: + # Too short — send another toggle to cancel + send_whispr_toggle() + print(f"\033[33m○ Cancelled (too short: {int(elapsed_ms)}ms)\033[0m") + is_recording = False + + pressed_keys.discard(key) + + +def main(): + print("═══════════════════════════════════════════") + print(" OpenWhispr Push-to-Talk") + print("═══════════════════════════════════════════") + print(f" Hold: Ctrl + Super → start dictation") + print(f" Release: Ctrl + Super → stop dictation") + print(f" Quit: Ctrl + C") + print("═══════════════════════════════════════════") + 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() + + +if __name__ == "__main__": + main()