chore: merge fix-ci into main

This commit is contained in:
Peter Steinberger 2025-11-24 19:01:29 +01:00
parent 5c5030f3bb
commit fd01c64840
44 changed files with 200 additions and 565 deletions

View File

@ -359,7 +359,15 @@ jobs:
- name: Build Peekaboo app (Xcode)
working-directory: Apps
run: |
/usr/bin/env -u DYLD_LIBRARY_PATH -u DYLD_FRAMEWORK_PATH -u DYLD_FALLBACK_FRAMEWORK_PATH \
/usr/bin/env \
-u DYLD_LIBRARY_PATH \
-u DYLD_FRAMEWORK_PATH \
-u DYLD_FALLBACK_FRAMEWORK_PATH \
-u DYLD_ROOT_PATH \
-u DYLD_INSERT_LIBRARIES \
-u DYLD_IMAGE_SUFFIX \
-u DYLD_VERSIONED_LIBRARY_PATH \
-u DYLD_VERSIONED_FRAMEWORK_PATH \
xcodebuild -workspace Peekaboo.xcworkspace \
-scheme Peekaboo \
-configuration Debug \
@ -370,7 +378,15 @@ jobs:
- name: Build Inspector app (Xcode)
working-directory: Apps/PeekabooInspector
run: |
/usr/bin/env -u DYLD_LIBRARY_PATH -u DYLD_FRAMEWORK_PATH -u DYLD_FALLBACK_FRAMEWORK_PATH \
/usr/bin/env \
-u DYLD_LIBRARY_PATH \
-u DYLD_FRAMEWORK_PATH \
-u DYLD_FALLBACK_FRAMEWORK_PATH \
-u DYLD_ROOT_PATH \
-u DYLD_INSERT_LIBRARIES \
-u DYLD_IMAGE_SUFFIX \
-u DYLD_VERSIONED_LIBRARY_PATH \
-u DYLD_VERSIONED_FRAMEWORK_PATH \
xcodebuild -project Inspector.xcodeproj \
-scheme Inspector \
-configuration Debug \

1
.gitignore vendored
View File

