From 4dc5acb878673d7651c459f183c43de251df5a5a Mon Sep 17 00:00:00 2001 From: shangyingbin <1078877341@qq.com> Date: Thu, 13 Aug 2026 10:06:34 +0800 Subject: [PATCH] Fix direct punctuation commits with marked text --- data/squirrel.yaml | 5 ++ sources/SquirrelInputController.swift | 88 ++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 3 deletions(-) diff --git a/data/squirrel.yaml b/data/squirrel.yaml index 493f83310..ced06405c 100644 --- a/data/squirrel.yaml +++ b/data/squirrel.yaml @@ -15,6 +15,11 @@ chord_duration: 0.1 # seconds # options: always | never | appropriate show_notifications_when: appropriate +# Compatibility workarounds for text clients with incomplete input-method support. +compatibility: + # Commit direct punctuation through a marked-text phase before insertText. + commit_direct_punct_as_marked_text: true + # Menu-bar status icon. # show — whether to show the icon at all; set to false for a clean menu bar. # The icon's text is the schema's short state label for ascii_mode (via diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index ed2aef244..139cafa3f 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -27,6 +27,7 @@ final class SquirrelInputController: IMKInputController { private var chordTimer: Timer? private var chordDuration: TimeInterval = 0 private var currentApp: String = "" + private var lastDirectPunctuationKeyText: String? // swiftlint:disable:next cyclomatic_complexity override func handle(_ event: NSEvent!, client sender: Any!) -> Bool { @@ -111,8 +112,34 @@ final class SquirrelInputController: IMKInputController { caps: modifiers.contains(.capsLock)) if rimeKeycode != 0 { let rimeModifiers = SquirrelKeycode.osxModifiersToRime(modifiers: modifiers) - handled = processKey(rimeKeycode, modifiers: rimeModifiers) - rimeUpdate() + let directPunctuationKeyText = + shouldPreMarkDirectPunctuationKey(String(char)) ? String(char) : nil + if let directPunctuationKeyText { + lastDirectPunctuationKeyText = directPunctuationKeyText + show( + preedit: directPunctuationKeyText, + selRange: NSRange(location: 0, length: directPunctuationKeyText.utf16.count), + caretPos: directPunctuationKeyText.utf16.count + ) + handled = true + // Some clients ignore direct punctuation commits unless the input + // method first exposes a marked-text phase. Let the key event finish + // after pre-marking, then ask Rime to commit on the next run loop. + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + let delayedHandled = self.processKey(rimeKeycode, modifiers: rimeModifiers) + self.rimeUpdate() + if !delayedHandled { + self.lastDirectPunctuationKeyText = nil + self.preedit = "" + self.hidePalettes() + } + } + } else { + lastDirectPunctuationKeyText = nil + handled = processKey(rimeKeycode, modifiers: rimeModifiers) + rimeUpdate() + } } } @@ -565,11 +592,16 @@ private extension SquirrelInputController { let forceMarkedText = session != 0 && rimeAPI.get_option(session, "force_marked_text_for_direct_commit") + let commitDirectPunctAsMarkedText = + NSApp.squirrelAppDelegate.config?.getBool("compatibility/commit_direct_punct_as_marked_text") ?? false // Direct commits such as full-width punctuation do not necessarily have an // active marked-text phase. Some NSTextInputClient implementations require // one before accepting insertText. - if forceMarkedText && preedit.isEmpty && !string.isEmpty { + if forceMarkedText && + preedit.isEmpty && + !string.isEmpty && + !(commitDirectPunctAsMarkedText && isPunctuationOrSymbolCommit(string)) { let markedText = NSMutableAttributedString(string: string) client.setMarkedText( markedText, @@ -578,11 +610,61 @@ private extension SquirrelInputController { ) } + if commitDirectPunctAsMarkedText && isPunctuationOrSymbolCommit(string), + let sourceText = lastDirectPunctuationKeyText { + if preedit != sourceText { + show( + preedit: sourceText, + selRange: NSRange(location: 0, length: sourceText.utf16.count), + caretPos: sourceText.utf16.count + ) + } + client.insertText(string, replacementRange: .empty) + lastDirectPunctuationKeyText = nil + preedit = "" + hidePalettes() + return + } + client.insertText(string, replacementRange: .empty) preedit = "" hidePalettes() } + func isPunctuationOrSymbolCommit(_ string: String) -> Bool { + guard !string.isEmpty else { return false } + return string.unicodeScalars.allSatisfy { scalar in + switch scalar.properties.generalCategory { + case .connectorPunctuation, .dashPunctuation, .openPunctuation, .closePunctuation, + .initialPunctuation, .finalPunctuation, .otherPunctuation, + .mathSymbol, .currencySymbol, .modifierSymbol, .otherSymbol, + .spaceSeparator: + return true + default: + return false + } + } + } + + func shouldPreMarkDirectPunctuationKey(_ string: String) -> Bool { + guard preedit.isEmpty, + !string.isEmpty, + !(session != 0 && rimeAPI.get_option(session, "ascii_mode")), + NSApp.squirrelAppDelegate.config?.getBool("compatibility/commit_direct_punct_as_marked_text") == true else { + return false + } + return string.unicodeScalars.allSatisfy { scalar in + switch scalar.properties.generalCategory { + case .connectorPunctuation, .dashPunctuation, .openPunctuation, .closePunctuation, + .initialPunctuation, .finalPunctuation, .otherPunctuation, + .mathSymbol, .currencySymbol, .modifierSymbol, .otherSymbol: + return true + default: + return false + } + } + } + func show(preedit: String, selRange: NSRange, caretPos: Int) { guard let client = client else { return } if self.preedit == preedit && self.caretPos == caretPos && self.selRange == selRange {