# Configure hotkeys and dictation mode

> Record a global shortcut in Settings, switch between hold-to-talk and hands-free toggle, validate against reserved macOS shortcuts, and verify registration after Accessibility is granted. notchHorizontalPosition and playFeedbackSounds side settings.

- Repository: cartesia-ai/InkIt
- GitHub: https://github.com/cartesia-ai/InkIt
- Human docs: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b
- Complete Markdown: https://www.grok-wiki.com/public/docs/cartesia-ai-inkit-18975554254b/llms-full.txt

## Source Files

- `InkIt/SettingsView.swift`
- `InkIt/SettingsStore.swift`
- `InkIt/HotkeyManager.swift`
- `InkIt/AppCoordinator.swift`
- `InkIt/FeedbackSoundPlayer.swift`

---

---
title: "Configure hotkeys and dictation mode"
description: "Record a global shortcut in Settings, switch between hold-to-talk and hands-free toggle, validate against reserved macOS shortcuts, and verify registration after Accessibility is granted. notchHorizontalPosition and playFeedbackSounds side settings."
---

InkIt exposes hotkey and activation controls on **Settings → Dictation**: `dictationMode`, `hotkey`, and `playFeedbackSounds` bind to `SettingsStore` and take effect immediately through `AppCoordinator.registerHotkey()` and the hotkey press/release handlers. `notchHorizontalPosition` is persisted in UserDefaults but has no Settings UI yet and is not read by `NotchHUDController`, which anchors to detected screen geometry.

## Where to configure

Open Settings from the Home gear icon (popover) or the standalone Settings window. The **Dictation** pane groups:

| Control | `SettingsStore` property | UserDefaults / storage |
| --- | --- | --- |
| Activation mode | `dictationMode` | `dictationMode` (`hold` \| `toggle`) |
| Dictation shortcut | `hotkey` | `hotkeyKind`, `hotkeyKeyCode`, `hotkeyModifiers` |
| Feedback sounds | `playFeedbackSounds` | `playFeedbackSounds` |
| Notch HUD horizontal offset | `notchHorizontalPosition` | `notchHorizontalPosition` |

Search also surfaces these rows (`general.activation`, `general.hotkey`, `general.sound`).

<Steps>
<Step title="Open Dictation settings">

Open **Settings → Dictation**. The pane shows **Activation mode** cards, a **Shortcut** group (hotkey recorder + sound toggle), then microphone and transcription sections.

</Step>
<Step title="Choose activation mode">

Pick one card:

- **Hold to talk** (`hold`, default) — press and hold the shortcut while speaking; release to finalize and paste.
- **Hands-free** (`toggle`) — first press starts recording; second press stops and pastes.

`AppCoordinator` routes press/release differently per mode: hold mode calls `startDictation()` on press and `stopDictation()` on release; toggle mode ignores release and toggles start/stop on each press.

</Step>
<Step title="Record a global shortcut">

Click the hotkey field (pencil icon). The recorder:

1. Calls `coordinator.unregisterHotkey()` so the live binding does not compete with capture.
2. Shows `press new shortcut` and listens for the next qualifying input.
3. On save, writes `settings.hotkey`, calls `coordinator.registerHotkey()`, and shows a **Shortcut saved** toast.
4. On cancel (Esc, click outside, window resigns key), restores the previous binding via `registerHotkey()`.

Press **Esc** or click outside to abandon without saving.

</Step>
<Step title="Grant Accessibility when using Fn or bare modifiers">

Fn/Globe and bare-modifier bindings use a `CGEventTap`. macOS requires **Accessibility** for a suppressing tap (Fn) or a listen-only tap (bare modifiers). After grant, `AppCoordinator` re-registers the hotkey so Fn upgrades from the passive `NSEvent` monitor to the suppressing tap.

Verify under **Settings → General → Permissions** that Accessibility shows **Enabled**, or test the shortcut in another app and confirm the notch HUD appears without macOS Globe/Emoji firing (Fn path).

</Step>
<Step title="Verify in any app">

With onboarding complete and a Cartesia API key set, switch to a text field in another app. Use the configured gesture:

- **Hold mode:** hold shortcut → speak → release → text pastes (or saves to History if no editable field is focused).
- **Toggle mode:** press once → speak → press again → paste.

Home status hint and the rotating header reflect `dictationModeVerb` (`Press` vs `Hold`) and `hotkeyDisplayString`.

</Step>
</Steps>

## Activation mode semantics

