ldnddev / dd_emailforge

Build an email in the terminal.

This tutorial walks through setup, install, and the TUI: structure tree, details blueprint, insert, FormEdit, preview, and export. Screenshots are captured from the live renderer — see Updating screenshots if the UI changes.

What you get

dd_emailforge is a Ratatui TUI. You edit a typed template.json. The app emits strict MJML and compiles HTML with official MJML 5 (mjml ^5.4.0).

template.json  →  template.mjml  →  template.html
                      │
                      └─ mjml -w → .preview/  (TUI p / CLI preview)

Prerequisites

Install

Download the package that matches this machine (Linux or macOS, x86_64 or aarch64):

curl -fsSL https://raw.githubusercontent.com/ldnddev/dd_emailforge/main/install.sh | bash

That installs $HOME/.local/bin/dd_emailforge, writes the default theme to ~/.config/ldnddev/dd_emailforge_theme.yml (only if none exists), and creates ~/.config/ldnddev/dd_emailforge/templates/. Pin a release with VERSION=v0.9.3.

From a clone of this repo (builds with cargo):

./install.sh
./install.sh --from-release   # GitHub package even from a clone
Override paths with PREFIX, BIN_DIR, or CONFIG_DIR. Honors XDG_CONFIG_HOME. Re-run is safe: an existing theme is left alone; the binary is overwritten.
curl -fsSL https://raw.githubusercontent.com/ldnddev/dd_emailforge/main/install.sh | bash -s -- uninstall
./install.sh uninstall    # same, from a clone
./install.sh --help

If ~/.local/bin is not on your PATH, add it:

export PATH="$HOME/.local/bin:$PATH"

Create a template

dd_emailforge init ./my-email --from welcome
# skip npm install when `mjml` is already on PATH
cd my-email && npm install
dd_emailforge .

A directory argument means dir/template.json. init writes:

--fromIntentFooter
welcome (default)Post-signupaddress + Unsubscribe + *|UNSUB|*
newsletterDigestsame
promoSalesame
transactionalReceipt / resetaddress; empty unsub label and href (F3 stays clean)

Welcome ships Google Font Raleway. Starter images are https://dummyimage.com/… placeholders.

Open the TUI

Bare dd_emailforge reopens the last template, or starts empty if none is remembered. A path argument opens that folder. You’ll get an info toast until a template exists; n New and o Open work in the editor:

Empty TUI: Structure shows HEAD, BRAND, BODY; a toast says run init
No template open. n creates a starter folder; o opens template.json or a folder.

After init --from welcome:

Welcome starter in the TUI: structure tree on the left, full email blueprint on the right
Master/detail: Structure tree (left) and a full-email ascii blueprint (right). Footer starts with F1: Help. Quit is Ctrl+Q only — bare q never quits.

Chrome

Below 48 columns, Details hides and you get Structure only. Tab toggles pane focus when both panes exist.

F1 is the full key + mouse list:

F1 help overlay listing global keys, tree keys, and FormEdit keys
Help overlay. Esc or F1 closes. j/k scroll.

Edit, insert, select

FormEdit

Select a row and press Enter (or double-click). Tab between fields, cycle enums with left/right, Ctrl+S saves and rewrites template.mjml so a live preview updates immediately, Esc cancels. On image URL fields, Ctrl+P opens a picker rooted at images/ (it cannot walk above the template folder).

FormEdit modal on email-header with logo src, alt, href, and width fields
Editing email-header. Dummyimage URLs in starters are valid https:// placeholders — swap them for real assets.

Insert

/ opens a fuzzy picker filtered to kinds that are legal for the current selection. Nested tags stay under their MJML parent: mj-navbar-link only under mj-navbar, accordion elements only under accordion, carousel images only under carousel. A section takes columns and groups, not leaves as direct children (a leaf splices into the last column, or wraps a column on empty BODY).

