Skip to content

Hotkeys and Recorder Widget

thefourCraft edited this page Jun 21, 2026 · 1 revision

Hotkeys & Recorder Widget

Two pieces of shared infrastructure that both dictation and Read Aloud depend on.

Hotkeys

HotkeyManager supports two kinds of triggers:

  • Bare modifier keys (e.g. Right Command, Right Option) via a global NSEvent .flagsChanged monitor. The KeyboardShortcuts library can't bind bare modifiers, so Zerm watches modifier state directly through a HotkeyOption enum.
  • Key combinations via the KeyboardShortcuts library (Sindre Sorhus).

Dictation and Read Aloud each have their own trigger (selectedHotkey1/2 and readAloudHotkey). Settings warn if the two overlap.

flowchart TB
    EV[NSEvent global monitor<br/>flagsChanged + keyDown] --> HM[HotkeyManager]
    HM --> GATE{canProcessHotkeyAction?<br/>state == idle-ish}
    GATE -->|dictation key| REC[ZermEngine.toggleRecord]
    GATE -->|Read Aloud key| RA[TTSController.toggle]
    GATE -->|busy / speaking / thinking| IGN[ignored]
Loading

canProcessHotkeyAction blocks the hotkey while the engine is transcribing, enhancing, busy, speaking, preparingSpeech, or generatingSpeech — so a trigger can't interrupt an in-flight operation.

macOS 26 note: bare-modifier monitoring uses objc2-app-kit's NSEvent global monitor. rdev::listen() is not used — it crashes on macOS 26 with SIGTRAP.

The recorder widget

One widget — the notch (NotchRecorderView) or mini (MiniRecorderView) panel — visualizes everything, owned by RecorderUIManager. It is reused by dictation and Read Aloud, and shows the current RecordingState:

State Widget
recording live audio bars (mic level)
transcribing / enhancing "Transcribing" / "Enhancing" loader
generatingSpeech "Thinking…" (on-device AI rewrite)
preparingSpeech "Preparing…" (synthesizing)
speaking live audio bars (TTS output level)

During Read Aloud the bars are driven by the real TTS output level — TTSPlayer taps the engine mixer (RMS → recorder.audioMeter), the same meter dictation uses.

Mutual exclusion

A single RecordingState on ZermEngine is the source of truth, so dictation and Read Aloud can never run at once. RecorderUIManager arbitrates: Read Aloud can only start from idle; double-Escape cancels whichever operation is active (cancelActiveOperation routes to the right one).

See: Architecture · Read Aloud · Speech-to-Text

Clone this wiki locally