Skip to content

Latest commit

 

History

94 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ditto for macOS

English · 简体中文

Ditto for macOS is a native Swift/AppKit clipboard manager for macOS 13 and later. It keeps a searchable local history of copied text, rich text, HTML, images, PDFs, and file lists, then lets you find, edit, transform, send, and paste those clips back into the app you were using.

Author: 伤感咩吖

Current version: 1.2.6

License: GPL-3.0

The app is local-first. Clipboard history is stored in SQLite on your Mac, and the app does not use a cloud service or telemetry pipeline.

Contents

Highlights

  • Menu-bar app with a configurable global activation hot key.
  • Persistent SQLite clipboard history.
  • Capture of plain text, RTF, HTML, PNG/TIFF images, PDF payloads, and file drop lists.
  • Friendly history window with search, type filters, group filters, result counts, empty-state messages, multi-select, and drag-out support.
  • Search modes: contains, wildcard (* and ?), and regular expression.
  • Search targets: description, quick-paste text, and extracted full text from RTF/HTML.
  • Inline search prefixes: /q for quick-paste text and /f for full-text search.
  • Special paste transformations:
    • plain text
    • UPPERCASE
    • lowercase
    • Title Case
    • Sentence case
    • camelCase
    • invert case
    • remove line feeds
    • collapse to one line feed
    • collapse to two line feeds
    • typoglycemia
    • trim whitespace
    • POSIX path conversion
    • ASCII-only text
    • slugify
    • append date/time
    • generate GUID
    • paste as image
  • Groups with nested folders, group filters, create, rename, delete, and move-to-group actions.
  • Favorites and pinned clips that are preserved during trimming.
  • Copy buffers: 5 independent numbered clipboard slots with per-slot copy and paste hot keys.
  • First-ten paste hot keys for the first 10 visible clips.
  • Per-clip quick-paste text and shortcut metadata.
  • Multi-paste with configurable separators, reverse order, and optional save-as-new-clip behavior.
  • System, light, and dark themes with configurable accent color.
  • Session and all-time statistics.
  • LAN sync over TCP with AES-256-GCM encryption and a friend list.
  • Manual send to all friends or to one selected friend.
  • QR code generation for text clips.
  • Clip properties window.
  • Rich text clip editor.
  • Side-by-side clip comparison.
  • Optional external diff app.
  • Image viewer and image thumbnails.
  • Hex color detection with color swatches.
  • Include/exclude app filters for capture.
  • Expiry settings for old clips.
  • Maximum clip size limits.
  • Login auto-start through a user LaunchAgent.
  • Accessibility permission detection and guided permission prompt.
  • 11 localization packs: English, Simplified Chinese, Traditional Chinese, Japanese, Korean, French, German, Spanish, Brazilian Portuguese, Russian, and Arabic.

Requirements

  • macOS 13.0 Ventura or later.
  • Swift 5.9 or later.
  • Apple Silicon or Intel Mac supported by SwiftPM.
  • System sqlite3 and zlib; both are provided by macOS.

No Xcode project is required. The project builds with Swift Package Manager.

Build

Debug build:

cd /Users/alexdavis/Ditto-macOS
swift build

Release build:

cd /Users/alexdavis/Ditto-macOS
swift build -c release

Run

Run the app from source:

cd /Users/alexdavis/Ditto-macOS
swift run DittoMac

Run the headless self-test:

cd /Users/alexdavis/Ditto-macOS
swift run DittoMac --selftest

Expected self-test result:

84 passed, 0 failed

The self-test covers text transforms, slugify, color detection, search, AES round trips, LAN-sync safety defaults, QR code generation, database persistence and SQLite-consistent backup/relocation round trips (including integrity verification, refusal to overwrite an existing database, and staging cleanup after a failed relocation), self-contained single-file backups, recovery of WAL-mode databases whose sidecars are missing, rejection of archives with missing attachments, capture de-duplication with a pinned clip on top, shared-blob retention across an overlapping import, PDF capture/archive/sync preservation, group reparenting, copy-buffer cleanup, blob lifecycle cleanup, Windows encryption compatibility helpers, Windows importer rejection behavior, and image paste-path behavior.

Run the GUI layout check (needs a desktop session; not run in CI):

cd /Users/alexdavis/Ditto-macOS
swift run DittoMac --selftest-ui

Expected layout self-test result:

198 passed, 0 failed

It builds the real history window in all 11 languages, in light, dark and system appearance, at both the default 820×552 size and the 640×360 minimum, and asserts that the search box, search mode, type filter, group filter, clip count and clip list never overlap, that every control stays inside the content view, and that the clip column keeps more than half the table width.