Insert picker on a column listing mj-text, mj-button, mj-navbar, mj-accordion, mj-carousel
Picker on an mj-column. Type to filter, Enter inserts, Esc cancels. Navbar / accordion / carousel are column children.

Blueprint click-to-select

The details canvas always shows the full email. Click a box (or its nested text) to select that node in the tree. Innermost hit wins. The tree expands ancestors so the row is visible.

mj-text selected in the structure tree and highlighted in the details blueprint
A selected mj-text is highlighted in Structure and in the blueprint. Click the other column’s text to hop there without walking the tree.

Other tree edits: d delete (not HEAD/BRAND/BODY), y duplicate, u undo (cap 20), U / Ctrl+R redo, J/K reorder, C/V add/remove a column (equal-% widths), c/v hop columns.

Autosave rewrites template.json 2s after a change when a path is set. Manual s also writes template.json.backup.

Validate, preview, export

ActionTUICLI
ValidateF3dd_emailforge validate <path>
Previewpdd_emailforge preview <path> [--port 8766]
Export MJML+HTMLShift+Edd_emailforge export <path> [--out <dir>]
Dump JSON—dd_emailforge show <path>

<path> is a template.json or a folder containing one, matching dd_emailforge <command> --help.

F3 / preview / export all gate on validation errors. Warnings (unused font, marketing unsub heuristic) toast or print but do not block.

dd_emailforge export ./my-email
dd_emailforge export ./my-email --out ./dist
dd_emailforge preview ./my-email --port 8766

Keys

KeyAction
F1Help
F2Theme source + sampled tokens
F3Validate (Enter jumps to the node/field)
nNew template
oOpen template.json or folder
pPreview
Shift+EExport
sSave JSON + .backup
/Insert picker (legal kinds only)
EnterFormEdit
Ctrl+QQuit (confirm if unsaved)
j/k g/G h/l SpaceTree move / jump / fold
d y u U J/KDelete / duplicate / undo / redo / reorder
C/V c/vAdd/remove column · prev/next column
Ctrl+PImage picker on src fields

Theme

Lookup order (first file that exists and declares version: 1 wins):

  1. ./dd_emailforge_theme.yml
  2. $XDG_CONFIG_HOME/ldnddev/dd_emailforge_theme.yml if XDG_CONFIG_HOME is set
  3. ~/.config/ldnddev/dd_emailforge_theme.yml
  4. Built-in defaults

Schema: LDNDDEV_TUI_VISUAL_STANDARD.md. Do not invent tokens.

Updating screenshots

The PNGs under docs/images/ are not hand-drawn. They are screenshots of HTML frames dumped from the live Ratatui renderer (same App::draw path as the TUI).

From the repo root:

./docs/capture.sh

That will:

  1. Run the ignored test tui::tutorial_shots::capture_tutorial_frames, which writes one HTML frame per shot using a pinned header quote and the welcome starter (or empty chrome).
  2. Open each frame in headless Chromium and write the PNG.
  3. Delete the temporary _frame-*.html files.

Requires cargo and a Chromium/Chrome binary on PATH (or set CHROME). Optional: EMAILFORGE_TUTORIAL_SHOTS=/some/dir to write somewhere else.

FileScene (edit src/tui/tutorial_shots.rs to change)
tui-empty.pngNo template; info toast pointing at init
tui-welcome.pngWelcome starter, BODY selected, full blueprint
tui-insert.png/ picker on an mj-column
tui-formedit.pngFormEdit on email-header
tui-help.pngF1 help overlay
tui-selected.pngmj-text selected in tree + blueprint highlight
After a TUI visual change (chrome, blueprint, picker, forms), re-run ./docs/capture.sh and commit the PNGs with the code. Do not redraw these in an image editor — the capture path is the source of truth so labels stay honest. To add a shot: append a writer in capture_tutorial_frames, add the filename here and in this page’s <figure>, then recapture.

More capture notes: docs/README.md.

More docs