Skip to content

Clipboard Engine

Last Updated: 2026-02-03 | Reading Time: 20 minutes

Deep dive into PasteShelf’s clipboard monitoring and capture system.



The Clipboard Engine is the core component responsible for monitoring macOS clipboard changes and capturing content efficiently.

FeatureDescription
Real-time monitoringCaptures clipboard changes within 250ms
Multi-type supportText, images, files, RTF, HTML, and more
Smart deduplicationPrevents duplicate entries
Sensitive detectionIdentifies passwords, keys, and PII
App awarenessTracks source application
Memory efficientHandles large content gracefully

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Clipboard Engine β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ System Layer β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ NSPasteboard.general β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ (macOS Clipboard System) β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Monitor Layer β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ ClipboardMonitor β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ ───────────────── β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Timer β”‚ β”‚ Change Countβ”‚ β”‚ Source β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ (250ms) │───▢│ Tracker │───▢│ Detector β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Processing Layer β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ ContentParser β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ ───────────── β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Text β”‚ β”‚ Image β”‚ β”‚ File β”‚ β”‚ Rich β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Parser β”‚ β”‚ Parser β”‚ β”‚ Parser β”‚ β”‚ Parser β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ SensitiveDataDetector β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ Deduplicator β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Storage Layer β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ StorageManager β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ ────────────── β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β€’ CoreData persistence β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β€’ Search index update β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β€’ CloudKit sync trigger ⭐ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Copy │────▢│ Detect │────▢│ Parse │────▢│ Filter │────▢│ Store β”‚
β”‚ Event β”‚ β”‚ Change β”‚ β”‚ Content β”‚ β”‚ & Check β”‚ β”‚ Item β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ changeCount NSPasteboard Exclusions CoreData
β”‚ check types Sensitive CloudKit
β”‚ 250ms Duplicate

TypeUTIDescription
Plain Textpublic.utf8-plain-textBasic text content
Rich Textpublic.rtfFormatted text (RTF)
HTMLpublic.htmlWeb content
PNG Imagepublic.pngPNG images
TIFF Imagepublic.tiffTIFF images
JPEG Imagepublic.jpegJPEG images
PDFcom.adobe.pdfPDF documents
File URLpublic.file-urlFile references
URLpublic.urlWeb URLs
enum ContentType: String, CaseIterable {
case plainText = "public.utf8-plain-text"
case richText = "public.rtf"
case html = "public.html"
case png = "public.png"
case tiff = "public.tiff"
case jpeg = "public.jpeg"
case pdf = "com.adobe.pdf"
case fileURL = "public.file-url"
case url = "public.url"
/// Priority for type selection (lower = higher priority)
var priority: Int {
switch self {
case .richText: return 1
case .html: return 2
case .plainText: return 3
case .png: return 4
case .jpeg: return 5
case .tiff: return 6
case .pdf: return 7
case .url: return 8
case .fileURL: return 9
}
}
/// Icon for display
var icon: String {
switch self {
case .plainText: return "doc.text"
case .richText: return "doc.richtext"
case .html: return "chevron.left.forwardslash.chevron.right"
case .png, .jpeg, .tiff: return "photo"
case .pdf: return "doc.fill"
case .fileURL: return "folder"
case .url: return "link"
}
}
}

When clipboard contains multiple representations:

class ContentParser {
/// Parse clipboard content, preferring richest representation
func parse(_ pasteboard: NSPasteboard) -> ClipboardContent? {
var content = ClipboardContent()
// Get all available types sorted by priority
let availableTypes = pasteboard.types?
.compactMap { ContentType(rawValue: $0.rawValue) }
.sorted { $0.priority < $1.priority } ?? []
guard !availableTypes.isEmpty else { return nil }
// Primary type is highest priority available
content.primaryType = availableTypes.first!
// Extract all representations
for type in availableTypes {
switch type {
case .plainText:
content.plainText = pasteboard.string(forType: .string)
case .richText:
content.rtfData = pasteboard.data(forType: .rtf)
case .html:
content.html = pasteboard.string(forType: .html)
case .png, .jpeg, .tiff:
if let image = NSImage(pasteboard: pasteboard) {
content.image = image
content.imageData = image.tiffRepresentation
}
case .fileURL:
content.fileURLs = pasteboard.readObjects(
forClasses: [NSURL.self],
options: [.urlReadingFileURLsOnly: true]
) as? [URL]
case .url:
content.url = URL(string: pasteboard.string(forType: .URL) ?? "")
case .pdf:
content.pdfData = pasteboard.data(forType: .pdf)
}
}
return content
}
}

