Skip to content

Repository files navigation

Snapmark

A native macOS menu-bar app for capturing, annotating, and sharing screenshots — built in Swift with ScreenCaptureKit, SwiftUI, and AppKit. Take a screenshot with a hotkey, point at what matters with arrows, boxes, and text, and paste. Tuned for the workflow of pasting screenshots into AI coding agents, but useful for anything.

Status: Snapmark is a personal learning project, not a commercial product. Its explicit goal is to reproduce the parts of CleanShot X that a keyboard-driven developer actually uses, in order to learn how a real native Mac app is built — menu-bar agent, global hotkeys, screen capture, an interactive drawing canvas, screen recording, and clipboard integration. "CleanShot X already does this" is the spec, not an objection. See .mo/idea.md for the full product concept.

macOS 14+ · Swift 6 toolchain · no external dependencies.


Table of contents


What it does

Snapmark lives in the menu bar (a camera.viewfinder icon, no Dock icon). It captures your screen the way macOS already does — a hotkey, drag a region, done — and adds the thing macOS doesn't do cleanly: a second hotkey opens the last shot you took in a fast annotator so you can circle the thing before you paste. From there it grew toward CleanShot X parity: window/full-screen/scrolling capture, screen + region + window recording with audio, a searchable history, pin-to-screen, OCR, and "beautify."


The core loop

Step Action Default shortcut
1 Capture Region — drag a rectangle; it lands on your clipboard ⌃⌥4
2 Annotate Last — the last capture opens in the annotator ⌃⌥A
3 Draw arrows, boxes, text…
4 Copy (or ⌘C / close) — the annotated image replaces the clipboard ⌘C

Or do it in one shot with Capture & Annotate (⌃⌥5): capture a region and jump straight into the annotator.


Full feature list

Capture

  • Region capture with a dimmed selection overlay and a live dimension HUD while dragging.
  • Window capture — hover to highlight a window, click to grab it.
  • Full-screen capture.
  • Timed capture — region after a 3s or 5s delay (for menus/hover states).
  • All-in-One picker (⌃⌥6) — one overlay to choose region / window / full screen.
  • Scrolling capture (Beta) — scrolls a region and stitches the frames into one tall image via pixel overlap-detection. See limitations.
  • Quick Save Region (⌃⌥S) — capture a region straight to disk (and clipboard), no annotator.
  • Capture source metadata — each shot records the app/window it came from (used for auto-naming and history search).

Annotate

A floating tool dock over an AppKit drawing canvas. Tools:

  • Arrow (straight or curved — drag the mid-handle to bow it), Line, Rectangle, Ellipse, Text, Highlighter, Pen (freehand), Numbered counter, Blur/Pixelate (real pixelation of the underlying pixels), Spotlight (dim everything except a highlighted area).
  • Select / move / resize / delete any mark; multi-step undo/redo.
  • Stroke styles — solid / dashed / dotted, adjustable width, optional fill for rectangles/ellipses.
  • Text labels on shapes — double-click (or select + T) an arrow, line, rectangle, or ellipse to type a caption that rides with it.
  • Crop.
  • Full color picker, per-tool color/width/font memory (last-used settings persist).
  • Saved presets (favorites) — save a tool + color + style combo and re-apply it.
  • Off-canvas drawing — marks drawn in the margin are preserved on export (the canvas grows to fit).

Beautify & compose

  • Beautify — gradient backgrounds, padding, rounded corners, and a drop shadow, via a visual popover with live previews.
  • Combine — append other recent captures side-by-side or stacked into one image, with selectable/deletable tiles, adjustable gutter, and cross-axis alignment. Annotations follow their tile when the layout reflows.

Share & output

  • Copy to clipboard (PNG), with a "copy without closing" path (⌘C).
  • Save — writes instantly to your configured folder (Desktop by default), auto-named from the source app/window and timestamp. Save As… opens a panel when you want to pick.
  • Pin to screen — float a capture as an always-on-top window: aspect-locked resize, opacity control (scroll or right-click), copy/save/annotate, -drag to drag the file out into another app. Pins persist across app launches.
  • Copy Text from Last (OCR) — Vision text recognition of the last capture to the clipboard.

History

  • Capture History window (⌃⌥H) — a browsable, searchable grid of recent captures. Search matches OCR'd text inside the screenshots and the source app/window name.
  • Date grouping (Today / Yesterday / Earlier this week / Earlier).
  • Persistent across launches — captures, their editable base image, annotations, and beautify state are stored on disk and rehydrated as editable (non-destructive) edits.
  • Configurable cap — keep the last N captures (default 25, adjustable 5–1000 in Settings).

Screen recording

  • Record a display, a dragged region, or a picked window.
  • System audio and/or microphone capture (mic requires macOS 15+).
  • On-screen recording border — a marching-ants outline around the recording area (excluded from the recording itself).
  • One-click stop from the menu bar while recording.
  • Recording editor — after a recording stops, a trim editor opens: scrub, set in/out points, and export a trimmed MP4 or GIF. Always ends with exactly one file (the raw recording is never silently lost).
  • Record Screen → GIF — a quick full-screen-to-GIF path that skips the editor.

System integration

  • Global hotkeys via Carbon RegisterEventHotKey — no Accessibility permission needed (except for scrolling capture; see below). Fully remappable in Settings.
  • Menu-bar recording indicator that doubles as the stop button.
  • Onboarding flow for granting Screen Recording and relaunching.
  • Post-capture actions — choose what happens after a capture: show a quick-access thumbnail, open the annotator, save to disk, or copy-only.

Keyboard shortcuts

Global (remappable in Settings → Shortcuts)

Action Default
Capture Region ⌃⌥4
Capture & Annotate ⌃⌥5
All-in-One ⌃⌥6
Quick Save Region ⌃⌥S
Annotate Last ⌃⌥A
Open History ⌃⌥H

To rebind: Settings → Shortcuts, click a shortcut, press the new combo. Conflicts are detected and flagged ("Already in use").

In the annotator

  • Tools: V select · C crop · A arrow · L line · R rectangle · O ellipse · T text · H highlighter · D pen (freehand) · N counter · B blur · S spotlight.
  • ⌘Z / ⇧⌘Z undo / redo · ⌘C copy (keeps window open) · G toggle the Combine tray · Esc copy & close.

Tool-key bindings live in AnnotatorWindowController's key monitor; tool tooltips show their keys.


Install & build

Snapmark is distributed as source. There is no notarized release build — you build it locally with the standard Swift toolchain.

Requirements

  • macOS 14.0+
  • Xcode 15+ / a Swift 6 toolchain (swift --version ≥ 6.0)

Build

git clone <this-repo>
cd screenshot-app

make app      # build (release) + assemble & sign dist/Snapmark.app
make open     # build, assemble, and launch the app
make run      # build and run the executable in the foreground (logs to terminal)
make clean    # remove .build and dist
make rebuild  # clean + app

Under the hood: swift build -c release produces the binary, then the Makefile assembles dist/Snapmark.app (copies the binary, Info.plist, and icon) and code-signs it. There is no .xcodeproj — it's a SwiftPM executable target.

Tests

swift test    # runs the SnapmarkTests target (pure-logic unit tests)

Tests cover the extractable pure logic (combine layout, crop math, counter numbering, annotation geometry, exporter naming, presets). Interactive paths — capture, annotation drawing, recording — require on-device testing with Screen Recording granted and can't be verified headlessly.


Permissions

Snapmark needs macOS privacy grants depending on what you use:

Permission Needed for Prompted
Screen Recording All capture and recording On first capture
Microphone Recording with mic audio (macOS 15+) When you enable mic recording
Accessibility Scrolling capture only (to post synthetic scroll events) When you first run scrolling capture

