Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
voice/state/last.txt
12 changes: 12 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ----------------------------
Expand Down Expand Up @@ -107,6 +113,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.sh"

# ----------------------------
# Final message
# ----------------------------
Expand Down
1 change: 1 addition & 0 deletions shell/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
20 changes: 20 additions & 0 deletions shell/tmux/install.sh
Original file line number Diff line number Diff line change
@@ -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"
24 changes: 24 additions & 0 deletions shell/tmux/tmux.conf
Original file line number Diff line number Diff line change
@@ -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 + r → start voice recording
# prefix + s → stop recording, transcribe, copy to clipboard
bind r run-shell "voice-record start"
bind s run-shell -b "voice-record stop"

# ── Local overrides ──────────────────────────────────────────────
if-shell "[ -f ~/.config/tmux/tmux.local.conf ]" {
source-file ~/.config/tmux/tmux.local.conf
}
1 change: 1 addition & 0 deletions shell/zsh/.zshrc
Original file line number Diff line number Diff line change
Expand Up @@ -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

# ------------------------------------------------------------------------------
Expand Down
34 changes: 34 additions & 0 deletions shell/zsh/voice-bindings.zsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# voice-bindings.zsh — ZLE widgets for voice-to-text input
#
# Alt-r → start recording (arecord in background)
# Alt-s → 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" '\er' voice-record-widget
bindkey -M "$map" '\es' voice-stop-paste-widget
done
13 changes: 13 additions & 0 deletions voice/bin/voice-paste
Original file line number Diff line number Diff line change
@@ -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
137 changes: 137 additions & 0 deletions voice/bin/voice-record
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/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")"
}

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)"

# 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
notify "🎙️ Recording started"
}

# ── 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"

notify "⏳ Transcribing…"

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="$(nice -n 15 "$VOICE_ASR_BIN" \
-m "$VOICE_MODEL" \
-f "$VOICE_WAV_FILE" \
-t 2 \
--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"

# ── 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"

printf 'Transcribed: %s\n' "$transcript" >&2
notify "📋 Transcribed to clipboard"
}

# ── Main ────────────────────────────────────────────────────────────
case "${1:-start}" in
start) start_recording ;;
--stop|stop) stop_recording ;;
*) die "Usage: voice-record [start|stop]" ;;
esac
12 changes: 12 additions & 0 deletions voice/config/config.env
Original file line number Diff line number Diff line change
@@ -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}"
Loading