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/),
|
||||
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
|
||||
|
||||
@@ -3,111 +3,133 @@
|
||||
[](LICENSE)
|
||||
[](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
@@ -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
@@ -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