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
150 lines
8.3 KiB
Swift
150 lines
8.3 KiB
Swift
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
|
|
/// without a GUI, then exits.
|
|
func dumpMenuBarItems() {
|
|
var lines: [String] = []
|
|
lines.append("bundle=\(Bundle.main.bundleIdentifier ?? "?")")
|
|
lines.append("authorized=\(AXIsProcessTrusted())")
|
|
let items = MenuBarItemManager.enumerate()
|
|
lines.append("count=\(items.count)")
|
|
for item in items {
|
|
let pos = MenuBarItemManager.pointAttribute(item.element, kAXPositionAttribute)
|
|
lines.append("\(item.appName) | \(item.title) | pid=\(item.pid) | id=\(item.id) | pos=\(pos.map { "\(Int($0.x)),\(Int($0.y))" } ?? "?")")
|
|
}
|
|
let own = MenuBarItemManager.enumerate(includeOwn: true).filter {
|
|
$0.pid == ProcessInfo.processInfo.processIdentifier
|
|
}
|
|
lines.append("own-status-items=\(own.count)")
|
|
for item in own {
|
|
let pos = MenuBarItemManager.pointAttribute(item.element, kAXPositionAttribute)
|
|
lines.append("OWN \(item.appName) | \(item.title) | id=\(item.id) | pos=\(pos.map { "\(Int($0.x)),\(Int($0.y))" } ?? "?")")
|
|
}
|
|
let text = lines.joined(separator: "\n")
|
|
try? text.write(toFile: "/tmp/simpletopbar-items.txt", atomically: true, encoding: .utf8)
|
|
print(text)
|
|
exit(0)
|
|
}
|
|
|
|
/// Headless verification for `swift run SimpleTopBar --self-test`.
|
|
/// Exercises the background-app overflow helpers and reports the live split
|
|
/// between regular and background applications, then exits 0/1.
|
|
func runSelfTest() {
|
|
var failures = 0
|
|
|
|
func check(_ condition: Bool, _ label: String) {
|
|
print((condition ? "PASS" : "FAIL") + ": " + label)
|
|
if !condition { failures += 1 }
|
|
}
|
|
|
|
// clampedVisibleCount never exceeds the total and never goes negative.
|
|
check(AppListModel.clampedVisibleCount(current: 10, total: 3) == 3, "clamp caps at total")
|
|
check(AppListModel.clampedVisibleCount(current: -2, total: 5) == 0, "clamp floors at zero")
|
|
check(AppListModel.clampedVisibleCount(current: 2, total: 5) == 2, "clamp keeps valid value")
|
|
|
|
// nextVisibleCount pages by backgroundPageSize and stops at the total.
|
|
let page = AppListModel.backgroundPageSize
|
|
check(AppListModel.nextVisibleCount(current: page, total: 100) == page * 2, "show more pages forward")
|
|
check(AppListModel.nextVisibleCount(current: 4, total: 5) == 5, "show more stops at total")
|
|
check(AppListModel.nextVisibleCount(current: 5, total: 5) == 5, "show more is a no-op at the end")
|
|
|
|
// The section reveals exactly `min(visibleCount, total)` entries.
|
|
let apps = (0..<12).map { index in
|
|
RunningApp(
|
|
id: pid_t(index + 1),
|
|
name: "App \(index)",
|
|
bundleID: nil,
|
|
icon: NSImage(),
|
|
isHidden: false,
|
|
isActive: false
|
|
)
|
|
}
|
|
let hidden = max(0, apps.count - 3)
|
|
check(hidden == 9, "overflow count hides the tail")
|
|
|
|
// Live snapshot: the split must cover every running app exactly once.
|
|
let running = NSWorkspace.shared.runningApplications.filter {
|
|
$0.bundleIdentifier != Bundle.main.bundleIdentifier
|
|
}
|
|
let regular = running.filter { $0.activationPolicy == .regular }.count
|
|
let background = running.filter { $0.activationPolicy != .regular }.count
|
|
print("running=\(running.count) regular=\(regular) background=\(background)")
|
|
check(regular + background == running.count, "every running app is classified")
|
|
|
|
// Menu bar item discovery degrades gracefully when it cannot read another
|
|
// app's UI (accessibility not granted, or the element has no extras bar).
|
|
let systemWide = AXUIElementCreateSystemWide()
|
|
check(MenuBarItemManager.extrasMenuBar(of: systemWide) == nil, "no extras bar on a non-app element")
|
|
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(), 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")
|
|
|
|
// Control Center items map to distinct symbols instead of one shared icon.
|
|
check(MenuBarItemManager.symbolName(for: "Wi\u{2011}Fi, connected, 3 bars") == "wifi", "wi-fi maps to wifi symbol")
|
|
check(MenuBarItemManager.symbolName(for: "Battery") == "battery.100", "battery maps to battery symbol")
|
|
check(MenuBarItemManager.symbolName(for: "Now Playing") == "play.circle", "now playing maps to play symbol")
|
|
check(MenuBarItemManager.symbolName(for: "Control Center") == "switch.2", "control center maps to switch symbol")
|
|
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 {
|
|
print("menu bar items discovered=\(MenuBarItemManager.enumerate().count)")
|
|
}
|
|
|
|
print(failures == 0 ? "ALL PASS" : "\(failures) FAILURE(S)")
|
|
exit(failures == 0 ? 0 : 1)
|
|
}
|