10 Commits
Author SHA1 Message Date
TZGyn 4bc6b333dc Put the hamburger at the left of the visible icons
The hamburger was landing among the system icons. Move it to the left
edge of the visible group so it reads as the entry point rather than one
more status item.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 10:40:27 +08:00
TZGyn abb0b548bf Keep all system icons visible and click Spotlight properly
The spacer boundary cut off Now Playing and the input source, which
should stay like Wi-Fi, so move it left of every Apple item. Spotlight's
status item ignores the accessibility press action, so click it at its
real position instead.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 10:35:20 +08:00
TZGyn 8d795d0a28 Give up focus when opening a tray menu
Closing the panel was not enough: SimpleTopBar stayed frontmost, so
Spotlight's search field never appeared. Deactivate the app when a tray
item is selected so transient panels can take focus, and allow a little
more time before the bar re-hides.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 10:30:20 +08:00
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
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
TZGyn 9c5416d27e Reveal tray icons only for the moment a menu is opened
Keeping every icon on the menu bar for as long as the panel was open
looked cluttered. Reveal them just long enough to open the clicked
item's menu, then hide again, so the panel stays clean.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-27 02:47:20 +08:00
7 changed files with 238 additions and 69 deletions
+14 -6
View File
@@ -134,12 +134,20 @@ preferred position controls where the hidden/kept boundary lands:
- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets - Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets
hidden too; above ~300 app icons (Now Playing, TextInput) leak back. hidden too; above ~300 app icons (Now Playing, TextInput) leak back.
- `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding. - `debug.spacerPos` (Double) overrides the seed for tuning without rebuilding.
- Pressing a tray tile calls `MenuBarItemManager.press` (`kAXPressAction`, then - Pressing a tray tile calls `MenuBarItemManager.press`. A hidden item sits
`kAXShowMenuAction`). Because a hidden item sits off-screen, its menu would off-screen, and its menu opens off-screen too (verified with `--diag-press`:
open off-screen; `AppDelegate` therefore calls `MenuBarHider.suspend()` the menu window lands at negative x). So press briefly `suspend()`s the hider,
(`popoverWillShow`) to reveal items while the panel is open and `resume()` waits ~0.2s for the layout to settle, performs the accessibility action
0.6s after `popoverDidClose`. `suspend`/`resume` do not change the user's (`kAXPressAction`, then `kAXShowMenuAction`), and `resume()`s ~0.1s later. The
`enabled` preference. menu is its own window anchored at the item's on-screen spot, so it survives
re-hiding.
- `press` first calls `onWillPress`, which `AppDelegate` wires to close the
popover **and** `NSApp.deactivate()`. Closing alone left our app frontmost, so
transient items (Spotlight's search field) still would not appear.
- **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* - **Order matters**: `AppDelegate` must call `setupStatusItem()` *before*
`MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the `MenuBarHider.shared.prepare()`. If the hider contracts the bar first, the
hamburger is created into the collapsed space and lands off-screen. hamburger is created into the collapsed space and lands off-screen.
+13 -11
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
@@ -34,14 +34,16 @@ automatically.
What it can and cannot hide: What it can and cannot hide:
- It hides **other third-party apps' icons** (cmux, Kapture, WireGuard, input - It hides **other third-party apps' icons** (cmux, Kapture, moomoo, WireGuard).
source, Now Playing, …) so only your apps' icons leave the bar. - It **keeps Wi-Fi, Now Playing, Spotlight, Clock, Battery and Control Center**
- It **keeps Wi-Fi, Spotlight, Clock, Battery and Control Center**. The spacer's visible, with the **hamburger at the left edge of that group**. The spacer's
boundary is tuned (preferred position 250) to fall just left of Wi-Fi, so the boundary (preferred position 300) falls just left of Now Playing. macOS
Apple items on the right stay visible. macOS reserves that far-right area: a reserves the far-right area, so an item can never be placed to the right of
third-party item can never be placed to the right of it, so those icons cannot the system icons.
be pushed away even in principle. (Bartender and Ice hit the same wall and - The input-source indicator is the one system icon that ends up hidden (it sits
cover the area with their own overlay bar instead.) immediately left of where the hamburger can go). To keep it, set
`defaults write dev.tzgyn.simpletopbar debug.spacerPos -float 350` (and
`debug.hamburgerPos -float 100`), which moves the hamburger one slot right.
- Newly launched apps are placed to the left of the spacer, so they are hidden - Newly launched apps are placed to the left of the spacer, so they are hidden
automatically. automatically.
- Advanced: set `defaults write dev.tzgyn.simpletopbar debug.spacerPos -float <n>` - Advanced: set `defaults write dev.tzgyn.simpletopbar debug.spacerPos -float <n>`
+39 -16
View File
@@ -17,7 +17,7 @@ struct SimpleTopBarApp: App {
/// full menu bar, a brand new item lands under the notch and is invisible; /// full menu bar, a brand new item lands under the notch and is invisible;
/// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a /// writing `NSStatusItem Preferred Position <name>` into defaults puts it in a
/// visible slot, and the autosave name lets the user's own drag persist. /// visible slot, and the autosave name lets the user's own drag persist.
final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem? private var statusItem: NSStatusItem?
private var popover: NSPopover? private var popover: NSPopover?
@@ -48,6 +48,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
setupStatusItem() setupStatusItem()
MenuBarHider.shared.prepare() 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") { if CommandLine.arguments.contains("--diag") {
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
dumpMenuBarItems() dumpMenuBarItems()
@@ -82,9 +103,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
let name = "SimpleTopBar" let name = "SimpleTopBar"
let positionKey = "NSStatusItem Preferred Position \(name)" let positionKey = "NSStatusItem Preferred Position \(name)"
// Only seed the position on first run so the user's own drag wins and // Only seed the position on first run so the user's own drag wins and
// persists through the autosave name. // persists through the autosave name. Kept just below the hider's
if UserDefaults.standard.object(forKey: positionKey) == nil { // preferred position so the hamburger lands at the left edge of the
UserDefaults.standard.set(100.0, forKey: positionKey) // visible icons. `debug.hamburgerPos` overrides it.
if let override = UserDefaults.standard.object(forKey: "debug.hamburgerPos") as? Double {
UserDefaults.standard.set(override, forKey: positionKey)
} else if UserDefaults.standard.object(forKey: positionKey) == nil {
UserDefaults.standard.set(290.0, forKey: positionKey)
} }
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
@@ -103,7 +128,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
let popover = NSPopover() let popover = NSPopover()
popover.behavior = .transient popover.behavior = .transient
popover.animates = false popover.animates = false
popover.delegate = self
popover.contentViewController = NSHostingController( popover.contentViewController = NSHostingController(
rootView: TopBarView( rootView: TopBarView(
model: AppListModel.shared, model: AppListModel.shared,
@@ -112,19 +136,18 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
) )
) )
self.popover = popover self.popover = popover
}
// Reveal the hidden menu bar items while the panel is open so pressing a // Selecting a tray item must dismiss our panel and give up focus,
// tray icon opens that app's menu at its real, on-screen position. // otherwise the panel stays on top of the menu and our app stays
func popoverWillShow(_ notification: Notification) { // frontmost, which stops transient items (Spotlight's search field)
MenuBarHider.shared.suspend() // from appearing at all.
Task { @MainActor [weak self] in
MenuBarItemManager.shared.onWillPress = {
self?.popover?.performClose(nil)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
NSApp.deactivate()
}
} }
func popoverDidClose(_ notification: Notification) {
// Small delay so a menu just opened from a tray tile is fully on screen
// before the bar contracts again.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
MenuBarHider.shared.resume()
} }
} }
+88 -6
View File
@@ -9,6 +9,10 @@ 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
/// Frame in Accessibility coordinates (origin top-left of the main screen).
let frame: CGRect?
let element: AXUIElement let element: AXUIElement
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool { static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
@@ -28,6 +32,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() {
@@ -70,10 +77,11 @@ final class MenuBarItemManager: ObservableObject {
var seenTitles = Set<String>() var seenTitles = Set<String>()
for (index, child) in children.enumerated() { for (index, child) in children.enumerated() {
let position = pointAttribute(child, kAXPositionAttribute)
// Control Center reports every module it owns, including the // Control Center reports every module it owns, including the
// ones not in the menu bar, which are parked off-screen around // ones not in the menu bar, which are parked off-screen around
// y=982. Keep only items actually sitting in the bar. // y=982. Keep only items actually sitting in the bar.
if let position = pointAttribute(child, kAXPositionAttribute), position.y > 60 { if let position, position.y > 60 {
continue continue
} }
@@ -85,12 +93,22 @@ final class MenuBarItemManager: ObservableObject {
// "Control Center"; show each distinct title once per app. // "Control Center"; show each distinct title once per app.
if !seenTitles.insert(title).inserted { continue } if !seenTitles.insert(title).inserted { continue }
let size = sizeAttribute(child, kAXSizeAttribute)
let frame: CGRect?
if let position, let size {
frame = CGRect(origin: position, size: size)
} else {
frame = nil
}
found.append(MenuBarItem( found.append(MenuBarItem(
id: "\(pid)-\(index)", id: "\(pid)-\(index)",
pid: pid, pid: pid,
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),
frame: frame,
element: child element: child
)) ))
} }
@@ -103,16 +121,66 @@ final class MenuBarItemManager: ObservableObject {
} }
} }
/// Opens the real menu bar item by pressing its accessibility element, so /// Opens the real menu bar item, so its menu/popover appears.
/// its menu/popover appears at its true screen location. ///
@discardableResult /// A hidden item sits off-screen, and its menu would open off-screen too.
func press(_ item: MenuBarItem) -> Bool { /// So briefly reveal the items, let the layout settle, press the item, then
/// hide again shortly after. The reveal is momentary rather than lasting the
/// whole time the panel is open.
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 needsReveal = hider.isHiding
guard needsReveal else {
_ = performPress(item)
return
}
hider.suspend()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
_ = self.performPress(item)
// The menu is its own window, positioned at the item's on-screen
// spot, so the bar can be re-hidden shortly after.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) {
hider.resume()
}
}
}
private func performPress(_ item: MenuBarItem) -> Bool {
// Spotlight's status item accepts the accessibility press action but
// does nothing with it; it needs a genuine click.
if item.appName.lowercased().contains("spotlight"), let frame = item.frame {
postClick(at: CGPoint(x: frame.midX, y: frame.midY))
return true
}
if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success { if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success {
return true return true
} }
return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success
} }
/// Posts a real left click at a point in Accessibility/global coordinates.
private func postClick(at point: CGPoint) {
let source = CGEventSource(stateID: .hidSystemState)
let down = CGEvent(mouseEventSource: source, mouseType: .leftMouseDown, mouseCursorPosition: point, mouseButton: .left)
let up = CGEvent(mouseEventSource: source, mouseType: .leftMouseUp, mouseCursorPosition: point, mouseButton: .left)
down?.post(tap: .cghidEventTap)
up?.post(tap: .cghidEventTap)
}
// 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
@@ -194,6 +262,16 @@ final class MenuBarItemManager: ObservableObject {
guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil } guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil }
return point return point
} }
nonisolated static func sizeAttribute(_ element: AXUIElement, _ attribute: String) -> CGSize? {
var value: CFTypeRef?
guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
let value,
CFGetTypeID(value) == AXValueGetTypeID() else { return nil }
var size = CGSize.zero
guard AXValueGetValue(value as! AXValue, .cgSize, &size) else { return nil }
return size
}
} }
/// Best-effort menu bar item hiding. A zero-width spacer status item is created /// Best-effort menu bar item hiding. A zero-width spacer status item is created
@@ -235,10 +313,14 @@ final class MenuBarHider: ObservableObject {
// The boundary between "hidden" and "kept" falls just left of Wi-Fi at // The boundary between "hidden" and "kept" falls just left of Wi-Fi at
// ~250, so the other apps hide while Apple's Wi-Fi/Clock/Battery/Control // ~250, so the other apps hide while Apple's Wi-Fi/Clock/Battery/Control
// Center/Spotlight stay visible. `debug.spacerPos` overrides the seed. // Center/Spotlight stay visible. `debug.spacerPos` overrides the seed.
// The boundary sits just left of Now Playing, so every Control Center
// item stays visible while the apps hide. It must stay *above* the
// hamburger's preferred position (see AppDelegate) for the hamburger to
// remain on the visible side. `debug.spacerPos` overrides the seed.
if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double { if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double {
UserDefaults.standard.set(override, forKey: key) UserDefaults.standard.set(override, forKey: key)
} else if UserDefaults.standard.object(forKey: key) == nil { } else if UserDefaults.standard.object(forKey: key) == nil {
UserDefaults.standard.set(250.0, forKey: key) UserDefaults.standard.set(300.0, forKey: key)
} }
let item = NSStatusBar.system.statusItem(withLength: 0) let item = NSStatusBar.system.statusItem(withLength: 0)
item.autosaveName = name item.autosaveName = name
+32 -4
View File
@@ -1,5 +1,28 @@
import AppKit import AppKit
import ApplicationServices 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 /// 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 /// 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)") 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
@@ -80,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, frame: nil, 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, frame: nil, 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, frame: nil, 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")
@@ -94,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 {
+50 -24
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 {
@@ -115,7 +95,53 @@ struct TopBarView: View {
.controlSize(.mini) .controlSize(.mini)
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.top, 6) .padding(.top, 6)
.help("Best-effort: pushes other apps' icons off the menu bar so they only live here. Apple's Wi-Fi, Spotlight, Clock, Battery and Control Center stay visible; macOS won't let any third-party app move them.") .help("Best-effort: pushes other apps' icons off the menu bar so they only live here. Apple's system items (Wi-Fi, Now Playing, Clock, Battery, Control Center, Spotlight) stay visible; macOS won't let any third-party app move them.")
}
}
}
/// 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)
} }
} }
} }
+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.6}"
swift build -c release swift build -c release