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
This commit is contained in:
Claude
2026-03-26 04:00:19 +00:00
parent a07869f8be
commit 22d4f64bb3
Executable
+589
View File
@@ -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 "$@"