Skip to content

Commit c939ec6

Browse files
committed
fix(perf): speed up overlay open for large entries
1 parent b6b441f commit c939ec6

3 files changed

Lines changed: 96 additions & 23 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project are documented in this file.
44

55
## [Unreleased]
66

7+
## [0.5.2] - 2026-03-27
8+
9+
### Fixed
10+
11+
- Removed overlay-open stalls caused by formatting and previewing very large clipboard items on the hot path. List titles now scan only a small prefix, inline text preview is excerpted, and inline image preview uses cached thumbnails instead of full image decodes.
12+
- Opening the overlay no longer triggers redundant search/filter resets when the search state is already at its default.
13+
714
## [0.5.1] - 2026-02-24
815

916
### Fixed
@@ -171,7 +178,8 @@ All notable changes to this project are documented in this file.
171178
- In-app preview toggle (`⌘Y`).
172179
- Delete/undo workflow (`⌘D` / `⌘Z`).
173180

174-
[Unreleased]: https://github.com/lolwierd/cb-manager/compare/v0.5.1...HEAD
181+
[Unreleased]: https://github.com/lolwierd/cb-manager/compare/v0.5.2...HEAD
182+
[0.5.2]: https://github.com/lolwierd/cb-manager/compare/v0.5.1...v0.5.2
175183
[0.5.1]: https://github.com/lolwierd/cb-manager/compare/v0.5.0...v0.5.1
176184
[0.5.0]: https://github.com/lolwierd/cb-manager/compare/v0.4.0...v0.5.0
177185
[0.4.0]: https://github.com/lolwierd/cb-manager/compare/v0.3.0...v0.4.0

