Contributing
mido is small enough to read in an afternoon. This page explains how the pieces fit, then walks through the two changes people make most often: teaching mido a new Markdown element, and writing the snapshot test that proves it.
Set up
git clone https://github.com/anistark/mido
cd mido
cargo run -- README.md
just check
just check runs cargo fmt --check, clippy with warnings denied, and the tests, which is what CI runs. Rust 1.93 or newer. The docs site needs pnpm and Node 24, and just docs starts it. just demo re-records the landing page video from docs/tapes/landing.tape and needs VHS 0.11.0 with ttyd and ffmpeg, because VHS 0.12.0 writes no output.
The pipeline
One pipeline, two outputs. Every stage is a plain function, so each can be tested on its own.
The repository is a Cargo workspace of two crates. crates/mido-core holds parse, the document model, layout, the themes and the glyph tiers, with no terminal dependency beyond ratatui-core's style and text types, so another tool can embed the renderer. The mido binary at the root holds the viewer, the config, print mode and the project scan, and re-exports the core as mido::markdown and mido::render. Paths below that start with core/ are under crates/mido-core/src/.
source text
-> parse pulldown-cmark events -> Document core/markdown/parse.rs
-> Document blocks and inlines, each block with its core/markdown/document.rs
byte range in the source
-> layout Document + width + theme -> lines, plus core/render/layout.rs
headings, links and a source map
-> paint ratatui frame in the viewer src/app/draw.rs
-> print ANSI text on stdout with -p src/render/ansi.rs
The Document in the middle is the important decision. Everything else walks it: the outline, search, link following, history, print mode and the snapshot tests. A block never loses its source range, which is how a resize keeps your reading position and how the outline knows which section you are in.
Around the pipeline:
src/app/mod.rsholds the state and the event loop.src/app/keys.rsis the keymap, the source of truth for the help overlay and the Keys page.src/app/outline.rsis the foldable tree used by both panels,finder.rsthe fuzzy file picker,search.rssmart-case search,watch.rsthe file watcher.src/project/scan.rswalks a folder with the ignore crate and picks the entry file.src/config.rsreads the config and the project.mido.tomland resolves theme names and paths, andsrc/themes.rsprintsmido themes.core/render/wrap.rswraps styled text without splitting a style run,syntax.rswraps syntect,theme.rsmaps semantic tokens to styles and reads theme files,themes/holds the built-in themes,glyphs.rsthe symbol tiers, andmermaid/turns diagram blocks into text.src/docs.rsembedsdocs/*.mdformido docs, andbuild.rscopies them in and generates the man page.
Add a block type
Say you want to render GitHub alerts, the > [!NOTE] blockquotes. The change touches each stage once.
- Model it. Add a variant to
BlockKindincore/markdown/document.rs. Keep it data only: what the block contains, nothing about how it looks. - Parse it. In
core/markdown/parse.rstheBuilderturns pulldown-cmark events into a tree ofNodeframes. A block with children needs aNodevariant that collects them, astartarm for itsTag, and anend_framearm that pushes the finishedBlockwith its byte span throughself.block. Blocks without children, like a rule, are pushed straight from the event. - Draw it. In
core/render/layout.rsadd an arm toRenderer::blockand a method besidequote,listandtable. Build styled pieces, wrap them withwrapatself.avail(), and emit lines withemit_line. For nested content push a prefix withpush_prefix, render the inner blocks withself.blocks, thenpop_prefix. Colors come fromcore/render/theme.rs: add a semantic token to thetokens!list there rather than using a raw color, and give it a value in every file undercore/render/themes/. A test fails when a built-in theme misses one, andjust colorsfails on a rawColor::anywhere else. Symbols come fromcore/render/glyphs.rs, with an ASCII form for each. - Check the outputs. Print mode uses the same lines, so it is done. The viewer paints the same lines, so it is done too, unless the block interacts with links or headings, in which case the indices in
Layoutneed the new entries. - Test it. A unit test in
parse.rsfor the tree, a fixture and a layout snapshot for the drawing, and a screen snapshot if the chrome changes. See the next section. - Document it. Add the element to What renders and a line to
CHANGELOG.mdunder the unreleased version, in the same commit.
Write a snapshot test
Snapshots are the main test style, through insta. The rendered text is the contract, so a change that looks different fails until someone approves it.
There are three kinds:
- Layout snapshots in
tests/layout.rsrender a fixture fromtests/fixtures/at 60 columns withlayoutand snapshot the plain lines. Add a fixture file for a new element and a line tosnapshots_at_60_columns. - Screen snapshots in
tests/app.rsdrive the whole app through ratatui'sTestBackendat 80, 120 or 140 columns, press keys withapp.press, and snapshot the terminal buffer. Use these when panels, overlays or the status bar change. - Diagram snapshots in
core/render/mermaid/cover each Mermaid type. - The theme gallery in
tests/gallery.rsdraws one screen per built-in theme and checks each paints its own colors.just gallerywrites the pictures totarget/gallery, and the docs site publishes them.
Writing one:
#[test]
fn alerts_render_with_a_label() {
let text = std::fs::read_to_string("tests/fixtures/alerts.md").unwrap();
let lines = layout(&parse(&text), &Theme::dark(), 60).plain_lines();
insta::assert_snapshot!("alerts", lines.join("\n"));
}
Run cargo test --workspace. A new or changed snapshot is written next to the old one as a .snap.new file and the test fails. Review it with cargo insta review if you have cargo-insta installed, or set INSTA_UPDATE=always on one run and read the diff with git diff tests/snapshots. Commit the .snap file, never the .snap.new. Width matters: the fixtures also run through lines_never_exceed_the_width, which is the test that catches a wrap bug.
Keys and docs
The keymap in src/app/keys.rs drives the help overlay and the Keys page, and the theme list and tokens drive the reference half of Themes. After editing either run just keys, which regenerates the tables in docs/keys.md and docs/themes.md. A test compares the two and fails when they drift, and another checks that every command line flag appears in Command line.
The docs pages are plain Markdown plus front matter. A test renders each one in print mode and rejects template syntax or raw HTML, because the same files are bundled into the binary for mido docs. Another walks every relative link and #anchor across the docs, README, CONTRIBUTING and CHANGELOG and fails on one that does not land on a file or a heading. After adding a page, run touch build.rs once so the bundle picks it up.
Before you open a pull request
just checkpasses.CHANGELOG.mdhas a line for the change.- Commit messages follow the
type: summaryshape in the history, for examplefeat: fold sections in the outline.