Package a DMG

Build a release app bundle and DMG:

cd /Users/alexdavis/Ditto-macOS
bash scripts/package-dmg.sh

Generated outputs:

dist/Ditto-macOS-1.2.6.dmg
.build/stage/Ditto.app

The packaging script:

  • builds the release binary;
  • stages Ditto.app;
  • copies Info.plist, icon, and localization resources;
  • ad-hoc signs the app;
  • creates a drag-to-Applications DMG layout;
  • writes one versioned DMG so release downloads are unambiguous.

Install and Permissions

Install

  1. Open the DMG.
  2. Drag Ditto.app into /Applications.
  3. Start Ditto from /Applications.

Because the app is ad-hoc signed, macOS may block first launch. If that happens:

  1. Open Finder.
  2. Go to /Applications.
  3. Right-click Ditto.app.
  4. Choose Open.
  5. Confirm the launch prompt.

Accessibility Permission

Ditto simulates Command-V to paste into the previously focused app. macOS requires Accessibility permission for that.

Grant permission here:

System Settings -> Privacy & Security -> Accessibility

Enable Ditto.app in the list. If an older permission entry stops working after a new build:

  1. Remove the old Ditto entry from Accessibility.
  2. Add /Applications/Ditto.app again.
  3. Enable the toggle.
  4. Restart Ditto.

Local Network Permission

LAN sync may trigger a macOS Local Network permission prompt. Allow it if you want to receive or send clips over the local network.

Using Ditto

Capture

Once Ditto is running, copy normally in any app. Captured clips appear in the history window.

Supported captured content:

  • plain text;
  • RTF;
  • HTML;
  • images;
  • PDFs;
  • file lists.

Ditto skips empty payloads, concealed/transient pasteboard types, and content blocked by include/exclude app filters or regex filters.

Open History

Use the menu-bar icon or the configured global hot key to show the history window.

The history window includes:

  • search field;
  • search mode menu;
  • type filter;
  • group filter;
  • count label;
  • empty-state message;
  • table of clips;
  • optional preview pane;
  • toolbar actions.

Search

Search modes:

  • Contains: simple text matching.
  • Wildcard: supports * and ?.
  • Regex: uses regular expressions.

Search scope settings can include:

  • description;
  • full text extracted from RTF/HTML;
  • quick-paste text.

Inline prefixes:

/q invoice
/f release notes

/q searches quick-paste text. /f searches extracted full text.

Paste

Select a clip and paste it into the previously focused app. Ditto writes the selected clip to the system pasteboard, activates the target app, and sends the paste command.

Optional paste behaviors:

  • move pasted clip to the top;
  • hide Ditto after paste;
  • restore the previous clipboard after paste;
  • paste as plain text by default;
  • use per-app paste key overrides.

Special Paste

Special paste actions transform the selected clip before paste. They are available from the context menu and app actions.

Multi-Paste

Select multiple text clips and paste them as one combined payload. The separator is configurable. The order can be reversed. The combined result can also be saved as a new clip. Use Command-Shift-V or the multi-selection context menu to paste the selected text clips.

Groups

Use groups to organize clips. Groups can be nested. Deleting a group can move children and clips safely rather than deleting the entire history.

Copy Buffers

Copy buffers are 5 numbered slots separate from the main history. Each slot can have a copy hot key and a paste hot key.

Favorites and Pins

Favorites mark clips for quick identification. Pinned clips are never auto-deleted and sort to the top.

Preferences

The Preferences window contains these sections:

  • General:
    • language;
    • global activation hot key;
    • max history size;
    • open at login;
    • sound, delete confirmation, startup message, update checks;
    • paste behavior.
  • Appearance:
    • theme;
    • accent color;
    • font size;
    • lines per row;
    • thumbnails;
    • always on top;
    • first-ten index display.
  • Search Mode:
    • description search;
    • full-text search;
    • quick-paste text search;
    • regex case-insensitive mode.
  • Advanced:
    • include apps;
    • exclude apps;
    • expiry;
    • max clip size;
    • duplicate handling;
    • multi-paste separator;
    • slugify separator;
    • diff app;
    • translate URL;
    • web search URL;
    • regex filters;
    • database location;
    • database backup and compaction.
  • Copy Buffers:
    • per-slot copy hot key;
    • per-slot paste hot key.
  • Network:
    • LAN sync master switch;
    • incoming clip switch;
    • port;
    • password.
  • Friends:
    • guidance for friend management.

The Advanced page is scrollable so longer localized labels and smaller windows do not hide settings.

