Split the tray into Apps and System sections
Control Center items sat in the same grid as third-party app icons, which
was hard to scan. Group them so Apple's Wi-Fi, Battery, Clock and friends
are listed separately from your apps' icons.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4.1-flash
This commit is contained in:
@@ -5,9 +5,9 @@ collects every other app's menu bar (tray) icons, plus every app running on your
|
|||||||
Mac, into three sections:
|
Mac, into three sections:
|
||||||
|
|
||||||
- **Menu Bar** — a tray of all other apps' menu bar items, discovered through
|
- **Menu Bar** — a tray of all other apps' menu bar items, discovered through
|
||||||
the Accessibility API. Click one to open its real menu. Optionally **hide the
|
the Accessibility API. It is split into **Apps** and **System** sections.
|
||||||
items from the menu bar** so they only live inside this panel (best-effort;
|
Click one to open its real menu. Optionally **hide the items from the menu
|
||||||
see below).
|
bar** so they only live inside this panel (best-effort; see below).
|
||||||
- **Apps** — regular apps that live in the Dock.
|
- **Apps** — regular apps that live in the Dock.
|
||||||
- **Background Apps** — accessory/agent apps without a Dock icon. This section
|
- **Background Apps** — accessory/agent apps without a Dock icon. This section
|
||||||
is a **dropdown**: it stays collapsed to a single header, and when you expand
|
is a **dropdown**: it stays collapsed to a single header, and when you expand
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ struct MenuBarItem: Identifiable, Equatable {
|
|||||||
let appName: String
|
let appName: String
|
||||||
let title: String
|
let title: String
|
||||||
let icon: NSImage
|
let icon: NSImage
|
||||||
|
/// True for Apple's own items (Control Center, Spotlight, …).
|
||||||
|
let isSystem: Bool
|
||||||
let element: AXUIElement
|
let element: AXUIElement
|
||||||
|
|
||||||
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
|
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
|
||||||
@@ -91,6 +93,7 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
|
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
|
||||||
title: title,
|
title: title,
|
||||||
icon: Self.icon(forTitle: title, app: app),
|
icon: Self.icon(forTitle: title, app: app),
|
||||||
|
isSystem: Self.isSystemBundle(app.bundleIdentifier),
|
||||||
element: child
|
element: child
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -134,6 +137,16 @@ final class MenuBarItemManager: ObservableObject {
|
|||||||
return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success
|
return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Classification
|
||||||
|
|
||||||
|
/// Apple's own menu bar items (Control Center, Spotlight, the input-source
|
||||||
|
/// agent, …) live under `com.apple.*` bundles; everything else is a third
|
||||||
|
/// party app.
|
||||||
|
nonisolated static func isSystemBundle(_ bundleID: String?) -> Bool {
|
||||||
|
guard let bundleID else { return false }
|
||||||
|
return bundleID.hasPrefix("com.apple.")
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Icons
|
// MARK: - Icons
|
||||||
|
|
||||||
/// Picks an icon for a menu bar item. Control Center owns many unrelated
|
/// Picks an icon for a menu bar item. Control Center owns many unrelated
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func dumpMenuBarItems() {
|
|||||||
lines.append("count=\(items.count)")
|
lines.append("count=\(items.count)")
|
||||||
for item in items {
|
for item in items {
|
||||||
let pos = MenuBarItemManager.pointAttribute(item.element, kAXPositionAttribute)
|
let pos = MenuBarItemManager.pointAttribute(item.element, kAXPositionAttribute)
|
||||||
lines.append("\(item.appName) | \(item.title) | pid=\(item.pid) | id=\(item.id) | pos=\(pos.map { "\(Int($0.x)),\(Int($0.y))" } ?? "?")")
|
lines.append("\(item.appName) | \(item.title) | pid=\(item.pid) | id=\(item.id) | system=\(item.isSystem) | pos=\(pos.map { "\(Int($0.x)),\(Int($0.y))" } ?? "?")")
|
||||||
}
|
}
|
||||||
let own = MenuBarItemManager.enumerate(includeOwn: true).filter {
|
let own = MenuBarItemManager.enumerate(includeOwn: true).filter {
|
||||||
$0.pid == ProcessInfo.processInfo.processIdentifier
|
$0.pid == ProcessInfo.processInfo.processIdentifier
|
||||||
@@ -103,9 +103,9 @@ 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", icon: NSImage(), element: element)
|
let first = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), isSystem: false, element: element)
|
||||||
let same = 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(), isSystem: false, element: element)
|
||||||
let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", icon: NSImage(), element: element)
|
let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", icon: NSImage(), isSystem: false, 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")
|
||||||
|
|
||||||
@@ -117,6 +117,11 @@ func runSelfTest() {
|
|||||||
check(MenuBarItemManager.symbolName(for: "Search") == "magnifyingglass", "search maps to magnifier 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")
|
check(MenuBarItemManager.symbolName(for: "cmux") == nil, "unknown titles fall back to the app icon")
|
||||||
|
|
||||||
|
// Tray sections: Apple bundles are "system", everything else is an app.
|
||||||
|
check(MenuBarItemManager.isSystemBundle("com.apple.controlcenter"), "apple bundles classify as system")
|
||||||
|
check(!MenuBarItemManager.isSystemBundle("com.cmux.app"), "third-party bundles classify as apps")
|
||||||
|
check(!MenuBarItemManager.isSystemBundle(nil), "unknown bundles are treated as apps")
|
||||||
|
|
||||||
let authorized = AXIsProcessTrusted()
|
let authorized = AXIsProcessTrusted()
|
||||||
print("accessibility authorized=\(authorized)")
|
print("accessibility authorized=\(authorized)")
|
||||||
if authorized {
|
if authorized {
|
||||||
|
|||||||
@@ -60,23 +60,6 @@ struct TopBarView: View {
|
|||||||
/// The hamburger tray: every other app's menu bar item lives here.
|
/// The hamburger tray: every other app's menu bar item lives here.
|
||||||
private var menuBarSection: some View {
|
private var menuBarSection: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(spacing: 6) {
|
|
||||||
Text("MENU BAR")
|
|
||||||
.font(.caption2)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
Spacer()
|
|
||||||
Text("\(menuBar.items.count)")
|
|
||||||
.font(.caption2)
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.vertical, 1)
|
|
||||||
.background(Capsule().fill(Color.secondary.opacity(0.15)))
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
.padding(.top, 4)
|
|
||||||
.padding(.bottom, 4)
|
|
||||||
|
|
||||||
if !menuBar.authorized {
|
if !menuBar.authorized {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text("Grant Accessibility to collect and open menu bar items.")
|
Text("Grant Accessibility to collect and open menu bar items.")
|
||||||
@@ -98,12 +81,9 @@ struct TopBarView: View {
|
|||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
} else {
|
} else {
|
||||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
|
trayGroup(title: "Apps", items: appTrayItems)
|
||||||
ForEach(menuBar.items) { item in
|
Divider().padding(.vertical, 2)
|
||||||
MenuBarTile(item: item, menuBar: menuBar)
|
trayGroup(title: "System", items: systemTrayItems)
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 12)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if menuBar.authorized {
|
if menuBar.authorized {
|
||||||
@@ -120,6 +100,52 @@ struct TopBarView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Third-party apps' menu bar icons.
|
||||||
|
private var appTrayItems: [MenuBarItem] {
|
||||||
|
menuBar.items.filter { !$0.isSystem }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apple's own menu bar items (Control Center, Spotlight, …).
|
||||||
|
private var systemTrayItems: [MenuBarItem] {
|
||||||
|
menuBar.items.filter { $0.isSystem }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func trayGroup(title: String, items: [MenuBarItem]) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 6) {
|
||||||
|
Text(title.uppercased())
|
||||||
|
.font(.caption2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
Text("\(items.count)")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.vertical, 1)
|
||||||
|
.background(Capsule().fill(Color.secondary.opacity(0.15)))
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.top, 4)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
|
||||||
|
if items.isEmpty {
|
||||||
|
Text("None")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.tertiary)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
} else {
|
||||||
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
|
||||||
|
ForEach(items) { item in
|
||||||
|
MenuBarTile(item: item, menuBar: menuBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func section(title: String, apps: [RunningApp], emptyText: String) -> some View {
|
private func section(title: String, apps: [RunningApp], emptyText: String) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(title.uppercased())
|
Text(title.uppercased())
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
VERSION="${1:-0.4.1}"
|
VERSION="${1:-0.4.2}"
|
||||||
|
|
||||||
swift build -c release
|
swift build -c release
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user