Skip to content
Merged
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
4 changes: 4 additions & 0 deletions Squirrel.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -305,6 +306,7 @@
B3216E5B2BF438F800E292D2 /* rime.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; name = rime.pdf; path = resources/rime.pdf; sourceTree = "<group>"; };
B32B80772BE7FAA200FCF3BC /* Squirrel.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; name = Squirrel.entitlements; path = resources/Squirrel.entitlements; sourceTree = "<group>"; };
B35D2FE72BF00839009D156B /* BridgingFunctions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BridgingFunctions.swift; path = sources/BridgingFunctions.swift; sourceTree = "<group>"; };
B3A001012F260100009D156B /* ReservedProperty.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ReservedProperty.swift; path = sources/ReservedProperty.swift; sourceTree = "<group>"; };
B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Squirrel-Bridging-Header.h"; path = "sources/Squirrel-Bridging-Header.h"; sourceTree = "<group>"; };
B38E9B902BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelApplicationDelegate.swift; path = sources/SquirrelApplicationDelegate.swift; sourceTree = "<group>"; };
B38E9B942BEAFEFD0036ABEF /* SquirrelInputController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SquirrelInputController.swift; path = sources/SquirrelInputController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -351,6 +353,7 @@
B39771282BEDAF4A0093A49B /* SquirrelView.swift */,
B39771242BED899F0093A49B /* SquirrelConfig.swift */,
B35D2FE72BF00839009D156B /* BridgingFunctions.swift */,
B3A001012F260100009D156B /* ReservedProperty.swift */,
B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */,
);
name = Sources;
Expand Down Expand Up @@ -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;
};
Expand Down
70 changes: 70 additions & 0 deletions sources/ReservedProperty.swift
Original file line number Diff line number Diff line change
@@ -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: [:])

Check warning on line 32 in sources/ReservedProperty.swift

View workflow job for this annotation

GitHub Actions / build

unused

Property 'empty' is unused

/// 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<Int> {
guard let raw = fields[Self.defaultField] else { throw .missingDefaultFields }
var out = Set<Int>()
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)
}
20 changes: 17 additions & 3 deletions sources/SquirrelApplicationDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<CChar>?, UnsafePointer<CChar>?) -> Void = notificationHandler
let context_object = Unmanaged.passUnretained(self).toOpaque()
Expand Down Expand Up @@ -269,11 +271,13 @@ extension RimeStringSlice {
}
}

// swiftlint:disable:next cyclomatic_complexity
private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessionId: RimeSessionId, messageTypeC: UnsafePointer<CChar>?, messageValueC: UnsafePointer<CChar>?) {
let delegate: SquirrelApplicationDelegate = Unmanaged<SquirrelApplicationDelegate>.fromOpaque(contextObject!).takeUnretainedValue()

let messageType = messageTypeC.map { String(cString: $0) }
let messageValue = messageValueC.map { String(cString: $0) }

if messageType == "deploy" {
switch messageValue {
case "start":
Expand All @@ -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 {
Expand Down Expand Up @@ -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[..<eqIndex])
let value = String(messageValue[messageValue.index(after: eqIndex)...])
Task.detached { @MainActor in
do {
try delegate.panel?.inputController?.handleReservedProperty(key: key, value: value, for: sessionId)
} catch {
print("Error processing handleReservedProperty: \(error)")
}
}
return
}

if delegate.enableNotifications {
Expand Down
27 changes: 24 additions & 3 deletions sources/SquirrelInputController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,23 @@
NSApp.squirrelAppDelegate.openWiki()
}

// Added for special properties reserved for librime plugins
private(set) var specialCommentIndices: [ReservedPropertyKey: Set<Int>] = [:]

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()
}
Expand Down Expand Up @@ -465,9 +482,13 @@
}
}

// 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) {

Check warning on line 487 in sources/SquirrelInputController.swift

View workflow job for this annotation

GitHub Actions / build

Function should have complexity 10 or less; currently complexity is 13 (cyclomatic_complexity)
// print("[DEBUG] rimeUpdate")
if clearReservedComments {
specialCommentIndices = [:]
}
rimeConsumeCommittedText()

var status = RimeStatus_stdbool.rimeStructInit()
Expand Down Expand Up @@ -644,7 +665,7 @@

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")
Expand Down
14 changes: 13 additions & 1 deletion sources/SquirrelPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions sources/SquirrelTheme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down
Loading