5 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
TZGyn 2312e09105 Shorten the icon reveal when opening a tray menu
The menu is its own window anchored where the item was revealed, so it
survives if the bar re-hides immediately. Revealing for ~0.3s instead of
over a second makes the flash far less noticeable while still opening
the menu in view.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 02:53:41 +08:00
7 changed files with 133 additions and 40 deletions
+10 -6
View File
@@ -135,12 +135,16 @@ preferred position controls where the hidden/kept boundary lands:
hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
- Pressing a tray tile calls `MenuBarItemManager.press`. A hidden item sits
off-screen, and its menu would open off-screen too, so press briefly
`suspend()`s the hider, waits ~0.18s for the layout to settle, performs the
accessibility action (`kAXPressAction`, then `kAXShowMenuAction`), and
`resume()`s ~1s later. The reveal is momentary, not for the whole time the
panel is open. macOS does not let us place or anchor another app's menu
anywhere else, so a momentary reveal is the only way to show it in view.
off-screen, and its menu opens off-screen too (verified with `--diag-press`:
the menu window lands at negative x). So press briefly `suspend()`s the hider,
waits ~0.2s for the layout to settle, performs the accessibility action
(`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
re-hiding.
- **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
need either per-item ordering control (not available) or covering the icons
with an overlay window instead of moving them off-screen.
- **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
hamburger is created into the collapsed space and lands off-screen.
+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:
- **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
items from the menu bar** so they only live inside this panel (best-effort;
see below).
the Accessibility API. It is split into **Apps** and **System** sections.
Click one to open its real menu. Optionally **hide the items from the menu
bar** so they only live inside this panel (best-effort; see below).
- **Apps** — regular apps that live in the Dock.
- **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
+21
View File
@@ -48,6 +48,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
setupStatusItem()
MenuBarHider.shared.prepare()
if CommandLine.arguments.contains("--diag-press") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
MenuBarHider.shared.suspend()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.25) {
if let item = MenuBarItemManager.enumerate().first(where: {
$0.title.lowercased().contains("wireguard")
}) {
NSLog("diag-press: pressing \(item.appName) after brief reveal")
_ = AXUIElementPerformAction(item.element, kAXPressAction as CFString)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2.45) {
MenuBarHider.shared.resume()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4.5) {
dumpWindows()
}
return
}
if CommandLine.arguments.contains("--diag") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
dumpMenuBarItems()
+17 -3
View File
@@ -9,6 +9,8 @@ struct MenuBarItem: Identifiable, Equatable {
let appName: String
let title: String
let icon: NSImage
/// True for Apple's own items (Control Center, Spotlight, …).
let isSystem: Bool
let element: AXUIElement
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
@@ -91,6 +93,7 @@ final class MenuBarItemManager: ObservableObject {
appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown",
title: title,
icon: Self.icon(forTitle: title, app: app),
isSystem: Self.isSystemBundle(app.bundleIdentifier),
element: child
))
}
@@ -117,10 +120,11 @@ final class MenuBarItemManager: ObservableObject {
return
}
hider.suspend()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
_ = self.performPress(item)
// Let the menu finish appearing before the bar contracts again.
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
// The menu is its own window, positioned at the item's on-screen
// spot, so the bar can be re-hidden almost immediately.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
hider.resume()
}
}
@@ -133,6 +137,16 @@ final class MenuBarItemManager: ObservableObject {
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
/// Picks an icon for a menu bar item. Control Center owns many unrelated
+32 -4
View File
@@ -1,5 +1,28 @@
import AppKit
import ApplicationServices
import CoreGraphics
/// Diagnostic used by `open SimpleTopBar.app --args --diag-press`. Presses a
/// hidden item's accessibility element without revealing it and dumps the
/// resulting high-level windows, to see where its menu actually opens.
func dumpWindows() {
var lines: [String] = []
if let list = CGWindowListCopyWindowInfo([], kCGNullWindowID) as? [[String: Any]] {
for window in list {
let layer = window[kCGWindowLayer as String] as? Int ?? -1
guard layer > 20 else { continue }
let owner = window[kCGWindowOwnerName as String] as? String ?? "?"
let bounds = window[kCGWindowBounds as String] as? [String: Any] ?? [:]
let x = bounds["X"] as? Double ?? 0
let y = bounds["Y"] as? Double ?? 0
let width = bounds["Width"] as? Double ?? 0
let height = bounds["Height"] as? Double ?? 0
lines.append("\(owner) layer=\(layer) frame=\(Int(x)),\(Int(y)) \(Int(width))x\(Int(height))")
}
}
try? lines.joined(separator: "\n").write(toFile: "/tmp/simpletopbar-windows.txt", atomically: true, encoding: .utf8)
exit(0)
}
/// Diagnostic used by `open SimpleTopBar.app --args --dump-items`. Writes the
/// live Accessibility view of the menu bar to /tmp so it can be inspected
@@ -12,7 +35,7 @@ func dumpMenuBarItems() {
lines.append("count=\(items.count)")
for item in items {
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 {
$0.pid == ProcessInfo.processInfo.processIdentifier
@@ -80,9 +103,9 @@ 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", 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)
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(), isSystem: false, 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 != different, "different ids are not equal")
@@ -94,6 +117,11 @@ func runSelfTest() {
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")
// 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()
print("accessibility authorized=\(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.
private var menuBarSection: some View {
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 {
VStack(alignment: .leading, spacing: 4) {
Text("Grant Accessibility to collect and open menu bar items.")
@@ -98,12 +81,9 @@ struct TopBarView: View {
.padding(.horizontal, 12)
.padding(.vertical, 4)
} else {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 34), spacing: 6)], spacing: 6) {
ForEach(menuBar.items) { item in
MenuBarTile(item: item, menuBar: menuBar)
}
}
.padding(.horizontal, 12)
trayGroup(title: "Apps", items: appTrayItems)
Divider().padding(.vertical, 2)
trayGroup(title: "System", items: systemTrayItems)
}
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 {
VStack(alignment: .leading, spacing: 0) {
Text(title.uppercased())
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")"
VERSION="${1:-0.2.3}"
VERSION="${1:-0.4.2}"
swift build -c release