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
9 changes: 9 additions & 0 deletions data/squirrel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ chord_duration: 0.1 # seconds
# options: always | never | appropriate
show_notifications_when: appropriate

# 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
# get_state_label_abbreviated). Schemas declaring `abbrev: [中, A]` get
# compact glyphs; otherwise the schema's `states:` value is used as-is.
# Falls back to "中" / "A" when no `states:` is defined.
status_icon:
show: true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

可以簡化爲 show_status_icon

誰知道會不會還有別的選項?
即使以後再加 status_icon 的詳細配置,把開關單寫出來也沒問題。


style:
color_scheme: native
# Optional: define both light and dark color schemes to match system appearance
Expand Down
6 changes: 6 additions & 0 deletions sources/InputSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ final class SquirrelInstaller {
}
}

static func currentInputSourceID() -> String? {
let source = TISCopyCurrentKeyboardInputSource().takeRetainedValue()
let idRef = TISGetInputSourceProperty(source, kTISPropertyInputSourceID)
return unsafeBitCast(idRef, to: CFString?.self) as String?
}

func disable(modes: [InputMode] = []) {
let modesToDisable = modes.isEmpty ? InputMode.allCases : modes
for (mode, inputSource) in getInputSource(modes: modesToDisable) {
Expand Down
113 changes: 97 additions & 16 deletions sources/SquirrelApplicationDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import UserNotifications
import Sparkle
import AppKit
import InputMethodKit

final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUStandardUserDriverDelegate, UNUserNotificationCenterDelegate {
static let rimeWikiURL = URL(string: "https://github.com/rime/home/wiki")!
Expand All @@ -18,6 +19,8 @@
var config: SquirrelConfig?
var panel: SquirrelPanel?
var enableNotifications = false
var showStatusIcon: Bool = true
var statusItem: NSStatusItem?
let updateController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
var supportsGentleScheduledUpdateReminders: Bool {
true
Expand Down Expand Up @@ -54,6 +57,7 @@

func applicationWillFinishLaunching(_ notification: Notification) {
panel = SquirrelPanel(position: .zero)
refreshStatusItem()
addObservers()
}

Expand All @@ -62,6 +66,16 @@
NotificationCenter.default.removeObserver(self)
DistributedNotificationCenter.default().removeObserver(self)
panel?.hide()
if let item = statusItem {
NSStatusBar.system.removeStatusItem(item)
statusItem = nil
}
}

func updateStatusIcon(asciiMode: Bool, schemaLabel: String?) {
DispatchQueue.main.async { [weak self] in
self?.applyStatusIcon(asciiMode: asciiMode, schemaLabel: schemaLabel)
}
}

func deploy() {
Expand Down Expand Up @@ -162,6 +176,8 @@
}

enableNotifications = config!.getString("show_notifications_when") != "never"
showStatusIcon = config!.getBool("status_icon/show") ?? true
refreshStatusItem()
if let panel = panel, let config = self.config {
panel.load(config: config, forDarkMode: false)
panel.load(config: config, forDarkMode: true)
Expand Down Expand Up @@ -225,6 +241,9 @@
let notifCenter = DistributedNotificationCenter.default()
notifCenter.addObserver(forName: .init("SquirrelReloadNotification"), object: nil, queue: nil, using: rimeNeedsReload)
notifCenter.addObserver(forName: .init("SquirrelSyncNotification"), object: nil, queue: nil, using: rimeNeedsSync)
notifCenter.addObserver(forName: .init(kTISNotifySelectedKeyboardInputSourceChanged as String), object: nil, queue: .main) { [weak self] _ in
self?.updateStatusItemVisibility()
}
}

func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
Expand All @@ -235,7 +254,19 @@

}

private extension RimeStringSlice {
/// Bridge the slice's pointer + length to a Swift String, honoring `.length`.
/// librime clips `.length` to the first Unicode character for abbreviated labels
/// when no explicit `abbrev:` field is defined, so reading past `.length` (e.g. with
/// `String(cString:)`) would incorrectly return the full `states:` value.
var asString: String? {
guard let ptr = str else { return nil }
let data = Data(bytes: UnsafeRawPointer(ptr), count: Int(length))
return String(data: data, encoding: .utf8)
}
}

private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessionId: RimeSessionId, messageTypeC: UnsafePointer<CChar>?, messageValueC: UnsafePointer<CChar>?) {

Check warning on line 269 in sources/SquirrelApplicationDelegate.swift

View workflow job for this annotation

GitHub Actions / build

Function should have complexity 10 or less; currently complexity is 13 (cyclomatic_complexity)
let delegate: SquirrelApplicationDelegate = Unmanaged<SquirrelApplicationDelegate>.fromOpaque(contextObject!).takeUnretainedValue()

let messageType = messageTypeC.map { String(cString: $0) }
Expand All @@ -253,30 +284,43 @@
}
return
}
// off
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code below needs cleanup.

