Files
steipete--codexbar/Sources/CodexBarCore/SessionWindowFocuser.swift
T
2026-07-13 12:22:33 +08:00

108 lines
4.4 KiB
Swift

#if os(macOS)
import AppKit
import ApplicationServices
import Foundation
public enum SessionFocusResult: Equatable, Sendable {
case focused
case activatedApplicationOnly
case failed
}
@MainActor
public enum SessionWindowFocuser {
private static let knownBundleIdentifiers: Set<String> = [
"com.mitchellh.ghostty",
"com.googlecode.iterm2",
"com.apple.Terminal",
"dev.warp.Warp-Stable",
"com.github.wez.wezterm",
"net.kovidgoyal.kitty",
"org.alacritty",
"com.microsoft.VSCode",
"com.todesktop.230313mzl4w4u92",
"dev.zed.Zed",
"com.anthropic.claudefordesktop",
]
@discardableResult
public static func focus(_ session: AgentSession, promptForAccessibility: Bool = true) -> SessionFocusResult {
guard let application = self.application(for: session) else { return .failed }
guard application.activate() else { return .failed }
let trusted = AXIsProcessTrustedWithOptions(
["AXTrustedCheckOptionPrompt": promptForAccessibility] as CFDictionary)
guard trusted else { return .activatedApplicationOnly }
let appElement = AXUIElementCreateApplication(application.processIdentifier)
var windowsValue: CFTypeRef?
guard AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsValue) == .success,
let windows = windowsValue as? [AXUIElement],
let window = self.preferredWindow(windows, session: session) ?? windows.first
else { return .activatedApplicationOnly }
AXUIElementPerformAction(window, kAXRaiseAction as CFString)
return .focused
}
private static func application(for session: AgentSession) -> NSRunningApplication? {
if let pid = session.pid {
var currentPID = pid
var fallback: NSRunningApplication?
var visited = Set<Int32>()
while currentPID > 0, visited.insert(currentPID).inserted {
if let application = NSRunningApplication(processIdentifier: currentPID) {
fallback = fallback ?? application
if let bundleIdentifier = application.bundleIdentifier,
self.knownBundleIdentifiers.contains(bundleIdentifier)
{
return application
}
}
guard let parent = self.parentPID(of: currentPID), parent != currentPID else { break }
currentPID = parent
}
if let fallback {
return fallback
}
}
let bundleIdentifier: String? = switch (session.provider, session.source) {
case (.claude, .desktopApp): "com.anthropic.claudefordesktop"
case (.codex, .desktopApp): "com.openai.codex"
default: nil
}
guard let bundleIdentifier else { return nil }
return NSRunningApplication.runningApplications(withBundleIdentifier: bundleIdentifier).first
}
private static func preferredWindow(_ windows: [AXUIElement], session: AgentSession) -> AXUIElement? {
let candidates = [session.projectName, session.cwd.map { URL(fileURLWithPath: $0).lastPathComponent }]
.compactMap { $0?.lowercased() }
.filter { !$0.isEmpty }
guard !candidates.isEmpty else { return nil }
return windows.first { window in
var titleValue: CFTypeRef?
guard AXUIElementCopyAttributeValue(window, kAXTitleAttribute as CFString, &titleValue) == .success,
let title = titleValue as? String
else { return false }
let lowercasedTitle = title.lowercased()
return candidates.contains { lowercasedTitle.contains($0) }
}
}
private static func parentPID(of pid: Int32) -> Int32? {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/ps")
process.arguments = ["-o", "ppid=", "-p", String(pid)]
let pipe = Pipe()
process.standardOutput = pipe
process.standardError = FileHandle.nullDevice
guard (try? process.run()) != nil else { return nil }
process.waitUntilExit()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
guard let output = String(data: data, encoding: .utf8) else { return nil }
return Int32(output.trimmingCharacters(in: .whitespacesAndNewlines))
}
}
#endif