Skip to content

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.

Terminal window
cargo add ratatui-diff

Rendering depends only on ratatui-core, so the widget works with any Ratatui backend. The optional syntax feature adds syntax colors.

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.

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.

examples/minimal.rs
//! 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:

The minimal example in unified view: the timeout and status check are removed and replaced, with the changed values emphasized.

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

The minimal example in split view, with the old function on the left and the new function on the right.

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);