Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 77 additions & 6 deletions Sources/Fluid/Services/GlobalHotkeyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,56 @@ 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.promptModeShortcutEnabled,
self.promptModeShortcut.matches(keyCode: keyCode, modifiers: modifiers)
{
return true
}
if self.commandModeShortcutEnabled,
let commandModeShortcut = self.commandModeShortcut,
commandModeShortcut.matches(keyCode: keyCode, modifiers: modifiers)
{
Comment on lines +564 to +567

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 Include prompt-mode shortcuts in fast-path guard

When the Secondary Dictation/Prompt Mode shortcut is assigned to Cmd+Space or Cmd+Tab, this helper still returns false because it checks prompt assignments and then jumps straight to command mode without testing promptModeShortcut. The new fast path in handleKeyEvent then returns before handlePromptModeKeyDown can run, so that configured Fluid binding is ignored and the macOS system shortcut fires instead. Please include the enabled prompt-mode shortcut in this guard like the other configured shortcuts.

Useful? React with 👍 / 👎.

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))
}
Expand Down Expand Up @@ -592,6 +642,32 @@ final class GlobalHotkeyManager: NSObject {
return tapRecoveryResult
}

// 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))
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)
}
}
}
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

if self.isShortcutCaptureActiveProvider?() ?? false {
self.resetModifierOnlyShortcutTracking()
return Unmanaged.passUnretained(event)
Expand All @@ -600,12 +676,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:
Expand Down
9 changes: 9 additions & 0 deletions Sources/Fluid/Views/BottomOverlayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -3088,6 +3096,7 @@ struct BottomOverlayView: View {
// NotchOverlayManager.shared.onNotchClicked?()
// }
// }
.accessibilityHidden(true)
}
}

Expand Down
Loading