4 Commits
Author SHA1 Message Date
TZGyn 29157c77fb 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
2026-09-27 10:18:28 +08:00
TZGyn b0671abed8 Set default release version to 0.4.1 2026-09-27 10:14:00 +08:00
TZGyn 569abe967f Revert "Hide menu bar icons with a cover instead of moving them off screen"
This reverts commit 3e57efe312.
2026-09-27 10:13:10 +08:00
TZGyn 3e57efe312 Hide menu bar icons with a cover instead of moving them off screen
Sliding the icons off the screen meant bringing them all back just to
open one menu, which flickered. Draw a frosted window over the other
apps' icons instead: they keep their real positions, so tapping a tile
opens its menu in place with no reveal, and the cover leaves a gap so
the hamburger is never hidden.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 03:06:15 +08:00
5 changed files with 75 additions and 31 deletions
+3 -3
View File
@@ -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
+13
View File
@@ -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
+9 -4
View File
@@ -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 {
+49 -23
View File
@@ -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
View File
@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")" cd "$(dirname "$0")"
VERSION="${1:-0.2.3}" VERSION="${1:-0.4.2}"
swift build -c release swift build -c release