Created
November 3, 2022 16:10
-
-
Save szhu/83caf8853979baa19797c3bb4bf59b8d to your computer and use it in GitHub Desktop.
Open 4 Stickies windows and drag them around to create a "hole".
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import AppKit | |
import ApplicationServices | |
import CoreFoundation | |
import Foundation | |
// Helpers | |
// https://github.com/keith/ModMove/blob/main/ModMove/AXValue%2BHelper.swift | |
extension AXValue { | |
func toValue<T>() -> T? { | |
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1) | |
let success = AXValueGetValue(self, AXValueGetType(self), pointer) | |
return success ? pointer.pointee : nil | |
} | |
static func from<T>(value: T, type: AXValueType) -> AXValue? { | |
let pointer = UnsafeMutablePointer<T>.allocate(capacity: 1) | |
pointer.pointee = value | |
return AXValueCreate(type, pointer) | |
} | |
} | |
func setWindowBounds(_ axWindow: AXUIElement, _ bounds: CGRect) { | |
let axOrigin = AXValue.from(value: bounds.origin, type: .cgPoint)! | |
AXUIElementSetAttributeValue(axWindow, kAXPositionAttribute as CFString, axOrigin) | |
let axSize = AXValue.from(value: bounds.size, type: .cgSize)! | |
AXUIElementSetAttributeValue(axWindow, kAXSizeAttribute as CFString, axSize) | |
} | |
func getWindowBounds(_ axWindow: AXUIElement) -> CGRect { | |
var axOrigin: AnyObject? | |
AXUIElementCopyAttributeValue(axWindow, kAXPositionAttribute as CFString, &axOrigin) | |
var origin = CGPoint.zero | |
AXValueGetValue(axOrigin as! AXValue, AXValueType(rawValue: kAXValueCGPointType)!, &origin) | |
var axSize: AnyObject? | |
AXUIElementCopyAttributeValue(axWindow, kAXSizeAttribute as CFString, &axSize) | |
var size = CGSize.zero | |
AXValueGetValue(axSize as! AXValue, AXValueType(rawValue: kAXValueCGSizeType)!, &size) | |
return CGRect(x: origin.x, y: origin.y, width: size.width, height: size.height) | |
} | |
func minIndexOfList(_ list: [CGFloat]) -> Int { | |
var minIndex = 0 | |
for i in 0 ..< list.count { | |
if list[i] < list[minIndex] { | |
minIndex = i | |
} | |
} | |
return minIndex | |
} | |
func maxIndexOfList(_ list: [CGFloat]) -> Int { | |
var maxIndex = 0 | |
for i in 0 ..< list.count { | |
if list[i] > list[maxIndex] { | |
maxIndex = i | |
} | |
} | |
return maxIndex | |
} | |
// | |
class Main: NSObject { | |
@objc | |
func loop() { | |
guard let screen = NSScreen.main else { | |
fatalError("No main screen") | |
} | |
let screenBounds = screen.frame | |
guard let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.Stickies").first else { | |
fatalError("Stickies not running") | |
} | |
let axApp = AXUIElementCreateApplication(runningApp.processIdentifier) | |
var value: AnyObject? | |
guard AXUIElementCopyAttributeValue(axApp, kAXWindowsAttribute as CFString, &value) == .success else { | |
fatalError("Could not get window list") | |
} | |
guard let windowList = value as? [AXUIElement] else { | |
fatalError("Could not get window list (incorrect type)") | |
} | |
func rect(_ left: CGFloat, _ top: CGFloat, _ right: CGFloat, _ bottom: CGFloat) -> CGRect { | |
return CGRect(x: left, y: top, width: right - left, height: bottom - top) | |
} | |
guard windowList.count >= 4 else { | |
fatalError("Not enough windows") | |
} | |
let windowBounds = [ | |
getWindowBounds(windowList[0]), | |
getWindowBounds(windowList[1]), | |
getWindowBounds(windowList[2]), | |
getWindowBounds(windowList[3]), | |
] | |
let iLeft = minIndexOfList(windowBounds.map { $0.maxX }) | |
let windowLeft = windowList[iLeft] | |
var holeLeft = windowBounds[iLeft].maxX | |
let iRight = maxIndexOfList(windowBounds.map { $0.minX }) | |
let windowRight = windowList[iRight] | |
var holeRight = windowBounds[iRight].minX | |
let iTop = minIndexOfList(windowBounds.map { $0.maxY }) | |
let windowTop = windowList[iTop] | |
var holeTop = windowBounds[iTop].maxY | |
let iBottom = maxIndexOfList(windowBounds.map { $0.minY }) | |
let windowBottom = windowList[iBottom] | |
var holeBottom = windowBounds[iBottom].minY | |
print("Hole (original): \(holeLeft), \(holeTop), \(holeRight), \(holeBottom)") | |
let screenLeft = screenBounds.minX | |
let screenTop = screenBounds.minY | |
let screenRight = screenBounds.maxX | |
let screenBottom = screenBounds.maxY | |
func bringWithinRange(_ value: CGFloat, _ min: CGFloat, _ max: CGFloat) -> CGFloat { | |
if value < min { | |
return min | |
} else if value > max { | |
return max | |
} else { | |
return value | |
} | |
} | |
holeLeft = bringWithinRange(holeLeft, screenLeft + 100, screenRight - 100) | |
holeRight = bringWithinRange(holeRight, screenLeft + 100, screenRight - 100) | |
if holeLeft >= holeRight { | |
holeLeft = holeRight | |
} | |
holeTop = bringWithinRange(holeTop, screenTop + 100, screenBottom - 100) | |
holeBottom = bringWithinRange(holeBottom, screenTop + 100, screenBottom - 100) | |
if holeTop >= holeBottom { | |
holeTop = holeBottom | |
} | |
print("Hole (adjusted): \(holeLeft), \(holeTop), \(holeRight), \(holeBottom)") | |
let targetBounds = [ | |
// Left | |
rect(screenLeft, screenTop, holeLeft, screenBottom), | |
// Top | |
rect(screenLeft, screenTop + 25, screenRight, holeTop), | |
// Right | |
rect(holeRight, screenTop, screenRight, screenBottom), | |
// Bottom | |
rect(screenLeft, holeBottom, screenRight, screenBottom), | |
] | |
setWindowBounds(windowLeft, targetBounds[0]) | |
setWindowBounds(windowTop, targetBounds[1]) | |
setWindowBounds(windowRight, targetBounds[2]) | |
setWindowBounds(windowBottom, targetBounds[3]) | |
// var i = 0 | |
// for window in windowList { | |
// if i >= targetBounds.count { | |
// break | |
// } | |
// print() | |
// print(getWindowBounds(window)) | |
// print(targetBounds[i]) | |
// setWindowBounds(window, targetBounds[i]) | |
// i += 1 | |
// } | |
} | |
} | |
let m = Main() | |
Timer.scheduledTimer(timeInterval: 0.3, target: m, selector: #selector(m.loop), userInfo: nil, repeats: true) | |
NSApplication.shared.run() | |
// https://stackoverflow.com/a/47489959/782045 | |
// let type = CGWindowListOption.optionOnScreenOnly | |
// let windowList = CGWindowListCopyWindowInfo(type, kCGNullWindowID) as NSArray? as? [[String: AnyObject]] | |
// for entry in windowList! { | |
// let owner = entry[kCGWindowOwnerName as String] as! String | |
// print(owner) | |
// // var bounds = entry[kCGWindowBounds as String] as? [String: Int] | |
// let pid = entry[kCGWindowOwnerPID as String] as? Int32 | |
// if owner == "Stickes" { | |
// let appRef = AXUIElementCreateApplication(pid!) // TopLevel Accessability Object of PID | |
// var value: AnyObject? | |
// _ = AXUIElementCopyAttributeValue(appRef, kAXWindowsAttribute as CFString, &value) | |
// if let windowList = value as? [AXUIElement] { print("windowList #\(windowList)") | |
// if let window = windowList.first { | |
// var newPoint = CGPoint(x: 0, y: 0) | |
// let position = AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &newPoint)! | |
// AXUIElementSetAttributeValue(window, kAXPositionAttribute as CFString, position) | |
// var newSize = CGSize(width: 400, height: 400) | |
// let size = AXValueCreate(AXValueType(rawValue: kAXValueCGSizeType)!, &newSize)! | |
// AXUIElementSetAttributeValue(window, kAXSizeAttribute as CFString, size) | |
// } | |
// } | |
// } | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment