import AppKit func osa(_ src: String) throws -> NSAppleEventDescriptor { return try DispatchQueue.main.sync { guard let script = NSAppleScript(source: "with timeout of 10 seconds\n\(src)\nend timeout") else { throw ToolError("AppleScript did not compile") } var err: NSDictionary? let r = script.executeAndReturnError(&err) if let err { throw ToolError("AppleScript: \(err[NSAppleScript.errorMessage] ?? err)") } return r } } func quote(_ s: String) -> String { return "\"" + s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"") + "\"" } func jsString(_ s: String) -> String { let d = try! JSONSerialization.data(withJSONObject: s, options: .fragmentsAllowed) return String(decoding: d, as: UTF8.self) } struct TabTarget { let window: Int let tab: Int? var spec: String { if let tab { return "tab \(tab) of window \(window)" } return "current tab of window \(window)" } } func js(_ code: String, _ t: TabTarget) throws -> String { let r = try osa("tell application \"Orion\" to do JavaScript \(quote(code)) in \(t.spec)") return r.stringValue ?? "undefined" } func orionWindow(_ index: Int) throws -> Int { return Int(try osa("tell application \"Orion\" to get id of window \(index)").int32Value) } func tabs() throws -> String { let src = """ tell application "Orion" set out to "" repeat with wi from 1 to count of windows try set w to window wi repeat with ti from 1 to count of tabs of w set t to tab ti of w set out to out & wi & " " & ti & " " & (name of t) & " | " & (URL of t) & linefeed end repeat end try end repeat return out end tell """ return "window tab title | url\n" + (try osa(src).stringValue ?? "") } func waitLoaded(_ t: TabTarget, timeout: Double) async throws -> String { let start = Date() await pause(300) while Date().timeIntervalSince(start) < timeout { if (try? js("document.readyState", t)) == "complete" { return String(format: "loaded after %.1fs", Date().timeIntervalSince(start)) } await pause(200) } return String(format: "still loading after %.1fs", timeout) } func browserTool(_ args: [String: Any]) async throws -> [Content] { let op = try args.require("op", args.string) let win = args.int("window") ?? 1 var target = TabTarget(window: win, tab: args.int("tab")) let lock = op == "tabs" && !args.bool("capture") ? nil : try await lockInput() defer { if let lock { close(lock) } } var log: [String] = [] switch op { case "tabs": log.append(try tabs()) case "navigate": let url = try args.require("url", args.string) if args.bool("new_tab") { _ = try osa("tell application \"Orion\" to tell window \(win) to set current tab to (make new tab with properties {URL:\(quote(url))})") target = TabTarget(window: win, tab: nil) } else { _ = try osa("tell application \"Orion\" to set URL of \(target.spec) to \(quote(url))") } log.append(try await waitLoaded(target, timeout: Double(try args.ms("timeout_ms") ?? 15000) / 1000)) log.append(try js("document.title + ' | ' + location.href", target)) case "js": log.append(try js(try args.require("code", args.string), target)) case "select": let tab = try args.require("tab", args.int) _ = try osa("tell application \"Orion\" to set current tab of window \(win) to tab \(tab) of window \(win)") log.append("selected tab \(tab)") target = TabTarget(window: win, tab: nil) case "close": let tab = try args.require("tab", args.int) _ = try osa("tell application \"Orion\" to close tab \(tab) of window \(win)") log.append("closed tab \(tab)") target = TabTarget(window: win, tab: nil) default: throw ToolError("unknown op \(op.debugDescription)") } let present: (String) async throws -> Bool = { needle in (try? js("String((document.title + '\\n' + (document.body ? document.body.innerText : '')).toLowerCase().includes(\(jsString(needle.lowercased()))))", target)) == "true" } if let note = try await waitFor(args, acted: false, present) { log.append(note) } var out: [Content] = [] if args.bool("capture") { if let tab = target.tab { _ = try osa("tell application \"Orion\" to set current tab of window \(win) to tab \(tab) of window \(win)") log.append("selected tab \(tab) to capture it") await pause(300) } let id = try orionWindow(win) let shot = try await capture(window: id, display: nil, crop: args.dict("crop"), maxEdge: args.double("max_edge") ?? 1568) log.append(shot.note) out.append(.png(shot.png)) } return [.text(log.joined(separator: "\n"))] + out }