From 43c46e53b9f8689e7b8aca37e921606eccefdc10 Mon Sep 17 00:00:00 2001 From: Alvie Stoddard Date: Tue, 18 Aug 2026 20:41:38 -0700 Subject: [PATCH] Show real dimensions and paths in the clip card footer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The footer's "Image" and "1 file" told you nothing you couldn't already see from the card itself. Image clips now read their pixel dimensions - "1920 × 1080" - and file clips say which files they actually point at. What a file clip's footer says depends on what the file is. An image already shows what it is by sitting on the card, so its dimensions are the useful fact - a screenshot's path is a timestamped folder nobody reads. Everything else gets its full location instead, abbreviated with "~", because two documents of the same name differ only by where they live. Multi-file clips list every filename rather than a bare "N files" count, which named none of them. Whether a file is an image is decided from its path extension, never by opening it: the footer is evaluated on every render, so it must not touch disk. A path is worth reading in full, so the meta line wraps to three lines for file clips rather than collapsing to an ellipsis; every other type stays on one line. The footer row aligns on its bottom edge so the quick-paste hint stays put as the path grows. Dimensions come from the image file's metadata via CGImageSource rather than from decoding it, and are cached by path: the footer asks on every render, so a decode per card would be paid over and over for a number that never changes. Clips whose image file is missing or unreadable fall back to the old "Image" label, and an image file whose size can't be read falls back to its filename. --- Sources/Pesty/UI/ClipCardView.swift | 65 +++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/Sources/Pesty/UI/ClipCardView.swift b/Sources/Pesty/UI/ClipCardView.swift index a8b4725..8738c90 100644 --- a/Sources/Pesty/UI/ClipCardView.swift +++ b/Sources/Pesty/UI/ClipCardView.swift @@ -1,5 +1,7 @@ import AppKit +import ImageIO import SwiftUI +import UniformTypeIdentifiers struct ClipCardView: View { let item: ClipItem @@ -155,11 +157,15 @@ struct ClipCardView: View { .font(.system(size: 12, weight: .semibold)) .foregroundStyle(Theme.cardTextPrimary).lineLimit(1) } - HStack(spacing: 6) { + HStack(alignment: .bottom, spacing: 6) { + // A path is worth reading in full, so it wraps rather than + // collapsing to an ellipsis; everything else stays one line. Text(metaLeft) .font(.system(size: 11)) .foregroundStyle(Theme.cardTextSecondary) - .lineLimit(1) + .lineLimit(item.type == .file ? 3 : 1) + .multilineTextAlignment(.leading) + .fixedSize(horizontal: false, vertical: true) Spacer(minLength: 4) if index < 9 { HStack(spacing: 3) { @@ -175,6 +181,17 @@ struct ClipCardView: View { .padding(.top, 8) } + /// Matched on the path extension rather than by loading the file: this is + /// evaluated on every card render, so it must not touch disk. + private var isSingleImageFile: Bool { + guard item.type == .file, + item.fileURLs.count == 1, + let url = item.fileURLs.first.flatMap(URL.init(string:)), + url.isFileURL, + let type = UTType(filenameExtension: url.pathExtension) else { return false } + return type.conforms(to: .image) + } + private var metaLeft: String { switch item.type { case .text, .richText: @@ -183,9 +200,24 @@ struct ClipCardView: View { return (item.text ?? "").replacingOccurrences(of: "https://", with: "") .replacingOccurrences(of: "http://", with: "") case .file: - return "\(item.fileURLs.count) file\(item.fileURLs.count == 1 ? "" : "s")" + guard item.fileURLs.count == 1, + let url = item.fileURLs.first.flatMap(URL.init(string:)) else { + return item.fileURLs + .compactMap { URL(string: $0)?.lastPathComponent } + .joined(separator: ", ") + } + // An image shows what it is, so its size is the useful fact; a + // screenshot's path is a timestamped folder nobody reads. Files + // without a preview get the full location instead, since two + // documents of the same name differ only by where they live. + if isSingleImageFile { + guard let size = ImagePixelSize.of(url) else { return url.lastPathComponent } + return "\(Int(size.width)) × \(Int(size.height))" + } + return (url.path as NSString).abbreviatingWithTildeInPath case .image: - return "Image" + guard let size = ImagePixelSize.of(item) else { return "Image" } + return "\(Int(size.width)) × \(Int(size.height))" case .color: return item.colorHex ?? "Color" } @@ -308,3 +340,28 @@ struct ClipCardView: View { return image } } + +/// Reads an image clip's pixel dimensions from the file's metadata instead of +/// decoding it, and remembers them: the card footer asks on every render. +@MainActor +enum ImagePixelSize { + private static var cache: [String: CGSize] = [:] + + static func of(_ item: ClipItem) -> CGSize? { + guard item.imageFileName != nil, + let url = ClipboardStore.shared.imageURL(for: item) else { return nil } + return of(url) + } + + static func of(_ url: URL) -> CGSize? { + let name = url.path + if let cached = cache[name] { return cached } + guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], + let width = properties[kCGImagePropertyPixelWidth] as? Int, + let height = properties[kCGImagePropertyPixelHeight] as? Int else { return nil } + let size = CGSize(width: width, height: height) + cache[name] = size + return size + } +}