diff --git a/AGENTS.md b/AGENTS.md index fe2058e..52647e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,12 +135,16 @@ preferred position controls where the hidden/kept boundary lands: 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 would open off-screen too, so press briefly - `suspend()`s the hider, waits ~0.18s for the layout to settle, performs the - accessibility action (`kAXPressAction`, then `kAXShowMenuAction`), and - `resume()`s ~1s later. The reveal is momentary, not for the whole time the - panel is open. macOS does not let us place or anchor another app's menu - anywhere else, so a momentary reveal is the only way to show it in view. + 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. diff --git a/Sources/SimpleTopBar/App.swift b/Sources/SimpleTopBar/App.swift index 4cdc96c..5131ba3 100644 --- a/Sources/SimpleTopBar/App.swift +++ b/Sources/SimpleTopBar/App.swift @@ -48,6 +48,27 @@ final class AppDelegate: NSObject, NSApplicationDelegate { setupStatusItem() 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") { DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { dumpMenuBarItems() diff --git a/Sources/SimpleTopBar/MenuBarItems.swift b/Sources/SimpleTopBar/MenuBarItems.swift index 2112207..cf460b6 100644 --- a/Sources/SimpleTopBar/MenuBarItems.swift +++ b/Sources/SimpleTopBar/MenuBarItems.swift @@ -117,10 +117,11 @@ final class MenuBarItemManager: ObservableObject { return } hider.suspend() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.18) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { _ = self.performPress(item) - // Let the menu finish appearing before the bar contracts again. - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + // 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() } } diff --git a/Sources/SimpleTopBar/SelfTest.swift b/Sources/SimpleTopBar/SelfTest.swift index f6cc91f..53dd532 100644 --- a/Sources/SimpleTopBar/SelfTest.swift +++ b/Sources/SimpleTopBar/SelfTest.swift @@ -1,5 +1,28 @@ import AppKit 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 /// live Accessibility view of the menu bar to /tmp so it can be inspected