@MainActor
final class ClipboardMonitor: ObservableObject {
@Published private(set) var isMonitoring = false
private var changeCount: Int = 0
private var timer: Timer?
private let pollInterval: TimeInterval = 0.25 // 250ms
// Callbacks
var onItemCaptured: ((ClipboardItem) -> Void)?
func startMonitoring() {
guard !isMonitoring else { return }
// Initialize with current count
changeCount = NSPasteboard.general.changeCount
// Start polling timer
timer = Timer.scheduledTimer(
withTimeInterval: pollInterval,
repeats: true
) { [weak self] _ in
Task { @MainActor in
self?.checkForChanges()
}
}
// Ensure timer runs in common modes (even during UI interaction)
RunLoop.main.add(timer!, forMode: .common)
isMonitoring = true
Logger.clipboard.info("Clipboard monitoring started")
}
func stopMonitoring() {
timer?.invalidate()
timer = nil
isMonitoring = false
Logger.clipboard.info("Clipboard monitoring stopped")
}
private func checkForChanges() {
let currentCount = NSPasteboard.general.changeCount
if currentCount != changeCount {
changeCount = currentCount
captureCurrentContent()
}
}
private func captureCurrentContent() {
let pasteboard = NSPasteboard.general
// Check for exclusions first
if shouldExcludeCurrentCapture() {
Logger.clipboard.debug("Clipboard capture excluded")
return
}
// Parse content
guard let content = ContentParser().parse(pasteboard) else {
Logger.clipboard.debug("No parseable content")
return
}
// Detect sensitive data
let sensitiveResult = SensitiveDataDetector().analyze(content)
// Check for duplicates
if isDuplicate(content) {
Logger.clipboard.debug("Duplicate content, updating access time")
return
}
// Create clipboard item
let item = ClipboardItem(
content: content,
sourceApp: getSourceApp(),
isSensitive: sensitiveResult.isSensitive
)
onItemCaptured?(item)
Logger.clipboard.info("Captured clipboard item: \(item.id)")
}
}
extension ClipboardMonitor {
private func getSourceApp() -> SourceApp? {
// Get frontmost application
guard let frontApp = NSWorkspace.shared.frontmostApplication else {
return nil
}
return SourceApp(
bundleId: frontApp.bundleIdentifier ?? "unknown",
name: frontApp.localizedName ?? "Unknown",
icon: frontApp.icon
)
}
}
struct SourceApp {
let bundleId: String
let name: String
let icon: NSImage?
}
extension ClipboardMonitor {
private func shouldExcludeCurrentCapture() -> Bool {
let frontApp = NSWorkspace.shared.frontmostApplication
// Check excluded apps
if let bundleId = frontApp?.bundleIdentifier,
ExclusionManager.shared.isExcluded(bundleId: bundleId) {
return true
}
// Check if it's our own paste operation
if frontApp?.bundleIdentifier == Bundle.main.bundleIdentifier {
return true
}
// Check for private browsing
if isPrivateBrowsingActive() {
return true
}
return false
}
private func isPrivateBrowsingActive() -> Bool {
guard let frontApp = NSWorkspace.shared.frontmostApplication else {
return false
}
// Safari private window detection
if frontApp.bundleIdentifier == "com.apple.Safari" {
// Use Accessibility API to check window title
if let windowTitle = getActiveWindowTitle(),
windowTitle.contains("Private") {
return true
}
}
// Chrome incognito detection
if frontApp.bundleIdentifier == "com.google.Chrome" {
if let windowTitle = getActiveWindowTitle(),
windowTitle.contains("Incognito") {
return true
}
}
return false
}
}

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Content Processing Pipeline β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ Raw NSPasteboard β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ 1. Type Detection β”‚ β”‚
β”‚ β”‚ β€’ Enumerate available UTI types β”‚ β”‚
β”‚ β”‚ β€’ Select primary type by priority β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ 2. Content Extraction β”‚ β”‚
β”‚ β”‚ β€’ Extract data for each type β”‚ β”‚
β”‚ β”‚ β€’ Convert to internal representations β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ 3. Normalization β”‚ β”‚
β”‚ β”‚ β€’ Standardize text encoding (UTF-8) β”‚ β”‚
β”‚ β”‚ β€’ Trim whitespace β”‚ β”‚
β”‚ β”‚ β€’ Extract plain text from rich formats β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ 4. Metadata Extraction β”‚ β”‚
β”‚ β”‚ β€’ Source app info β”‚ β”‚
β”‚ β”‚ β€’ URL from web content β”‚ β”‚
β”‚ β”‚ β€’ File info (size, type, path) β”‚ β”‚
β”‚ β”‚ β€’ Character/word count β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ 5. Preview Generation β”‚ β”‚
β”‚ β”‚ β€’ Text preview (first 500 chars) β”‚ β”‚
β”‚ β”‚ β€’ Image thumbnail (256px) β”‚ β”‚
β”‚ β”‚ β€’ File icon β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ ClipboardContent (ready for storage) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
class ImageProcessor {
private let maxStorageSize: Int = 10_000_000 // 10MB
private let thumbnailSize: CGFloat = 256
func process(_ image: NSImage) -> ProcessedImage {
var processed = ProcessedImage()
// Generate thumbnail
processed.thumbnail = generateThumbnail(image)
// Get original data
if let tiffData = image.tiffRepresentation,
let bitmap = NSBitmapImageRep(data: tiffData) {
// Compress if too large
if tiffData.count > maxStorageSize {
processed.data = compressImage(bitmap)
processed.isCompressed = true
} else {
processed.data = bitmap.representation(
using: .png,
properties: [:]
)
}
}
// Extract dimensions
processed.width = Int(image.size.width)
processed.height = Int(image.size.height)
return processed
}
private func generateThumbnail(_ image: NSImage) -> Data? {
let targetSize = CGSize(width: thumbnailSize, height: thumbnailSize)
// Calculate aspect-fit size
let ratio = min(
targetSize.width / image.size.width,
targetSize.height / image.size.height
)
let newSize = CGSize(
width: image.size.width * ratio,
height: image.size.height * ratio
)
let thumbnail = NSImage(size: newSize)
thumbnail.lockFocus()
image.draw(
in: NSRect(origin: .zero, size: newSize),
from: NSRect(origin: .zero, size: image.size),
operation: .copy,
fraction: 1.0
)
thumbnail.unlockFocus()
return thumbnail.tiffRepresentation
}
private func compressImage(_ bitmap: NSBitmapImageRep) -> Data? {
// Use JPEG compression
return bitmap.representation(
using: .jpeg,
properties: [.compressionFactor: 0.8]
)
}
}

