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 "$@"