also looking at this
feat(prefs): a Settings… item and a preference store that keeps its types
#58
14 files
+2289
-10
| @@ -9,6 +9,7 @@ The Craft JavaScript Bridge provides a seamless interface for your web applicati | ||
| 9 | 9 | - [System Tray API](#system-tray-api) |
| 10 | 10 | - [Application Menu (macOS)](#application-menu-macos) |
| 11 | 11 | - [Global Shortcuts (macOS)](#global-shortcuts-macos) |
| 12 | - [Settings and Preferences (macOS)](#settings-and-preferences-macos) | |
| 12 | 13 | - [Window API](#window-api) |
| 13 | 14 | - [App API](#app-api) |
| 14 | 15 | - [TypeScript Support](#typescript-support) |
| @@ -367,6 +368,102 @@ unsubscribe function. | ||
| 367 | 368 | | `isRegistered(id): Promise<boolean>` | whether the id is known, enabled or not | |
| 368 | 369 | | `list(): Promise<Shortcut[]>` | `{ id, accelerator, key, enabled }` for each | |
| 369 | 370 | |
| 371 | ## Settings and Preferences (macOS) | |
| 372 | ||
| 373 | ### The Settings… item | |
| 374 | ||
| 375 | craft's default App menu ships a **Settings… ⌘,** where every Mac app has it, | |
| 376 | between About and Hide. Choosing it — or pressing ⌘, — fires a `craft:settings:open` | |
| 377 | event into the page. An app only has to listen: | |
| 378 | ||
| 379 | ```javascript | |
| 380 | window.craft.settings.onOpen(({ source }) => { | |
| 381 | showSettingsPanel() | |
| 382 | }) | |
| 383 | ``` | |
| 384 | ||
| 385 | `source` is `'menu'` for the menu item. Your own UI can reach the same handler | |
| 386 | rather than duplicating it: | |
| 387 | ||
| 388 | ```javascript | |
| 389 | gearButton.onclick = () => window.craft.settings.open('toolbar') | |
| 390 | ``` | |
| 391 | ||
| 392 | That call is page-local; nothing crosses the bridge for it. | |
| 393 | ||
| 394 | It is **not** delivered as `craft:menu:action` with a reserved id, deliberately. | |
| 395 | An app that never calls `craft.menu.set()` should not have to subscribe to the | |
| 396 | menu API to catch ⌘,, and a "reserved" menu id would be indistinguishable from | |
| 397 | an app-declared item that happened to pick the same string. | |
| 398 | ||
| 399 | Calling `craft.menu.set()` replaces the whole bar, so the default item goes with | |
| 400 | it — as documented above. Declare it yourself to keep it: | |
| 401 | ||
| 402 | ```javascript | |
| 403 | { id: 'settings', role: 'settings', label: 'Settings…', shortcut: 'cmd+,' } | |
| 404 | ``` | |
| 405 | ||
| 406 | `role: 'settings'` (or `'preferences'`) resolves to craft's own handler, so the | |
| 407 | same `craft:settings:open` event arrives either way. The `id` is required — | |
| 408 | items without one are skipped. | |
| 409 | ||
| 410 | ### `window.craft.prefs` | |
| 411 | ||
| 412 | A small preference store over the platform's own mechanism, so `defaults read` | |
| 413 | and a native settings pane both see what the app wrote. | |
| 414 | ||
| 415 | ```javascript | |
| 416 | await window.craft.prefs.set('theme', 'dark') | |
| 417 | await window.craft.prefs.set('fontSize', 13) | |
| 418 | await window.craft.prefs.set('firstRunDone', true) | |
| 419 | ||
| 420 | const theme = await window.craft.prefs.get('theme', 'light') | |
| 421 | ``` | |
| 422 | ||
| 423 | | Call | What it does | | |
| 424 | |---|---| | |
| 425 | | `set(key, value)` | Write. Resolves once the value is **on disk**, not merely once the message was posted | | |
| 426 | | `get(key, fallback?)` | Read. `fallback` is returned for an absent key | | |
| 427 | | `delete(key)` | Remove. Resolves to whether it was there | | |
| 428 | | `clear()` | Remove every preference this app set. Resolves to how many | | |
| 429 | | `keys()` | Every key this app set, sorted | | |
| 430 | | `info()` | `{ domain, prefix, count, readCommand }` — see below | | |
| 431 | ||
| 432 | **Strings, numbers and booleans only.** Not a corner cut: the preferences API | |
| 433 | raises an Objective-C exception for a value that is not a property-list type, | |
| 434 | and craft cannot catch one, so refusing containers in the page is what makes | |
| 435 | that crash unreachable rather than merely unlikely. Serialise structure | |
| 436 | yourself: | |
| 437 | ||
| 438 | ```javascript | |
| 439 | await craft.prefs.set('recents', JSON.stringify(list)) | |
| 440 | const list = JSON.parse(await craft.prefs.get('recents', '[]')) | |
| 441 | ``` | |
| 442 | ||
| 443 | Refusals reject with a `code` you can branch on: `PREFS_UNSUPPORTED_VALUE`, | |
| 444 | `PREFS_NON_FINITE`, `PREFS_VALUE_TOO_LARGE` (8 KiB), `PREFS_BAD_KEY` (keys must | |
| 445 | match `/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/`), and `PREFS_FOREIGN_VALUE` when | |
| 446 | something outside craft wrote a value craft cannot represent. | |
| 447 | ||
| 448 | **Types survive the round trip.** A stored `true` reads back as `true`, not as | |
| 449 | `1`; an integer does not come back as a float. Passing a `fallback` also | |
| 450 | declares the expected type, so a value left behind by an older build of the app | |
| 451 | degrades to the default instead of to a surprise. | |
| 452 | ||
| 453 | **Where it goes.** Preferences land in the app's own domain — the bundle | |
| 454 | identifier inside a packaged `.app`, the executable's name otherwise. That | |
| 455 | means preferences set while developing against the shared `craft` binary are | |
| 456 | in a different domain from the packaged app's, and every dev-mode app shares | |
| 457 | one. `info()` reports which domain is live and how to read it: | |
| 458 | ||
| 459 | ```javascript | |
| 460 | const { domain, readCommand } = await craft.prefs.info() | |
| 461 | // readCommand: "defaults read com.example.myapp" | |
| 462 | ``` | |
| 463 | ||
| 464 | craft namespaces its keys, so `clear()` and `keys()` cannot disturb the window | |
| 465 | positions and inspector state AppKit and WebKit keep in the same domain. | |
| 466 | ||
| 370 | 467 | ## Window API |
| 371 | 468 | |
| 372 | 469 | Control the application window from JavaScript. |
| @@ -601,6 +698,8 @@ async function onComplete() { | ||
| 601 | 698 | | Window Control | ✅ | ✅ | ✅ | |
| 602 | 699 | | Hide Dock Icon | ✅ | ➖ | ➖ | |
| 603 | 700 | | Global Shortcuts | ✅ | �� | �� | |
| 701 | | Settings… item | ✅ | �� | �� | | |
| 702 | | Preferences | ✅ | �� | �� | | |
| 604 | 703 | |
| 605 | 704 | ✅ Implemented | �� In Progress | ➖ Not Applicable |
| 606 | 705 | |