# Add Preference

> Contract for adding or changing a persisted setting in BetterCmdTab. Use when touching Preferences.swift, a settings pane, or any Switcher.* UserDefaults key.

- Skill: `rokartur/add-preference` (Agent Skill)
- Install (CLI): `npx skillmds@latest add rokartur/add-preference`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rokartur/add-preference/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: rokartur (https://skillmd.com/u/rokartur)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/rokartur/add-preference

---


# 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. 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.

