From e32dc5f690ca044a5cd9d211b33dcd5e94b1850c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 19:56:43 +0000 Subject: [PATCH 01/10] Add push-to-talk wrapper for OpenWhispr Python script that converts Ctrl+Super hold-to-talk into Ctrl+` toggle pairs for OpenWhispr, working around the app's broken Hold activation mode on Linux. Uses pynput for key listening and xdotool to send hotkeys. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- openwhispr-ptt.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 openwhispr-ptt.py 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() From aa8f40039a8c926a436dee5d0df7546c5124d41c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 20:00:19 +0000 Subject: [PATCH 02/10] Fix PTT rapid-fire: track key state as booleans, ignore repeats Super key generates rapid press/release events from key repeat. Changed from set-based tracking to boolean flags (ctrl_held, super_held) so repeated press events are ignored when the key is already held. Removed min-hold cancel logic. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- openwhispr-ptt.py | 64 ++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index 635c683..e290ad0 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -29,18 +29,19 @@ except ImportError: PTT_MODIFIER = keyboard.Key.ctrl_l PTT_KEY = keyboard.Key.cmd # Super/Windows key +# Also accept right Ctrl +PTT_MODIFIER_ALT = keyboard.Key.ctrl_r + # 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 +ctrl_held = False +super_held = False lock = threading.Lock() @@ -60,46 +61,51 @@ def send_whispr_toggle(): def on_press(key): - global is_recording, press_time + global is_recording, press_time, ctrl_held, super_held with lock: - pressed_keys.add(key) + # Track modifier state — ignore key repeat (already True) + if key in (PTT_MODIFIER, PTT_MODIFIER_ALT): + ctrl_held = True + elif key == PTT_KEY: + super_held = True - # 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") + # 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 + global is_recording, press_time, ctrl_held, super_held 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 + released_ptt = False - pressed_keys.discard(key) + if key in (PTT_MODIFIER, PTT_MODIFIER_ALT): + ctrl_held = False + released_ptt = True + elif key == PTT_KEY: + 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(f" Hold: Ctrl + Super → start dictation") - print(f" Release: Ctrl + Super → stop dictation") - print(f" Quit: Ctrl + C") + print(" Hold: Ctrl + Super → start dictation") + print(" Release: Ctrl + Super → stop dictation") + print(" Quit: Ctrl + C") print("═══════════════════════════════════════════") print("Listening...\n") From 723436dae9939cc2a273e5cb3e6f336b6fdecb89 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 20:01:10 +0000 Subject: [PATCH 03/10] Switch PTT hotkey from Ctrl+Super to Ctrl+Alt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- openwhispr-ptt.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index e290ad0..4817742 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -2,9 +2,9 @@ """ 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 +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 @@ -27,10 +27,11 @@ except ImportError: # The key combo YOU hold to talk PTT_MODIFIER = keyboard.Key.ctrl_l -PTT_KEY = keyboard.Key.cmd # Super/Windows key +PTT_KEY = keyboard.Key.alt_l -# Also accept right Ctrl +# 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 @@ -67,7 +68,7 @@ def on_press(key): # Track modifier state — ignore key repeat (already True) if key in (PTT_MODIFIER, PTT_MODIFIER_ALT): ctrl_held = True - elif key == PTT_KEY: + elif key in (PTT_KEY, PTT_KEY_ALT): super_held = True # Start recording when both held and not already recording @@ -87,7 +88,7 @@ def on_release(key): if key in (PTT_MODIFIER, PTT_MODIFIER_ALT): ctrl_held = False released_ptt = True - elif key == PTT_KEY: + elif key in (PTT_KEY, PTT_KEY_ALT): super_held = False released_ptt = True @@ -103,8 +104,8 @@ def main(): print("═══════════════════════════════════════════") print(" OpenWhispr Push-to-Talk") print("═══════════════════════════════════════════") - print(" Hold: Ctrl + Super → start dictation") - print(" Release: Ctrl + Super → stop dictation") + print(" Hold: Ctrl + Alt → start dictation") + print(" Release: Ctrl + Alt → stop dictation") print(" Quit: Ctrl + C") print("═══════════════════════════════════════════") print("Listening...\n") From 150485ee0121bc507354765a2334bf0b9ad14b94 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 20:08:57 +0000 Subject: [PATCH 04/10] 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 --- openwhispr-ptt.py | 146 ++++++++++++++++++++++------------------------ 1 file changed, 71 insertions(+), 75 deletions(-) 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__": From 9137bd2d80a85d185769f9e0182f4030b9ccb62e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 20:33:03 +0000 Subject: [PATCH 05/10] Fix stop toggle not reaching OpenWhispr on key release Add --clearmodifiers to xdotool so held Ctrl/Alt don't corrupt the simulated Ctrl+` into Ctrl+Alt+`. Also add 50ms delay before stop to let key release propagate to X11. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- openwhispr-ptt.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index 2451fb3..0ec52dc 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -13,6 +13,7 @@ Usage: sudo python3 openwhispr-ptt.py import subprocess import signal import sys +import time try: import evdev @@ -35,9 +36,14 @@ is_recording = False def send_whispr_toggle(): + """Send Ctrl+` to OpenWhispr via xdotool. + + Uses --clearmodifiers to release any physically held keys first, + so the simulated Ctrl+` isn't mangled into Ctrl+Alt+`. + """ try: subprocess.run( - ["xdotool", "key", WHISPR_HOTKEY], + ["xdotool", "key", "--clearmodifiers", WHISPR_HOTKEY], timeout=2, capture_output=True, ) @@ -108,6 +114,7 @@ def main(): # 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) From 6991fdbbee135d608144ebdb9457bbdae3abc4f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 20:37:05 +0000 Subject: [PATCH 06/10] Fix Ctrl+grave being sent without Ctrl due to --clearmodifiers --clearmodifiers releases ALL modifiers including Ctrl, so xdotool sends bare grave (types 'v'). Instead, explicitly release only Alt before sending Ctrl+grave so OpenWhispr receives the correct combo. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- openwhispr-ptt.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index 0ec52dc..e08b042 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -38,12 +38,18 @@ is_recording = False def send_whispr_toggle(): """Send Ctrl+` to OpenWhispr via xdotool. - Uses --clearmodifiers to release any physically held keys first, - so the simulated Ctrl+` isn't mangled into Ctrl+Alt+`. + We first release Alt (which we're physically holding), send Ctrl+grave, + then re-press Alt. This way OpenWhispr sees clean Ctrl+` without + the Alt modifier corrupting it. """ try: subprocess.run( - ["xdotool", "key", "--clearmodifiers", WHISPR_HOTKEY], + ["xdotool", "keyup", "alt"], + timeout=2, + capture_output=True, + ) + subprocess.run( + ["xdotool", "key", WHISPR_HOTKEY], timeout=2, capture_output=True, ) From 124fc8bc5bddd93dedaef00ca8b62b4d9e2fd22c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 20:40:47 +0000 Subject: [PATCH 07/10] Release all modifiers before sending clean Ctrl+grave sequence Use explicit keyup ctrl alt super first, then the exact working sequence: keydown ctrl, key grave, keyup ctrl. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- openwhispr-ptt.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index e08b042..c9423ca 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -38,18 +38,17 @@ is_recording = False def send_whispr_toggle(): """Send Ctrl+` to OpenWhispr via xdotool. - We first release Alt (which we're physically holding), send Ctrl+grave, - then re-press Alt. This way OpenWhispr sees clean Ctrl+` without - the Alt modifier corrupting it. + Release all held modifiers first, then send the full + keydown ctrl, key grave, keyup ctrl sequence cleanly. """ try: subprocess.run( - ["xdotool", "keyup", "alt"], + ["xdotool", "keyup", "ctrl", "alt", "super"], timeout=2, capture_output=True, ) subprocess.run( - ["xdotool", "key", WHISPR_HOTKEY], + ["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"], timeout=2, capture_output=True, ) From a07869f8be51fae0d5613baaa6b49f435da1af6d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 21:42:45 +0000 Subject: [PATCH 08/10] Skip virtual keyboard devices (ydotoold) in device detection find_keyboard() was matching ydotoold virtual device instead of the real AT Translated Set 2 keyboard because evdev lists devices in reverse order. Now skips devices with 'virtual' or 'ydotool' in name. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- openwhispr-ptt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openwhispr-ptt.py b/openwhispr-ptt.py index c9423ca..0c25d64 100644 --- a/openwhispr-ptt.py +++ b/openwhispr-ptt.py @@ -60,9 +60,13 @@ def send_whispr_toggle(): def find_keyboard(): - """Find the keyboard device.""" + """Find the real physical keyboard device (not virtual devices).""" devices = [evdev.InputDevice(path) for path in evdev.list_devices()] for dev in devices: + # Skip virtual devices (ydotool, xdotool, etc.) + name_lower = dev.name.lower() + if "virtual" in name_lower or "ydotool" in name_lower: + continue caps = dev.capabilities(verbose=False) if ecodes.EV_KEY in caps: keys = caps[ecodes.EV_KEY] From 22d4f64bb316ded5f0f695516a9b7ffc1cb30d4d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 04:00:19 +0000 Subject: [PATCH 09/10] Add one-shot PTT setup script with systemd service and uninstall setup-ptt.sh handles everything in one run: - Adds user to input group (no sudo for PTT) - Installs evdev Python module - Interactive hotkey selection (5 options) - Generates PTT Python script with chosen hotkey - Creates systemd user service (auto-start, restart on failure) - Checks if OpenWhispr is running, offers to launch - Creates uninstall script - Prints useful commands summary https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- setup-ptt.sh | 589 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 589 insertions(+) create mode 100755 setup-ptt.sh diff --git a/setup-ptt.sh b/setup-ptt.sh new file mode 100755 index 0000000..1c06ba6 --- /dev/null +++ b/setup-ptt.sh @@ -0,0 +1,589 @@ +#!/usr/bin/env bash +# setup-ptt.sh — One-shot setup for OpenWhispr Push-to-Talk +# https://github.com/outis1one/openwhispr-easy-setup +# License: MIT + +main() { + +SCRIPT_VERSION="1.0.0" +PTT_DIR="$HOME/.local/share/openwhispr-ptt" +PTT_SCRIPT="$PTT_DIR/openwhispr-ptt.py" +UNINSTALL_SCRIPT="$PTT_DIR/uninstall-ptt.sh" +SERVICE_NAME="openwhispr-ptt" +SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service" + +# ── Colours ─────────────────────────────────────────────────────────────── + +if [[ -t 1 ]]; then + BOLD='\033[1m' GREEN='\033[0;32m' YELLOW='\033[0;33m' + RED='\033[0;31m' CYAN='\033[0;36m' DIM='\033[2m' RESET='\033[0m' +else + BOLD='' GREEN='' YELLOW='' RED='' CYAN='' DIM='' RESET='' +fi + +# shellcheck disable=SC2059 +info() { printf "${GREEN}[✓]${RESET} %s\n" "$*"; } +# shellcheck disable=SC2059 +warn() { printf "${YELLOW}[!]${RESET} %s\n" "$*"; } +# shellcheck disable=SC2059 +err() { printf "${RED}[✗]${RESET} %s\n" "$*" >&2; } +# shellcheck disable=SC2059 +step() { printf "\n${BOLD}${CYAN}[%s]${RESET} ${BOLD}%s${RESET}\n" "$1" "$2"; } +# shellcheck disable=SC2059 +divider() { printf "${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}\n"; } + +ask() { + local prompt="$1" default="$2" varname="$3" reply + printf "%s [default: %s]: " "$prompt" "$default" > /dev/tty + read -r reply < /dev/tty + reply="${reply:-$default}" + printf -v "$varname" '%s' "$reply" +} + +# ══════════════════════════════════════════════════════════════════════════ +# [1/6] Intro +# ══════════════════════════════════════════════════════════════════════════ + +step "1/6" "OpenWhispr Push-to-Talk Setup v${SCRIPT_VERSION}" +divider +cat <<'INTRO' +This script sets up push-to-talk (hold-to-talk) for OpenWhispr. + + Hold a key combo → OpenWhispr starts listening + Release → OpenWhispr stops and transcribes + +What it will do: + 1. Add your user to the 'input' group (no sudo needed for PTT) + 2. Install Python dependencies (evdev) + 3. Create the PTT script + 4. Let you choose your PTT hotkey + 5. Set up a systemd service (auto-start, auto-restart) + 6. Create an uninstall script +INTRO +divider +printf "\nPress Enter to continue or Ctrl-C to cancel... " > /dev/tty +read -r < /dev/tty + +# ══════════════════════════════════════════════════════════════════════════ +# [2/6] Dependencies +# ══════════════════════════════════════════════════════════════════════════ + +step "2/6" "Dependencies" + +# Check for OpenWhispr +OW_INSTALLED=false +if command -v open-whispr >/dev/null 2>&1 || \ + command -v openwhispr >/dev/null 2>&1 || \ + dpkg -l open-whispr 2>/dev/null | grep -q '^ii'; then + OW_INSTALLED=true + info "OpenWhispr is installed." +else + err "OpenWhispr does not appear to be installed." + err "Install it first: https://github.com/OpenWhispr/openwhispr/releases" + exit 1 +fi + +# Check for xdotool +if command -v xdotool >/dev/null 2>&1; then + info "xdotool is installed." +else + warn "xdotool is required. Installing..." + sudo apt install -y xdotool || { + err "Failed to install xdotool." + exit 1 + } + info "xdotool installed." +fi + +# Add user to input group +if groups "$USER" | grep -qw input; then + info "User '$USER' is already in the 'input' group." +else + warn "Adding '$USER' to the 'input' group (requires sudo)..." + sudo usermod -aG input "$USER" || { + err "Failed to add user to input group." + exit 1 + } + info "Added '$USER' to 'input' group." + warn "You will need to log out and back in for this to take effect." + NEEDS_RELOGIN=true +fi + +# Install evdev Python module +if python3 -c "import evdev" 2>/dev/null; then + info "Python evdev module is installed." +else + warn "Installing Python evdev module..." + pip3 install evdev --break-system-packages 2>/dev/null || \ + sudo pip3 install evdev --break-system-packages 2>/dev/null || { + err "Failed to install evdev." + exit 1 + } + info "evdev installed." +fi + +# ══════════════════════════════════════════════════════════════════════════ +# [3/6] Choose PTT hotkey +# ══════════════════════════════════════════════════════════════════════════ + +step "3/6" "Choose your Push-to-Talk hotkey" + +cat <<'HOTKEY_MENU' + +Which key combo do you want to HOLD for push-to-talk? + + 1) Ctrl + Alt (recommended — works on all desktops) + 2) Ctrl + Shift + 3) Alt + Shift + 4) Right Ctrl alone (easy one-hand use) + 5) Right Alt alone (easy one-hand use) + +Note: Avoid Super/Windows key — most desktops intercept it. + The OpenWhispr toggle hotkey (Ctrl+`) must NOT conflict. + +HOTKEY_MENU + +ask "Enter 1-5" "1" HOTKEY_CHOICE + +case "$HOTKEY_CHOICE" in + 1) + PTT_LABEL="Ctrl + Alt" + PTT_KEY1_NAME="ctrl" + PTT_KEY2_NAME="alt" + PTT_KEY1_CODES="KEY_LEFTCTRL, KEY_RIGHTCTRL" + PTT_KEY2_CODES="KEY_LEFTALT, KEY_RIGHTALT" + ;; + 2) + PTT_LABEL="Ctrl + Shift" + PTT_KEY1_NAME="ctrl" + PTT_KEY2_NAME="shift" + PTT_KEY1_CODES="KEY_LEFTCTRL, KEY_RIGHTCTRL" + PTT_KEY2_CODES="KEY_LEFTSHIFT, KEY_RIGHTSHIFT" + ;; + 3) + PTT_LABEL="Alt + Shift" + PTT_KEY1_NAME="alt" + PTT_KEY2_NAME="shift" + PTT_KEY1_CODES="KEY_LEFTALT, KEY_RIGHTALT" + PTT_KEY2_CODES="KEY_LEFTSHIFT, KEY_RIGHTSHIFT" + ;; + 4) + PTT_LABEL="Right Ctrl (alone)" + PTT_KEY1_NAME="rctrl" + PTT_KEY2_NAME="" + PTT_KEY1_CODES="KEY_RIGHTCTRL" + PTT_KEY2_CODES="" + ;; + 5) + PTT_LABEL="Right Alt (alone)" + PTT_KEY1_NAME="ralt" + PTT_KEY2_NAME="" + PTT_KEY1_CODES="KEY_RIGHTALT" + PTT_KEY2_CODES="" + ;; + *) + warn "Invalid choice — defaulting to Ctrl + Alt." + PTT_LABEL="Ctrl + Alt" + PTT_KEY1_NAME="ctrl" + PTT_KEY2_NAME="alt" + PTT_KEY1_CODES="KEY_LEFTCTRL, KEY_RIGHTCTRL" + PTT_KEY2_CODES="KEY_LEFTALT, KEY_RIGHTALT" + ;; +esac + +info "PTT hotkey: ${PTT_LABEL}" + +# ══════════════════════════════════════════════════════════════════════════ +# [4/6] Create PTT script +# ══════════════════════════════════════════════════════════════════════════ + +step "4/6" "Creating PTT script" + +mkdir -p "$PTT_DIR" + +# Generate the appropriate Python code based on single vs dual key +if [[ -z "$PTT_KEY2_NAME" ]]; then + # Single key mode + cat > "$PTT_SCRIPT" << PYEOF +#!/usr/bin/env python3 +""" +openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr +PTT hotkey: ${PTT_LABEL} +Generated by setup-ptt.sh v${SCRIPT_VERSION} +""" + +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_UP = ["xdotool", "keyup", "ctrl", "alt", "super"] +WHISPR_HOTKEY_SEND = ["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"] +PTT_CODES = (ecodes.${PTT_KEY1_CODES},) + +is_recording = False + + +def send_whispr_toggle(): + try: + subprocess.run(WHISPR_HOTKEY_UP, timeout=2, capture_output=True) + subprocess.run(WHISPR_HOTKEY_SEND, timeout=2, capture_output=True) + except FileNotFoundError: + print("Error: xdotool not installed.") + sys.exit(1) + except subprocess.TimeoutExpired: + pass + + +def find_keyboard(): + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + for dev in devices: + name_lower = dev.name.lower() + if "virtual" in name_lower or "ydotool" in name_lower: + continue + 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 is_recording + + print("=" * 47) + print(" OpenWhispr Push-to-Talk") + print("=" * 47) + print(" Hold: ${PTT_LABEL} -> start dictation") + print(" Release: ${PTT_LABEL} -> stop dictation") + print(" Quit: Ctrl + C") + print("=" * 47) + + kb = find_keyboard() + if kb is None: + print("\\nError: No keyboard found.") + print("Check: groups | grep input") + print("Fix: sudo usermod -aG input \$USER (then log out/in)") + sys.exit(1) + + print(f" Keyboard: {kb.name}") + print("=" * 47) + print("Listening...\\n") + + signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) + + for event in kb.read_loop(): + if event.type != ecodes.EV_KEY or event.value == 2: + continue + + if event.code in PTT_CODES: + if event.value == 1 and not is_recording: + is_recording = True + send_whispr_toggle() + print("\\033[32m● Recording...\\033[0m", flush=True) + elif event.value == 0 and is_recording: + time.sleep(0.05) + send_whispr_toggle() + is_recording = False + print("\\033[0m○ Stopped", flush=True) + + +if __name__ == "__main__": + main() +PYEOF + +else + # Dual key mode + cat > "$PTT_SCRIPT" << PYEOF +#!/usr/bin/env python3 +""" +openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr +PTT hotkey: ${PTT_LABEL} +Generated by setup-ptt.sh v${SCRIPT_VERSION} +""" + +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_UP = ["xdotool", "keyup", "ctrl", "alt", "super"] +WHISPR_HOTKEY_SEND = ["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"] +KEY1_CODES = (ecodes.${PTT_KEY1_CODES},) +KEY2_CODES = (ecodes.${PTT_KEY2_CODES},) + +key1_held = False +key2_held = False +is_recording = False + + +def send_whispr_toggle(): + try: + subprocess.run(WHISPR_HOTKEY_UP, timeout=2, capture_output=True) + subprocess.run(WHISPR_HOTKEY_SEND, timeout=2, capture_output=True) + except FileNotFoundError: + print("Error: xdotool not installed.") + sys.exit(1) + except subprocess.TimeoutExpired: + pass + + +def find_keyboard(): + devices = [evdev.InputDevice(path) for path in evdev.list_devices()] + for dev in devices: + name_lower = dev.name.lower() + if "virtual" in name_lower or "ydotool" in name_lower: + continue + 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 key1_held, key2_held, is_recording + + print("=" * 47) + print(" OpenWhispr Push-to-Talk") + print("=" * 47) + print(" Hold: ${PTT_LABEL} -> start dictation") + print(" Release: ${PTT_LABEL} -> stop dictation") + print(" Quit: Ctrl + C") + print("=" * 47) + + kb = find_keyboard() + if kb is None: + print("\\nError: No keyboard found.") + print("Check: groups | grep input") + print("Fix: sudo usermod -aG input \$USER (then log out/in)") + sys.exit(1) + + print(f" Keyboard: {kb.name}") + print("=" * 47) + print("Listening...\\n") + + signal.signal(signal.SIGINT, lambda *_: sys.exit(0)) + + for event in kb.read_loop(): + if event.type != ecodes.EV_KEY or event.value == 2: + continue + + pressed = (event.value == 1) + + if event.code in KEY1_CODES: + key1_held = pressed + elif event.code in KEY2_CODES: + key2_held = pressed + + if key1_held and key2_held and not is_recording: + is_recording = True + send_whispr_toggle() + print("\\033[32m● Recording...\\033[0m", flush=True) + + if is_recording and not (key1_held and key2_held): + time.sleep(0.05) + send_whispr_toggle() + is_recording = False + print("\\033[0m○ Stopped", flush=True) + + +if __name__ == "__main__": + main() +PYEOF + +fi + +chmod +x "$PTT_SCRIPT" +info "PTT script created: $PTT_SCRIPT" + +# ══════════════════════════════════════════════════════════════════════════ +# [5/6] Create systemd service +# ══════════════════════════════════════════════════════════════════════════ + +step "5/6" "Setting up systemd service" + +mkdir -p "$HOME/.config/systemd/user" + +cat > "$SERVICE_FILE" << SVCEOF +[Unit] +Description=OpenWhispr Push-to-Talk (${PTT_LABEL}) +Documentation=https://github.com/outis1one/openwhispr-easy-setup +After=graphical-session.target +StartLimitIntervalSec=60 +StartLimitBurst=5 + +[Service] +Type=simple +ExecStart=/usr/bin/python3 ${PTT_SCRIPT} +Restart=on-failure +RestartSec=3 +Environment=DISPLAY=:0 +Environment=XAUTHORITY=%h/.Xauthority + +[Install] +WantedBy=default.target +SVCEOF + +systemctl --user daemon-reload +info "Systemd service created: $SERVICE_FILE" + +# Enable but don't start yet if relogin needed +systemctl --user enable "$SERVICE_NAME" 2>/dev/null +info "Service enabled (will start on login)." + +# Try to start now if input group is already active +if groups "$USER" | grep -qw input && [[ -z "${NEEDS_RELOGIN:-}" ]]; then + systemctl --user start "$SERVICE_NAME" 2>/dev/null + if systemctl --user is-active "$SERVICE_NAME" >/dev/null 2>&1; then + info "Service started successfully." + else + warn "Service failed to start. Check: journalctl --user -u $SERVICE_NAME" + fi +else + warn "Service will start after you log out and back in." +fi + +# ══════════════════════════════════════════════════════════════════════════ +# [6/6] Create uninstall script & check OpenWhispr +# ══════════════════════════════════════════════════════════════════════════ + +step "6/6" "Finishing up" + +# Create uninstall script +cat > "$UNINSTALL_SCRIPT" << 'UNINSTEOF' +#!/usr/bin/env bash +echo "Uninstalling OpenWhispr PTT..." + +# Stop and disable service +systemctl --user stop openwhispr-ptt 2>/dev/null +systemctl --user disable openwhispr-ptt 2>/dev/null +rm -f "$HOME/.config/systemd/user/openwhispr-ptt.service" +systemctl --user daemon-reload + +# Remove PTT files +rm -rf "$HOME/.local/share/openwhispr-ptt" + +echo "" +echo "OpenWhispr PTT has been uninstalled." +echo "Note: OpenWhispr itself was NOT removed." +echo "Note: User was NOT removed from 'input' group." +echo " To remove: sudo gpasswd -d $USER input" +UNINSTEOF +chmod +x "$UNINSTALL_SCRIPT" +info "Uninstall script created: $UNINSTALL_SCRIPT" + +# Check if OpenWhispr is running +OW_RUNNING=false +if pgrep -f "open-whispr" >/dev/null 2>&1; then + OW_RUNNING=true + info "OpenWhispr is running." +else + warn "OpenWhispr is not running." + printf "Start OpenWhispr now? [Y/n]: " > /dev/tty + read -r ow_reply < /dev/tty + ow_reply="${ow_reply:-Y}" + if [[ "${ow_reply,,}" == "y" ]]; then + # Find and launch + for cmd in open-whispr openwhispr; do + if command -v "$cmd" >/dev/null 2>&1; then + "$cmd" &>/dev/null & + info "OpenWhispr launched." + OW_RUNNING=true + break + fi + done + if [[ "$OW_RUNNING" != true ]]; then + # Try common paths + for path in /opt/OpenWhispr/open-whispr /usr/lib/open-whispr/open-whispr; do + if [[ -x "$path" ]]; then + "$path" &>/dev/null & + info "OpenWhispr launched." + OW_RUNNING=true + break + fi + done + fi + if [[ "$OW_RUNNING" != true ]]; then + # Try dpkg + local_bin="$(dpkg -L open-whispr 2>/dev/null | grep -E '/bin/|/opt/' | head -1)" + if [[ -n "$local_bin" && -x "$local_bin" ]]; then + "$local_bin" &>/dev/null & + info "OpenWhispr launched." + OW_RUNNING=true + else + warn "Could not find OpenWhispr binary. Launch it manually." + fi + fi + fi +fi + +# ── Summary ─────────────────────────────────────────────────────────────── + +printf "\n" +divider +# shellcheck disable=SC2059 +printf "${BOLD} OpenWhispr PTT is ready${RESET}\n" +divider +cat << EOF + + PTT hotkey: ${PTT_LABEL} (hold to talk, release to stop) + OW hotkey: Ctrl + \` (used internally — don't change in OW) + + Files: + PTT script: ${PTT_SCRIPT} + Service: ${SERVICE_FILE} + Uninstall: ${UNINSTALL_SCRIPT} + +EOF + +if [[ -n "${NEEDS_RELOGIN:-}" ]]; then +cat << 'EOF' + ⚠ LOG OUT AND BACK IN for input group to take effect. + After re-login the PTT service will start automatically. + +EOF +fi + +cat << 'EOF' + Useful commands: + systemctl --user status openwhispr-ptt check PTT status + systemctl --user restart openwhispr-ptt restart PTT + systemctl --user stop openwhispr-ptt stop PTT + journalctl --user -u openwhispr-ptt -f view PTT logs + + systemctl --user start openwhispr-ptt manual start + systemctl --user disable openwhispr-ptt disable auto-start + systemctl --user enable openwhispr-ptt re-enable auto-start + + Test PTT manually: + python3 ~/.local/share/openwhispr-ptt/openwhispr-ptt.py + + Uninstall: + bash ~/.local/share/openwhispr-ptt/uninstall-ptt.sh + +EOF +divider +printf "\n" + +} # end main() + +main "$@" From 62b2a2277edee80afd9aaf469f7aa4e0d9fb9699 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Mar 2026 04:06:49 +0000 Subject: [PATCH 10/10] Fix NameError: add ecodes. prefix to all key code references The template was expanding KEY_LEFTCTRL, KEY_RIGHTCTRL but only prepending ecodes. to the first one. Now all key codes include the ecodes. prefix in the case statement values. https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm --- setup-ptt.sh | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/setup-ptt.sh b/setup-ptt.sh index 1c06ba6..b1c8b4a 100755 --- a/setup-ptt.sh +++ b/setup-ptt.sh @@ -150,35 +150,35 @@ case "$HOTKEY_CHOICE" in PTT_LABEL="Ctrl + Alt" PTT_KEY1_NAME="ctrl" PTT_KEY2_NAME="alt" - PTT_KEY1_CODES="KEY_LEFTCTRL, KEY_RIGHTCTRL" - PTT_KEY2_CODES="KEY_LEFTALT, KEY_RIGHTALT" + PTT_KEY1_CODES="ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL" + PTT_KEY2_CODES="ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTALT" ;; 2) PTT_LABEL="Ctrl + Shift" PTT_KEY1_NAME="ctrl" PTT_KEY2_NAME="shift" - PTT_KEY1_CODES="KEY_LEFTCTRL, KEY_RIGHTCTRL" - PTT_KEY2_CODES="KEY_LEFTSHIFT, KEY_RIGHTSHIFT" + PTT_KEY1_CODES="ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL" + PTT_KEY2_CODES="ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT" ;; 3) PTT_LABEL="Alt + Shift" PTT_KEY1_NAME="alt" PTT_KEY2_NAME="shift" - PTT_KEY1_CODES="KEY_LEFTALT, KEY_RIGHTALT" - PTT_KEY2_CODES="KEY_LEFTSHIFT, KEY_RIGHTSHIFT" + PTT_KEY1_CODES="ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTALT" + PTT_KEY2_CODES="ecodes.KEY_LEFTSHIFT, ecodes.KEY_RIGHTSHIFT" ;; 4) PTT_LABEL="Right Ctrl (alone)" PTT_KEY1_NAME="rctrl" PTT_KEY2_NAME="" - PTT_KEY1_CODES="KEY_RIGHTCTRL" + PTT_KEY1_CODES="ecodes.KEY_RIGHTCTRL" PTT_KEY2_CODES="" ;; 5) PTT_LABEL="Right Alt (alone)" PTT_KEY1_NAME="ralt" PTT_KEY2_NAME="" - PTT_KEY1_CODES="KEY_RIGHTALT" + PTT_KEY1_CODES="ecodes.KEY_RIGHTALT" PTT_KEY2_CODES="" ;; *) @@ -186,8 +186,8 @@ case "$HOTKEY_CHOICE" in PTT_LABEL="Ctrl + Alt" PTT_KEY1_NAME="ctrl" PTT_KEY2_NAME="alt" - PTT_KEY1_CODES="KEY_LEFTCTRL, KEY_RIGHTCTRL" - PTT_KEY2_CODES="KEY_LEFTALT, KEY_RIGHTALT" + PTT_KEY1_CODES="ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL" + PTT_KEY2_CODES="ecodes.KEY_LEFTALT, ecodes.KEY_RIGHTALT" ;; esac @@ -227,7 +227,7 @@ except ImportError: WHISPR_HOTKEY_UP = ["xdotool", "keyup", "ctrl", "alt", "super"] WHISPR_HOTKEY_SEND = ["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"] -PTT_CODES = (ecodes.${PTT_KEY1_CODES},) +PTT_CODES = (${PTT_KEY1_CODES},) is_recording = False @@ -326,8 +326,8 @@ except ImportError: WHISPR_HOTKEY_UP = ["xdotool", "keyup", "ctrl", "alt", "super"] WHISPR_HOTKEY_SEND = ["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"] -KEY1_CODES = (ecodes.${PTT_KEY1_CODES},) -KEY2_CODES = (ecodes.${PTT_KEY2_CODES},) +KEY1_CODES = (${PTT_KEY1_CODES},) +KEY2_CODES = (${PTT_KEY2_CODES},) key1_held = False key2_held = False