mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-02-28 06:22:45 -08:00
166 lines
5.0 KiB
Swift
166 lines
5.0 KiB
Swift
#!/usr/bin/env swift
|
|
//
|
|
// get-mpv-window-macos.swift
|
|
// SubMiner - Get mpv window geometry on macOS
|
|
//
|
|
// This script uses Core Graphics APIs to find mpv windows system-wide.
|
|
// It works with both bundled and unbundled mpv installations.
|
|
//
|
|
// Usage: swift get-mpv-window-macos.swift
|
|
// Output: "x,y,width,height" or "not-found"
|
|
//
|
|
|
|
import Cocoa
|
|
|
|
private struct WindowGeometry {
|
|
let x: Int
|
|
let y: Int
|
|
let width: Int
|
|
let height: Int
|
|
}
|
|
|
|
private func geometryFromRect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> WindowGeometry {
|
|
let minX = Int(floor(x))
|
|
let minY = Int(floor(y))
|
|
let maxX = Int(ceil(x + width))
|
|
let maxY = Int(ceil(y + height))
|
|
return WindowGeometry(
|
|
x: minX,
|
|
y: minY,
|
|
width: max(0, maxX - minX),
|
|
height: max(0, maxY - minY)
|
|
)
|
|
}
|
|
|
|
private func normalizedMpvName(_ name: String) -> Bool {
|
|
let normalized = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
|
|
return normalized == "mpv"
|
|
}
|
|
|
|
private func geometryFromAXWindow(_ axWindow: AXUIElement) -> WindowGeometry? {
|
|
var positionRef: CFTypeRef?
|
|
var sizeRef: CFTypeRef?
|
|
|
|
let positionStatus = AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &positionRef)
|
|
let sizeStatus = AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &sizeRef)
|
|
|
|
guard positionStatus == .success,
|
|
sizeStatus == .success,
|
|
let positionRaw = positionRef,
|
|
let sizeRaw = sizeRef,
|
|
CFGetTypeID(positionRaw) == AXValueGetTypeID(),
|
|
CFGetTypeID(sizeRaw) == AXValueGetTypeID() else {
|
|
return nil
|
|
}
|
|
|
|
let positionValue = positionRaw as! AXValue
|
|
let sizeValue = sizeRaw as! AXValue
|
|
|
|
guard AXValueGetType(positionValue) == .cgPoint,
|
|
AXValueGetType(sizeValue) == .cgSize else {
|
|
return nil
|
|
}
|
|
|
|
var position = CGPoint.zero
|
|
var size = CGSize.zero
|
|
|
|
guard AXValueGetValue(positionValue, .cgPoint, &position),
|
|
AXValueGetValue(sizeValue, .cgSize, &size) else {
|
|
return nil
|
|
}
|
|
|
|
let geometry = geometryFromRect(
|
|
x: position.x,
|
|
y: position.y,
|
|
width: size.width,
|
|
height: size.height
|
|
)
|
|
|
|
guard geometry.width >= 100, geometry.height >= 100 else {
|
|
return nil
|
|
}
|
|
|
|
return geometry
|
|
}
|
|
|
|
private func geometryFromAccessibilityAPI() -> WindowGeometry? {
|
|
let runningApps = NSWorkspace.shared.runningApplications.filter { app in
|
|
guard let name = app.localizedName else {
|
|
return false
|
|
}
|
|
return normalizedMpvName(name)
|
|
}
|
|
|
|
for app in runningApps {
|
|
let appElement = AXUIElementCreateApplication(app.processIdentifier)
|
|
var windowsRef: CFTypeRef?
|
|
let status = AXUIElementCopyAttributeValue(appElement, kAXWindowsAttribute as CFString, &windowsRef)
|
|
guard status == .success, let windows = windowsRef as? [AXUIElement], !windows.isEmpty else {
|
|
continue
|
|
}
|
|
|
|
for window in windows {
|
|
var minimizedRef: CFTypeRef?
|
|
let minimizedStatus = AXUIElementCopyAttributeValue(window, kAXMinimizedAttribute as CFString, &minimizedRef)
|
|
if minimizedStatus == .success, let minimized = minimizedRef as? Bool, minimized {
|
|
continue
|
|
}
|
|
|
|
if let geometry = geometryFromAXWindow(window) {
|
|
return geometry
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
private func geometryFromCoreGraphics() -> WindowGeometry? {
|
|
// Keep the CG fallback for environments without Accessibility permissions.
|
|
// Use on-screen layer-0 windows to avoid off-screen helpers/shadows.
|
|
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
|
let windowList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? []
|
|
|
|
for window in windowList {
|
|
guard let ownerName = window[kCGWindowOwnerName as String] as? String,
|
|
normalizedMpvName(ownerName) else {
|
|
continue
|
|
}
|
|
|
|
if let layer = window[kCGWindowLayer as String] as? Int, layer != 0 {
|
|
continue
|
|
}
|
|
if let alpha = window[kCGWindowAlpha as String] as? Double, alpha <= 0.01 {
|
|
continue
|
|
}
|
|
if let onScreen = window[kCGWindowIsOnscreen as String] as? Int, onScreen == 0 {
|
|
continue
|
|
}
|
|
|
|
guard let bounds = window[kCGWindowBounds as String] as? [String: CGFloat] else {
|
|
continue
|
|
}
|
|
|
|
let geometry = geometryFromRect(
|
|
x: bounds["X"] ?? 0,
|
|
y: bounds["Y"] ?? 0,
|
|
width: bounds["Width"] ?? 0,
|
|
height: bounds["Height"] ?? 0
|
|
)
|
|
|
|
guard geometry.width >= 100, geometry.height >= 100 else {
|
|
continue
|
|
}
|
|
|
|
return geometry
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if let window = geometryFromAccessibilityAPI() ?? geometryFromCoreGraphics() {
|
|
print("\(window.x),\(window.y),\(window.width),\(window.height)")
|
|
} else {
|
|
print("not-found")
|
|
}
|