-
Notifications
You must be signed in to change notification settings - Fork 1
Hotkeys and Recorder Widget
Two pieces of shared infrastructure that both dictation and Read Aloud depend on.
HotkeyManager supports two kinds of triggers:
-
Bare modifier keys (e.g. Right Command, Right Option) via a global
NSEvent.flagsChangedmonitor. TheKeyboardShortcutslibrary can't bind bare modifiers, so Zerm watches modifier state directly through aHotkeyOptionenum. -
Key combinations via the
KeyboardShortcutslibrary (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]
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'sNSEventglobal monitor.rdev::listen()is not used — it crashes on macOS 26 withSIGTRAP.
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.
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