LAN Sync

LAN sync uses:

  • TCP listener;
  • default port 23443;
  • length-prefixed messages;
  • JSON headers;
  • AES-256-GCM encrypted payloads;
  • friend records with name, IP address, port, and send-all setting.

Manual sending supports:

  • send to all configured friends;
  • send to one selected friend from the history context menu.

Broadcast sending can send new copies to friends marked send all.

LAN sync is disabled by default and requires a non-empty network password. The password must match on both machines. Ports are limited to 1024 through 65535. If receive is disabled, Ditto can still keep outbound settings without opening the listener.

Import and Export

macOS History Archive

Ditto can export and import a self-contained SQLite archive for this macOS port. Archives preserve clip payloads including PDF data and retain group hierarchy; imports remap group IDs safely when the destination already has groups.

Windows Database Import

Ditto can import Windows Ditto SQLite databases and exported SQLite data. The importer maps common formats:

  • CF_UNICODETEXT;
  • CF_TEXT;
  • Rich Text Format;
  • HTML Format;
  • PNG;
  • CF_DIB;
  • CF_HDROP.

When the original database contains groups, Ditto recreates the available group hierarchy and assigns imported clips to their mapped groups.

The importer handles zlib-compressed payloads when the source database records an original size.

Windows peer-to-peer network protocol integration is separate from database import and is still limited.

Data Locations

Default database:

~/Library/Application Support/Ditto/Ditto.db

The database is created automatically on first launch. In Preferences, choose a folder under Database Location to create a complete copy named Ditto.db in that folder; restart Ditto to switch to it. Ditto never overwrites an existing Ditto.db in the chosen folder, and the original database is retained until you verify the new location.

Each clip stores its original save time and most recent paste time. Normal edits use incremental SQLite transactions instead of rewriting the full history, and WAL checkpoints keep temporary journal growth bounded. If the selected database cannot be opened, Ditto keeps new clips in the stable ~/Library/Application Support/Ditto/Ditto-emergency.db database instead of creating a different temporary database after every restart. Once the selected database is available again, Ditto safely imports the emergency history.

Legacy JSON migration source:

~/Library/Application Support/Ditto/history.json
~/Library/Application Support/Ditto/Data/

Singleton lock:

~/Library/Caches/org.ditto-cp.DittoMac.singleton.lock

Login item:

~/Library/LaunchAgents/org.ditto-cp.DittoMac.plist

The login item starts Ditto once at login. It does not use a crash-restart watchdog, so a broken build cannot enter a relaunch loop or repeatedly steal focus.

Development

Recommended validation loop:

cd /Users/alexdavis/Ditto-macOS
swift build
swift run DittoMac --selftest

Release validation:

cd /Users/alexdavis/Ditto-macOS
swift build -c release
swift run DittoMac --selftest
bash scripts/package-dmg.sh

When adding UI strings, update all of these:

  • Sources/DittoMac/Localization/LocalizationManager.swift;
  • every JSON file under Sources/DittoMac/Localizations/.

Current language pack files:

ar.json
de.json
en.json
es.json
fr.json
ja.json
ko.json
pt-BR.json
ru.json
zh-Hans.json
zh-Hant.json

Threading rule:

  • ClipboardStore protects entries and groups with NSRecursiveLock.
  • UI reads should use snapshotEntries() and snapshotGroups().
  • Do not iterate the live entries or groups arrays from the main thread.

Architecture

Sources/CSystem/
└── module.modulemap        # system sqlite3 shim

