From b2d8a935ef27ac720155189abee454eaf0268eca Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 13:56:01 -0500 Subject: [PATCH 1/7] =?UTF-8?q?feat(voice):=20=E2=9C=A8=20add=20keyboard-d?= =?UTF-8?q?riven=20voice-to-text=20terminal=20input?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add push-to-talk voice input for zsh terminal workflows. Ctrl-B starts background mic capture via arecord/PipeWire, Ctrl-Y stops recording, runs local ASR via whisper.cpp, and pastes the transcript into the prompt. All logic is CLI-first and dotfiles-managed. ## Highlights - `voice-record` script: start/stop arecord in background, transcribe via whisper-cli, write transcript atomically to state file - `voice-paste` script: print last transcript to stdout for composability - ZLE widgets bind Ctrl-B (record) and Ctrl-Y (stop + transcribe + paste) - Installer builds whisper.cpp from source and downloads ggml-base.en model - Shared config.env with env-var overrides for device, format, rate, ASR binary, model path, and state paths Co-Authored-By: Claude Opus 4.6 --- .gitignore | 1 + install.sh | 6 ++ shell/install.sh | 1 + shell/zsh/.zshrc | 1 + shell/zsh/voice-bindings.zsh | 34 ++++++++++ voice/bin/voice-paste | 13 ++++ voice/bin/voice-record | 108 +++++++++++++++++++++++++++++++ voice/config/config.env | 12 ++++ voice/install/install.sh | 122 +++++++++++++++++++++++++++++++++++ voice/state/.gitkeep | 0 10 files changed, 298 insertions(+) create mode 100644 .gitignore create mode 100644 shell/zsh/voice-bindings.zsh create mode 100755 voice/bin/voice-paste create mode 100755 voice/bin/voice-record create mode 100644 voice/config/config.env create mode 100755 voice/install/install.sh create mode 100644 voice/state/.gitkeep diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..442c1f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +voice/state/last.txt diff --git a/install.sh b/install.sh index 3a8c2f6..853e599 100755 --- a/install.sh +++ b/install.sh @@ -107,6 +107,12 @@ bash "$DOTFILES_DIR/nvim/install.sh" --backup --native --yes echo "▶ Installing AI Tooling" bash "$DOTFILES_DIR/ai/install.sh" +# ---------------------------- +# Voice input +# ---------------------------- +echo "▶ Installing Voice input" +bash "$DOTFILES_DIR/voice/install/install.sh" + # ---------------------------- # Final message # ---------------------------- diff --git a/shell/install.sh b/shell/install.sh index 523e011..357e917 100755 --- a/shell/install.sh +++ b/shell/install.sh @@ -40,6 +40,7 @@ copy() { rm -f "$2"; cp -f "$1" "$2"; } for filename in \ keybindings.zsh \ word-jump.zsh \ + voice-bindings.zsh \ env.zsh \ aliases.zsh \ completions.zsh \ diff --git a/shell/zsh/.zshrc b/shell/zsh/.zshrc index fe476ec..4d7169a 100644 --- a/shell/zsh/.zshrc +++ b/shell/zsh/.zshrc @@ -20,6 +20,7 @@ if [[ -o interactive ]]; then source "$ZDOTDIR/compdefs.zsh" source "$ZDOTDIR/keybindings.zsh" source "$ZDOTDIR/word-jump.zsh" + source "$ZDOTDIR/voice-bindings.zsh" fi # ------------------------------------------------------------------------------ diff --git a/shell/zsh/voice-bindings.zsh b/shell/zsh/voice-bindings.zsh new file mode 100644 index 0000000..386cada --- /dev/null +++ b/shell/zsh/voice-bindings.zsh @@ -0,0 +1,34 @@ +# voice-bindings.zsh — ZLE widgets for voice-to-text input +# +# Ctrl-B → start recording (arecord in background) +# Ctrl-Y → stop recording, transcribe, paste into prompt + +voice-record-widget() { + zle -I + voice-record start 2>&1 + zle reset-prompt +} + +voice-stop-paste-widget() { + zle -I + voice-record stop 2>/dev/null + + local transcript_file="${VOICE_TRANSCRIPT:-$HOME/.config/voice/state/last.txt}" + if [[ -f "$transcript_file" ]]; then + local text + text="$(<"$transcript_file")" + if [[ -n "$text" ]]; then + LBUFFER+="$text" + fi + fi + + zle reset-prompt +} + +zle -N voice-record-widget +zle -N voice-stop-paste-widget + +for map in emacs viins; do + bindkey -M "$map" '^B' voice-record-widget + bindkey -M "$map" '^Y' voice-stop-paste-widget +done diff --git a/voice/bin/voice-paste b/voice/bin/voice-paste new file mode 100755 index 0000000..7a6cdee --- /dev/null +++ b/voice/bin/voice-paste @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# voice-paste — print last transcript to stdout +set -euo pipefail + +VOICE_CONFIG="${VOICE_CONFIG:-$HOME/.config/voice/config.env}" +# shellcheck source=/dev/null +[[ -f "$VOICE_CONFIG" ]] && source "$VOICE_CONFIG" + +: "${VOICE_TRANSCRIPT:=$HOME/.config/voice/state/last.txt}" + +if [[ -f "$VOICE_TRANSCRIPT" ]]; then + cat "$VOICE_TRANSCRIPT" +fi diff --git a/voice/bin/voice-record b/voice/bin/voice-record new file mode 100755 index 0000000..b281cdb --- /dev/null +++ b/voice/bin/voice-record @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# voice-record — start background audio capture via arecord +# Usage: voice-record (start recording) +# voice-record --stop (stop recording, transcribe, output transcript) +set -euo pipefail + +# ── Config ────────────────────────────────────────────────────────── +VOICE_CONFIG="${VOICE_CONFIG:-$HOME/.config/voice/config.env}" +# shellcheck source=/dev/null +[[ -f "$VOICE_CONFIG" ]] && source "$VOICE_CONFIG" + +# Fallback defaults (in case config is missing or empty) +: "${VOICE_DEVICE:=pipewire}" +: "${VOICE_FORMAT:=S16_LE}" +: "${VOICE_RATE:=16000}" +: "${VOICE_CHANNELS:=1}" +: "${VOICE_ASR_BIN:=whisper-cli}" +: "${VOICE_MODEL:=$HOME/.local/share/whisper/ggml-base.en.bin}" +: "${VOICE_TRANSCRIPT:=$HOME/.config/voice/state/last.txt}" +: "${VOICE_PID_FILE:=$HOME/.config/voice/state/arecord.pid}" +: "${VOICE_WAV_FILE:=$HOME/.config/voice/state/capture.wav}" + +# ── Helpers ───────────────────────────────────────────────────────── +die() { printf '%s\n' "$*" >&2; exit 1; } + +ensure_state_dir() { + mkdir -p "$(dirname "$VOICE_TRANSCRIPT")" +} + +# ── Start recording ──────────────────────────────────────────────── +start_recording() { + command -v arecord >/dev/null 2>&1 || die "✖ arecord not found (install alsa-utils)" + + # If already recording, bail + if [[ -f "$VOICE_PID_FILE" ]] && kill -0 "$(cat "$VOICE_PID_FILE")" 2>/dev/null; then + printf 'Already recording (PID %s)\n' "$(cat "$VOICE_PID_FILE")" >&2 + return 0 + fi + + ensure_state_dir + + arecord \ + -D "$VOICE_DEVICE" \ + -f "$VOICE_FORMAT" \ + -r "$VOICE_RATE" \ + -c "$VOICE_CHANNELS" \ + -t wav \ + "$VOICE_WAV_FILE" &>/dev/null & + + printf '%d' $! > "$VOICE_PID_FILE" + printf 'Recording started (PID %d)\n' $! >&2 +} + +# ── Stop recording + transcribe ──────────────────────────────────── +stop_recording() { + if [[ ! -f "$VOICE_PID_FILE" ]]; then + die "✖ No recording in progress" + fi + + local pid + pid="$(cat "$VOICE_PID_FILE")" + + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi + rm -f "$VOICE_PID_FILE" + + if [[ ! -f "$VOICE_WAV_FILE" ]]; then + die "✖ No WAV file found at $VOICE_WAV_FILE" + fi + + # ── Validate ASR tooling ───────────────────────────────────────── + command -v "$VOICE_ASR_BIN" >/dev/null 2>&1 \ + || die "✖ ASR binary not found: $VOICE_ASR_BIN" + [[ -f "$VOICE_MODEL" ]] \ + || die "✖ Whisper model not found: $VOICE_MODEL" + + # ── Transcribe ─────────────────────────────────────────────────── + local transcript + transcript="$("$VOICE_ASR_BIN" \ + -m "$VOICE_MODEL" \ + -f "$VOICE_WAV_FILE" \ + --no-timestamps \ + -nt 2>/dev/null)" || die "✖ Transcription failed" + + # Strip leading/trailing whitespace + transcript="$(printf '%s' "$transcript" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" + + # ── Write transcript atomically ────────────────────────────────── + ensure_state_dir + local tmp + tmp="$(mktemp "${VOICE_TRANSCRIPT}.XXXXXX")" + printf '%s' "$transcript" > "$tmp" + mv -f "$tmp" "$VOICE_TRANSCRIPT" + + # Cleanup WAV + rm -f "$VOICE_WAV_FILE" + + printf 'Transcribed: %s\n' "$transcript" >&2 +} + +# ── Main ──────────────────────────────────────────────────────────── +case "${1:-start}" in + start) start_recording ;; + --stop|stop) stop_recording ;; + *) die "Usage: voice-record [start|stop]" ;; +esac diff --git a/voice/config/config.env b/voice/config/config.env new file mode 100644 index 0000000..de639a7 --- /dev/null +++ b/voice/config/config.env @@ -0,0 +1,12 @@ +# voice/config/config.env — shared defaults for voice scripts +# Override any variable via environment before sourcing. + +: "${VOICE_DEVICE:=pipewire}" +: "${VOICE_FORMAT:=S16_LE}" +: "${VOICE_RATE:=16000}" +: "${VOICE_CHANNELS:=1}" +: "${VOICE_ASR_BIN:=whisper-cli}" +: "${VOICE_MODEL:=$HOME/.local/share/whisper/ggml-base.en.bin}" +: "${VOICE_TRANSCRIPT:=$HOME/.config/voice/state/last.txt}" +: "${VOICE_PID_FILE:=$HOME/.config/voice/state/arecord.pid}" +: "${VOICE_WAV_FILE:=$HOME/.config/voice/state/capture.wav}" diff --git a/voice/install/install.sh b/voice/install/install.sh new file mode 100755 index 0000000..8641c59 --- /dev/null +++ b/voice/install/install.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# voice/install/install.sh — install voice input tooling +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +if [[ -d "$REPO_ROOT/dotfiles/voice" ]]; then + DOTFILES_DIR="$REPO_ROOT/dotfiles" +else + DOTFILES_DIR="$REPO_ROOT" +fi + +VOICE_SRC="$DOTFILES_DIR/voice" +ZDOTDIR_PATH="${ZDOTDIR:-$HOME/.config/zsh}" + +copy() { rm -f "$2"; cp -f "$1" "$2"; } + +# ── Install voice scripts ────────────────────────────────────────── +echo " → Installing voice scripts" +mkdir -p "$HOME/.local/bin" +for cmd in voice-record voice-paste; do + copy "$VOICE_SRC/bin/$cmd" "$HOME/.local/bin/$cmd" + chmod +x "$HOME/.local/bin/$cmd" +done + +# ── Install config (skip if user already has one) ────────────────── +echo " → Installing voice config" +mkdir -p "$HOME/.config/voice" +if [[ ! -f "$HOME/.config/voice/config.env" ]]; then + copy "$VOICE_SRC/config/config.env" "$HOME/.config/voice/config.env" +else + echo " (config.env already exists, skipping)" +fi + +# ── Create state directory ───────────────────────────────────────── +mkdir -p "$HOME/.config/voice/state" + +# ── Install zsh bindings ─────────────────────────────────────────── +echo " → Installing zsh voice bindings" +mkdir -p "$ZDOTDIR_PATH" +copy "$DOTFILES_DIR/shell/zsh/voice-bindings.zsh" "$ZDOTDIR_PATH/voice-bindings.zsh" + +# ── Build whisper.cpp from source ────────────────────────────────── +WHISPER_DIR="$HOME/.local/src/whisper.cpp" +WHISPER_BIN="$HOME/.local/bin/whisper-cli" + +if command -v whisper-cli >/dev/null 2>&1; then + echo " → whisper-cli already installed, skipping build" +else + echo " → Building whisper.cpp from source" + + # Install build dependencies + if command -v zypper >/dev/null 2>&1; then + sudo zypper install -y cmake gcc-c++ make git alsa-utils || true + fi + + if [[ -d "$WHISPER_DIR" ]]; then + echo " Updating existing checkout" + git -C "$WHISPER_DIR" pull --ff-only 2>/dev/null || true + else + mkdir -p "$(dirname "$WHISPER_DIR")" + git clone https://github.com/ggml-org/whisper.cpp.git "$WHISPER_DIR" + fi + + cmake -B "$WHISPER_DIR/build" -S "$WHISPER_DIR" -DCMAKE_BUILD_TYPE=Release + cmake --build "$WHISPER_DIR/build" -j"$(nproc)" --config Release + + # Install binary + if [[ -f "$WHISPER_DIR/build/bin/whisper-cli" ]]; then + cp -f "$WHISPER_DIR/build/bin/whisper-cli" "$WHISPER_BIN" + chmod +x "$WHISPER_BIN" + echo " ✔ whisper-cli installed to $WHISPER_BIN" + else + echo " ✖ whisper-cli build failed — binary not found" + echo " Try building manually: cd $WHISPER_DIR && cmake -B build && cmake --build build" + fi +fi + +# ── Download model if missing ────────────────────────────────────── +MODEL_DIR="$HOME/.local/share/whisper" +MODEL_FILE="$MODEL_DIR/ggml-base.en.bin" + +if [[ -f "$MODEL_FILE" ]]; then + echo " → Model already present: $MODEL_FILE" +else + echo " → Downloading whisper base.en model" + mkdir -p "$MODEL_DIR" + + if [[ -f "$WHISPER_DIR/models/download-ggml-model.sh" ]]; then + bash "$WHISPER_DIR/models/download-ggml-model.sh" base.en + # The script downloads to whisper.cpp/models/; move to our location + if [[ -f "$WHISPER_DIR/models/ggml-base.en.bin" ]]; then + cp -f "$WHISPER_DIR/models/ggml-base.en.bin" "$MODEL_FILE" + echo " ✔ Model installed to $MODEL_FILE" + fi + else + MODEL_URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin" + if command -v curl >/dev/null 2>&1; then + curl -L -o "$MODEL_FILE" "$MODEL_URL" + echo " ✔ Model downloaded to $MODEL_FILE" + elif command -v wget >/dev/null 2>&1; then + wget -O "$MODEL_FILE" "$MODEL_URL" + echo " ✔ Model downloaded to $MODEL_FILE" + else + echo " ✖ Cannot download model — install curl or wget" + echo " Download manually: $MODEL_URL → $MODEL_FILE" + fi + fi +fi + +# ── Summary ──────────────────────────────────────────────────────── +echo +if ! command -v whisper-cli >/dev/null 2>&1 && [[ ! -x "$WHISPER_BIN" ]]; then + echo " ⚠ whisper-cli not found in PATH" + echo " Ensure $HOME/.local/bin is in your PATH" +fi +if [[ ! -f "$MODEL_FILE" ]]; then + echo " ⚠ Whisper model not found: $MODEL_FILE" + echo " Download: https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin" +fi +echo " ✔ Voice input installation complete" diff --git a/voice/state/.gitkeep b/voice/state/.gitkeep new file mode 100644 index 0000000..e69de29 From 7c241783e258ee878d9d451c855fb9424f6c09a8 Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 13:58:38 -0500 Subject: [PATCH 2/7] =?UTF-8?q?fix(voice):=20=F0=9F=9A=9A=20move=20install?= =?UTF-8?q?.sh=20to=20subtree=20root=20to=20match=20convention?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other subtree uses `/install.sh`, not a nested `install/` directory. Fixes the REPO_ROOT path detection accordingly. Co-Authored-By: Claude Opus 4.6 --- install.sh | 2 +- voice/{install => }/install.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename voice/{install => }/install.sh (97%) diff --git a/install.sh b/install.sh index 853e599..9b2c244 100755 --- a/install.sh +++ b/install.sh @@ -111,7 +111,7 @@ bash "$DOTFILES_DIR/ai/install.sh" # Voice input # ---------------------------- echo "▶ Installing Voice input" -bash "$DOTFILES_DIR/voice/install/install.sh" +bash "$DOTFILES_DIR/voice/install.sh" # ---------------------------- # Final message diff --git a/voice/install/install.sh b/voice/install.sh similarity index 97% rename from voice/install/install.sh rename to voice/install.sh index 8641c59..1e491a6 100755 --- a/voice/install/install.sh +++ b/voice/install.sh @@ -1,9 +1,9 @@ #!/usr/bin/env bash -# voice/install/install.sh — install voice input tooling +# voice/install.sh — install voice input tooling set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" if [[ -d "$REPO_ROOT/dotfiles/voice" ]]; then DOTFILES_DIR="$REPO_ROOT/dotfiles" From 0becc0bdc9d99331ba9a45e71349ddaeb1dd8c53 Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 14:54:08 -0500 Subject: [PATCH 3/7] =?UTF-8?q?feat(voice):=20=E2=9C=A8=20add=20clipboard?= =?UTF-8?q?=20injection,=20tmux=20bindings,=20and=20KDE=20shortcuts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clipboard-based transcript injection allows pasting voice transcripts into any application (Claude Code, Codex, etc.) that doesn't support ZLE widgets. - copy_to_clipboard() auto-detects Wayland vs X11 (wl-copy / xclip) - kdialog passive popup notifications for start/stop feedback - Minimal tmux config with prefix+v / prefix+y voice keybindings - KDE .desktop files for Meta+Shift+V / Meta+Shift+Y global shortcuts - Install scripts wire everything together with absolute path rewriting Co-Authored-By: Claude Opus 4.6 --- install.sh | 6 ++++++ shell/tmux/install.sh | 20 ++++++++++++++++++++ shell/tmux/tmux.conf | 24 ++++++++++++++++++++++++ voice/bin/voice-record | 21 +++++++++++++++++++++ voice/install.sh | 15 ++++++++++++++- voice/kde/voice-record-start.desktop | 15 +++++++++++++++ voice/kde/voice-record-stop.desktop | 15 +++++++++++++++ 7 files changed, 115 insertions(+), 1 deletion(-) create mode 100755 shell/tmux/install.sh create mode 100644 shell/tmux/tmux.conf create mode 100644 voice/kde/voice-record-start.desktop create mode 100644 voice/kde/voice-record-stop.desktop diff --git a/install.sh b/install.sh index 9b2c244..ac41fa9 100755 --- a/install.sh +++ b/install.sh @@ -57,6 +57,12 @@ mkdir -p ~/.config/{zsh,nvim,helix,konsole,starship} # ---------------------------- bash "$DOTFILES_DIR/shell/install.sh" +# ---------------------------- +# Tmux +# ---------------------------- +echo "▶ Installing Tmux config" +bash "$DOTFILES_DIR/shell/tmux/install.sh" + # ---------------------------- # Set default shell # ---------------------------- diff --git a/shell/tmux/install.sh b/shell/tmux/install.sh new file mode 100755 index 0000000..d1e8faf --- /dev/null +++ b/shell/tmux/install.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# shell/tmux/install.sh — install tmux config (XDG layout) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMUX_DEST="$HOME/.config/tmux" + +echo " → Installing tmux config" +mkdir -p "$TMUX_DEST" + +rm -f "$TMUX_DEST/tmux.conf" +cp -f "$SCRIPT_DIR/tmux.conf" "$TMUX_DEST/tmux.conf" + +# Create local overrides placeholder if missing +if [[ ! -f "$TMUX_DEST/tmux.local.conf" ]]; then + printf '# Local tmux overrides (not managed by dotfiles)\n' > "$TMUX_DEST/tmux.local.conf" + echo " → Created tmux.local.conf placeholder" +fi + +echo " ✔ tmux config installed" diff --git a/shell/tmux/tmux.conf b/shell/tmux/tmux.conf new file mode 100644 index 0000000..cba0b3e --- /dev/null +++ b/shell/tmux/tmux.conf @@ -0,0 +1,24 @@ +# tmux.conf — managed by dotfiles +# ───────────────────────────────────────────────────────────────── + +# ── Sensible defaults ──────────────────────────────────────────── +set -g default-terminal "tmux-256color" +set -ga terminal-overrides ",xterm-256color:Tc" +set -g mouse on +set -g history-limit 50000 +set -g base-index 1 +setw -g pane-base-index 1 +set -g renumber-windows on +set -g escape-time 10 +set -g focus-events on + +# ── Voice keybindings ──────────────────────────────────────────── +# prefix + v → start voice recording +# prefix + y → stop recording, transcribe, copy to clipboard +bind v run-shell "voice-record start" +bind y run-shell "voice-record stop" + +# ── Local overrides ────────────────────────────────────────────── +if-shell "[ -f ~/.config/tmux/tmux.local.conf ]" { + source-file ~/.config/tmux/tmux.local.conf +} diff --git a/voice/bin/voice-record b/voice/bin/voice-record index b281cdb..6662fb0 100755 --- a/voice/bin/voice-record +++ b/voice/bin/voice-record @@ -27,6 +27,22 @@ ensure_state_dir() { mkdir -p "$(dirname "$VOICE_TRANSCRIPT")" } +copy_to_clipboard() { + local text="$1" + if [[ "${XDG_SESSION_TYPE:-}" == "wayland" ]] && command -v wl-copy >/dev/null 2>&1; then + printf '%s' "$text" | wl-copy + elif command -v xclip >/dev/null 2>&1; then + printf '%s' "$text" | xclip -selection clipboard + fi +} + +notify() { + local msg="$1" + if command -v kdialog >/dev/null 2>&1; then + kdialog --passivepopup "$msg" 3 &>/dev/null & + fi +} + # ── Start recording ──────────────────────────────────────────────── start_recording() { command -v arecord >/dev/null 2>&1 || die "✖ arecord not found (install alsa-utils)" @@ -49,6 +65,7 @@ start_recording() { printf '%d' $! > "$VOICE_PID_FILE" printf 'Recording started (PID %d)\n' $! >&2 + notify "🎙️ Recording started" } # ── Stop recording + transcribe ──────────────────────────────────── @@ -94,10 +111,14 @@ stop_recording() { printf '%s' "$transcript" > "$tmp" mv -f "$tmp" "$VOICE_TRANSCRIPT" + # ── Copy to clipboard ─────────────────────────────────────────── + copy_to_clipboard "$transcript" + # Cleanup WAV rm -f "$VOICE_WAV_FILE" printf 'Transcribed: %s\n' "$transcript" >&2 + notify "📋 Transcribed to clipboard" } # ── Main ──────────────────────────────────────────────────────────── diff --git a/voice/install.sh b/voice/install.sh index 1e491a6..b56ec82 100755 --- a/voice/install.sh +++ b/voice/install.sh @@ -41,6 +41,19 @@ echo " → Installing zsh voice bindings" mkdir -p "$ZDOTDIR_PATH" copy "$DOTFILES_DIR/shell/zsh/voice-bindings.zsh" "$ZDOTDIR_PATH/voice-bindings.zsh" +# ── Install KDE global shortcuts ────────────────────────────────── +KDE_SHORTCUT_DIR="$HOME/.local/share/kglobalaccel" +if [[ -d "$VOICE_SRC/kde" ]]; then + echo " → Installing KDE global shortcuts" + mkdir -p "$KDE_SHORTCUT_DIR" + for desktop_file in "$VOICE_SRC/kde"/*.desktop; do + [[ -f "$desktop_file" ]] || continue + dest="$KDE_SHORTCUT_DIR/$(basename "$desktop_file")" + sed "s|PLACEHOLDER_VOICE_RECORD|$HOME/.local/bin/voice-record|g" \ + "$desktop_file" > "$dest" + done +fi + # ── Build whisper.cpp from source ────────────────────────────────── WHISPER_DIR="$HOME/.local/src/whisper.cpp" WHISPER_BIN="$HOME/.local/bin/whisper-cli" @@ -52,7 +65,7 @@ else # Install build dependencies if command -v zypper >/dev/null 2>&1; then - sudo zypper install -y cmake gcc-c++ make git alsa-utils || true + sudo zypper install -y cmake gcc-c++ make git alsa-utils wl-clipboard || true fi if [[ -d "$WHISPER_DIR" ]]; then diff --git a/voice/kde/voice-record-start.desktop b/voice/kde/voice-record-start.desktop new file mode 100644 index 0000000..d597d3b --- /dev/null +++ b/voice/kde/voice-record-start.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Type=Application +Name=Voice Record Start +Comment=Start voice recording via voice-record +Exec=PLACEHOLDER_VOICE_RECORD start +Icon=audio-input-microphone +Terminal=false +Categories=Utility; +X-KDE-GlobalAccel-CommandShortcut=true +Actions=InvokeAction + +[Desktop Action InvokeAction] +Name=Start voice recording +Exec=PLACEHOLDER_VOICE_RECORD start +X-KDE-Shortcuts=Meta+Shift+V diff --git a/voice/kde/voice-record-stop.desktop b/voice/kde/voice-record-stop.desktop new file mode 100644 index 0000000..5956488 --- /dev/null +++ b/voice/kde/voice-record-stop.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Type=Application +Name=Voice Record Stop +Comment=Stop voice recording, transcribe, and copy to clipboard +Exec=PLACEHOLDER_VOICE_RECORD stop +Icon=audio-input-microphone +Terminal=false +Categories=Utility; +X-KDE-GlobalAccel-CommandShortcut=true +Actions=InvokeAction + +[Desktop Action InvokeAction] +Name=Stop voice recording +Exec=PLACEHOLDER_VOICE_RECORD stop +X-KDE-Shortcuts=Meta+Shift+Y From ab6d86016306148ffa32af8fc2ebd9d4706ac798 Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 15:17:24 -0500 Subject: [PATCH 4/7] =?UTF-8?q?fix(voice):=20=F0=9F=90=9B=20rebind=20voice?= =?UTF-8?q?=20keys=20to=20avoid=20tmux=20prefix=20conflict?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ZLE: Ctrl-B/Ctrl-Y → Alt-r/Alt-s (avoids tmux prefix capture) - tmux: prefix+v/prefix+y → prefix+r/prefix+s (mnemonic: record/stop) Co-Authored-By: Claude Opus 4.6 --- shell/tmux/tmux.conf | 8 ++++---- shell/zsh/voice-bindings.zsh | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/shell/tmux/tmux.conf b/shell/tmux/tmux.conf index cba0b3e..618d663 100644 --- a/shell/tmux/tmux.conf +++ b/shell/tmux/tmux.conf @@ -13,10 +13,10 @@ set -g escape-time 10 set -g focus-events on # ── Voice keybindings ──────────────────────────────────────────── -# prefix + v → start voice recording -# prefix + y → stop recording, transcribe, copy to clipboard -bind v run-shell "voice-record start" -bind y run-shell "voice-record stop" +# prefix + r → start voice recording +# prefix + s → stop recording, transcribe, copy to clipboard +bind r run-shell "voice-record start" +bind s run-shell "voice-record stop" # ── Local overrides ────────────────────────────────────────────── if-shell "[ -f ~/.config/tmux/tmux.local.conf ]" { diff --git a/shell/zsh/voice-bindings.zsh b/shell/zsh/voice-bindings.zsh index 386cada..085aeda 100644 --- a/shell/zsh/voice-bindings.zsh +++ b/shell/zsh/voice-bindings.zsh @@ -1,7 +1,7 @@ # voice-bindings.zsh — ZLE widgets for voice-to-text input # -# Ctrl-B → start recording (arecord in background) -# Ctrl-Y → stop recording, transcribe, paste into prompt +# Alt-r → start recording (arecord in background) +# Alt-s → stop recording, transcribe, paste into prompt voice-record-widget() { zle -I @@ -29,6 +29,6 @@ zle -N voice-record-widget zle -N voice-stop-paste-widget for map in emacs viins; do - bindkey -M "$map" '^B' voice-record-widget - bindkey -M "$map" '^Y' voice-stop-paste-widget + bindkey -M "$map" '\er' voice-record-widget + bindkey -M "$map" '\es' voice-stop-paste-widget done From bf86f3ad023cc775c38b7fc43139a65a01914112 Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 15:42:45 -0500 Subject: [PATCH 5/7] =?UTF-8?q?fix(voice):=20=F0=9F=90=9B=20prevent=20syst?= =?UTF-8?q?em=20freeze=20during=20whisper=20transcription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Background the stop command in tmux so it doesn't block input - Cap whisper-cli to 2 threads to avoid starving the system - Lower CPU priority with nice -n 15 - Add "Transcribing…" notification so user gets immediate feedback Co-Authored-By: Claude Opus 4.6 --- shell/tmux/tmux.conf | 2 +- voice/bin/voice-record | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/shell/tmux/tmux.conf b/shell/tmux/tmux.conf index 618d663..9770460 100644 --- a/shell/tmux/tmux.conf +++ b/shell/tmux/tmux.conf @@ -16,7 +16,7 @@ set -g focus-events on # prefix + r → start voice recording # prefix + s → stop recording, transcribe, copy to clipboard bind r run-shell "voice-record start" -bind s run-shell "voice-record stop" +bind s run-shell "voice-record stop &" # ── Local overrides ────────────────────────────────────────────── if-shell "[ -f ~/.config/tmux/tmux.local.conf ]" { diff --git a/voice/bin/voice-record b/voice/bin/voice-record index 6662fb0..15e490a 100755 --- a/voice/bin/voice-record +++ b/voice/bin/voice-record @@ -83,6 +83,8 @@ stop_recording() { fi rm -f "$VOICE_PID_FILE" + notify "⏳ Transcribing…" + if [[ ! -f "$VOICE_WAV_FILE" ]]; then die "✖ No WAV file found at $VOICE_WAV_FILE" fi @@ -95,9 +97,10 @@ stop_recording() { # ── Transcribe ─────────────────────────────────────────────────── local transcript - transcript="$("$VOICE_ASR_BIN" \ + transcript="$(nice -n 15 "$VOICE_ASR_BIN" \ -m "$VOICE_MODEL" \ -f "$VOICE_WAV_FILE" \ + -t 2 \ --no-timestamps \ -nt 2>/dev/null)" || die "✖ Transcription failed" From 6843cb6338d3e2e66c11ebf31e1622b60281c3b8 Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 15:52:33 -0500 Subject: [PATCH 6/7] =?UTF-8?q?fix(voice):=20=F0=9F=90=9B=20detach=20trans?= =?UTF-8?q?cription=20from=20tmux=20process=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use setsid to fully detach stop command so run-shell returns immediately - Inject transcript directly into tmux pane via send-keys Co-Authored-By: Claude Opus 4.6 --- shell/tmux/tmux.conf | 2 +- voice/bin/voice-record | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/shell/tmux/tmux.conf b/shell/tmux/tmux.conf index 9770460..249f146 100644 --- a/shell/tmux/tmux.conf +++ b/shell/tmux/tmux.conf @@ -16,7 +16,7 @@ set -g focus-events on # prefix + r → start voice recording # prefix + s → stop recording, transcribe, copy to clipboard bind r run-shell "voice-record start" -bind s run-shell "voice-record stop &" +bind s run-shell "setsid voice-record stop >/dev/null 2>&1 &" # ── Local overrides ────────────────────────────────────────────── if-shell "[ -f ~/.config/tmux/tmux.local.conf ]" { diff --git a/voice/bin/voice-record b/voice/bin/voice-record index 15e490a..0f37e30 100755 --- a/voice/bin/voice-record +++ b/voice/bin/voice-record @@ -117,6 +117,11 @@ stop_recording() { # ── Copy to clipboard ─────────────────────────────────────────── copy_to_clipboard "$transcript" + # ── Inject into tmux pane if running inside tmux ──────────────── + if [[ -n "${TMUX:-}" ]]; then + tmux send-keys -l -- "$transcript" + fi + # Cleanup WAV rm -f "$VOICE_WAV_FILE" From 2fa36a1aa3ff085cb5304fc4427ff94d822f9ec1 Mon Sep 17 00:00:00 2001 From: Andrew Hu Date: Sun, 8 Feb 2026 16:01:21 -0500 Subject: [PATCH 7/7] =?UTF-8?q?fix(voice):=20=F0=9F=90=9B=20use=20tmux=20n?= =?UTF-8?q?ative=20background=20for=20stop=20command?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setsid broke clipboard and tmux injection by detaching from the session. run-shell -b (tmux 3.2+) backgrounds natively without losing environment context. Co-Authored-By: Claude Opus 4.6 --- shell/tmux/tmux.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell/tmux/tmux.conf b/shell/tmux/tmux.conf index 249f146..7b9dbfa 100644 --- a/shell/tmux/tmux.conf +++ b/shell/tmux/tmux.conf @@ -16,7 +16,7 @@ set -g focus-events on # prefix + r → start voice recording # prefix + s → stop recording, transcribe, copy to clipboard bind r run-shell "voice-record start" -bind s run-shell "setsid voice-record stop >/dev/null 2>&1 &" +bind s run-shell -b "voice-record stop" # ── Local overrides ────────────────────────────────────────────── if-shell "[ -f ~/.config/tmux/tmux.local.conf ]" {