also looking at this
fix(shortcuts): make global hotkeys actually fire
#54
11 files
+2435
-417
| @@ -8,6 +8,7 @@ The Craft JavaScript Bridge provides a seamless interface for your web applicati | ||
| 8 | 8 | - [Getting Started](#getting-started) |
| 9 | 9 | - [System Tray API](#system-tray-api) |
| 10 | 10 | - [Application Menu (macOS)](#application-menu-macos) |
| 11 | - [Global Shortcuts (macOS)](#global-shortcuts-macos) | |
| 11 | 12 | - [Window API](#window-api) |
| 12 | 13 | - [App API](#app-api) |
| 13 | 14 | - [TypeScript Support](#typescript-support) |
| @@ -286,6 +287,86 @@ a newer bridge surface therefore fails soft on an older binary. | ||
| 286 | 287 | | `checkItem(itemId)` / `uncheckItem(itemId)` | checkmark | |
| 287 | 288 | | `setItemLabel(itemId, label)` | rename in place | |
| 288 | 289 | |
| 290 | ## Global Shortcuts (macOS) | |
| 291 | ||
| 292 | System-wide hotkeys: they fire whether or not your app is frontmost. macOS only | |
| 293 | — on Linux and Windows every registration is refused, because craft has no | |
| 294 | implementation there and a shortcut that can never fire is worse than one that | |
| 295 | was never accepted. | |
| 296 | ||
| 297 | Craft registers these through Carbon's `RegisterEventHotKey`, which needs no | |
| 298 | Accessibility or Input Monitoring permission and only ever calls back for the | |
| 299 | exact combinations you asked for. Your app never sees any other keystroke. | |
| 300 | ||
| 301 | ### `window.craft.shortcuts.register(id, accelerator): Promise<void>` | |
| 302 | ||
| 303 | Reserve a combination. `id` is yours to choose and is what comes back when the | |
| 304 | key is pressed. Registering an id twice replaces the first binding. | |
| 305 | ||
| 306 | ```javascript | |
| 307 | await window.craft.shortcuts.register('summon', 'Cmd+Shift+H'); | |
| 308 | ||
| 309 | window.craft.shortcuts.on(({ id, accelerator }) => { | |
| 310 | if (id === 'summon') window.craft.window.toggle(); | |
| 311 | }); | |
| 312 | ``` | |
| 313 | ||
| 314 | Accelerators are `+`-separated and case-insensitive. The last component is the | |
| 315 | key; everything before it is a modifier: | |
| 316 | ||
| 317 | | Modifier | Accepted as | | |
| 318 | |---|---| | |
| 319 | | Command | `Cmd`, `Command`, `Meta`, `Super`, `⌘` | | |
| 320 | | Control | `Ctrl`, `Control`, `⌃` | | |
| 321 | | Option | `Alt`, `Option`, `Opt`, `⌥` | | |
| 322 | | Shift | `Shift`, `⇧` | | |
| 323 | | Either | `CmdOrCtrl`, `CommandOrControl`, `Mod` — command on macOS, control elsewhere | | |
| 324 | ||
| 325 | Keys are named by position on the keyboard, not by the character they produce, | |
| 326 | so a shortcut survives the user switching layout. Letters, digits, punctuation, | |
| 327 | `Space`, `Tab`, `Return`, `Escape`, `Delete`, the arrows, `Home`/`End`/`PageUp`/ | |
| 328 | `PageDown`, `F1`–`F20` and the keypad (`Keypad0`–`Keypad9`, `KeypadEnter`, …) | |
| 329 | are all bindable. Web and Electron spellings — `Backspace`, `Esc`, `Enter`, | |
| 330 | `ArrowLeft`, `Plus` — resolve to the same keys. | |
| 331 | ||
| 332 | **At least one of Command, Control or Option is required**, except on the | |
| 333 | function keys. A bare global hotkey on `H` would mean no application on the | |
| 334 | system — including yours — ever saw the user type an h again; failing the | |
| 335 | registration is recoverable, and that is not. Shift alone does not count, since | |
| 336 | Shift+H is how you type a capital H. | |
| 337 | ||
| 338 | ### `window.craft.shortcuts.onError(handler): () => void` | |
| 339 | ||
| 340 | Where a failed call arrives: `{ id, code, message }`. Every verb here except | |
| 341 | `isRegistered` and `list` is fire-and-forget — the promise resolves as soon as | |
| 342 | the message is posted, not when the key is reserved — so this is how you learn | |
| 343 | that a combination belongs to another app or to the system, or that `enable()` | |
| 344 | could not take a released key back. | |
| 345 | ||
| 346 | ```javascript | |
| 347 | window.craft.shortcuts.onError(({ id, message }) => { | |
| 348 | showToast(`Could not bind ${id}: ${message}`); | |
| 349 | }); | |
| 350 | ``` | |
| 351 | ||
| 352 | ### `window.craft.shortcuts.on(handler): () => void` | |
| 353 | ||
| 354 | Fires on every press of a registered, enabled shortcut, with `{ id, accelerator }`. | |
| 355 | The accelerator is craft's canonical spelling — `Cmd+Delete`, whatever you | |
| 356 | originally wrote — so it round-trips back through `register()`. Returns an | |
| 357 | unsubscribe function. | |
| 358 | ||
| 359 | ### The rest | |
| 360 | ||
| 361 | | Call | What it does | | |
| 362 | |---|---| | |
| 363 | | `unregister(id)` | give the key back to the system and forget the shortcut | | |
| 364 | | `unregisterAll()` | the same, for every shortcut this app holds | | |
| 365 | | `disable(id)` | stop firing **and release the key**, so other apps can use it again; the shortcut stays listed | | |
| 366 | | `enable(id)` | take it back — this can fail if something else claimed it in the meantime, and reports on `onError` | | |
| 367 | | `isRegistered(id): Promise<boolean>` | whether the id is known, enabled or not | | |
| 368 | | `list(): Promise<Shortcut[]>` | `{ id, accelerator, key, enabled }` for each | | |
| 369 | ||
| 289 | 370 | ## Window API |
| 290 | 371 | |
| 291 | 372 | Control the application window from JavaScript. |
| @@ -519,6 +600,7 @@ async function onComplete() { | ||
| 519 | 600 | | Tray Menu | �� | �� | �� | |
| 520 | 601 | | Window Control | ✅ | ✅ | ✅ | |
| 521 | 602 | | Hide Dock Icon | ✅ | ➖ | ➖ | |
| 603 | | Global Shortcuts | ✅ | �� | �� | | |
| 522 | 604 | |
| 523 | 605 | ✅ Implemented | �� In Progress | ➖ Not Applicable |
| 524 | 606 | |