import AppKit import ApplicationServices import Combine /// One live menu bar (status/tray) item discovered via the Accessibility API. struct MenuBarItem: Identifiable, Equatable { let id: String let pid: pid_t let appName: String let title: String let appIcon: NSImage let element: AXUIElement static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool { lhs.id == rhs.id && lhs.title == rhs.title } } /// Enumerates every other app's menu bar items so they can be shown (and /// pressed) from SimpleTopBar's hamburger panel. Requires Accessibility /// permission; without it the list is empty. @MainActor final class MenuBarItemManager: ObservableObject { static let shared = MenuBarItemManager() @Published private(set) var items: [MenuBarItem] = [] @Published private(set) var authorized = AXIsProcessTrusted() private var observers: [NSObjectProtocol] = [] private init() {} func start() { authorized = AXIsProcessTrusted() refresh() let center = NSWorkspace.shared.notificationCenter let names: [Notification.Name] = [ NSWorkspace.didLaunchApplicationNotification, NSWorkspace.didTerminateApplicationNotification, NSWorkspace.didActivateApplicationNotification, NSWorkspace.didHideApplicationNotification, NSWorkspace.didUnhideApplicationNotification, ] for name in names { observers.append(center.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in Task { @MainActor in self?.refresh() } }) } } func refresh() { authorized = AXIsProcessTrusted() guard authorized else { items = [] return } items = Self.enumerate() } nonisolated static func enumerate(includeOwn: Bool = false) -> [MenuBarItem] { let ownPID = ProcessInfo.processInfo.processIdentifier var found: [MenuBarItem] = [] for app in NSWorkspace.shared.runningApplications { let pid = app.processIdentifier if !includeOwn && pid == ownPID { continue } let axApp = AXUIElementCreateApplication(pid) guard let bar = extrasMenuBar(of: axApp), let children = children(of: bar) else { continue } var seenTitles = Set() for (index, child) in children.enumerated() { // 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 { continue } let title = stringAttribute(child, kAXDescriptionAttribute) ?? stringAttribute(child, kAXTitleAttribute) ?? app.localizedName ?? "Item" // Several Control Center modules share the generic title // "Control Center"; show each distinct title once per app. if !seenTitles.insert(title).inserted { continue } found.append(MenuBarItem( id: "\(pid)-\(index)", pid: pid, appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown", title: title, appIcon: app.icon ?? NSImage(named: NSImage.applicationIconName) ?? NSImage(), element: child )) } } return found.sorted { a, b in let byApp = a.appName.localizedCaseInsensitiveCompare(b.appName) if byApp == .orderedSame { return a.title.localizedCaseInsensitiveCompare(b.title) == .orderedAscending } return byApp == .orderedAscending } } /// Opens the real menu bar item by pressing its accessibility element, so /// its menu/popover appears at its true screen location. @discardableResult func press(_ item: MenuBarItem) -> Bool { if AXUIElementPerformAction(item.element, kAXPressAction as CFString) == .success { return true } return AXUIElementPerformAction(item.element, kAXShowMenuAction as CFString) == .success } // MARK: - AX helpers nonisolated static func extrasMenuBar(of app: AXUIElement) -> AXUIElement? { var value: CFTypeRef? guard AXUIElementCopyAttributeValue(app, kAXExtrasMenuBarAttribute as CFString, &value) == .success, let value, CFGetTypeID(value) == AXUIElementGetTypeID() else { return nil } return (value as! AXUIElement) } nonisolated static func children(of element: AXUIElement) -> [AXUIElement]? { var value: CFTypeRef? guard AXUIElementCopyAttributeValue(element, kAXChildrenAttribute as CFString, &value) == .success else { return nil } return value as? [AXUIElement] } nonisolated static func stringAttribute(_ element: AXUIElement, _ attribute: String) -> String? { var value: CFTypeRef? guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success, let string = value as? String, !string.isEmpty else { return nil } return string } nonisolated static func pointAttribute(_ element: AXUIElement, _ attribute: String) -> CGPoint? { var value: CFTypeRef? guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success, let value, CFGetTypeID(value) == AXValueGetTypeID() else { return nil } var point = CGPoint.zero guard AXValueGetValue(value as! AXValue, .cgPoint, &point) else { return nil } return point } } /// Best-effort menu bar item hiding. A zero-width spacer status item is created /// at launch; expanding its length to a huge value pushes the other apps' icons /// off the visible menu bar, so they can only be reached through SimpleTopBar's /// panel. On by default; the choice is persisted. Always restored on quit, and /// a crash cleans up automatically because status items die with the process. @MainActor final class MenuBarHider: ObservableObject { static let shared = MenuBarHider() /// Width that pushes the neighbouring items off the visible menu bar. static let hiddenLength: CGFloat = 10_000 private static let defaultsKey = "hideMenuBarItems" @Published private(set) var isHiding = false @Published var enabled: Bool { didSet { guard enabled != oldValue else { return } UserDefaults.standard.set(enabled, forKey: Self.defaultsKey) apply(enabled) } } private var spacer: NSStatusItem? private init() { // Hide by default: the menu bar then shows only SimpleTopBar's hamburger // plus the Apple items macOS will not let us move. enabled = UserDefaults.standard.object(forKey: Self.defaultsKey) as? Bool ?? true } /// Create the spacer early (at launch) so it lands as far right as possible. func prepare() { guard spacer == nil else { return } let name = "SimpleTopBarHider" let key = "NSStatusItem Preferred Position \(name)" // 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 // Center/Spotlight stay visible. `debug.spacerPos` overrides the seed. 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) } let item = NSStatusBar.system.statusItem(withLength: 0) item.autosaveName = name item.button?.isHidden = true spacer = item apply(enabled) } private func apply(_ hiding: Bool) { spacer?.length = hiding ? Self.hiddenLength : 0 isHiding = hiding } /// Put the menu bar back the way we found it. Called on quit. Leaves the /// user's preference intact. func restore() { spacer?.length = 0 isHiding = false } } enum AccessibilityPermission { static var isGranted: Bool { AXIsProcessTrusted() } static func request() { let options = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary _ = AXIsProcessTrustedWithOptions(options) } }