Skip to content

Fix shortcut capture and modifier-only hotkeys#261

Merged
altic-dev merged 5 commits intomainfrom
B/1.5.12-more-fixes
Apr 8, 2026
Merged

Fix shortcut capture and modifier-only hotkeys#261
altic-dev merged 5 commits intomainfrom
B/1.5.12-more-fixes

Conversation

@altic-dev
Copy link
Copy Markdown
Owner

Description

Fixes shortcut capture ownership and modifier-only hotkey handling.

Type of Change

  • 🐞 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update

Related Issues

Testing

  • Tested on Intel Mac
  • Tested on Apple Silicon Mac
  • Tested on macOS 15
  • Ran linter locally: swiftlint --strict --config .swiftlint.yml Sources
  • Ran formatter locally: swiftformat --config .swiftformat Sources
  • Built locally: sh build_incremental.sh

Notes

  • Makes shortcut capture single-owner so reassignment cannot land in the wrong slot.
  • Fixes modifier-only shortcuts for multi-modifier combos and preserves side-specific modifiers.
  • Branch also includes existing commit 0b30897 (changed paste method naming).

Screenshots / Video

  • None

@altic-dev
Copy link
Copy Markdown
Owner Author

possibly fixes #210 and fixed #221

@altic-dev altic-dev marked this pull request as ready for review April 8, 2026 03:49
@altic-dev
Copy link
Copy Markdown
Owner Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70f773952c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +560 to +564
var pressedModifierKeyCodes = self.pressedModifierKeyCodes
if pressedModifierKeyCodes.contains(keyCode) {
pressedModifierKeyCodes.remove(keyCode)
} else {
// Modifier released
let wasCleanPress = !self.otherKeyPressedDuringModifier
self.modifierOnlyKeyDown = false
self.otherKeyPressedDuringModifier = false
self.modifierPressStartTime = nil
pressedModifierKeyCodes.insert(keyCode)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recompute modifier state from flags instead of toggling

Updating pressedModifierKeyCodes by toggling membership on each flagsChanged event can desynchronize the internal state whenever one modifier event is missed (for example during event-tap disable/re-enable or while shortcut capture bypasses handling). Once out of sync, modifier-only shortcuts that rely on exact key-code sets will stop firing correctly because press/release transitions are interpreted backward until restart. Build this set from the current eventModifiers/event state each time (or otherwise resync) rather than flip-flopping cached membership.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Added a follow-up fix in 0281318 to reset cached modifier-only hotkey state at known event-flow boundaries.

This clears stale modifier tracking when shortcut capture is active, when the event tap is disabled/recovered, and when the hotkey manager is reinitialized, so missed flagsChanged history does not leave modifier-only shortcuts stuck in a bad state.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02813185d2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


let reason = (type == .tapDisabledByTimeout) ? "timeout" : "user input"
DebugLogger.shared.warning("Event tap disabled by \(reason) — attempting immediate re-enable", source: "GlobalHotkeyManager")
self.resetModifierOnlyShortcutTracking()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop hold recording before clearing pressed shortcut state

Calling resetModifierOnlyShortcutTracking() on tap-disable clears isKeyPressed/isCommandModeKeyPressed/etc. immediately, but it does not stop an already-running press-and-hold recording. The release handlers only call stopRecordingIfNeeded() when those booleans are still true, so if the tap is disabled while a modifier-only hold shortcut is active, releasing the key after recovery is ignored and recording can stay stuck on until the user manually toggles it off.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 543eecd.

Reset paths now stop an active hold recording before clearing the pressed-state booleans, so tap-disable / reinit / shortcut-capture reset cannot leave a modifier-only hold recording stuck on.

Comment on lines +818 to +822
guard self.modifierOnlyKeyDown || behavior.isModeKeyPressed(),
expectedModifierKeyCodes.contains(keyCode),
!pressedModifierKeyCodes.contains(keyCode)
else {
return false
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel pending hold when extra modifiers are pressed

For multi-modifier shortcuts, when the user presses an additional modifier not in the configured set (for example configured ⌘+⌥, then pressing before completing a chord), this path returns false without marking otherKeyPressedDuringModifier or cancelling pendingHoldModeStart. That leaves the delayed hold task alive, so the mode can still start after 150ms even though the user transitioned into a larger key combo.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 543eecd.

Pending hold-start is now cancelled when the pressed modifier set grows beyond the configured chord, so adding an extra modifier no longer allows a stale delayed hold-start to fire.

Copy link
Copy Markdown
Owner Author

Pushed follow-up hardening in 543eecd.

This removes the remaining fragile path in modifier-only tracking by rebuilding side-specific modifier state from macOS live key state on modifier changes instead of relying on cached toggle history. It also keeps the earlier fixes to stop active hold recording before reset and to cancel pending hold-start when extra modifiers join the chord.

Verified with:

  • swiftformat --config .swiftformat Sources
  • swiftlint --strict --config .swiftlint.yml Sources/
  • sh build_incremental.sh

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 543eecd665

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +898 to +900
guard self.modifierOnlyKeyDown || behavior.isModeKeyPressed(),
expectedModifierKeyCodes.contains(keyCode),
!pressedModifierKeyCodes.contains(keyCode)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope modifier-release handling to the active shortcut

modifierOnlyKeyDown is global across all modifier-only shortcuts, so this release guard can fire for a different shortcut than the one that actually started tracking. With overlapping shortcuts (e.g., dictation = , prompt = ⌘+⌥), pressing and releasing can enter the prompt release path first, consume the event, and skip dictation’s release cleanup/toggle path, leaving mode state inconsistent and triggering the wrong action. The release path should also verify that this behavior is the active one (for example via pendingHoldModeType/per-behavior down state) before acting.

Useful? React with 👍 / 👎.

@altic-dev altic-dev merged commit d0abfe0 into main Apr 8, 2026
2 checks passed
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.

[🐞 BUG] Multi-modifier hotkey combos (e.g. fn + ctrl) can't be recorded or triggered

1 participant