Created
March 13, 2026 05:25
-
-
Save hamidzr/a3bfca7b5447039df9e7621b0d548aa8 to your computer and use it in GitHub Desktop.
macOS equivalent of slop (Select Operation) - interactive screen region selection in Swift
This file contains hidden or 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
| // 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