import AppKit import CoreGraphics import ScreenCaptureKit final class CaptureController: NSObject { static let shared = CaptureController() private var overlays: [NSWindow] = [] private(set) var isSelecting = false private var previousApp: NSRunningApplication? private var overlayIDs: [Int] = [] /// Session event log for debugging (dumped via Copy Diagnostics). static var sessionLog: [String] = [] static func log(_ line: String) { sessionLog.append(line) if sessionLog.count > 100 { sessionLog.removeFirst() } NSLog("[Kapture] \(line)") } /// Diagnostic snapshot for "Copy Diagnostics" menu item. func diagnostics() -> String { var lines: [String] = [] lines.append("Kapture diagnostics — version \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "?")") lines.append("--- session log (newest last) ---") lines.append(contentsOf: Self.sessionLog) lines.append("--- screens ---") for (i, s) in NSScreen.screens.enumerated() { let scale = s.backingScaleFactor let cgID = (s.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0 lines.append("screen[\(i)]: frame=\(s.frame) scale=\(scale) backingSize=\(s.frame.width*scale)x\(s.frame.height*scale) cgID=\(cgID)") } if #available(macOS 14.0, *) { Task { if let content = try? await SCShareableContent.current { for (i, d) in content.displays.enumerated() { lines.append("SCDisplay[\(i)]: frame=\(d.frame) width=\(d.width)x\(d.height) id=\(d.displayID)") } } else { lines.append("SCShareableContent: unavailable (no Screen Recording permission?)") } let text = lines.joined(separator: "\n") let pb = NSPasteboard.general pb.clearContents() pb.setString(text, forType: .string) } } else { let text = lines.joined(separator: "\n") let pb = NSPasteboard.general pb.clearContents() pb.setString(text, forType: .string) } return lines.joined(separator: "\n") } func begin() { guard !isSelecting else { return } guard PermissionManager.isGranted else { PermissionManager.request() return } isSelecting = true previousApp = NSWorkspace.shared.frontmostApplication // Force activation and log whether it stuck — inactive apps don't // receive overlay clicks. let activated = NSApp.activate(ignoringOtherApps: true) CaptureController.log("begin: activate(ignoringOtherApps)=\(activated) active=\(NSApp.isActive)") overlays = NSScreen.screens.map { screen in OverlayWindow( screen: screen, onComplete: { [weak self] rect in self?.finishSelection(rect) }, onCancel: { [weak self] in self?.cancelSelection() }, onWindowPick: { [weak self] rect in self?.finishWindowPick(rect) } ) } overlays.forEach { $0.makeKeyAndOrderFront(nil) } overlayIDs = overlays.map { $0.windowNumber } CaptureController.log("begin: \(overlays.count) overlays, ids=\(overlayIDs), active=\(NSApp.isActive)") } /// A window was picked from hover — capture it with rounded corners. private func finishWindowPick(_ rect: NSRect) { let ids = overlayIDs hideOverlays() isSelecting = false restorePreviousApp() DispatchQueue.main.async { [self] in capture(rect, excludingWindowIDs: ids, roundedCorners: true) } } private func finishSelection(_ rect: NSRect) { let ids = overlayIDs hideOverlays() guard rect.width >= 2, rect.height >= 2 else { cancelSelection() return } DispatchQueue.main.async { [self] in capture(rect, excludingWindowIDs: ids) } } private func cancelSelection() { hideOverlays() isSelecting = false restorePreviousApp() } private func hideOverlays() { overlays.forEach { $0.orderOut(nil) } overlays = [] } private func restorePreviousApp() { previousApp?.activate(options: [.activateIgnoringOtherApps]) previousApp = nil } private func capture(_ rect: NSRect, excludingWindowIDs ids: [Int], roundedCorners: Bool = false) { ScreenCapture.shared.capture(rect, excludingWindowIDs: ids, roundedCorners: roundedCorners) { [weak self] image in DispatchQueue.main.async { self?.isSelecting = false self?.restorePreviousApp() Task { @MainActor in guard let image else { ResultWindow.shared.show(image: ResultWindow.failureImage("Capture failed. Check Screen Recording permission.")) return } ResultWindow.shared.show(image: image) } } } } } final class ScreenCapture { static let shared = ScreenCapture() private init() {} @available(macOS 14.0, *) private func crop(_ image: CGImage, to cgRect: CGRect, display: SCDisplay, roundedCorners: Bool = false) -> CGImage? { // cgRect is in SCDisplay space (bottom-left origin, Y flipped vs AppKit). // The captured image has a TOP-LEFT origin. So within the display, // we must flip Y: pixelY = displayHeight - (cgY - displayMinY) - cgHeight. let scaleX = CGFloat(image.width) / display.frame.width let scaleY = CGFloat(image.height) / display.frame.height guard scaleX > 0, scaleY > 0 else { return nil } let localMinX = cgRect.minX - display.frame.minX let localMaxY = cgRect.maxY - display.frame.minY let flippedY = display.frame.height - localMaxY let pxRect = CGRect( x: localMinX * scaleX, y: flippedY * scaleY, width: cgRect.width * scaleX, height: cgRect.height * scaleY ) // Guard against negative offsets when the selection reaches beyond the // display's frame (e.g. secondary monitor coordinate quirks). let safeRect = pxRect.intersection(CGRect(x: 0, y: 0, width: CGFloat(image.width), height: CGFloat(image.height))) guard safeRect.width >= 1, safeRect.height >= 1 else { return nil } // Use a pixel-exact subimage crop instead of redrawing through a // CGContext, which resamples and loses quality on retina captures. let rounded = safeRect.integral guard rounded.width >= 1, rounded.height >= 1 else { return nil } guard var result = image.cropping(to: rounded) else { return nil } // Apply a rounded-corner alpha mask for window captures, matching // macOS window chrome (≈10pt radius, scaled to image pixels). if roundedCorners { let radius = 10 * scaleX let maskRect = CGRect(x: 0, y: 0, width: result.width, height: result.height) guard let maskCtx = CGContext( data: nil, width: result.width, height: result.height, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue ) else { return result } maskCtx.setFillColor(CGColor(gray: 1, alpha: 1)) maskCtx.fill(maskRect) maskCtx.setBlendMode(.clear) maskCtx.setFillColor(CGColor(gray: 0, alpha: 1)) maskCtx.addPath(CGPath(roundedRect: maskRect, cornerWidth: radius, cornerHeight: radius, transform: nil)) maskCtx.fillPath() maskCtx.setBlendMode(.normal) if let mask = maskCtx.makeImage(), let masked = result.masking(mask) { result = masked } } return result } @available(macOS 14.0, *) private func displayContaining(_ rect: NSRect, in content: SCShareableContent) -> SCDisplay? { let mid = NSPoint(x: rect.midX, y: rect.midY) guard let screen = NSScreen.screens.first(where: { $0.frame.contains(mid) }) ?? NSScreen.screens.first else { return nil } let cgID = (screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? NSNumber)?.uint32Value ?? 0 return content.displays.first(where: { $0.displayID == cgID }) ?? content.displays.first } func capture(_ rect: NSRect, excludingWindowIDs excludedIDs: [Int], roundedCorners: Bool = false, completion: @escaping (CGImage?) -> Void) { if #available(macOS 14.0, *) { Task { guard let content = try? await SCShareableContent.current, let display = displayContaining(rect, in: content) else { completion(nil) return } let excludedSet = Set(excludedIDs.map { CGWindowID($0) }) let excluded = content.windows.filter { excludedSet.contains($0.windowID) } let filter = SCContentFilter(display: display, excludingWindows: excluded) let config = SCStreamConfiguration() // SCDisplay.width/height are in POINTS; SCStreamConfiguration // width/height are output PIXELS. Multiply by the backing scale // of the screen containing the selection so retina displays are // captured at full resolution and 1x displays stay 1x. guard let screen = NSScreen.screens.first(where: { $0.frame.contains(NSPoint(x: rect.midX, y: rect.midY)) }) else { completion(nil) return } let backingScale = screen.backingScaleFactor config.width = Int(CGFloat(display.width) * backingScale) config.height = Int(CGFloat(display.height) * backingScale) config.showsCursor = true // ScreenCaptureKit uses a different origin than AppKit for // screens stacked around the main display: per-display, // SCDisplay y = appKitY - NSScreen.maxY + SCDisplay.maxY. let scRect = CGRect( x: rect.minX, y: rect.minY - screen.frame.maxY + display.frame.maxY, width: rect.width, height: rect.height ) guard let image = try? await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config) else { completion(nil) return } completion(self.crop(image, to: scRect, display: display, roundedCorners: roundedCorners)) } } else { guard let primary = NSScreen.screens.first(where: { $0.frame.origin == .zero }) ?? NSScreen.screens.first else { completion(nil) return } let cgRect = CGRect( x: rect.minX, y: primary.frame.height - rect.maxY, width: rect.width, height: rect.height ) completion(CGWindowListCreateImage(cgRect, .optionOnScreenOnly, kCGNullWindowID, [.bestResolution, .boundsIgnoreFraming])) } } }