Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mcditto

A macOS clipboard manager in the spirit of Ditto.

macOS keeps exactly one clipboard entry, so everything you copy overwrites what came before. mcditto records every copy — text, rich text, images, files — into a local SQLite database, and gives you back any of it with ⌘⇧V: a resizable popup opens at the mouse pointer, you pick an entry with the arrow keys or by typing, press Return, and it lands at the cursor in whatever app you were just using.

Everything stays on your Mac. Nothing is uploaded anywhere.

Download the latest build of Ditto.app   Project website

The Ditto history popup showing clipboard entries with search and a preview pane.

That download link always resolves to the most recent release, so it never needs updating — see Releases.

The app is signed ad-hoc, not notarized, so the first launch needs right-click → Open to get past Gatekeeper.

Build

There is no Xcode project — the app is a Swift Package plus a script that assembles the bundle, so the Command Line Tools are enough.

make app

That produces build/Ditto.app. To run it:

make run

To keep it around:

make install

Other targets: make selftest (store and capture checks), make icon (regenerates Resources/AppIcon.icns), make clean.

Permissions

Accessibility — required only for pasting. mcditto pastes by putting the entry on the clipboard, switching back to your previous app and sending a ⌘V keystroke, and macOS gates synthetic keystrokes behind Accessibility. Grant it in System Settings → Privacy & Security → Accessibility.

Without it, everything else still works: the history is recorded, the popup opens, and picking an entry copies it to the clipboard so you can press ⌘V yourself.

The ⌘⇧V hotkey itself needs no permission — it is registered through Carbon, not an event tap.

If the Accessibility grant keeps disappearing

macOS ties the grant to the app's code signature. make app signs ad-hoc with a stable identifier, which normally survives rebuilds, but if it does not, sign with a self-signed certificate instead:

  1. Keychain Access → Certificate Assistant → Create a Certificate…
  2. Name it e.g. mcditto-dev, type "Code Signing", self-signed.
  3. Build with it:
CODESIGN_IDENTITY=mcditto-dev make app

Using it

Key Action
⌘⇧V Open the history popup at the mouse pointer
type Filter entries
↑ ↓ Move the selection
Page ↑ / ↓, Home, End Jump
Return, double-click Paste the selected entry
⌘1 … ⌘9 Paste the Nth visible entry
⌘⌫ Delete the selected entry
Esc Close

Digits type into the search box as normal — only ⌘-digit is a shortcut, so searching for numbers still works.

The popup remembers the size you drag it to and always reopens at the pointer. The preview panel on the right can be turned off in Settings → General for a narrower, list-only popup. The menu bar icon covers everything else: pause recording, clear history, settings, quit.

What gets recorded

Every flavor of a copy is kept, so pasting styled text into Pages keeps its formatting while pasting into a terminal gives plain text. Copying the same thing twice does not create a second entry — the existing one moves to the top and its use count goes up.

Payloads under 128 KB live inline in the database; larger ones (images, mostly) go to files under blobs/, referenced from the row.

Not recorded: anything a password manager marks with the standard org.nspasteboard.ConcealedType flag, and anything copied from an app you list in Settings → Privacy.

Retention

Two rules run at launch and then hourly:

  • entries not used in the last 365 days are deleted
  • beyond 10 000 entries, the least recently used are deleted

Both numbers are adjustable in Settings → History. Blob files whose rows are gone are cleaned up in the same pass.

Where things live

~/Library/Application Support/mcditto/
├── clips.db        SQLite database (WAL mode, FTS5 index on previews)
└── blobs/          payloads too large to store inline

To look at the history yourself:

sqlite3 ~/Library/Application\ Support/mcditto/clips.db "SELECT id,kind,use_count,source_app,substr(preview,1,60) FROM entries ORDER BY last_used_at DESC LIMIT 20;"

Layout

Sources/mcditto/
├── main.swift              agent bootstrap (--selftest lives here too)
├── AppDelegate.swift       wires everything together, schedules pruning
├── SelfTest.swift          store, retention and capture checks
├── Store/                  SQLite wrapper, schema, blobs, retention
├── Capture/                pasteboard polling and flavor extraction
├── Paste/                  hotkey, permissions, paste injection
├── UI/                     popup panel, list, menu bar, settings
└── Support/                paths and settings

Releases

The git tag is the only place a version is written down. Nothing in the Swift source or in Info.plist holds a version number — Info.plist ships placeholders that scripts/make-app.sh fills in at build time.

Pushing a v* tag is what cuts a release. Commits on main build nothing on their own:

git tag -a v0.1.5 -m "v0.1.5"
git push origin v0.1.5

.github/workflows/build.yml then compiles the release binary, assembles the bundle, verifies Info.plist really carries the tag's version, runs make selftest, and publishes Ditto.app.zip as a GitHub release with generated notes. Tag v0.1.5 therefore ships with CFBundleShortVersionString 0.1.5.

Each release is marked latest, which is what keeps the download button above pointing at the newest one. That URL follows whichever release GitHub considers latest; it does not need a tag named latest.

A local make app with no VERSION set describes itself from the tags instead (0.1.4-dirty, say), so a development build is never mistaken for a release. You can always override explicitly with VERSION=1.2.3 make app.

The project page under docs/ is served at https://bertyhell.github.io/mcditto/, deployed by .github/workflows/pages.yml on the same version tags — or on demand from the Actions tab when the page needs updating between releases. Set Settings → Pages → Build and deployment → Source to GitHub Actions for that workflow to take over; the "Deploy from a branch" option runs a GitHub-managed workflow instead, which cannot be edited and still pins deprecated action versions.

Limitations

  • Not sandboxed, and not notarized. A global hotkey and synthetic keystrokes both rule out the App Store; this is an app you build and run yourself.
  • While a password field has focus anywhere on the system, macOS blocks simulated keystrokes. mcditto detects this and tells you to press ⌘V yourself rather than failing silently.
  • If another app already owns ⌘⇧V, registration fails and you get a warning at launch; the menu bar icon still opens the popup.