Skip to content

Latest commit

 

History

19 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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 Command+Shift+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.

On recent macOS versions that is not always enough — the download carries a quarantine flag and Gatekeeper refuses it outright with “Ditto.app” Not Opened — Apple could not verify “Ditto.app” is free of malware, offering only Done or Move to Bin. Strip the quarantine flag and it opens normally:

sudo xattr -dr com.apple.quarantine /Applications/Ditto.app

Adjust the path if you keep the app somewhere else than /Applications.

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 Command+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 Command+V yourself.

The Command+Shift+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
Command+Shift+V Open the history popup at the mouse pointer
type Filter entries
↑ ↓ Move the selection
Shift+Up Shift+Down Extend the selection to several entries
Page ↑ / ↓, Home, End Jump (hold Shift to extend)
Return, double-click Paste the selection
Shift+Return Paste the selection as plain text, without formatting (on an image, recognises and pastes its text)
Command+1 … Command+9 Paste the Nth visible entry
Command+Delete Delete the selected entries
Esc Close

Selecting several entries and pressing Return pastes them one after another, in the order they appear in the list, with a newline between each — as plain text, since a mixed selection has no single rich format.

Holding Shift while pressing Return pastes the plain text of the selection, dropping the rich formatting (RTF, HTML, images) that was captured with it. It works for a single entry as well as a multi-entry selection.

On an image entry, Shift+Return reads the text out of the picture first, using macOS's own on-device text recognition, and pastes that — so a screenshot of a code snippet or an error dialog goes in as text. The popup stays open with a spinner on the row while it works. If the image holds no legible text, nothing is pasted: the popup stays open and the row says No text detected.

Digits type into the search box as normal — only Command-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.

Settings → General: shortcut, launch at login, preview panel toggle, and Accessibility access status.

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.

Settings → Privacy: per-app retention overrides and a toggle for recording content concealed by password managers.

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.

Settings → History: stored entry count, retention limits, clipboard polling interval, and clean-up actions.

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 Command+V yourself rather than failing silently.
  • If another app already owns Command+Shift+V, registration fails and you get a warning at launch; the menu bar icon still opens the popup.