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
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user