3 Commits
Author SHA1 Message Date
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
6 changed files with 70 additions and 10 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
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
+13
View File
@@ -132,6 +132,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
)
)
self.popover = popover
// Selecting a tray item must dismiss our panel and give up focus,
// otherwise the panel stays on top of the menu and our app stays
// frontmost, which stops transient items (Spotlight's search field)
// 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()
}
}
}
}
@objc private func togglePopover() {
+49 -5
View File
@@ -11,6 +11,8 @@ struct MenuBarItem: Identifiable, Equatable {
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
static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool {
@@ -30,6 +32,9 @@ final class MenuBarItemManager: ObservableObject {
private var observers: [NSObjectProtocol] = []
/// Called just before pressing an item so the owner can close its panel.
var onWillPress: (() -> Void)?
private init() {}
func start() {
@@ -72,10 +77,11 @@ final class MenuBarItemManager: ObservableObject {
var seenTitles = Set<String>()
for (index, child) in children.enumerated() {
let position = pointAttribute(child, kAXPositionAttribute)
// Control Center reports every module it owns, including the
// ones not in the menu bar, which are parked off-screen around
// 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
}
@@ -87,6 +93,14 @@ final class MenuBarItemManager: ObservableObject {
// "Control Center"; show each distinct title once per app.
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(
id: "\(pid)-\(index)",
pid: pid,
@@ -94,6 +108,7 @@ final class MenuBarItemManager: ObservableObject {
title: title,
icon: Self.icon(forTitle: title, app: app),
isSystem: Self.isSystemBundle(app.bundleIdentifier),
frame: frame,
element: child
))
}
@@ -113,6 +128,10 @@ final class MenuBarItemManager: ObservableObject {
/// 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 {
@@ -120,23 +139,38 @@ final class MenuBarItemManager: ObservableObject {
return
}
hider.suspend()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
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 almost immediately.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
// 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 {
return true
}
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
@@ -228,6 +262,16 @@ final class MenuBarItemManager: ObservableObject {
guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil }
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
@@ -272,7 +316,7 @@ final class MenuBarHider: ObservableObject {
if let override = UserDefaults.standard.object(forKey: "debug.spacerPos") as? Double {
UserDefaults.standard.set(override, forKey: key)
} else if UserDefaults.standard.object(forKey: key) == nil {
UserDefaults.standard.set(250.0, forKey: key)
UserDefaults.standard.set(350.0, forKey: key)
}
let item = NSStatusBar.system.statusItem(withLength: 0)
item.autosaveName = name
+3 -3
View File
@@ -103,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(), 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)
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(), isSystem: false, frame: nil, 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 != different, "different ids are not equal")
+1 -1
View File
@@ -95,7 +95,7 @@ struct TopBarView: View {
.controlSize(.mini)
.padding(.horizontal, 12)
.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.")
}
}
}
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")"
VERSION="${1:-0.4.2}"
VERSION="${1:-0.4.5}"
swift build -c release