diff --git a/README.md b/README.md index 8addde8..dd9ad30 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,9 @@ bindsym $mod+d exec talktype 2. Speak 3. Press the shortcut again → transcribes and types the text at your cursor +**Cancel:** Double-press the shortcut (press twice within ~1.5 seconds) to cancel +without transcribing. You can also press the shortcut during transcription to cancel. + ## Backends Server backends auto-start on first use — the model loads once and stays in diff --git a/talktype b/talktype index 4ac0e62..606f5b5 100755 --- a/talktype +++ b/talktype @@ -22,6 +22,8 @@ TALKTYPE_DIR="${TALKTYPE_DIR:-${XDG_RUNTIME_DIR:-/tmp}/talktype}" PIDFILE="$TALKTYPE_DIR/rec.pid" AUDIOFILE="$TALKTYPE_DIR/rec.wav" NOTIFYFILE="$TALKTYPE_DIR/notify.id" +TRANSFILE="$TALKTYPE_DIR/trans.pid" +STARTFILE="$TALKTYPE_DIR/rec.start" mkdir -p "$TALKTYPE_DIR" @@ -53,7 +55,11 @@ notify() { notify_close() { if [ -f "$NOTIFYFILE" ]; then - notify-send -a TalkType -r "$(cat "$NOTIFYFILE")" -e "TalkType" "" 2>/dev/null || true + gdbus call --session \ + --dest org.freedesktop.Notifications \ + --object-path /org/freedesktop/Notifications \ + --method org.freedesktop.Notifications.CloseNotification \ + "$(cat "$NOTIFYFILE")" &>/dev/null || true rm -f "$NOTIFYFILE" fi } @@ -125,6 +131,17 @@ check_deps() { check_deps +# ── If transcription in progress → cancel it ── +if [ -f "$TRANSFILE" ]; then + PID=$(cat "$TRANSFILE") + rm -f "$TRANSFILE" + if kill -0 "$PID" 2>/dev/null; then + kill "$PID" 2>/dev/null || true + notify_close + exit 0 + fi +fi + # ── If currently recording → stop, transcribe, type ── if [ -f "$PIDFILE" ]; then PID=$(cat "$PIDFILE") @@ -133,19 +150,41 @@ if [ -f "$PIDFILE" ]; then while kill -0 "$PID" 2>/dev/null; do sleep 0.05; done rm -f "$PIDFILE" - notify process-working "Transcribing..." + # Double-press cancel: if recording was under 1.5 seconds, discard + if [ -f "$STARTFILE" ]; then + START=$(cat "$STARTFILE") + rm -f "$STARTFILE" + if NOW=$(date +%s%N 2>/dev/null) && [ $(( (NOW - START) / 1000000 )) -lt 1500 ]; then + rm -f "$AUDIOFILE" + notify_close + exit 0 + fi + fi - # Run the transcription command with the audio file as last arg - TEXT=$($TALKTYPE_CMD "$AUDIOFILE") + notify_close - rm -f "$AUDIOFILE" + # Run transcription in background so it can be cancelled + TRANSOUT="$TALKTYPE_DIR/trans.out" + $TALKTYPE_CMD "$AUDIOFILE" > "$TRANSOUT" & + TRANS_PID=$! + echo "$TRANS_PID" > "$TRANSFILE" - if [ -z "$TEXT" ]; then - notify dialog-warning "No speech detected" + WAIT_STATUS=0 + wait "$TRANS_PID" || WAIT_STATUS=$? + + rm -f "$TRANSFILE" "$AUDIOFILE" + + if [ "$WAIT_STATUS" -ne 0 ]; then + rm -f "$TRANSOUT" exit 0 fi - notify_close + TEXT=$(cat "$TRANSOUT") + rm -f "$TRANSOUT" + + if [ -z "$TEXT" ]; then + exit 0 + fi # Type text at cursor type_text "$TEXT" @@ -163,5 +202,6 @@ else PID=$! disown "$PID" echo "$PID" > "$PIDFILE" + date +%s%N > "$STARTFILE" 2>/dev/null || true notify audio-input-microphone "Listening..." fi diff --git a/test/mocks/notify-send b/test/mocks/notify-send index 774a3a3..91dba45 100755 --- a/test/mocks/notify-send +++ b/test/mocks/notify-send @@ -1,3 +1,8 @@ #!/usr/bin/env bash -# Mock notify-send: no-op +# Mock notify-send + +# Support -p: output a fake notification ID +for arg in "$@"; do + [[ "$arg" == "-p" ]] && echo "42" +done exit 0 diff --git a/test/talktype.bats b/test/talktype.bats index a3b159e..955067c 100644 --- a/test/talktype.bats +++ b/test/talktype.bats @@ -20,10 +20,12 @@ setup() { } teardown() { - # Kill any leftover recording processes - if [ -f "$TALKTYPE_DIR/rec.pid" ]; then - kill "$(cat "$TALKTYPE_DIR/rec.pid")" 2>/dev/null || true - fi + # Kill any leftover processes + for pidfile in rec.pid trans.pid; do + if [ -f "$TALKTYPE_DIR/$pidfile" ]; then + kill "$(cat "$TALKTYPE_DIR/$pidfile")" 2>/dev/null || true + fi + done rm -rf "$TALKTYPE_DIR" } @@ -32,6 +34,8 @@ start_fake_recording() { sleep 300 & echo $! > "$TALKTYPE_DIR/rec.pid" echo "audio data" > "$TALKTYPE_DIR/rec.wav" + # Write a timestamp 10 seconds in the past (well above cancel threshold) + echo $(( $(date +%s%N) - 10000000000 )) > "$TALKTYPE_DIR/rec.start" } # ── Recording lifecycle ── @@ -122,8 +126,8 @@ start_fake_recording() { run "$TALKTYPE" - # Script should fail (set -e catches the non-zero exit) - [ "$status" -ne 0 ] + # Script exits cleanly — failure is caught by the wait pattern + [ "$status" -eq 0 ] # No typing tool should have been called [ ! -f "$TALKTYPE_DIR/ydotool.log" ] @@ -238,6 +242,50 @@ start_fake_recording() { # ── Dependency checking ── +# ── Cancel mechanisms ── + +@test "double-press cancels recording without transcribing" { + start_fake_recording + # Overwrite with a fresh timestamp (just now — under 1 second) + date +%s%N > "$TALKTYPE_DIR/rec.start" + + run "$TALKTYPE" + [ "$status" -eq 0 ] + + # Should not have transcribed or typed + [ ! -f "$TALKTYPE_DIR/wtype.log" ] + [ ! -f "$TALKTYPE_DIR/ydotool.log" ] + # Audio file should be cleaned up + [ ! -f "$TALKTYPE_DIR/rec.wav" ] +} + +@test "hotkey during transcription cancels it" { + # Create a fake transcription process + sleep 300 & + local trans_pid=$! + echo "$trans_pid" > "$TALKTYPE_DIR/trans.pid" + + run "$TALKTYPE" + [ "$status" -eq 0 ] + + # Transcription process should be killed + ! kill -0 "$trans_pid" 2>/dev/null + [ ! -f "$TALKTYPE_DIR/trans.pid" ] +} + +@test "stale trans.pid falls through to start recording" { + echo "99999" > "$TALKTYPE_DIR/trans.pid" + + run "$TALKTYPE" + [ "$status" -eq 0 ] + + # Should have started recording + [ -f "$TALKTYPE_DIR/rec.pid" ] + [ ! -f "$TALKTYPE_DIR/trans.pid" ] +} + +# ── Dependency checking ── + @test "fails when no typing tool is available" { # Create a minimal PATH with only recorder + notify (no typing tools) local sparse="$BATS_TEST_TMPDIR/sparse_path"