```text
Hold (.hold)                    Toggle (.toggle)
─────────────                   ───────────────
press  → startDictation()       press while idle → startDictation()
release → stopDictation()       press while recording → stopDictation()
release ignored in toggle       (no release action)
```

<ParamField body="dictationMode" type="DictationMode" required>
Persisted activation style. Values: `hold` (default), `toggle`. Drives `AppCoordinator.handleHotkeyPress()` / `handleHotkeyRelease()` and UI copy via `dictationModeVerb`.
</ParamField>

<ResponseField name="dictationModeVerb" type="string">
`"Press"` when `dictationMode == .toggle`; `"Hold"` otherwise. Used in Home hints, onboarding copy, and `TryItPracticeCard`.
</ResponseField>

## Recording shortcuts

`HotkeyRecorder` accepts three `HotkeyBinding` variants:

| Variant | Capture path | Runtime backend |
| --- | --- | --- |
| `.carbon(keyCode, modifiers)` | Key-down with ≥1 modifier (⌘⌥⌃⇧) | Carbon `RegisterEventHotKey` |
| `.fn` | Fn / Globe key-down or `flagsChanged` | `CGEventTap` (suppresses Globe when AX granted) |
| `.modifierKey(keyCode)` | Lone modifier press+release (no other key) | Listen-only `CGEventTap` or passive monitor |

**Carbon combos** require at least one modifier. A bare letter or function key without modifiers is rejected with an error toast.

**Fn** is captured via a dedicated `FnKeyCapture` tap thread (same pattern as `HotkeyManager`) or passive monitors if the tap cannot be created.

**Bare modifiers** (left/right ⌘⌥⌃⇧) record on release only when held alone; pressing another key while the modifier is down clears the candidate so `⌘E` records as a combo, not a lone `⌘`.

### Defaults and persistence

- **Fresh install default:** `.fn` (Globe / Fn key).
- **Legacy invalid carbon restore:** if stored `hotkeyKind == "carbon"` fails `isValidShortcut`, load falls back to `.fn`.
- **Display:** `hotkeyDisplayString` and keycap tokens via `HotkeyConversion.displayTokens(for:)`.

## Reserved shortcut validation

Only `.carbon` bindings are validated. Fn and bare modifiers always pass.

`HotkeyBinding.isValidShortcut` rejects combos that clash with macOS system shortcuts:

| Rejected combo | Reason |
| --- | --- |
| `⌘` + A, C, F, L, N, O, P, Q, R, S, T, V, W, X, Z | Common app shortcuts |
| `⌘` + Space | Spotlight |
| `⌃` + Space (no other modifiers) | Input source switching |
| `⌘⌥` + Esc | Force Quit |
| `⌘⌃` + Q | Lock Screen |
| `⌘⇧` + 3, 4, 5 | Screenshot shortcuts |
| `⌘` + Tab (no non-⌘ modifiers) | App switcher |

Invalid capture shows: `{keys} is invalid. Please try another.` (toast, `.error` style). Valid capture shows **Shortcut saved**.

<Warning>
Reserved checks are advisory for Carbon combos only. They do not guarantee macOS will register every combo — `RegisterEventHotKey` can still fail at runtime (logged as `InkIt: RegisterEventHotKey failed`).
</Warning>

## Hotkey registration and Accessibility

Hotkeys register only after onboarding completes (`hasCompletedOnboarding`), except during the onboarding **Try it** trial (`beginOnboardingTrial` / `endOnboardingTrial`).

```text
AppCoordinator.refreshHUD()
  ├─ onboarding incomplete → unregisterHotkey(), no HUD
  └─ onboarding complete   → NotchHUDController, registerHotkey()
         hotkey.register(binding: settings.hotkey)
```

### Fn / Globe binding

| Accessibility state | Fn backend | Side effect |
| --- | --- | --- |
| Granted | Suppressing `CGEventTap` | Globe / Emoji / Dictation suppressed |
| Not granted | Passive `NSEvent` monitor | Fn observed; system Globe may still fire |

`permissions.$hasAccessibility` triggers `registerHotkey()` when trust flips on (upgrade tap) or off (downgrade to passive monitor after tap death).

### Dictation without Accessibility

Pressing the hotkey while Accessibility is missing shows notch error **Accessibility needed**, opens System Settings (throttled to once per 10s), and does not start dictation. Microphone and API key are checked first.

<Note>
Accessibility is also required for paste (`PasteService` Cmd+V synthesis) and focus detection. Hotkey registration and paste share the same permission gate in practice.
</Note>