@ -182,7 +182,6 @@ docs/debug/*
# Build artifacts and derived data
.artifacts/
.derived-data/
.derivedData/
# Crush directory
.crush/

View File

@ -324,7 +324,8 @@ final class AgentChatUI {
MarkdownComponent(
text: text,
padding: .init(horizontal: 1, vertical: 0),
defaultTextStyle: .init(color: color))
defaultTextStyle: .init(color: color)
)
}
private func removeLoader() {
@ -556,11 +557,11 @@ final class AgentChatEventDelegate: AgentEventDelegate {
private func valuesEqual(_ lhs: Any, _ rhs: Any) -> Bool {
switch (lhs, rhs) {
case let (l as String, r as String): return l == r
case let (l as Int, r as Int): return l == r
case let (l as Double, r as Double): return l == r
case let (l as Bool, r as Bool): return l == r
default: return false
case let (l as String, r as String): l == r
case let (l as Int, r as Int): l == r
case let (l as Double, r as Double): l == r
case let (l as Bool, r as Bool): l == r
default: false
}
}

View File

@ -326,7 +326,6 @@ extension AgentCommand {
}
let runTask = Task { () throws -> AgentExecutionResult in
if let existingSessionId = startingSessionId {
let outputDelegate = self.makeDisplayDelegate(for: batchedInput)
let streamingDelegate = self.makeStreamingDelegate(using: outputDelegate)

View File

@ -3,9 +3,9 @@ import Darwin
import Dispatch
import Foundation
import Logging
import PeekabooAgentRuntime
import PeekabooCore
import PeekabooFoundation
import PeekabooAgentRuntime
import Spinner
import Tachikoma
import TachikomaMCP

View File

@ -141,7 +141,7 @@ extension AgentOutputDelegate {
return // no change; avoid spamming the log
}
let diffSummary = self.diffSummary(for: name, newArgs: args)
let (formatter, _ /* toolType */) = self.toolFormatter(for: name)
let (formatter, _ /* toolType */ ) = self.toolFormatter(for: name)
switch self.outputMode {
case .minimal:
@ -498,12 +498,12 @@ extension AgentOutputDelegate {
private func valuesEqual(_ lhs: Any, _ rhs: Any) -> Bool {
switch (lhs, rhs) {
case let (l as String, r as String): return l == r
case let (l as Int, r as Int): return l == r
case let (l as Double, r as Double): return l == r
case let (l as Bool, r as Bool): return l == r
case let (l as String, r as String): l == r
case let (l as Int, r as Int): l == r
case let (l as Double, r as Double): l == r
case let (l as Bool, r as Bool): l == r
default:
return false
false
}
}
@ -530,7 +530,8 @@ extension AgentOutputDelegate {
default:
if let data = try? JSONSerialization.data(withJSONObject: ["v": value], options: []),
let text = String(data: data, encoding: .utf8) {
return text.replacingOccurrences(of: "{\"v\":", with: "").trimmingCharacters(in: CharacterSet(charactersIn: "}"))
return text.replacingOccurrences(of: "{\"v\":", with: "")
.trimmingCharacters(in: CharacterSet(charactersIn: "}"))
}
return ""
}

View File

@ -587,8 +587,7 @@ enum MenuServiceBridge {
}
static func listMenuBarItems(menu: any MenuServiceProtocol, includeRaw: Bool = false) async throws
-> [MenuBarItemInfo]
{
-> [MenuBarItemInfo] {
try await Task { @MainActor in
try await menu.listMenuBarItems(includeRaw: includeRaw)
}.value

View File

@ -321,7 +321,10 @@ extension TypeCommand: CommanderBindableCommand {
if let delay: Int = try values.decodeOption("delay", as: Int.self) {
self.delay = delay
}
if let wpm: Int = try values.decodeOption("wordsPerMinute", as: Int.self) ?? values.decodeOption("wpm", as: Int.self) {
if let wpm: Int = try values.decodeOption("wordsPerMinute", as: Int.self) ?? values.decodeOption(
"wpm",
as: Int.self
) {
self.wordsPerMinute = wpm
}
if let profile = values.singleOption("profileOption") ?? values.singleOption("profile") {

View File

@ -60,6 +60,7 @@ struct CleanCommand: OutputFormattable, RuntimeOptionsConfigurable {
// to the parsed runtime options so flags like --json-output are visible.
return self.runtimeOptions.makeConfiguration()
}
var jsonOutput: Bool { self.configuration.jsonOutput }
@MainActor

View File

@ -20,7 +20,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
restore Restore a previously saved slot.
load Shortcut for set with --file-path.
""",
showHelpOnEmptyInvocation: true)
showHelpOnEmptyInvocation: true
)
}
}
@ -121,7 +122,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
size: result.data.count,
filePath: output,
slot: nil,
textPreview: result.textPreview)
textPreview: result.textPreview
)
self.output(payload) {
if let text = String(data: result.data, encoding: .utf8) {
@ -129,7 +131,9 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
} else if let output {
print("📋 Saved \(result.data.count) bytes (\(result.utiIdentifier)) to \(output)")
} else {
print("📋 Clipboard contains \(result.data.count) bytes of \(result.utiIdentifier); use --output to save.")
print(
"📋 Clipboard contains \(result.data.count) bytes of \(result.utiIdentifier); use --output to save."
)
}
}
}
@ -143,7 +147,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
size: result.data.count,
filePath: nil,
slot: nil,
textPreview: result.textPreview)
textPreview: result.textPreview
)
self.output(payload) {
print("✅ Set clipboard (\(result.utiIdentifier), \(result.data.count) bytes)")
@ -162,7 +167,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
size: result.data.count,
filePath: path,
slot: nil,
textPreview: result.textPreview)
textPreview: result.textPreview
)
self.output(payload) {
print("✅ Loaded \(result.data.count) bytes (\(result.utiIdentifier)) from \(path) into clipboard")
@ -171,7 +177,14 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
private func handleClear() {
self.services.clipboard.clear()
let payload = ClipboardCommandResult(action: "clear", uti: nil, size: nil, filePath: nil, slot: nil, textPreview: nil)
let payload = ClipboardCommandResult(
action: "clear",
uti: nil,
size: nil,
filePath: nil,
slot: nil,
textPreview: nil
)
self.output(payload) {
print("🧹 Cleared clipboard")
}
@ -180,7 +193,14 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
private func handleSave() throws {
let slotName = self.slot ?? "0"
try self.services.clipboard.save(slot: slotName)
let payload = ClipboardCommandResult(action: "save", uti: nil, size: nil, filePath: nil, slot: slotName, textPreview: nil)
let payload = ClipboardCommandResult(
action: "save",
uti: nil,
size: nil,
filePath: nil,
slot: slotName,
textPreview: nil
)
self.output(payload) {
print("💾 Saved clipboard to slot \"\(slotName)\"")
}
@ -195,7 +215,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
size: result.data.count,
filePath: nil,
slot: slotName,
textPreview: result.textPreview)
textPreview: result.textPreview
)
self.output(payload) {
print("♻️ Restored slot \"\(slotName)\" (\(result.utiIdentifier), \(result.data.count) bytes)")
}
@ -210,7 +231,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
ClipboardRepresentation(utiIdentifier: UTType.plainText.identifier, data: Data(text.utf8)),
],
alsoText: self.alsoText,
allowLarge: self.allowLarge)
allowLarge: self.allowLarge
)
}
if let path = overridePath ?? self.filePath ?? self.imagePath {
@ -220,7 +242,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
return ClipboardWriteRequest(
representations: [ClipboardRepresentation(utiIdentifier: uti.identifier, data: data)],
alsoText: self.alsoText,
allowLarge: self.allowLarge)
allowLarge: self.allowLarge
)
}
if let b64 = self.dataBase64, let utiId = self.uti {
@ -230,7 +253,8 @@ struct ClipboardCommand: OutputFormattable, RuntimeOptionsConfigurable {
return ClipboardWriteRequest(
representations: [ClipboardRepresentation(utiIdentifier: utiId, data: data)],
alsoText: self.alsoText,
allowLarge: self.allowLarge)
allowLarge: self.allowLarge
)
}
throw ValidationError("Provide --text, --file-path/--image-path, or --data-base64 with --uti")

View File

@ -1,6 +1,6 @@
import AXorcist
import CoreGraphics
import Commander
import CoreGraphics
import Foundation
import PeekabooCore
import PeekabooFoundation
@ -95,7 +95,8 @@ struct MenuBarCommand: ParsableCommand, OutputFormattable {
self.logger.debug("Listing menu bar items includeRawDebug=\(self.includeRawDebug)")
let menuBarItems = try await MenuServiceBridge.listMenuBarItems(
menu: self.services.menu,
includeRaw: self.includeRawDebug)
includeRaw: self.includeRawDebug
)
if self.jsonOutput {
let output = ListJSONOutput(
@ -106,18 +107,18 @@ struct MenuBarCommand: ParsableCommand, OutputFormattable {
raw_title: item.rawTitle,
bundle_id: item.bundleIdentifier,
owner_name: item.ownerName,
identifier: item.identifier,
ax_identifier: item.axIdentifier,
ax_description: item.axDescription,
raw_window_id: item.rawWindowID,
raw_window_layer: item.rawWindowLayer,
raw_owner_pid: item.rawOwnerPID,
raw_source: item.rawSource,
index: item.index,
isVisible: item.isVisible,
description: item.description
)
},
identifier: item.identifier,
ax_identifier: item.axIdentifier,
ax_description: item.axDescription,
raw_window_id: item.rawWindowID,
raw_window_layer: item.rawWindowLayer,
raw_owner_pid: item.rawOwnerPID,
raw_source: item.rawSource,
index: item.index,
isVisible: item.isVisible,
description: item.description
)
},
executionTime: Date().timeIntervalSince(startTime)
)
outputSuccessCodable(data: output, logger: self.outputLogger)

View File

@ -36,7 +36,7 @@ private enum VersionMetadata {
gitCommit: "unknown",
gitCommitDate: "unknown",
gitBranch: "unknown",
buildDate: iso8601Now()
buildDate: self.iso8601Now()
)
}
@ -51,7 +51,7 @@ private enum VersionMetadata {
let commit = info["PeekabooGitCommit"] as? String ?? "unknown"
let commitDate = info["PeekabooGitCommitDate"] as? String ?? "unknown"
let branch = info["PeekabooGitBranch"] as? String ?? "unknown"
let buildDate = info["PeekabooBuildDate"] as? String ?? iso8601Now()
let buildDate = info["PeekabooBuildDate"] as? String ?? self.iso8601Now()
return Values(
current: display,
@ -63,25 +63,25 @@ private enum VersionMetadata {
}
private static func valuesFromWorkingCopy() -> Values? {
let root = repositoryRoot()
let root = self.repositoryRoot()
guard FileManager.default.fileExists(atPath: root.path) else { return nil }
let versionString = workingCopyVersion(root: root) ?? "0.0.0"
var commit = git(["rev-parse", "--short", "HEAD"], root: root) ?? "unknown"
let diffStatus = git(["status", "--porcelain"], root: root) ?? ""
let versionString = self.workingCopyVersion(root: root) ?? "0.0.0"
var commit = self.git(["rev-parse", "--short", "HEAD"], root: root) ?? "unknown"
let diffStatus = self.git(["status", "--porcelain"], root: root) ?? ""
if !diffStatus.isEmpty {
commit += "-dirty"
}
let commitDate = git(["show", "-s", "--format=%ci", "HEAD"], root: root) ?? "unknown"
let branch = git(["rev-parse", "--abbrev-ref", "HEAD"], root: root) ?? "unknown"
let commitDate = self.git(["show", "-s", "--format=%ci", "HEAD"], root: root) ?? "unknown"
let branch = self.git(["rev-parse", "--abbrev-ref", "HEAD"], root: root) ?? "unknown"
return Values(
current: "Peekaboo \(versionString)",
gitCommit: commit,
gitCommitDate: commitDate,
gitBranch: branch,
buildDate: iso8601Now()
buildDate: self.iso8601Now()
)
}

View File

@ -1,6 +1,6 @@
import Commander
import Foundation
import PeekabooCore
import Commander
import Testing
@testable import PeekabooCLI

View File

@ -189,7 +189,7 @@ struct MenuCommandIntegrationTests {
extension MenuCommandIntegrationTests {
/// Trim any progress/preamble characters emitted by the test runner and decode from the first JSON token.
fileprivate func decodeJSON<T: Decodable>(_ type: T.Type, from output: String) throws -> T {
private func decodeJSON<T: Decodable>(_ type: T.Type, from output: String) throws -> T {
guard let start = output.firstIndex(where: { $0 == "{" || $0 == "[" }) else {
throw DecodingError.dataCorrupted(
.init(codingPath: [], debugDescription: "No JSON object found in output: \(output)")

View File

@ -1,5 +1,5 @@
import Foundation
import Darwin
import Foundation
import PeekabooCore
@testable import PeekabooCLI

View File

@ -798,7 +798,8 @@ final class StubClipboardService: ClipboardServiceProtocol {
let result = ClipboardReadResult(
utiIdentifier: primary.utiIdentifier,
data: primary.data,
textPreview: request.alsoText)
textPreview: request.alsoText
)
self.current = result
return result
}

View File

@ -79,7 +79,7 @@ struct TTYCommandRunner {
}
let waitDeadline = Date().addingTimeInterval(1.5)
while proc.isRunning, Date() < waitDeadline {
usleep(80_000)
usleep(80000)
}
if proc.isRunning {
if let pgid = processGroup {
@ -128,7 +128,7 @@ struct TTYCommandRunner {
if let byteDeadline = afterFirstByteDeadline, now >= byteDeadline { break }
if afterFirstByteDeadline == nil, now >= primaryDeadline { break }
usleep(60_000)
usleep(60000)
}
guard let text = String(data: buffer, encoding: .utf8), !text.isEmpty else {
@ -137,6 +137,7 @@ struct TTYCommandRunner {
return Result(text: text)
}
// swiftlint:enable cyclomatic_complexity function_body_length
static func which(_ tool: String) -> String? {
@ -166,7 +167,7 @@ struct TTYCommandRunner {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let path = String(data: data, encoding: .utf8)?
.trimmingCharacters(in: .whitespacesAndNewlines),
!path.isEmpty else { return nil }
!path.isEmpty else { return nil }
return path
}

View File

@ -26,7 +26,8 @@ struct TTYCommandRunnerTests {
let result = try runner.run(
binary: scriptURL.path,
send: "",
options: .init(rows: 5, cols: 40, timeout: 0.8, extraArgs: []))
options: .init(rows: 5, cols: 40, timeout: 0.8, extraArgs: [])
)
guard let childPID = Self.extractChildPID(result.text) else {
Issue.record("Did not capture child PID from PTY output. Output: \(result.text)")

View File

@ -1,4 +1,3 @@
import Combine
import OSLog
import SwiftUI

View File

@ -412,7 +412,7 @@ private struct ScrollAccessibilityConfigurator: NSViewRepresentable {
scrollView.contentView.setAccessibilityIdentifier("\(id)-clip")
scrollView.documentView?.setAccessibilityIdentifier("\(id)-content")
NSAccessibility.post(element: scrollView, notification: .layoutChanged)
NSAccessibility.post(notification: .layoutChanged, for: scrollView)
}
}
}

View File

@ -1,10 +1,5 @@
# Changelog
All notable changes to Peekaboo will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added

View File

@ -65,7 +65,7 @@ extension PeekabooAgentService {
// If queue mode is "all" and we have queued messages, inject them
// before the next turn so the model sees them together.
if queueMode == .all && !queuedMessages.isEmpty {
if queueMode == .all, !queuedMessages.isEmpty {
state.messages.append(contentsOf: queuedMessages)
queuedMessages.removeAll()
}
@ -332,7 +332,7 @@ extension PeekabooAgentService {
"(?i)bearer\\s+[a-z0-9._-]{8,}",
"(?i)api[_-]?key\\s*[:=]\\s*[a-z0-9._-]{6,}",
"(?i)sess[a-z0-9]{12,}",
"(?i)token\\s*[:=]\\s*[a-z0-9._-]{12,}"
"(?i)token\\s*[:=]\\s*[a-z0-9._-]{12,}",
]
var output = text

View File

@ -2,6 +2,5 @@
// either one at a time per turn, or all queued together before the next turn.
public enum QueueMode: String, Sendable {
case oneAtATime = "one-at-a-time"
case all = "all"
case all
}

View File

@ -97,7 +97,9 @@ public struct ClickTool: MCPTool {
return await UISessionManager.shared.getSession(id: sessionId)
}
return await UISessionManager.shared.getMostRecentSession()
// Get most recent session
// For now, return nil - in a real implementation we'd track the most recent session
return nil
}
private func resolveClickTarget(for request: ClickRequest) async throws -> ClickResolution {

View File

@ -36,7 +36,8 @@ public struct ClipboardTool: MCPTool {
"dataBase64": SchemaBuilder.string(description: "Base64-encoded data to copy"),
"uti": SchemaBuilder.string(description: "Uniform Type Identifier for dataBase64 or to force type"),
"prefer": SchemaBuilder.string(description: "Preferred UTI when reading clipboard"),
"outputPath": SchemaBuilder.string(description: "When reading, path to write binary data. Use '-' for stdout."),
"outputPath": SchemaBuilder
.string(description: "When reading, path to write binary data. Use '-' for stdout."),
"slot": SchemaBuilder.string(description: "Save/restore slot name (default: \"0\")"),
"alsoText": SchemaBuilder.string(description: "Optional plain text companion when setting binary data"),
"allowLarge": SchemaBuilder.boolean(description: "Allow writes larger than the 10 MB guard"),
@ -113,7 +114,7 @@ public struct ClipboardTool: MCPTool {
@MainActor
private func handleLoad(arguments: ToolArguments) throws -> ToolResponse {
// Alias for set; validation occurs in makeWriteRequest.
return try self.handleSet(arguments: arguments)
try self.handleSet(arguments: arguments)
}
@MainActor

View File

@ -205,7 +205,9 @@ public struct DragTool: MCPTool {
return await UISessionManager.shared.getSession(id: sessionId)
}
return await UISessionManager.shared.getMostRecentSession()
// Get most recent session
// For now, return nil - in a real implementation we'd track the most recent session
return nil
}
private func focusTargetAppIfNeeded(request: DragRequest) async throws {

View File

@ -311,7 +311,9 @@ public struct MoveTool: MCPTool {
return await UISessionManager.shared.getSession(id: sessionId)
}
return await UISessionManager.shared.getMostRecentSession()
// Get most recent session
// For now, return nil - in a real implementation we'd track the most recent session
return nil
}
private func resolveMovementParameters(for request: MoveRequest, distance: CGFloat) -> MovementParameters {

View File

@ -87,7 +87,9 @@ public struct ScrollTool: MCPTool {
return await UISessionManager.shared.getSession(id: sessionId)
}
return await UISessionManager.shared.getMostRecentSession()
// Get most recent session
// For now, return nil - in a real implementation we'd track the most recent session
return nil
}
private func parseRequest(arguments: ToolArguments) throws -> ScrollToolRequest {

View File

@ -617,10 +617,6 @@ actor UISession {
self.lastAccessedAt = Date()
}
func markAccessed() {
self.lastAccessedAt = Date()
}
func getElement(byId id: String) -> UIElement? {
self.uiElements.first { $0.id == id }
}
@ -647,29 +643,8 @@ actor UISessionManager {
return session
}
func getSession(id: String) async -> UISession? {
guard let session = self.sessions[id] else { return nil }
await session.markAccessed()
return session
}
func getMostRecentSession() async -> UISession? {
var newest: (session: UISession, lastAccessed: Date)?
for session in self.sessions.values {
let accessed = await session.lastAccessedAt
if let current = newest {
if accessed > current.lastAccessed {
newest = (session, accessed)
}
} else {
newest = (session, accessed)
}
}
guard let session = newest?.session else { return nil }
await session.markAccessed()
return session
func getSession(id: String) -> UISession? {
self.sessions[id]
}
func removeSession(id: String) {

View File

@ -81,7 +81,9 @@ public struct TypeTool: MCPTool {
return await UISessionManager.shared.getSession(id: sessionId)
}
return await UISessionManager.shared.getMostRecentSession()
// Get most recent session
// For now, return nil - in a real implementation we'd track the most recent session
return nil
}
private func parseRequest(arguments: ToolArguments) throws -> TypeRequest {

View File

@ -104,7 +104,8 @@ public final class ConfigurationManager: @unchecked Sendable {
public func migrateIfNeeded() throws {
// Allow tests or automation to disable migration to isolate temporary config roots.
if let disable = ProcessInfo.processInfo.environment["PEEKABOO_CONFIG_DISABLE_MIGRATION"],
disable.lowercased() == "1" || disable.lowercased() == "true" {
disable.lowercased() == "1" || disable.lowercased() == "true"
{
return
}

View File

@ -91,20 +91,24 @@ public final class ClipboardService: ClipboardServiceProtocol {
public func get(prefer uti: UTType?) throws -> ClipboardReadResult? {
guard let types = self.pasteboard.types, !types.isEmpty else { return nil }
let targetType: NSPasteboard.PasteboardType
if let uti, let preferred = types.first(where: { $0.rawValue == uti.identifier }) {
targetType = preferred
let targetType: NSPasteboard.PasteboardType = if let uti,
let preferred = types
.first(where: { $0.rawValue == uti.identifier })
{
preferred
} else if let stringType = types.first(where: { $0 == .string || $0 == .init("public.utf8-plain-text") }) {
targetType = stringType
stringType
} else {
targetType = types[0]
types[0]
}
let data: Data?
var textPreview: String?
if targetType == .string, let string = self.pasteboard.string(forType: .string) {
let normalized = string.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(of: "\r", with: "\n")
let normalized = string.replacingOccurrences(of: "\r\n", with: "\n").replacingOccurrences(
of: "\r",
with: "\n")
data = normalized.data(using: .utf8)
textPreview = Self.makePreview(normalized)
} else {
@ -147,15 +151,14 @@ public final class ClipboardService: ClipboardServiceProtocol {
}
let primary = request.representations.first!
let preview: String?
if let text = request.alsoText {
preview = Self.makePreview(text)
let preview: String? = if let text = request.alsoText {
Self.makePreview(text)
} else if primary.utiIdentifier == UTType.plainText.identifier,
let string = String(data: primary.data, encoding: .utf8)
{
preview = Self.makePreview(string)
Self.makePreview(string)
} else {
preview = nil
nil
}
return ClipboardReadResult(

View File

@ -8,12 +8,13 @@ import Foundation
// Option bits (mirrored from Ice)
private struct CGSWindowListOption: OptionSet {
let rawValue: UInt32
static let onScreen = CGSWindowListOption(rawValue: 1 << 0)
static let onScreen = CGSWindowListOption(rawValue: 1 << 0)
static let menuBarItems = CGSWindowListOption(rawValue: 1 << 1)
static let activeSpace = CGSWindowListOption(rawValue: 1 << 2)
static let activeSpace = CGSWindowListOption(rawValue: 1 << 2)
}
// MARK: - Dynamic loading helpers
// MARK: - Dynamic loading helpers
private func loadCGSHandle() -> UnsafeMutableRawPointer? {
@ -71,10 +72,14 @@ func cgsMenuBarWindowIDs(onScreen: Bool = false, activeSpace: Bool = false) -> [
func cgsProcessMenuBarWindowIDs(onScreenOnly: Bool = true) -> [CGWindowID] {
typealias CGSConnectionID = Int32
typealias CGSMainConnectionFunc = @convention(c) () -> CGSConnectionID
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>) -> Int32
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>)
-> Int32
typealias CGSGetProcessMenuBarWindowListFunc = @convention(c) (
CGSConnectionID, CGSConnectionID, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32
typealias CGSGetOnScreenWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>) -> Int32
typealias CGSGetOnScreenWindowCountFunc = @convention(c) (
CGSConnectionID,
CGSConnectionID,
UnsafeMutablePointer<Int32>) -> Int32
typealias CGSGetOnScreenWindowListFunc = @convention(c) (
CGSConnectionID, CGSConnectionID, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32
typealias CGSCopySpacesForWindowsFunc = @convention(c) (CGSConnectionID, UInt32, CFArray) -> Unmanaged<CFArray>?
@ -115,7 +120,7 @@ func cgsProcessMenuBarWindowIDs(onScreenOnly: Bool = true) -> [CGWindowID] {
// Active space filter to mirror Ice.
let activeSpace = getActiveSpace(mainConn())
ids = ids.filter { windowID in
guard let spaces = copySpaces(mainConn(), 1 << 0 /*includes current*/, [windowID] as CFArray)?
guard let spaces = copySpaces(mainConn(), 1 << 0 /* includes current */, [windowID] as CFArray)?
.takeRetainedValue() as? [UInt32]
else { return true }
return spaces.contains(activeSpace)
@ -142,7 +147,7 @@ private func cgsIsWindowOnActiveSpace(_ windowID: CGWindowID) -> Bool {
let cid = mainConn()
let activeSpace = getActiveSpace(cid)
guard let spaces = copySpaces(cid, 1 << 0 /*includes current*/, [windowID] as CFArray)?
guard let spaces = copySpaces(cid, 1 << 0 /* includes current */, [windowID] as CFArray)?
.takeRetainedValue() as? [UInt32]
else { return true }
return spaces.contains(activeSpace)

View File

@ -188,17 +188,13 @@ 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)
let combinedIDs = Array(Set(cgsIDs + legacyIDs))
self.logger.debug("CGS menuBarItems returned \(cgsIDs.count) ids; processMenuBar returned \(legacyIDs.count); combined \(combinedIDs.count)")
self.logger
.debug(
"CGS menuBarItems returned \(cgsIDs.count) ids; processMenuBar returned \(legacyIDs.count); combined \(combinedIDs.count)")
if !combinedIDs.isEmpty {
// Use CGWindow metadata per window ID to resolve owner/bundle.
for id in combinedIDs {
@ -228,47 +224,13 @@ 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 {
windowInfo = info
} else if let refreshed = CGWindowListCopyWindowInfo([.optionIncludingWindow], windowID) as? [[String: Any]],
let first = refreshed.first {
let first = refreshed.first
{
windowInfo = first
} else {
return nil
@ -370,7 +332,7 @@ extension MenuService {
let identifier = extra.identifier()
let hasIdentifier = identifier?.isEmpty == false
let hasNonPlaceholderTitle = !isPlaceholderMenuTitle(baseTitle)
if !hasIdentifier && !hasNonPlaceholderTitle {
if !hasIdentifier, !hasNonPlaceholderTitle {
continue
}
@ -442,8 +404,7 @@ extension MenuService {
.first(where: { !isPlaceholderMenuTitle($0) })
{
effectiveTitle = childDerived
}
else if let ident = sanitizedMenuText(extra.identifier()), !ident.isEmpty {
} else if let ident = sanitizedMenuText(extra.identifier()), !ident.isEmpty {
effectiveTitle = ident
}
}
@ -472,7 +433,7 @@ extension MenuService {
var results: [MenuExtraInfo] = []
let commonMenuTitles: Set<String> = [
"apple", "file", "edit", "view", "window", "help", "history", "bookmarks", "navigate", "tab", "tools",
"cut", "copy", "paste", "format"
"cut", "copy", "paste", "format",
]
func collectElements(from element: Element, depth: Int = 0, limit: Int = 4) -> [Element] {
@ -510,17 +471,18 @@ extension MenuService {
// Fallbacks to app name when placeholder/short/common menu words.
if isPlaceholderMenuTitle(effectiveTitle) ||
effectiveTitle.count <= 2 ||
commonMenuTitles.contains(effectiveTitle.lowercased()) {
commonMenuTitles.contains(effectiveTitle.lowercased())
{
effectiveTitle = app.localizedName ?? effectiveTitle
}
let position = extra.position() ?? .zero
// Restrict to top-of-screen positions to avoid stray elements.
if position != .zero && position.y > 100 { continue }
if position != .zero, position.y > 100 { continue }
// Avoid duplicating children of a status item: require that this element itself is status-like.
let childrenRoles = (extra.children() ?? []).compactMap { $0.role() }
if !isStatusLike && childrenRoles.contains(where: { $0 == "AXMenuItem" }) {
if !isStatusLike, childrenRoles.contains(where: { $0 == "AXMenuItem" }) {
continue
}
@ -547,9 +509,10 @@ extension MenuService {
/// Hit-test window extras to attach AX identifiers/titles when CGS gives only placeholders.
private func enrichWindowExtrasWithAXHitTest(_ extras: [MenuExtraInfo]) -> [MenuExtraInfo] {
return extras.map { extra in
guard extra.identifier == nil || isPlaceholderMenuTitle(extra.title) || isPlaceholderMenuTitle(extra.rawTitle),
extra.position != .zero
extras.map { extra in
guard extra
.identifier == nil || isPlaceholderMenuTitle(extra.title) || isPlaceholderMenuTitle(extra.rawTitle),
extra.position != .zero
else { return extra }
guard let hit = Element.elementAtPoint(extra.position) else {
@ -561,12 +524,12 @@ extension MenuService {
let isStatusLike = role == "AXStatusItem" || subrole == "AXStatusItem" || subrole == "AXMenuExtra"
if !isStatusLike { return extra }
let hitTitle = sanitizedMenuText(hit.identifier())
?? sanitizedMenuText(hit.help())
?? sanitizedMenuText(hit.title())
?? hit.descriptionText()
?? extra.title
?? extra.rawTitle
let hitTitle = sanitizedMenuText(hit.identifier())
?? sanitizedMenuText(hit.help())
?? sanitizedMenuText(hit.title())
?? hit.descriptionText()
?? extra.title
?? extra.rawTitle
let hitIdentifier = hit.identifier() ?? extra.identifier
return MenuExtraInfo(

View File

@ -1,5 +1,4 @@
import Foundation
import CoreGraphics
import MCP
import PeekabooFoundation
import TachikomaMCP
@ -194,7 +193,6 @@ private enum MCPToolTestHelpers {
private final class MockAutomationService: UIAutomationServiceProtocol {
private let accessibilityGranted: Bool
var lastCadence: TypingCadence?
var lastClickTarget: ClickTarget?
init(accessibilityGranted: Bool) {
self.accessibilityGranted = accessibilityGranted
@ -206,9 +204,7 @@ private final class MockAutomationService: UIAutomationServiceProtocol {
throw PeekabooError.notImplemented("mock detectElements")
}
func click(target: ClickTarget, clickType _: ClickType, sessionId _: String?) async throws {
self.lastClickTarget = target
}
func click(target _: ClickTarget, clickType _: ClickType, sessionId _: String?) async throws {}
func type(text _: String, target _: String?, clearExisting _: Bool, typingDelay _: Int, sessionId _: String?) async
throws {}
@ -468,130 +464,6 @@ struct MCPToolErrorHandlingTests {
Issue.record("Expected linear cadence, got \(cadence)")
}
}
@Test("Type tool falls back to latest session when session is omitted")
func typeToolUsesLatestSessionWhenMissingSessionId() async throws {
let automation = await MainActor.run { MockAutomationService(accessibilityGranted: true) }
try await MCPToolTestHelpers.withContext(automation: automation) {
// Older session that should NOT be picked
let oldSession = await UISessionManager.shared.createSession()
await oldSession.setUIElements([
UIElement(
id: "old",
elementId: "old",
role: "textField",
title: "Old",
label: "Old",
value: nil,
description: nil,
help: nil,
roleDescription: nil,
identifier: "old-id",
frame: CGRect(x: 0, y: 0, width: 10, height: 10),
isActionable: true),
])
// Ensure timestamps differ
try await Task.sleep(nanoseconds: 1_000_000)
// Newest session that should be selected by fallback
let newSession = await UISessionManager.shared.createSession()
let targetElement = UIElement(
id: "new",
elementId: "new",
role: "textField",
title: "Target",
label: "Target",
value: nil,
description: nil,
help: nil,
roleDescription: nil,
identifier: "target-field",
frame: CGRect(x: 50, y: 50, width: 20, height: 20),
isActionable: true)
await newSession.setUIElements([targetElement])
let tool = TypeTool()
let response = try await tool.execute(arguments: ToolArguments(raw: [
"on": targetElement.id,
"text": "hi",
]))
#expect(response.isError == false)
// Cleanup
await UISessionManager.shared.removeSession(id: oldSession.id)
await UISessionManager.shared.removeSession(id: newSession.id)
}
}
@Test("Click tool falls back to latest session when session is omitted")
func clickToolUsesLatestSessionWhenMissingSessionId() async throws {
let automation = await MainActor.run { MockAutomationService(accessibilityGranted: true) }
try await MCPToolTestHelpers.withContext(automation: automation) {
// Older session should be ignored
let oldSession = await UISessionManager.shared.createSession()
await oldSession.setUIElements([
UIElement(
id: "old",
elementId: "old",
role: "button",
title: "Old",
label: "Old",
value: nil,
description: nil,
help: nil,
roleDescription: nil,
identifier: "old-id",
frame: CGRect(x: 0, y: 0, width: 10, height: 10),
isActionable: true),
])
try await Task.sleep(nanoseconds: 1_000_000)
// Newest session should be used
let newSession = await UISessionManager.shared.createSession()
let targetElement = UIElement(
id: "new",
elementId: "new",
role: "button",
title: "Target",
label: "Target",
value: nil,
description: nil,
help: nil,
roleDescription: nil,
identifier: "target-button",
frame: CGRect(x: 100, y: 200, width: 40, height: 20),
isActionable: true)
await newSession.setUIElements([targetElement])
let tool = ClickTool()
let response = try await tool.execute(arguments: ToolArguments(raw: [
"on": targetElement.id,
]))
#expect(response.isError == false)
let lastClick = await MainActor.run { automation.lastClickTarget }
guard let clickTarget = lastClick else {
Issue.record("Expected automation click to be invoked")
return
}
switch clickTarget {
case let .coordinates(point):
#expect(Int(point.x) == 120)
#expect(Int(point.y) == 210)
default:
Issue.record("Expected coordinate click target")
}
await UISessionManager.shared.removeSession(id: oldSession.id)
await UISessionManager.shared.removeSession(id: newSession.id)
}
}
}
@Suite("MCP Tool Integration Tests", .tags(.integration))

View File

@ -1,24 +0,0 @@
<?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>

View File

@ -1,86 +0,0 @@
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)

View File

@ -1,24 +0,0 @@
<?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>

View File

@ -1,85 +0,0 @@
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)
}
}

View File

@ -9,7 +9,5 @@ let package = Package(
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.executableTarget(
name: "cgs-menu-probe"
),
]
)
name: "cgs-menu-probe"),
])

View File

@ -6,13 +6,14 @@ import Foundation
// Run as plain CLI; to test GUI privilege, re-run after wrapping in an LSUIElement app
// or via an inspector helper. Outputs counts from both private APIs and CGWindowList.
struct CGSMenuProbe {
enum CGSMenuProbe {
typealias CGSConnectionID = UInt32
typealias CGSMainConnectionFunc = @convention(c) () -> CGSConnectionID
typealias CGSCopyWindowsFunc = @convention(c) (CGSConnectionID, Int32, UInt32) -> CFArray?
typealias CGSGetProcessMenuBarWindowListFunc = @convention(c) (
CGSConnectionID, CGSConnectionID, Int32, UnsafeMutablePointer<CGWindowID>, UnsafeMutablePointer<Int32>) -> Int32
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>) -> Int32
typealias CGSGetWindowCountFunc = @convention(c) (CGSConnectionID, CGSConnectionID, UnsafeMutablePointer<Int32>)
-> Int32
private static func loadSymbol<T>(_ name: String, handle: UnsafeMutableRawPointer?) -> T? {
guard let sym = dlsym(handle, name) else { return nil }
@ -22,17 +23,21 @@ struct CGSMenuProbe {
static func run() {
let handles = [
"/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight",
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics"
"/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics",
]
var chosen: UnsafeMutableRawPointer?
for h in handles { if let ptr = dlopen(h, RTLD_NOW) { chosen = ptr; break } }
for h in handles {
if let ptr = dlopen(h, RTLD_NOW) { chosen = ptr; break }
}
guard let handle = chosen else { print("could not load CGS symbols"); return }
guard
let mainConn: CGSMainConnectionFunc = loadSymbol("CGSMainConnectionID", handle: handle),
let copyWindows: CGSCopyWindowsFunc = loadSymbol("CGSCopyWindowsWithOptions", handle: handle),
let getCount: CGSGetWindowCountFunc = loadSymbol("CGSGetWindowCount", handle: handle),
let getMenuBarList: CGSGetProcessMenuBarWindowListFunc = loadSymbol("CGSGetProcessMenuBarWindowList", handle: handle)
let getMenuBarList: CGSGetProcessMenuBarWindowListFunc = loadSymbol(
"CGSGetProcessMenuBarWindowList",
handle: handle)
else { print("missing symbols"); return }
let cid = mainConn()
@ -52,7 +57,9 @@ struct CGSMenuProbe {
let ids3 = Array(buf.prefix(Int(out)))
// Public CGWindowList fallback
let cgList = CGWindowListCopyWindowInfo([.optionAll, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
let cgList = CGWindowListCopyWindowInfo(
[.optionAll, .excludeDesktopElements],
kCGNullWindowID) as? [[String: Any]] ?? []
let layer25 = cgList.filter { ($0[kCGWindowLayer as String] as? Int) == 25 }
print("CGSCopyWindows menuBar: count=\(ids1.count) ids=\(ids1)")

View File

@ -273,7 +273,7 @@
{
"label": "Changelog",
"placement": "summary",
"command": "node -e \"const fs=require('fs');const md=fs.readFileSync('CHANGELOG.md','utf8').split(/\\\\r?\\\\n/);const start=md.findIndex((l)=>l.startsWith('## '));const end=md.findIndex((l,i)=>i>start&&l.startsWith('## '));const chunk=md.slice(start,end>0?end:md.length);const out=chunk.map((l)=>{if(/^#+\\\\s*/.test(l))return l.replace(/^#+\\\\s*/,'').toUpperCase();if(/^\\\\s*-\\\\s+/.test(l))return '• '+l.replace(/^\\\\s*-\\\\s+/,'');return l.trim();}).filter(Boolean);console.log(out.slice(0,50).join('\\\\n'));\"",
"command": "node -e \"const fs=require('fs');const lines=fs.readFileSync('CHANGELOG.md','utf8').split(/\\\\r?\\\\n/);const start=lines.findIndex((l)=>l.startsWith('## '));if(start<0){process.exit(0);}const end=lines.findIndex((l,i)=>i>start&&l.startsWith('## '));const slice=lines.slice(start,end>0?end:lines.length);const headingLine=slice[0]||'## Changelog';const heading=headingLine.replace(/^##\\\\s*/,'').trim();const bullets=slice.filter((l)=>l.trim().startsWith('- ')).length;console.log('@count: '+heading+' · '+bullets);console.log(slice.slice(0,50).join('\\\\n'));\"",
"refreshSeconds": 600,
"timeoutSeconds": 5,
"maxLines": 50,

View File

@ -1,21 +0,0 @@
#!/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"