Skip to content
kirahowePublic

About

A small native macOS editor for markdown files: edit the rendered document in place, save plain markdown back

Resources

Stars

2 stars

Watchers

1 watching

Forks

Repository files navigation

editmd

A small native macOS editor that shows a markdown file as rendered text you can edit in place, and saves it back as markdown.

open -a editmd notes.md

kirahowe.github.io/editmd

Install

editmd needs macOS 14 or later. Install it with Homebrew:

brew install --cask kirahowe/tap/editmd

The app is signed ad hoc rather than with an Apple Developer ID, so the cask removes the quarantine flag that would otherwise make Gatekeeper refuse to open it. If you download the zip from a release yourself, run xattr -dr com.apple.quarantine editmd.app before opening it.

To build it from source instead, you need Xcode (or the Swift toolchain) and bun:

make install     # builds build/editmd.app, copies it to ~/Applications, registers it

make build only builds the app, make run opens it, and make check runs the tests. Set DEST=/Applications to install somewhere else. scripts/release.sh publishes the version in Resources/Info.plist: it runs the checks, builds a universal app, attaches it to a GitHub release and updates the cask (DRY_RUN=1 builds without publishing). With DEMOS="name …" it also records those demo videos and attaches them.

Using it

Type as you would in any word processor. Markdown shortcuts also work as you type: # starts a heading, - a list, > a quote, ``` a code block, and **bold** turns bold when you close it.

editmd saves as you go. A second after you stop typing, or every five seconds if you keep going, it writes the file. The title bar says "Saving…" until the changes are on disk, then "Saved" for a moment. Cmd-S saves straight away, and closing a window saves instead of asking. If a save fails, the title bar says "Not Saved", and hovering over it shows why. If the editor page crashes, the window reloads it with the file as it was last opened or saved. Any changes not yet saved are lost with the page, and a sheet on the window says so. A new document has no file yet, so it works the usual Mac way (a dot on the close button, a prompt on close) until you save it somewhere. Save As, Revert to Saved, Open Recent and printing work too.

Other programs can edit the file while it is open, a coding agent for example. editmd notices and merges their changes into the window. With nothing unsaved, the window just shows the new version. With unsaved edits, both sides' changes are kept: edits to different paragraphs, list items or table rows combine, your cursor stays where it was, and Cmd-Z undoes only your own edits. If you and the other program change the same paragraph at the same time, the paragraph keeps both changes, which may need tidying. Before writing, a save reads the file one last time, and if someone else has changed it, merges their changes and starts over. A write that lands in the instant between that check and the file being replaced is still overwritten. The window is a few microseconds, but nothing closes it without the other program's cooperation.

Two agents edit a plan open in editmd: each one's changes are highlighted in its own color, and the legend lists them

editmd highlights every change another program makes while a file is open, in the color of whoever made it. A legend in the corner lists each agent and how many of its changes are highlighted. Click a name to jump to that agent's next change, double-click it to rename the agent, or click × to clear its highlights. The – button minimizes the legend to a row of colored dots, and every window remembers that. A new name applies in every window and to the agent's later changes until editmd quits. Added text is tinted, new blocks are shaded with a bar in the margin, and a thin marker shows where text was deleted. Hovering over a highlight shows who made it and when, and hovering over a marker also shows the deleted text. Highlights follow the text through your own edits, undo and later merges. They are never saved, and they last until you clear them, switch to the markdown source, or revert. Changes to front matter are not highlighted.

The file system doesn't say which program wrote a file, so editmd guesses. When a file changes, it looks for running coding agents (Claude Code, Codex, Gemini, Aider, OpenCode, Amp, Goose and Cursor's agent) whose working folder contains the file, and credits the one working closest to it, as "Claude · notes" for example. Each agent process counts as one agent. If no agent is working above the file, or two are working in the same folder, the change is shown as "Outside edit".

