leaf-core 0.1.4

Frontend-neutral core of leaf: a byte-offset caret/selection model over twig, plus an AST→glyph map, styled with a toolkit-agnostic Style.
Documentation
---
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

| crate                                 | what it is                                                                                                                                                                                                                                                         |
| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`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

| package | what it is |
|---------|------------|
| [`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

| app | what it is |
|-----|------------|
| [`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:

| action | twig op |
|--------|---------|
| 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

| key | action |
|-----|--------|
| *(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.