141 lines
6.0 KiB
Swift
141 lines
6.0 KiB
Swift
import AppKit
|
|
import CoreGraphics
|
|
import ImageIO
|
|
import ScreenCaptureKit
|
|
import UniformTypeIdentifiers
|
|
|
|
struct WindowInfo {
|
|
let id: CGWindowID
|
|
let pid: pid_t
|
|
let app: String
|
|
let title: String
|
|
let frame: CGRect
|
|
}
|
|
|
|
func windowInfos(_ list: [[String: Any]]) -> [WindowInfo] {
|
|
return list.compactMap { w in
|
|
guard (w[kCGWindowLayer as String] as? Int) == 0,
|
|
let id = w[kCGWindowNumber as String] as? Int,
|
|
let pid = w[kCGWindowOwnerPID as String] as? Int,
|
|
let b = w[kCGWindowBounds as String] as? NSDictionary,
|
|
let frame = CGRect(dictionaryRepresentation: b),
|
|
frame.width > 1, frame.height > 1
|
|
else {
|
|
return nil
|
|
}
|
|
return WindowInfo(id: CGWindowID(id), pid: pid_t(pid), app: w[kCGWindowOwnerName as String] as? String ?? "", title: w[kCGWindowName as String] as? String ?? "", frame: frame)
|
|
}
|
|
}
|
|
|
|
func onScreenWindows() -> [WindowInfo] {
|
|
let list = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] ?? []
|
|
return windowInfos(list)
|
|
}
|
|
|
|
func window(_ id: Int) throws -> WindowInfo {
|
|
guard let wid = CGWindowID(exactly: id) else {
|
|
throw ToolError("no window \(id); screen_list shows the current ones")
|
|
}
|
|
let list = CGWindowListCopyWindowInfo(.optionIncludingWindow, wid) as? [[String: Any]] ?? []
|
|
guard let w = windowInfos(list).first else {
|
|
throw ToolError("no window \(id); screen_list shows the current ones")
|
|
}
|
|
return w
|
|
}
|
|
|
|
func describe(_ w: WindowInfo) -> String {
|
|
return "\(w.id) \(w.app) \(w.title.debugDescription) \(Int(w.frame.width))x\(Int(w.frame.height)) at \(Int(w.frame.minX)),\(Int(w.frame.minY))"
|
|
}
|
|
|
|
func screenList(_ args: [String: Any]) async throws -> [Content] {
|
|
var out = "apps (pid name bundle):\n"
|
|
for app in NSWorkspace.shared.runningApplications where app.activationPolicy == .regular {
|
|
out += "\(app.processIdentifier) \(app.localizedName ?? "") \(app.bundleIdentifier ?? "")\(app.isActive ? " active" : "")\(app.isHidden ? " hidden" : "")\n"
|
|
}
|
|
out += "\nwindows, front to back (id app title size at x,y):\n"
|
|
for w in onScreenWindows() {
|
|
out += describe(w) + "\n"
|
|
}
|
|
out += "\ndisplays (index size at x,y):\n"
|
|
for (i, s) in NSScreen.screens.enumerated() {
|
|
let id = s.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID ?? 0
|
|
let b = CGDisplayBounds(id)
|
|
out += "\(i) \(Int(b.width))x\(Int(b.height)) at \(Int(b.minX)),\(Int(b.minY))\(CGDisplayIsMain(id) != 0 ? " main" : "")\n"
|
|
}
|
|
return [.text(out)]
|
|
}
|
|
|
|
struct Shot {
|
|
let png: Data
|
|
let note: String
|
|
}
|
|
|
|
func capture(window id: Int?, display: Int?, crop: [String: Any]?, maxEdge: Double) async throws -> Shot {
|
|
if !maxEdge.isFinite || maxEdge < 16 || maxEdge > 8192 {
|
|
throw ToolError("max_edge must be between 16 and 8192 pixels")
|
|
}
|
|
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: false)
|
|
let filter: SCContentFilter
|
|
let size: CGSize
|
|
let what: String
|
|
var base = CGPoint.zero
|
|
if let id {
|
|
guard let w = content.windows.first(where: { Int($0.windowID) == id }), let app = w.owningApplication else {
|
|
throw ToolError("no window \(id) to capture; screen_list shows the current ones")
|
|
}
|
|
let center = CGPoint(x: w.frame.midX, y: w.frame.midY)
|
|
guard w.isOnScreen, let d = content.displays.first(where: { $0.frame.contains(center) }) else {
|
|
throw ToolError("window \(id) is not on screen")
|
|
}
|
|
filter = SCContentFilter(display: d, including: [app], exceptingWindows: [])
|
|
size = w.frame.size
|
|
base = CGPoint(x: w.frame.minX - d.frame.minX, y: w.frame.minY - d.frame.minY)
|
|
what = "window \(id)"
|
|
} else {
|
|
let screens = NSScreen.screens
|
|
let i = display ?? 0
|
|
guard i >= 0, i < screens.count,
|
|
let did = screens[i].deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID,
|
|
let d = content.displays.first(where: { $0.displayID == did })
|
|
else {
|
|
throw ToolError("no display \(i)")
|
|
}
|
|
filter = SCContentFilter(display: d, excludingWindows: [])
|
|
size = CGSize(width: d.width, height: d.height)
|
|
what = "display \(i)"
|
|
}
|
|
var src = CGRect(origin: .zero, size: size)
|
|
if let crop {
|
|
src = CGRect(x: crop.double("x") ?? 0, y: crop.double("y") ?? 0, width: crop.double("width") ?? size.width, height: crop.double("height") ?? size.height).intersection(src)
|
|
if src.isEmpty {
|
|
throw ToolError("crop is outside the \(Int(size.width))x\(Int(size.height)) \(what)")
|
|
}
|
|
}
|
|
let k = min(1, maxEdge / max(src.width, src.height))
|
|
let cfg = SCStreamConfiguration()
|
|
cfg.sourceRect = src.offsetBy(dx: base.x, dy: base.y)
|
|
cfg.width = max(1, Int((src.width * k).rounded()))
|
|
cfg.height = max(1, Int((src.height * k).rounded()))
|
|
cfg.showsCursor = false
|
|
let img = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: cfg)
|
|
let data = NSMutableData()
|
|
let dest = CGImageDestinationCreateWithData(data, UTType.png.identifier as CFString, 1, nil)!
|
|
CGImageDestinationAddImage(dest, img, nil)
|
|
CGImageDestinationFinalize(dest)
|
|
var note = "\(what), \(Int(size.width))x\(Int(size.height)) points; image \(img.width)x\(img.height)"
|
|
if k < 1 || src.origin != .zero {
|
|
note += String(format: "; \(what) point = image pixel / %.4f", k)
|
|
if src.origin != .zero {
|
|
note += " + (\(Int(src.minX)), \(Int(src.minY)))"
|
|
}
|
|
} else {
|
|
note += ", 1 pixel = 1 point"
|
|
}
|
|
return Shot(png: data as Data, note: note)
|
|
}
|
|
|
|
func captureTool(_ args: [String: Any]) async throws -> [Content] {
|
|
let shot = try await capture(window: args.int("window"), display: args.int("display"), crop: args.dict("crop"), maxEdge: args.double("max_edge") ?? 1568)
|
|
return [.text(shot.note), .png(shot.png)]
|
|
}
|