Post-capture routing is everything that happens after a capture file exists: destination selection, clipboard copy, Quick Access handoff, annotate auto-open, and history recording. This doc covers PostCaptureActionHandler, TempCaptureManager, and their supporting services as of HEAD.
- Screenshots:
ScreenCaptureManageremits every saved file URL oncaptureCompletedPublisher(sent fromsaveImage(...)whenemitCompletionis true). The single subscription lives inScreenCaptureViewModel's init (Snapzy/Features/Capture/CaptureViewModel.swift) and callsPostCaptureActionHandler.handleScreenshotCapture(url:). - Batch screenshots (multi-display fullscreen) call
handleScreenshotCaptures(urls:)directly from the capture flow instead of the publisher. - Recordings:
RecordingCoordinatorcallshandleVideoCapture(url:)after the writer finishes and the file is moved to its destination; the GIF flow callshandleVideoCapture(url:skipQuickAccess: true)after conversion because the placeholder card was already inserted. SeeRECORDING.md. - OCR exception: Capture Text is the only capture path that writes no file — recognized text/QR payloads go straight to the pasteboard as plain text, so no post-capture routing runs. See
CAPTURE.md. - Scrolling capture saves through
saveProcessedImage, so it enters routing through the same publisher as any screenshot. SeeSCROLLING_CAPTURE.md.
AfterCaptureAction (Snapzy/Features/Preferences/PreferencesManager.swift) has 4 cases as of HEAD, each gated per CaptureType (.screenshot, .recording):
| Action | Screenshot default | Recording default | Effect |
|---|---|---|---|
save |
ON | ON | Chooses export directory vs temp capture directory |
showQuickAccess |
ON | ON | Adds a card to the Quick Access stack |
copyFile |
ON | ON | Copies file/image to the pasteboard |
openAnnotate |
OFF (screenshot only) | n/a | Auto-opens the Annotate editor |
- The matrix is stored as
[AfterCaptureAction: [CaptureType: Bool]]JSON-encoded inUserDefaultsunder theafterCaptureActionskey viaPreferencesManager; unset entries fall back to the defaults above. SeePREFERENCES.md. AfterCaptureAction.uploadToCloudwas removed at commitdd4ccd5(with its TOML keyupload_to_cloud). Cloud upload is now manual-only: Quick Access cards, Annotate, Video Editor, and History surfaces, gated onCloudManager.isConfiguredplus theuploadToCloudQuick Access action configuration. Nothing inPostCaptureActionHandlerauto-uploads. SeeCLOUD.mdandCONFIGURATION.md.
flowchart TD
A["Capture file ready"] --> B{"Save enabled for this capture type?"}
B -->|Screenshot: Yes| C["Write into user export directory"]
B -->|Screenshot: No| D["Write into Application Support temp capture directory"]
B -->|Recording: Yes| E["Record in processing dir, move final video to export directory"]
B -->|Recording: No| F["Record in processing dir, move final video to temp capture root"]
C --> G["PostCaptureActionHandler"]
D --> G
E --> G
F --> G
G --> H["ScreenshotPresetAutoApplier.applyDefaultPresetIfNeeded (screenshots)"]
H --> I{"Copy file enabled?"}
I -->|Yes| J["ClipboardHelper.copyImage / copyMediaFile (FIRST)"]
I -->|No| K
J --> K{"Show Quick Access enabled?"}
K -->|Yes| L["QuickAccessManager.addScreenshot / addVideo"]
K -->|No| M
L --> M{"pinToScreen requested?"}
M -->|Yes| N["Pin window via QuickAccessManager"]
M -->|No| O
N --> O{"openAnnotate enabled (screenshot only)?"}
O -->|Yes| P["AnnotateManager.openAnnotation"]
O -->|No| Q
P --> Q["CaptureHistoryStore.addCapture"]
executeActions(for:url:skipQuickAccess:pinToScreen:) runs on the main actor in a deliberate order:
- Scoped file access —
SandboxFileAccessing.beginAccessingURL(url)wraps the whole sequence (security-scoped bookmark access for files outside the container); missing files bail out with a diagnostic log. - Preset auto-apply (screenshots) —
ScreenshotPresetAutoApplier.applyDefaultPresetIfNeeded(to:)checks the default Annotate canvas preset; when it changes the canvas it renders effects through the lightweightAnnotateExporter.renderCanvasEffects(sourceImage:effects:)path (no fullAnnotateState), atomically rewrites the screenshot file, and returnsAnnotationSessionDatawhich is persisted viaAnnotationSessionStoreand cached on the Quick Access item so the capture reopens editable. SeeANNOTATE.md. - copyFile FIRST — clipboard copy runs before any thumbnail generation, overlay presentation, or editor work so auto-copy is never blocked by slower UI actions.
- showQuickAccess —
QuickAccessManager.addScreenshot(url:)/addVideo(url:); skipped whenskipQuickAccessis true (GIF two-step flow). - pinToScreen — optional caller flag (inline annotate Pin): pins the existing Quick Access item or pins directly from URL.
- openAnnotate — screenshots only; opens through
AnnotateManagerwith the Quick Access item when one exists, otherwise from the URL with preset session data. - History record — screenshots read pixel dimensions via
CGImageSource; videos read duration and tracknaturalSizeviaAVURLAsset(macOS 15 async load APIs with older fallbacks);.gifextension maps to the GIF history type.
Batch variant handleScreenshotCaptures(urls:): filters missing files, delegates single-URL batches to the normal path, auto-applies presets per file, copies all file URLs at once (ClipboardHelper.copyFileURLs), adds every file to Quick Access, opens only the first capture in Annotate, and records each file in history.
copyEditedCaptureToClipboardIfEnabled(for:url:) re-runs the clipboard automation after an in-place edit save (Annotate/Video Editor) when copyFile is enabled for that capture type.
AfterCaptureAction.saveis not a post-write callback:- Screenshots:
TempCaptureManager.resolveSaveDirectory(for:exportDirectory:)decides the destination before the write — export directory when ON, temp directory when OFF. - Recordings: the AVAssetWriter always writes into a per-session
Captures/RecordingProcessing/<UUID>/directory first (TempCaptureManager.makeRecordingSavePlan); after the writer finishes, the final video moves to the export directory (Save ON) or the temp capture root (Save OFF) and the processing directory plus writer sidecars are deleted. A failed final move falls back to a unique name in the temp root (makeRecoveredRecordingURL).
- Screenshots:
- Export directory:
SandboxFileAccessManager.resolvedExportDirectoryURL()resolves the stored security-scoped bookmark (removing invalid bookmarks), falling back to a default location; first-use flows prompt throughensureExportDirectoryForOperation(promptMessage:). SeePREFERENCES.md. - Temp directory:
~/Library/Application Support/Snapzy/Captures/— Application Support, deliberately not/tmp, so macOS never purges temp files mid drag-and-drop and paste-time reads stay valid. - Saving a temp capture later (Quick Access Save action):
TempCaptureManager.saveToExportLocation(tempURL:)moves the file into the export directory preserving its relative path (naming-template subfolders survive), moves the recording metadata sidecar for videos, and prunes emptied temp subdirectories. - Deletion:
deleteTempFile(at:)removes the file plus its recording metadata sidecar and prunes empty directories. - Launch cleanup:
cleanupOrphanedFiles()(called fromSnapzyAppinit) sweeps the temp directory but preserves files that have an active history record, and — while history is enabled — files still inside the retention window or files it cannot reconcile against the database; retention sweeps and explicit cache clearing are the mechanisms that actually delete those.
ImageFormat(Snapzy/Services/Capture/ScreenCaptureManager.swift):.png,.jpeg(quality:),.webp; extensionspng/jpg/webp.- PNG/JPEG write through
CGImageDestinationwith DPI metadata set toscaleFactor × 72(PNG also gets pixels-per-meter); JPEG carrieskCGImageDestinationLossyCompressionQuality. - WebP writes through
WebPEncoderService(Snapzy/Services/WebPEncoder.swift, libwebp via Swift-WebP):.photopreset,method = 1, multithreaded, atomic file write. - Screenshot outputs use each display's native pixel density (
minimumScreenshotOutputScaleFactorfloor is 1.0). Non-Retina external-display captures stay at 1×; mixed-DPI composites may still promote low-density slices to the highest native scale in the selection. - Naming goes through
CaptureOutputNaming(Snapzy/Services/Capture/CaptureOutputNaming.swift):- Default templates:
Snapzy_{datetime}_{ms}(screenshot),Snapzy_Recording_{datetime}(recording), overridable per kind in Settings. - Tokens:
{datetime},{date},{year},{yearShort},{month},{monthName},{monthShort},{day},{time},{ms},{timestamp},{type},{appName}plus snake/short aliases;{appName}resolves from the captured app (or frontmost app for fullscreen/area) and is empty when unavailable. /creates sanitized subfolders under the destination; traversal segments and invalid path characters are stripped; known media extensions embedded in templates are removed.makeUniqueFileURLdedupes with_2,_3, … suffixes.
- Default templates:
Snapzy/Services/Clipboard/ClipboardHelper.swift writes one pasteboard item per capture so receiving apps pick their preferred representation:
copyImage(from:)—writeObjects([NSURL])(grants the receiver a sandbox extension), then augments the same item with the encoded data type (.png/JPEG/WebP UTI) and.tiffpixel data. WhenNSImagecannot decode the file (e.g. WebP on macOS 13), the item still carries the file URL and original encoded bytes.copyMediaFile(from:)— videos/GIFs: file-URL write plus same-item.URLand.stringfallbacks for Teams/Electron/WebView paste targets.copyFileURLs(_:)— batch file-URL copy for multi-display screenshot sets.- Render-based
copyImage(_:format:)— Annotate/Mockup copies render to the configured format, write a temp file (Snapzy_clipboard_<uuid>), then copy like a file. - Temp files must not be deleted after copying: receivers read them at paste time. Orphans are reclaimed by
cleanupOrphanedFiles()on the next launch.
- Quick Access cards expose hover actions and a matching context menu (copy, save/open, edit, cloud upload, dismiss, delete/trash); temp captures show Save, saved captures keep Open in the same slot even when the after-capture Save preference is off. Action visibility, order, and card slots come from
QuickAccessActionConfigurationStore. SeeQUICK_ACCESS.md. - Card countdowns pause while the item is being edited (Annotate/Video Editor), converted to GIF, or uploaded to cloud, and resume when the activity ends.
- GIF output is a two-step flow: record video → placeholder Quick Access card →
GIFConverter→ card URL swapped to the GIF →handleVideoCapture(skipQuickAccess: true)finishes routing. SeeRECORDING.md. - Screenshot pin opens an independent always-on-top pin window (zoom, drag-to-app, click-through lock mode). See
QUICK_ACCESS.md.
CaptureStorageManager(Snapzy/Services/FileAccess/CaptureStorageManager.swift) owns theApplication Support/Snapzy/Capturescache:calculateCacheSize()(background enumeration) andclearCache().- The UI lives in Settings → History (
PreferencesHistorySettingsView): shows the formatted cache size and offers cache clearing.clearCache()refuses to run while a capture or recording is in progress (CacheCleanupError.operationInProgress), removes every file in the captures directory (skipping locked files), and deletes matching history records and recording metadata sidecars. SeeHISTORY.md.
| File | Responsibility |
|---|---|
Snapzy/Services/Capture/PostCaptureActionHandler.swift |
Post-capture action execution, ordering, batch handling, history records |
Snapzy/Services/Capture/TempCaptureManager.swift |
Save-vs-temp destination, recording save plan, temp lifecycle, orphan cleanup |
Snapzy/Services/Capture/ScreenCaptureManager.swift |
captureCompletedPublisher, ImageFormat, file writing, native output scale |
Snapzy/Services/Capture/CaptureOutputNaming.swift |
Naming templates, context tokens, sanitization, unique dedupe |
Snapzy/Services/Capture/ScreenshotPresetAutoApplier.swift |
Default Annotate canvas preset bake-in during routing |
Snapzy/Services/WebPEncoder.swift |
libwebp-backed WebP encoding |
Snapzy/Services/Clipboard/ClipboardHelper.swift |
Format-aware single-item pasteboard writes |
Snapzy/Services/FileAccess/SandboxFileAccessManager.swift |
Security-scoped export-directory bookmarks and scoped access |
Snapzy/Services/FileAccess/CaptureStorageManager.swift |
Captures cache sizing and clearing (Settings → History) |
Snapzy/Features/Preferences/PreferencesManager.swift |
AfterCaptureAction matrix storage and defaults |
CAPTURE.md— capture modes that feed this pipelineSCROLLING_CAPTURE.md— long-screenshot save path into routingRECORDING.md— recording writer, GIF two-step flowQUICK_ACCESS.md— card stack, actions, countdown, pin windowsANNOTATE.md— preset auto-apply and editable sessionsHISTORY.md— history records, retention, cache clearingCLOUD.md— manual cloud upload entry pointsPREFERENCES.md— after-capture matrix and export folder settingsCONFIGURATION.md— TOML keys for after-capture actionsLOCALIZATION.md— ownership of post-capture copy