5 Commits
Author SHA1 Message Date
TZGyn e465acba12 Close the panel when a tray item is selected
Selecting a tile opened the app's menu while SimpleTopBar's own panel
stayed on top of it, and could leave menus such as Spotlight stranded
behind it. Dismiss the panel first so the selected menu is unobstructed.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 10:26:09 +08:00
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
7 changed files with 93 additions and 31 deletions
+3
View File
@@ -141,6 +141,9 @@ preferred position controls where the hidden/kept boundary lands:
(`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The (`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
menu is its own window anchored at the item's on-screen spot, so it survives menu is its own window anchored at the item's on-screen spot, so it survives
re-hiding. re-hiding.
- `press` first calls `onWillPress`, which `AppDelegate` wires to close the
popover. Without it the panel stays open on top of the menu that opens and can
leave menus (e.g. Spotlight) stranded behind it.
- **Cannot reveal a single icon.** The spacer hides a contiguous block to its - **Cannot reveal a single icon.** The spacer hides a contiguous block to its
left, so a press can only reveal the whole block. Isolating one item would left, so a press can only reveal the whole block. Isolating one item would
need either per-item ordering control (not available) or covering the icons need either per-item ordering control (not available) or covering the icons
+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
+8
View File
@@ -132,6 +132,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
) )
) )
self.popover = popover self.popover = popover
// Selecting a tray item must dismiss our panel, otherwise it stays on
// top of the menu that opens (and can strand menus behind it).
Task { @MainActor [weak self] in
MenuBarItemManager.shared.onWillPress = {
self?.popover?.performClose(nil)
}
}
} }
@objc private func togglePopover() { @objc private func togglePopover() {
+20
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 {
@@ -28,6 +30,9 @@ final class MenuBarItemManager: ObservableObject {
private var observers: [NSObjectProtocol] = [] private var observers: [NSObjectProtocol] = []
/// Called just before pressing an item so the owner can close its panel.
var onWillPress: (() -> Void)?
private init() {} private init() {}
func start() { func start() {
@@ -91,6 +96,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
)) ))
} }
@@ -110,6 +116,10 @@ final class MenuBarItemManager: ObservableObject {
/// hide again shortly after. The reveal is momentary rather than lasting the /// hide again shortly after. The reveal is momentary rather than lasting the
/// whole time the panel is open. /// whole time the panel is open.
func press(_ item: MenuBarItem) { func press(_ item: MenuBarItem) {
// Close our own panel first, otherwise it stays open on top of the
// menu that is about to appear (and can leave menus behind it).
onWillPress?()
let hider = MenuBarHider.shared let hider = MenuBarHider.shared
let needsReveal = hider.isHiding let needsReveal = hider.isHiding
guard needsReveal else { guard needsReveal else {
@@ -134,6 +144,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.3}"
swift build -c release swift build -c release