diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index 124ef7b2e..46a9f06d7 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -236,10 +236,13 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta notifCenter.addObserver(forName: .init("SquirrelSyncNotification"), object: nil, queue: nil, using: rimeNeedsSync) notifCenter.addObserver(forName: .init("SquirrelToggleASCIIModeNotification"), object: nil, queue: nil, using: rimeToggleASCIIMode) notifCenter.addObserver(forName: .init("SquirrelGetASCIIModeNotification"), object: nil, queue: nil, using: rimeGetASCIIMode) - notifCenter.addObserver(forName: .init(kTISNotifySelectedKeyboardInputSourceChanged as String), object: nil, queue: .main) { [weak self] _ in - self?.updateStatusItemVisibility() - self?.finalizeStrandedComposition() - } + // Suspension behavior matters: the default coalescing holds notifications + // back while the process is inactive, which is exactly the state Squirrel + // enters when the user switches away — the icon would fail to hide until + // the next activation. Deliver immediately instead. + notifCenter.addObserver(self, selector: #selector(inputSourceChanged(_:)), + name: .init(kTISNotifySelectedKeyboardInputSourceChanged as String), + object: nil, suspensionBehavior: .deliverImmediately) } func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { @@ -361,6 +364,13 @@ private extension SquirrelApplicationDelegate { updateStatusItemVisibility() } + @objc func inputSourceChanged(_: Notification) { + DispatchQueue.main.async { [weak self] in + self?.updateStatusItemVisibility() + self?.finalizeStrandedComposition() + } + } + func updateStatusItemVisibility() { guard let statusItem = statusItem else { return } let currentInputSourceID = SquirrelInstaller.currentInputSourceID() ?? "" diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index c2adf74eb..bd9597684 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -177,6 +177,30 @@ final class SquirrelInputController: IMKInputController { if keyboardLayout != "" { client?.overrideKeyboard(withKeyboardNamed: keyboardLayout) } + // Switching to Squirrel produces no flagsChanged event, so a Caps Lock + // already engaged in the previous input source would go unnoticed by Rime. + // NSEvent.modifierFlags only reflects this process's own event stream and + // is stale here; query the session-wide hardware state instead. + let capsLockOn = CGEventSource.flagsState(.combinedSessionState).contains(.maskAlphaShift) + // Do NOT force ascii_mode from a pre-existing Caps Lock here: macOS clears + // Caps Lock whenever the input source changes, so a Caps Lock observed at + // activation is transient — acting on it raced against that clearing and + // left ascii_mode stuck on with no way for Rime to learn caps was dropped. + // Instead, only repaint the icon from the session's actual state; it may + // be stale because a fresh session created with switches/@N/reset: 1 + // never fires an option notification. + if session != 0 && rimeAPI.find_session(session) { + let asciiMode = rimeAPI.get_option(session, "ascii_mode") + let label = "ascii_mode".withCString { name in + rimeAPI.get_state_label_abbreviated(session, name, asciiMode, true).asString + } + NSApp.squirrelAppDelegate.updateStatusIcon(asciiMode: asciiMode, schemaLabel: label) + } + if capsLockOn { + lastModifiers.insert(.capsLock) + } else { + lastModifiers.remove(.capsLock) + } preedit = "" if session != 0 { let state = rimeAPI.get_option(session, "ascii_mode")