Vision defaulted to the first listed language and turned Chinese into
Latin garbage (你好世界 became 177WR). automaticallyDetectsLanguage fixes
it; the smoke test now covers Chinese so it cannot regress.
💘 Generated with Crush
Assisted-by: Crush:deepseek-v4-flash-0731
166 lines
5.4 KiB
Swift
166 lines
5.4 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.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("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()
|
|
}
|