Switch PTT to evdev for reliable key state (no repeat spam)
pynput on X11 sees key repeats as real press/release pairs, causing rapid-fire toggling. evdev gives event.value=2 for repeats which we simply ignore. Only real press (1) and release (0) are handled. Requires sudo or 'input' group membership. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm
This commit is contained in:
+71
-75
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user