menubar: add LSUIElement helper and invoke it before CGS fallbacks
This commit is contained in:
parent
4051bb69ff
commit
5c5030f3bb
@ -188,6 +188,12 @@ extension MenuService {
|
||||
private func getMenuBarItemsViaWindows() -> [MenuExtraInfo] {
|
||||
var items: [MenuExtraInfo] = []
|
||||
|
||||
// Preferred: call LSUIElement helper (AppKit context) to get WindowServer view like Ice.
|
||||
if let helperItems = self.getMenuBarItemsViaHelper(), !helperItems.isEmpty {
|
||||
self.logger.debug("MenuService helper returned \(helperItems.count) items")
|
||||
return helperItems
|
||||
}
|
||||
|
||||
// Preferred path: CGS menuBarItems window list (private API, mirrored from Ice).
|
||||
let cgsIDs = cgsMenuBarWindowIDs(onScreen: true, activeSpace: true)
|
||||
let legacyIDs = cgsProcessMenuBarWindowIDs(onScreenOnly: true)
|
||||
@ -222,6 +228,41 @@ extension MenuService {
|
||||
return items
|
||||
}
|
||||
|
||||
/// Invoke the LSUIElement helper (if built) to enumerate menu bar windows from a GUI context.
|
||||
private func getMenuBarItemsViaHelper() -> [MenuExtraInfo]? {
|
||||
let helperPath = "\(FileManager.default.currentDirectoryPath)/Helpers/MenuBarHelper/build/MenubarHelper.app/Contents/MacOS/menubar-helper"
|
||||
guard FileManager.default.isExecutableFile(atPath: helperPath) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let process = Process()
|
||||
process.launchPath = helperPath
|
||||
|
||||
let pipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
do {
|
||||
try process.run()
|
||||
} catch {
|
||||
self.logger.debug("Failed to run menubar helper: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
|
||||
process.waitUntilExit()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let windows = json["windows"] as? [[String: Any]]
|
||||
else { return nil }
|
||||
|
||||
var items: [MenuExtraInfo] = []
|
||||
for windowInfo in windows {
|
||||
guard let windowID = windowInfo["CGSWindowID"] as? UInt32 else { continue }
|
||||
if let item = self.makeMenuExtra(from: CGWindowID(windowID), info: windowInfo) {
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private func makeMenuExtra(from windowID: CGWindowID, info: [String: Any]? = nil) -> MenuExtraInfo? {
|
||||
let windowInfo: [String: Any]
|
||||
if let info {
|
||||
|
||||
24
Helpers/MenuBarHelper/Info.plist
Normal file
24
Helpers/MenuBarHelper/Info.plist
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>menubar-helper</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>boo.peekaboo.menubarhelper</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>MenuBar Helper</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
86
Helpers/MenuBarHelper/main.swift
Normal file
86
Helpers/MenuBarHelper/main.swift
Normal file
@ -0,0 +1,86 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
|
||||
// LSUIElement helper that enumerates menu bar windows via private CGS APIs and prints JSON.
|
||||
// Running inside AppKit provides the GUI WindowServer connection needed to see third-party extras.
|
||||
|
||||
private struct CGSWindowListOption: OptionSet {
|
||||
let rawValue: UInt32
|
||||
static let onScreen = CGSWindowListOption(rawValue: 1 << 0)
|
||||
static let menuBarItems = CGSWindowListOption(rawValue: 1 << 1)
|
||||
static let activeSpace = CGSWindowListOption(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
private func loadSymbol<T>(_ name: String, handle: UnsafeMutableRawPointer?) -> T? {
|
||||
guard let sym = dlsym(handle, name) else { return nil }
|
||||
return unsafeBitCast(sym, to: T.self)
|
||||
}
|
||||
|
||||
private func loadCGSHandle() -> UnsafeMutableRawPointer? {
|
||||
let handles = [
|
||||
"/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
|
||||
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics",
|
||||
]
|
||||
for path in handles {
|
||||
if let h = dlopen(path, RTLD_NOW) { return h }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func listMenuBarWindows() -> [[String: Any]] {
|
||||
guard let handle = loadCGSHandle(),
|
||||
let mainConnSym: @convention(c) () -> UInt32 = loadSymbol("CGSMainConnectionID", handle: handle),
|
||||
let copySym: @convention(c) (UInt32, Int32, UInt32) -> CFArray? =
|
||||
loadSymbol("CGSCopyWindowsWithOptions", handle: handle),
|
||||
let getCountSym: @convention(c) (UInt32, UInt32, UnsafeMutablePointer<Int32>) -> Int32 =
|
||||
loadSymbol("CGSGetWindowCount", handle: handle),
|
||||
let getMenuBarSym: @convention(c) (
|
||||
UInt32, UInt32, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32 =
|
||||
loadSymbol("CGSGetProcessMenuBarWindowList", handle: handle)
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
let cid = mainConnSym()
|
||||
|
||||
// Process-level list (Ice primary path).
|
||||
var total: Int32 = 0
|
||||
_ = getCountSym(cid, 0, &total)
|
||||
var buf = [CGWindowID](repeating: 0, count: Int(max(total, 32)))
|
||||
var out: Int32 = 0
|
||||
_ = getMenuBarSym(cid, 0, total, &buf, &out)
|
||||
let procIDs = Array(buf.prefix(Int(out)))
|
||||
|
||||
// Copy-with-options (sometimes returns extras).
|
||||
let opts: CGSWindowListOption = [.menuBarItems, .onScreen, .activeSpace]
|
||||
let copyIDs = (copySym(cid, 0, opts.rawValue) as? [UInt32]) ?? []
|
||||
|
||||
let ids = Array(Set(procIDs + copyIDs))
|
||||
guard !ids.isEmpty else { return [] }
|
||||
|
||||
let windowInfo = CGWindowListCopyWindowInfo(.optionAll, kCGNullWindowID) as? [[String: Any]] ?? []
|
||||
let dictByID: [CGWindowID: [String: Any]] = Dictionary(uniqueKeysWithValues: windowInfo.compactMap { info in
|
||||
guard let id = info[kCGWindowNumber as String] as? CGWindowID else { return nil }
|
||||
return (id, info)
|
||||
})
|
||||
|
||||
return ids.compactMap { id in
|
||||
var info = dictByID[id] ?? [:]
|
||||
info["CGSWindowID"] = id
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize AppKit to get a GUI connection (LSUIElement).
|
||||
NSApplication.shared
|
||||
|
||||
let windows = listMenuBarWindows()
|
||||
let payload: [String: Any] = ["windows": windows]
|
||||
if let data = try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted]) {
|
||||
FileHandle.standardOutput.write(data)
|
||||
} else {
|
||||
fputs("{\"error\":\"serialization_failed\"}", stdout)
|
||||
}
|
||||
fflush(stdout)
|
||||
exit(0)
|
||||
24
experiments/cgs-menu-probe-app/Contents/Info.plist
Normal file
24
experiments/cgs-menu-probe-app/Contents/Info.plist
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>cgs-menu-probe-app</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>boo.peekaboo.cgs-menu-probe</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>cgs-menu-probe-app</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSUIElement</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
experiments/cgs-menu-probe-app/Contents/MacOS/cgs-menu-probe-app
Executable file
BIN
experiments/cgs-menu-probe-app/Contents/MacOS/cgs-menu-probe-app
Executable file
Binary file not shown.
85
experiments/cgs-menu-probe-app/Contents/MacOS/main.swift
Normal file
85
experiments/cgs-menu-probe-app/Contents/MacOS/main.swift
Normal file
@ -0,0 +1,85 @@
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import AppKit
|
||||
import Darwin
|
||||
|
||||
// Dynamic loader mirroring Peekaboo CGS bridge
|
||||
private struct CGSWindowListOption: OptionSet {
|
||||
let rawValue: UInt32
|
||||
static let onScreen = CGSWindowListOption(rawValue: 1 << 0)
|
||||
static let menuBarItems = CGSWindowListOption(rawValue: 1 << 1)
|
||||
static let activeSpace = CGSWindowListOption(rawValue: 1 << 2)
|
||||
}
|
||||
|
||||
func loadHandle() -> UnsafeMutableRawPointer? {
|
||||
let candidates = [
|
||||
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics",
|
||||
"/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
|
||||
"/System/Library/PrivateFrameworks/SkyLight.framework/Versions/A/SkyLight",
|
||||
]
|
||||
for path in candidates {
|
||||
if let h = dlopen(path, RTLD_NOW) { return h }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cgsMenuBarWindowIDs() -> [UInt32] {
|
||||
guard let handle = loadHandle(),
|
||||
let mainSym = dlsym(handle, "CGSMainConnectionID"),
|
||||
let copySym = dlsym(handle, "CGSCopyWindowsWithOptions") else { return [] }
|
||||
|
||||
typealias MainConn = @convention(c) () -> UInt32
|
||||
typealias CopyWins = @convention(c) (UInt32, Int32, UInt32) -> CFArray?
|
||||
let mainConnection = unsafeBitCast(mainSym, to: MainConn.self)
|
||||
let copyWindows = unsafeBitCast(copySym, to: CopyWins.self)
|
||||
|
||||
let cid = mainConnection()
|
||||
let opts: CGSWindowListOption = [.menuBarItems, .onScreen, .activeSpace]
|
||||
if let arr = copyWindows(cid, 0, opts.rawValue) as? [UInt32] {
|
||||
return arr
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
func cgsProcessMenuBarIDs() -> [UInt32] {
|
||||
guard let handle = loadHandle(),
|
||||
let mainSym = dlsym(handle, "CGSMainConnectionID"),
|
||||
let countSym = dlsym(handle, "CGSGetWindowCount"),
|
||||
let listSym = dlsym(handle, "CGSGetProcessMenuBarWindowList") else { return [] }
|
||||
|
||||
typealias MainConn = @convention(c) () -> UInt32
|
||||
typealias GetCount = @convention(c) (UInt32, UInt32, UnsafeMutablePointer<Int32>) -> Int32
|
||||
typealias GetList = @convention(c) (UInt32, UInt32, Int32, UnsafeMutablePointer<UInt32>, UnsafeMutablePointer<Int32>) -> Int32
|
||||
|
||||
let mainConnection = unsafeBitCast(mainSym, to: MainConn.self)
|
||||
let getCount = unsafeBitCast(countSym, to: GetCount.self)
|
||||
let getList = unsafeBitCast(listSym, to: GetList.self)
|
||||
|
||||
let cid = mainConnection()
|
||||
var total: Int32 = 0
|
||||
_ = getCount(cid, 0, &total)
|
||||
var buf = [UInt32](repeating: 0, count: Int(max(total, 32)))
|
||||
var out: Int32 = 0
|
||||
_ = getList(cid, 0, total, &buf, &out)
|
||||
return Array(buf.prefix(Int(out)))
|
||||
}
|
||||
|
||||
func cgLayer25Count() -> Int {
|
||||
let cgList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
|
||||
return cgList.filter { ($0[kCGWindowLayer as String] as? Int) == 25 }.count
|
||||
}
|
||||
|
||||
@main
|
||||
struct ProbeApp {
|
||||
static func main() {
|
||||
NSApplication.shared // ensure AppKit init for LSUIElement context
|
||||
let idsCopy = cgsMenuBarWindowIDs()
|
||||
let idsProc = cgsProcessMenuBarIDs()
|
||||
let layer25 = cgLayer25Count()
|
||||
print("CGSCopy menuBar=\(idsCopy.count) ids=\(idsCopy)")
|
||||
print("CGSGetProcessMenuBarWindowList=\(idsProc.count) ids=\(idsProc)")
|
||||
print("CGWindowList layer25=\(layer25)")
|
||||
fflush(stdout)
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
21
scripts/build-menubar-helper.sh
Executable file
21
scripts/build-menubar-helper.sh
Executable file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HELPER_DIR="$ROOT_DIR/Helpers/MenuBarHelper"
|
||||
BUILD_DIR="$HELPER_DIR/build"
|
||||
APP_DIR="$BUILD_DIR/MenubarHelper.app"
|
||||
|
||||
rm -rf "$BUILD_DIR"
|
||||
mkdir -p "$APP_DIR/Contents/MacOS"
|
||||
|
||||
# Build the helper binary; allow undefined private symbols (resolved at runtime via dlopen).
|
||||
swiftc -O -framework AppKit \
|
||||
-Xlinker -undefined -Xlinker dynamic_lookup \
|
||||
"$HELPER_DIR/main.swift" \
|
||||
-o "$APP_DIR/Contents/MacOS/menubar-helper"
|
||||
|
||||
# Copy Info.plist to make it LSUIElement.
|
||||
cp "$HELPER_DIR/Info.plist" "$APP_DIR/Contents/Info.plist"
|
||||
|
||||
echo "Built helper at $APP_DIR"
|
||||
Loading…
Reference in New Issue
Block a user