From f4606e44a45f3d676a8c94a5fb399443caf576de Mon Sep 17 00:00:00 2001 From: root Date: Sun, 25 Oct 2026 20:23:41 +0300 Subject: [PATCH] fix: avoid input source work in event tap --- .../CapswitchApp/MacOSInputController.swift | 32 ++++++++++++------- .../CapswitchApp/StatusBarController.swift | 8 +++-- Taskfile.yml | 3 +- 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/Sources/CapswitchApp/MacOSInputController.swift b/Sources/CapswitchApp/MacOSInputController.swift index 4d3dfc0..4ed0e3a 100644 --- a/Sources/CapswitchApp/MacOSInputController.swift +++ b/Sources/CapswitchApp/MacOSInputController.swift @@ -25,6 +25,8 @@ final class InputSourceManager { let id: String } + private var orderedSources: [Source] = [] + var currentDisplayName: String { guard let source = currentSource() else { return "—" @@ -43,9 +45,13 @@ final class InputSourceManager { return name } + func refresh() { + orderedSources = enabledKeyboardSourcesInUserOrder() + } + @discardableResult func selectNextInputSource() -> Bool { - let sources = enabledKeyboardSourcesInUserOrder() + let sources = orderedSources guard sources.count > 1 else { return false } @@ -163,6 +169,11 @@ final class MacOSInputController { throw InputControllerError.accessibilityRequired } + // Build the source order before the event tap starts. Enumerating TIS + // sources from inside a keyboard callback can block long enough for + // macOS to disable the tap while the user is typing quickly. + inputSources.refresh() + let eventMask = CGEventMask(1 << CGEventType.flagsChanged.rawValue) let refcon = Unmanaged.passUnretained(self).toOpaque() guard let tap = CGEvent.tapCreate( @@ -221,6 +232,10 @@ final class MacOSInputController { onStateChanged?() } + func refreshInputSources() { + inputSources.refresh() + } + func openAccessibilitySettings() { guard let url = URL( string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility" @@ -328,16 +343,11 @@ final class MacOSInputController { case .none: break case .switchToNextInputSource: - // TISSelectInputSource can synchronously trigger more keyboard - // notifications. Let the event tap return first so a rapid key + - // Caps sequence sees the new source before the next key is typed. - DispatchQueue.main.async { [weak self] in - guard let self, self.isRunning, !self.isPaused else { - return - } - if self.inputSources.selectNextInputSource() { - self.onStateChanged?() - } + // The source list is cached before the tap starts, so this path is + // deliberately synchronous: the next key must see the new source + // even when the user is typing continuously. + if inputSources.selectNextInputSource() { + onStateChanged?() } case .toggleCapsLock: postSyntheticCapsLock() diff --git a/Sources/CapswitchApp/StatusBarController.swift b/Sources/CapswitchApp/StatusBarController.swift index d1b1049..a418ab4 100644 --- a/Sources/CapswitchApp/StatusBarController.swift +++ b/Sources/CapswitchApp/StatusBarController.swift @@ -77,9 +77,11 @@ final class StatusBarController: NSObject, NSMenuDelegate { } private func refreshController() { - if inputController.accessibilityTrusted, - !inputController.isRunning { - try? inputController.start() + if inputController.accessibilityTrusted { + if !inputController.isRunning { + try? inputController.start() + } + inputController.refreshInputSources() } update() } diff --git a/Taskfile.yml b/Taskfile.yml index a162f67..59ed65d 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -56,7 +56,8 @@ tasks: - grep -q "LSUIElement" Resources/Info.plist - grep -q ".flagsChanged" Sources/CapswitchApp/MacOSInputController.swift - grep -q "keyboardEventKeycode" Sources/CapswitchApp/MacOSInputController.swift - - grep -q "DispatchQueue.main.async" Sources/CapswitchApp/MacOSInputController.swift + - grep -q "orderedSources" Sources/CapswitchApp/MacOSInputController.swift + - grep -q "inputSources.refresh" Sources/CapswitchApp/MacOSInputController.swift - grep -q "CGEvent.tapCreate" Sources/CapswitchApp/MacOSInputController.swift - grep -q "CGEvent.tapEnable" Sources/CapswitchApp/MacOSInputController.swift - test "$(grep -c "CGEventTapCreate" Sources/CapswitchApp/MacOSInputController.swift || true)" -eq 0