Skip to content

feat: add image clipboard support - #2

Merged
castroofelipee merged 2 commits into
masterfrom
feature/support-to-images
Aug 4, 2026
Merged

castroofelipee merged 2 commits into
masterfrom
feature/support-to-images

Conversation

@castroofelipee

Copy link
Copy Markdown
Member

Summary

Adds first-class image support to CopyIT, allowing users to capture copied images in clipboard history, preview them, and restore them to the macOS pasteboard for pasting into other applications.

What changed

  • adds an image clipboard item type with persisted binary data and its original pasteboard type
  • detects PNG and TIFF first, then supports other image formats recognized by macOS through Uniform Type Identifiers
  • prioritizes image payloads when a pasteboard entry also exposes a textual representation
  • displays image thumbnails directly in clipboard history
  • restores the original image data and format when an item is selected
  • deduplicates images by binary payload while preserving the existing text deduplication behavior
  • keeps previously persisted text-only history compatible
  • updates the feature documentation to include image support

Tests

Added coverage for:

  • image item creation and metadata preservation
  • image persistence and retrieval
  • binary image deduplication
  • restoring image data to NSPasteboard

The application sources pass Swift compiler type-checking. The full xcodebuild test suite could not be run locally because the environment only provides Command Line Tools and does not have a complete Xcode installation selected.

Closes #1

@castroofelipee castroofelipee linked an issue Aug 4, 2026 that may be closed by this pull request
@castroofelipee castroofelipee self-assigned this Aug 4, 2026
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add image clipboard capture, persistence, preview, and restore on macOS

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Capture images (PNG/TIFF prioritized, then any UTI-conforming image) from NSPasteboard.
• Persist image payload + original pasteboard type and dedupe by binary data.
• Render image thumbnails in history and restore original image data back to pasteboard.
Diagram

graph TD
  PB{{"NSPasteboard"}} --> MON["ClipboardMonitor"] --> ITEM["ClipboardItem"] --> STORE[("UserDefaultsStore")] --> VM["PopoverViewModel"] --> UI["Popover UI"]
  UI --> COPY["CopyItemUseCase"] --> PB

  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _svc["Service"] ~~~ _db[("Store")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Store images on disk (cache dir) + persist only metadata/path
  • ➕ Avoids bloating UserDefaults with large Data blobs
  • ➕ Easier to enforce size limits/eviction policies (LRU)
  • ➕ Potentially faster startup/decoding for large histories
  • ➖ Requires filesystem management, cleanup, and migration logic
  • ➖ More failure modes (missing files, permissions, partial writes)
  • ➖ Slightly higher implementation complexity than Data-in-JSON
2. Use NSPasteboardItem/NSImage serialization instead of raw pasteboard bytes
  • ➕ Leverages AppKit for supported formats/conversions
  • ➕ Can normalize to a canonical representation (e.g., PNG) to simplify dedupe
  • ➖ May lose the original pasteboard type/format fidelity
  • ➖ Extra conversions can be slower and may alter metadata (color profile, etc.)

Recommendation: For an initial implementation, persisting raw image bytes + original pasteboard type (as done here) is a strong choice because it preserves fidelity and keeps the design simple. The main caveat is storage growth: consider adding a maximum image byte size, or moving to a disk-backed cache if users commonly copy large images.

Files changed (12) +133 / -12

Enhancement (8) +84 / -11
AppDelegate.swiftPlumb ClipboardItem through monitor callback +1/-2

Plumb ClipboardItem through monitor callback

• Updates ClipboardMonitoring callback wiring to receive a fully-built ClipboardItem instead of a raw String. AppDelegate now saves the emitted item directly and refreshes the view model.

ClipboardManager/App/AppDelegate.swift

ClipboardItem.swiftAdd image payload fields, factory, and payload-aware dedupe helper +30/-1

Add image payload fields, factory, and payload-aware dedupe helper

• Extends ClipboardItem with optional imageData and imagePasteboardType for persisted image support. Adds an image factory initializer and a hasSamePayload helper that compares image bytes for image items and content for non-image items.

ClipboardManager/Core/Models/ClipboardItem.swift

ClipboardItemType.swiftIntroduce .image clipboard item type +1/-0

Introduce .image clipboard item type

• Adds a new ClipboardItemType case for images so the rest of the app can branch on image vs text/url/unknown behavior.

ClipboardManager/Core/Models/ClipboardItemType.swift

ClipboardMonitoring.swiftChange monitoring callback to emit ClipboardItem objects +1/-1

Change monitoring callback to emit ClipboardItem objects

• Updates ClipboardMonitoring.onNewItem signature from String to ClipboardItem to support non-text payloads (images).

ClipboardManager/Core/Protocols/ClipboardMonitoring.swift

ClipboardMonitor.swiftDetect and emit image clipboard items (UTType-based) before text +23/-2

Detect and emit image clipboard items (UTType-based) before text

• Adds image detection that prioritizes PNG and TIFF, then falls back to any pasteboard type whose UTI conforms to .image. If an image is found, emits an image ClipboardItem and skips string handling, ensuring images win when multiple representations exist.

ClipboardManager/Core/Services/ClipboardMonitor.swift

CopyItemUseCase.swiftRestore images to NSPasteboard using original type + bytes +7/-1

Restore images to NSPasteboard using original type + bytes

• Writes image items back to the pasteboard with setData(forType:) using the preserved pasteboard type. Retains existing string behavior for non-image items.

ClipboardManager/Core/UseCases/CopyItemUseCase.swift

UserDefaultsStore.swiftDeduplicate history by image bytes for images, text content otherwise +1/-1

Deduplicate history by image bytes for images, text content otherwise

• Switches dedupe logic to use ClipboardItem.hasSamePayload, enabling binary deduplication for images while preserving text/url dedupe semantics. Existing persisted text-only history remains decodable via optional fields.

ClipboardManager/Infrastructure/Persistence/UserDefaultsStore.swift

ClipboardItemRow.swiftRender image thumbnails in history rows +20/-3

Render image thumbnails in history rows

• Adds an itemPreview that decodes NSImage from stored imageData and displays a thumbnail for image items. Falls back to the existing SF Symbol icon for non-image types and adds a photo icon mapping for .image.

ClipboardManager/UI/Popover/ClipboardItemRow.swift

Tests (3) +48 / -0
ClipboardMonitorTests.swiftAdd unit test for image item factory metadata preservation +13/-0

Add unit test for image item factory metadata preservation

• Introduces a test ensuring ClipboardItem.from(imageData:pasteboardType:) sets type, payload bytes, and pasteboard type correctly.

Tests/CoreTests/ClipboardMonitorTests.swift

ClipboardStoreTests.swiftAdd tests for image persistence and binary deduplication +20/-0

Add tests for image persistence and binary deduplication

• Adds coverage verifying imageData survives a save/fetch cycle and that image items are deduped by Data rather than the shared display label ("Image").

Tests/CoreTests/ClipboardStoreTests.swift

UseCaseTests.swiftTest writing image data back to NSPasteboard +15/-0

Test writing image data back to NSPasteboard

• Adds a test using an isolated NSPasteboard instance to validate CopyItemUseCase writes image bytes under the expected pasteboard type.

Tests/CoreTests/UseCaseTests.swift

Documentation (1) +1 / -1
README.mdDocument image support in feature list +1/-1

Document image support in feature list

• Updates the feature list to state that clipboard history tracks text, URLs, and images.

README.md

@qodo-code-review

qodo-code-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. UserDefaults stores image blobs ✓ Resolved 🐞 Bug ➹ Performance
Description
ClipboardItem now includes full imageData, and UserDefaultsStore JSON-encodes/rewrites the entire
history on each save, making each clipboard event’s persistence work proportional to total stored
image bytes. This can significantly increase CPU/time and storage usage when users copy large images
(e.g., screenshots).
Code

ClipboardManager/Core/Models/ClipboardItem.swift[R8-9]

+    let imageData: Data?
+    let imagePasteboardType: String?
Evidence
The PR introduces imageData: Data? on the persisted model, and the store persists the full
[ClipboardItem] via JSONEncoder into a single UserDefaults key; with images present, every
save/fetch now processes large binary payloads repeatedly.

ClipboardManager/Core/Models/ClipboardItem.swift[3-48]
ClipboardManager/Infrastructure/Persistence/UserDefaultsStore.swift[15-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ClipboardItem` now carries raw image bytes (`Data`), and the existing `UserDefaultsStore` persists the entire `[ClipboardItem]` by JSON-encoding it. With images, this becomes a potentially large read/modify/write cycle per clipboard change.

## Issue Context
- `UserDefaultsStore.save()` reads + decodes all items, mutates, then re-encodes + writes the full array back.
- `Data` is JSON-encoded as base64, increasing encoded size and adding CPU overhead.

## Fix Focus Areas
- ClipboardManager/Core/Models/ClipboardItem.swift[3-48]
- ClipboardManager/Infrastructure/Persistence/UserDefaultsStore.swift[15-44]

## Suggested direction
- Persist image payloads outside `UserDefaults` (e.g., Application Support directory files, CoreData blobs, or a lightweight on-disk cache).
- Store only a stable reference in `ClipboardItem` (e.g., `imageFileName`/`imageDigest`), plus `imagePasteboardType`.
- Add an image size budget (per-item and/or total) and skip or downscale images beyond the cap.
- Keep backward compatibility by decoding older entries without the new fields and by migrating existing stored entries when first loaded.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. NSImage decoded in body ✓ Resolved 🐞 Bug ➹ Performance
Description
ClipboardItemRow constructs NSImage(data:) inside a computed view used by body, so view
invalidations can trigger repeated decoding of full image data on the main thread. This can increase
CPU/memory pressure and make the history list feel sluggish with large images.
Code

ClipboardManager/UI/Popover/ClipboardItemRow.swift[R50-53]

+        if item.type == .image,
+           let data = item.imageData,
+           let image = NSImage(data: data) {
+            Image(nsImage: image)
Evidence
The new itemPreview view decodes NSImage(data:) inline as part of body rendering, which can be
executed multiple times across list updates and state changes.

ClipboardManager/UI/Popover/ClipboardItemRow.swift[48-63]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The row view decodes raw image bytes (`NSImage(data:)`) during SwiftUI view construction. SwiftUI may reevaluate `body` frequently, which can repeatedly decode the same large payload.

## Issue Context
The preview is displayed at 40×32, but the decode input is still the full-resolution data.

## Fix Focus Areas
- ClipboardManager/UI/Popover/ClipboardItemRow.swift[48-63]

## Suggested direction
- Generate and persist a small thumbnail when ingesting the image (e.g., in `ClipboardMonitor.makeImageItem()` or when saving), and render that thumbnail in the row.
- Or cache the decoded `NSImage`/thumbnail by `ClipboardItem.id` (e.g., via an image cache in the view model or an `@StateObject` loader) so decoding happens once per item rather than on each render.
- Consider decoding off the main thread if generating thumbnails at runtime.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread ClipboardManager/Core/Models/ClipboardItem.swift Outdated
Comment thread ClipboardManager/UI/Popover/ClipboardItemRow.swift Outdated
@castroofelipee
castroofelipee merged commit 649c327 into master Aug 4, 2026
1 check passed
@castroofelipee
castroofelipee deleted the feature/support-to-images branch August 4, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support to images

1 participant