Sources/DittoMac/
├── main.swift
├── App/
│   ├── AppDelegate.swift
│   ├── SelfTest.swift
│   └── LayoutSelfTest.swift
├── Models/
│   ├── ClipboardEntry.swift
│   ├── DittoSettings.swift
│   ├── HotKeyChoice.swift
│   ├── Theme.swift
│   └── Friend.swift
├── Storage/
│   ├── MacClipboardDatabase.swift
│   ├── ClipboardStore.swift
│   └── WindowsDittoDatabaseImporter.swift
├── Clipboard/
│   ├── ClipboardMonitor.swift
│   ├── PasteSimulator.swift
│   └── ClipboardSaveRestore.swift
├── Text/
│   ├── SpecialPasteOptions.swift
│   ├── Slugify.swift
│   └── SlugifyTransliteration.swift
├── Features/
│   ├── CRC32.swift
│   ├── CopyBufferManager.swift
│   ├── Statistics.swift
│   ├── QRCodeGenerator.swift
│   ├── ColorCodeDetector.swift
│   ├── SearchEngine.swift
│   ├── ImageCompositor.swift
│   └── DiffPresenter.swift
├── HotKey/
│   └── HotKeyController.swift
├── System/
│   ├── LoginAgentManager.swift
│   └── ActiveAppTracker.swift
├── Sync/
│   ├── SyncCoordinator.swift
│   ├── AESEncryption.swift
│   ├── WindowsEncryption.swift
│   └── WindowsProtocol.swift
├── Localization/
│   └── LocalizationManager.swift
├── Localizations/
│   └── 11 JSON language packs
└── UI/
    ├── HistoryWindowController.swift
    ├── PreferencesWindowController.swift
    ├── ClipTableCellView.swift
    ├── ClipPropertiesWindowController.swift
    ├── ClipEditorWindowController.swift
    ├── GroupsWindowController.swift
    ├── FriendsWindowController.swift
    ├── QRCodeWindowController.swift
    ├── StatisticsWindowController.swift
    ├── ImageViewerWindowController.swift
    ├── SaveNotifier.swift
    ├── SaveAnimation.swift
    └── MagneticWindow.swift

Database Schema

Main tables:

ClipboardEntries
ClipBlobs
Groups
CopyBuffers
Friends

ClipboardEntries stores clip metadata:

id TEXT PRIMARY KEY
text TEXT
rtfBlobKey TEXT
htmlBlobKey TEXT
imageBlobKey TEXT
pdfBlobKey TEXT
fileURLsJson TEXT
createdAt REAL
lastPasteDate REAL
isFavorite INTEGER
neverAutoDelete INTEGER
quickPasteText TEXT
clipOrder REAL
shortcutKey INTEGER
shortcutGlobal INTEGER
moveToGroupShortcut INTEGER
globalMoveToGroup INTEGER
crc INTEGER
sourceApp TEXT
pasteCount INTEGER
groupId INTEGER

ClipBlobs stores heavier payloads:

blobKey TEXT PRIMARY KEY
fileExtension TEXT
data BLOB

Groups stores nested groups:

id INTEGER PRIMARY KEY
name TEXT
parentId INTEGER
sortOrder REAL
createdAt REAL

CopyBuffers stores numbered slots:

bufferNumber INTEGER PRIMARY KEY
entryId TEXT

Friends stores LAN sync peers:

id INTEGER PRIMARY KEY
name TEXT
ipAddress TEXT
port INTEGER
sendAll INTEGER

The database migration is idempotent. Startup runs column checks and applies missing columns without relying only on a stored schema version.

CI

GitHub Actions workflow:

.github/workflows/ci.yml

The CI workflow:

  1. Builds debug.
  2. Builds release.
  3. Runs swift run DittoMac --selftest.
  4. Packages a DMG on pushes, tags, and manual dispatch.
  5. Uploads the DMG artifact.
  6. Requires .github/release-notes/<tag>.md with English and Simplified Chinese sections.
  7. Creates a release with one versioned DMG when the ref is a v* tag.

Known Limitations

  • Windows database import is implemented; Windows peer-to-peer LAN protocol integration is still limited.
  • Network file transfer for file clips is not fully implemented.
  • Arbitrary custom clipboard formats are not yet captured.
  • The app is ad-hoc signed; first launch and Accessibility permission may require manual approval.
  • Some preferences from the Windows app are not yet exposed as macOS UI controls.
  • Some low-level timing values are currently fixed in code.

Troubleshooting

Ditto opens but paste does not happen

Grant Accessibility permission:

System Settings -> Privacy & Security -> Accessibility -> Ditto

If permission is already enabled but paste still does not work, remove Ditto from the list, add /Applications/Ditto.app again, enable it, and restart the app.

The app launches twice

Ditto uses a singleton lock:

~/Library/Caches/org.ditto-cp.DittoMac.singleton.lock

If a stale process exists, quit all Ditto instances and launch again.

LAN sync does not receive clips

Check these settings:

  1. LAN sync is enabled.
  2. Incoming clips are allowed.
  3. Both machines use the same password.
  4. The configured port is open on the local network.
  5. macOS Local Network permission is allowed.
  6. The friend IP address and port are correct.

A copied item is not saved

Check these settings:

  1. Include/exclude app filters.
  2. Regex skip filters.
  3. Maximum clip size.
  4. Expiry settings.
  5. Duplicate suppression settings.

License

Ditto for macOS is distributed under GPL-3.0.

Author: 伤感咩吖

About

Ditto clipboard manager — native macOS reimplementation (Swift/AppKit). A complete port of sabrogden/Ditto.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages