From aae3c96eb4589dfcab36c8f27689b4e3a72fa189 Mon Sep 17 00:00:00 2001 From: Alvie Stoddard Date: Sat, 15 Aug 2026 15:01:03 -0700 Subject: [PATCH] Copy the selected clip with Command-C - Add a Copied to Clipboard toast, shown after every successful copy (Command-C and the context menu's Copy action both go through copyItem). - Promote the copied clip to the front of history, since copying an existing clip back to the pasteboard is a deliberate re-use of it and the clipboard monitor correctly ignores Pesty's own pasteboard writes. --- Sources/Pesty/AppController.swift | 13 ++++ Sources/Pesty/Store/ClipboardStore.swift | 10 +++ Sources/Pesty/UI/CopyToastController.swift | 74 ++++++++++++++++++++++ 3 files changed, 97 insertions(+) create mode 100644 Sources/Pesty/UI/CopyToastController.swift diff --git a/Sources/Pesty/AppController.swift b/Sources/Pesty/AppController.swift index 6d3f395..431145c 100644 --- a/Sources/Pesty/AppController.swift +++ b/Sources/Pesty/AppController.swift @@ -8,6 +8,7 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate { let store = ClipboardStore.shared let monitor = ClipboardMonitor() + private let copyToast = CopyToastController() private var barController: BarWindowController? private var statusItem: NSStatusItem? @@ -324,9 +325,19 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate { } func copyItem(_ item: ClipItem) { + let previousChange = NSPasteboard.general.changeCount let change = PasteService.copy(item) monitor.suppressUntilChangeCount = change + if change != previousChange { + store.promoteCopiedItem(item) + } hideBar() + copyToast.show() + } + + func copySelected() { + guard let item = store.selectedItem else { return } + copyItem(item) } var pasteMenuTitle: String { @@ -550,6 +561,8 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate { case kVK_ForwardDelete: deleteEffectiveSelection() return nil + case kVK_ANSI_C: + if cmd { copySelected(); return nil } default: break } diff --git a/Sources/Pesty/Store/ClipboardStore.swift b/Sources/Pesty/Store/ClipboardStore.swift index c0bcd40..bb3562a 100644 --- a/Sources/Pesty/Store/ClipboardStore.swift +++ b/Sources/Pesty/Store/ClipboardStore.swift @@ -123,6 +123,16 @@ final class ClipboardStore { scheduleSave() } + /// A Copy from the Paste Bar is an intentional use of an existing clip. + /// Promote it explicitly because the clipboard monitor correctly ignores + /// Pesty's own pasteboard writes, so a plain re-copy would otherwise leave + /// the clip sitting wherever it already was in history. + func promoteCopiedItem(_ item: ClipItem, at date: Date = .now) { + var copied = item + copied.createdAt = date + addCaptured(copied) + } + func applyRetentionPolicy() { trimHistory(); scheduleSave() } func retentionRemovalCount(mode: HistoryRetentionMode, limit: Int, days: Int) -> Int { diff --git a/Sources/Pesty/UI/CopyToastController.swift b/Sources/Pesty/UI/CopyToastController.swift new file mode 100644 index 0000000..147a86a --- /dev/null +++ b/Sources/Pesty/UI/CopyToastController.swift @@ -0,0 +1,74 @@ +import AppKit +import SwiftUI + +@MainActor +final class CopyToastController { + private let panel: NSPanel + private var dismissWorkItem: DispatchWorkItem? + + init() { + panel = NSPanel( + contentRect: NSRect(x: 0, y: 0, width: 236, height: 48), + styleMask: [.borderless, .nonactivatingPanel], + backing: .buffered, + defer: false) + panel.isOpaque = false + panel.backgroundColor = .clear + panel.hasShadow = true + panel.level = .floating + panel.ignoresMouseEvents = true + panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .transient] + panel.contentView = NSHostingView(rootView: CopyToastView()) + } + + func show() { + dismissWorkItem?.cancel() + let screen = NSScreen.screens.first(where: { $0.frame.contains(NSEvent.mouseLocation) }) + ?? NSScreen.main + ?? NSScreen.screens.first + guard let screen else { return } + + let frame = screen.visibleFrame + panel.setFrameOrigin(NSPoint(x: frame.midX - panel.frame.width / 2, y: frame.minY + 34)) + panel.alphaValue = 0 + panel.orderFrontRegardless() + + NSAnimationContext.runAnimationGroup { context in + context.duration = 0.16 + panel.animator().alphaValue = 1 + } + + let dismiss = DispatchWorkItem { [weak self] in self?.dismiss() } + dismissWorkItem = dismiss + DispatchQueue.main.asyncAfter(deadline: .now() + 1.25, execute: dismiss) + } + + private func dismiss() { + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0.18 + panel.animator().alphaValue = 0 + }, completionHandler: { [weak panel] in + panel?.orderOut(nil) + }) + } +} + +private struct CopyToastView: View { + var body: some View { + HStack(spacing: 9) { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 17, weight: .semibold)) + .foregroundStyle(Theme.selection) + Text("Copied to Clipboard") + .font(.system(size: 13, weight: .semibold)) + .foregroundStyle(.primary) + } + .padding(.horizontal, 16) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.regularMaterial, in: Capsule(style: .continuous)) + .overlay { + Capsule(style: .continuous) + .strokeBorder(.white.opacity(0.28)) + } + } +}