Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9eddf799f |
@@ -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`.
|
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
|
`enumerate()` drops items whose AX position `y > 60` and de-duplicates by
|
||||||
title per app, otherwise the tray fills with identical "Control Center" tiles.
|
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
|
- Reading another app's UI is the only TCC permission needed; there is no Screen
|
||||||
Recording or Apple Events usage.
|
Recording or Apple Events usage.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ struct MenuBarItem: Identifiable, Equatable {
|
|||||||
let pid: pid_t
|
let pid: pid_t
|
||||||
let appName: String
|
let appName: String
|
||||||
let title: String
|
let title: String
|
||||||
let appIcon: NSImage
|
let icon: NSImage
|
||||||
let element: AXUIElement
|
let element: AXUIElement
|
||||||
|
|
||||||
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
|
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
|
||||||
@@ -90,7 +90,7 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
pid: pid,
|
pid: pid,
|
||||||
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
|
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
|
||||||
title: title,
|
title: title,
|
||||||
appIcon: app.icon ?? NSImage(named: NSImage.applicationIconName) ?? NSImage(),
|
icon: Self.icon(forTitle: title, app: app),
|
||||||
element: child
|
element: child
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,52 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success
|
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
|
// MARK: - AX helpers
|
||||||
|
|
||||||
nonisolated static func extrasMenuBar(of app: AXUIElement) -> AXUIElement? {
|
nonisolated static func extrasMenuBar(of app: AXUIElement) -> AXUIElement? {
|
||||||
|
|||||||
@@ -80,12 +80,20 @@ func runSelfTest() {
|
|||||||
check(MenuBarItemManager.children(of: systemWide) == nil, "no children on a non-app element")
|
check(MenuBarItemManager.children(of: systemWide) == nil, "no children on a non-app element")
|
||||||
|
|
||||||
let element = AXUIElementCreateSystemWide()
|
let element = AXUIElementCreateSystemWide()
|
||||||
let first = MenuBarItem(id: "1-0", 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", appIcon: 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", appIcon: 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 == same, "menu bar items compare by id and title")
|
||||||
check(first != different, "different ids are not equal")
|
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()
|
let authorized = AXIsProcessTrusted()
|
||||||
print("accessibility authorized=\(authorized)")
|
print("accessibility authorized=\(authorized)")
|
||||||
if authorized {
|
if authorized {
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ private struct MenuBarTile: View {
|
|||||||
Button {
|
Button {
|
||||||
menuBar.press(item)
|
menuBar.press(item)
|
||||||
} label: {
|
} label: {
|
||||||
Image(nsImage: item.appIcon)
|
Image(nsImage: item.icon)
|
||||||
.resizable()
|
.resizable()
|
||||||
.frame(width: 20, height: 20)
|
.frame(width: 20, height: 20)
|
||||||
.padding(6)
|
.padding(6)
|
||||||
|
|||||||
Reference in New Issue
Block a user