Files
TZGyn c074be3378 Initial commit: a Copper-style panel for prompts and photos
Keeps text prompts and photos in a floating always-on-top panel and
copies them back to the clipboard on click. Double-Shift captures the
selection from any app; sections, search, copy-as-list, merge, and
local JSON persistence mirror shadcn's Copper workflow.

💘 Generated with Crush

Assisted-by: Crush:kimi-k3
2026-10-07 20:54:49 +08:00

79 lines
2.8 KiB
Swift

import AppKit
// Capture reads the current selection by simulating Cmd+C, watching the
// pasteboard change count, then restoring the previous clipboard contents so
// the user's own clipboard is untouched. Requires Accessibility permission
// for the synthesized events to be delivered.
enum Capture {
static func captureSelectionIntoStore() {
let sourceApp = NSWorkspace.shared.frontmostApplication?.localizedName
guard let text = captureSelection() else { return }
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return }
Task { @MainActor in
Store.shared.addText(text, sourceApp: sourceApp)
PanelController.shared.show()
}
}
static func captureSelection() -> String? {
let pasteboard = NSPasteboard.general
let backup = backupPasteboard(pasteboard)
let oldChangeCount = pasteboard.changeCount
simulateCopy()
let deadline = Date().addingTimeInterval(0.5)
var result: String?
while Date() < deadline {
if pasteboard.changeCount != oldChangeCount {
result = pasteboard.string(forType: .string)
break
}
usleep(10_000)
}
restorePasteboard(pasteboard, backup: backup)
return result
}
private static func simulateCopy() {
let source = CGEventSource(stateID: .hidSystemState)
let keyCodeC: CGKeyCode = 8 // kVK_ANSI_C
let keyDown = CGEvent(keyboardEventSource: source, virtualKey: keyCodeC, keyDown: true)
keyDown?.flags = .maskCommand
keyDown?.post(tap: .cghidEventTap)
let keyUp = CGEvent(keyboardEventSource: source, virtualKey: keyCodeC, keyDown: false)
keyUp?.flags = .maskCommand
keyUp?.post(tap: .cghidEventTap)
}
private static func backupPasteboard(_ pasteboard: NSPasteboard) -> [[String: Data]] {
var backup: [[String: Data]] = []
for item in pasteboard.pasteboardItems ?? [] {
var entry: [String: Data] = [:]
for type in item.types {
if let data = item.data(forType: type) {
entry[type.rawValue] = data
}
}
backup.append(entry)
}
return backup
}
private static func restorePasteboard(_ pasteboard: NSPasteboard, backup: [[String: Data]]) {
pasteboard.clearContents()
let items = backup.map { entry -> NSPasteboardItem in
let item = NSPasteboardItem()
for (type, data) in entry {
item.setData(data, forType: NSPasteboard.PasteboardType(type))
}
return item
}
if !items.isEmpty {
pasteboard.writeObjects(items)
}
}
}