- name
- add-preference
- description
- Contract for adding or changing a persisted setting in BetterCmdTab. Use when touching Preferences.swift, a settings pane, or any Switcher.* UserDefaults key.
# Add a preference
A setting is a multi-file contract keyed on one string. Follow every step that
applies; skipping one produces a setting that silently fails to persist,
export, or apply on the hot path.
## 1. Declare the key and property
In `BetterCmdTab/App/Preferences.swift`:
- Add the key to the `Keys` enum as `"Switcher.camelCaseName"`. The string is
the contract — it is read by name in other files, exported to config files,
and never renamed after shipping.
- Add a `@Published` property whose `didSet` persists to `UserDefaults`, and
load it in `init` and in `reloadFromDefaults()` (settings import and
config-file sync call this — a property missing there ignores imports).
Copy the exact pattern of an adjacent property of the same type.
- Clamped ints get a `static let …Range: ClosedRange<Int>` next to the other
range constants, and clamp on read.
**Complete when:** the property round-trips through `reloadFromDefaults()`.
## 2. Wire the hot path, if any
If the value is read while the switcher opens or cycles (`CatalogFilter`,
`SwitcherController`, `WindowEnumerator`, `Activator`, `HotkeyTap`,
`RecentlyClosedStore`), those consumers read the key string directly from
`UserDefaults.standard` off the main actor — never touch
`Preferences.shared` there. Match the existing direct-read sites in the file
you are editing.
**Complete when:** no hot-path code awaits the main actor to read the value.
## 3. Per-shortcut override, if applicable
If the setting should be overridable per shortcut (#74), extend
`ShortcutOverride` in `Preferences.swift`: add the optional field, its
`dictionary` encoding, the `init?(dictionary:)` decode, and the entry in
`knownKeys` (an omitted `knownKeys` entry makes the field bounce into
`passthrough` and never apply). Then resolve it where the other overrides
resolve into `CatalogFilter.Config` / reveal-time reads.
**Complete when:** the field survives a `dictionary` → `init?(dictionary:)`
round trip and is absent from `passthrough`.
## 4. Portability and config file — usually free
Export/import (`SettingsPortability.swift`) and
`~/.config/bettercmdtab/config.json` (`ConfigFile.swift`) pick up any
`Switcher.*` key automatically. Only act if the value is device-local state
(caches, recently-closed, machine-specific paths): add it to
`exportExcludedKeys` in `SettingsPortability.swift`.
## 5. UI and strings
Add the control to the matching pane view controller in
`BetterCmdTab/Settings/` — Switcher/Controls/Tabs all live in
`SwitcherPanesViewController`, picked by its `Pane` parameter. Fragile or new
behavior ships off by default under a “These features are unstable” notice on
its own section. Register the control in `SettingsCatalog.swift` if the pane
search should find it. All labels use
`String(localized:)` — then follow the `localize-strings` skill for the
catalog entries.
## 6. Config reference docs
`docs/src/data/config-schema.json` is generated; never edit it by hand. After
launching the new build once, copy it from the app (English system, see
`docs/CONTRIBUTING.md`):
```bash
cp ~/.config/bettercmdtab/schema.json docs/src/data/config-schema.json
```
Then add the key to `sections` and its `reloadFromDefaults()` value to
`defaults` in `docs/src/lib/config-reference.ts`.
**Complete when:** `ConfigSchemaDocsTests` passes and `cd docs && bun run build`
passes; the build fails and names any key missing from either map.
## 7. Verify
New pure-logic behavior gets a Swift Testing test. Then run the suites the
contract touches:
```bash
xcodebuild -scheme "BetterCmdTab Debug" -destination 'platform=macOS' test \
-only-testing:BetterCmdTabTests/PreferencesEnumTests \
-only-testing:BetterCmdTabTests/SettingsPortabilityTests \
-only-testing:BetterCmdTabTests/LocalizationCatalogTests
```
**Complete when:** those suites pass and the setting survives an
export → import round trip.
Ver no GitHub