## Feedback sounds

<ParamField body="playFeedbackSounds" type="bool">
Audible cues on hotkey press (dictation start) and release/stop (dictation finalize). Default: `true` when the key has never been set.
</ParamField>

When enabled, `AppCoordinator` calls:

- `FeedbackSoundPlayer.shared.playStart()` in `startDictation()`
- `FeedbackSoundPlayer.shared.playStop()` in `stopDictation()`

Sounds are bundled `cue-start.aiff` and `cue-stop.aiff`, played at volume `0.4` through cached `NSSound` instances so rapid press/release does not clobber playback.

Toggle: **Play sound on press and release** in the **Shortcut** group.

## notchHorizontalPosition

<ParamField body="notchHorizontalPosition" type="double">
Normalized horizontal HUD anchor on the active screen: `0.0` = left edge, `1.0` = right edge. Default `0.38` (slightly left of center). Clamped to `[0.04, 0.96]` on write.
</ParamField>

This key is persisted and published on `SettingsStore`, but **no Settings UI control** exists yet. `NotchHUDController.reposition()` currently centers the HUD from `NotchGeometry.detect(on:)` (physical notch center or screen midpoint on non-notch displays), not from `notchHorizontalPosition`.

To set manually (advanced):

```bash
defaults write $(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$(dirname "$(dirname "$(pgrep -l InkIt | head -1)")")/Info.plist" 2>/dev/null || echo "com.cartesia.InkIt") notchHorizontalPosition -float 0.38
```

Or edit `~/Library/Preferences/<bundle-id>.plist` after locating InkIt's bundle identifier. Restart InkIt after changing UserDefaults until a live-binding UI ships.

<Info>
On Macs without a camera notch, the HUD uses a floating capsule below the menu bar rather than merging with a physical notch. See runtime troubleshooting for display-specific behavior.
</Info>

## Verification checklist

| Signal | Expected when configured correctly |
| --- | --- |
| Settings hotkey field | Keycap tokens match your binding (e.g. `🌐 fn`, `⌃ Ctrl` + `⌥ Opt` + `S`) |
| Home status hint | `{Press\|Hold}` + keycaps + `to dictate` / `to start and stop` |
| Notch HUD on press | Live waveform island while `state == .recording` |
| Feedback sound | Rising cue on press, falling cue on stop (if toggle enabled) |
| Fn + Accessibility | Globe picker does not appear on Fn press |
| Toast on save | **Shortcut saved** after valid capture |
| General → Accessibility | **Enabled** for Fn suppression and paste |

## Troubleshooting

| Symptom | Likely cause | Fix |
| --- | --- | --- |
| Shortcut never triggers | Onboarding incomplete, or hotkey unregistered during Settings edit | Complete onboarding; close hotkey recorder (saves or cancels) |
| `Accessibility needed` on press | `AXIsProcessTrusted()` false | Settings → General → Permissions → Enable Accessibility; relaunch if stale |
| Globe / Emoji opens on Fn | Passive Fn monitor (no AX) | Grant Accessibility; verify re-registration |
| Invalid shortcut toast | Reserved macOS combo | Pick a different modifier+key pair |
| No start/stop sounds | `playFeedbackSounds == false` | Enable **Play sound on press and release** |
| Shortcut works in InkIt only | Duplicate InkIt bundles with split AX grants | Quit duplicate copies; grant AX to one bundle path |
| `RegisterEventHotKey failed` in Console | Combo taken by another app or OS | Choose another shortcut |

## Related pages

<CardGroup>
<Card title="Hotkey bindings" href="/hotkey-bindings">
Carbon, Fn/Globe, and bare-modifier backends, `HotkeyManager` tap threads, and `isValidShortcut` implementation detail.
</Card>
<Card title="Permissions model" href="/permissions-model">
Microphone and Accessibility tri-state, polling, TCC prompts, and silent relaunch after Accessibility grant.
</Card>
<Card title="Dictation state machine" href="/dictation-state-machine">
How hotkey press/release drives `DictationState` transitions through recording, finalizing, and paste.
</Card>
<Card title="Settings reference" href="/settings-reference">
Full `SettingsStore` key table including `hotkeyKind`, `dictationMode`, `playFeedbackSounds`, and `notchHorizontalPosition`.
</Card>
<Card title="Runtime troubleshooting" href="/runtime-troubleshooting">
Duplicate bundle instances, Bluetooth mic delay, debug logging, and notch HUD behavior on non-notch displays.
</Card>
</CardGroup>
