Skip to content

fix(hotkey): make Change replace and Remove unbind - #707

Open
dschnurbusch wants to merge 1 commit into
altic-dev:mainfrom
dschnurbusch:fix/primary-hotkey-reassignment
Open

fix(hotkey): make Change replace and Remove unbind#707
dschnurbusch wants to merge 1 commit into
altic-dev:mainfrom
dschnurbusch:fix/primary-hotkey-reassignment

Conversation

@dschnurbusch

@dschnurbusch dschnurbusch commented Jul 24, 2026

Copy link
Copy Markdown

Description

Changing or removing a Primary Dictation shortcut currently mutates SwiftUI state and relies on an onChange side effect to persist the list and refresh GlobalHotkeyManager. The shortcut-capture callback can bypass that refresh, leaving the manager's in-memory list stale: Change behaves like Add, and removed shortcuts continue firing until restart.

This PR routes Change and Remove through one persist-and-live-update method. It also permits removing the final primary shortcut, preserving an explicit unbound state instead of silently restoring the legacy shortcut. Fresh installs still receive the existing default when no primary-shortcut preference has ever been stored.

Type of Change

  • 🐞 Bug fix
  • ✨ New feature
  • 💥 Breaking change
  • 🧹 Chore
  • 📝 Documentation update

Related Issue or Discussion

Related to #470 and #498.

#474 contains the direct live-manager refresh for Change, but does not address Remove or an explicitly unbound primary shortcut. #503 covers a narrower active modifier-only press-state edge case; this PR addresses settings-to-live-manager synchronization and removal semantics observed on 1.6.5.

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS version:
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources
  • Ran formatter locally: swiftformat --config .swiftformat Sources
  • Ran tests locally:
  • Parsed all changed Swift files with xcrun swiftc -parse
  • Ran git diff --check
  • Fork CI passed SwiftLint, Xcode build, and tests: https://github.com/dschnurbusch/FluidVoice/actions/runs/30101354571

The local machine has Command Line Tools rather than full Xcode, so the full Xcode build/test suite was run on the same commit (83568b8) through the fork's macOS workflow.

Screenshots / Video

Before: FluidVoice 1.6.5 shows Remove disabled when only one Primary Dictation shortcut remains. The submitted change enables removal of that final shortcut; once removed, the row disappears and Add shortcut remains available.

FluidVoice 1.6.5 Global Hotkey settings with disabled Remove button

  • No UI/visual changes; screenshots/video are not applicable.

Notes

The settings mutation now updates persisted state, normalized SwiftUI state, and the live hotkey manager in one call. The existing onChange remains as an idempotent fallback for other state writes.

The explicit empty list is distinguishable from a fresh install: missing preference data still falls back to the legacy/default shortcut, while stored [] remains unbound. The legacy single-shortcut value is left intact for backward-compatible backup/export consumers but is no longer allowed to resurrect an explicitly cleared primary list.

@github-actions github-actions Bot added needs PR template Pull request is missing required template content. needs screenshots Pull request needs screenshot or video evidence. labels Jul 24, 2026
@github-actions

Copy link
Copy Markdown

The PR Policy check is blocking this PR because required template information is missing.

Please update the PR description with:

  • Description
  • Type of Change
  • Related Issue or Discussion
  • Testing
  • Screenshots / Video

Visual files detected:

  • Sources/Fluid/ContentView.swift
  • Sources/Fluid/Persistence/SettingsStore.swift
  • Sources/Fluid/UI/SettingsView.swift

Screenshots or video are required for UI, UX, settings, onboarding, overlay, menu bar, or visual behavior changes. If this PR has no visual changes, check the no-visual-change box in the template.

If this remains incomplete for 48 hours after opening, the PR may be closed.

@greptile-apps

greptile-apps Bot commented Jul 24, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes primary dictation shortcut changes immediately update persistence and the live hotkey manager.

  • Routes shortcut replacement and Settings binding writes through one update method.
  • Allows removing the final primary shortcut and preserves an explicitly persisted empty list.
  • Keeps the legacy fallback only when no primary-shortcut preference exists.
  • Adds regression coverage for the explicitly unbound state.

Confidence Score: 5/5

The PR appears safe to merge with no actionable defects identified in the changed shortcut update and persistence paths.

Shortcut changes are normalized before updating both SwiftUI state and the live manager, while empty arrays are safely handled by persistence, backup restoration, UI consumers, and hotkey matching.

Reviews (1): Last reviewed commit: "fix(hotkey): make shortcut changes repla..." | Re-trigger Greptile

@github-actions github-actions Bot removed needs PR template Pull request is missing required template content. needs screenshots Pull request needs screenshot or video evidence. labels Jul 24, 2026
@dschnurbusch

Copy link
Copy Markdown
Author

Full fork CI passed on the same head commit (83568b8): SwiftLint, Xcode build, and tests all green. Run: https://github.com/dschnurbusch/FluidVoice/actions/runs/30101354571

@altic-dev

Copy link
Copy Markdown
Owner

Thanks for the PR!

Does this affect only the primary key or can we translate this to other keys and hotkey related areas?

@altic-dev altic-dev self-assigned this Jul 28, 2026
@altic-dev altic-dev added the awaiting response Older issue will close after three days unless this label is removed. label Jul 28, 2026
@dschnurbusch

Copy link
Copy Markdown
Author

The concrete bug and code path in this PR are specific to Primary Dictation. Primary is the outlier because it is a list cached by GlobalHotkeyManager, and Settings UI mutations were relying on SwiftUI onChange to persist the list and refresh that cache. This PR gives Add/Change/Remove one explicit path that updates the store, normalized view state, and live manager together; the persistence change also lets an empty primary list mean intentionally unbound.

The same persist-and-live-update principle applies to the other hotkeys, but most already do the equivalent:

  • Secondary Dictation, Command Mode, and Edit Mode explicitly persist replacements and call their corresponding manager update methods. Their enabled toggles also update the manager.
  • Command Mode already supports Remove by setting the shortcut to nil and disabling it. Paste Last also supports Remove, and both Paste Last and Cancel are read directly from SettingsStore while handling events rather than from a stale manager cache.
  • Prompt-specific shortcuts rebuild the manager assignments explicitly after a change.

So the pattern is reusable, but I would not directly broaden this PR: Remove/unbind for Secondary, Edit, or Cancel would be a separate UX/defaults decision, not the same stale-cache bug. A later cleanup could centralize each shortcut mutation behind a persist + runtime-sync API so the UI cannot accidentally update only one layer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting response Older issue will close after three days unless this label is removed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants