From e210d88b427dec48e87dbf193be2e78229ae6527 Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 11:58:29 +0800 Subject: [PATCH 1/8] fix: Previewing two adjacent image history items does not switch the preview content. --- Maccy/Views/PreviewItemView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Maccy/Views/PreviewItemView.swift b/Maccy/Views/PreviewItemView.swift index 9a36e7145..e2c4e71dd 100644 --- a/Maccy/Views/PreviewItemView.swift +++ b/Maccy/Views/PreviewItemView.swift @@ -49,6 +49,7 @@ struct PreviewItemView: View { } } } + .id(item.id) } else { ScrollView { Text(item.text) From a8b674d27339c60a88e0d16287bbbeb4cf172541 Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 15:30:17 +0800 Subject: [PATCH 2/8] feat: add button to save image content to Pictures folder --- Maccy/Maccy.entitlements | 4 +- Maccy/Views/ToolbarView.swift | 42 +++++++++++++++++++- Maccy/Views/en.lproj/PreviewItemView.strings | 1 + 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Maccy/Maccy.entitlements b/Maccy/Maccy.entitlements index e429db977..a2dead2a0 100644 --- a/Maccy/Maccy.entitlements +++ b/Maccy/Maccy.entitlements @@ -8,8 +8,8 @@ com.apple.security.temporary-exception.mach-lookup.global-name - $(PRODUCT_BUNDLE_IDENTIFIER)-spks - $(PRODUCT_BUNDLE_IDENTIFIER)-spki + $(PRODUCT_BUNDLE_IDENTIFIER)-spks + $(PRODUCT_BUNDLE_IDENTIFIER)-spki diff --git a/Maccy/Views/ToolbarView.swift b/Maccy/Views/ToolbarView.swift index 2e145f9b5..9bc0d6cb4 100644 --- a/Maccy/Views/ToolbarView.swift +++ b/Maccy/Views/ToolbarView.swift @@ -36,9 +36,10 @@ struct ToolbarButton: View { var body: some View { Button(action: action) { label() + .frame(maxWidth: .infinity, maxHeight: .infinity) } .buttonStyle(.plain) - .frame(height: 23) + .frame(width: 23, height: 23) .onHover(perform: { inside in if let window = appState.appDelegate?.panel { window.isMovableByWindowBackground = !inside @@ -82,11 +83,50 @@ struct ToolbarView: View { && appState.navigator.selection.items.contains { !$0.isPinned } } + private func savePreviewImage() { + guard let item = appState.navigator.leadHistoryItem, + let image = item.item.image else { + return + } + + let picturesDir = FileManager.default.urls( + for: .picturesDirectory, in: .userDomainMask + ).first ?? FileManager.default.homeDirectoryForCurrentUser + + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd-HHmmss" + let filename = "Maccy-\(formatter.string(from: item.item.firstCopiedAt)).png" + let fileURL = picturesDir.appendingPathComponent(filename) + + // Remove existing file before writing to replace it. + try? FileManager.default.removeItem(at: fileURL) + + guard let tiffData = image.tiffRepresentation, + let bitmap = NSBitmapImageRep(data: tiffData), + let pngData = bitmap.representation(using: .png, properties: [:]) else { + return + } + + try? pngData.write(to: fileURL) + + // Reveal in Finder + NSWorkspace.shared.activateFileViewerSelecting([fileURL]) + } + var body: some View { HStack { if !appState.navigator.selection.isEmpty { Spacer() + if appState.navigator.leadHistoryItem?.hasImage == true { + ToolbarButton { + savePreviewImage() + } label: { + Image(systemName: "square.and.arrow.down.on.square") + } + .help(Text("SaveImage", tableName: "PreviewItemView")) + } + ToolbarButton { withAnimation { appState.togglePin() diff --git a/Maccy/Views/en.lproj/PreviewItemView.strings b/Maccy/Views/en.lproj/PreviewItemView.strings index 2c3fb5d2d..2111cbcd8 100644 --- a/Maccy/Views/en.lproj/PreviewItemView.strings +++ b/Maccy/Views/en.lproj/PreviewItemView.strings @@ -5,4 +5,5 @@ "PinKey" = "Press {pinKey} to pin."; "UnpinKey" = "Press {pinKey} to unpin."; "DeleteKey" = "Press {deleteKey} to delete."; +"SaveImage" = "Save image to Pictures folder."; "PreviewKey" = "Press {previewKey} to toggle preview."; From ccb3dc04b59a917b7e146a23fa01e241c1945255 Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 15:38:25 +0800 Subject: [PATCH 3/8] feat: add locate button for file content --- Maccy/Observables/HistoryItemDecorator.swift | 1 + Maccy/Views/PreviewItemView.swift | 31 ++++++++++++++++++-- Maccy/Views/ToolbarView.swift | 24 ++++++++++++++- Maccy/Views/en.lproj/PreviewItemView.strings | 1 + 4 files changed, 54 insertions(+), 3 deletions(-) diff --git a/Maccy/Observables/HistoryItemDecorator.swift b/Maccy/Observables/HistoryItemDecorator.swift index 573e829d1..4c036b134 100644 --- a/Maccy/Observables/HistoryItemDecorator.swift +++ b/Maccy/Observables/HistoryItemDecorator.swift @@ -40,6 +40,7 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { } var hasImage: Bool { item.image != nil } + var hasFileURLs: Bool { !item.fileURLs.isEmpty } var previewImageGenerationTask: Task<(), Error>? var thumbnailImageGenerationTask: Task<(), Error>? diff --git a/Maccy/Views/PreviewItemView.swift b/Maccy/Views/PreviewItemView.swift index e2c4e71dd..b31fcdd73 100644 --- a/Maccy/Views/PreviewItemView.swift +++ b/Maccy/Views/PreviewItemView.swift @@ -4,6 +4,13 @@ import SwiftUI struct PreviewItemView: View { var item: HistoryItemDecorator + private func revealFile(_ url: URL) { + var isDirectory: ObjCBool = false + if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) { + NSWorkspace.shared.activateFileViewerSelecting([url]) + } + } + @ViewBuilder func previewImage(content: () -> some View) -> some View { content() @@ -50,6 +57,28 @@ struct PreviewItemView: View { } } .id(item.id) + } else if item.hasFileURLs { + ScrollView { + VStack(alignment: .leading, spacing: 4) { + ForEach(item.item.fileURLs, id: \.self) { url in + HStack { + Text(url.absoluteString.removingPercentEncoding ?? url.path) + .font(.body) + .lineLimit(1) + .truncationMode(.middle) + Spacer() + Button { + revealFile(url) + } label: { + Image(systemName: "folder") + .frame(width: 20, height: 20) + } + .buttonStyle(.plain) + .help(Text("RevealInFinder", tableName: "PreviewItemView")) + } + } + } + } } else { ScrollView { Text(item.text) @@ -57,8 +86,6 @@ struct PreviewItemView: View { } } - Spacer(minLength: 0) - Divider() .padding(.vertical) diff --git a/Maccy/Views/ToolbarView.swift b/Maccy/Views/ToolbarView.swift index 9bc0d6cb4..366521ad3 100644 --- a/Maccy/Views/ToolbarView.swift +++ b/Maccy/Views/ToolbarView.swift @@ -113,12 +113,34 @@ struct ToolbarView: View { NSWorkspace.shared.activateFileViewerSelecting([fileURL]) } + private func revealInFinder() { + guard let item = appState.navigator.leadHistoryItem else { + return + } + + let urls = item.item.fileURLs.filter { url in + var isDirectory: ObjCBool = false + return FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + } + + guard !urls.isEmpty else { return } + + NSWorkspace.shared.activateFileViewerSelecting(urls) + } + var body: some View { HStack { if !appState.navigator.selection.isEmpty { Spacer() - if appState.navigator.leadHistoryItem?.hasImage == true { + if appState.navigator.leadHistoryItem?.hasFileURLs == true { + ToolbarButton { + revealInFinder() + } label: { + Image(systemName: "folder") + } + .help(Text("RevealInFinder", tableName: "PreviewItemView")) + } else if appState.navigator.leadHistoryItem?.hasImage == true { ToolbarButton { savePreviewImage() } label: { diff --git a/Maccy/Views/en.lproj/PreviewItemView.strings b/Maccy/Views/en.lproj/PreviewItemView.strings index 2111cbcd8..d1b48e1db 100644 --- a/Maccy/Views/en.lproj/PreviewItemView.strings +++ b/Maccy/Views/en.lproj/PreviewItemView.strings @@ -6,4 +6,5 @@ "UnpinKey" = "Press {pinKey} to unpin."; "DeleteKey" = "Press {deleteKey} to delete."; "SaveImage" = "Save image to Pictures folder."; +"RevealInFinder" = "Reveal in Finder."; "PreviewKey" = "Press {previewKey} to toggle preview."; From 23d97b03d1e6e0663b570d72dfd323b5a57028de Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 16:48:16 +0800 Subject: [PATCH 4/8] feat: add title lines setting and improve title truncation in history item views --- Maccy/Extensions/Defaults.Keys+Names.swift | 1 + Maccy/Models/HistoryItem.swift | 38 ++++--- Maccy/Observables/HistoryItemDecorator.swift | 5 + Maccy/Settings/AppearanceSettingsPane.swift | 18 +++- .../en.lproj/AppearanceSettings.strings | 2 + Maccy/Views/HistoryItemView.swift | 2 + Maccy/Views/ListItemTitleView.swift | 102 +++++++++++++++--- Maccy/Views/ListItemView.swift | 23 +++- Maccy/Views/PreviewItemView.swift | 4 + 9 files changed, 159 insertions(+), 36 deletions(-) diff --git a/Maccy/Extensions/Defaults.Keys+Names.swift b/Maccy/Extensions/Defaults.Keys+Names.swift index 4a08a4bc2..eaa87b3bc 100644 --- a/Maccy/Extensions/Defaults.Keys+Names.swift +++ b/Maccy/Extensions/Defaults.Keys+Names.swift @@ -52,6 +52,7 @@ extension Defaults.Keys { static let searchVisibility = Key("searchVisibility", default: .always) static let showSpecialSymbols = Key("showSpecialSymbols", default: true) static let showTitle = Key("showTitle", default: true) + static let titleLines = Key("titleLines", default: 1) static let size = Key("historySize", default: 200) static let sortBy = Key("sortBy", default: .lastCopiedAt) static let suppressClearAlert = Key("suppressClearAlert", default: false) diff --git a/Maccy/Models/HistoryItem.swift b/Maccy/Models/HistoryItem.swift index 08e7b726a..d77b35074 100644 --- a/Maccy/Models/HistoryItem.swift +++ b/Maccy/Models/HistoryItem.swift @@ -85,6 +85,25 @@ class HistoryItem { } } + func formatForDisplay(_ raw: String, replaceNewlines: Bool) -> String { + var result = raw + if Defaults[.showSpecialSymbols] { + if let range = result.range(of: "^ +", options: .regularExpression) { + result = result.replacingOccurrences(of: " ", with: "·", range: range) + } + if let range = result.range(of: " +$", options: .regularExpression) { + result = result.replacingOccurrences(of: " ", with: "·", range: range) + } + if replaceNewlines { + result = result.replacingOccurrences(of: "\n", with: "⏎") + } + result = result.replacingOccurrences(of: "\t", with: "⇥") + } else { + result = result.trimmingCharacters(in: .whitespacesAndNewlines) + } + return result + } + func generateTitle() -> String { guard image == nil else { Task { @@ -94,23 +113,8 @@ class HistoryItem { } // 1k characters is trade-off for performance - var title = previewableText.shortened(to: 1_000) - - if Defaults[.showSpecialSymbols] { - if let range = title.range(of: "^ +", options: .regularExpression) { - title = title.replacingOccurrences(of: " ", with: "·", range: range) - } - if let range = title.range(of: " +$", options: .regularExpression) { - title = title.replacingOccurrences(of: " ", with: "·", range: range) - } - title = title - .replacingOccurrences(of: "\n", with: "⏎") - .replacingOccurrences(of: "\t", with: "⇥") - } else { - title = title.trimmingCharacters(in: .whitespacesAndNewlines) - } - - return title + let raw = previewableText.shortened(to: 1_000) + return formatForDisplay(raw, replaceNewlines: true) } var previewableText: String { diff --git a/Maccy/Observables/HistoryItemDecorator.swift b/Maccy/Observables/HistoryItemDecorator.swift index 4c036b134..5ad1bb419 100644 --- a/Maccy/Observables/HistoryItemDecorator.swift +++ b/Maccy/Observables/HistoryItemDecorator.swift @@ -51,6 +51,11 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { // 10k characters seems to be more than enough on large displays var text: String { item.previewableText.shortened(to: 10_000) } + /// `text` with `showSpecialSymbols` applied, but newlines preserved for multi-line truncation. + var displayText: String { + item.formatForDisplay(text, replaceNewlines: false) + } + var isPinned: Bool { item.pin != nil } var isUnpinned: Bool { item.pin == nil } diff --git a/Maccy/Settings/AppearanceSettingsPane.swift b/Maccy/Settings/AppearanceSettingsPane.swift index 2ad25f2fa..d7d61ce4d 100644 --- a/Maccy/Settings/AppearanceSettingsPane.swift +++ b/Maccy/Settings/AppearanceSettingsPane.swift @@ -8,6 +8,7 @@ struct AppearanceSettingsPane: View { @Default(.popupScreen) private var popupScreen @Default(.pinTo) private var pinTo @Default(.imageMaxHeight) private var imageHeight + @Default(.titleLines) private var titleLines @Default(.previewDelay) private var previewDelay @Default(.highlightMatch) private var highlightMatch @Default(.menuIcon) private var menuIcon @@ -26,7 +27,12 @@ struct AppearanceSettingsPane: View { formatter.maximum = 200 return formatter }() - + private let titleLinesFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.minimum = 1 + formatter.maximum = 5 + return formatter + }() private let numberOfItemsFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.minimum = 0 @@ -100,6 +106,16 @@ struct AppearanceSettingsPane: View { } } + Settings.Section(label: { Text("TitleLines", tableName: "AppearanceSettings") }) { + HStack { + TextField("", value: $titleLines, formatter: titleLinesFormatter) + .frame(width: 120) + .help(Text("TitleLinesTooltip", tableName: "AppearanceSettings")) + Stepper("", value: $titleLines, in: 1...5) + .labelsHidden() + } + } + Settings.Section(label: { Text("PreviewDelay", tableName: "AppearanceSettings") }) { HStack { TextField("", value: $previewDelay, formatter: previewDelayFormatter) diff --git a/Maccy/Settings/en.lproj/AppearanceSettings.strings b/Maccy/Settings/en.lproj/AppearanceSettings.strings index 0a40f573a..7c95000ab 100644 --- a/Maccy/Settings/en.lproj/AppearanceSettings.strings +++ b/Maccy/Settings/en.lproj/AppearanceSettings.strings @@ -16,6 +16,8 @@ "PinToTooltip" = "Change the location of pinned items.\nDefault: Top."; "ImageHeight" = "Image height:"; "ImageHeightTooltip" = "Maximum image preview height.\nDefault: 40.\nHint: Set to 16 to look like text items."; +"TitleLines" = "Title lines:"; +"TitleLinesTooltip" = "Number of lines displayed per history item.\nDefault: 1.\nHint: Increase to see more text in each item."; "PreviewDelay" = "Preview delay:"; "PreviewDelayTooltip" = "Delay in milliseconds until a preview popup is shown.\nDefault: 1500."; "HighlightMatches" = "Highlight matches:"; diff --git a/Maccy/Views/HistoryItemView.swift b/Maccy/Views/HistoryItemView.swift index 656140aee..4654b56af 100644 --- a/Maccy/Views/HistoryItemView.swift +++ b/Maccy/Views/HistoryItemView.swift @@ -39,6 +39,7 @@ struct HistoryItemView: View { image: item.thumbnailImage, accessoryImage: item.thumbnailImage != nil ? nil : ColorImage.from(item.title), attributedTitle: item.attributedTitle, + rawTitle: item.displayText, shortcuts: item.shortcuts, isSelected: item.isSelected, selectionIndex: visualIndex, @@ -46,6 +47,7 @@ struct HistoryItemView: View { ) { Text(verbatim: item.title) } + .padding(.vertical, 0.5) .onAppear { item.ensureThumbnailImage() } diff --git a/Maccy/Views/ListItemTitleView.swift b/Maccy/Views/ListItemTitleView.swift index 33d9a88c5..f91dccf3c 100644 --- a/Maccy/Views/ListItemTitleView.swift +++ b/Maccy/Views/ListItemTitleView.swift @@ -1,23 +1,99 @@ +import Defaults import SwiftUI +// MARK: - Truncation helpers + +/// Font matching SwiftUI's default `.body` on macOS. +private let bodyFont = NSFont.systemFont(ofSize: NSFont.systemFontSize) + +/// Truncate a single line by inserting " ... " in the middle if it exceeds maxWidth. +/// Uses NSAttributedString for accurate pixel-width measurement. +func truncateLine(_ text: String, maxWidth: CGFloat) -> String { + let ellipsis = " ... " + let attrs: [NSAttributedString.Key: Any] = [.font: bodyFont] + + func width(of s: String) -> CGFloat { + NSAttributedString(string: s, attributes: attrs).size().width + } + + guard width(of: text) > maxWidth else { return text } + + let chars = Array(text) + + // Binary search the longest prefix that still allows suffix + ellipsis to fit. + var lo = 0 + var hi = chars.count / 2 + while lo < hi { + let mid = (lo + hi + 1) / 2 + let suffixStart = chars.count - mid + let candidate = String(chars[0.. String { + let lines = raw.components(separatedBy: .newlines) + let capped = lines.prefix(maxLines) + return capped.map { truncateLine($0, maxWidth: maxWidth) }.joined(separator: "\n") +} + +// MARK: - ListItemTitleView + struct ListItemTitleView: View { var attributedTitle: AttributedString? + var rawTitle: String? @ViewBuilder var title: () -> Title + @Default(.titleLines) private var titleLines + @Default(.windowSize) private var windowSize + @Default(.showApplicationIcons) private var showIcons + + /// Estimated text area width: window width minus padding for icons, spacers, + /// shortcuts, and trailing margin. + private var estimatedTextWidth: CGFloat { + let base = windowSize.width + let iconArea: CGFloat = showIcons ? 30 : 15 + let shortcutsArea: CGFloat = 40 + let margin: CGFloat = 20 + return max(base - iconArea - shortcutsArea - margin, 80) + } var body: some View { - if let attributedTitle { - Text(attributedTitle) - .accessibilityIdentifier("copy-history-item") - .lineLimit(1) - .truncationMode(.middle) - } else { - title() - .accessibilityIdentifier("copy-history-item") - .lineLimit(1) - .truncationMode(.middle) - // Workaround for macOS 26 to avoid flipped text - // https://github.com/p0deje/Maccy/issues/1113 - .drawingGroup() + Group { + if let attributedTitle { + Text(attributedTitle) + .accessibilityIdentifier("copy-history-item") + .lineLimit(titleLines) + .truncationMode(.middle) + } else if let rawTitle { + Text(truncateText(rawTitle, maxWidth: estimatedTextWidth, maxLines: titleLines)) + .accessibilityIdentifier("copy-history-item") + .lineLimit(titleLines) + } else { + title() + .accessibilityIdentifier("copy-history-item") + .lineLimit(titleLines) + .truncationMode(.middle) + .drawingGroup() + } } } } diff --git a/Maccy/Views/ListItemView.swift b/Maccy/Views/ListItemView.swift index 0170beb25..bf9e0ac32 100644 --- a/Maccy/Views/ListItemView.swift +++ b/Maccy/Views/ListItemView.swift @@ -35,6 +35,7 @@ struct ListItemView: View { var image: NSImage? var accessoryImage: NSImage? var attributedTitle: AttributedString? + var rawTitle: String? var shortcuts: [KeyShortcut] var isSelected: Bool var selectionIndex: Int? @@ -74,7 +75,7 @@ struct ListItemView: View { .padding(.trailing, 5) .padding(.vertical, 5) } else { - ListItemTitleView(attributedTitle: attributedTitle, title: title) + ListItemTitleView(attributedTitle: attributedTitle, rawTitle: rawTitle, title: title) .padding(.trailing, 5) } @@ -109,10 +110,22 @@ struct ListItemView: View { .frame(minHeight: Popup.itemHeight) .id(id) .frame(maxWidth: .infinity, alignment: .leading) - .foregroundStyle(isSelected ? Color.white : .primary) - // macOS 26 broke hovering if no background is present. - // The slight opcaity white background is a workaround - .background(isSelected ? Color.accentColor.opacity(0.8) : .white.opacity(0.001)) + .foregroundStyle(isSelected ? Color.accentColor : .primary) + .background( + RoundedRectangle(cornerRadius: Popup.cornerRadius) + .fill( + isSelected + ? Color.accentColor.opacity(0.12) + : Color.primary.opacity(0.03) + ) + ) + .overlay( + RoundedRectangle(cornerRadius: Popup.cornerRadius) + .strokeBorder( + isSelected ? Color.accentColor : Color.primary.opacity(0.15), + lineWidth: 1 + ) + ) .clipShape(selectionAppearance.rect(cornerRadius: Popup.cornerRadius)) .hoverSelectionId(selectionId) .help(help ?? "") diff --git a/Maccy/Views/PreviewItemView.swift b/Maccy/Views/PreviewItemView.swift index b31fcdd73..b64954d9f 100644 --- a/Maccy/Views/PreviewItemView.swift +++ b/Maccy/Views/PreviewItemView.swift @@ -1,6 +1,8 @@ import KeyboardShortcuts import SwiftUI +// MARK: - PreviewItemView + struct PreviewItemView: View { var item: HistoryItemDecorator @@ -86,6 +88,8 @@ struct PreviewItemView: View { } } + Spacer(minLength: 0) + Divider() .padding(.vertical) From 2b6e4b81efbd22d2a6a64309a5b84f56ca0fac83 Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 22:18:23 +0800 Subject: [PATCH 5/8] feat: refactor formatForDisplay to improve title handling and simplify newline processing --- Maccy/Models/HistoryItem.swift | 30 ++++++++++++++------ Maccy/Observables/HistoryItemDecorator.swift | 2 +- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Maccy/Models/HistoryItem.swift b/Maccy/Models/HistoryItem.swift index d77b35074..f9c23033b 100644 --- a/Maccy/Models/HistoryItem.swift +++ b/Maccy/Models/HistoryItem.swift @@ -85,23 +85,35 @@ class HistoryItem { } } - func formatForDisplay(_ raw: String, replaceNewlines: Bool) -> String { - var result = raw + func formatForDisplay(_ raw: String) -> String { + let titleLines = Defaults[.titleLines] + + if titleLines > 1 { + let lines = raw.components(separatedBy: "\n") + let firstLines = lines.filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty }.prefix(titleLines) + return firstLines.map { formatLine($0) }.joined(separator: "\n") + } + + if Defaults[.showSpecialSymbols] { + return formatLine(raw.replacingOccurrences(of: "\n", with: "⏎")) + } + + return raw.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func formatLine(_ line: String) -> String { if Defaults[.showSpecialSymbols] { + var result = line if let range = result.range(of: "^ +", options: .regularExpression) { result = result.replacingOccurrences(of: " ", with: "·", range: range) } if let range = result.range(of: " +$", options: .regularExpression) { result = result.replacingOccurrences(of: " ", with: "·", range: range) } - if replaceNewlines { - result = result.replacingOccurrences(of: "\n", with: "⏎") - } result = result.replacingOccurrences(of: "\t", with: "⇥") - } else { - result = result.trimmingCharacters(in: .whitespacesAndNewlines) + return result } - return result + return line.trimmingCharacters(in: .whitespaces) } func generateTitle() -> String { @@ -114,7 +126,7 @@ class HistoryItem { // 1k characters is trade-off for performance let raw = previewableText.shortened(to: 1_000) - return formatForDisplay(raw, replaceNewlines: true) + return formatForDisplay(raw) } var previewableText: String { diff --git a/Maccy/Observables/HistoryItemDecorator.swift b/Maccy/Observables/HistoryItemDecorator.swift index 5ad1bb419..2ffc551fb 100644 --- a/Maccy/Observables/HistoryItemDecorator.swift +++ b/Maccy/Observables/HistoryItemDecorator.swift @@ -53,7 +53,7 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { /// `text` with `showSpecialSymbols` applied, but newlines preserved for multi-line truncation. var displayText: String { - item.formatForDisplay(text, replaceNewlines: false) + item.formatForDisplay(text) } var isPinned: Bool { item.pin != nil } From a9c537a19b5a9cab8adb4a937cb7fba3b43cf1d9 Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 22:59:18 +0800 Subject: [PATCH 6/8] feat: optimize text handling in HistoryItemDecorator and improve display in PreviewItemView --- Maccy/Observables/HistoryItemDecorator.swift | 15 ++++++++++----- Maccy/Views/PreviewItemView.swift | 12 ++++++++++-- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Maccy/Observables/HistoryItemDecorator.swift b/Maccy/Observables/HistoryItemDecorator.swift index 2ffc551fb..99fa6eea6 100644 --- a/Maccy/Observables/HistoryItemDecorator.swift +++ b/Maccy/Observables/HistoryItemDecorator.swift @@ -48,13 +48,13 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { var thumbnailImage: NSImage? var applicationImage: ApplicationImage - // 10k characters seems to be more than enough on large displays - var text: String { item.previewableText.shortened(to: 10_000) } + // 10k characters seems to be more than enough on large displays. + // Cached as stored property to avoid recomputing previewableText (which + // may parse RTF/HTML) on every SwiftUI access. + private(set) var text: String = "" /// `text` with `showSpecialSymbols` applied, but newlines preserved for multi-line truncation. - var displayText: String { - item.formatForDisplay(text) - } + private(set) var displayText: String = "" var isPinned: Bool { item.pin != nil } var isUnpinned: Bool { item.pin == nil } @@ -74,6 +74,11 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { self.title = item.title self.applicationImage = ApplicationImageCache.shared.getImage(item: item) + // Precompute text to avoid repeated previewableText (RTF/HTML parsing) on SwiftUI reads. + let rawText = item.previewableText.shortened(to: 10_000) + self.text = rawText + self.displayText = item.formatForDisplay(rawText) + synchronizeItemPin() synchronizeItemTitle() } diff --git a/Maccy/Views/PreviewItemView.swift b/Maccy/Views/PreviewItemView.swift index b64954d9f..e8dd7e990 100644 --- a/Maccy/Views/PreviewItemView.swift +++ b/Maccy/Views/PreviewItemView.swift @@ -6,6 +6,10 @@ import SwiftUI struct PreviewItemView: View { var item: HistoryItemDecorator + private var paragraphs: [String] { + item.text.components(separatedBy: "\n") + } + private func revealFile(_ url: URL) { var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) { @@ -83,8 +87,12 @@ struct PreviewItemView: View { } } else { ScrollView { - Text(item.text) - .font(.body) + LazyVStack(alignment: .leading, spacing: 0) { + ForEach(paragraphs, id: \.self) { paragraph in + Text(paragraph) + .font(.body) + } + } } } From 0240f09a185f01193bcbe1ed7baecf82689295ca Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 23:23:42 +0800 Subject: [PATCH 7/8] fix: modify title lines would not update the old history item lines --- Maccy/Models/HistoryItem.swift | 2 +- Maccy/Observables/HistoryItemDecorator.swift | 24 ++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Maccy/Models/HistoryItem.swift b/Maccy/Models/HistoryItem.swift index f9c23033b..2843b9c54 100644 --- a/Maccy/Models/HistoryItem.swift +++ b/Maccy/Models/HistoryItem.swift @@ -101,7 +101,7 @@ class HistoryItem { return raw.trimmingCharacters(in: .whitespacesAndNewlines) } - private func formatLine(_ line: String) -> String { + func formatLine(_ line: String) -> String { if Defaults[.showSpecialSymbols] { var result = line if let range = result.range(of: "^ +", options: .regularExpression) { diff --git a/Maccy/Observables/HistoryItemDecorator.swift b/Maccy/Observables/HistoryItemDecorator.swift index 99fa6eea6..b13ea7852 100644 --- a/Maccy/Observables/HistoryItemDecorator.swift +++ b/Maccy/Observables/HistoryItemDecorator.swift @@ -51,10 +51,23 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { // 10k characters seems to be more than enough on large displays. // Cached as stored property to avoid recomputing previewableText (which // may parse RTF/HTML) on every SwiftUI access. - private(set) var text: String = "" + let text: String - /// `text` with `showSpecialSymbols` applied, but newlines preserved for multi-line truncation. - private(set) var displayText: String = "" + /// First `maxTitleLines` non-blank lines, pre-split to avoid re-splitting on every access. + private let firstLines: [String] + + /// `text` truncated to `titleLines` with `showSpecialSymbols` applied. + /// Computed so that changing `titleLines` in preferences affects all items immediately. + var displayText: String { + let lines = Defaults[.titleLines] + if lines > 1 { + return firstLines.prefix(lines).map { item.formatLine($0) }.joined(separator: "\n") + } + if Defaults[.showSpecialSymbols] { + return item.formatLine(text.replacingOccurrences(of: "\n", with: "⏎")) + } + return text.trimmingCharacters(in: .whitespacesAndNewlines) + } var isPinned: Bool { item.pin != nil } var isUnpinned: Bool { item.pin == nil } @@ -77,7 +90,10 @@ class HistoryItemDecorator: Identifiable, Hashable, HasVisibility { // Precompute text to avoid repeated previewableText (RTF/HTML parsing) on SwiftUI reads. let rawText = item.previewableText.shortened(to: 10_000) self.text = rawText - self.displayText = item.formatForDisplay(rawText) + self.firstLines = Array(rawText + .components(separatedBy: "\n") + .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } + .prefix(5)) // max titleLines from stepper range synchronizeItemPin() synchronizeItemTitle() From a920a8d2a9a497449fa27f30415595ac66cf7b0b Mon Sep 17 00:00:00 2001 From: "xinbi.nie" Date: Thu, 4 Jun 2026 23:23:53 +0800 Subject: [PATCH 8/8] feat: update paragraph rendering in PreviewItemView to use enumerated indices --- Maccy/Views/PreviewItemView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Maccy/Views/PreviewItemView.swift b/Maccy/Views/PreviewItemView.swift index e8dd7e990..135ba35cc 100644 --- a/Maccy/Views/PreviewItemView.swift +++ b/Maccy/Views/PreviewItemView.swift @@ -88,7 +88,7 @@ struct PreviewItemView: View { } else { ScrollView { LazyVStack(alignment: .leading, spacing: 0) { - ForEach(paragraphs, id: \.self) { paragraph in + ForEach(Array(paragraphs.enumerated()), id: \.offset) { _, paragraph in Text(paragraph) .font(.body) }