A minimal monospace markdown editor for macOS.
No cloud, no telemetry, no AI (despite the name — that's for later).
Just a terminal-feeling WYSIWYG editor that opens .md files from
Finder, exports clean PDFs, and gets out of your way.
Built on Tauri v2 + Milkdown. The shipped .app is ~10 MB.
- Live preview, Typora-style. Markdown renders inline as you type, powered by Milkdown + ProseMirror + remark (CommonMark & GFM: tables, task lists, strikethrough).
- Flip to raw markdown anytime. Toolbar toggle swaps the rendered view for an editable plain-text view of the on-disk source — frontmatter and all. Edit either way; the file round-trips byte-for-byte.
- Monospace by default, system font one click away. Toggle in the top-right toolbar.
- Grayscale UI, light/dark/auto. No blue tints, no color noise. Follows macOS appearance, or pick a mode manually.
- Tables that actually look like tables. Column widths adapt to the content on the page — numeric columns stay tight, prose columns get wider. Never narrower than the surrounding paragraphs.
- Real PDF export.
⌘E(or the toolbar button) opens a save dialog — pick a name and location, get a clean PDF. - Finder integration. Double-click any
.mdfile and it opens in iso.md. - Keyboard-first.
⌘Z/⇧⌘Zundo,⌘=/⌘-/⌘0zoom,⌘S/⌘Osave/open,⌘⇧Dtheme. - Left-aligned, full-height, pinned to the left of your screen. Optimized for writing alongside something else (docs, browser, Figma).
If you don't want to build from source, grab the latest .app from the
Releases page (or ask the person who sent you this repo — they
probably have a build handy), drag it to /Applications/, then:
# Make iso.md the default handler for .md files (optional)
swift -e 'import Cocoa
LSSetDefaultRoleHandlerForContentType(
"net.daringfireball.markdown" as CFString,
.all,
"com.aaro.isomd" as CFString
)'Requires: macOS, Node 20+, pnpm, and a Rust toolchain via rustup.
# One-time toolchain setup
brew install rustup pnpm
rustup default stable
export PATH="$HOME/.cargo/bin:$PATH"
# Clone + build
git clone https://github.com/aaroi/iso-md.git
cd iso-md
pnpm install
pnpm tauri build
# Install to /Applications
cp -R src-tauri/target/release/bundle/macos/iso.md.app /Applications/First build takes a few minutes (Rust crate compile). Subsequent builds are ~30 seconds.
pnpm tauri dev # hot-reload dev window with DevTools attached
pnpm build # frontend type-check + Vite bundle
pnpm tauri build # full production .appThe Tauri dev window auto-opens DevTools. In release builds, the
devtools feature is enabled — right-click → Inspect to open the
inspector if you need to debug.
src/ Frontend (TypeScript + Vite)
├── main.ts App bootstrap, toolbar, menu, file I/O, PDF export
├── editor.ts Milkdown setup (CommonMark, GFM, history)
├── table-sizer.ts Content-adaptive column widths (see note below)
├── styles.css Grayscale theme, mono/system font
└── print.css Print-media overrides for PDF export
src-tauri/ Rust backend (Tauri v2)
├── src/lib.rs Menu, window sizing, Finder-open events
├── tauri.conf.json Bundle config + file association
└── capabilities/ fs & dialog permissions
| Layer | What |
|---|---|
| Shell | Tauri v2 (Rust + WKWebView) |
| Editor | Milkdown (ProseMirror + remark) |
| Build | Vite + TypeScript |
| html2pdf.js | |
| Font stack | ui-monospace, "SF Mono", Menlo, … |
The app uses Tauri's fine-grained permission system:
- Filesystem — read/write text files under
$HOME,$DOCUMENTS,$DESKTOP,$DOWNLOADS, and$TEMP. No other paths are reachable. Seesrc-tauri/capabilities/default.json. - Dialog — open/save/ask dialogs (native macOS sheets).
- No network access. No analytics, no auto-update, no telemetry.
If a teammate opens a .md file outside one of those directories, the
read will fail. Adjust the scope list in capabilities/default.json
if you need broader access.
Getting content-adaptive columns in a ProseMirror editor is surprisingly hard:
- CSS
table-layout: autodistributes space near-evenly in WebKit, regardless of content length. - Setting inline styles on individual
<td>/<col>elements gets wiped when ProseMirror replaces table DOM on state updates.
The working answer is to measure each cell's natural (no-wrap) text
width in JS, compute per-column pixel widths (clamped to
[60px, 520 + padding]), and inject them as CSS rules into a
<style> element in <head>. ProseMirror doesn't manage <head>, so
the rules survive its DOM churn. Tables scale up to match the prose
column width if their content is narrower than the text below them.
The full logic lives in src/table-sizer.ts (about 150 lines).
MIT © 2026 Aaro Isosaari
