FocusManager.swift
1 import AppKit 2 3 struct FocusManager { 4 /// Activates an app by name, optionally verifying focus. 5 static func focusApp(appName: String, windowTitle: String?, verify: Bool) -> (ActionResult?, ErrorInfo?) { 6 guard let app = findApp(named: appName) else { 7 return (nil, ErrorInfo(code: -1, message: "App '\(appName)' not found or not running")) 8 } 9 10 app.activate() 11 12 if verify { 13 // Brief wait for activation 14 Thread.sleep(forTimeInterval: 0.3) 15 guard let frontmost = NSWorkspace.shared.frontmostApplication, 16 frontmost.processIdentifier == app.processIdentifier else { 17 return (nil, ErrorInfo(code: -1, message: "Failed to bring '\(appName)' to front")) 18 } 19 } 20 21 let pid = Int(app.processIdentifier) 22 return (ActionResult(result: "focused \(appName) (pid \(pid))"), nil) 23 } 24 25 /// Returns the frontmost app's PID and window title. 26 static func frontmost() -> (ActionResult?, ErrorInfo?) { 27 guard let app = NSWorkspace.shared.frontmostApplication else { 28 return (nil, ErrorInfo(code: -1, message: "Cannot determine frontmost application")) 29 } 30 let name = app.localizedName ?? "Unknown" 31 let pid = Int(app.processIdentifier) 32 33 // Get window title via AX 34 let appRef = AXUIElementCreateApplication(Int32(pid)) 35 var windowTitle = "" 36 if let windows = axValue(appRef, "AXWindows") as? [AXUIElement], 37 let win = windows.first { 38 windowTitle = axString(win, "AXTitle") ?? "" 39 } 40 41 struct FrontmostResult: Encodable { 42 let app: String 43 let pid: Int 44 let window: String 45 } 46 // Return as simple action result with details 47 return (ActionResult(result: "\(name) (pid \(pid), window: \(windowTitle))"), nil) 48 } 49 50 /// Lists all windows for an app. 51 static func listWindows(pid: Int) -> [[String: String]] { 52 let appRef = AXUIElementCreateApplication(Int32(pid)) 53 guard let windows = axValue(appRef, "AXWindows") as? [AXUIElement] else { 54 return [] 55 } 56 var result: [[String: String]] = [] 57 for (i, win) in windows.enumerated() { 58 let title = axString(win, "AXTitle") ?? "" 59 let role = axString(win, "AXRole") ?? "" 60 result.append(["index": "\(i)", "title": title, "role": role]) 61 } 62 return result 63 } 64 65 private static func findApp(named name: String) -> NSRunningApplication? { 66 let lower = name.lowercased() 67 for app in NSWorkspace.shared.runningApplications { 68 if let n = app.localizedName, n.lowercased() == lower { 69 return app 70 } 71 if let bundleID = app.bundleIdentifier, bundleID.lowercased().contains(lower) { 72 return app 73 } 74 } 75 return nil 76 } 77 }