After granting Screen Recording you must relaunch — macOS caches the authorization at process start. The onboarding window and Settings → General both have a Relaunch button. AppController.relaunch() handles this.

Why the Screen Recording grant "kept resetting"

macOS ties the Screen Recording grant to the app's code signature. An ad-hoc signature (codesign -s -) is derived from the binary hash, so it changes on every rebuild — and macOS then treats each rebuild as a brand-new, ungranted app and re-prompts endlessly, even though System Settings shows the toggle "on."

The Makefile fixes this: its sign target auto-detects a stable signing identity (Apple Development or Developer ID Application) from your keychain via security find-identity and signs with it, falling back to ad-hoc only if none exists. With a stable identity the grant persists across rebuilds.

If it gets into a weird state, reset and re-grant once:

make reset-permission   # tccutil reset ScreenCapture com.mooumari.snapmark
make open               # grant when prompted, then Relaunch

Signing note: the dev entitlements disable the App Sandbox so ScreenCaptureKit and global hotkeys work without provisioning. spctl will report the Development-signed app as "rejected" — that's normal and only affects Gatekeeper distribution, not local run or TCC. Sandboxing/notarization would need revisiting before any real distribution.


Where your files live

What Location
Saved screenshots (default) ~/Desktop (configurable in Settings → After capture)
Persistent history ~/Library/Application Support/Snapmark/Captures/
Persistent pins ~/Library/Application Support/Snapmark/Pins/
Recordings (raw + exports) ~/Movies

History captures store three files each: <id>.png (flattened output), <id>.orig.png (editable base, when different), and <id>.json (a sidecar with annotations, beautify state, point sizes, OCR text, and source metadata — so reopened shots stay editable and correctly scaled).


Settings

Open with ⌘, from the menu, or Settings….

  • General — history cap (5–1000), relaunch button.
  • Shortcuts — rebind any global hotkey; conflict detection.
  • After capture — post-capture action (thumbnail / annotate / save / copy-only), save-folder chooser, capture sound toggle.
  • Recording — system-audio and microphone toggles (mic disabled with an explanation on macOS < 15).

Architecture

  • Shell (SwiftUI): MenuBarExtra menu, Settings scene, Window scenes (History, Recording editor), the floating tool dock. App is an .accessory (menu-bar-only) agent via LSUIElement + NSApp.setActivationPolicy(.accessory).
  • Canvas (AppKit): AnnotationCanvasView — custom draw(_:) + mouse handling; the annotator is AppKit for precise interactive control.
  • Capture: ScreenCaptureKit (SCScreenshotManager / SCStream) behind dimmed selection overlays.
  • Recording: SCStreamAVAssetWriter (H.264), with separate audio tracks for system + mic.
  • Hotkeys: Carbon RegisterEventHotKey (works without Accessibility).
  • Clipboard: NSPasteboard PNG/TIFF write-back.
  • OCR: Vision (VNRecognizeTextRequest, .accurate).

Pure, testable logic (layout/crop/geometry math) is factored into standalone types (CombineLayout, CropMath, CounterNumbering, AnnotationGeometry) so it can be unit tested without a running UI.


Limitations & incomplete features

Snapmark is a learning project working toward parity — these are the honest gaps:

Not built (need a backend or are out of scope)

  • Share-as-a-link / cloud upload. The headline CleanShot feature (upload → short URL). Requires a backend and account system — a separate, multi-session project. Not started.
  • Freeze capture (freeze the screen while you select).
  • Emoji / callout preset library.

Beta / needs on-device tuning

  • Scrolling capture is Beta. It works but the overlap-detection stitcher is tuned by hand and can mis-stitch on unusual content (very uniform/periodic layouts, momentum scrolling, sticky headers). It requires Accessibility permission to post synthetic scroll events; without it, it degrades to a single frame and bails with a prompt. There is a temporary diagnostic log at /tmp/snapmark-scroll.log used while tuning (to be removed).