Sources/CBManager/ClipboardStore.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ struct ClipboardEntry: Identifiable, Hashable, Sendable {
113113
/// Searching deep into a multi-MB clipboard entry is pointless for
114114
/// fuzzy matching and extremely expensive.
115115
private static let searchContentLimit = 500
116+
/// Maximum characters to scan when generating a one-line list title.
117+
/// The overlay only needs a compact summary, not the full payload.
118+
private static let titleLineScanLimit = 512
116119

117120
private static func computeSearchHints(kind: Kind, content: String) -> String {
118121
var hints: [String] = [kind.rawValue.lowercased()]
@@ -155,10 +158,18 @@ struct ClipboardEntry: Identifiable, Hashable, Sendable {
155158
}
156159

157160
private static func compactLine(_ text: String, limit: Int) -> String {
158-
let oneLine = text
161+
let scanEnd = text.index(text.startIndex, offsetBy: titleLineScanLimit, limitedBy: text.endIndex) ?? text.endIndex
162+
let truncatedAtSource = scanEnd != text.endIndex
163+
let excerpt = String(text[..<scanEnd])
164+
165+
let oneLine = excerpt
166+
.replacingOccurrences(of: "\r\n", with: " ")
159167
.replacingOccurrences(of: "\n", with: " ")
168+
.replacingOccurrences(of: "\r", with: " ")
160169
.trimmingCharacters(in: .whitespacesAndNewlines)
161-
guard oneLine.count > limit else { return oneLine }
170+
guard oneLine.count > limit else {
171+
return truncatedAtSource ? oneLine + "" : oneLine
172+
}
162173
return String(oneLine.prefix(limit)) + ""
163174
}
164175
}
@@ -307,8 +318,12 @@ final class ClipboardStore: ObservableObject {
307318
// Clear stale search state so the overlay opens instantly.
308319
// Skip reset when re-showing after preview dismiss.
309320
if resetSearch {
310-
query = ""
311-
selectedFilter = .all
321+
if !query.isEmpty {
322+
query = ""
323+
}
324+
if selectedFilter != .all {
325+
selectedFilter = .all
326+
}
312327
}
313328
overlayPresentedToken = UUID()
314329
}

Sources/CBManager/Views/SearchOverlayView.swift

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ import Carbon
33
import SwiftUI
44

55
struct SearchOverlayView: View {
6+
private static let inlinePreviewCharacterLimit = 12_000
7+
private static let metadataScanCharacterLimit = 20_000
8+
69
@ObservedObject var store: ClipboardStore
710
let onClose: () -> Void
811
let onConfirm: (ClipboardEntry) -> Void
@@ -220,10 +223,19 @@ struct SearchOverlayView: View {
220223
if selectedEntry.kind == .image {
221224
imagePreview(for: selectedEntry, availableSize: geometry.size)
222225
} else {
223-
Text(selectedEntry.content)
224-
.font(font(for: selectedEntry.kind))
225-
.textSelection(.enabled)
226-
.frame(maxWidth: .infinity, alignment: .topLeading)
226+
VStack(alignment: .leading, spacing: 10) {
227+
Text(overlayPreviewText(for: selectedEntry))
228+
.font(font(for: selectedEntry.kind))
229+
.textSelection(.enabled)
230+
.frame(maxWidth: .infinity, alignment: .topLeading)
231+
232+
if isInlinePreviewTruncated(for: selectedEntry) {
233+
Text("Showing an excerpt here for speed. Use ⌘Y for the full preview.")
234+
.font(.system(size: 11, weight: .regular, design: .rounded))
235+
.foregroundStyle(.secondary)
236+
}
237+
}
238+
.frame(maxWidth: .infinity, alignment: .topLeading)
227239
}
228240

229241
Divider().overlay(.white.opacity(0.08))
@@ -257,18 +269,26 @@ struct SearchOverlayView: View {
257269
@ViewBuilder
258270
private func imagePreview(for entry: ClipboardEntry, availableSize: CGSize) -> some View {
259271
if let imagePath = entry.imagePath,
260-
let image = NSImage(contentsOfFile: imagePath) {
261-
let previewHeight = adaptiveImagePreviewHeight(for: availableSize, imageSize: image.size)
272+
let imageSize = ThumbnailCache.imageDimensions(at: imagePath) {
273+
let previewHeight = adaptiveImagePreviewHeight(for: availableSize, imageSize: imageSize)
274+
let maxPixelSize = max(availableSize.width, availableSize.height) * 2
275+
let previewImage = ThumbnailCache.shared.thumbnail(for: imagePath, maxPixelSize: maxPixelSize)
262276

263277
ZStack {
264278
RoundedRectangle(cornerRadius: 12, style: .continuous)
265279
.fill(.white.opacity(0.05))
266280

267-
Image(nsImage: image)
268-
.resizable()
269-
.scaledToFit()
270-
.frame(maxWidth: .infinity, maxHeight: .infinity)
271-
.padding(8)
281+
if let previewImage {
282+
Image(nsImage: previewImage)
283+
.resizable()
284+
.scaledToFit()
285+
.frame(maxWidth: .infinity, maxHeight: .infinity)
286+
.padding(8)
287+
} else {
288+
Text("Image preview unavailable")
289+
.font(.system(size: 12, weight: .regular, design: .rounded))
290+
.foregroundStyle(.secondary)
291+
}
272292
}
273293
.frame(maxWidth: .infinity)
274294
.frame(height: previewHeight)
@@ -449,11 +469,6 @@ struct SearchOverlayView: View {
449469
}
450470

451471
private func metadataRows(for entry: ClipboardEntry, contentForStats: String) -> [(title: String, value: String)] {
452-
let chars = contentForStats.count
453-
let words = contentForStats
454-
.split(whereSeparator: { $0.isWhitespace || $0.isNewline })
455-
.count
456-
457472
if entry.kind == .image,
458473
let imagePath = entry.imagePath,
459474
let size = ThumbnailCache.imageDimensions(at: imagePath) {
@@ -480,15 +495,50 @@ struct SearchOverlayView: View {
480495
return rows
481496
}
482497

498+
let stats = inlineMetadataStats(for: contentForStats)
483499
return [
484500
("Type", entry.kind.rawValue),
485501
("Source", entry.sourceApp ?? "Unknown"),
486-
("Characters", "\(chars)"),
487-
("Words", "\(words)"),
502+
("Characters", stats.characterLabel),
503+
("Words", stats.wordLabel),
488504
("Copied", entry.date.formatted(date: .abbreviated, time: .shortened))
489505
]
490506
}
491507

508+
private func overlayPreviewText(for entry: ClipboardEntry) -> String {
509+
let preview = truncatedPrefix(
510+
of: entry.content,
511+
limit: Self.inlinePreviewCharacterLimit
512+
)
513+
return preview.text
514+
}
515+
516+
private func isInlinePreviewTruncated(for entry: ClipboardEntry) -> Bool {
517+
truncatedPrefix(
518+
of: entry.content,
519+
limit: Self.inlinePreviewCharacterLimit
520+
).truncated
521+
}
522+
523+
private func inlineMetadataStats(for content: String) -> (characterLabel: String, wordLabel: String) {
524+
let preview = truncatedPrefix(
525+
of: content,
526+
limit: Self.metadataScanCharacterLimit
527+
)
528+
let characterCount = preview.text.count
529+
let wordCount = preview.text
530+
.split(whereSeparator: { $0.isWhitespace || $0.isNewline })
531+
.count
532+
533+
let suffix = preview.truncated ? "+" : ""
534+
return ("\(characterCount)\(suffix)", "\(wordCount)\(suffix)")
535+
}
536+
537+
private func truncatedPrefix(of text: String, limit: Int) -> (text: String, truncated: Bool) {
538+
let end = text.index(text.startIndex, offsetBy: limit, limitedBy: text.endIndex) ?? text.endIndex
539+
return (String(text[..<end]), end != text.endIndex)
540+
}
541+
492542
private func font(for kind: ClipboardEntry.Kind) -> Font {
493543
switch kind {
494544
case .code, .path:

0 commit comments

Comments
 (0)