Compare commits
14
Commits
723436dae9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df058569c4 | ||
|
|
9292ebf714 | ||
|
|
6d21f18a20 | ||
|
|
62b2a2277e | ||
|
|
22d4f64bb3 | ||
|
|
a07869f8be | ||
|
|
124fc8bc5b | ||
|
|
6991fdbbee | ||
|
|
9137bd2d80 | ||
|
|
150485ee01 | ||
|
|
11e8fa9c26 | ||
|
|
2ab06ca664 | ||
|
|
02d332bcaa | ||
|
|
d7d2aad396 |
@@ -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/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/).
|
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
|
## [1.0.0] — 2026-03-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -3,111 +3,133 @@
|
|||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/outis1one/openwhispr-easy-setup/actions/workflows/lint.yml)
|
[](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
|
```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
|
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 |
|
| Step | Action | Details |
|
||||||
|------|--------|---------|
|
|------|--------|---------|
|
||||||
| 1/8 | **Intro** | Explains OpenWhispr and the privacy model |
|
| 1/8 | Intro | Explains OpenWhispr and the privacy model |
|
||||||
| 2/8 | **Detect distro + arch** | Ubuntu, Debian, Mint, Fedora, openSUSE, Arch — amd64/arm64 |
|
| 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 |
|
| 3/8 | Install OpenWhispr | Fetches latest release from GitHub |
|
||||||
| 4/8 | **Paste dependencies** | xdotool (X11), wtype (Wayland), kdotool (KDE), or D-Bus (GNOME) |
|
| 4/8 | Paste dependencies | xdotool (X11), wtype (Wayland), kdotool (KDE) |
|
||||||
| 5/8 | **STT provider** | Local Whisper/Parakeet, Groq, or OpenAI Realtime |
|
| 5/8 | STT provider | Local Whisper/Parakeet, Groq, or OpenAI Realtime |
|
||||||
| 6/8 | **AI cleanup** | OpenAI, Anthropic, Groq, Ollama, or skip |
|
| 6/8 | AI cleanup | OpenAI, Anthropic, Groq, Ollama, or skip |
|
||||||
| 7/8 | **First launch** | Opens OpenWhispr for model download and hotkey setup |
|
| 7/8 | First launch | Opens OpenWhispr for model download |
|
||||||
| 8/8 | **Summary** | Prints config, useful commands, and next steps |
|
| 8/8 | Summary | Prints config and next steps |
|
||||||
|
|
||||||
## STT options
|
</details>
|
||||||
|
|
||||||
| 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).
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
- **OpenWhispr** installed ([releases](https://github.com/OpenWhispr/openwhispr/releases))
|
||||||
- **bash 4+**
|
- **bash 4+**
|
||||||
- **curl** — for downloading
|
- **Python 3** with `evdev` (installed by setup script)
|
||||||
- **jq** — for parsing GitHub API (script offers to install if missing)
|
- **xdotool** (installed by setup script)
|
||||||
|
- **X11** session (Wayland support may vary)
|
||||||
|
|
||||||
## Safe to re-run
|
## Known OpenWhispr Issues on Linux
|
||||||
|
|
||||||
The script is idempotent:
|
| Issue | Status | Workaround |
|
||||||
- Skips OpenWhispr install if the latest version is already present
|
|-------|--------|------------|
|
||||||
- Never overwrites existing API keys in `~/.bashrc`
|
| Hold mode doesn't persist | App bug | This PTT script |
|
||||||
- Detects already-installed paste dependencies
|
| 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 |
|
||||||
## Troubleshooting
|
| NVIDIA provider on non-GPU systems | Config issue | Switch to CPU Whisper in Settings → Transcription |
|
||||||
|
|
||||||
- [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)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
+88
-76
@@ -2,119 +2,131 @@
|
|||||||
"""
|
"""
|
||||||
openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr
|
openwhispr-ptt.py — Push-to-talk wrapper for OpenWhispr
|
||||||
|
|
||||||
Turns Ctrl+Alt (hold) into Ctrl+` toggle pairs:
|
Turns Ctrl+Alt (hold) into Ctrl+` toggle pairs using raw evdev input.
|
||||||
- Press Ctrl+Alt → sends Ctrl+` to START dictation
|
No key repeat issues — reads physical key state directly.
|
||||||
- Release Ctrl+Alt → sends Ctrl+` to STOP dictation
|
|
||||||
|
|
||||||
Requires: pynput, xdotool
|
Requires: evdev, xdotool
|
||||||
Usage: python3 openwhispr-ptt.py
|
May need to run as root or add user to 'input' group.
|
||||||
|
Usage: sudo python3 openwhispr-ptt.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
|
||||||
import time
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pynput import keyboard
|
import evdev
|
||||||
|
from evdev import ecodes
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("Error: pynput not installed.")
|
print("Error: evdev not installed.")
|
||||||
print("Install with: pip3 install pynput")
|
print("Install with: pip3 install evdev --break-system-packages")
|
||||||
sys.exit(1)
|
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"
|
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
|
ctrl_held = False
|
||||||
super_held = False
|
alt_held = False
|
||||||
lock = threading.Lock()
|
is_recording = False
|
||||||
|
|
||||||
|
|
||||||
def send_whispr_toggle():
|
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:
|
try:
|
||||||
subprocess.run(
|
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,
|
timeout=2,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
)
|
)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
print("Error: xdotool not installed. Install with: sudo apt install xdotool")
|
print("Error: xdotool not installed.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def on_press(key):
|
def find_keyboard():
|
||||||
global is_recording, press_time, ctrl_held, super_held
|
"""Find the real physical keyboard device (not virtual devices)."""
|
||||||
|
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
|
||||||
with lock:
|
for dev in devices:
|
||||||
# Track modifier state — ignore key repeat (already True)
|
# Skip virtual devices (ydotool, xdotool, etc.)
|
||||||
if key in (PTT_MODIFIER, PTT_MODIFIER_ALT):
|
name_lower = dev.name.lower()
|
||||||
ctrl_held = True
|
if "virtual" in name_lower or "ydotool" in name_lower:
|
||||||
elif key in (PTT_KEY, PTT_KEY_ALT):
|
continue
|
||||||
super_held = True
|
caps = dev.capabilities(verbose=False)
|
||||||
|
if ecodes.EV_KEY in caps:
|
||||||
# Start recording when both held and not already recording
|
keys = caps[ecodes.EV_KEY]
|
||||||
if ctrl_held and super_held and not is_recording:
|
if ecodes.KEY_A in keys and ecodes.KEY_LEFTCTRL in keys:
|
||||||
press_time = time.time()
|
return dev
|
||||||
is_recording = True
|
return None
|
||||||
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 main():
|
def main():
|
||||||
print("═══════════════════════════════════════════")
|
global ctrl_held, alt_held, is_recording
|
||||||
print(" OpenWhispr Push-to-Talk")
|
|
||||||
print("═══════════════════════════════════════════")
|
print("=" * 43)
|
||||||
print(" Hold: Ctrl + Alt → start dictation")
|
print(" OpenWhispr Push-to-Talk (evdev)")
|
||||||
print(" Release: Ctrl + Alt → stop dictation")
|
print("=" * 43)
|
||||||
|
print(" Hold: Ctrl + Alt -> start dictation")
|
||||||
|
print(" Release: Ctrl + Alt -> stop dictation")
|
||||||
print(" Quit: Ctrl + C")
|
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")
|
print("Listening...\n")
|
||||||
|
|
||||||
# Handle Ctrl+C gracefully
|
|
||||||
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
signal.signal(signal.SIGINT, lambda *_: sys.exit(0))
|
||||||
|
|
||||||
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
|
for event in kb.read_loop():
|
||||||
listener.join()
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Executable
+589
@@ -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 "$@"
|
||||||
Reference in New Issue
Block a user