#!/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="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="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="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="ecodes.KEY_RIGHTCTRL" PTT_KEY2_CODES="" ;; 5) PTT_LABEL="Right Alt (alone)" PTT_KEY1_NAME="ralt" PTT_KEY2_NAME="" PTT_KEY1_CODES="ecodes.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="ecodes.KEY_LEFTCTRL, ecodes.KEY_RIGHTCTRL" PTT_KEY2_CODES="ecodes.KEY_LEFTALT, ecodes.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 = (${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 = (${PTT_KEY1_CODES},) KEY2_CODES = (${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 "$@"