Compare commits

...
14 Commits
Author SHA1 Message Date
Outis df058569c4 Merge pull request #5 from outis1one/claude/create-bash-installer-GSmST
ShellCheck / ShellCheck (push) Canceled after 0s
Restructure repo: PTT workaround is now the primary focus
2026-03-26 09:50:46 -04:00
Claude 9292ebf714 Restructure repo: PTT workaround is now the primary focus
README rewritten to lead with the push-to-talk setup script as
the main feature, with the full OpenWhispr installer demoted to
an optional collapsed section. Documents known Linux bugs in
OpenWhispr (Hold mode, Ctrl+Super, settings persistence).

CHANGELOG updated with v1.1.0 entry.

https://claude.ai/code/session_01XKYC1basxdwtHy71tky7xm
2026-03-26 13:25:44 +00:00
Outis 6d21f18a20 Merge pull request #4 from outis1one/claude/create-bash-installer-GSmST
Claude/create bash installer g sm st
2026-03-26 00:09:19 -04:00
Claude 62b2a2277e 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
2026-03-26 04:06:49 +00:00
Claude 22d4f64bb3 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
2026-03-26 04:00:19 +00:00
Claude a07869f8be 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
2026-03-25 21:42:45 +00:00
Claude 124fc8bc5b 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
2026-03-25 20:40:47 +00:00
Claude 6991fdbbee 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
2026-03-25 20:37:05 +00:00
Claude 9137bd2d80 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
2026-03-25 20:33:03 +00:00
Claude 150485ee01 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
2026-03-25 20:08:57 +00:00
Outis 11e8fa9c26 Merge pull request #3 from outis1one/claude/create-bash-installer-GSmST
Claude/create bash installer g sm st
2026-03-25 10:41:04 -04:00
Outis 2ab06ca664 Merge pull request #2 from outis1one/claude/create-bash-installer-GSmST
Fix stdin when piped, improve binary detection, replace YOUR_USERNAME
2026-03-25 09:32:27 -04:00
Outis 02d332bcaa Update installation script URL in README 2026-03-25 09:10:37 -04:00
Outis d7d2aad396 Merge pull request #1 from outis1one/claude/create-bash-installer-GSmST
Add single-file bash installer for OpenWhispr
2026-03-25 09:07:14 -04:00
4 changed files with 816 additions and 165 deletions
+28
View File
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/),
and this project adheres to [Semantic Versioning](https://semver.org/).
## [1.1.0] — 2026-03-26
### Added
- **Push-to-talk setup script** `setup-ptt.sh` — one-command PTT setup
- Interactive hotkey selection (Ctrl+Alt, Ctrl+Shift, Alt+Shift, Right Ctrl, Right Alt)
- Generates PTT Python script using `evdev` for reliable key detection
- Creates systemd user service (auto-start, restart on failure)
- Adds user to `input` group (no sudo needed for PTT)
- Checks if OpenWhispr is running, offers to launch
- Creates uninstall script
- PTT script `openwhispr-ptt.py` using `evdev` for physical key events
- Filters key repeats (evdev value=2) — no rapid-fire toggling
- Skips virtual keyboards (ydotoold)
- Clean xdotool keyup/keydown sequence for reliable Ctrl+` delivery
### Changed
- README restructured — PTT workaround is now the primary focus
- Original installer (`setup-openwhispr.sh`) moved to optional/secondary
### Fixed
- Script works when run via `bash <(curl ...)` — all reads use `/dev/tty`
- Script wrapped in `main()` to prevent line-by-line execution bug
- Binary detection searches `/opt/`, `/usr/lib/`, `dpkg -L` paths
- Replaced `YOUR_USERNAME` placeholder with `outis1one`
## [1.0.0] — 2026-03-25
### Added
+111 -89
View File
@@ -3,111 +3,133 @@
[![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![ShellCheck](https://github.com/outis1one/openwhispr-easy-setup/actions/workflows/lint.yml/badge.svg)](https://github.com/outis1one/openwhispr-easy-setup/actions/workflows/lint.yml)
**One-command installer for [OpenWhispr](https://github.com/OpenWhispr/openwhispr) on Linux** — distro detection, dependencies, API keys, first-run setup.
Setup scripts for [OpenWhispr](https://github.com/OpenWhispr/openwhispr) on Linux**push-to-talk workaround**, dependency setup, and systemd autostart.
> *Your voice stays on your machine. Always. The only question is whether you want AI to clean up the text after transcription.*
> *Your voice stays on your machine. Always.*
## Quick install
## The Problem
OpenWhispr v1.6.6 on Linux has bugs where:
- **Hold-to-talk ("Hold" mode) doesn't persist** — always reverts to Tap/toggle
- **Ctrl+Super hotkey fails** to register on most desktop environments
- Settings changes in the UI don't save to config correctly
## The Solution: Push-to-Talk Wrapper
`setup-ptt.sh` installs a lightweight Python script that provides real hold-to-talk on top of OpenWhispr's toggle mode. Hold your chosen keys to start dictation, release to stop.
### Quick install
```bash
curl -fsSL https://raw.githubusercontent.com/outis1one/openwhispr-easy-setup/main/setup-ptt.sh -o /tmp/setup-ptt.sh && bash /tmp/setup-ptt.sh
```
### What it does
| Step | Action |
|------|--------|
| 1/6 | Intro |
| 2/6 | Install dependencies (xdotool, evdev), add user to `input` group |
| 3/6 | Choose PTT hotkey (Ctrl+Alt, Ctrl+Shift, Alt+Shift, Right Ctrl, Right Alt) |
| 4/6 | Generate PTT Python script with your chosen hotkey |
| 5/6 | Create systemd user service (auto-start on login, restart on failure) |
| 6/6 | Check OpenWhispr is running, create uninstall script, print summary |
### How it works
The PTT script uses `evdev` to read physical key events directly (no key-repeat issues), then sends `xdotool` commands to toggle OpenWhispr's Ctrl+` hotkey:
1. **Hold** your PTT keys → script sends Ctrl+` to start recording
2. **Release** → script sends Ctrl+` again to stop recording
3. OpenWhispr transcribes and pastes the text
### Hotkey options
| Option | Keys | Notes |
|--------|------|-------|
| 1 (default) | Ctrl + Alt | 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 |
Avoid Super/Windows key — most desktops intercept it.
### Useful commands
```bash
# Check PTT status
systemctl --user status openwhispr-ptt
# View live logs
journalctl --user -u openwhispr-ptt -f
# Restart PTT
systemctl --user restart openwhispr-ptt
# Stop PTT
systemctl --user stop openwhispr-ptt
# Disable auto-start
systemctl --user disable openwhispr-ptt
# Re-enable auto-start
systemctl --user enable openwhispr-ptt
# Run manually (for debugging)
python3 ~/.local/share/openwhispr-ptt/openwhispr-ptt.py
```
### Uninstall PTT
```bash
bash ~/.local/share/openwhispr-ptt/uninstall-ptt.sh
```
This removes only the PTT wrapper, not OpenWhispr itself.
---
## Optional: Full Installer
`setup-openwhispr.sh` is a separate script that installs OpenWhispr itself from GitHub releases with distro detection, paste dependencies, and API key setup. Most users can just install OpenWhispr directly from their [releases page](https://github.com/OpenWhispr/openwhispr/releases) and skip this.
<details>
<summary>Full installer details</summary>
```bash
curl -fsSL https://raw.githubusercontent.com/outis1one/openwhispr-easy-setup/main/setup-openwhispr.sh -o /tmp/setup-openwhispr.sh && bash /tmp/setup-openwhispr.sh
```
Or clone and run:
```bash
git clone https://github.com/outis1one/openwhispr-easy-setup.git
cd openwhispr-easy-setup
bash setup-openwhispr.sh
```
## What it does
| Step | Action | Details |
|------|--------|---------|
| 1/8 | **Intro** | Explains OpenWhispr and the privacy model |
| 2/8 | **Detect distro + arch** | Ubuntu, Debian, Mint, Fedora, openSUSE, Arch — amd64/arm64 |
| 3/8 | **Install OpenWhispr** | Fetches latest release from GitHub, picks correct package |
| 4/8 | **Paste dependencies** | xdotool (X11), wtype (Wayland), kdotool (KDE), or D-Bus (GNOME) |
| 5/8 | **STT provider** | Local Whisper/Parakeet, Groq, or OpenAI Realtime |
| 6/8 | **AI cleanup** | OpenAI, Anthropic, Groq, Ollama, or skip |
| 7/8 | **First launch** | Opens OpenWhispr for model download and hotkey setup |
| 8/8 | **Summary** | Prints config, useful commands, and next steps |
| 1/8 | Intro | Explains OpenWhispr and the privacy model |
| 2/8 | Detect distro + arch | Ubuntu, Debian, Mint, Fedora, openSUSE, Arch — amd64/arm64 |
| 3/8 | Install OpenWhispr | Fetches latest release from GitHub |
| 4/8 | Paste dependencies | xdotool (X11), wtype (Wayland), kdotool (KDE) |
| 5/8 | STT provider | Local Whisper/Parakeet, Groq, or OpenAI Realtime |
| 6/8 | AI cleanup | OpenAI, Anthropic, Groq, Ollama, or skip |
| 7/8 | First launch | Opens OpenWhispr for model download |
| 8/8 | Summary | Prints config and next steps |
## STT options
| Option | Method | Streaming | Cost | Audio sent to |
|--------|--------|-----------|------|---------------|
| 1 (default) | Local Whisper/Parakeet | No | Free | Nobody, ever |
| 2 | Groq whisper-large-v3-turbo | No | $0.04/hr* | Groq servers |
| 3 | OpenAI Realtime API | Yes | $0.06/min | OpenAI servers |
\*Groq free tier: ~2,000 audio seconds/day.
## AI cleanup options
| Provider | Model | Per use | 50/day/mo | 200/day/mo |
|----------|-------|---------|-----------|------------|
| OpenAI | gpt-4o-mini-2024-07-18 | $0.000058 | $0.09 | $0.35 |
| Anthropic | claude-haiku-4-5 | $0.000450 | $0.68 | $2.70 |
| Groq | llama-3.3-70b-versatile | Free* | Free* | Free* |
| Ollama | Local (fully offline) | Free | Free | Free |
| Skip | Raw transcription only | Free | Free | Free |
\*Groq free: 1,000 req/day, 100K tokens/day.
## Privacy
| Data | Where it goes | Condition |
|------|---------------|-----------|
| **Audio** | Stays on your machine | Default (local Whisper/Parakeet) |
| **Audio** | Groq or OpenAI servers | Only if you choose cloud STT |
| **Text** | AI cleanup provider | Only if you choose AI cleanup |
| **API keys** | `~/.bashrc` on your machine | Never sent anywhere by this script |
No provider uses your data for training. Cloud data is deleted within 30 days.
## Useful commands
```bash
openwhispr # Launch the app
openwhispr --help # Show options
source ~/.bashrc # Reload API keys in current shell
```
In the app:
- **Settings → Hotkey** — change your hotkey (default: backtick `` ` ``)
- **Settings → Models** — download better speech models
- **Settings → Processing** — change STT or AI provider
## Supported distros
| Distro | Package format | Status |
|--------|---------------|--------|
| Ubuntu / Debian / Mint | `.deb` | Supported |
| Fedora | `.rpm` (dnf) | Supported |
| openSUSE | `.rpm` (zypper) | Supported |
| Arch / Manjaro / EndeavourOS | `.tar.gz` | Supported |
Architecture: `x86_64` (amd64) and `aarch64` (arm64).
</details>
## Requirements
- **OpenWhispr** installed ([releases](https://github.com/OpenWhispr/openwhispr/releases))
- **bash 4+**
- **curl** — for downloading
- **jq** — for parsing GitHub API (script offers to install if missing)
- **Python 3** with `evdev` (installed by setup script)
- **xdotool** (installed by setup script)
- **X11** session (Wayland support may vary)
## Safe to re-run
## Known OpenWhispr Issues on Linux
The script is idempotent:
- Skips OpenWhispr install if the latest version is already present
- Never overwrites existing API keys in `~/.bashrc`
- Detects already-installed paste dependencies
## Troubleshooting
- [OpenWhispr Troubleshooting Guide](https://github.com/OpenWhispr/openwhispr/blob/main/TROUBLESHOOTING.md)
- [Open an issue](https://github.com/outis1one/openwhispr-easy-setup/issues/new/choose)
| Issue | Status | Workaround |
|-------|--------|------------|
| Hold mode doesn't persist | App bug | This PTT script |
| Ctrl+Super hotkey fails | App bug | Use Ctrl+` for OW, PTT script for hold |
| Settings don't save | App bug | Edit `.env` or LevelDB directly |
| NVIDIA provider on non-GPU systems | Config issue | Switch to CPU Whisper in Settings → Transcription |
## License
+88 -76
View File
@@ -2,119 +2,131 @@
"""
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
import time
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."""
"""Send Ctrl+` to OpenWhispr via xdotool.
Release all held modifiers first, then send the full
keydown ctrl, key grave, keyup ctrl sequence cleanly.
"""
try:
subprocess.run(
["xdotool", "key", WHISPR_HOTKEY],
["xdotool", "keyup", "ctrl", "alt", "super"],
timeout=2,
capture_output=True,
)
subprocess.run(
["xdotool", "keydown", "ctrl", "key", "grave", "keyup", "ctrl"],
timeout=2,
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 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]
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):
time.sleep(0.05) # Let key release propagate to X11
send_whispr_toggle()
is_recording = False
print("\033[0m○ Stopped", flush=True)
if __name__ == "__main__":
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="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 "$@"