From d9eddf799f467fe84c3e691d5f1c00cdddc7fe92 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 27 Sep 2026 02:18:57 +0800 Subject: [PATCH] Give Control Center items distinct icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every Control Center module shares one app icon, so the tray was a wall of identical tiles. Map each item's title to its own SF Symbol so Wi-Fi, Battery, Now Playing and friends are distinguishable at a glance. 💘 Generated with Crush Assisted-by: Crush:deepseek-v4.1-flash --- AGENTS.md | 4 ++ Sources/SimpleTopBar/MenuBarItems.swift | 50 ++++++++++++++++++++++++- Sources/SimpleTopBar/SelfTest.swift | 14 +++++-- Sources/SimpleTopBar/TopBarView.swift | 2 +- 4 files changed, 64 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 76fc73c..86681bd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,10 @@ executable SwiftPM package, no external dependencies (all Apple frameworks). including ones not in the bar, which are parked off-screen at `y≈982`. `enumerate()` drops items whose AX position `y > 60` and de-duplicates by title per app, otherwise the tray fills with identical "Control Center" tiles. +- Icons: every Control Center module shares the same app icon, so + `symbolName(for:)` maps titles (Wi‑Fi, Battery, Now Playing, …) to distinct SF + Symbols, falling back to the owning app's icon. Note the non-breaking hyphen + in "Wi‑Fi" (`\u{2011}`) is normalised before matching. - Reading another app's UI is the only TCC permission needed; there is no Screen Recording or Apple Events usage. diff --git a/Sources/SimpleTopBar/MenuBarItems.swift b/Sources/SimpleTopBar/MenuBarItems.swift index f3ab437..ff025e3 100644 --- a/Sources/SimpleTopBar/MenuBarItems.swift +++ b/Sources/SimpleTopBar/MenuBarItems.swift @@ -8,7 +8,7 @@ struct MenuBarItem: Identifiable, Equatable { let pid: pid_t let appName: String let title: String - let appIcon: NSImage + let icon: NSImage let element: AXUIElement static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool { @@ -90,7 +90,7 @@ final class MenuBarItemManager: ObservableObject { pid: pid, appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown", title: title, - appIcon: app.icon ?? NSImage(named: NSImage.applicationIconName) ?? NSImage(), + icon: Self.icon(forTitle: title, app: app), element: child )) } @@ -113,6 +113,52 @@ final class MenuBarItemManager: ObservableObject { return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success } + // MARK: - Icons + + /// Picks an icon for a menu bar item. Control Center owns many unrelated + /// modules, and they all share the Control Center app icon, so map their + /// titles to distinct SF Symbols for readability. Anything unknown falls + /// back to the owning app's icon. + nonisolated static func icon(forTitle title: String, app: NSRunningApplication) -> NSImage { + if let symbol = symbolName(for: title), + let image = NSImage(systemSymbolName: symbol, accessibilityDescription: title) { + image.isTemplate = true + return image + } + return app.icon ?? NSImage(named: NSImage.applicationIconName) ?? NSImage() + } + + /// Keyword table mapping a menu bar item's title/description to an SF Symbol. + nonisolated static func symbolName(for title: String) -> String? { + // Control Center uses a non-breaking hyphen in "Wi‑Fi". + let text = title.lowercased() + .replacingOccurrences(of: "\u{2010}", with: "-") + .replacingOccurrences(of: "\u{2011}", with: "-") + let table: [(matches: [String], symbol: String)] = [ + (["wi-fi", "wifi"], "wifi"), + (["battery"], "battery.100"), + (["clock", "time"], "clock"), + (["now playing"], "play.circle"), + (["focus"], "moon.fill"), + (["sound", "volume", "audio"], "speaker.wave.2"), + (["airdrop"], "dot.radiowaves.left.and.right"), + (["bluetooth"], "dot.radiowaves.left.and.right"), + (["mirroring"], "rectangle.on.rectangle"), + (["search", "spotlight"], "magnifyingglass"), + (["siri"], "waveform"), + (["keyboard"], "keyboard"), + (["stage manager"], "rectangle.3.group"), + (["brightness", "display"], "sun.max"), + (["control center"], "switch.2"), + (["notification"], "bell"), + (["vpn"], "lock.shield"), + ] + for entry in table where entry.matches.contains(where: { text.contains($0) }) { + return entry.symbol + } + return nil + } + // MARK: - AX helpers nonisolated static func extrasMenuBar(of app: AXUIElement) -> AXUIElement? { diff --git a/Sources/SimpleTopBar/SelfTest.swift b/Sources/SimpleTopBar/SelfTest.swift index 384dfcc..f6cc91f 100644 --- a/Sources/SimpleTopBar/SelfTest.swift +++ b/Sources/SimpleTopBar/SelfTest.swift @@ -80,12 +80,20 @@ func runSelfTest() { check(MenuBarItemManager.children(of: systemWide) == nil, "no children on a non-app element") let element = AXUIElementCreateSystemWide() - let first = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", appIcon: NSImage(), element: element) - let same = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", appIcon: NSImage(), element: element) - let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", appIcon: NSImage(), element: element) + let first = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element) + let same = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element) + let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element) check(first == same, "menu bar items compare by id and title") check(first != different, "different ids are not equal") + // Control Center items map to distinct symbols instead of one shared icon. + check(MenuBarItemManager.symbolName(for: "Wi\u{2011}Fi, connected, 3 bars") == "wifi", "wi-fi maps to wifi symbol") + check(MenuBarItemManager.symbolName(for: "Battery") == "battery.100", "battery maps to battery symbol") + check(MenuBarItemManager.symbolName(for: "Now Playing") == "play.circle", "now playing maps to play symbol") + check(MenuBarItemManager.symbolName(for: "Control Center") == "switch.2", "control center maps to switch symbol") + check(MenuBarItemManager.symbolName(for: "Search") == "magnifyingglass", "search maps to magnifier symbol") + check(MenuBarItemManager.symbolName(for: "cmux") == nil, "unknown titles fall back to the app icon") + let authorized = AXIsProcessTrusted() print("accessibility authorized=\(authorized)") if authorized { diff --git a/Sources/SimpleTopBar/TopBarView.swift b/Sources/SimpleTopBar/TopBarView.swift index 241a0fb..ed2c46d 100644 --- a/Sources/SimpleTopBar/TopBarView.swift +++ b/Sources/SimpleTopBar/TopBarView.swift @@ -245,7 +245,7 @@ private struct MenuBarTile: View { Button { menuBar.press(item) } label: { - Image(nsImage: item.appIcon) + Image(nsImage: item.icon) .resizable() .frame(width: 20, height: 20) .padding(6)