class Deduplicator {
private let storage: StorageManager
/// Check if content is duplicate of recent item
func isDuplicate(_ content: ClipboardContent) -> Bool {
let hash = computeHash(content)
// Check recent items (last 100)
let recentItems = storage.fetchRecent(limit: 100)
return recentItems.contains { $0.contentHash == hash }
}
/// Compute content hash for deduplication
func computeHash(_ content: ClipboardContent) -> String {
var hasher = SHA256()
// Hash based on primary type
switch content.primaryType {
case .plainText:
if let text = content.plainText {
hasher.update(data: Data(text.utf8))
}
case .richText:
// Hash plain text extraction for RTF
if let rtfData = content.rtfData,
let attrString = NSAttributedString(rtf: rtfData, documentAttributes: nil) {
hasher.update(data: Data(attrString.string.utf8))
}
case .png, .jpeg, .tiff:
// Hash image data
if let imageData = content.imageData {
hasher.update(data: imageData)
}
case .fileURL:
// Hash file paths
if let urls = content.fileURLs {
let paths = urls.map(\.path).joined(separator: "\n")
hasher.update(data: Data(paths.utf8))
}
default:
// Generic hash
if let plainText = content.plainText {
hasher.update(data: Data(plainText.utf8))
}
}
let digest = hasher.finalize()
return digest.map { String(format: "%02x", $0) }.joined()
}
}
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Duplicate Handling Options β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ Preferences β†’ Behavior β†’ Duplicates: β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ When duplicate content is copied: β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ ● Move existing to top (update timestamp) β”‚ β”‚
β”‚ β”‚ β—‹ Create new entry (allow duplicates) β”‚ β”‚
β”‚ β”‚ β—‹ Ignore (keep original position) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ ───────────────────────────────────────────────── β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β˜‘ Consider whitespace differences as unique β”‚ β”‚
β”‚ β”‚ β˜‘ Consider case differences as unique β”‚ β”‚
β”‚ β”‚ ☐ Consider formatting differences as unique β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

