Ratatui — the seam is a grid, and only one of its ten methods draws
Category: cell-only. Last reviewed: August 23, 2026. Pinned at a2ca2df5.
The terminal end of the range done deliberately. Ratatui's renderer seam is not a drawing API at all: widgets write into a reified cell grid, and the Backend trait exists only to push the difference between two such grids at a terminal. Everything canvas-seam-friction.md's eight entries argue about — measurement, semantics, command shape, payload ownership — lives above the seam, in the Buffer.
| Field | Value |
|---|---|
| Language | Rust (ratatui-core is #![no_std] + alloc — lib.rs:1) |
| License | MIT (LICENSE) |
| Repository | ratatui/ratatui |
| Documentation | docs.rs/ratatui, ratatui.rs |
| Category | cell-only |
| Pinned revision | a2ca2df5688772baffb743b494761f4ec82b3174 |
| Target range | character cells only — one target class, many terminal libraries |
| Backends shipped | ratatui-crossterm, ratatui-termion, ratatui-termina, ratatui-termwiz, plus TestBackend in the core crate |
| The seam | trait Backend — 10 required methods, of which one carries content |
| The intermediate | Buffer — a Vec<Cell> plus a Rect, not a command stream |
Overview
What it solves
A terminal application must turn a widget tree into the smallest possible byte stream of cursor moves, SGR changes and printed graphemes, across four incompatible terminal-library crates and one in-memory test target. Ratatui splits that into two problems with a data structure between them.
Design philosophy
Stated at the top of Buffer (buffer.rs):
No widget in the library interacts directly with the terminal. Instead each of them is required to draw their state to an intermediate buffer. It is basically a grid where each cell contains a grapheme, a foreground color and a background color. This grid will then be used to output the appropriate escape sequences and characters to draw the UI as the user has defined it.
And, on the seam itself (backend.rs):
Most applications should not need to interact with the
Backendtrait directly as theTerminalstruct provides a higher level interface for interacting with the terminal.
The seam is not a public authoring surface. It is a device driver, and the authoring surface is the grid.
How it works
Widget is the whole widget contract (widget.rs):
pub trait Widget {
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized;
}A widget receives a Rect and a mutable grid. There is no painter, no clip stack, no state object. Clipping is the Rect: Buffer::set_stringn clamps its remaining width to self.area.right(), and Buffer::cell_mut returns Option rather than panicking outside the area (buffer.rs).
The backend seam is the other half (backend.rs):
pub trait Backend {
type Error: core::error::Error;
fn draw<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
fn hide_cursor(&mut self) -> Result<(), Self::Error>;
fn show_cursor(&mut self) -> Result<(), Self::Error>;
fn get_cursor_position(&mut self) -> Result<Position, Self::Error>;
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> Result<(), Self::Error>;
fn clear(&mut self) -> Result<(), Self::Error>;
fn clear_region(&mut self, clear_type: ClearType) -> Result<(), Self::Error>;
fn size(&self) -> Result<Size, Self::Error>;
fn window_size(&mut self) -> Result<WindowSize, Self::Error>;
fn flush(&mut self) -> Result<(), Self::Error>;
// + `append_lines`, defaulted to `Ok(())`; two deprecated cursor shims; and
// + `scroll_region_up`/`_down` under `#[cfg(feature = "scrolling-regions")]`
}Ten required methods; nine are terminal lifecycle. The single content method takes an iterator of positioned cell references, not commands.
Terminal holds buffers: [Buffer; 2] and an index, and the render pass is three lines of real work (terminal/buffers.rs):
pub fn flush(&mut self) -> Result<(), B::Error> {
let previous_buffer = &self.buffers[1 - self.current];
let current_buffer = &self.buffers[self.current];
let updates = previous_buffer.diff_iter(current_buffer).inspect(/* track last pos */);
self.backend.draw(updates)?;
// ...
}
pub fn swap_buffers(&mut self) {
self.buffers[1 - self.current].reset();
self.current = 1 - self.current;
}Terminal::try_draw is autoresize → build a Frame over the current buffer → run the user callback → apply_buffer_with_cursor (terminal/render.rs).
A backend is then a peephole optimiser over that stream: the crossterm one keeps fg/bg/underline_color/modifier plus last_pos as running registers, emits MoveTo only when the cell is not horizontally adjacent to the previous one, and emits SGR only on change (ratatui-crossterm/src/lib.rs).
BufferDiff (buffer/diff.rs) is a zero-allocation Iterator yielding (u16, u16, &Cell). Most of its complexity is not diffing but terminal physics: a wide grapheme's trailing column is a cell the terminal painted but the buffer models as blank, so shrinking a styled wide glyph must force-emit the trailing cells or the old background lingers. Six regression tests in that file pin that behaviour, including issue #2585.
Q1 — measurement units, and who answers
Width is a property of the string, computed above the seam and never asked of a backend. Backend has no measurement method at all.
The oracle is trait CellWidth (buffer/cell_width.rs):
pub trait CellWidth {
fn cell_width(&self) -> u16;
}
impl CellWidth for str {
fn cell_width(&self) -> u16 {
if self.len() == 1 { /* ASCII fast path */ 1 }
else {
let width = self.width() as u16; // unicode-width
width.saturating_add(count_halfwidth_sound_marks(self))
}
}
}Two details are worth taking. First, the unit is u16 cells and nothing else — Ratatui pays no cost for a unit it cannot use, which is exactly the trade Slint declines by making its length an associated type. Second, the answer is deliberately not unicode-width's: halfwidth katakana dakuten/handakuten (U+FF9E/U+FF9F) are zero-width by the Unicode property and one cell in practice, so count_halfwidth_sound_marks adds them back. unicode-width is pinned >=0.2.0 with a comment pointing at issue #1271 (Cargo.toml) — the width table is a compatibility hazard, tracked as such.
IMPORTANT
Ratatui carries two width functions and they can disagree. Buffer::set_stringn consumes width via CellWidth::cell_width (with the dakuten adjustment), while Line::width and Span::width return raw UnicodeWidthStr::width (text/line.rs:441) — so the layout-facing width of a line containing U+FF9E is one less than the number of cells writing it actually consumes. Even in a system that has exactly one unit, having exactly one width oracle turns out to be the harder half.
Finally, the per-cell override: CellDiffOption::ForcedWidth(NonZeroU16) (buffer/cell.rs) lets a caller declare a width the symbol's text does not imply, documented for escape-sequence payloads whose "computed width does not match what is written to the screen". Measurement is a default, not a law.
Q2 — is the contract stated in one place?
Yes, and it is the strongest answer in the survey so far. Backend is one trait declaration; the required set is what has no body. There is no __traits(compiles) probing, no optional-method discovery, and no capability enum: implement the trait or you are not a backend.
Optionality is expressed three ways, each visible in the declaration:
| Mechanism | Example | What it means |
|---|---|---|
| Default body | append_lines → Ok(()) | silently degrades to nothing |
| Cargo feature on the trait | scroll_region_up / scroll_region_down under scrolling-regions | the method set itself changes at compile time |
Self::Error | any method | "this backend cannot do that" is a runtime Err, not a lie |
The associated type Error is the underrated one. Because the seam is generic over its error type, a backend refuses rather than degrades — which is the "refusable degrade" half of F5, obtained for free from the type rather than from a NODEGRADE flag.
The feature-gated methods are the interesting failure mode: enabling scrolling-regions anywhere in the dependency graph adds two required methods to the trait, so a third-party backend that compiled yesterday stops compiling. A capability expressed as a cargo feature is global, not per-backend.
WARNING
One documented capability does not match its declaration. clear_region's doc comment says "This method is optional and may not be implemented by all backends. The default implementation calls clear if the clear_type is ClearType::All and returns an error otherwise" — but the declaration at backend.rs:302 has no body, so it is required. Even the survey's cleanest contract drifted from its own prose.
Q3 — semantic operations, or primitives?
Neither: the seam has no operations. A backend is never told a scrollbar, a border or a text input was intended, because by the time cells reach it there is nothing left to know. Scrollbar is a StatefulWidget that resolves its own geometry and writes strings (ratatui-widgets/src/scrollbar.rs:504):
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// ...
let areas = area.columns().flat_map(Rect::rows);
let bar_symbols = self.bar_symbols(area, state);
for (area, bar) in areas.zip(bar_symbols) {
if let Some((symbol, style)) = bar {
buf.set_string(area.x, area.y, symbol, style);
}
}
}Degradation that sparkles:ui puts in the backend, Ratatui puts in the widget's configuration: symbols::scrollbar::Set is a four-field record (track, thumb, begin, end) with named constants VERTICAL, DOUBLE_VERTICAL, DOUBLE_HORIZONTAL, … (symbols/scrollbar.rs). Choosing the glyph vocabulary is the application's decision, made once, above everything.
This is the direct falsification of the premise behind friction §3 — scrollbar as a widget concept in the drawing seam. Our Scrollbar payload carries the rail's inputs (content extent, viewport extent, offset, an edge, a rail-expansion percentage) beside two fallback glyphs a pixel backend never reads, so that each backend resolves the thumb at its own resolution — interp/immediate.d applying paintScrollbarCells for a canvas that declines the primitive, RaylibCanvas resolving continuously for one that does not. Ratatui resolves it once, in the widget, and ships cells. The reason we cannot simply copy that is Q5 — but the inputs are the price of the semantics, and trackGlyph/thumbGlyph are the price of one target's answer riding past every backend that will never read it.
Because the intermediate is a readable grid, widgets can also compose in a way a command stream forbids. MergeStrategy (symbols/merge.rs) merges box-drawing symbols against what is already in the cell:
assert_eq!(MergeStrategy::Replace.merge("│", "━"), "━");
assert_eq!(MergeStrategy::Exact.merge("│", "─"), "┼");
assert_eq!(MergeStrategy::Fuzzy.merge("┘", "╔"), "╬");Two adjacent Blocks collapse their shared border into ┼ because the second widget can read the first widget's output. Read-back is a capability of the grid model, not of the drawing model.
Q4 — command shape
There is no command. This is the survey's cleanest alternative to DrawOp, and it is not egui's sum type either: the reified thing is a cell array.
pub struct Buffer {
pub area: Rect,
pub content: Vec<Cell>, // len == area.width * area.height
}Cell is 5–6 live fields — symbol: Option<CompactString>, fg, bg, optional underline_color, modifier, diff_option (buffer/cell.rs) — and every one is live for every cell. There is no tag, so there are no dead fields and no arms. DrawOp answers the illegal-combination problem the way sparkles.input.events does — a closed sum, one arm per kind, dispatched by op.match!(…) — and pays for the answer twice: in the eight arms every walker and every accessor must spell out, and in a uniform operation width, since static assert(DrawOp.sizeof <= 64) budgets every op at the widest payload's size. A grid pays neither, because it has nothing to discriminate.
The properties RecordingCanvas exists to give us come along free, and better:
- Comparable.
BufferderivesPartialEq/Hash;Buffer::with_lines(["hello"])builds a fixture from string literals (buffer.rs:92). - Diffable. Comparison is not just possible, it is the rendering algorithm.
- Compositable.
Buffer::mergeunions two buffers' areas and overlays content. - Canonical for tests. The
TestBackenddocs go out of their way to point past themselves (backend/test.rs): "it is preferable to write unit tests for widgets directly against the buffer rather than using this backend."
That last line is the finding for us. Ratatui has our RecordingCanvas — a conforming in-memory backend used by the integration tests — and its own documentation says the better assertion target is the intermediate, one level up. An op stream records what a widget did; a grid records what the user sees, and only the second is stable under a refactor that reorders drawing.
The cost is equally clear. A grid is O(area) whether or not anything changed, cannot represent anything sub-cell, cannot represent overlap except by destruction, and cannot carry a payload larger than a cell. That is not a defect: it is what makes the model exactly the size of its target. A seam that must also reach a GPU cannot reify a grid, which is why this answer does not transfer wholesale.
Q5 — sub-unit placement
Ratatui has our constraint — integer cells, no unit below one — and answers it entirely above the seam, in the widget that wants the resolution.
symbols::Marker (symbols/marker.rs) is a fidelity ladder as an enum: Dot, Block, Bar, Braille (2×4), HalfBlock (1×2), Quadrant (2×2), Sextant (2×3), Octant (2×4), Custom(char). The Canvas widget maps a marker to a Grid implementation whose resolution() is measured in dots rather than cells (ratatui-widgets/src/canvas.rs):
fn marker_to_grid(width: u16, height: u16, marker: Marker) -> Box<dyn Grid> {
match marker {
Marker::Braille => Box::new(PatternGrid::<2, 4>::new(width, height, &BRAILLE)),
Marker::HalfBlock => Box::new(HalfBlockGrid::new(width, height)),
Marker::Quadrant => Box::new(PatternGrid::<2, 2>::new(width, height, &QUADRANTS)),
Marker::Sextant => Box::new(PatternGrid::<2, 3>::new(width, height, &SEXTANTS)),
Marker::Octant => Box::new(PatternGrid::<2, 4>::new(width, height, &OCTANTS)),
// ...
}
}The sub-cell grid is resolved down to graphemes before a single Cell is written, so the Backend never learns that a plot had 2×4 resolution.
Compare Notcurses, which puts the same ladder in the seam as a blitter. Ratatui and Notcurses agree on the first of the three mechanisms F6 puts in RuleEdge's place — name a fidelity, not a position — and disagree only on where the name lives. The deciding factor is visible in each design: Notcurses's blitter must be in the seam because different terminals support different blitters, so the decision is a property of the device. Ratatui's marker is a property of the chart, so it lives with the chart. RuleEdge is neither — it is a position, chosen by the widget, resolved by the backend, and that split is the friction.
Q6 — resolved or semantic styling
A Cell carries one appearance, not two. There is no semantic role beside the resolved colours; the widget resolves the style and the cell holds the result. Six of our eight payloads store a Slot beside the resolved appearance their primitive paints from — an Ink for the four content operations, its own colour fields for a fill.
But the resolved vocabulary is itself deliberately unresolved at the bottom. Color (style/color.rs) is Reset, the sixteen ANSI names, Indexed(u8) and Rgb(u8, u8, u8) — so Color::Red is not a colour, it is a reference into the terminal's palette, and the final resolution happens in the terminal emulator, past every backend. Color::Reset is the same trick for "the user's default". Ratatui pays for one style channel and still gets user-theme re-resolution, because it inherited a vocabulary the device already speaks.
Style differs from Cell: it uses Option<Color> plus add_modifier / sub_modifier so styles can be patched (style.rs:239), then applied into the cell's non-optional fields. Partial styling is a composition-time concept that does not survive to the seam.
This weakens the premise of our friction §6 — a resolved appearance and a semantic role on every drawing op — and it is the second of the cheaper encodings F9 names: carrying the role beside the resolved colour is not the only way to serve a re-resolving backend. The alternative is a resolved vocabulary whose leaves are already symbolic — which is precisely what Slot would be if a Visual could name a palette entry instead of only an RgbColor. It also fixes which of our two channels is the redundant one: a role plus a theme yields the resolved colour, and no arrangement of resolved colours yields the role.
Q7 — payload ownership
The grid owns everything, by value. Cell::symbol is a Option<CompactString> — a small-string type with inline storage, chosen so a one-grapheme symbol never heap-allocates (PR #601, cited in the field's doc comment). Buffer is Clone, Eq, Hash and, under the serde feature, Serialize/Deserialize. Nothing in the seam borrows from the widget that drew it.
The only borrow in the whole pipeline is the diff iterator's &'a Cell, and its lifetime is provably the buffer's: Terminal::flush borrows both buffers, calls Backend::draw, and returns before swap_buffers resets anything. A backend that wants to retain a symbol clones a CompactString.
F8 tabulates eight ownership mechanisms across the survey and finds not one subject that borrows a payload across a frame; this is the cheapest of the eight — copy, because the payload is bounded — and it works only because a cell's payload is one grapheme cluster. A run of text is not bounded, which is why CmdBuffer.textRun copies into a frame arena rather than into the operation: the bytes belong to the toolkit, not to the widget that drew them, and the rule stated on the type is that an operation is valid while the buffer that built it is alive and unreset. What friction §7 records is the step Ratatui never has to take. The rule is stated on the type, which makes it enforceable rather than advisory, but the type system cannot express it — a private launder cast is what carries the slice past dip1000 — and the retain boundary an offset pair settles for free stays open as UI-O4.
For payloads that genuinely cannot fit in a cell — sixel/Kitty images, OSC 8 links — Ratatui does not extend the seam. It adds a negative directive:
pub enum CellDiffOption {
None,
Skip, // "something else owns this cell; never write it"
AlwaysUpdate, // "another renderer may draw over this; always rewrite it"
ForcedWidth(NonZeroU16),
}Skip is documented for cells "covered by something from an escape sequence, such as graphics or links"; AlwaysUpdate for when "another renderer may draw over the same area, such as an external image pipeline" (buffer/cell.rs). Rather than teaching the seam about images, the seam learned to cede territory. That is a genuinely transferable idea for a toolkit that will meet the same problem.
Q8 — extent query
Answered three times over, at three different layers, which is why this is the least interesting question for Ratatui and the most instructive about F7's three questions.
| Layer | Query | Meaning |
|---|---|---|
| The device | Backend::size() -> Size | the terminal's cells |
| The device, richer | Backend::window_size() -> WindowSize | cells and pixels |
| The scene | Buffer::area() -> &Rect | the grid's own extent, always exact |
| The viewport | Frame::area() -> Rect | "guaranteed not to change during rendering" |
The self-describing case is free: a Buffer is its extent, because content.len() must equal area.width * area.height. An offscreen consumer builds a buffer at the size it wants, or uses Buffer::with_lines, which derives width from the longest line and height from the count (buffer.rs:92) — the exact "size a surface to content" operation skia-canvas-render.d hand-rolls by scanning ops, because nothing on CmdBuffer, the display list or the arena reports a built stream's extent.
WindowSize is worth a second look for the terminal↔GPU question. It reports both units from one TIOCGWINSZ, with the pixel field documented as possibly 0,0 because terminals do not have to fill it in. A seam that spans units can carry both and mark one as best-effort; it does not have to pick.
Strengths
- The contract is the trait. Ten methods, no probing, no capability enum, no side-channel. A backend author reads one declaration.
- One content method. All of "drawing" is
draw(impl Iterator<Item = (u16, u16, &Cell)>); the other nine are terminal state. Writing a fifth backend is a weekend. - The intermediate is a value. Comparable, hashable, clonable, serialisable, mergeable, and constructible from string literals — every property a test harness wants, without a recorder type existing for that purpose.
- Diffing is the render algorithm, not an optimisation bolted on: correctness and minimal output are the same code path.
type Errormakes refusal typed without a negotiation protocol.- The seam cedes territory rather than growing.
CellDiffOption::Skiplets a foreign renderer own cells the toolkit will never touch.
Weaknesses
- Structurally single-target. A
Vec<Cell>cannot express a hairline, a glyph at a fractional advance, or overlapping translucent content. Nothing here scales to a GPU surface; the design is excellent because it refused to. - Two width oracles.
CellWidth::cell_widthandUnicodeWidthStr::widthdisagree on halfwidth dakuten, and layout uses the second while writing uses the first (text/line.rs,buffer/cell_width.rs). - Terminal physics leak into the diff.
BufferDiffneedsTrailingState, aforceflag and aVISIBLE_ON_BLANKmodifier set to model what terminals do to the trailing column of a wide glyph. The grid model is not quite the device model, and the gap is paid for in the diff (buffer/diff.rs). - Feature-gated trait methods are global.
scrolling-regionschanges the required method set for every backend in the graph. clear_region's documented default does not exist (backend.rs:302).O(area)per frame regardless of change, and a full second buffer ofCells in memory.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Reify a grid, not a command stream | The target is a grid; the intermediate can be exactly as expressive as the device, and no more | Nothing sub-cell, nothing overlapping, nothing non-terminal is representable |
Backend::draw takes an iterator of changed cells | Diffing is the algorithm; the backend never sees an unchanged cell | The backend cannot batch by style or region — it gets cells in row order and must peephole |
| Widgets write cells; no semantic ops at the seam | Degradation is chosen once, in widget config (symbols::scrollbar::Set), where the app can override it | A backend can never do better than the widget's glyph choice; no backend-specific rendering of a known widget |
Sub-cell fidelity is a widget option (Marker) | Resolution is a property of the chart, not of the device, for the terminals Ratatui targets | A terminal with pixel protocols cannot upgrade a Braille plot; the widget already collapsed it |
Cell owns its symbol (CompactString, inline) | Bounded payload ⇒ copying is cheap and every lifetime question disappears | Only viable because a payload is one grapheme; a text run or an image needs a different answer |
Foreign content handled by Skip/AlwaysUpdate, not new ops | Images and hyperlinks are other renderers' territory; the seam only has to stop overwriting it | The toolkit has no idea what is there — no measurement, no layout participation, no z-order |
Colour stays symbolic at the leaf (Reset, ANSI, Indexed) | The device already resolves palettes against the user's theme; re-resolution is free | Toolkit-side effects (blending, contrast checks) are impossible on a named colour |
type Error on the trait | A backend refuses in its own error type instead of degrading silently | Every call site is a Result; Terminal's generic parameter propagates the error type everywhere |
Capability by cargo feature (scrolling-regions) | Zero runtime cost, checked by the compiler | Global: enabling it anywhere adds required methods to every backend in the graph |
Bearing on the proposal
Reifying a grid is a live alternative to reifying commands — for the terminal half. F3 keeps the reification and leaves its encoding a live trade: a closed sum eliminates illegal combinations and keeps operations comparable, variable-stride per-op records pay only for what each op uses. Ratatui sits outside both: reify the result, not the instructions. It yields everything
RecordingCanvasyields, plus diffing and read-back composition, and it is whatsparkles:ui-tuialready produces one layer down. It does not scale to Skia — which sharpens the umbrella's open question: if the terminal arm's natural seam is a grid and the GPU arm's is a command stream, the single-seam premise is what is under pressure, notDrawOp's encoding.The assertion target should be the frame, not the op stream. Ratatui ships a
RecordingCanvasequivalent and its own docs recommend asserting against theBufferinstead (backend/test.rs). An op stream is unstable under drawing-order refactors that leave the result identical; a grid is not. That is F12's conclusion from the other end: the op stream is the parity oracle, not the golden.Friction §3's derived-geometry fields are the cost of deferring, not of semantics. Ratatui derives the rail once in the widget and configures degradation with a four-glyph
Set.scrollbarThumbcomputes that geometry once insparkles.ui.state, andcanvas.dre-exports the lowerings built on it —scrollbarCellCount,scrollbarCell,ruleEndpoints(canvas.d) — so the derivation a backend re-runs from the op's fields is already published as a helper it could call instead. With F4, the resolution is: keep a semantic op where a backend can genuinely do better (a real box shadow), resolve to primitives where it cannot.Adopt a "cede territory" directive before adding an image op.
CellDiffOption::Skip/AlwaysUpdatesolve foreign-renderer coexistence — sixel, Kitty graphics, OSC 8 — without the seam learning what an image is.sparkles:uimeets this withsparkles:terminal-viewpanes and image protocols, and it is cheaper than an op kind.type Errorbeats a capability query for refusal. F5 asks for a refusable degrade; Ratatui gets it from the return type, at every call site, unforgettably.Sharpens F1 rather than dissenting from it. F1 places Ratatui in the free-function camp, and the detail worth taking is that the free function sits behind no abstraction at all:
cell_widthis one unit overstr, non-negotiable, with a per-cell escape hatch (ForcedWidth). For a single-unit toolkit aTextShapertrait is over-engineering. The transferable rule is narrower than placement alone: measurement must not be a backend method — whether it must be an abstraction depends on whether the units genuinely differ.One unit is not one oracle — the point F2 draws from this subject. Ratatui has exactly one unit and still ships two width functions that disagree on
U+FF9E/U+FF9F(text/line.rsvsbuffer/cell_width.rs). Whatever we do about friction §1, layout and painting must call the same function —cellsOfdoes that accidentally; a shaping-aware redesign must do it deliberately.Confirms F7 with the cheapest maintained-at-construction case in the survey. Extent is answered by the device (
Backend::size,window_size) and by the scene (Buffer::area), two of F7's three questions, and nothing is scanned to get either: a grid is its extent, becausecontent.len()must equalarea.width * area.height.buildDisplayListhands back aDrawOp[]without the number layout already computed, so a caller that needs painted bounds foldsop.rectback out of the stream.
Sources
All paths verified to exist at a2ca2df5688772baffb743b494761f4ec82b3174 via git cat-file -e; the local clone matches origin/main at that revision.
- The seam:
ratatui-core/src/backend.rs(theBackendtrait,ClearType,WindowSize) andbackend/test.rs(TestBackend) - The intermediate:
buffer/buffer.rs,buffer/cell.rs(Cell,CellDiffOption),buffer/cell_width.rs,buffer/diff.rs(BufferDiff) - The pipeline:
terminal/buffers.rs(flush,swap_buffers),terminal/render.rs,terminal/frame.rs,widgets/widget.rs - Vocabulary:
symbols/marker.rs,symbols/merge.rs,symbols/scrollbar.rs,style/color.rs,style.rs,text/line.rs - Consumers:
ratatui-crossterm/src/lib.rs,ratatui-widgets/src/scrollbar.rs,ratatui-widgets/src/canvas.rs - Context:
ARCHITECTURE.md,Cargo.toml,ratatui-core/src/lib.rs,LICENSE, docs.rs/ratatui, ratatui.rs