Files
kapture/Sources/Kapture/App.swift
T
TZGyn e4e20c51d3 Add frozen-screen capture on Ctrl+Shift+Cmd+S
A second hotkey snapshots every display the moment it is pressed and
shows that still image as the overlay, so the pointer and hover states
(tooltips, open menus) are captured. Selection is marquee-only — no
window detection — and the result is cropped from the snapshot rather
than recaptured, keeping the frame exactly as it was.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-25 17:40:12 +08:00

170 lines
5.6 KiB
Swift

import SwiftUI
import AppKit
import CoreGraphics
import ServiceManagement
@main
struct KaptureApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
var body: some Scene {
MenuBarExtra("Kapture", systemImage: "text.viewfinder") {
MenuView()
}
.menuBarExtraStyle(.menu)
}
}
final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
if CommandLine.arguments.contains("--test-ocr") {
runOCRSelfTest()
return
}
// Single instance: stale copies keep their own hotkey + overlays alive
// after app updates and eat clicks meant for the new instance.
let myPid = ProcessInfo.processInfo.processIdentifier
let others = NSRunningApplication.runningApplications(withBundleIdentifier: "dev.tzgyn.kapture")
.filter { $0.processIdentifier != myPid }
if !others.isEmpty {
CaptureController.log("launch: other Kapture instances pids=\(others.map { String($0.processIdentifier) }.joined(separator: ",")) — terminating them")
for other in others {
other.forceTerminate()
}
}
HotkeyManager.shared.onTrigger = { CaptureController.shared.begin() }
HotkeyManager.shared.onFrozenTrigger = { CaptureController.shared.begin(frozen: true) }
HotkeyManager.shared.register()
}
}
struct MenuView: View {
@State private var granted = PermissionManager.isGranted
var body: some View {
Button("Capture & OCR") { CaptureController.shared.begin() }
.keyboardShortcut("s", modifiers: [.command, .shift])
Button("Capture Frozen Screen") { CaptureController.shared.begin(frozen: true) }
.keyboardShortcut("s", modifiers: [.command, .shift, .control])
Button("Extract from Image File…") { ResultWindow.shared.showOpenPanel() }
Divider()
if granted {
Text("Screen Recording: Granted")
} else {
Button("Grant Screen Recording Permission…") {
PermissionManager.request()
granted = PermissionManager.isGranted
}
}
Divider()
Button("Copy Diagnostics") {
let diag = CaptureController.shared.diagnostics()
let pasteboard = NSPasteboard.general
pasteboard.clearContents()
pasteboard.setString(diag, forType: .string)
}
Button(LoginItem.isEnabled ? "Disable Launch at Login" : "Launch at Login") {
if !LoginItem.toggle() {
let alert = NSAlert()
alert.messageText = "Could not update Launch at Login"
alert.informativeText = "Launch at Login requires a signed app bundle in /Applications. It is not available when running via `swift run`."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
Divider()
Button("Quit Kapture") { NSApp.terminate(nil) }
.keyboardShortcut("q")
}
}
enum PermissionManager {
static var isGranted: Bool { CGPreflightScreenCaptureAccess() }
static func request() {
CGRequestScreenCaptureAccess()
}
static var isAccessibilityGranted: Bool { AXIsProcessTrusted() }
static func requestAccessibility() {
let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
_ = AXIsProcessTrustedWithOptions(opts)
}
}
enum LoginItem {
static var isEnabled: Bool {
SMAppService.mainApp.status == .enabled
}
static func toggle() -> Bool {
let service = SMAppService.mainApp
do {
if service.status == .enabled {
try service.unregister()
} else {
try service.register()
}
return true
} catch {
return false
}
}
}
func renderTestImage(_ text: String, fontSize: CGFloat = 56) -> CGImage? {
let size = NSSize(width: 1900, height: 220)
guard let rep = NSBitmapImageRep(
bitmapDataPlanes: nil, pixelsWide: Int(size.width), pixelsHigh: Int(size.height),
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
) else { return nil }
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
NSColor.white.setFill()
NSRect(origin: .zero, size: size).fill()
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.systemFont(ofSize: fontSize),
.foregroundColor: NSColor.black,
]
text.draw(at: NSPoint(x: 60, y: 75), withAttributes: attrs)
NSGraphicsContext.restoreGraphicsState()
return rep.cgImage
}
func runOCRSelfTest() {
let cases: [(expected: String, contains: String)] = [
("Kapture OCR test 1234, hello world", "Kapture OCR test 1234"),
("你好世界,这是一个测试", "你好世界"),
]
var index = 0
func runNext() {
guard index < cases.count else { exit(0) }
let current = cases[index]
index += 1
guard let image = renderTestImage(current.expected) else { exit(1) }
OCRService.shared.recognize(image) { recognized in
print("case: \(current.expected)")
print("got: \(recognized)")
let ok = recognized.contains(current.contains)
print(ok ? "PASS" : "FAIL")
if !ok { exit(1) }
runNext()
}
}
runNext()
RunLoop.main.run()
}