Skip to content
Open
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

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
96 changes: 82 additions & 14 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 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta

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

Expand All @@ -62,6 +66,16 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
}

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 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta
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 Down Expand Up @@ -253,30 +272,42 @@ private func notificationHandler(contextObject: UnsafeMutableRawPointer?, sessio
}
return
}
// off
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 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 longLabel = stateLabelLong.str .map { String(cString: $0) }
let shortLabel = stateLabelShort.str.map { String(cString: $0) }
delegate.showStatusMessage(msgTextLong: longLabel, msgTextShort: shortLabel)
if optionName == "ascii_mode" {
delegate.updateStatusIcon(asciiMode: state, schemaLabel: shortLabel)
}
if delegate.enableNotifications {
delegate.showStatusMessage(msgTextLong: longLabel, msgTextShort: shortLabel)
}
}
}
return
}

// off
if !delegate.enableNotifications {
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.

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
}
}

Expand All @@ -287,6 +318,43 @@ private extension SquirrelApplicationDelegate {
}
}

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() ?? ""
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