Recording caveats

  • No real-time audio mixing. System audio and microphone are recorded as separate tracks in the same file. Some players (e.g. QuickTime) play only the first track. A proper mix-down is a follow-up.
  • Mic requires macOS 15+ (SCStreamConfiguration.captureMicrophone); on 14 the mic toggle is disabled.
  • Recordings export next to the raw recording in ~/Movies — no per-export save-location picker yet.

Editor follow-ups (nice-to-have)

  • Filmstrip/thumbnails in the trim bar.
  • More recording-editor keyboard shortcuts (space = play, ⌘S = save).

Distribution

  • Not sandboxed, not notarized. Ships as source you build and sign yourself. Sandbox + notarization would need to be added before any general distribution.

Known issues & gotchas

These are documented because they bit during development and will bite anyone extending it:

  • Menu-bar item shift. When recording starts, the SwiftUI menu item is swapped for an AppKit status item (so a single click stops recording); the icon may shift position slightly during the swap.
  • Window lifecycle. A menu-bar app must survive window closes. Any new NSWindow opened from Snapmark needs three things set or it will crash/quit the whole app: applicationShouldTerminateAfterLastWindowClosed → false, isReleasedWhenClosed = false on programmatically-owned windows, and deferring the drop of a controller's last strong reference out of its own windowWillClose (DispatchQueue.main.async). See SnapmarkApp.swift / RecordingEditor.swift.
  • Capture exclusion for overlays. Floating UI that must not appear in a capture/recording (the HUD, the recording border) is either marked sharingType = .none or passed to SCContentFilter(excludingWindows:). The .none approach is more robust (no timing race).

Project layout

Sources/Snapmark/
  App/          AppController (orchestrator), SnapmarkApp, AppDelegate
  MenuBar/      MenuBarContent, RecordingStatusItem
  Capture/      CaptureEngine, RegionSelection/WindowSelection controllers,
                ScreenRecorder, ScrollingCapture(+HUD), RecordingBorderController,
                ThumbnailController, GIFExporter, VideoExporter, Capture (model)
  Annotator/    AnnotatorWindowController, AnnotationCanvasView, ToolDockView,
                Annotation, AnnotationRenderer, AnnotationGeometry, AnnotatorState,
                BackgroundStyle/BlurRenderer/CombineLayout, PinnedShotController
  Recording/    RecordingEditor (trim + MP4/GIF export)
  Clipboard/    ClipboardWriter, Exporter
  Hotkeys/      HotkeyManager, HotkeyStore, HotkeySpec extensions
  History/      CaptureHistory, HistoryView
  Settings/     SettingsView, OnboardingView
  Support/      AppPreferences, PermissionsManager, TextRecognizer
  DesignSystem/ Theme (color/type tokens), Buttons

Resources/      Info.plist, entitlements, AppIcon.icns
Tests/          SnapmarkTests (pure-logic unit tests)
Makefile        build / assemble / sign / permission helpers
Package.swift   SwiftPM executable + test targets

Design language: a dark, floating "instrument" look. Accents signal #FF5A3C (warm red) and cyan #38D3F5; signature elements are the live dimension HUD and the floating tool dock. Tokens live in DesignSystem/Theme.swift.


Contributing

This is a personal learning project, but the code is open. If you build on it:

  • Run swift test — keep the pure-logic tests green.
  • Interactive changes (capture, drawing, recording) must be tested on-device with Screen Recording granted; they cannot be verified headlessly.
  • Match the surrounding style; extract pure logic into testable types where you can.
  • Rename the bundle id (com.mooumari.snapmark in Makefile + Info.plist) if you fork — it's a placeholder.

License

Released under the MIT License — see LICENSE. You're free to use, modify, and distribute it, including commercially, as long as the copyright notice is included.

About

A native macOS menu-bar screenshot, annotation & screen-recording app (CleanShot X-style), built in Swift.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages