Skip to content

fix(settings): honor empty key-mapping list, survive duplicate exclusion IDs, follow OS login-item state, inert new mapping rows#124

Merged
kazukinakai merged 1 commit into
mainfrom
fix/settings-persistence-and-ui
Jul 5, 2026
Merged

fix(settings): honor empty key-mapping list, survive duplicate exclusion IDs, follow OS login-item state, inert new mapping rows#124
kazukinakai merged 1 commit into
mainfrom
fix/settings-persistence-and-ui

Conversation

@kazukinakai

Copy link
Copy Markdown
Member

Summary

Six fixes in the settings/persistence layer:

  • Deleted mappings resurrectedloadKeyMappings treated a stored empty array like "never written" and fell back to factory defaults, so removing every mapping didn't survive a relaunch. A stored array is now authoritative, including empty (matching loadExclusionApps semantics).
  • Duplicate exclusion-app IDs bricked the appDictionary(uniqueKeysWithValues:) traps on duplicate keys and ran unconditionally in init, so one duplicate entry in persisted data crashed every launch with no recovery path. loadExclusionApps now de-dups by id (first wins) and both dictionary sites use uniquingKeysWith:.
  • Login item re-registered behind the user's backbootstrap() force re-registered whenever the stored toggle was on but the service wasn't enabled, reverting a user who disabled ⌘IME in System Settings → Login Items. It now switches on SMAppService.mainApp.status: .requiresApproval (registered, user disabled/unapproved) follows the OS and syncs the toggle off; .notRegistered/.notFound (never registered — e.g. legacy lunchAtStartup migration) still honors stored intent and registers.
  • "Add" created a live A→A mappingKeyboardShortcut()'s default keyCode 0 is the real "A" key and new rows were enabled, so a half-configured row (input set to Right ⌘, action forgotten) made Right Command type a literal "A". New rows start disabled and auto-enable only once both input and output have been explicitly chosen (new KeyboardShortcut.isUnset; the view has no per-row enable toggle).
  • Stale "Recently active" list — un-excluding an app now calls reloadRecent() so it reappears immediately.
  • Silently shadowed duplicate inputsfindMapping is first-match-wins; a later enabled row with an identical input now shows an inline warning icon with a tooltip instead of silently never firing.

Tests

swift test: 42 executed, 0 failures (1 pre-existing unrelated skip). New coverage: empty-vs-missing key-mapping persistence, duplicate exclusion de-dup, bootstrap in the (only reachable in-process) .notFound state, isUnset/auto-enable transitions, and isShadowed logic in a new ShortcutsSettingsViewTests.swift. The .requiresApproval bootstrap branch has no automated coverage — SMAppService has no injection seam and the test process reports .notFound; noted in a test comment.

🤖 Generated with Claude Code

…ion IDs, follow OS login-item state, inert new mapping rows

- Bug 1: loadKeyMappings(from:) now treats any stored array as authoritative,
  including an empty one, instead of falling back to factory defaults when
  the user removes every mapping.
- Bug 2: loadExclusionApps(from:) de-dups by id (keep first occurrence), and
  both Dictionary(uniqueKeysWithValues:) call sites (the $exclusionApps sink
  and publishGlobalsFromState()) now use uniquingKeysWith so a duplicate id
  in persisted data can never trap.
- Bug 3: bootstrap() now follows the OS state symmetrically, but distinguishes
  *why* the service isn't enabled via SMAppService.mainApp.status instead of
  treating every non-enabled state the same: `.requiresApproval` (registered,
  but the user disabled/hasn't approved it in System Settings → Login Items)
  syncs the stored toggle to "off" (via the isApplyingExternalUpdate
  re-entrancy guard); `.notRegistered` / `.notFound` / any unknown future
  status (never registered at all — e.g. a legacy-migration user whose old
  `lunchAtStartup=true` was carried over but who never went through
  SMAppService registration) honors the stored intent and registers instead,
  restoring the pre-fix behavior for that specific case only.
- Bug 4: addKeyMapping() appends a disabled row (KeyMapping()'s default
  shortcut is keyCode 0 / "A"), and updateKeyMapping(at:) only re-enables it
  once both input and output have been explicitly set through the preset
  menus (added KeyboardShortcut.isUnset), so a half-configured row can never
  act as a live A -> A remap.
- Bug 5: removing an excluded app now calls reloadRecent() so it reappears
  in "Recently active" immediately.
- Bug 6: ShortcutsSettingsView.isShadowed(_:at:) flags a later enabled row
  with an inline warning icon when an earlier enabled row already has the
  identical input shortcut, since findMapping is first-match-wins.

Extends AppSettingsTests.swift and adds ShortcutsSettingsViewTests.swift
covering all six fixes; swift test and swiftlint pass with no new
violations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kazukinakai kazukinakai enabled auto-merge July 5, 2026 06:15
@kazukinakai kazukinakai merged commit 8b43388 into main Jul 5, 2026
2 checks passed
@kazukinakai kazukinakai deleted the fix/settings-persistence-and-ui branch July 5, 2026 06:16
kazukinakai added a commit that referenced this pull request Jul 5, 2026
Patch release shipping the fixes merged in #122 (event-tap rebuild leak,
modifier-chord false fire, media-key stuck keyUp), #124 (settings
persistence and UI fixes), and #123 (release pipeline idempotency).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant