From 7ea11f28586814acfea3b91eec991dd9cd71395e Mon Sep 17 00:00:00 2001 From: Mansour Alshekhi <25720890+Alshekhi@users.noreply.github.com> Date: Sat, 11 Jul 2026 21:21:58 +0300 Subject: [PATCH 1/3] fix: hide overlays from VoiceOver and pass through system hotkeys - GlobalHotkeyManager: fast-path pass-through for Cmd+Tab/Space/` in the event tap so VoiceOver isn't delayed on the dictation hotkey. - BottomOverlayView: setAccessibilityElement(false)/role .unknown on all overlay panels and .accessibilityHidden(true) on the body so VoiceOver ignores the overlays. Co-Authored-By: Claude Opus 4.8 --- Sources/Fluid/Services/GlobalHotkeyManager.swift | 13 +++++++++++++ Sources/Fluid/Views/BottomOverlayView.swift | 9 +++++++++ 2 files changed, 22 insertions(+) diff --git a/Sources/Fluid/Services/GlobalHotkeyManager.swift b/Sources/Fluid/Services/GlobalHotkeyManager.swift index e974e41f..34fc00aa 100644 --- a/Sources/Fluid/Services/GlobalHotkeyManager.swift +++ b/Sources/Fluid/Services/GlobalHotkeyManager.swift @@ -592,6 +592,19 @@ final class GlobalHotkeyManager: NSObject { return tapRecoveryResult } + // Fast path: pass through system shortcuts (Cmd+Tab, Cmd+Shift+Tab, Cmd+Space, etc.) + // to avoid adding latency to app switching, especially with VoiceOver. + if type == .keyDown || type == .keyUp { + let flags = event.flags + if flags.contains(.maskCommand) { + let kc = UInt16(event.getIntegerValueField(.keyboardEventKeycode)) + // Tab(48), Space(49), Backtick(50 — Cmd+` for window cycling) + if kc == 48 || kc == 49 || kc == 50 { + return Unmanaged.passUnretained(event) + } + } + } + if self.isShortcutCaptureActiveProvider?() ?? false { self.resetModifierOnlyShortcutTracking() return Unmanaged.passUnretained(event) diff --git a/Sources/Fluid/Views/BottomOverlayView.swift b/Sources/Fluid/Views/BottomOverlayView.swift index 47a29e89..a71a6192 100644 --- a/Sources/Fluid/Views/BottomOverlayView.swift +++ b/Sources/Fluid/Views/BottomOverlayView.swift @@ -339,6 +339,8 @@ final class BottomOverlayWindowController { panel.isMovableByWindowBackground = false panel.hidesOnDeactivate = false panel.animationBehavior = .none + panel.setAccessibilityElement(false) + panel.setAccessibilityRole(.unknown) let contentView = BottomOverlayView() let hostingView = BottomOverlayHostingView(rootView: contentView) @@ -611,6 +613,8 @@ final class BottomOverlayPromptMenuController { panel.isMovableByWindowBackground = false panel.hidesOnDeactivate = false panel.animationBehavior = .none + panel.setAccessibilityElement(false) + panel.setAccessibilityRole(.unknown) let contentView = BottomOverlayPromptMenuView( promptMode: self.resolvedPromptMode(), @@ -888,6 +892,8 @@ final class BottomOverlayModeMenuController { panel.isMovableByWindowBackground = false panel.hidesOnDeactivate = false panel.animationBehavior = .none + panel.setAccessibilityElement(false) + panel.setAccessibilityRole(.unknown) let contentView = BottomOverlayModeMenuView( maxWidth: self.menuMaxWidth, @@ -1152,6 +1158,8 @@ final class BottomOverlayActionsMenuController { panel.isMovableByWindowBackground = false panel.hidesOnDeactivate = false panel.animationBehavior = .none + panel.setAccessibilityElement(false) + panel.setAccessibilityRole(.unknown) let contentView = BottomOverlayActionsMenuView( maxWidth: self.menuMaxWidth, @@ -3088,6 +3096,7 @@ struct BottomOverlayView: View { // NotchOverlayManager.shared.onNotchClicked?() // } // } + .accessibilityHidden(true) } } From 66b909a60c965bc69af46d5b78c2a24ad601ef67 Mon Sep 17 00:00:00 2001 From: Mansour Alshekhi <25720890+Alshekhi@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:33:33 +0300 Subject: [PATCH 2/3] fix: scope hotkey fast-path to avoid swallowing shortcuts (review) Addresses automated review feedback on the system-shortcut fast-path: - Skip the fast-path when the chord matches a user-configured Fluid shortcut (cancel, paste-last, prompt, command, rewrite, primary), so real bindings on Cmd+Tab/Space are never silently swallowed. - Call markOtherInputDuringModifierOnly() before passing the event through, so a system chord pressed during a modifier-only shortcut isn't mistaken for a clean tap on release. - Drop keycode 50 (Cmd+`): it is ANSI-only and maps to a different physical key on ISO layouts. Keep only Tab(48)/Space(49), which are layout-stable. - Extract flag->modifier mapping into modifierFlags(from:) so the tap and matcher share one source of truth. Co-Authored-By: Claude Opus 4.8 --- .../Fluid/Services/GlobalHotkeyManager.swift | 75 ++++++++++++++++--- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/Sources/Fluid/Services/GlobalHotkeyManager.swift b/Sources/Fluid/Services/GlobalHotkeyManager.swift index 34fc00aa..59293d1b 100644 --- a/Sources/Fluid/Services/GlobalHotkeyManager.swift +++ b/Sources/Fluid/Services/GlobalHotkeyManager.swift @@ -528,6 +528,51 @@ final class GlobalHotkeyManager: NSObject { self.otherKeyPressedDuringModifier = true } + /// Maps a `CGEventFlags` bitmask to the `NSEvent.ModifierFlags` used for shortcut + /// matching. Single source of truth for the flag translation. + private static func modifierFlags(from flags: CGEventFlags) -> NSEvent.ModifierFlags { + var modifiers: NSEvent.ModifierFlags = [] + if flags.contains(.maskSecondaryFn) { modifiers.insert(.function) } + if flags.contains(.maskCommand) { modifiers.insert(.command) } + if flags.contains(.maskAlternate) { modifiers.insert(.option) } + if flags.contains(.maskControl) { modifiers.insert(.control) } + if flags.contains(.maskShift) { modifiers.insert(.shift) } + return modifiers + } + + /// Whether a key event matches any shortcut the user has configured. The + /// system-shortcut fast-path consults this so it never silently swallows a real + /// binding that happens to use the same chord (e.g. Cmd+Space bound to a Fluid action). + private func eventMatchesAnyConfiguredShortcut(keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> Bool { + if SettingsStore.shared.cancelRecordingHotkeyShortcut.matches(keyCode: keyCode, modifiers: modifiers) { + return true + } + if SettingsStore.shared.pasteLastTranscriptionShortcutEnabled, + let paste = SettingsStore.shared.pasteLastTranscriptionHotkeyShortcut, + paste.matches(keyCode: keyCode, modifiers: modifiers) + { + return true + } + if self.promptShortcutAssignments.contains(where: { $0.shortcut.matches(keyCode: keyCode, modifiers: modifiers) }) { + return true + } + if self.commandModeShortcutEnabled, + let commandModeShortcut = self.commandModeShortcut, + commandModeShortcut.matches(keyCode: keyCode, modifiers: modifiers) + { + return true + } + if self.rewriteModeShortcutEnabled, + self.rewriteModeShortcut.matches(keyCode: keyCode, modifiers: modifiers) + { + return true + } + if self.primaryShortcuts.contains(where: { $0.matches(keyCode: keyCode, modifiers: modifiers) }) { + return true + } + return false + } + private func mouseButton(from event: CGEvent) -> Int { Int(event.getIntegerValueField(.mouseEventButtonNumber)) } @@ -592,15 +637,28 @@ final class GlobalHotkeyManager: NSObject { return tapRecoveryResult } - // Fast path: pass through system shortcuts (Cmd+Tab, Cmd+Shift+Tab, Cmd+Space, etc.) - // to avoid adding latency to app switching, especially with VoiceOver. + // Fast path: pass the macOS system shortcuts Cmd+Tab (app switcher) and + // Cmd+Space (Spotlight / input-source) straight through with minimal work, to + // avoid adding latency to app/space switching — especially with VoiceOver, whose + // event handling is latency-sensitive. Skipped when the user has bound one of + // Fluid's own shortcuts to the same chord, so real bindings are never swallowed. + // + // Only Tab(48) and Space(49) are handled: both are layout-stable hardware + // keycodes. Cmd+` is deliberately excluded because its keycode differs across + // ANSI/ISO keyboard layouts, so a hardcoded value would match the wrong key. if type == .keyDown || type == .keyUp { let flags = event.flags if flags.contains(.maskCommand) { let kc = UInt16(event.getIntegerValueField(.keyboardEventKeycode)) - // Tab(48), Space(49), Backtick(50 — Cmd+` for window cycling) - if kc == 48 || kc == 49 || kc == 50 { - return Unmanaged.passUnretained(event) + if kc == 48 || kc == 49 { + let modifiers = Self.modifierFlags(from: flags) + if !self.eventMatchesAnyConfiguredShortcut(keyCode: kc, modifiers: modifiers) { + // Preserve modifier-only bookkeeping: a system chord pressed while + // a modifier-only Fluid shortcut is held must count as "other input" + // so the later modifier release isn't mistaken for a clean tap. + if type == .keyDown { self.markOtherInputDuringModifierOnly() } + return Unmanaged.passUnretained(event) + } } } } @@ -613,12 +671,7 @@ final class GlobalHotkeyManager: NSObject { let keyCode = UInt16(event.getIntegerValueField(.keyboardEventKeycode)) let flags = event.flags - var eventModifiers: NSEvent.ModifierFlags = [] - if flags.contains(.maskSecondaryFn) { eventModifiers.insert(.function) } - if flags.contains(.maskCommand) { eventModifiers.insert(.command) } - if flags.contains(.maskAlternate) { eventModifiers.insert(.option) } - if flags.contains(.maskControl) { eventModifiers.insert(.control) } - if flags.contains(.maskShift) { eventModifiers.insert(.shift) } + let eventModifiers = Self.modifierFlags(from: flags) switch type { case .keyDown: From 198b6ba38b33712e2463f36cb59607c2001ddf94 Mon Sep 17 00:00:00 2001 From: Mansour Alshekhi <25720890+Alshekhi@users.noreply.github.com> Date: Sat, 11 Jul 2026 22:39:58 +0300 Subject: [PATCH 3/3] fix: include prompt-mode shortcut in fast-path guard (review) Codex review noted eventMatchesAnyConfiguredShortcut checked prompt assignments and command mode but skipped the prompt-mode shortcut (promptModeShortcut / handlePromptModeKeyDown). A prompt-mode binding on Cmd+Tab/Space would still be swallowed. Add it so the guard covers every configured shortcut the keyDown path matches. Co-Authored-By: Claude Opus 4.8 --- Sources/Fluid/Services/GlobalHotkeyManager.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Sources/Fluid/Services/GlobalHotkeyManager.swift b/Sources/Fluid/Services/GlobalHotkeyManager.swift index 59293d1b..5b4c0f9a 100644 --- a/Sources/Fluid/Services/GlobalHotkeyManager.swift +++ b/Sources/Fluid/Services/GlobalHotkeyManager.swift @@ -556,6 +556,11 @@ final class GlobalHotkeyManager: NSObject { if self.promptShortcutAssignments.contains(where: { $0.shortcut.matches(keyCode: keyCode, modifiers: modifiers) }) { return true } + if self.promptModeShortcutEnabled, + self.promptModeShortcut.matches(keyCode: keyCode, modifiers: modifiers) + { + return true + } if self.commandModeShortcutEnabled, let commandModeShortcut = self.commandModeShortcut, commandModeShortcut.matches(keyCode: keyCode, modifiers: modifiers)