diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index 148fe3f37..813deaeb0 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 7B5488C91D2DACDF0056A1BE /* symbols.yaml in Copy Shared Support Files */ = {isa = PBXBuildFile; fileRef = 7B54883B1D2DAAD10056A1BE /* symbols.yaml */; }; B3216E5C2BF438F800E292D2 /* rime.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B3216E5B2BF438F800E292D2 /* rime.pdf */; }; B35D2FE82BF00839009D156B /* BridgingFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B35D2FE72BF00839009D156B /* BridgingFunctions.swift */; }; + B3A001022F260100009D156B /* ReservedProperty.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3A001012F260100009D156B /* ReservedProperty.swift */; }; B38E9B912BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38E9B902BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift */; }; B38E9B952BEAFEFD0036ABEF /* SquirrelInputController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B38E9B942BEAFEFD0036ABEF /* SquirrelInputController.swift */; }; B39771232BECEA150093A49B /* MacOSKeyCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = B39771222BECEA150093A49B /* MacOSKeyCodes.swift */; }; @@ -305,6 +306,7 @@ B3216E5B2BF438F800E292D2 /* rime.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = rime.pdf; path = resources/rime.pdf; sourceTree = ""; }; B32B80772BE7FAA200FCF3BC /* Squirrel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = Squirrel.entitlements; path = resources/Squirrel.entitlements; sourceTree = ""; }; B35D2FE72BF00839009D156B /* BridgingFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BridgingFunctions.swift; path = sources/BridgingFunctions.swift; sourceTree = ""; }; + B3A001012F260100009D156B /* ReservedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReservedProperty.swift; path = sources/ReservedProperty.swift; sourceTree = ""; }; B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Squirrel-Bridging-Header.h"; path = "sources/Squirrel-Bridging-Header.h"; sourceTree = ""; }; B38E9B902BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelApplicationDelegate.swift; path = sources/SquirrelApplicationDelegate.swift; sourceTree = ""; }; B38E9B942BEAFEFD0036ABEF /* SquirrelInputController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelInputController.swift; path = sources/SquirrelInputController.swift; sourceTree = ""; }; @@ -351,6 +353,7 @@ B39771282BEDAF4A0093A49B /* SquirrelView.swift */, B39771242BED899F0093A49B /* SquirrelConfig.swift */, B35D2FE72BF00839009D156B /* BridgingFunctions.swift */, + B3A001012F260100009D156B /* ReservedProperty.swift */, B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */, ); name = Sources; @@ -623,6 +626,7 @@ B38E9B912BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift in Sources */, B39771272BED9B250093A49B /* SquirrelTheme.swift in Sources */, B35D2FE82BF00839009D156B /* BridgingFunctions.swift in Sources */, + B3A001022F260100009D156B /* ReservedProperty.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/sources/ReservedProperty.swift b/sources/ReservedProperty.swift new file mode 100644 index 000000000..dd36915f5 --- /dev/null +++ b/sources/ReservedProperty.swift @@ -0,0 +1,70 @@ +// +// ReservedProperty.swift +// Squirrel +// +// Reserved librime properties for plugin -> frontend coordination. +// See rime/squirrel#1124. +// +// Values use URL-style query strings. Bare values are stored under +// "value" for compatibility with historical comma-list payloads. + +import Foundation + +/// Reserved property keys recognised by Squirrel. +enum ReservedPropertyKey: String { + /// Candidate comment indices using `accent_text_color`. + case commentHighlight = "_comment_highlight" + + /// Candidate comment indices using `warning_text_color`. + case commentWarning = "_comment_warning" + + /// Requests a candidate panel refresh. + case refreshUI = "_refresh_ui" +} + +/// Parsed reserved-property fields. +struct ReservedPropertyValue { + let fields: [String: String] + + /// Field used for bare values and index lists. + static let defaultField = "value" + + static let empty = ReservedPropertyValue(fields: [:]) + + /// Parses query strings or bare values written by plugins. + static func parse(_ raw: String) throws(ReservedPropertyError) -> ReservedPropertyValue { + guard !raw.isEmpty else { throw .emptyInput } + if !raw.contains("=") { + return ReservedPropertyValue(fields: [defaultField: raw]) + } + // URLComponents needs a scheme-less URL with a leading "?". + if let queryItems = URLComponents(string: "?\(raw)")?.queryItems { + let pairs = queryItems.map { ($0.name, $0.value ?? "") } + let dict = Dictionary(pairs, uniquingKeysWith: { _, new in new }) + return ReservedPropertyValue(fields: dict) + } + throw .unknownInput(raw) + } + + /// Extracts non-negative indices from the `value` field. + func indices() throws(ReservedPropertyError) -> Set { + guard let raw = fields[Self.defaultField] else { throw .missingDefaultFields } + var out = Set() + for part in raw.split(separator: ",") { + let trimmed = part.trimmingCharacters(in: .whitespaces) + if let index = Int(trimmed), index >= 0 { + out.insert(index) + } else { + throw .invalidIndex(trimmed) + } + } + return out + } +} + +enum ReservedPropertyError: Error { + case emptyInput + case unknownInput(String) + case missingDefaultFields + case invalidIndex(String) +} diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index 27f5f02ae..376f2fcc1 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -139,6 +139,8 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta func setupRime() { createDirIfNotExist(path: SquirrelApp.userDir) createDirIfNotExist(path: SquirrelApp.logDir) + // expose log file location to librime for librime plugins to write into + setenv("RIME_LOG_DIR", SquirrelApp.logDir.path(), 1) // swiftlint:disable identifier_name let notification_handler: @convention(c) (UnsafeMutableRawPointer?, RimeSessionId, UnsafePointer?, UnsafePointer?) -> Void = notificationHandler let context_object = Unmanaged.passUnretained(self).toOpaque() @@ -269,11 +271,13 @@ extension RimeStringSlice { } } +// swiftlint:disable:next cyclomatic_complexity private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessionId: RimeSessionId, messageTypeC: UnsafePointer?, messageValueC: UnsafePointer?) { let delegate: SquirrelApplicationDelegate = Unmanaged.fromOpaque(contextObject!).takeUnretainedValue() let messageType = messageTypeC.map { String(cString: $0) } let messageValue = messageValueC.map { String(cString: $0) } + if messageType == "deploy" { switch messageValue { case "start": @@ -286,9 +290,7 @@ private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessio break } return - } - - if messageType == "option" { + } else if messageType == "option" { let state = messageValue?.first != "!" let optionName: String? if state { @@ -317,6 +319,18 @@ private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessio } } return + } else if messageType == "property", let messageValue = messageValue, + let eqIndex = messageValue.firstIndex(of: "="), messageValue.first == "_" { + let key = String(messageValue[..] = [:] + + func handleReservedProperty(key rawKey: String, value rawValue: String, for sessionId: RimeSessionId) throws(ReservedPropertyError) { + guard session == sessionId, session != 0, rimeAPI.find_session(session) else { return } + guard let key = ReservedPropertyKey(rawValue: rawKey) else { throw .unknownInput(rawKey) } + let parsed = try ReservedPropertyValue.parse(rawValue) + switch key { + case .commentHighlight: + specialCommentIndices[.commentHighlight] = try parsed.indices() + case .commentWarning: + specialCommentIndices[.commentWarning] = try parsed.indices() + case .refreshUI: + rimeUpdate(clearReservedComments: false) + } + } + deinit { destroySession() } @@ -465,9 +482,13 @@ private extension SquirrelInputController { } } - // swiftlint:disable:next cyclomatic_complexity - func rimeUpdate() { + // `clearReservedComments` defaults to true to preserve previous behavior + // false is used when `refresh_ui` message is sent from librime + func rimeUpdate(clearReservedComments: Bool = true) { // print("[DEBUG] rimeUpdate") + if clearReservedComments { + specialCommentIndices = [:] + } rimeConsumeCommittedText() var status = RimeStatus_stdbool.rimeStructInit() @@ -644,7 +665,7 @@ private extension SquirrelInputController { private func reportASCIIMode(_: Notification) { // Only active input controller should respond - guard let client = client else { return } + guard client != nil else { return } guard session != 0 && rimeAPI.find_session(session) else { return } let isASCIIMode = rimeAPI.get_option(session, "ascii_mode") diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift index 7742b2ebb..87251c057 100644 --- a/sources/SquirrelPanel.swift +++ b/sources/SquirrelPanel.swift @@ -240,7 +240,19 @@ final class SquirrelPanel: NSPanel { } } for range in line.string.ranges(of: /\[comment\]/) { - line.addAttributes(commentAttrs, range: convert(range: range, in: line.string)) + let convertedRange = convert(range: range, in: line.string) + // Apply semantic accent/warning colors only for non-highlighted rows + if let inputController, !inputController.specialCommentIndices.isEmpty && i != index { + var newCommentAttrs = commentAttrs + if let accent = inputController.specialCommentIndices[.commentHighlight], accent.contains(i) { + newCommentAttrs[.foregroundColor] = theme.accentCommentTextColor + } else if let warning = inputController.specialCommentIndices[.commentWarning], warning.contains(i) { + newCommentAttrs[.foregroundColor] = theme.warningCommentTextColor + } + line.addAttributes(newCommentAttrs, range: convertedRange) + } else { + line.addAttributes(commentAttrs, range: convertedRange) + } } line.mutableString.replaceOccurrences(of: "[label]", with: label, range: NSRange(location: 0, length: line.length)) let labeledLine = line.copy() as! NSAttributedString diff --git a/sources/SquirrelTheme.swift b/sources/SquirrelTheme.swift index feacfccb4..d14d370ee 100644 --- a/sources/SquirrelTheme.swift +++ b/sources/SquirrelTheme.swift @@ -47,6 +47,9 @@ final class SquirrelTheme { private var highlightedCandidateLabelColor: NSColor? = .secondaryLabelColor private var commentTextColor: NSColor? = .secondaryLabelColor private var highlightedCommentTextColor: NSColor? = .secondaryLabelColor + // Semantic comment colors (proposal in rime/squirrel#1124). + private(set) var accentCommentTextColor: NSColor? + private(set) var warningCommentTextColor: NSColor? private(set) var cornerRadius: CGFloat = 0 private(set) var hilitedCornerRadius: CGFloat = 0 @@ -243,6 +246,8 @@ final class SquirrelTheme { highlightedCandidateLabelColor = config.getColor("\(prefix)/hilited_candidate_label_color", inSpace: colorSpace) commentTextColor = config.getColor("\(prefix)/comment_text_color", inSpace: colorSpace) highlightedCommentTextColor = config.getColor("\(prefix)/hilited_comment_text_color", inSpace: colorSpace) + accentCommentTextColor = config.getColor("\(prefix)/accent_text_color", inSpace: colorSpace) + warningCommentTextColor = config.getColor("\(prefix)/warning_text_color", inSpace: colorSpace) // the following per-color-scheme configurations, if exist, will // override configurations with the same name under the global 'style'