Skip to content

feat: add cancel recording hotkey (Escape by default, configurable)#391

Open
N0amG wants to merge 1 commit intoOpenWhispr:mainfrom
N0amG:feature/cancel-recording-hotkey
Open

feat: add cancel recording hotkey (Escape by default, configurable)#391
N0amG wants to merge 1 commit intoOpenWhispr:mainfrom
N0amG:feature/cancel-recording-hotkey

Conversation

@N0amG
Copy link

@N0amG N0amG commented Mar 7, 2026

feat: Cancel recording hotkey (Escape by default, fully configurable)

Summary

As describe in the issue #290
Pressing a configurable key (default: Escape) during an active recording cancels it without transcribing. Outside of a recording, the key behaves normally system-wide.


How it works

The cancel hotkey is registered as a globalShortcut only while recording is active, then immediately unregistered on stop, cancel, or unexpected disconnect. Zero system-wide footprint at rest.


Changes

src/helpers/ipcHandlers.js

  • register-cancel-hotkey — calls hotkeyManager.registerSlot("cancel", key, cb) (reuses existing slot system)
  • unregister-cancel-hotkey — calls hotkeyManager.unregisterSlot("cancel")

preload.js

  • Exposes registerCancelHotkey, unregisterCancelHotkey, onCancelHotkeyPressed to the renderer via the existing registerListener pattern

src/hooks/useAudioRecording.js

  • performStartRecording() — registers the cancel hotkey on successful start
  • performStopRecording() — unregisters it before stopping
  • cancelRecording() — unregisters it before cancelling
  • onStateChangebug fix: unregisters if isRecording drops to false unexpectedly (e.g. streaming server disconnect), preventing a permanent Escape intercept

src/App.jsx

  • Listens for onCancelHotkeyPressed and calls cancelRecording() (guarded by isRecording)

src/stores/settingsStore.ts

  • cancelKey — persisted to localStorage, default "Escape", no IPC side-effects (key is read at recording start, not at boot)

src/hooks/useSettings.ts

  • Exposes cancelKey / setCancelKey through SettingsContext

src/components/SettingsPage.tsx

  • New HotkeyInput for the cancel key under Settings → Shortcuts
  • Reset-to-default button shown when value differs from "Escape"

src/components/ui/HotkeyInput.tsx

  • New prop skipListeningMode — when true, focusing the input does not suspend/resume the dictation hotkey (used for the cancel key input to avoid interference)
  • Adds data-capturing attribute while capturing, used by SidebarModal below

src/components/ui/SidebarModal.tsx

  • onEscapeKeyDown on DialogPrimitive.Content — prevents Radix from closing the modal when Escape is being captured by a HotkeyInput

src/locales/*/translation.json (10 languages)

  • New keys for the cancel hotkey UI label, description, and reset tooltip

Architecture note

The cancel key deliberately uses globalShortcut (same as the dictation key) rather than a DOM keydown listener, so it works even when OpenWhispr is in the background. It reuses the existing hotkeyManager slot system — no new infrastructure was needed.


Testing checklist

  • Escape cancels an active recording (no transcription triggered)
  • Escape works normally outside of recording
  • Custom cancel key can be set in Settings → Shortcuts
  • Cancel key resets to Escape via the reset button
  • Streaming disconnect should not leave the cancel key registered globally
  • Settings modal does not close when pressing Escape inside the cancel key input when registering hotkey
  • Dictation hotkey is not disrupted when focusing the cancel key input

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant