import SwiftUI import SwiftData import Cocoa import Combine import AVFoundation @main struct CapturaApp: App { @NSApplicationDelegateAdaptor(CapturaAppDelegate.self) var appDelegate var body: some Scene { WindowGroup { PreferencesScreen() .handlesExternalEvents(preferring: Set(arrayLiteral: "PreferencesScreen"), allowing: Set(arrayLiteral: "*")) .frame(width: 650, height: 450) } .handlesExternalEvents(matching: Set(arrayLiteral: "PreferencesScreen")) .modelContainer(for: CapturaRemoteFile.self) } } class CapturaAppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { @Environment(\.openURL) var openURL var statusItem: NSStatusItem! var captureState: CaptureState = .idle var recordingWindow: RecordingWindow? = nil var preferencesWindow: PreferencesWindow? = nil var boxListener: AnyCancellable? = nil var popover: NSPopover? = nil var helpShown = false var captureSession: CapturaCaptureSession? = nil var images: [CGImage] = [] var outputFile: CapturaFile? = nil var gifCallbackTimer = ContinuousClock.now var fps = CapturaSettings.frameRate var pixelDensity: CGFloat = 1.0 var stopTimer: DispatchWorkItem? func applicationDidFinishLaunching(_ notification: Notification) { setupMenu() NotificationCenter.default.addObserver( self, selector: #selector(self.didReceiveNotification(_:)), name: nil, object: nil) closeWindow() } // MARK: - Setup Functions private func setupMenu() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem.button { button.image = NSImage(systemSymbolName: "rectangle.dashed.badge.record", accessibilityDescription: "Captura") } statusItem.isVisible = true statusItem.menu = NSMenu() statusItem.menu?.delegate = self // Create the Popover popover = NSPopover() popover?.contentViewController = HelpPopoverViewController() popover?.behavior = .transient let recordItem = NSMenuItem(title: "Record", action: #selector(CapturaAppDelegate.onClickStartRecording), keyEquivalent: "6") recordItem.keyEquivalentModifierMask = [.command, .shift] statusItem.menu?.addItem(recordItem) statusItem.menu?.addItem(NSMenuItem.separator()) let preferencesItem = NSMenuItem(title: "Preferences", action: #selector(CapturaAppDelegate.onOpenPreferences), keyEquivalent: "") statusItem.menu?.addItem(preferencesItem) let quitItem = NSMenuItem(title: "Quit", action: #selector(CapturaAppDelegate.onQuit), keyEquivalent: "") statusItem.menu?.addItem(quitItem) } private func closeWindow() { if let window = NSApplication.shared.windows.first { window.close() } } // MARK: - UI Event Handlers func menuWillOpen(_ menu: NSMenu) { if captureState != .idle { menu.cancelTracking() if captureState == .recording { NotificationCenter.default.post(name: .stopRecording, object: nil, userInfo: nil) } } } @objc private func onClickStartRecording() { NotificationCenter.default.post(name: .startAreaSelection, object: nil, userInfo: nil) } @objc private func onOpenPreferences() { NSApp.activate(ignoringOtherApps: true) if preferencesWindow == nil { preferencesWindow = PreferencesWindow() } else { preferencesWindow?.makeKeyAndOrderFront(nil) } } @objc private func onQuit() { NSApplication.shared.terminate(self) } // MARK: - App State Event Listeners @objc func didReceiveNotification(_ notification: Notification) { switch(notification.name) { case .startAreaSelection: startAreaSelection() case .startRecording: startRecording() case .stopRecording: stopRecording() case .finalizeRecording: DispatchQueue.main.async { self.finalizeRecording() } case .reset: reset() case .failedToStart: failedToStart() case .receivedFrame: if let frame = notification.userInfo?["frame"] { receivedFrame(frame as! CVImageBuffer) } default: return } } func startAreaSelection() { helpShown = false NSApp.activate(ignoringOtherApps: true) if captureState != .selectingArea { captureState = .selectingArea if let button = statusItem.button { let rectInWindow = button.convert(button.bounds, to: nil) let rectInScreen = button.window?.convertToScreen(rectInWindow) recordingWindow = RecordingWindow(rectInScreen) boxListener = recordingWindow?.recordingContentView.$box .debounce(for: .seconds(0.3), scheduler: RunLoop.main) .sink { newValue in if newValue != nil { self.updateImage() if !self.helpShown { self.helpShown = true self.showPopoverWithMessage("Click here when you're ready to record.") } } } } } } func startRecording() { captureState = .recording updateImage() fps = CapturaSettings.frameRate outputFile = nil images = []; pixelDensity = recordingWindow?.pixelDensity ?? 1.0 recordingWindow?.recordingContentView.startRecording() if let box = recordingWindow?.recordingContentView.box { if let screen = recordingWindow?.screen { captureSession = CapturaCaptureSession(screen, box: box) if let captureSession { stopTimer = DispatchWorkItem { self.stopRecording() } DispatchQueue.main.asyncAfter(deadline: .now() + 300, execute: stopTimer!) outputFile = CapturaFile() if CapturaSettings.shouldSaveMp4 { captureSession.startRecording(to: outputFile!.mp4URL) } else { captureSession.startRunning() } return } } } NotificationCenter.default.post(name: .failedToStart, object: nil, userInfo: nil) } func stopRecording() { captureState = .uploading updateImage() stop() if !CapturaSettings.shouldSaveMp4 { NotificationCenter.default.post(name: .finalizeRecording, object: nil, userInfo: nil) return } Task.detached { if let outputFile = self.outputFile { await GifRenderer.render(self.images, at: self.fps, to: outputFile.gifURL) NotificationCenter.default.post(name: .finalizeRecording, object: nil, userInfo: nil) } } } func finalizeRecording() { captureState = .uploaded copyToClipboard() updateImage() DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) } } func reset() { captureState = .idle updateImage() stop() } func receivedFrame(_ frame: CVImageBuffer) { let now = ContinuousClock.now if now - gifCallbackTimer > .nanoseconds(1_000_000_000 / UInt64(fps)) { gifCallbackTimer = now DispatchQueue.main.async { if let cgImage = frame.cgImage?.resize(by: self.pixelDensity) { self.images.append(cgImage) } } } } func failedToStart() { captureState = .error updateImage() requestPermissionToRecord() stop() DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { NotificationCenter.default.post(name: .reset, object: nil, userInfo: nil) } } // MARK: - Presentation Helpers private func requestPermissionToRecord() { showPopoverWithMessage("Please grant Captura permission to record") if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording") { NSWorkspace.shared.open(url) } } private func showPopoverWithMessage(_ message: String) { if let button = statusItem.button { (self.popover?.contentViewController as? HelpPopoverViewController)?.updateLabel(message) self.popover?.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { self.popover?.performClose(nil) } } } private func updateImage() { if let button = statusItem.button { let image: String = switch captureState { case .idle: "rectangle.dashed.badge.record" case .selectingArea: "circle.rectangle.dashed" case .recording: "checkmark.rectangle" case .uploading: "dock.arrow.up.rectangle" case .uploaded: "checkmark.rectangle.fill" case .error: "xmark.rectangle.fill" } button.image = NSImage(systemSymbolName: image, accessibilityDescription: "Captura") } } private func stop() { stopTimer?.cancel() captureSession?.stopRunning() captureSession = nil boxListener?.cancel() recordingWindow?.close() recordingWindow = nil } private func copyToClipboard() { let fileType: NSPasteboard.PasteboardType = .init(rawValue: CapturaSettings.shouldSaveGif ? "com.compuserve.gif" : "public.mpeg-4") if let url = CapturaSettings.shouldSaveGif ? outputFile?.gifURL : outputFile?.mp4URL { if let data = try? Data(contentsOf: url) { let pasteboard = NSPasteboard.general pasteboard.declareTypes([fileType], owner: nil) pasteboard.setData(data, forType: fileType) } } } }