Initial commit

This commit is contained in:
Alex E. Tafoya
2026-09-17 15:15:45 -05:00
commit 87fe7927c4
36 changed files with 5113 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
// CaptureEngine.swift
// GreenshotMac
//
// ScreenCaptureKit replacement for Greenshot's WindowCapture / GDI capture code.
import AppKit
import ScreenCaptureKit
/// A frozen snapshot of one display, used as the backdrop of the selection overlay.
struct DisplaySnapshot {
let screen: NSScreen
let display: SCDisplay
let image: CGImage
/// Display bounds in CoreGraphics global coordinates (top-left origin, points).
let cgFrame: CGRect
var scale: CGFloat { CGFloat(image.width) / cgFrame.width }
}
/// A window that can be picked in window capture mode.
struct WindowInfo {
let window: SCWindow
/// Window frame in CoreGraphics global coordinates (top-left origin, points).
var cgFrame: CGRect { window.frame }
var title: String { window.title ?? "" }
var applicationName: String { window.owningApplication?.applicationName ?? "" }
var bundleID: String? { window.owningApplication?.bundleIdentifier }
}
enum CaptureEngine {
// MARK: - Permission
@discardableResult
static func ensurePermission() -> Bool {
if CGPreflightScreenCaptureAccess() { return true }
CGRequestScreenCaptureAccess()
return CGPreflightScreenCaptureAccess()
}
// MARK: - Shareable content
static func shareableContent() async throws -> SCShareableContent {
do {
return try await SCShareableContent.excludingDesktopWindows(true, onScreenWindowsOnly: true)
} catch {
throw CaptureError.permissionDenied
}
}
/// Windows worth offering to the user: on screen, reasonably sized, not our own overlay.
static func selectableWindows(from content: SCShareableContent) -> [WindowInfo] {
let ownPID = ProcessInfo.processInfo.processIdentifier
return content.windows
.filter { window in
window.isOnScreen &&
window.frame.width > 40 && window.frame.height > 40 &&
window.owningApplication?.processID != ownPID
}
.map(WindowInfo.init)
}
// MARK: - Snapshots
/// Grabs every display so the selection overlay can show a frozen desktop.
static func snapshotAllDisplays() async throws -> [DisplaySnapshot] {
let content = try await shareableContent()
guard !content.displays.isEmpty else { throw CaptureError.noContent }
var snapshots: [DisplaySnapshot] = []
for display in content.displays {
guard let screen = ScreenGeometry.screen(for: display.displayID) else { continue }
let image = try await capture(display: display, content: content, sourceRect: nil, screen: screen)
snapshots.append(DisplaySnapshot(screen: screen,
display: display,
image: image,
cgFrame: CGRect(x: CGFloat(display.frame.origin.x),
y: CGFloat(display.frame.origin.y),
width: CGFloat(display.width),
height: CGFloat(display.height))))
}
guard !snapshots.isEmpty else { throw CaptureError.noContent }
return snapshots
}
private static func capture(display: SCDisplay,
content: SCShareableContent,
sourceRect: CGRect?,
screen: NSScreen) async throws -> CGImage {
let configuration = SCStreamConfiguration()
let scale = screen.backingScaleFactor
let rect = sourceRect ?? CGRect(x: 0, y: 0, width: display.width, height: display.height)
configuration.sourceRect = rect
configuration.width = Int((rect.width * scale).rounded())
configuration.height = Int((rect.height * scale).rounded())
configuration.scalesToFit = false
configuration.showsCursor = Configuration.shared.captureMousepointer
configuration.captureResolution = .best
if #available(macOS 14.0, *) {
configuration.ignoreShadowsSingleWindow = true
}
let ownWindows = content.windows.filter {
$0.owningApplication?.processID == ProcessInfo.processInfo.processIdentifier
}
let filter = SCContentFilter(display: display, excludingWindows: ownWindows)
do {
return try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
} catch {
throw CaptureError.captureFailed(error.localizedDescription)
}
}
// MARK: - Public capture entry points
/// Full screen capture of the display the mouse is on, or of a given screen.
static func captureFullscreen(screen requested: NSScreen? = nil) async throws -> Capture {
let content = try await shareableContent()
let mouseLocation = NSEvent.mouseLocation
let screen = requested ?? ScreenGeometry.screen(containing: mouseLocation) ?? NSScreen.main
guard let screen,
let displayID = ScreenGeometry.displayID(for: screen),
let display = content.displays.first(where: { $0.displayID == displayID })
else { throw CaptureError.noContent }
let image = try await capture(display: display, content: content, sourceRect: nil, screen: screen)
var details = CaptureDetails()
details.captureMode = .fullscreen
details.title = screen.localizedName
details.scaleFactor = screen.backingScaleFactor
details.screenBounds = CGRect(x: CGFloat(display.frame.origin.x),
y: CGFloat(display.frame.origin.y),
width: CGFloat(display.width),
height: CGFloat(display.height))
return Capture(image: image, details: details)
}
/// Captures one window on its own, without whatever overlaps it.
static func captureWindow(_ info: WindowInfo) async throws -> Capture {
let configuration = SCStreamConfiguration()
let screen = ScreenGeometry.screen(containing: ScreenGeometry.appKitRect(fromCG: info.cgFrame).center)
?? NSScreen.main
let scale = screen?.backingScaleFactor ?? 2
configuration.width = Int((info.cgFrame.width * scale).rounded())
configuration.height = Int((info.cgFrame.height * scale).rounded())
configuration.showsCursor = false
configuration.scalesToFit = false
configuration.captureResolution = .best
if #available(macOS 14.0, *) {
configuration.ignoreShadowsSingleWindow = Configuration.shared.windowCaptureMode != .windowWithShadow
}
let filter = SCContentFilter(desktopIndependentWindow: info.window)
let image: CGImage
do {
image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: configuration)
} catch {
throw CaptureError.captureFailed(error.localizedDescription)
}
var details = CaptureDetails()
details.captureMode = .window
details.title = info.title.isEmpty ? info.applicationName : info.title
details.applicationName = info.applicationName
details.applicationBundleID = info.bundleID
details.scaleFactor = scale
details.screenBounds = info.cgFrame
return Capture(image: image, details: details)
}
/// Crops a region out of an already frozen display snapshot.
static func crop(snapshot: DisplaySnapshot, cgRect: CGRect, mode: CaptureMode) throws -> Capture {
let local = CGRect(x: cgRect.origin.x - snapshot.cgFrame.origin.x,
y: cgRect.origin.y - snapshot.cgFrame.origin.y,
width: cgRect.width,
height: cgRect.height)
guard let cropped = ScreenGeometry.crop(snapshot.image, toPointRect: local, scale: snapshot.scale) else {
throw CaptureError.captureFailed("The selected region was empty.")
}
var details = CaptureDetails()
details.captureMode = mode
details.title = snapshot.screen.localizedName
details.scaleFactor = snapshot.scale
details.screenBounds = cgRect
return Capture(image: cropped, details: details)
}
// MARK: - Feedback
@MainActor
static func playFeedback(on rect: CGRect?) {
if Configuration.shared.playCameraSound {
NSSound(named: NSSound.Name("Grab"))?.play()
}
if Configuration.shared.showFlashAnimation, let rect {
FlashOverlay.flash(appKitRect: ScreenGeometry.appKitRect(fromCG: rect))
}
}
}
extension CGRect {
var center: CGPoint { CGPoint(x: midX, y: midY) }
}
/// Short white flash over the captured area, the equivalent of Greenshot's capture flash.
@MainActor
final class FlashOverlay {
private static var windows: [NSWindow] = []
static func flash(appKitRect rect: CGRect) {
guard rect.width > 1, rect.height > 1 else { return }
let window = NSWindow(contentRect: rect, styleMask: .borderless, backing: .buffered, defer: false)
window.isOpaque = false
window.backgroundColor = .white
window.alphaValue = 0.7
window.level = .screenSaver
window.ignoresMouseEvents = true
window.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
window.orderFrontRegardless()
windows.append(window)
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.18
window.animator().alphaValue = 0
} completionHandler: {
// Run on the main thread by contract, so main actor isolation can be assumed.
MainActor.assumeIsolated {
window.orderOut(nil)
FlashOverlay.windows.removeAll { $0 === window }
}
}
}
}
+380
View File
@@ -0,0 +1,380 @@
// CaptureOverlay.swift
// GreenshotMac
//
// The equivalent of Greenshot's CaptureForm: a frozen, dimmed copy of the desktop
// on which the user drags a region or clicks a window.
import AppKit
import ScreenCaptureKit
enum OverlayResult {
case region(DisplaySnapshot, CGRect) // rect in CoreGraphics global coordinates
case window(WindowInfo)
case cancelled
}
enum OverlayMode {
case region
case window
}
@MainActor
final class CaptureOverlayController {
private var windows: [OverlayWindow] = []
private let snapshots: [DisplaySnapshot]
private let windowInfos: [WindowInfo]
private var mode: OverlayMode
private var completion: ((OverlayResult) -> Void)?
private var finished = false
init(snapshots: [DisplaySnapshot], windows windowInfos: [WindowInfo], mode: OverlayMode) {
self.snapshots = snapshots
self.windowInfos = windowInfos
self.mode = mode
}
func present(completion: @escaping (OverlayResult) -> Void) {
self.completion = completion
for snapshot in snapshots {
let window = OverlayWindow(snapshot: snapshot, windows: windowInfos, mode: mode, controller: self)
windows.append(window)
window.orderFrontRegardless()
}
windows.first?.makeKey()
NSApp.activate(ignoringOtherApps: true)
}
/// Keeps every display's overlay in the same mode.
func setMode(_ newMode: OverlayMode) {
mode = newMode
for window in windows { window.overlayView.mode = newMode }
}
func toggleMode() {
setMode(mode == .region ? .window : .region)
}
func finish(_ result: OverlayResult) {
guard !finished else { return }
finished = true
for window in windows { window.orderOut(nil) }
windows.removeAll()
completion?(result)
completion = nil
}
}
// MARK: - Window
@MainActor
final class OverlayWindow: NSWindow {
let overlayView: OverlayView
init(snapshot: DisplaySnapshot, windows: [WindowInfo], mode: OverlayMode, controller: CaptureOverlayController) {
overlayView = OverlayView(snapshot: snapshot, windows: windows, mode: mode, controller: controller)
super.init(contentRect: snapshot.screen.frame,
styleMask: .borderless,
backing: .buffered,
defer: false)
isOpaque = false
backgroundColor = .clear
level = .screenSaver
hasShadow = false
ignoresMouseEvents = false
collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
acceptsMouseMovedEvents = true
contentView = overlayView
setFrame(snapshot.screen.frame, display: true)
}
override var canBecomeKey: Bool { true }
override var canBecomeMain: Bool { true }
}
// MARK: - View
@MainActor
final class OverlayView: NSView {
private let snapshot: DisplaySnapshot
private let windowInfos: [WindowInfo]
private weak var controller: CaptureOverlayController?
private var backdrop: NSImage
private var anchor: CGPoint?
private var current: CGPoint?
private var mouseLocation: CGPoint = .zero
private var hoveredWindow: WindowInfo?
private var trackingArea: NSTrackingArea?
var mode: OverlayMode {
didSet {
hoveredWindow = nil
anchor = nil
current = nil
needsDisplay = true
}
}
init(snapshot: DisplaySnapshot, windows: [WindowInfo], mode: OverlayMode, controller: CaptureOverlayController) {
self.snapshot = snapshot
self.windowInfos = windows
self.mode = mode
self.controller = controller
self.backdrop = NSImage(cgImage: snapshot.image,
size: CGSize(width: snapshot.cgFrame.width, height: snapshot.cgFrame.height))
super.init(frame: CGRect(origin: .zero, size: snapshot.screen.frame.size))
wantsLayer = true
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }
// MARK: Tracking and cursor
override func updateTrackingAreas() {
super.updateTrackingAreas()
if let trackingArea { removeTrackingArea(trackingArea) }
let area = NSTrackingArea(rect: bounds,
options: [.activeAlways, .mouseMoved, .mouseEnteredAndExited, .inVisibleRect],
owner: self,
userInfo: nil)
addTrackingArea(area)
trackingArea = area
}
override func resetCursorRects() {
addCursorRect(bounds, cursor: .crosshair)
}
override var acceptsFirstResponder: Bool { true }
// MARK: Coordinate helpers
/// View point (bottom-left origin) to CoreGraphics global point (top-left origin).
private func cgPoint(from viewPoint: CGPoint) -> CGPoint {
CGPoint(x: snapshot.cgFrame.origin.x + viewPoint.x,
y: snapshot.cgFrame.origin.y + (bounds.height - viewPoint.y))
}
/// CoreGraphics global rect to a rect in this view.
private func viewRect(fromCG rect: CGRect) -> CGRect {
CGRect(x: rect.origin.x - snapshot.cgFrame.origin.x,
y: bounds.height - (rect.origin.y - snapshot.cgFrame.origin.y) - rect.height,
width: rect.width,
height: rect.height)
}
private var selectionRect: CGRect? {
guard let anchor, let current else { return nil }
let rect = CGRect(x: min(anchor.x, current.x),
y: min(anchor.y, current.y),
width: abs(current.x - anchor.x),
height: abs(current.y - anchor.y))
return rect.width >= 1 && rect.height >= 1 ? rect : nil
}
private var highlightRect: CGRect? {
switch mode {
case .region:
return selectionRect
case .window:
guard let hoveredWindow else { return nil }
return viewRect(fromCG: hoveredWindow.cgFrame).intersection(bounds)
}
}
// MARK: Mouse
override func mouseMoved(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
if mode == .window {
updateHoveredWindow()
}
needsDisplay = true
}
override func mouseDown(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
if mode == .window {
updateHoveredWindow()
if let hoveredWindow {
controller?.finish(.window(hoveredWindow))
}
return
}
anchor = mouseLocation
current = mouseLocation
needsDisplay = true
}
override func mouseDragged(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
if mode == .region {
if anchor == nil { anchor = mouseLocation }
current = mouseLocation
}
needsDisplay = true
}
override func mouseUp(with event: NSEvent) {
mouseLocation = convert(event.locationInWindow, from: nil)
guard mode == .region else { return }
current = mouseLocation
guard let rect = selectionRect, rect.width >= 3, rect.height >= 3 else {
anchor = nil
current = nil
needsDisplay = true
return
}
let topLeft = cgPoint(from: CGPoint(x: rect.minX, y: rect.maxY))
let cgRect = CGRect(x: topLeft.x.rounded(), y: topLeft.y.rounded(),
width: rect.width.rounded(), height: rect.height.rounded())
controller?.finish(.region(snapshot, cgRect))
}
override func keyDown(with event: NSEvent) {
switch event.keyCode {
case 53: // Escape
controller?.finish(.cancelled)
case 49: // Space
controller?.toggleMode()
default:
super.keyDown(with: event)
}
}
private func updateHoveredWindow() {
let point = cgPoint(from: mouseLocation)
hoveredWindow = windowInfos.first { $0.cgFrame.contains(point) }
}
// MARK: Drawing
override func draw(_ dirtyRect: CGRect) {
guard let context = NSGraphicsContext.current?.cgContext else { return }
backdrop.draw(in: bounds, from: .zero, operation: .copy, fraction: 1.0)
let highlight = highlightRect
NSColor(calibratedWhite: 0, alpha: 0.42).setFill()
if let highlight {
// Dim everything but the highlighted area.
let path = NSBezierPath(rect: bounds)
path.append(NSBezierPath(rect: highlight))
path.windingRule = .evenOdd
path.fill()
NSColor.systemGreen.setStroke()
let border = NSBezierPath(rect: highlight.insetBy(dx: -0.5, dy: -0.5))
border.lineWidth = 1.5
border.stroke()
} else {
bounds.fill()
drawCrosshair(in: context)
}
if Configuration.shared.showSelectionSize, let highlight {
drawSizeLabel(for: highlight)
}
if Configuration.shared.showMagnifier, highlight == nil || mode == .region {
drawMagnifier()
}
if highlight == nil {
drawHint()
}
}
private func drawCrosshair(in context: CGContext) {
context.saveGState()
context.setStrokeColor(NSColor.systemGreen.withAlphaComponent(0.8).cgColor)
context.setLineWidth(1)
context.move(to: CGPoint(x: 0, y: mouseLocation.y))
context.addLine(to: CGPoint(x: bounds.width, y: mouseLocation.y))
context.move(to: CGPoint(x: mouseLocation.x, y: 0))
context.addLine(to: CGPoint(x: mouseLocation.x, y: bounds.height))
context.strokePath()
context.restoreGState()
}
private func drawSizeLabel(for rect: CGRect) {
let scale = snapshot.scale
let text: String
switch mode {
case .region:
text = "\(Int(rect.width * scale)) × \(Int(rect.height * scale)) px"
case .window:
let title = hoveredWindow.map { $0.applicationName.isEmpty ? $0.title : $0.applicationName } ?? ""
text = "\(title) \(Int(rect.width * scale)) × \(Int(rect.height * scale)) px"
}
draw(label: text, near: CGPoint(x: rect.minX, y: rect.maxY + 6))
}
private func drawHint() {
let text = mode == .region
? "Drag to select a region · Space for window mode · Esc to cancel"
: "Click a window to capture it · Space for region mode · Esc to cancel"
draw(label: text, near: CGPoint(x: mouseLocation.x + 18, y: mouseLocation.y - 34))
}
private func draw(label text: String, near point: CGPoint) {
let attributes: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: 12, weight: .medium),
.foregroundColor: NSColor.white
]
let size = (text as NSString).size(withAttributes: attributes)
var box = CGRect(x: point.x, y: point.y, width: size.width + 12, height: size.height + 8)
box.origin.x = min(max(4, box.origin.x), bounds.width - box.width - 4)
box.origin.y = min(max(4, box.origin.y), bounds.height - box.height - 4)
NSColor(calibratedWhite: 0, alpha: 0.75).setFill()
NSBezierPath(roundedRect: box, xRadius: 5, yRadius: 5).fill()
(text as NSString).draw(at: CGPoint(x: box.minX + 6, y: box.minY + 4), withAttributes: attributes)
}
private func drawMagnifier() {
let magnification: CGFloat = 8
let side: CGFloat = 120
let sourceSide = side / magnification
let cgCursor = cgPoint(from: mouseLocation)
let localPoint = CGPoint(x: cgCursor.x - snapshot.cgFrame.origin.x,
y: cgCursor.y - snapshot.cgFrame.origin.y)
let sourceRect = CGRect(x: localPoint.x - sourceSide / 2,
y: localPoint.y - sourceSide / 2,
width: sourceSide,
height: sourceSide)
guard let zoomed = ScreenGeometry.crop(snapshot.image, toPointRect: sourceRect, scale: snapshot.scale) else {
return
}
var frame = CGRect(x: mouseLocation.x + 20, y: mouseLocation.y + 20, width: side, height: side)
if frame.maxX > bounds.width - 8 { frame.origin.x = mouseLocation.x - side - 20 }
if frame.maxY > bounds.height - 8 { frame.origin.y = mouseLocation.y - side - 20 }
guard let context = NSGraphicsContext.current?.cgContext else { return }
context.saveGState()
context.interpolationQuality = .none
let clip = NSBezierPath(roundedRect: frame, xRadius: 8, yRadius: 8)
clip.addClip()
context.translateBy(x: frame.minX, y: frame.minY + frame.height)
context.scaleBy(x: 1, y: -1)
context.draw(zoomed, in: CGRect(origin: .zero, size: frame.size))
context.restoreGState()
NSColor.white.withAlphaComponent(0.9).setStroke()
clip.lineWidth = 1
clip.stroke()
// Centre marker showing the exact pixel under the cursor.
NSColor.systemGreen.setStroke()
let marker = NSBezierPath(rect: CGRect(x: frame.midX - magnification / 2,
y: frame.midY - magnification / 2,
width: magnification,
height: magnification))
marker.lineWidth = 1
marker.stroke()
}
}
+56
View File
@@ -0,0 +1,56 @@
// ScreenGeometry.swift
// GreenshotMac
//
// Conversions between AppKit's bottom-left global space and CoreGraphics'
// top-left global space, plus display lookups.
import AppKit
enum ScreenGeometry {
/// Height of the primary display, the anchor for both coordinate systems.
static var primaryHeight: CGFloat {
NSScreen.screens.first(where: { $0.frame.origin == .zero })?.frame.height
?? NSScreen.screens.first?.frame.height
?? 0
}
/// AppKit global rect (bottom-left origin) to CoreGraphics global rect (top-left origin).
static func cgRect(fromAppKit rect: CGRect) -> CGRect {
CGRect(x: rect.origin.x,
y: primaryHeight - rect.origin.y - rect.height,
width: rect.width,
height: rect.height)
}
/// CoreGraphics global rect (top-left origin) to AppKit global rect (bottom-left origin).
static func appKitRect(fromCG rect: CGRect) -> CGRect {
CGRect(x: rect.origin.x,
y: primaryHeight - rect.origin.y - rect.height,
width: rect.width,
height: rect.height)
}
static func displayID(for screen: NSScreen) -> CGDirectDisplayID? {
screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID
}
static func screen(for displayID: CGDirectDisplayID) -> NSScreen? {
NSScreen.screens.first { ScreenGeometry.displayID(for: $0) == displayID }
}
static func screen(containing point: CGPoint) -> NSScreen? {
NSScreen.screens.first { $0.frame.contains(point) } ?? NSScreen.main
}
/// Crops a CGImage using a rect expressed in points relative to the image's top-left corner.
static func crop(_ image: CGImage, toPointRect rect: CGRect, scale: CGFloat) -> CGImage? {
let pixelRect = CGRect(x: (rect.origin.x * scale).rounded(.down),
y: (rect.origin.y * scale).rounded(.down),
width: (rect.width * scale).rounded(),
height: (rect.height * scale).rounded())
.intersection(CGRect(x: 0, y: 0, width: CGFloat(image.width), height: CGFloat(image.height)))
guard pixelRect.width >= 1, pixelRect.height >= 1 else { return nil }
return image.cropping(to: pixelRect)
}
}