Files
kapture/Sources/Kapture/App.swift
T
TZGyn a7cad2fc2f Terminate stale Kapture instances at launch; log topmost windows
A stale copy surviving an update keeps its own hotkey and overlays on
screen, silently eating every click meant for the new instance (matching
the empty session logs). New copies now force-terminate older instances,
and the first window scan logs the topmost on-screen windows so anything
covering the overlays is visible in diagnostics.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4-flash-0731
2026-09-09 11:59:03 +08:00

145 lines
4.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.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()
}
}
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 runOCRSelfTest() {
let text = "Kapture OCR test 1234, hello world"
let size = NSSize(width: 1600, 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 { exit(1) }
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
NSColor.white.setFill()
NSRect(origin: .zero, size: size).fill()
let attrs: [NSAttributedString.Key: Any] = [
.font: NSFont.boldSystemFont(ofSize: 56),
.foregroundColor: NSColor.black,
]
text.draw(at: NSPoint(x: 60, y: 75), withAttributes: attrs)
NSGraphicsContext.restoreGraphicsState()
guard let cgImage = rep.cgImage else { exit(1) }
OCRService.shared.recognize(cgImage) { recognized in
print("expected: \(text)")
print("got: \(recognized)")
let ok = recognized == text
print(ok ? "PASS" : "FAIL")
exit(ok ? 0 : 1)
}
RunLoop.main.run()
}