Super key is intercepted by Cinnamon/window managers before reaching the app. Ctrl+Alt is a better choice — works reliably on all DEs. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm
122 lines
3.9 KiB
Python
122 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
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
|
|
|
|
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.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 ────────────────────────────────────────────────────────────────────
|
|
|
|
is_recording = False
|
|
press_time = 0
|
|
ctrl_held = False
|
|
super_held = False
|
|
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, 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 main():
|
|
print("═══════════════════════════════════════════")
|
|
print(" OpenWhispr Push-to-Talk")
|
|
print("═══════════════════════════════════════════")
|
|
print(" Hold: Ctrl + Alt → start dictation")
|
|
print(" Release: Ctrl + Alt → stop dictation")
|
|
print(" 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()
|