The control flow is not easy to understand.

if !delegate.enableNotifications {
return
}

if messageType == "schema", let messageValue = messageValue, let schemaName = try? /^[^\/]*\/(.*)$/.firstMatch(in: messageValue)?.output.1 {
delegate.showStatusMessage(msgTextLong: String(schemaName), msgTextShort: String(schemaName))
return
} else if messageType == "option" {
if messageType == "option" {
let state = messageValue?.first != "!"
let optionName = if state {
messageValue
let optionName: String?
if state {
optionName = messageValue
} else if let value = messageValue {
optionName = String(value[value.index(after: value.startIndex)...])
} else {
String(messageValue![messageValue!.index(after: messageValue!.startIndex)...])
optionName = nil
}
if let optionName = optionName {
optionName.withCString { name in
let stateLabelLong = delegate.rimeAPI.get_state_label_abbreviated(sessionId, name, state, false)
let stateLabelShort = delegate.rimeAPI.get_state_label_abbreviated(sessionId, name, state, true)
let longLabel = stateLabelLong.str.map { String(cString: $0) }
let shortLabel = stateLabelShort.str.map { String(cString: $0) }
delegate.showStatusMessage(msgTextLong: longLabel, msgTextShort: shortLabel)
func shortLabel() -> String? {
let stateLabelShort = delegate.rimeAPI.get_state_label_abbreviated(sessionId, name, state, true)
return stateLabelShort.asString
}
func longLabel() -> String? {
let stateLabelLong = delegate.rimeAPI.get_state_label_abbreviated(sessionId, name, state, false)
return stateLabelLong.asString
}
if optionName == "ascii_mode" {
delegate.updateStatusIcon(asciiMode: state, schemaLabel: shortLabel())
}
if delegate.enableNotifications {
delegate.showStatusMessage(msgTextLong: longLabel(), msgTextShort: shortLabel())
}
}
}
return
}

if delegate.enableNotifications {
if messageType == "schema", let messageValue = messageValue, let schemaName = try? /^[^\/]*\/(.*)$/.firstMatch(in: messageValue)?.output.1 {
delegate.showStatusMessage(msgTextLong: String(schemaName), msgTextShort: String(schemaName))
return
}
}
}

Expand All @@ -287,6 +331,43 @@
}
}

func refreshStatusItem() {
if showStatusIcon {
if statusItem == nil {
setupStatusItem()
}
} else if let item = statusItem {
NSStatusBar.system.removeStatusItem(item)
statusItem = nil
}
}

func setupStatusItem() {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
if let button = item.button {
button.font = NSFont.systemFont(ofSize: NSFont.systemFontSize, weight: .semibold)
button.toolTip = NSLocalizedString("Squirrel", comment: "")
}
statusItem = item
applyStatusIcon(asciiMode: false, schemaLabel: nil)
updateStatusItemVisibility()
}

func updateStatusItemVisibility() {
guard let statusItem = statusItem else { return }
let id = SquirrelInstaller.currentInputSourceID() ?? ""

Check warning on line 358 in sources/SquirrelApplicationDelegate.swift

View workflow job for this annotation

GitHub Actions / build

Variable name 'id' should be between 3 and 40 characters long (identifier_name)
statusItem.isVisible = id.hasPrefix("im.rime.inputmethod.Squirrel")
}

func applyStatusIcon(asciiMode: Bool, schemaLabel: String?) {
guard let button = statusItem?.button else { return }
if let schemaLabel = schemaLabel, !schemaLabel.isEmpty {
button.title = schemaLabel
} else {
button.title = asciiMode ? "A" : "中"
}
}

func shutdownRime() {
config?.close()
rimeAPI.finalize()
Expand Down