From 28f50c84a627e1b096ec93e7ffaf239c2fe82fee Mon Sep 17 00:00:00 2001 From: Akshay Kolli Date: Tue, 30 Jun 2026 01:43:23 -0700 Subject: [PATCH] WIP: add Quick Look preview groundwork --- Package.swift | 1 + .../services/ClipboardCacheService.swift | 27 ++++++++++++++ .../views/ClipboardPanelController.swift | 36 +++++++++++++++++-- .../views/ClipboardPanelViewModel.swift | 9 +++++ 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 9d7c495..6de5404 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ let package = Package( .linkedFramework("AppKit"), .linkedFramework("Carbon"), .linkedFramework("LocalAuthentication"), + .linkedFramework("QuickLook"), .linkedFramework("Security"), .linkedFramework("Vision"), .linkedLibrary("sqlite3") diff --git a/sources/clipbored/services/ClipboardCacheService.swift b/sources/clipbored/services/ClipboardCacheService.swift index fe554f7..addfc94 100644 --- a/sources/clipbored/services/ClipboardCacheService.swift +++ b/sources/clipbored/services/ClipboardCacheService.swift @@ -182,6 +182,23 @@ final class ClipboardCacheService { } } + func temporaryPreviewURL(for item: ClipboardItem) -> URL? { + switch item.kind { + case .file: + let urls = FilePayload.urls(from: item.payload) + return urls.first { fileManager.fileExists(atPath: $0.path) } + case .text, .unknown: + let text = item.payload.clipboardTrimmed.isEmpty ? item.displayText : item.payload + guard !text.clipboardTrimmed.isEmpty else { return nil } + return writeTemporaryCopy(data: Data(text.utf8), id: item.id, fileExtension: "txt") + case .url: + guard let data = webLocationData(for: item.payload) else { return nil } + return writeTemporaryCopy(data: data, id: item.id, fileExtension: "webloc") + case .image, .pdf, .audio, .richText: + return temporaryReadableURL(for: item) + } + } + func encryptCachedReferencesIfNeeded(for items: [ClipboardItem]) { queue.async { [weak self] in guard let self else { return } @@ -320,6 +337,16 @@ final class ClipboardCacheService { } } + private func webLocationData(for value: String) -> Data? { + let trimmed = value.clipboardTrimmed + guard !trimmed.isEmpty else { return nil } + return try? PropertyListSerialization.data( + fromPropertyList: ["URL": trimmed], + format: .xml, + options: 0 + ) + } + private func removeTemporaryPreviewFiles() { guard fileManager.fileExists(atPath: temporaryPreviewDirectory.path) else { return diff --git a/sources/clipbored/views/ClipboardPanelController.swift b/sources/clipbored/views/ClipboardPanelController.swift index 1721cd7..3f52411 100644 --- a/sources/clipbored/views/ClipboardPanelController.swift +++ b/sources/clipbored/views/ClipboardPanelController.swift @@ -1,4 +1,5 @@ import AppKit +import QuickLook struct ClipboardPanelAnimationProfile { let showDuration: TimeInterval @@ -15,10 +16,11 @@ struct ClipboardPanelReflowPlan { enum ClipboardPanelShortcutAction: Equatable { case copy case open + case preview case reveal } -final class ClipboardPanelController: NSObject, NSWindowDelegate { +final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate { private enum Animation { static let showDuration: TimeInterval = 0.16 static let hideDuration: TimeInterval = 0.12 @@ -44,6 +46,7 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate { private let preferredScreenProvider: () -> NSScreen? private let openSettings: () -> Void private var isAnimating = false + private var quickLookURL: URL? private var screenParametersObserver: NSObjectProtocol? private static let collectionShortcuts: [UInt16: ClipboardSortMode] = [ 18: .mostRecent, @@ -82,7 +85,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate { panelView = ClipboardPanelView( viewModel: viewModel, onClose: { [weak self] in self?.hide() }, - onSettings: { [weak self] in self?.openSettings() } + onSettings: { [weak self] in self?.openSettings() }, + onPreview: { [weak self] in self?.previewSelected() } ) let contentSize = NSSize(width: 1200, height: 420) @@ -333,6 +337,9 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate { case 53: self.hide() return nil + case 49: + self.previewSelected() + return nil case 36: self.viewModel.pasteSelected() return nil @@ -360,11 +367,26 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate { viewModel.copySelected() case .open: viewModel.openSelected() + case .preview: + previewSelected() case .reveal: viewModel.revealSelected() } } + private func previewSelected() { + guard let url = viewModel.previewURLForSelected() else { return } + quickLookURL = url + guard let previewPanel = QLPreviewPanel.shared() else { + NSWorkspace.shared.open(url) + return + } + previewPanel.dataSource = self + previewPanel.delegate = self + previewPanel.currentPreviewItemIndex = 0 + previewPanel.makeKeyAndOrderFront(nil) + } + private func shouldHandlePanelKeyEvent(_ event: NSEvent) -> Bool { shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: false) } @@ -399,6 +421,8 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate { return .copy case 31: return .open + case 16: + return .preview case 15: return .reveal default: @@ -406,6 +430,14 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate { } } + func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int { + quickLookURL == nil ? 0 : 1 + } + + func previewPanel(_ panel: QLPreviewPanel!, previewItemAt index: Int) -> QLPreviewItem! { + quickLookURL as NSURL? + } + #if DEBUG var debugPanelFrame: NSRect { panel.frame diff --git a/sources/clipbored/views/ClipboardPanelViewModel.swift b/sources/clipbored/views/ClipboardPanelViewModel.swift index e7c767f..31068fc 100644 --- a/sources/clipbored/views/ClipboardPanelViewModel.swift +++ b/sources/clipbored/views/ClipboardPanelViewModel.swift @@ -168,6 +168,15 @@ final class ClipboardPanelViewModel { return pasteService.pasteboardWriters(for: visibleItems[index]) } + func previewURLForSelected() -> URL? { + guard let item = selectedItem else { return nil } + return previewURL(for: item) + } + + internal func previewURL(for item: ClipboardItem) -> URL? { + cacheService.temporaryPreviewURL(for: item) + } + func openSelected() { guard let item = selectedItem else { return } switch item.kind {