Skip to content

Instantly share code, notes, and snippets.

@MaximBazarov
Forked from ihnorton/bouncywin.swift
Created December 7, 2019 15:22
Show Gist options
  • Save MaximBazarov/49784e65dd8beea9e8b5a0f8198f6f7c to your computer and use it in GitHub Desktop.
Save MaximBazarov/49784e65dd8beea9e8b5a0f8198f6f7c to your computer and use it in GitHub Desktop.
Bouncy window manager "solution"
import Cocoa
import Foundation
import AppKit
import CoreFoundation
import ApplicationServices
/// https://jvns.ca/blog/2019/11/25/challenge--make-a-bouncy-window-manager/
/*
This code runs the following sequence:
- create a callback for a mouseclick
- callback figures out the underlying pid for the application of the clicked window
- callback starts bouncy_window loop which does the actual window movement until
the program is stopped.
*/
let speed = 1.5
/// place-holder to save the eventTap so we can cancel it inside the callback
var taps: [CFMachPort] = []
func start() {
let userinfo_ptr: UnsafeMutableRawPointer = UnsafeMutableRawPointer.allocate(byteCount: MemoryLayout.size(ofValue: pid_t()),
alignment: 0)
/// based on https://stackoverflow.com/a/31898592/508431
let eventMask = (1 << CGEventType.leftMouseDown.rawValue)
guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(eventMask),
callback: mouseCallback,
userInfo: userinfo_ptr) else {
print("failed to create event tap")
exit(1)
}
let runLoop = CFRunLoopGetCurrent()
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(runLoop, runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
// save a reference to the eventTap for cancelation in the callback
taps.insert(eventTap, at: 0)
}
// callback to get pid from mouse lick
func mouseCallback(proxy: CGEventTapProxy,
type: CGEventType,
event: CGEvent,
refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
let nsevent = NSEvent(cgEvent: event)!
let cgpt = CGPoint(x: nsevent.locationInWindow.x,
y: CGDisplayBounds(0).maxY - nsevent.locationInWindow.y)
let axgl = AXUIElementCreateSystemWide()
let clickel = UnsafeMutablePointer<AXUIElement?>.allocate(capacity: 1)
AXUIElementCopyElementAtPosition(axgl, Float(cgpt.x), Float(cgpt.y), clickel)
// set global pid variable
var pid: pid_t = 0
AXUIElementGetPid(clickel.pointee!, &pid)
if (taps.count > 0) {
CGEvent.tapEnable(tap: taps[0], enable: false)
CFMachPortInvalidate(taps[0])
}
// run this async so we don't block the callback -- makes the window
// recording super jittery
DispatchQueue.global().async {
bounce_window(pid: pid)
}
/// unreachable?
return Unmanaged.passRetained(event)
}
/// return window size
func getwsize(window: UnsafeMutablePointer<CFTypeRef?>) -> CGSize {
/// Get window size
let axsize = UnsafeMutablePointer<CFTypeRef?>.allocate(capacity: 1)
AXUIElementCopyAttributeValue(window.pointee as! AXUIElement, kAXSizeAttribute as CFString, axsize)
var wsize = CGSize()
AXValueGetValue(axsize.pointee as! AXValue, AXValueType(rawValue: kAXValueCGSizeType)!, &wsize)
return wsize
}
/// set window position
func setpos(window: UnsafeMutablePointer<CFTypeRef?>, _ x: Double, _ y: Double) {
var p = CGPoint(x: x, y: y);
let axp: CFTypeRef = AXValueCreate(AXValueType(rawValue: kAXValueCGPointType)!, &p)!;
AXUIElementSetAttributeValue(window.pointee as! AXUIElement, kAXPositionAttribute as CFString, axp);
}
/// get current window position
func getpos(window: UnsafeMutablePointer<CFTypeRef?>) -> CGPoint {
var p = CGPoint()
let axp = UnsafeMutablePointer<CFTypeRef?>.allocate(capacity: 1)
AXUIElementCopyAttributeValue(window.pointee as! AXUIElement, kAXPositionAttribute as CFString, axp);
AXValueGetValue(axp.pointee as! AXValue, AXValueType(rawValue: kAXValueCGPointType)!, &p)
return p
}
/// helpers
func rv() -> Double {
return Double.random(in: 0..<1)
}
func rvt() -> Double {
return Double.random(in: 0.5..<(0.9))
}
func nnv(_ v: (Double,Double)) -> (Double,Double) {
let l = sqrt(pow(v.0,2) + pow(v.1,2))
return (speed * v.0 / l, speed * v.1 / l)
}
/// check intersection and find next direction
func update(wsize: CGSize, bounds: CGRect, point: CGPoint) -> (Bool, (Double,Double)) {
let brect = [
point,
CGPoint(x: point.x + wsize.width, y: point.y),
CGPoint(x: point.x, y: point.y + wsize.height),
CGPoint(x: point.x + wsize.width, y: point.y + wsize.height)
]
var hit: Bool = false
var dv: (Double, Double) = (0,0)
for checkp in brect {
if (checkp.x >= bounds.width) {
dv = (-1.0 * rvt(), rv())
hit = true
break
} else if (checkp.y >= bounds.height) {
dv = (rv(), -1.0 * rvt())
hit = true
break
} else if (checkp.x <= 1) {
dv = (1.0 * rvt(), rv())
hit = true
break
} else if (checkp.y <= 26) {
// 26: need to account for the menu bar
dv = (rv(), 1.0 * rvt())
hit = true
break
}
}
return (hit, nnv(dv))
}
/// get application window PID from a mouse click
/// TODO does not support multiple-window applications
///=======================================================================================================
func bounce_window(pid: pid_t) {
/// Accessibility API object for application
let app = AXUIElementCreateApplication(pid)
/// Get main window from the application
let w = UnsafeMutablePointer<CFTypeRef?>.allocate(capacity: 0)
AXUIElementCopyAttributeValue(app, "AXMainWindow" as CFString, w)
/// Get screen size
let bounds = CGDisplayBounds(0)
let wsize = getwsize(window: w)
var v: (Double,Double) = nnv((rv(), rv()))
while true {
let p = getpos(window: w);
let (hit, nextv) = update(wsize: wsize, bounds: bounds, point: p);
if hit {
v = nextv
}
setpos(window: w, Double(p.x) + v.0, Double(p.y) + v.1)
//usleep(3 * 1000);
DispatchQueue.main.async {
CFRunLoopRunInMode(CFRunLoopMode.defaultMode, 0.1, true);
}
}
}
start()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment