Skip to content

Instantly share code, notes, and snippets.

@hamidzr
Created March 13, 2026 05:25
Show Gist options
  • Select an option

  • Save hamidzr/a3bfca7b5447039df9e7621b0d548aa8 to your computer and use it in GitHub Desktop.

Select an option

Save hamidzr/a3bfca7b5447039df9e7621b0d548aa8 to your computer and use it in GitHub Desktop.
macOS equivalent of slop (Select Operation) - interactive screen region selection in Swift
// macOS equivalent of slop (Select Operation)
// shows a transparent overlay for interactive screen region selection
//
// interactive modes:
// - hover over a window and click to select it
// - click and drag to select a custom region
//
// non-interactive modes:
// --screen output full display geometry (including menu bar)
// --visible output visible area geometry (excludes menu bar and dock)
//
// build: swiftc -O -o select-region select-region.swift -framework Cocoa
// usage: select-region [--screen | --visible] [-f FORMAT]
// format placeholders: %w %h %x %y (native pixels, top-left origin)
// default format: %wx%h+%x+%y
// interactive keys: s/f = full screen, v = visible area
// cancel: ESC, ctrl+C, ctrl+D (all exit 1)
import Cocoa
var formatStr = "%wx%h+%x+%y"
var mode: String? // nil = interactive, "screen" or "visible"
var args = Array(CommandLine.arguments.dropFirst())
var i = 0
while i < args.count {
switch args[i] {
case "-f" where i + 1 < args.count:
formatStr = args[i + 1]
i += 2
case "--screen":
mode = "screen"
i += 1
case "--visible":
mode = "visible"
i += 1
default:
i += 1
}
}
struct WinInfo {
let frame: CGRect // quartz coords (top-left origin, points)
let name: String
}
let myPID = ProcessInfo.processInfo.processIdentifier
func queryWindows() -> [WinInfo] {
guard let list = CGWindowListCopyWindowInfo(
[.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID
) as? [[String: Any]] else { return [] }
var result: [WinInfo] = []
for info in list {
guard let pid = info[kCGWindowOwnerPID as String] as? Int32,
pid != myPID,
let boundsRaw = info[kCGWindowBounds as String]
else { continue }
var rect = CGRect.zero
// swiftlint:disable:next force_cast
guard CGRectMakeWithDictionaryRepresentation(boundsRaw as! CFDictionary, &rect) else { continue }
if rect.width > 10 && rect.height > 10 {
let name = info[kCGWindowOwnerName as String] as? String ?? ""
result.append(WinInfo(frame: rect, name: name))
}
}
return result
}
func formatRegion(_ rect: CGRect, scale: CGFloat) -> String {
let x = Int(rect.origin.x * scale)
let y = Int(rect.origin.y * scale)
var w = Int(rect.width * scale)
var h = Int(rect.height * scale)
// ffmpeg needs even dimensions
w -= w % 2
h -= h % 2
var out = formatStr
out = out.replacingOccurrences(of: "%w", with: "\(w)")
out = out.replacingOccurrences(of: "%h", with: "\(h)")
out = out.replacingOccurrences(of: "%x", with: "\(x)")
out = out.replacingOccurrences(of: "%y", with: "\(y)")
return out
}
func outputRegion(_ rect: CGRect, scale: CGFloat) {
print(formatRegion(rect, scale: scale))
NSApp.terminate(nil)
}
class SelectionView: NSView {
// drag state
var anchor: NSPoint?
var cursor: NSPoint?
// window hover state (refreshed periodically by timer)
var windows: [WinInfo] = []
var hoveredWindow: WinInfo?
var scale: CGFloat = 1
var screenH: CGFloat = 0
override var acceptsFirstResponder: Bool { true }
override func updateTrackingAreas() {
super.updateTrackingAreas()
for area in trackingAreas { removeTrackingArea(area) }
addTrackingArea(NSTrackingArea(
rect: bounds,
options: [.mouseMoved, .activeAlways],
owner: self
))
}
// coordinate conversions between AppKit (bottom-left) and quartz (top-left)
func toCG(_ p: NSPoint) -> CGPoint {
CGPoint(x: p.x, y: screenH - p.y)
}
func toNS(_ r: CGRect) -> NSRect {
NSRect(x: r.origin.x, y: screenH - r.origin.y - r.height,
width: r.width, height: r.height)
}
func dragRect(from a: NSPoint, to b: NSPoint) -> NSRect {
NSRect(x: min(a.x, b.x), y: min(a.y, b.y),
width: abs(b.x - a.x), height: abs(b.y - a.y))
}
func findWindow(at cgPoint: CGPoint) -> WinInfo? {
// windows are front-to-back, first hit is topmost
windows.first { $0.frame.contains(cgPoint) }
}
// MARK: - mouse events
override func mouseMoved(with event: NSEvent) {
guard anchor == nil else { return }
hoveredWindow = findWindow(at: toCG(event.locationInWindow))
setNeedsDisplay(bounds)
}
override func mouseDown(with event: NSEvent) {
windows = queryWindows()
anchor = event.locationInWindow
cursor = anchor
setNeedsDisplay(bounds)
}
override func mouseDragged(with event: NSEvent) {
cursor = event.locationInWindow
setNeedsDisplay(bounds)
}
override func mouseUp(with event: NSEvent) {
cursor = event.locationInWindow
guard let a = anchor, let b = cursor else { return }
let dr = dragRect(from: a, to: b)
anchor = nil
cursor = nil
if dr.width > 5 && dr.height > 5 {
// drag selection: convert NSView rect to quartz coords
let cgRect = CGRect(x: dr.origin.x,
y: screenH - dr.origin.y - dr.height,
width: dr.width, height: dr.height)
outputRegion(cgRect, scale: scale)
} else if let win = hoveredWindow {
// single click: select the hovered window
outputRegion(win.frame, scale: scale)
}
}
// swallow all key events in the view to prevent beeps
// actual cancel handling is in the app-level local event monitor
override func keyDown(with event: NSEvent) {}
override func flagsChanged(with event: NSEvent) {}
// MARK: - drawing
func drawLabel(_ text: String, above rect: NSRect) {
let label = text as NSString
let attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: NSColor.white,
.font: NSFont.monospacedSystemFont(ofSize: 13, weight: .medium),
]
let sz = label.size(withAttributes: attrs)
let bg = NSRect(x: rect.midX - sz.width / 2 - 6, y: rect.maxY + 6,
width: sz.width + 12, height: sz.height + 6)
NSColor(white: 0, alpha: 0.7).setFill()
NSBezierPath(roundedRect: bg, xRadius: 4, yRadius: 4).fill()
label.draw(at: NSPoint(x: bg.origin.x + 6, y: bg.origin.y + 3),
withAttributes: attrs)
}
override func draw(_ dirtyRect: NSRect) {
// dim the screen
NSColor(white: 0, alpha: 0.2).setFill()
bounds.fill()
// window hover highlight (only when not dragging)
if anchor == nil, let win = hoveredWindow {
let r = toNS(win.frame)
NSColor(red: 0.3, green: 0.65, blue: 1.0, alpha: 0.12).setFill()
r.fill()
NSColor(red: 0.3, green: 0.65, blue: 1.0, alpha: 0.7).setStroke()
let path = NSBezierPath(rect: r)
path.lineWidth = 2
path.stroke()
let dims = "\(Int(win.frame.width)) x \(Int(win.frame.height))"
let text = win.name.isEmpty ? dims : "\(win.name) \(dims)"
drawLabel(text, above: r)
}
// drag selection
guard let a = anchor, let b = cursor else { return }
let r = dragRect(from: a, to: b)
guard r.width > 0, r.height > 0 else { return }
NSColor(red: 0.2, green: 0.5, blue: 1.0, alpha: 0.12).setFill()
r.fill()
NSColor(red: 0.3, green: 0.65, blue: 1.0, alpha: 0.85).setStroke()
let path = NSBezierPath(rect: r)
path.lineWidth = 1.5
path.stroke()
drawLabel("\(Int(r.width)) x \(Int(r.height))", above: r)
}
}
class App: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_: Notification) {
guard let screen = NSScreen.main else {
fputs("error: no screen found\n", stderr)
exit(1)
}
let frame = screen.frame
let scale = screen.backingScaleFactor
// snapshot window list before showing our overlay
let windows = queryWindows()
window = NSWindow(contentRect: frame, styleMask: .borderless,
backing: .buffered, defer: false)
window.level = .screenSaver
window.backgroundColor = .clear
window.isOpaque = false
window.hasShadow = false
window.acceptsMouseMovedEvents = true
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
let view = SelectionView(frame: frame)
view.windows = windows
view.scale = scale
view.screenH = frame.height
window.contentView = view
window.makeKeyAndOrderFront(nil)
window.makeFirstResponder(view)
NSCursor.crosshair.push()
// refresh window list periodically so hover highlights stay accurate
// after workspace switches (e.g. via aerospace)
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak view] _ in
guard let view = view, view.anchor == nil else { return }
view.windows = queryWindows()
let mouse = NSEvent.mouseLocation
view.hoveredWindow = view.findWindow(at: view.toCG(mouse))
view.setNeedsDisplay(view.bounds)
}
// catch cancel and shortcut keys at the app level (more reliable than view keyDown)
// returning nil swallows handled events; returning event passes through
// to let global hotkeys (aerospace workspace switching etc) work
NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
// ESC
if event.keyCode == 53 { exit(1) }
// ctrl+C, ctrl+D
if event.modifierFlags.contains(.control),
let ch = event.charactersIgnoringModifiers, "cd".contains(ch) {
exit(1)
}
// s → full screen, v → visible area
if let ch = event.charactersIgnoringModifiers {
let screen = NSScreen.main!
if ch == "s" || ch == "f" {
outputRegion(screen.frame, scale: scale)
return nil
}
if ch == "v" {
let vf = screen.visibleFrame
let rect = CGRect(x: vf.origin.x,
y: screen.frame.height - vf.origin.y - vf.height,
width: vf.width, height: vf.height)
outputRegion(rect, scale: scale)
return nil
}
}
return event
}
}
}
// handle SIGINT (ctrl+C from terminal) and SIGTERM
signal(SIGINT) { _ in exit(1) }
signal(SIGTERM) { _ in exit(1) }
let app = NSApplication.shared
// non-interactive modes: output screen geometry and exit immediately
if let mode = mode {
let screen = NSScreen.main!
let scale = screen.backingScaleFactor
let rect: CGRect
switch mode {
case "screen":
rect = screen.frame
case "visible":
let vf = screen.visibleFrame
// convert AppKit coords (bottom-left origin) to quartz coords (top-left origin)
rect = CGRect(x: vf.origin.x,
y: screen.frame.height - vf.origin.y - vf.height,
width: vf.width, height: vf.height)
default:
fatalError("unreachable")
}
print(formatRegion(rect, scale: scale))
exit(0)
}
let delegate = App()
app.delegate = delegate
app.setActivationPolicy(.accessory)
app.activate(ignoringOtherApps: true)
app.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment