From 3e57efe31245b790e28de3cbfd2b5936575d4e67 Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 27 Sep 2026 03:06:15 +0800 Subject: [PATCH] Hide menu bar icons with a cover instead of moving them off screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- AGENTS.md | 58 +++++------ README.md | 36 +++---- Sources/SimpleTopBar/App.swift | 20 ++-- Sources/SimpleTopBar/MenuBarItems.swift | 124 +++++++++--------------- Sources/SimpleTopBar/Overlay.swift | 124 ++++++++++++++++++++++++ Sources/SimpleTopBar/SelfTest.swift | 27 +++++- 6 files changed, 246 insertions(+), 143 deletions(-) create mode 100644 Sources/SimpleTopBar/Overlay.swift diff --git a/AGENTS.md b/AGENTS.md index 52647e1..3cc001f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -120,38 +120,32 @@ no control over this, so the app creates its own `NSStatusItem` and: `/tmp/simpletopbar-items.txt`; use it to verify the icon is not under the notch (x should be ~1200, not ~765). -## Hiding other menu bar items +## Hiding other menu bar items (cover) -`MenuBarHider` creates a zero-width spacer `NSStatusItem` at launch and grows its -length to `hiddenLength` (10_000) to push the icons on its left off-screen. It is -**on by default** (`hideMenuBarItems` defaults to true, then persisted). Its -preferred position controls where the hidden/kept boundary lands: +`MenuBarHider` drives `MenuBarCover`, which hides other apps' icons by drawing +our own frosted window ("cover") over them instead of moving them off-screen. +Because the icons keep their real positions, `press` opens their menus in place +with no reveal. Verified with `--diag-cover` (cover windows at level 26) and +`--diag-press` (the WireGuard menu opens at a positive x, not off-screen). -- Seed **250** (default) puts the boundary just left of Wi-Fi, so every - third-party app icon goes to x < 0 (cmux, Kapture, moomoo, WireGuard, - TextInputMenuAgent, Now Playing) while **Wi-Fi (x≈1173), Spotlight, Clock, - Battery, Control Center and our hamburger stay visible**. -- Verified with `--diag-hide` across positions 100–400. Below ~200 Wi-Fi gets - 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 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. -- **Wi-Fi/Spotlight/Clock/Battery/Control Center cannot be hidden**: macOS pins - them to the far right and a third-party item can never be inserted to their - right, so the spacer has nothing to push them with. This is a macOS - constraint, not a bug. +- `MenuBarCover.shouldHide` covers everything except the Apple keep-list + (Wi-Fi, Battery, Clock, Control Center, Spotlight/Search). +- `runs(from:)` merges adjacent item frames into contiguous cover windows; + `subtracting(_:from:)` carves out the hamburger so it is never covered + (`MenuBarItemManager.refresh` passes `avoid:` = our own item's frame). +- Frames come from AX (`kAXPositionAttribute` + `kAXSizeAttribute`) with a + **top-left origin**; `windowFrame(for:primaryHeight:)` flips Y for NSWindow. +- The cover window level is 26 (just above status items at 25, below menus at + ~101), so opened menus draw on top of it. +- `ignoresMouseEvents = false` makes the covered strip swallow clicks so the + hidden icons cannot be clicked through. + +### Legacy: off-screen spacer + +An earlier approach used a zero-width spacer `NSStatusItem` grown to width +10_000 to push icons off the left edge. It is no longer used because it required +a visible reveal to open a menu. Findings if it is ever revived: preferred +position ~250 kept Wi-Fi while hiding apps, and `debug.spacerPos` overrode it. ## Gotchas @@ -162,7 +156,7 @@ preferred position controls where the hidden/kept boundary lands: - Reading `activationPolicy` is reliable but must happen on the main thread; keep all `NSWorkspace` access inside the model. - The menu bar tray needs Accessibility permission; do not add Screen Recording - or Apple Events usage. `MenuBarHider` must always be restored on quit, but - because a status item dies with its process, a crash cleans up on its own. + or Apple Events usage. `MenuBarHider` is restored on quit, and because the + cover windows belong to the process, a crash removes them automatically. - `.gitignore` covers `.build/`, `SimpleTopBar.app/`, `dist/` — build artifacts should never be committed. diff --git a/README.md b/README.md index 31f07a6..99bb9e8 100644 --- a/README.md +++ b/README.md @@ -23,30 +23,24 @@ permission (System Settings → Privacy & Security → Accessibility). The panel prompts for it; without it the Menu Bar section is empty and everything else still works. -## Hiding menu bar items (best-effort) +## Hiding menu bar items -macOS has no public API to move another app's status item. The **Hide items from -the menu bar** toggle uses the spacer technique: a zero-width status item is -created at launch whose length is expanded to push the other icons off-screen so -they only live in this panel. It is **on by default** (the choice is -remembered), always undone on quit, and if the app dies macOS removes the spacer -automatically. +macOS has no public API to move another app's status item. Instead of moving +them, the **Hide items from the menu bar** toggle (on by default, remembered) +draws a frosted **cover** window over the other apps' icons. They keep their real +positions, so tapping a tile in the panel opens that app's menu in the right +place with **no flicker**, and the cover leaves a gap for the hamburger so it is +never hidden. -What it can and cannot hide: +What it does and does not cover: -- It hides **other third-party apps' icons** (cmux, Kapture, WireGuard, input - source, Now Playing, …) so only your apps' icons leave the bar. -- It **keeps Wi-Fi, Spotlight, Clock, Battery and Control Center**. The spacer's - boundary is tuned (preferred position 250) to fall just left of Wi-Fi, so the - Apple items on the right stay visible. macOS reserves that far-right area: a - third-party item can never be placed to the right of it, so those icons cannot - be pushed away even in principle. (Bartender and Ice hit the same wall and - cover the area with their own overlay bar instead.) -- Newly launched apps are placed to the left of the spacer, so they are hidden - automatically. -- Advanced: set `defaults write dev.tzgyn.simpletopbar debug.spacerPos -float ` - to move the boundary (larger = keep more icons; > ~400 starts leaking app - icons back into the bar). +- Covers **other third-party apps' icons** (cmux, Kapture, WireGuard, input + source, Now Playing, …). +- Keeps **Wi-Fi, Spotlight, Clock, Battery and Control Center** visible — macOS + reserves that far-right area, so those icons stay put. +- The cover matches the menu bar's frosted look only approximately; over an + unusual wallpaper the covered strip may be slightly visible. +- Turning the toggle off removes the cover immediately; quitting does too. ## Install with Homebrew diff --git a/Sources/SimpleTopBar/App.swift b/Sources/SimpleTopBar/App.swift index 5131ba3..0c841c8 100644 --- a/Sources/SimpleTopBar/App.swift +++ b/Sources/SimpleTopBar/App.swift @@ -43,27 +43,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate { AppListModel.shared.start() MenuBarItemManager.shared.start() - // Create the hamburger before the hider so it is placed in the visible - // part of the bar; the hider then contracts the space around it. + // Create the hamburger before the cover so its own icon is never covered. setupStatusItem() - MenuBarHider.shared.prepare() + MenuBarItemManager.shared.refresh() 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") + NSLog("diag-press: pressing \(item.appName)") _ = AXUIElementPerformAction(item.element, kAXPressAction as CFString) } } - DispatchQueue.main.asyncAfter(deadline: .now() + 2.45) { - MenuBarHider.shared.resume() + DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { + dumpWindows() } - DispatchQueue.main.asyncAfter(deadline: .now() + 4.5) { + return + } + + if CommandLine.arguments.contains("--diag-cover") { + DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { dumpWindows() } return diff --git a/Sources/SimpleTopBar/MenuBarItems.swift b/Sources/SimpleTopBar/MenuBarItems.swift index cf460b6..7de9502 100644 --- a/Sources/SimpleTopBar/MenuBarItems.swift +++ b/Sources/SimpleTopBar/MenuBarItems.swift @@ -9,6 +9,8 @@ struct MenuBarItem: Identifiable, Equatable { let appName: String let title: String let icon: NSImage + /// Frame in Accessibility coordinates (origin top-left of the main screen). + let frame: CGRect? let element: AXUIElement static func == (lhs: MenuBarItem, rhs: MenuBarItem) -> Bool { @@ -52,9 +54,15 @@ final class MenuBarItemManager: ObservableObject { authorized = AXIsProcessTrusted() guard authorized else { items = [] + MenuBarHider.shared.apply(items: [], avoid: nil) return } - items = Self.enumerate() + let ownPID = ProcessInfo.processInfo.processIdentifier + let all = Self.enumerate(includeOwn: true) + items = all.filter { $0.pid != ownPID } + // Our own hamburger must never be covered, even if it lands inside a run. + let avoid = all.first { $0.pid == ownPID }?.frame + MenuBarHider.shared.apply(items: items, avoid: avoid) } nonisolated static func enumerate(includeOwn: Bool = false) -> [MenuBarItem] { @@ -70,10 +78,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 } @@ -85,12 +94,21 @@ 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, appName: app.localizedName ?? app.bundleIdentifier ?? "Unknown", title: title, icon: Self.icon(forTitle: title, app: app), + frame: frame, element: child )) } @@ -103,28 +121,11 @@ final class MenuBarItemManager: ObservableObject { } } - /// Opens the real menu bar item, so its menu/popover appears. - /// - /// A hidden item sits off-screen, and its menu would open off-screen too. - /// 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. + /// Opens the real menu bar item, so its menu/popover appears at its own + /// on-screen position. With the cover approach the item never moves, so no + /// reveal is needed. func press(_ item: MenuBarItem) { - let hider = MenuBarHider.shared - let needsReveal = hider.isHiding - guard needsReveal else { - _ = performPress(item) - return - } - hider.suspend() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { - _ = 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) { - hider.resume() - } - } + _ = performPress(item) } private func performPress(_ item: MenuBarItem) -> Bool { @@ -215,20 +216,26 @@ 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 -/// 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. +/// Menu bar item hiding via a "cover": the icons stay exactly where macOS put +/// them and are hidden by our own frosted window drawn over them. Because they +/// never move, opening one's menu needs no reveal. On by default; the choice is +/// persisted. Always restored on quit. @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 @@ -236,64 +243,27 @@ final class MenuBarHider: ObservableObject { didSet { guard enabled != oldValue else { return } UserDefaults.standard.set(enabled, forKey: Self.defaultsKey) - apply(enabled) + MenuBarItemManager.shared.refresh() } } - 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. + // plus the Apple items macOS will not let us cover. 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) + /// Recompute the cover windows for the current menu bar items. `avoid` is + /// our own hamburger's frame, which the cover must never hide. + func apply(items: [MenuBarItem], avoid: CGRect?) { + isHiding = enabled + MenuBarCover.shared.update(enabled: enabled, items: items, avoid: avoid) } - private func apply(_ hiding: Bool) { - spacer?.length = hiding ? Self.hiddenLength : 0 - isHiding = hiding - } - - /// Temporarily reveal the items (e.g. while the panel is open) so pressing - /// a tray icon opens its menu at its real, on-screen position. Keeps the - /// user's `enabled` preference untouched. - func suspend() { - guard enabled else { return } - spacer?.length = 0 - isHiding = false - } - - /// Restore hiding after a `suspend()`. - func resume() { - guard enabled else { return } - spacer?.length = Self.hiddenLength - isHiding = true - } - - /// Put the menu bar back the way we found it. Called on quit. Leaves the - /// user's preference intact. + /// Remove the cover. Called on quit. Leaves the user's preference intact. func restore() { - spacer?.length = 0 isHiding = false + MenuBarCover.shared.update(enabled: false, items: []) } } diff --git a/Sources/SimpleTopBar/Overlay.swift b/Sources/SimpleTopBar/Overlay.swift new file mode 100644 index 0000000..2e98cbe --- /dev/null +++ b/Sources/SimpleTopBar/Overlay.swift @@ -0,0 +1,124 @@ +import AppKit + +/// Hides other apps' menu bar icons by drawing a frosted window ("cover") over +/// them, instead of moving them off-screen. The icons keep their real positions, +/// so their menus open in the right place with no reveal, and a target item's +/// real icon can be left visible by cutting a hole in the cover. +@MainActor +final class MenuBarCover { + static let shared = MenuBarCover() + + /// Just above the status items (level 25) but well below menus (~101). + private static let windowLevel = NSWindow.Level(rawValue: 26) + + private var windows: [NSWindow] = [] + + private init() {} + + func update(enabled: Bool, items: [MenuBarItem], avoid: CGRect? = nil) { + removeAllWindows() + guard enabled else { return } + + let frames = items.compactMap { item -> CGRect? in + guard Self.shouldHide(item), let frame = item.frame else { return nil } + return frame + } + var runs = Self.runs(from: frames) + if let avoid { + runs = Self.subtracting(avoid, from: runs) + } + let primaryHeight = NSScreen.screens.first?.frame.height ?? 0 + for run in runs { + let frame = Self.windowFrame(for: run, primaryHeight: primaryHeight) + windows.append(makeWindow(frame: frame)) + } + } + + /// Which items the cover hides. Everything is covered except the Apple + /// items the user asked to keep visible. + nonisolated static func shouldHide(_ item: MenuBarItem) -> Bool { + let text = item.title.lowercased() + .replacingOccurrences(of: "\u{2010}", with: "-") + .replacingOccurrences(of: "\u{2011}", with: "-") + let keep = ["wi-fi", "wifi", "battery", "clock", "control center", "search", "spotlight"] + return !keep.contains(where: { text.contains($0) }) + } + + /// Merge overlapping or near frames (in Accessibility coordinates) into + /// contiguous runs, so adjacent icons get one cover window rather than many. + nonisolated static func runs(from frames: [CGRect], gap: CGFloat = 6) -> [CGRect] { + let sorted = frames.sorted { $0.minX < $1.minX } + var result: [CGRect] = [] + for frame in sorted { + if let last = result.last, frame.minX <= last.maxX + gap { + result[result.count - 1] = last.union(frame) + } else { + result.append(frame) + } + } + return result + } + + /// Carve `avoid` out of the runs so the cover never hides our own icon. + nonisolated static func subtracting(_ avoid: CGRect, from runs: [CGRect]) -> [CGRect] { + var result: [CGRect] = [] + for run in runs { + guard run.intersects(avoid) else { + result.append(run) + continue + } + if avoid.minX > run.minX { + result.append(CGRect(x: run.minX, y: run.minY, width: avoid.minX - run.minX, height: run.height)) + } + if avoid.maxX < run.maxX { + result.append(CGRect(x: avoid.maxX, y: run.minY, width: run.maxX - avoid.maxX, height: run.height)) + } + } + return result + } + + /// Accessibility coordinates use a top-left origin; NSWindow uses a + /// bottom-left one, so flip Y against the primary screen's height. + nonisolated static func windowFrame(for axFrame: CGRect, primaryHeight: CGFloat) -> CGRect { + CGRect( + x: axFrame.minX, + y: primaryHeight - axFrame.maxY, + width: axFrame.width, + height: axFrame.height + ) + } + + private func makeWindow(frame: CGRect) -> NSWindow { + let window = NSWindow( + contentRect: frame, + styleMask: [.borderless], + backing: .buffered, + defer: false + ) + window.level = Self.windowLevel + window.isOpaque = false + window.backgroundColor = .clear + window.hasShadow = false + // Swallow clicks so the covered icons cannot be clicked through. + window.ignoresMouseEvents = false + window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary, .ignoresCycle] + window.animationBehavior = .none + + let effect = NSVisualEffectView(frame: CGRect(origin: .zero, size: frame.size)) + effect.material = .menu + effect.blendingMode = .behindWindow + effect.state = .active + effect.autoresizingMask = [.width, .height] + window.contentView = effect + + window.orderFrontRegardless() + return window + } + + private func removeAllWindows() { + for window in windows { + window.orderOut(nil) + } + windows.removeAll() + } +} diff --git a/Sources/SimpleTopBar/SelfTest.swift b/Sources/SimpleTopBar/SelfTest.swift index 53dd532..dab6205 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(), 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(), frame: nil, element: element) + let same = MenuBarItem(id: "1-0", pid: 1, appName: "A", title: "T", icon: NSImage(), frame: nil, element: element) + let different = MenuBarItem(id: "1-1", pid: 1, appName: "A", title: "T", icon: NSImage(), frame: nil, element: element) check(first == same, "menu bar items compare by id and title") check(first != different, "different ids are not equal") @@ -117,6 +117,27 @@ 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") + // Cover geometry: adjacent icons merge into one run, far ones stay separate. + let merged = MenuBarCover.runs(from: [CGRect(x: 0, y: 0, width: 10, height: 20), + CGRect(x: 12, y: 0, width: 10, height: 20)]) + check(merged == [CGRect(x: 0, y: 0, width: 22, height: 20)], "adjacent frames merge into one cover") + let split = MenuBarCover.runs(from: [CGRect(x: 0, y: 0, width: 10, height: 20), + CGRect(x: 100, y: 0, width: 10, height: 20)]) + check(split.count == 2, "distant frames stay separate covers") + + // Cover hides third-party apps but keeps the Apple items. + let hiddenApp = MenuBarItem(id: "1-0", pid: 1, appName: "cmux", title: "cmux", icon: NSImage(), frame: nil, element: element) + let keptWiFi = MenuBarItem(id: "2-0", pid: 2, appName: "Control Center", title: "Wi\u{2011}Fi, connected", icon: NSImage(), frame: nil, element: element) + let keptBattery = MenuBarItem(id: "2-1", pid: 2, appName: "Control Center", title: "Battery", icon: NSImage(), frame: nil, element: element) + let hiddenPlaying = MenuBarItem(id: "2-2", pid: 2, appName: "Control Center", title: "Now Playing", icon: NSImage(), frame: nil, element: element) + check(MenuBarCover.shouldHide(hiddenApp), "third-party apps are covered") + check(!MenuBarCover.shouldHide(keptWiFi), "wi-fi stays visible") + check(!MenuBarCover.shouldHide(keptBattery), "battery stays visible") + check(MenuBarCover.shouldHide(hiddenPlaying), "now playing is covered") + + let converted = MenuBarCover.windowFrame(for: CGRect(x: 10, y: 0, width: 30, height: 24), primaryHeight: 1000) + check(converted == CGRect(x: 10, y: 976, width: 30, height: 24), "AX frame flips Y for NSWindow") + let authorized = AXIsProcessTrusted() print("accessibility authorized=\(authorized)") if authorized {