A navigable unified and split diff widget for Ratatui.
Display text comparisons or Git patches with line numbers, word highlights, and keyboard-driven
navigation. Your application supplies terminal setup, key bindings, and input data. Rendering
depends only on ratatui-core.
Prepare a DiffDocument once and keep a DiffState between frames to retain the viewport
and cached layout. Build a Diff in the draw closure to select presentation options:
use ratatui_diff::{Diff, DiffDocument, DiffState, ViewMode};
let document = DiffDocument::from_text("hello world\n", "hello Rust\n");
let mut state = DiffState::new();
ratatui::run(|terminal| {
terminal.draw(|frame| {
let diff = Diff::new(&document).mode(ViewMode::Split);
frame.render_stateful_widget(&diff, frame.area(), &mut state);
})?;
// Call from your PageDown handler, then redraw.
state.scroll_pages(1);
Ok::<(), std::io::Error>(())
})?;ratatui::run sets up a Crossterm terminal and restores it when the closure returns. The draw
closure works with any Ratatui backend. The interactive viewer includes key bindings and an
event loop.
DiffDocument::from_textcompares old/new UTF-8 source with three context lines.DiffDocument::compareselects the number of context lines when generating hunks.DiffDocument::parsereads unified or multi-file Git patches; errors include byte offsets when available.DiffDocument::newvalidates caller-provided files, hunks, lines, and highlight ranges.
use ratatui_diff::DiffDocument;
let patch = "--- a/greeting\n+++ b/greeting\n@@ -1 +1 @@\n-hello world\n+hello Rust\n";
let document = DiffDocument::parse(patch)?;
assert_eq!(document.files().len(), 1);A patch displays only its supplied changes and context. Binary changes appear as summaries. Combined merge diffs are rejected. Source content retains CRLF and final-newline differences; control characters are displayed as visible text.
The defaults are unified view, visible line numbers and word highlights, no wrapping or
whitespace markers, four-cell tab stops, and the dark theme. Split replacements use bounded
similarity anchors and pad unmatched lines; unanchored gaps and large runs pair in source order.
Each DiffTheme style can be customized independently.
use ratatui_diff::{Diff, DiffDocument, DiffTheme, ViewMode};
let document = DiffDocument::from_text("old\n", "new\n");
let diff = Diff::new(&document)
.mode(ViewMode::Split)
.theme(DiffTheme::light())
.wrap(true)
.whitespace(true)
.tab_width(4);Wrapped split rows use the taller side's height. Without wrapping, horizontal scrolling is
synchronized across panes. Graphemes are never split at viewport edges. Disable inline
highlights with Diff::word_highlights when whole-line styling is sufficient.
Enable the non-default syntax feature to use bundled Syntect grammars and two-face themes.
Choose a language and theme; the adapter handles tokenization and source-byte mapping. Prepare
outside drawing, then borrow the result. Complete source establishes lexical state through
omitted lines; explicit SyntaxSource::Retained is best-effort when only patch text is
available.
use ratatui_diff::{
Diff, DiffDocument, DiffTheme, FileSyntax, SyntaxHighlighter, SyntaxSource,
};
let old = "fn retry() { let timeout = 500; }\n";
let new = "fn retry() { let timeout = 1500; }\n";
let document = DiffDocument::from_text(old, new);
let theme = DiffTheme::aardvark_ink();
let highlighter = SyntaxHighlighter::bundled("Coldark-Dark")?.contrast_with(theme)?;
let styles = highlighter.prepare(
&document,
&[FileSyntax {
file: 0,
language: "rs",
source: SyntaxSource::Full { old, new },
}],
)?;
let diff = Diff::new(&document).theme(theme).syntax_styles(&styles)?;contrast_with explicitly adjusts syntax foregrounds for the selected RGB diff backgrounds;
omit it to preserve bundled colors. Word/search/selection overlays remain later in composition.
Syntax results own no source references. Replacing them preserves navigation and interaction
state; a new document requires new preparation. Size limits bound accepted input and stored
spans, not regex time. Hosts can schedule preparation on a worker. Distributing applications
must include the bundled asset notices from SyntaxHighlighter::acknowledgements.
Render once before page navigation or source mapping so DiffState has the viewport
dimensions and row positions. Scrolling counts displayed rows, including headers and wrapped
continuations. Source positions identify a file, an old or new side, and a one-based line
number.
- Lines, pages, half pages:
scroll_lines,scroll_pages,scroll_half_pages. - Horizontal cells:
scroll_horizontal. - Start/end:
start,end. - Hunk/file boundaries:
next_hunk,previous_hunk,next_file,previous_file. - Source positions:
scroll_to_source,source_at.
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::widgets::StatefulWidget;
use ratatui_diff::{Diff, DiffDocument, DiffState, Side, SourcePosition};
let document = DiffDocument::from_text("before\n", "after\n");
let area = Rect::new(0, 0, 80, 3);
let mut buffer = Buffer::empty(area);
let mut state = DiffState::new();
let diff = Diff::new(&document);
diff.render(area, &mut buffer, &mut state);
assert!(state.scroll_to_source(SourcePosition {
file: 0,
side: Side::New,
line: 1
}));Resizing or switching modes retains the nearest available source anchor. Replacing the document resets navigation on the next render. Theme and word-highlight changes preserve layout. Give each independently navigated pane its own state, even when both borrow the same document.
Keep anchor and focus in DiffState::set_selection, then extend focus with
SelectionMotion from host key bindings. Pointer handlers can convert a hit SourceRange
with start_boundary and
end_boundary. Selection stays within one file and side, survives
resizing and view changes, and clears on document replacement.
selected_text returns original source bytes for the host's
clipboard integration. It preserves tabs, controls, CRLF, and final-newline state; it excludes
gutters, wrapping, padding, and display notation. Invalid grapheme boundaries and missing patch
context are rejected. Diff::selection_style controls the overlay applied after word
highlights.
Update a case-sensitive literal query in your event handler, then navigate cached occurrences.
Both-side search counts shared context once. Restrict search with Some(Side::Old) or
Some(Side::New); an empty query clears it. Matches follow document order and navigation wraps.
Omitted patch context, file headers, and metadata are not searched.
use ratatui_diff::{DiffDocument, DiffState};
let document = DiffDocument::from_text("timeout: 500\n", "timeout: 1500\n");
let mut state = DiffState::new();
state.set_search(&document, "timeout", None);
assert_eq!(state.search_matches().len(), 2);
assert!(state.next_match());
assert_eq!(state.active_match(), Some(0));
// Render the widget to reveal the selected occurrence.
assert!(state.previous_match());
assert_eq!(state.active_match(), Some(1));Matches retain source byte ranges through wrapping, resizing, and mode changes. Search styles compose over changed-word styles; the selected occurrence uses reverse video in every preset, including monochrome. Query updates scan available source synchronously; frame rendering reuses the results. Updating a query or side clears the active match, while identical updates preserve it. Rendering a replacement document clears search; the host can then reapply its query.
Comparison is synchronous; prepare large documents outside the event loop. Rendering reuses the comparison results and draws only visible rows, but first layout and resize can process the whole document. Large replacements fall back to whole-line styling when they exceed the word-comparison size limits.
This pre-1.0 API may change between minor releases.
Unified view keeps additions and deletions together, with changed words emphasized.
Split view places old source on the left and new source on the right.
Whitespace markers make spaces visible. The animated demo holds each captioned view for five seconds.
The user guide walks through inputs, appearance, and
interaction with screenshots. See Contributing,
architecture, dependency decisions, and
deferred work. Run just example for the interactive viewer. See
releasing for package and screenshot publication.
Licensed under either MIT or Apache-2.0, at your option.

