The Invisible Complexity of Clipboard Monitoring
From 0.5s to 15s — How Deck makes clipboard monitoring both responsive and power-efficient through adaptive polling, content parsing, OOM protection, and IDE integration.
From 0.5s to 15s — How Deck Makes Clipboard Monitoring Both Responsive and Power-Efficient
The core of clipboard monitoring on macOS is straightforward: poll NSPasteboard.general.changeCount on a timer, check if the value changed, read the new content. A weekend prototype can get this working.
But a clipboard manager that runs 10+ hours a day needs to handle things the prototype ignores — battery impact from constant polling, 200 MB screenshots that blow up memory, Figma's non-standard clipboard format, and IDE source tracking across five editor families. This post covers how Deck handles these problems: adaptive polling, content-type parsing, OOM protection, IDE integration, and Figma-aware clipboard detection.
1. Adaptive Polling: Doing Less When It Matters
The naive approach is a fixed-interval timer — say, every 0.5 seconds. That's responsive, but it's also 7,200 wakeups per hour. On a laptop running on battery, that's unacceptable.
Deck solves this with an adaptive polling mechanism that dynamically adjusts the interval based on three signals: user activity, power state, and thermal pressure.
The Bounds
| Constant | Value | Purpose |
|---|---|---|
minInterval |
0.5s | Fastest poll rate (active use) / 最快轮询间隔(活跃使用时) |
maxInterval |
2.0s | Normal ceiling / 正常最大间隔 |
idleSecondsThreshold |
8.0s | Idle detection threshold / 用户空闲判定阈值 |
idleMaxInterval |
15.0s | Ceiling when user is idle / 空闲时最大间隔 |
thermalMaxInterval |
10.0s | Ceiling under thermal pressure / 热状态下最大间隔 |
pollBoundsCacheTTL |
1.0s | Cache TTL for computed bounds / 轮询边界缓存有效期 |
The core function effectivePollBounds() computes the current min/max interval by layering these conditions:
func effectivePollBounds() -> (min: TimeInterval, max: TimeInterval) {
var effectiveMin = minInterval // 0.5s
var effectiveMax = maxInterval // 2.0s
// Low Power Mode: widen the interval significantly
if ProcessInfo.processInfo.isLowPowerModeEnabled {
effectiveMin = min(effectiveMin * 2.0, 2.0)
effectiveMax = max(effectiveMax * 4.0, 8.0)
}
// Thermal pressure: back off further
if thermalState == .serious || thermalState == .critical {
effectiveMin = max(effectiveMin, 1.0)
effectiveMax = max(effectiveMax, thermalMaxInterval) // 10s
}
// User idle: stretch the max
if secondsSinceUserInteraction() > idleSecondsThreshold {
effectiveMax = max(effectiveMax, idleMaxInterval) // 15s
}
return (effectiveMin, effectiveMax)
}When the clipboard changes, the interval snaps back to min immediately. When nothing happens, it backs off by 1.5× on each tick, up to the computed max. So when the user is actively copying, the poll rate stays at 0.5s; after 8 seconds of inactivity, it gradually stretches to 15s.
func adjustPollInterval(success: Bool) {
let bounds = effectivePollBounds()
if success {
pollInterval = bounds.min
} else {
pollInterval = min(pollInterval * 1.5, bounds.max)
}
}Timer Architecture
The timer runs on a dedicated DispatchQueue at .utility QoS, not on the main thread:
let pollQueue = DispatchQueue(label: "deck.clipboard.poll", qos: .utility)
let pollTimer = DispatchSource.makeTimerSource(queue: pollQueue)A 25% leeway is applied (clamped to 50–500ms), letting macOS coalesce timer wakeups with other system timers. This reduces the actual wakeup frequency noticeably on battery.
Power State Awareness
Deck listens to NSProcessInfoPowerStateDidChange to detect Low Power Mode transitions. Beyond adjusting poll intervals, it also throttles downstream work — for example, link previews skip image fetching when running on battery:
let includeImage = !ProcessInfo.processInfo.isLowPowerModeEnabled
LinkMetadataService.shared.prefetch(for: url, includeImage: includeImage)2. ⌘C Interception: Bypassing the Poll Cycle
Polling is inherently reactive. Even at 0.5s intervals, there's a perceptible delay. Deck eliminates this by intercepting ⌘C/⌘X globally and triggering a clipboard check immediately.
func installCopyMonitor() {
NSEvent.addGlobalMonitorForEvents(matching: .keyDown) { event in
guard event.modifierFlags.contains(.command) else { return }
let keyCode = event.keyCode
// 0x08 = C, 0x07 = X
guard keyCode == 0x08 || keyCode == 0x07 else { return }
self.pendingCopyCheck?.cancel()
self.pendingCopyCheck = Task {
try? await Task.sleep(nanoseconds: 80_000_000) // 80ms
await self.checkForChanges()
}
}
}The 80ms delay is deliberate: the source app needs a moment to actually write data to NSPasteboard. Cancel-and-reschedule ensures rapid consecutive ⌘C presses don't spawn redundant checks.
3. Content Type Resolution: The Priority Ladder
NSPasteboard is not a single-value container. A single copy operation can deposit dozens of representations — PNG, TIFF, RTF, HTML, plain text, private app types — and the clipboard manager must pick the most meaningful one.
Deck defines an explicit priority order:
// PasteboardType.supportedTypes (highest → lowest)
.png, .tiff, .jpeg, .heic, .heif, .gif, .webp, .bmp, // Images
.rtfd, .flatRTFD, .rtf, // Rich text
.fileURL, // Files
.publicURL, // URLs
.string // Plain textImages take the highest priority because they're the most information-dense and hardest to reconstruct. Plain text sits at the bottom — it's almost always available as a fallback.
The Three-Level Text Fallback Chain
When the primary type can't be parsed, Deck doesn't give up. It walks a three-level fallback chain:
resolveHTMLTextPayload()— Checks for HTML content or Figma markup first.resolveUTF16PlainText()— Triespublic.utf16-external-plain-text, thenpublic.utf16-plain-text.resolveGenericTextPayload()— Iterates all types conforming toUTType.text.resolveHTMLTextPayload()resolveGenericTextPayload()
An exclusion list of ~20 types (including .string, .rtf, .html, .pdf, various WebKit/Chromium internal types, and macOS system types) prevents the fallback chain from re-processing types that were already tried or are known to be noise.
A practical optimization: when an RTFD item also carries a plain text representation, Deck prefers the plain text — avoiding the overhead of RTFD decoding when the content is textual anyway.
4. OOM Protection: Defending Against the 200 MB Screenshot
A clipboard manager that crashes when you copy a large image is worse than no clipboard manager at all. Deck implements multi-layered OOM protection with escalating thresholds:
| Threshold | Value | What it guards |
|---|---|---|
maxPerTypeBytes |
1 MB | Per-type snapshot cap / 单类型快照上限 |
maxSnapshotBytes |
4 MB | Total snapshot byte limit / 粘贴快照总字节上限 |
maxInlineImagePasteBytes |
25 MB | Beyond this, skip inline image bytes if file URL exists / 超过此值且有文件 URL 时不写入图片字节 |
largeBlobThreshold |
512 KB | Data above this is stored as a separate blob file / 超过此值存为独立 Blob 文件 |
maxImageDataSize |
100 MB | Images above this are never loaded directly / 超过此值的图片不直接加载 |
The 512 KB blob threshold is particularly interesting. Instead of storing large data inline in SQLite, Deck writes it to a separate file via BlobStorage.shared.storeAsync and stores only the path in the database. This keeps the database lean and prevents SQLite page fragmentation from large BLOBs.
For images exceeding 100 MB, resolvedData() returns nil outright — the image is simply too large to hold in memory. For display purposes, thumbnail() uses CGImageSourceCreateThumbnailAtIndex to generate a thumbnail via progressive decoding, never loading the full bitmap:
func generateSafeThumbnailFromData(_ data: Data, maxDimension: CGFloat) -> NSImage? {
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceThumbnailMaxPixelSize: maxDimension,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceShouldCacheImmediately: false
]
guard let source = CGImageSourceCreateWithData(data as CFData, nil),
let thumbnail = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary)
else { return nil }
return NSImage(cgImage: thumbnail, size: .zero)
}5. IDE Source Anchor Tracking: Know Where It Came From
Example: you copied a snippet from VS Code 20 minutes ago. You want to paste it, but you also want to jump back to where you copied it from.
Deck captures a SourceAnchor at copy time — the file path, line number, column, and IDE identity — so you can navigate back with a single click.
struct SourceAnchor {
var ide: IDEKind
var filePath: String
var line: Int?
var column: Int?
var sourceBundleId: String?
var captureMethod: String?
var capturedAt: Int64?
var openHint: IDEOpenHint?
}Supported IDEs
| IDE | Bundle ID(s) |
|---|---|
| VS Code | com.microsoft.VSCode, com.microsoft.VSCodeInsiders |
| Cursor | com.todesktop.230313mzl4w4u92, com.anysphere.cursor, com.cursor.Cursor |
| Windsurf | com.exafunction.windsurf |
| Xcode | com.apple.dt.Xcode |
| JetBrains IDEs | com.jetbrains.* |
How It Works
Source identification starts with org.nspasteboard.source — a pasteboard type that some apps (especially IDE-adjacent tools) write. If that's missing, Deck falls back to sourceApp.bundleIdentifier.
The file path and cursor position come from the macOS Accessibility API. Deck queries the focused UI element for:
kAXInsertionPointLineNumberAttribute→ cursor line numberkAXSelectedTextRangeAttribute+kAXLineForIndexParameterizedAttribute→ selection start linekAXRangeForLineParameterizedAttribute→ column calculation
Different IDEs have different Accessibility tree depths, so Deck uses tuned search budgets:
| IDE Family | maxDepth |
maxNodes |
|---|---|---|
| VS Code / Cursor / Windsurf | 8 | 2000 |
| Xcode / JetBrains | 4 | 200 |
Jump Back
Each IDE family has its own mechanism for "open file at line":
- VS Code / Cursor / Windsurf:
code -g file:line:columnorvscode://file/...URL scheme - Xcode:
xed -l line filePath - JetBrains:
http://localhost:63342/api/file/...(local REST API)
6. Figma Clipboard Detection
Figma runs as a Chromium-based desktop app, and its clipboard behavior reflects that. When you copy a frame or component in Figma, the pasteboard receives what looks like standard HTML — but embedded inside it is base64-encoded design data wrapped in custom (figma) / (figmeta) markers. Most clipboard managers treat this as HTML or plain text, stripping the Figma-specific data entirely. When you paste it back, the design structure is gone — you get a flat image or garbled text instead of the original editable component.
Deck detects Figma content via containsFigmaMarker(in:), which scans the HTML for these markers. Once detected, a multi-step extraction pipeline kicks in:
resolveFigmaPayload()— Entry point; only runs for unresolved types.extractFigmaPayload()— SearchesUnsupportedPasteboardPayload.itemsfor Figma HTML.extractFigmaPayloadFromPlistFallback()— Falls back to plist parsing if needed.buildFigmaPayload()— Extracts(figma)and(figmeta)base64 data from the HTML.resolveFigmaPayload()extractFigmaPayloadFromPlistFallback()buildFigmaPayload()
The extracted metadata is structured as:
struct FigmaClipboardMeta {
var fileKey: String
var pasteID: String
var dataType: String
}
struct FigmaClipboardPayload {
var html: String
var figmaBase64: String
var figmetaBase64: String
var meta: FigmaClipboardMeta
var sourceURL: String? // from org.chromium.source-url
}A critical design decision: once Figma content is detected, Deck skips the text fallback chain entirely. The structured Figma data is preserved intact, rather than being flattened into a plain text representation that would lose all design semantics.
7. The Full Monitoring Loop
Putting it all together, here's what happens on every clipboard check:
sequenceDiagram
participant Timer as DispatchSourceTimer
participant Key as ⌘C Monitor
participant Check as checkForChanges()
participant PB as NSPasteboard
participant Filter as Skip Filters
participant Parse as Content Parser
participant IDE as IDEAnchorService
participant Store as DeckDataStore
alt Timer fires
Timer->>Check: poll interval elapsed
else ⌘C / ⌘X detected
Key->>Check: after 80ms delay
end
Check->>Check: guard !isCheckingForChanges (re-entrancy lock)
Check->>PB: read changeCount
PB-->>Check: currentChangeCount
alt changeCount unchanged
Check->>Check: adjustPollInterval(success: false) → interval × 1.5
else changeCount changed
Check->>Filter: apply skip filters
Note over Filter: • Deck's own writes (deckPasteboardType)<br/>• Recently pasted content<br/>• Sensitive / concealed items<br/>• Ignored applications
alt filtered out
Filter-->>Check: skip
else passes filters
Check->>Parse: ClipboardItem(with:pasteboard:)
Note over Parse: type priority resolution<br/>+ text fallback chain<br/>+ Figma detection<br/>+ OOM guards
Parse-->>Check: clipboardItem
Check->>IDE: captureAnchor(for: sourceApp)
IDE-->>Check: SourceAnchor?
Note over Check: optional post-processing:<br/>steganography decode,<br/>smart line-joining,<br/>link preview prefetch
Check->>Store: addParsedItemWithRules(item)
Check->>Check: adjustPollInterval(success: true) → min interval
end
endWrapping Up
Most of these systems interact with each other. Thermal pressure changes the poll interval, which changes how quickly ⌘C interception matters. A Figma paste can carry 20+ pasteboard types, hitting both the content priority ladder and the OOM guards simultaneously. IDE anchor capture has to complete within the 80ms window before the next poll cycle overwrites the Accessibility state.
There are areas we're still improving. The Accessibility API can be slow on certain apps (Electron-based editors with deep DOM trees), and we're experimenting with caching strategies to reduce AX query frequency. We're also looking into using NSPasteboard observation APIs if Apple exposes more event-driven hooks in future macOS versions, which could reduce polling overhead further.
Deck is a macOS clipboard manager that keeps everything local. Learn more at deckclip.app.