import ApplicationServices
|
import CoreGraphics
|
import Foundation
|
|
struct PrivateWindowActivationOutcome {
|
let getProcessStatus: OSStatus
|
let setFrontError: CGError?
|
let makeKeyFirstError: CGError?
|
let makeKeySecondError: CGError?
|
|
var succeeded: Bool {
|
getProcessStatus == 0
|
&& (setFrontError == nil || setFrontError == .success)
|
&& (makeKeyFirstError == nil || makeKeyFirstError == .success)
|
&& (makeKeySecondError == nil || makeKeySecondError == .success)
|
}
|
}
|
|
final class PrivateWindowActivationBridge: @unchecked Sendable {
|
enum BridgeError: Error {
|
case frameworkUnavailable
|
case symbolUnavailable(String)
|
}
|
|
private typealias SLPSSetFrontProcessWithOptionsFunction = @convention(c) (
|
UnsafeMutablePointer<ProcessSerialNumber>,
|
CGWindowID,
|
UInt32
|
) -> CGError
|
private typealias SLPSPostEventRecordToFunction = @convention(c) (
|
UnsafeMutablePointer<ProcessSerialNumber>,
|
UnsafeMutablePointer<UInt8>
|
) -> CGError
|
|
static let shared = try? PrivateWindowActivationBridge()
|
|
private let handle: UnsafeMutableRawPointer?
|
private let setFrontProcessWithOptions: SLPSSetFrontProcessWithOptionsFunction
|
private let postEventRecordTo: SLPSPostEventRecordToFunction
|
|
private init() throws {
|
guard let handle = dlopen("/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight", RTLD_LAZY) else {
|
throw BridgeError.frameworkUnavailable
|
}
|
|
self.handle = handle
|
self.setFrontProcessWithOptions = try Self.symbol("_SLPSSetFrontProcessWithOptions", in: handle)
|
self.postEventRecordTo = try Self.symbol("SLPSPostEventRecordTo", in: handle)
|
}
|
|
deinit {
|
if let handle {
|
dlclose(handle)
|
}
|
}
|
|
func activate(processIdentifier: Int32, windowID: UInt32) -> PrivateWindowActivationOutcome {
|
var psn = ProcessSerialNumber()
|
let getProcessStatus = GetProcessForPID(pid_t(processIdentifier), &psn)
|
guard getProcessStatus == 0 else {
|
return PrivateWindowActivationOutcome(
|
getProcessStatus: getProcessStatus,
|
setFrontError: nil,
|
makeKeyFirstError: nil,
|
makeKeySecondError: nil
|
)
|
}
|
|
let cgWindowID = CGWindowID(windowID)
|
let setFrontError = setFrontProcessWithOptions(&psn, cgWindowID, 0x200)
|
let makeKeyErrors = makeKeyWindow(processSerialNumber: &psn, windowID: cgWindowID)
|
return PrivateWindowActivationOutcome(
|
getProcessStatus: getProcessStatus,
|
setFrontError: setFrontError,
|
makeKeyFirstError: makeKeyErrors.0,
|
makeKeySecondError: makeKeyErrors.1
|
)
|
}
|
|
private func makeKeyWindow(
|
processSerialNumber: inout ProcessSerialNumber,
|
windowID: CGWindowID
|
) -> (CGError, CGError) {
|
var mutableWindowID = windowID
|
var bytes = [UInt8](repeating: 0, count: 0xf8)
|
bytes[0x04] = 0xf8
|
bytes[0x3a] = 0x10
|
memcpy(&bytes[0x3c], &mutableWindowID, MemoryLayout<UInt32>.size)
|
memset(&bytes[0x20], 0xff, 0x10)
|
|
bytes[0x08] = 0x01
|
let first = bytes.withUnsafeMutableBufferPointer { buffer in
|
postEventRecordTo(&processSerialNumber, buffer.baseAddress!)
|
}
|
|
bytes[0x08] = 0x02
|
let second = bytes.withUnsafeMutableBufferPointer { buffer in
|
postEventRecordTo(&processSerialNumber, buffer.baseAddress!)
|
}
|
|
return (first, second)
|
}
|
|
private static func symbol<T>(_ name: String, in handle: UnsafeMutableRawPointer) throws -> T {
|
guard let rawSymbol = dlsym(handle, name) else {
|
throw BridgeError.symbolUnavailable(name)
|
}
|
|
return unsafeBitCast(rawSymbol, to: T.self)
|
}
|
}
|
|
@_silgen_name("GetProcessForPID") @discardableResult
|
private func GetProcessForPID(_ pid: pid_t, _ psn: UnsafeMutablePointer<ProcessSerialNumber>) -> OSStatus
|
|
enum PrivateAXWindowIDReader {
|
static func windowID(for window: AXUIElement) -> UInt32? {
|
var windowID = CGWindowID(0)
|
guard _AXUIElementGetWindow(window, &windowID) == .success, windowID != 0 else {
|
return nil
|
}
|
|
return UInt32(windowID)
|
}
|
}
|
|
@_silgen_name("_AXUIElementGetWindow") @discardableResult
|
private func _AXUIElementGetWindow(_ axUIElement: AXUIElement, _ windowID: UnsafeMutablePointer<CGWindowID>) -> AXError
|