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
170 lines
5.6 KiB
Swift
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()
|
|
}
|