class ClipboardMonitor {
// Limit in-memory content size
private let maxInMemorySize: Int = 50_000_000 // 50MB
private func captureCurrentContent() {
// Check total pasteboard size first
let pasteboardSize = estimatePasteboardSize()
if pasteboardSize > maxInMemorySize {
Logger.clipboard.warning("Clipboard content too large: \(pasteboardSize) bytes")
// Stream to disk instead
captureToFile()
return
}
// Normal capture
// ...
}
private func estimatePasteboardSize() -> Int {
let pasteboard = NSPasteboard.general
var totalSize = 0
for type in pasteboard.types ?? [] {
if let data = pasteboard.data(forType: type) {
totalSize += data.count
}
}
return totalSize
}
}
MetricTargetNotes
Capture latency< 50msTime from copy to capture
Memory overhead< 50MBFor monitoring
CPU usage (idle)< 1%When not capturing
CPU usage (capture)< 5%During content processing
Timer accuracyΒ±50msPolling interval consistency
struct ClipboardEngineMetrics {
var captureCount: Int = 0
var duplicateCount: Int = 0
var excludedCount: Int = 0
var errorCount: Int = 0
var averageCaptureTime: TimeInterval = 0
var averageContentSize: Int = 0
var lastCaptureTime: Date?
var lastError: Error?
}

Problem: Copy operations not appearing in history
Checklist:
1. βœ… Accessibility permission granted?
System Settings β†’ Privacy & Security β†’ Accessibility β†’ PasteShelf βœ“
2. βœ… App not excluded?
Preferences β†’ Privacy β†’ Excluded Apps
3. βœ… Monitoring enabled?
Check menu bar icon is active (not grayed out)
4. βœ… Content type supported?
Some custom clipboard formats may not be supported
5. βœ… Not a private browsing window?
Private/Incognito windows are excluded by default
Problem: PasteShelf using excessive CPU
Solutions:
1. Check for runaway loop
- Restart PasteShelf
2. Large clipboard content
- Clear the system clipboard
3. Reduce history limit
- Preferences β†’ History β†’ Maximum Items
4. Disable image previews
- Preferences β†’ Display β†’ Show Thumbnails βœ—
Problem: High memory usage
Solutions:
1. Clear old history
- Preferences β†’ Privacy β†’ Clear History
2. Reduce image storage
- Preferences β†’ Storage β†’ Image Quality β†’ Lower
3. Enable auto-cleanup
- Preferences β†’ Privacy β†’ Auto-delete after: 30 days
Terminal window
# Enable debug logging
defaults write com.pasteshelf.PasteShelf DebugLogging -bool true
# View logs
log stream --predicate 'subsystem == "com.pasteshelf.PasteShelf"' --level debug
# Disable debug logging
defaults delete com.pasteshelf.PasteShelf DebugLogging

DocumentDescription
ArchitectureSystem overview
Search EngineSearch system
Privacy & SecurityData protection
PerformanceOptimization

Last updated: 2026-02-03