Getting started
ratatui-diff is a widget, not an application. You prepare a DiffDocument, keep a DiffState
between frames, and render a Diff inside your own draw callback. Your application owns the
terminal, the event loop, key bindings, and where the source text comes from.
Install
Section titled “Install”cargo add ratatui-diffRendering depends only on ratatui-core, so the widget works with any Ratatui backend. The optional
syntax feature adds syntax colors.
The three pieces
Section titled “The three pieces”| Type | Lifetime | Role |
|---|---|---|
DiffDocument |
Once per comparison | Immutable files, hunks, lines, and word highlights. Prepare it once. |
DiffState |
As long as the view exists | Viewport, cached layout, folds, search, and selection. |
Diff |
One frame | Borrows the document and selects presentation options for this frame. |
Building a Diff is cheap; build it in the draw closure. Changing presentation (mode, wrapping,
theme) keeps the viewport anchored to the nearest source line. Rendering a different document resets
navigation on that render.
A complete application
Section titled “A complete application”This is
examples/minimal.rs. It
compares two strings, renders them with the Aardvark Ink theme, and wires a few keys to navigation.
Run it with cargo run --example minimal.
//! Smallest complete host: compare two strings, draw the diff, and scroll it from the keyboard.//!//! The documentation site embeds this file as its getting-started example.use std::error::Error;
use crossterm::event::{self, KeyCode};use ratatui::DefaultTerminal;use ratatui_diff::{Diff, DiffDocument, DiffState, DiffTheme, ViewMode};
const OLD: &str = "\fn retry_delay(status: u16) -> Option<u64> { let timeout_ms = 500; if status == 503 { Some(timeout_ms) } else { None }}";
const NEW: &str = "\fn retry_delay(status: u16) -> Option<u64> { let timeout_ms = 1500; if matches!(status, 429 | 503) { Some(timeout_ms) } else { None }}";
fn main() -> Result<(), Box<dyn Error>> { // Compare once; the document is immutable and can be borrowed by every frame. let document = DiffDocument::from_text(OLD, NEW); ratatui::run(|terminal| run(terminal, &document))}
fn run(terminal: &mut DefaultTerminal, document: &DiffDocument) -> Result<(), Box<dyn Error>> { // State keeps the viewport and cached layout between frames. let mut state = DiffState::new(); let mut mode = ViewMode::Unified; loop { // Build the widget each frame; it only borrows the document. let diff = Diff::new(document) .mode(mode) .theme(DiffTheme::aardvark_ink()); terminal.draw(|frame| frame.render_stateful_widget(&diff, frame.area(), &mut state))?;
// The host owns key bindings; navigation methods take effect on the next draw. if let Some(key) = event::read()?.as_key_press_event() { match key.code { KeyCode::Char('q') | KeyCode::Esc => return Ok(()), KeyCode::Char('s') => { mode = match mode { ViewMode::Unified => ViewMode::Split, _ => ViewMode::Unified, } } KeyCode::Char('j') | KeyCode::Down => state.scroll_lines(1), KeyCode::Char('k') | KeyCode::Up => state.scroll_lines(-1), KeyCode::PageDown => state.scroll_pages(1), KeyCode::PageUp => state.scroll_pages(-1), KeyCode::Char(']') => state.next_hunk(), KeyCode::Char('[') => state.previous_hunk(), _ => {} } } }}It opens in unified view:

Press s to switch to split view. The same DiffState is reused, so the viewport stays in place:

Render before you navigate
Section titled “Render before you navigate”Navigation methods like scroll_pages, next_hunk, and scroll_to_source need the viewport size
and row positions, which the state learns during rendering. Draw at least once before handling
navigation or hit-testing. Methods called between frames take effect on the next draw.
Tests and non-interactive code can render directly into a buffer:
use ratatui_core::buffer::Buffer;use ratatui_core::layout::Rect;use ratatui_core::widgets::StatefulWidget;use ratatui_diff::{Diff, DiffDocument, DiffState};
let document = DiffDocument::from_text("hello world\n", "hello Rust\n");let area = Rect::new(0, 0, 80, 20);let mut buffer = Buffer::empty(area);let mut state = DiffState::new();let diff = Diff::new(&document);diff.render(area, &mut buffer, &mut state);state.scroll_pages(1);Next steps
Section titled “Next steps”- Choose an input: strings, Git patches, or structured hunks.
- Appearance: layouts, wrapping, whitespace, and themes.
- Interactive viewer: a fuller host with mouse, search, selection, and folding.