diff --git a/Sources/SimpleTopBar/MenuBarItems.swift b/Sources/SimpleTopBar/MenuBarItems.swift index de7f7b6..29aea57 100644 --- a/Sources/SimpleTopBar/MenuBarItems.swift +++ b/Sources/SimpleTopBar/MenuBarItems.swift @@ -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 { @@ -75,10 +77,11 @@ final class MenuBarItemManager: ObservableObject { var seenTitles = Set() 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 } @@ -90,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, @@ -97,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 )) } @@ -138,12 +150,27 @@ final class MenuBarItemManager: ObservableObject { } 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 @@ -235,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 @@ -279,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 diff --git a/Sources/SimpleTopBar/SelfTest.swift b/Sources/SimpleTopBar/SelfTest.swift index e781077..3538acf 100644 --- a/Sources/SimpleTopBar/SelfTest.swift +++ b/Sources/SimpleTopBar/SelfTest.swift @@ -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") diff --git a/Sources/SimpleTopBar/TopBarView.swift b/Sources/SimpleTopBar/TopBarView.swift index 424ad3a..3175566 100644 --- a/Sources/SimpleTopBar/TopBarView.swift +++ b/Sources/SimpleTopBar/TopBarView.swift @@ -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.") } } } diff --git a/build-app.sh b/build-app.sh index 28f2c52..b154e76 100755 --- a/build-app.sh +++ b/build-app.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "$0")" -VERSION="${1:-0.4.4}" +VERSION="${1:-0.4.5}" swift build -c release