diff --git a/Squirrel.xcodeproj/project.pbxproj b/Squirrel.xcodeproj/project.pbxproj index becf3ce7c..64c31782b 100644 --- a/Squirrel.xcodeproj/project.pbxproj +++ b/Squirrel.xcodeproj/project.pbxproj @@ -90,6 +90,7 @@ D26434552706A15100857391 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D26434542706A15100857391 /* QuartzCore.framework */; }; E93074B70A5C264700470842 /* InputMethodKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E93074B60A5C264700470842 /* InputMethodKit.framework */; }; F45E005F2B8CA81C00179B75 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F45E005E2B8CA81C00179B75 /* UserNotifications.framework */; }; + 4D2A6EB7-BA19-4424-8EE7-631DBEC1AA87 /* StatusBarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBA6C012-5161-4121-AC4F-E125D9F00676 /* StatusBarManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -297,6 +298,7 @@ D26434542706A15100857391 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; }; E93074B60A5C264700470842 /* InputMethodKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = InputMethodKit.framework; path = /System/Library/Frameworks/InputMethodKit.framework; sourceTree = ""; }; F45E005E2B8CA81C00179B75 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + FBA6C012-5161-4121-AC4F-E125D9F00676 /* StatusBarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StatusBarManager.swift; path = sources/StatusBarManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -327,6 +329,7 @@ B39771282BEDAF4A0093A49B /* SquirrelView.swift */, B39771242BED899F0093A49B /* SquirrelConfig.swift */, B35D2FE72BF00839009D156B /* BridgingFunctions.swift */, + FBA6C012-5161-4121-AC4F-E125D9F00676 /* StatusBarManager.swift */, B38E9B8F2BE9AE1E0036ABEF /* Squirrel-Bridging-Header.h */, ); name = Sources; @@ -589,6 +592,7 @@ B38E9B912BE9AE1E0036ABEF /* SquirrelApplicationDelegate.swift in Sources */, B39771272BED9B250093A49B /* SquirrelTheme.swift in Sources */, B35D2FE82BF00839009D156B /* BridgingFunctions.swift in Sources */, + 4D2A6EB7-BA19-4424-8EE7-631DBEC1AA87 /* StatusBarManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/data/squirrel.yaml b/data/squirrel.yaml index 40227deb2..43e8f0228 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 +# Whether to show the current ascii_mode state in the system menu bar +# true: show "中"/"A" in menu bar +# false: do not show (default) +menubar_ascii_mode: false + style: color_scheme: native # Optional: define both light and dark color schemes to match system appearance diff --git a/sources/SquirrelApplicationDelegate.swift b/sources/SquirrelApplicationDelegate.swift index c60376040..5e24eff3b 100644 --- a/sources/SquirrelApplicationDelegate.swift +++ b/sources/SquirrelApplicationDelegate.swift @@ -19,6 +19,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta var panel: SquirrelPanel? var enableNotifications = false let updateController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + let statusBarManager = StatusBarManager() var supportsGentleScheduledUpdateReminders: Bool { true } @@ -61,6 +62,7 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta // swiftlint:disable:next notification_center_detachment NotificationCenter.default.removeObserver(self) DistributedNotificationCenter.default().removeObserver(self) + statusBarManager.teardown() panel?.hide() } @@ -162,6 +164,8 @@ final class SquirrelApplicationDelegate: NSObject, NSApplicationDelegate, SPUSta } enableNotifications = config!.getString("show_notifications_when") != "never" + let showStatusInMenuBar = config!.getBool("menubar_ascii_mode") ?? false + statusBarManager.setup(enabled: showStatusInMenuBar) if let panel = panel, let config = self.config { panel.load(config: config, forDarkMode: false) panel.load(config: config, forDarkMode: true) @@ -284,6 +288,7 @@ private extension SquirrelApplicationDelegate { func showStatusMessage(msgTextLong: String?, msgTextShort: String?) { if !(msgTextLong ?? "").isEmpty || !(msgTextShort ?? "").isEmpty { panel?.updateStatus(long: msgTextLong ?? "", short: msgTextShort ?? "") + statusBarManager.updateStatus(text: msgTextShort ?? "") } } diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index 398a469af..a6dbd0f09 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -194,6 +194,9 @@ final class SquirrelInputController: IMKInputController { if keyboardLayout != "" { client?.overrideKeyboard(withKeyboardNamed: keyboardLayout) } + if session != 0 { + updateMenuBarStatus() + } preedit = "" } @@ -295,6 +298,18 @@ final class SquirrelInputController: IMKInputController { private extension SquirrelInputController { + func updateMenuBarStatus() { + guard session != 0 else { return } + let isAsciiMode = rimeAPI.get_option(session, "ascii_mode") + "ascii_mode".withCString { name in + let label = rimeAPI.get_state_label_abbreviated(session, name, isAsciiMode, true) + if let str = label.str { + let text = String(cString: str) + NSApp.squirrelAppDelegate.statusBarManager.updateStatus(text: text) + } + } + } + func onChordTimer(_: Timer) { // chord release triggered by timer var processedKeys = false diff --git a/sources/StatusBarManager.swift b/sources/StatusBarManager.swift new file mode 100644 index 000000000..3b2a0e19b --- /dev/null +++ b/sources/StatusBarManager.swift @@ -0,0 +1,45 @@ +// +// StatusBarManager.swift +// Squirrel +// + +import AppKit + +final class StatusBarManager { + private var statusItem: NSStatusItem? + + deinit { + teardown() + } + + /// 根据配置初始化或销毁菜单栏图标 + /// - Parameters: + /// - enabled: true 创建状态项,false 移除 + /// - initialText: 初始显示的文本,默认为空字符串 + func setup(enabled: Bool, initialText: String = "") { + if enabled { + if statusItem == nil { + statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + } + if !initialText.isEmpty { + statusItem?.button?.title = initialText + } + } else { + teardown() + } + } + + /// 更新菜单栏显示的文本 + /// - Parameter text: 要显示的文本,如 "中" 或 "A" + func updateStatus(text: String) { + statusItem?.button?.title = text + } + + /// 清理资源,从菜单栏移除状态项 + func teardown() { + if let item = statusItem { + NSStatusBar.system.removeStatusItem(item) + statusItem = nil + } + } +}