To tell apart agents that share a folder, have them report their edits. An agent's tool hook can run editmd --report-edit before and after each edit, with the hook's JSON payload on stdin. The command passes the report to the running app, prints nothing, and always exits 0. For Claude Code, add this to settings.json (adjust the path if the app lives elsewhere):

"hooks": {
  "PreToolUse": [{ "matcher": "Edit|MultiEdit|Write", "hooks": [{ "type": "command", "command": "~/Applications/editmd.app/Contents/MacOS/editmd --report-edit" }] }],
  "PostToolUse": [{ "matcher": "Edit|MultiEdit|Write", "hooks": [{ "type": "command", "command": "~/Applications/editmd.app/Contents/MacOS/editmd --report-edit" }] }]
}

The command also reads the file headers in a Codex apply_patch, so Codex's tool hooks can run the same command. A reporting agent counts as one agent per session, and a subagent gets its own entry. Setting EDITMD_AGENT_NAME in the agent's environment sets its starting name. A report wins over the folder guess: a change goes to the agent that most recently reported it was about to write that file. That claim lasts until a few seconds after the agent reports the write as done, or ten minutes if it never does (a denied permission prompt, say).

If you open a file and save it without editing, the bytes on disk stay exactly as they were. Once you edit, the whole document is written out by the serializer, which normalizes some style choices: * bullets become -, table columns get padded, and a few literal characters get backslash escapes. Links, images and reference links keep their form. A reference link stays a reference while its definition still matches it; change its URL or delete the definition, and it is written as an inline link. A table row with more cells than its header gets empty header cells added at the end, so every cell stays in its column.

editmd reads UTF-8, UTF-16 with a byte order mark, and Windows-1252, and saves an edited file in the encoding and line endings it came with. A file with mixed line endings gets whichever kind it used most. If an edit adds a character the encoding can't hold, such as an emoji in a Windows-1252 file, editmd saves the file as UTF-8 and tells you. It refuses to open binary files.

Shortcut Action
Cmd-B / Cmd-I / Cmd-E Bold, italic, inline code
Shift-Cmd-X Strikethrough
Cmd-K Add, edit or remove a link
Opt-Cmd-1 … 6, Opt-Cmd-0 Heading level, body text
Shift-Cmd-8 / Shift-Cmd-7 Bulleted / numbered list
Cmd-] / Cmd-[ Indent / outdent list item
Shift-Cmd-B Quote
Opt-Cmd-C Code block
Cmd-/ Switch between rendered view and markdown source
Cmd-F, Cmd-G, Shift-Cmd-G Find, next, previous
Cmd-= / Cmd-- / Cmd-0 Zoom in, out, actual size

Cmd-click opens a link. Web and mail links open in your browser or mail app. Links to other text files open in editmd, and any other local file, folder or app is shown in Finder rather than opened, so a link in someone else's README can't launch anything. A link with any other scheme asks first and shows the full URL. #heading links scroll within the document, using the anchors GitHub gives headings, so a table of contents written for GitHub works here too. A new document has no folder yet, so its relative links do nothing until it is saved.

Click a task's checkbox to tick it. Front matter (a leading --- YAML or +++ TOML block) appears as a plain text box above the document and is saved verbatim. While it has the cursor, Cmd-Z undoes your edits there.

Images with relative paths load from the document's folder. Drop an image file on the document to add it where you drop it (beside a code block rather than inside it), linked by a path relative to the document's folder (or an absolute path if the document has not been saved yet). Dropping a text file opens it, and any other file is shown in Finder. Images from the web don't load until you ask, as in Mail, so an image in a document can't tell anyone you opened it. Each one shows as a small placeholder with its alt text until you choose View > Load Remote Images, which loads them for that window without reloading the document. A diagram that uses a web image shows an error until then.

Raw HTML stays in the file, shown as source text, and so do link reference definitions ([label]: url) and links with no text.

Fenced mermaid code blocks render as diagrams. Put the cursor in one, or click the diagram, to edit its source with a live preview underneath. The file keeps the ordinary fenced block, and Mermaid only loads when a document contains a diagram. The window follows the system's light or dark appearance.

How it works

editmd is an AppKit document-based app. NSDocument handles opening, writing files and the edited state. Each window holds a WKWebView running a Milkdown editor (ProseMirror for editing, remark for parsing and serializing CommonMark and GFM), bundled into the app so nothing is loaded from the network. Mermaid is bundled the same way, split into chunks the page loads on demand. A WebKit content rule list blocks every load except the app's own editmd: scheme and data the page already holds, and the page shows placeholders instead of asking for remote images. Load Remote Images removes the rules for that window.

The document in each window lives in a Yjs CRDT, bound to ProseMirror with y-prosemirror. The page remembers the state the file was last known to hold. When the file changes, the page parses the new text, replays the difference as edits against that state, block by block, and lets Yjs merge them with the user's. Front matter and source mode merge the same way, as plain text.

The deciding is kept apart from the doing. The merge logic is pure functions over encoded Yjs states, and when to save, when to reread the file and what the title bar says come from a pure state machine. The editor page, Document and the views carry out their results. Both halves have property tests.

  • Sources/editmd/ is the Swift app. Document.swift reads, writes and watches files, carrying out the decisions of Persistence. On save it merges anything newer on disk and asks the page for markdown. EditorBridge.swift wraps the page's window.editmd API, AssetSchemeHandler.swift serves the page and local images over an editmd: URL scheme, and RemoteContentBlocker.swift installs the rules that keep the page off the network. AgentEdits.swift posts agents' edit reports from --report-edit, collects them in the running app, lists running agents, and keeps the names the user gives them.
  • Sources/EditmdCore/ is the app's pure core: Persistence.swift (the autosave and file-watching state machine), TextFile.swift (encodings and line endings; the page only ever sees LF), LinkPolicy.swift (what opening a link does), RemoteContent.swift (what the page may load), ImageLink.swift (how a dropped image is linked) and AgentEdits.swift (reading agents' hook reports, and deciding whose change is whose from those or from the agents running). Tests/EditmdCoreTests/ checks it against a simulated editor, file and clock.
  • web/src/editor.ts is the editor: loading, merging, serializing, dirty tracking, source mode and formatting commands. web/src/sync.ts and web/src/diff.ts are the merge core, web/src/diagrams.ts renders mermaid blocks, web/src/anchors.ts gives headings GitHub's anchors, and web/src/remote.ts holds remote images back until they are allowed. web/src/changes.ts finds what a merge changed, and web/src/highlights.ts anchors those changes in the Yjs document and draws them. web/src/markdown/ adjusts how Milkdown reads and writes markdown where its defaults would change the file, such as ragged tables, list items that start with a heading, and links, images and references. web/test/ holds their unit and property tests, which run under bun with happy-dom and fast-check.
  • scripts/ builds, installs and checks the app. The app icon is drawn by scripts/make-icon.swift at build time. scripts/demo/ records short demo videos of the editor page in headless Chrome: scripts/demo/record.sh NAME plays scripts/demo/scenarios/NAME.ts and writes build/demo/NAME.mp4 and NAME.gif.

The binary has a few headless modes, used by make check, that drive the real page in WebKit without showing a window:

editmd --report-edit           # run by agents' tool hooks: report an edit to the running app
editmd --roundtrip FILE        # what Save writes for an untouched FILE
editmd --normalize FILE        # FILE re-serialized by the editor
editmd --snapshot FILE OUT.png [--dark] [--width N]
editmd --selftest FILE         # opens, edits, undoes, saves and reverts FILE (overwrites it),
                               # then checks autosave and outside edits on a scratch file beside it

About

A small native macOS editor for markdown files: edit the rendered document in place, save plain markdown back

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages