---
contents:
- '[Sample](sample.md)'
---
# leaf (Work in progress!!)
A caret-based rich-text editor for documents, built on [`twig`](../twig).
## Workspace layout
leaf is a Cargo workspace in three tiers. The caret/selection model and the
AST→glyph mapping live in a frontend-neutral **core**; embeddable editor
**widgets** wrap it per toolkit; and thin **apps** host a widget with a window,
clipboard, and file I/O.
### `crates/` — Rust libraries
| [`leaf-core`](crates/leaf-core) | the document model — a `twig::Editor` with a byte-offset caret + selection, and the WYSIWYG `VisualMap`. Glyphs carry a **toolkit-agnostic `Style`**; no UI dependency. |
| [`leaf-ratatui`](crates/leaf-ratatui) | the **embeddable terminal widget** (ratatui + crossterm): renders the editing surface into a `Rect` and turns key/mouse events into `Doc` edits, returning an `Outcome` for what the host owns (quit, save, clipboard, dialogs). The terminal peer of `leaf-gpui`. |
| [`leaf-gpui`](crates/leaf-gpui) | the **embeddable GUI widget** on [gpui](https://github.com/zed-industries/zed): the `Editor` view plus its input, pixel-wrapping renderer, and `register_keybindings`. Renders only the editing surface and leaves window chrome, file I/O, and quit to the host. |
| [`leaf-ffi`](crates/leaf-ffi) | the **UniFFI Rust binding** — wraps the filesystem-free `Doc` behind a C ABI so a native Apple app can drive it. Paired with the `leaf-swift` package. |
| [`leaf-wasm`](crates/leaf-wasm) | the **wasm-bindgen Rust binding** — wraps the `Doc` for the browser (`LeafDoc` + a typed `DocView`). Paired with the `leaf-web` package. |
### `packages/` — importable non-Rust widget packages
| [`leaf-swift`](packages/leaf-swift) | the Swift Package (manifest at the repo root, so SwiftPM can resolve it by version) — `LeafUI`, the AppKit/UIKit editor view, over the committed UniFFI `leaf-ffi` binding. The Apple peer of `leaf-ratatui`/`leaf-gpui`. |
| [`leaf-web`](packages/leaf-web) | the npm package (not yet published; private until there is a consumer) — `LeafEditor`, a framework-agnostic web editor, over the `leaf-wasm` binding. Tables draw as a real grid from core's structural `TableView`, not as the box-glyph picture; links and task boxes are clickable; the toolbar dims what the format cannot spell. |
### `apps/` — runnable frontends
| [`leaf-tui`](apps/leaf-tui) | the terminal editor (binary `leaf`) — a thin host around `leaf-ratatui` wiring a terminal, clipboard, and dialogs. The workspace default `cargo run`. |
| [`leaf`](apps/leaf) | the standalone gpui **application** (binary `leaf-gui`) — a thin host around `leaf-gpui`. A standalone workspace, like `leaf-ios`. |
| [`leaf-ios`](apps/leaf-ios) | the gpui iOS host (a standalone workspace on the gpui-mobile platform). |
| [`leaf-editor`](apps/leaf-editor) | the cross-platform (macOS + iOS) AppKit/UIKit/UniFFI demo app, consuming `packages/leaf-swift`. |
| [`leaf-web-demo`](apps/leaf-web-demo) | the web demo page, consuming `packages/leaf-web`. |
```sh
cargo run -- path/to/document.md # the TUI (workspace default)
# The GUI is its own workspace (see `exclude` in Cargo.toml), so it is reached
# by manifest rather than by `-p`:
cargo run --manifest-path apps/leaf/Cargo.toml -- path/to/document.md
```
The other two frontends aren't a `cargo run` away — the Apple app is a Rust
staticlib behind a UniFFI binding behind an Xcode project, and the web demo is a
wasm build behind a static server. Both are one word through the task runner in
[`xtask/`](xtask):
```sh
cargo xtask swift # build + launch apps/leaf-editor on macOS
cargo xtask swift --ios # …in the iOS Simulator instead (--device to pick one)
cargo xtask web # build the wasm, serve apps/leaf-web-demo, open it
cargo xtask web --test # …serve the leaf-web editor tests instead
```
`cargo xtask swift` regenerates the UniFFI binding and the Xcode project when
they're missing, so a fresh checkout needs no separate bootstrap step; pass
`--regen` to force it after changing the Rust *API* surface. `cargo xtask --help`
lists every task and flag.
The runner holds the checks; releasing is its own tool. `cargo xtask ci` runs
everything a release has to pass — rustfmt, clippy with warnings denied, the
workspace tests, and a `cargo check` of each crate alone in each feature shape a
host actually builds. `release release <patch|minor|major|X.Y.Z>` runs those
checks, moves the version through every manifest and the lockfile, cuts the
generated region of [`docs/CHANGELOG.md`](docs/CHANGELOG.md) into a released
section, and commits and tags — locally. Pushing takes `--push`, and even a
pushed tag publishes nothing: `cargo publish --workspace` is a separate,
deliberate command, and `--dry-run` shows what it would upload, in dependency
order, along with every crate it holds back.
`leaf-wasm` and `leaf-ffi` are two projections of the same `leaf_core::Doc`, and
`cargo xtask ci` holds them level: a method exported by one binding and not the
other fails the `the_two_bindings_export_the_same_methods` test, which exists
because that is exactly how the browser ended up a year behind the Apple app.
Genuinely host-specific spellings are listed, with reasons, in
`BINDING_DIVERGENCE` in [`xtask/src/ci.rs`](xtask/src/ci.rs).
The web editor's own tests live in a browser, not in `cargo test`
([`packages/leaf-web/test`](packages/leaf-web/test)): what they cover is a
`TreeWalker`, a `Range`, a native selection, and text laid out in a proportional
font, and a stub DOM would mostly be testing the stub. `cargo xtask web --test`
serves them; the page reports into `document.title` and `window.__results` as
well as on screen. There is no browser in `cargo xtask ci`, so this is a task a
person runs.
`release` is the shared tooling in [diaryx-org/devtools][devtools], which leaf,
prov, twig, flower, and the historica repos all cut releases with; what makes
leaf leaf is [`.config/release.toml`](.config/release.toml) and nothing else.
[devtools]: https://github.com/diaryx-org/devtools
`leaf` and `leaf-gpui` pin gpui to a specific Zed commit (gpui isn't published to
crates.io); the first build fetches and compiles the gpui tree, so it is slow.
It has **both views**, toggled with `⌘e`, just like the TUI's `⌥w`:
- **source** — the raw document, caret in source bytes.
- **wysiwyg** — `leaf-core`'s `VisualMap` resolved: `**bold**` painted bold,
`_italic_` italic, `# ` / `` ` `` / `**` delimiters hidden, headings coloured,
list markers as bullets. Each rendered glyph still maps back to its source
byte, so the caret, selection, and clicks ride the *visible* text and step over
hidden delimiters — the identical `VisualMap` the TUI renders, here with real
proportional bold/italic via the per-glyph `to_gpui` styling in
`leaf-gpui/src/style.rs`.
Both views share one rendering path (a `RowLayout` per visual row carrying each
character's source offset), so caret, selection, and mouse hit-testing are
written once and work in either view. Keys: arrows/Home/End (+`⇧` to select),
type to edit, `⌘b`/`⌘i` bold/italic, `⌘e` toggle view, `⌘s` save, `⌘q` quit.
```sh
cargo run --manifest-path apps/leaf/Cargo.toml -- document.md
cargo run --manifest-path apps/leaf/Cargo.toml -- document.md wysiwyg
```
**gpui gotchas (macOS), learned the hard way:**
- The `gpui_platform` dependency **must** enable the `font-kit` feature. Without
it, gpui's macOS backend uses a placeholder text system that lays text out but
rasterizes *no glyphs* — the window, caret, and selection all render, but every
character is invisible. This is not a version issue; it's a feature flag.
- gpui uses library features stabilized in Rust 1.95, and its macOS backend
compiles Metal shaders at build time — so a full Xcode with the **Metal
Toolchain** component is required (`xcodebuild -downloadComponent
MetalToolchain`). The pinned toolchain lives in `rust-toolchain.toml`.
Sibling to [`bough`](../bough): **same backend, opposite model.** Where bough
moves a selection through the document's AST and edits the *tree*, leaf gives you
an ordinary text **caret**, mouse, selection, and a formatting toolbar — and
turns every keystroke into one of twig's offset-addressed edits. The document
stays a live, round-trippable AST the whole time you type into it, so a Markdown
file and a Djot file are edited through the exact same operations.
Two views, toggled with `⌥w`:
- **source** — the raw document with the caret in source bytes.
- **wysiwyg** — the markup *resolved*: headings coloured, `**bold**` as real
bold, the `#` / `**` / `` ` `` delimiters hidden. The caret still works because
every rendered glyph is tied back to the source byte it came from, so cursor
motion, clicks, and selection ride the *visible* text and step right over the
hidden delimiters. Because it reads the AST, Markdown and Djot that parse alike
render identically — the [`mdfried`](https://github.com/benjajaja/mdfried)
idea, made editable.
Every action maps onto twig's editor surface:
| type / delete | `edit_range(start, end, text)` |
| re-anchor the caret after an edit | the returned `Change` |
| breadcrumb / cursor context | `ancestors_at(offset)` |
| click to place the caret | `node_at` + the flat `nodes()` snapshot |
| **bold / italic / code / mark** | `toggle_inline(range, kind)` |
| **heading / body** | `set_block(offset, kind)` |
## Usage
```sh
cargo run -- path/to/document.md
```
Formats are detected by extension: `.md`/`.markdown`, `.dj`/`.djot`,
`.html`/`.htm`, `.xml`. The formatting toolbar targets the lightweight-markup
formats (Markdown, Djot).
The palette follows the terminal. On startup leaf asks it whether it is light or
dark (an `OSC 11` query, the same moment it probes for a graphics protocol) and
picks the matching colors, so the tint behind a code block is a lift off *your*
background rather than a fixed dark slab. Terminals that won't answer are
detected as such quickly and fall back to `COLORFGBG`, then to dark. To pin it:
```sh
LEAF_THEME=light cargo run -- path/to/document.md
```
## Keys
| *(printable)* | insert at the caret (replacing any selection) |
| `Enter` / `Backspace` / `Delete` | the usual — `Enter` in a table drops to the cell below, growing the table when there isn't one |
| `Tab` / `⇧Tab` | indent / outdent — in a table, walk the cells, appending a row off the last one |
| `⌥Enter` | an in-cell line break (the terminal's spelling of `⇧Enter`, which a terminal can't tell from `Enter`) |
| arrows / `Home` / `End` | move the caret |
| `Shift`+move | extend the selection |
| click / drag | place / drag the caret |
| `⌥b` / `⌥i` / `⌥c` | toggle **bold** / *italic* / `code` on the selection |
| `⌥m` / `⌥d` / `⌥u` | toggle mark/highlight (Djot) / strikethrough / underline |
| `⌥1`…`⌥6` | make the block at the caret a heading of that level |
| `⌥0` | make it a paragraph |
| `⌥7` / `⌥8` / `⌥9` | numbered list / bulleted list / quote |
| `⌥t` / `⌥x` | give the item at the caret a checkbox / tick the box |
| `⌥k` / `⌥l` | set the link destination / the code block's language |
| `⌥e` / `⌥f` / `⌥r` | insert an image / a footnote / a horizontal rule |
| `⌥g` | follow what the caret is on — a footnote to its note, a note back to its reference, a `#fragment` to the heading it names |
| `⌥w` | switch between the source and wysiwyg views |
| `⌥⇧w` / `⌥⇧f` | cycle the markup mode / flip line flow |
| `^p` or `⌥p` | **the command palette** |
| `F1` or `⌥h` | the key reference |
| `^s` / `⌥s` / `⌥n` / `^q` | save / save as / new / quit |
| right-click | the context menu: Format, Insert, Table and View flyouts |
| hover | peek at what's under the pointer — a footnote's note, a link's destination — without moving the caret |
Everything the keyboard doesn't reach is in the **command palette** — the three
media kinds, all fourteen table operations, and the markup/line-flow modes named
outright rather than cycled. Type a few letters of a command's name and press
Return; `table` finds the whole grid family, `ir` finds Insert Row Above.
The palette, the context menu, and the key reference are all generated from one
command table (`apps/leaf-tui/src/commands.rs`), so a command cannot gain a key
without gaining a menu row and a line in the help. Every one of those surfaces
also **dims what the document's format cannot spell** — `==highlight==` in a
Markdown file, a footnote in an HTML one — rather than hiding it, so what a
format can't do stays legible instead of merely absent.
## Status
Both views work: caret editing, mouse, selection, the format-aware toolbar, undo/
redo, and live AST awareness, in either source or wysiwyg. Tables are editable as
a grid; footnotes can be written and walked in both directions; images, video and
audio embed; and the markup and line-flow dials are reachable.
Known rough edges (next steps): no soft-wrap-aware width for wide/emoji glyphs
(columns are counted in chars); code blocks render read-styled but map coarsely,
so edit code in the source view; no inline-image rendering yet (kitty/sixel is
the natural follow-up now that the glyph map exists); and `⌥g` names an external
link's destination rather than opening it — launching a browser out of a text
editor is a decision for the person at the keyboard, and most terminals already
make a printed URL clickable.