| Crates.io | r3bl_tui |
| lib.rs | r3bl_tui |
| version | 0.7.8 |
| created_at | 2022-10-05 02:26:12.834296+00 |
| updated_at | 2026-01-23 22:04:48.630056+00 |
| description | TUI library to build modern apps inspired by React, Elm, with Flexbox, CSS, editor component, emoji support, and more |
| homepage | https://r3bl.com |
| repository | https://github.com/r3bl-org/r3bl-open-core/tree/main/tui |
| max_upload_size | |
| id | 680265 |
| size | 7,219,511 |
R3BL TUI library allows you to create apps to enhance developer productivity.
Please read the main
README.md of the
r3bl-open-core monorepo and workspace to get a better understanding of the context
in which this crate is meant to exist.
PixelCharRendererCliTextInline: Styled Text FragmentsOutputDevice: Thread-Safe Terminal
OutputYou can build fully async TUI (text user interface) apps with a modern API that brings the best of the web frontend development ideas to TUI apps written in Rust:
And since this is using Rust and Tokio you get the advantages of concurrency and parallelism built-in. No blocking the main thread for user input, async middleware, or rendering.
This framework is loosely coupled and strongly coherent meaning that you can pick and choose whatever pieces you would like to use without having the cognitive load of having to grok all the things in the codebase. Its more like a collection of mostly independent modules that work well with each other, but know very little about each other.
This is the main crate that contains the core functionality for building TUI apps. It allows you to build apps that range from "full" TUI to "partial" TUI, and everything in the middle.
Here are some videos that you can watch to get a better understanding of TTY programming.
Here are some highlights of this library:
tokio::mpsc channel and signals.direct_to_ansi backends). The direct_to_ansi backend is part of this
R3BL TUI crate and is the default on Linux, with no reliance on Crossterm at all.
We plan to roll this out to macOS and Windows.This crate allows you to build apps that range from "full" TUI to "partial" TUI, and everything in the middle. Here are some videos that you can watch to get a better understanding of TTY programming.
[mod@readline_async::choose_api] allows you to build less interactive apps that ask
a user user to make choices from a list of options and then use a decision tree to
perform actions.
An example of this is this "Partial TUI" app giti in the
r3bl-cmdr crate. You
can install & run this with the following command:
cargo install r3bl-cmdr
giti
[mod@readline_async::readline_async_api] gives you the ability to easily ask for
user input in a line editor. You can customize the prompt, and other behaviors, like
input history.
Using this, you can build your own async shell programs using "async readline & stdout". Use advanced features like showing indeterminate progress spinners, and even write to stdout in an async manner, without clobbering the prompt / async readline, or the spinner. When the spinner is active, it pauses output to stdout, and resumes it when the spinner is stopped.
An example of this is this "Partial TUI" app giti in the
r3bl-cmdr crate. You
can install & run this with the following command:
cargo install r3bl-cmdr
giti
Here are other examples of this:
The bulk of this document is about this. [mod@tui::terminal_window_api] gives
you "raw mode", "alternate screen" and "full screen" support, while being totally
async. An example of this is the "Full TUI" app edi in the
r3bl-cmdr crate. You
can install & run this with the following command:
cargo install r3bl-cmdr
edi
You can mix and match "Full TUI" with "Partial TUI" to build for whatever use case you
need. r3bl_tui allows you to create application state that can be moved between
various "applets", where each "applet" can be "Full TUI" or "Partial TUI".
Please check out the changelog to see how the library has evolved over time.
To learn how we built this crate, please take a look at the following resources.
Once you've cloned the repo to a folder on your computer, follow these steps:
๐ The easiest way to get started is to use the bootstrap script:
./bootstrap.sh
fish run.fish install-cargo-tools
This script above automatically installs:
For complete development setup and all available commands, see the repository README.
After setup, you can run the examples interactively from the repository root:
# Run examples interactively (choose from list)
fish run.fish run-examples
# Run examples with release optimizations
fish run.fish run-examples --release
# Run examples without logging
fish run.fish run-examples --no-log
You can also run examples directly:
cd tui/examples
cargo run --release --example demo -- --no-log
These examples cover the entire surface area of the TUI API. The unified
run.fish script at
the repository root provides all development commands for the entire workspace.
For TUI library development, use these commands from the repository root:
# Terminal 1: Monitor logs from examples
fish run.fish log
# Terminal 2: Run examples interactively
fish run.fish run-examples
| Command | Description |
|---|---|
fish run.fish run-examples |
Run TUI examples interactively with options |
fish run.fish run-examples-flamegraph-svg |
Generate SVG flamegraph for performance analysis |
fish run.fish run-examples-flamegraph-fold |
Generate perf-folded format for analysis |
fish run.fish bench |
Run benchmarks with real-time output |
fish run.fish log |
Monitor log files with smart detection |
| Command | Description |
|---|---|
fish run.fish test |
Run all tests |
fish run.fish watch-all-tests |
Watch files, run all tests |
fish run.fish watch-one-test <pattern> |
Watch files, run specific test |
fish run.fish clippy |
Run clippy with fixes |
fish run.fish watch-clippy |
Watch files, run clippy |
fish run.fish docs |
Generate documentation |
The TUI library includes comprehensive VT100/ANSI escape sequence conformance tests that validate the terminal emulation pipeline:
# Run all VT100 ANSI conformance tests
cargo test vt_100_pty_output_conformance_tests
# Run specific conformance test categories
cargo test test_real_world_scenarios # vim, emacs, tmux patterns
cargo test test_cursor_operations # cursor positioning & movement
cargo test test_sgr_and_character_sets # text styling & colors
Testing Architecture Features:
CsiSequence], [EscSequence], and
[SgrCode] builders instead of hardcoded escape stringsThe conformance tests ensure the ANSI parser correctly processes sequences from real terminal applications and maintains compatibility with VT100 specifications.
The markdown parser includes a comprehensive conformance test suite with organized test data that validates parsing correctness across diverse markdown content:
# Run all markdown parser tests
cargo test md_parser
# Run specific test categories
cargo test parser_snapshot_tests # Snapshot testing for parser output
cargo test parser_bench_tests # Performance benchmarks
cargo test conformance_test_data # Conformance test data validation
Testing Infrastructure Features:
Test Data Categories:
The conformance tests ensure the parser correctly handles both standard markdown syntax and R3BL extensions while maintaining performance and reliability.
The TUI library features production-grade integration testing using pseudo-terminals (PTYs) that simulate real interactive terminal applications. Unlike traditional unit tests, these tests spawn the test binary itself in a PTY slave process and send raw byte sequences through the PTY masterโexactly like a real terminal emulator would.
This is how we achieve "next level" testing:
Traditional Unit Tests PTY Integration Tests (Ours)
โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Mock objects Real PTY pair (master/slave)
Synthetic input Raw byte sequences (like real apps)
Isolated functions Full interactive child process
No terminal state Raw mode enabled (fully interactive)
Limited realism Production-equivalent environment
Why PTY Testing is a Superpower:
Implementation powered by generate_pty_test! macro:
The generate_pty_test! macro handles PTY infrastructure automatically:
Example test structure:
generate_pty_test! {
test_fn: interactive_input_parsing,
slave: || {
// Runs in PTY slave - fully interactive terminal
enable_raw_mode();
let input_device = InputDevice::new();
process_terminal_events(&input_device);
std::process::exit(0);
},
master: |pty_pair, child| {
// Runs in PTY master - sends input, verifies output
let mut writer = pty_pair.controller().take_writer();
writer.write_all(b"\x1b[A").unwrap(); // Send Up Arrow
let output = read_pty_output(&pty_pair);
assert!(output.contains("UpArrow event received"));
child.wait().unwrap();
}
}
The macro takes three parameters:
test_fn: Name of the generated test functionslave: Closure that runs in the PTY slave process (interactive terminal)master: Closure that runs in the PTY master process (sends input, verifies output)For a complete working example, see the test_pty_input_device module which
demonstrates:
DirectToAnsiInputDeviceReal-world applications:
integration_tests validates VT-100 input sequencesraw_mode_integration_tests tests termios configurationFor complete PTY test implementation details and examples, see:
generate_pty_test!integration_testsraw_mode_integration_testsFor complete development setup and all available commands, see the repository README.
./run.fish run-examples-flamegraph-fold --benchmarkex_editor input sequence that stress tests the rendering pipeline.perf-folded files are comparable across commitsinotifywait (Linux) or fswatch (macOS)The project includes an AI-powered performance regression detection system that uses flamegraph analysis to detect performance changes:
How it works:
Baseline capture: A performance baseline
(flamegraph-benchmark-baseline.perf-folded) is committed to git, representing the
"current best" performance state
Reproducible benchmarks: The --benchmark flag uses expect to script input,
ensuring identical workloads across runs for apples-to-apples comparisons
Automated analysis: Claude Code's analyze-performance skill compares current
flamegraphs against baseline, identifying:
Commands:
# Generate reproducible benchmark data
./run.fish run-examples-flamegraph-fold --benchmark
# Analyze with Claude Code (detects regressions, suggests optimizations)
# Use the /check-regression command or invoke the analyze-performance skill
Workflow:
Make code change
โ
Run: ./run.fish run-examples-flamegraph-fold --benchmark
โ
Analyze: Compare flamegraph-benchmark.perf-folded vs baseline
โ
โโ Performance improved?
โ โโ YES โ Update baseline, commit
โ โโ NO โ Investigate regressions, optimize
โโ Repeat
This enables continuous performance monitoring โ regressions are caught before they reach production, and optimizations are quantified with real data.

Here's a video of a prototype of R3BL CMDR app built using this TUI engine.

The R3BL TUI engine uses a comprehensive type-safe bounds checking system that eliminates off-by-one errors and prevents mixing incompatible index types (like comparing row positions with column widths) at compile time.
Off-by-one errors and index confusion have plagued programming since its inception. UI and layout development (web, mobile, desktop, GUI, TUI) amplifies these challenges with multiple sources of confusion:
[min, max] vs exclusive [start, end) vs
position+size [start, start+width) - different use cases demand different
semantics// โ Unsafe: raw integers hide these distinctions
let cursor_row: usize = 5; // Is this 0-based or 1-based?
let viewport_width: usize = 80; // Is this a size or position?
let buffer_size: usize = 100; // Can I use this as an index?
let buffer: Vec<u8> = vec![0; 100];
// Problem 1: Dimension confusion
if cursor_row < viewport_width { /* Mixing row index with column size! */ }
// Problem 2: 0-based vs 1-based confusion
if buffer_size > 0 {
let last = buffer[buffer_size]; /* Off-by-one: size is 1-based! PANICS! */
}
// Problem 3: Range boundary confusion
let scroll_region_start = 2_usize;
let scroll_region_end = 5_usize;
// Is this [2, 5] inclusive or [2, 5) exclusive?
// VT-100 uses inclusive, but iteration needs exclusive!
for row in scroll_region_start..scroll_region_end {
// Processes rows 2, 3, 4 (exclusive end)
// But VT-100 scroll region 2..=5 includes row 5!
// Easy to create off-by-one errors when converting
}
Use strongly-typed indices and lengths with semantic validation:
use r3bl_tui::{row, height, ArrayBoundsCheck, ArrayOverflowResult};
let cursor_row = row(5); // RowIndex (0-based position)
let viewport_height = height(24); // RowHeight (1-based size)
// โ
Type-safe: Compiler prevents row/column confusion
if cursor_row.overflows(viewport_height) == ArrayOverflowResult::Within {
// Safe to access buffer[cursor_row]
}
RowIndex] with [ColWidth]The system uses a two-tier trait architecture:
IndexOps], [LengthOps]) that work
with any index/length typeArrayBoundsCheck],
[CursorBoundsCheck], [ViewportBoundsCheck], [RangeBoundsExt],
[RangeConvertExt])Array/buffer access (strict bounds):
use r3bl_tui::{col, width, ArrayBoundsCheck, ArrayOverflowResult};
let index = col(5);
let buffer_width = width(10);
// Check before accessing
if index.overflows(buffer_width) == ArrayOverflowResult::Within {
let ch = buffer[index.as_usize()]; // Safe access
}
Text cursor positioning (allows end-of-line):
use r3bl_tui::{col, width, CursorBoundsCheck, CursorPositionBoundsStatus};
let cursor_col = col(10);
let line_width = width(10);
// Cursor can be placed after last character (position == length)
match line_width.check_cursor_position_bounds(cursor_col) {
CursorPositionBoundsStatus::AtEnd => { /* Valid: cursor after last char */ }
CursorPositionBoundsStatus::Within => { /* Valid: cursor on character */ }
CursorPositionBoundsStatus::Beyond => { /* Invalid: out of bounds */ }
_ => {}
}
Viewport visibility (rendering optimization):
use r3bl_tui::{row, height, ViewportBoundsCheck, RangeBoundsResult};
let content_row = row(15);
let viewport_start = row(10);
let viewport_size = height(20);
// Check if content is visible before rendering
if content_row.check_viewport_bounds(viewport_start, viewport_size) == RangeBoundsResult::Within {
// Render this row
}
Range boundary handling (inclusive vs exclusive):
use r3bl_tui::{row, RangeConvertExt};
// VT-100 scroll region: inclusive bounds [2, 5] means rows 2,3,4,5
let scroll_region = row(2)..=row(5);
// Convert to exclusive for Rust iteration: [2, 6) means rows 2,3,4,5
let iter_range = scroll_region.to_exclusive(); // row(2)..row(6)
// Now safe to use for iteration - no off-by-one errors!
// for row in iter_range { /* process rows 2,3,4,5 */ }
For comprehensive documentation including:
See the extensive and detailed bounds_check module
documentation.
The R3BL TUI engine provides comprehensive Unicode support through grapheme cluster handling, ensuring correct text manipulation regardless of character complexity.
Unicode text contains characters that may:
๐จ๐พโ๐คโ๐จ๐ฟ is 5 codepoints combined)This creates a fundamental mismatch between:
Traditional string indexing fails with such text:
// โ Unsafe: byte indexing can split multi-byte characters
let text = "Hello ๐๐ฝ"; // Wave emoji with skin tone modifier
let byte_len = text.len(); // 14 bytes (not 7 characters!)
let _substring = &text[0..7]; // PANICS! Splits ๐ emoji mid-character
The grapheme system uses three distinct index types to handle text correctly:
[ByteIndex] - Memory position (UTF-8 byte offset)
[SegIndex] - Logical position (grapheme cluster index)
[ColIndex] - Display position (terminal column)
String: "H๐!"
ByteIndex: 0 1 2 3 4 5
Content: [H][๐----][!]
SegIndex: 0 1 2
Segments: [H] [๐] [!]
ColIndex: 0 1 2 3
Display: [H][๐--] [!]
Use [GCStringOwned] for grapheme-aware string operations:
use r3bl_tui::*;
let text = GCStringOwned::new("Hello ๐๐ฝ");
let grapheme_count = text.len(); // 7 grapheme clusters
let display_width = text.display_width; // Actual terminal columns needed
// Safe conversions between index types
// ByteIndex โ SegIndex: find which character contains a byte
// ColIndex โ SegIndex: find which character is at a column
// SegIndex โ ColIndex: find the display column of a character
Grapheme cluster awareness: Correctly handles composed characters
๐๐ฝ (wave + skin tone)๐จ๐พโ๐คโ๐จ๐ฟ (5 codepoints, 1 user-perceived character)รฉ (may be 1 or 2 codepoints)Display width calculation: Accurately computes terminal column width
Safe slicing: Substring operations never split multi-byte characters
Option<SegIndex>] for invalid indicesByteIndex] in the middle of a character โ NoneIterator support: Iterate over graphemes, not bytes or codepoints
For comprehensive documentation including:
GCStringOwned]See the extensive and detailed graphemes module
documentation documentation.
The current render pipeline flow is:
RenderOpIRVecRenderOpIRVec โ Rendered to [OffscreenBuffer] ([PixelChar] grid)OffscreenBuffer] โ Diffed with previous buffer โ Generate diff chunksRenderOpOutputVec for paintingRenderOpOutputVec execution โ Each op routed through crossterm backendโญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ โ
โ main.rs โ
โ โญโโโโโโโโโโโโโโโโโโโฎ โ
โ GlobalData โโโโโโโโโโโโ>โ window size โ โ
โ HasFocus โ offscreen buffer โ โ
โ ComponentRegistryMap โ state โ โ
โ App & Component(s) โ channel sender โ โ
โ โฐโโโโโโโโโโโโโโโโโโโฏ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
request_shutdown this TUI
app, it will return your terminal to where you'd left off.main_event_loop] is where many global structs live which are shared across
the lifetime of your app. These include the following:
HasFocus]ComponentRegistryMap]GlobalData] which contains the following
App::app_init]),
and is responsible for creating all the Components that it uses, and saving them
to the [ComponentRegistryMap].
App::app_render],
[App::app_handle_input_event], and [App::app_handle_signal] can be called at a
later time.App::app_render] method is responsible for creating the layout by using
Surface and [FlexBox] to arrange whatever Component's are in the
[ComponentRegistryMap].App::app_handle_input_event] method is responsible for handling events that
are sent to the App trait when user input is detected from the keyboard or mouse.
Similarly the [App::app_handle_signal] deals with signals that are sent from
background threads (Tokio tasks) to the main thread, which then get routed to the
App trait object. Typically this will then get routed to the Component that
currently has focus.Versions of this crate <= 0.3.10 used shared memory to communicate between the
background threads and the main thread. This was done using the async Arc<RwLock<T>>
from tokio. The state storage, mutation, subscription (on change handlers) were all
managed by the
r3bl_redux
crate. The use of the Redux pattern, inspired by React, brought with it a lot of
overhead both mentally and in terms of performance (since state changes needed to be
cloned every time a change was made, and memcpy or clone is expensive).
Versions > 0.3.10 use message passing to communicate between the background threads
using the tokio::mpsc channel (also async). This is a much easier and more
performant model given the nature of the engine and the use cases it has to handle. It
also has the benefit of providing an easy way to attach protocol servers in the future
over various transport layers (eg: TCP, IPC, etc.); these protocol servers can be used
to manage a connection between a process running the engine, and other processes
running on the same host or on other hosts, in order to handle use cases like
synchronizing rendered output, or state.
Here are some papers outlining the differences between message passing and shared memory for communication between threads.
Dependency injection is used to inject the
required resources into the main_event_loop function. This allows for easy testing
and for modularity and extensibility in the codebase. The r3bl_terminal_async crate
shares the same infrastructure for input and output devices. In fact the
[crate::InputDevice] and [crate::OutputDevice] structs are in the r3bl_core
crate.
stdin
and stdout while preserving all the existing code and functionality. This can
produce some interesting headless apps in the future, where the UI might be
delegated to a window using eGUI or
iced-rs or wgpu.There is a clear separation of concerns in this library. To illustrate what goes where, and how things work let's look at an example that puts the main event loop front and center & deals with how the system handles an input event (key press or mouse).
cargo run).โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โIn band input event โ
โ โ
โ Input โโ> [TerminalWindow] โ
โ Event โซ โ โ
โ โ โฉ [ComponentRegistryMap] stores โ
โ โ [App]โโโโโโโโโโโโโโ> [Component]s at 1st render โ
โ โ โ โ
โ โ โ โ
โ โ โ โญโโโโโโ> id=1 has focus โ
โ โ โ โ โ
โ โ โโโ> [Component] id=1 โโโโโโฎ โ
โ โ โ โ โ
โ โ โฐโโ> [Component] id=2 โ โ
โ โ โ โ
โ default handler โ โ
โ โซ โ โ
โ โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โOut of band app signal โ
โ โ
โ App โ
โ Signal โโ> [App] โ
โ โซ โ
โ โ โ
โ โฐโโโโโโ> Update state โ
โ main thread rerender โ
โ โซ โ
โ โ โ
โ โฐโโโโโ>[App] โ
โ โซ โ
โ โฐโโโโ> [Component]s โ
โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ
Let's trace the journey through the diagram when an input even is generated by the
user (eg: a key press, or mouse event). When the app is started via cargo run it
sets up a main loop, and lays out all the 3 components, sizes, positions, and then
paints them. Then it asynchronously listens for input events (no threads are blocked).
When the user types something, this input is processed by the main loop of
[TerminalWindow].
FlexBox] with id=1 currently has focus.TerminalWindow] looks at the event.TerminalWindow]. Further, the specificity of the Component that
currently has focus is the highest. In other words, the input event gets routed by
the App to the Component that currently has focus (Component id=1 in our
example).TerminalWindow]'s default
handler. If the default handler doesn't process it, then it is simply ignored.An input event is processed by the main thread in the main event loop. This is a synchronous operation and thus it is safe to mutate state directly in this code path. This is why there is no sophisticated locking in place. You can mutate the state directly in
App::app_handle_input_event]Component::handle_event]This is great for input events which are generated by the user using their keyboard or
mouse. These are all considered "in-band" events or signals, which have no delay or
asynchronous behavior. But what about "out of band" signals or events, which do have
unknown delays and asynchronous behaviors? These are important to handle as well. For
example, if you want to make an HTTP request, you don't want to block the main thread.
In these cases you can use a tokio::mpsc channel to send a signal from a background
thread to the main thread. This is how you can handle "out of band" events or signals.
To provide support for these "out of band" events or signals, the App trait has a
method called [App::app_handle_signal]. This is where you can handle signals that
are sent from background threads. One of the arguments to this associated function is
a signal. This signal needs to contain all the data that is needed for a state
mutation to occur on the main thread. So the background thread has the responsibility
of doing some work (eg: making an HTTP request), getting some information as a result,
and then packaging that information into a signal and sending it to the main thread.
The main thread then handles this signal by calling the [App::app_handle_signal]
method. This method can then mutate the state of the App and return an
[EventPropagation] enum indicating whether the main thread should repaint the UI or
not.
So far we have covered what happens when the App receives a signal. Who sends this
signal? Who actually creates the tokio::spawn task that sends this signal? This can
happen anywhere in the App and Component. Any code that has access to
[GlobalData] can use the [crate::send_signal!] macro to send a signal in a
background task. However, only the App can receive the signal and do something with
it, which is usually apply the signal to update the state and then tell the main
thread to repaint the UI.
Now that we have seen this whirlwind overview of the life of an input event, let's look at the details in each of the sections below.
The main building blocks of a TUI app are:
TerminalWindow] - You can think of this as the main "window" of the app. All the
content of your app is painted inside of this "window". And the "window"
conceptually maps to the screen that is contained inside your terminal emulator
program (eg: tilix, Terminal.app, etc). Your TUI app will end up taking up 100% of
the screen space of this terminal emulator. It will also enter raw mode, and paint
to an alternate screen buffer, leaving your original scroll back buffer and history
intact. When you request_shutdown this TUI app, it will return your terminal to
where you'd left off. You don't write this code, this is something that you use.TerminalWindow] to bootstrap your TUI app. You can just use App to build your
app, if it is a simple one & you don't really need any sophisticated layout or
styling. But if you want layout and styling, now we have to deal with [FlexBox],
Component, and [crate::TuiStyle].Inside of your App if you want to use flexbox like layout and CSS like styling you can think of composing your code in the following way:
RenderOpIR]s into
a [RenderPipeline]. This is kind of like virtual DOM in React. This queue of
commands is collected from all the components and ultimately painted to the screen,
for each render! Your app's state is mutable and is stored in the [GlobalData]
struct. You can handle out of band events as well using the signal mechanism.Typically your App will look like this:
#[derive(Default)]
pub struct AppMain {
// Might have some app data here as well.
// Or `_phantom: std::marker::PhantomData<(State, AppSignal)>,`
}
As we look at Component & App more closely we will find a curious thing
[ComponentRegistry] (that is managed by the App). The reason this exists is for
input event routing. The input events are routed to the [Component] that currently
has focus.
The [HasFocus] struct takes care of this. This provides 2 things:
id of a [FlexBox] / [Component] that has focus.crate::Pos] for each id. This is used to
represent a cursor (whatever that means to your app & component). This cursor is
maintained for each id. This allows a separate cursor for each Component that
has focus. This is needed to build apps like editors and viewers that maintains a
cursor position between focus switches.Another thing to keep in mind is that the App and [TerminalWindow] is persistent
between re-renders.
[TerminalWindow] gives App first dibs when it comes to handling input events.
[ComponentRegistry::route_event_to_focused_component] can be used to route events
directly to components that have focus. If it punts handling this event, it will be
handled by the default input event handler. And if nothing there matches this event,
then it is simply dropped.
The R3BL TUI engine provides two complementary rendering architectures optimized for
different use cases. Both leverage a high-performance [PixelChar] concept which
represents a single "pixel" in the terminal screen at a given col and row index
position. There are only as many [PixelChar]s as there are rows and cols in a
terminal screen, and the index maps directly to the position of the pixel in the
terminal screen.
The R3BL TUI engine supports two distinct rendering approaches, each optimized for different use cases and complexity levels:
RenderOpIRVec โ [OffscreenBuffer] โ (diff) โ
RenderOpOutputVec โ [PixelChar] array โ [PixelCharRenderer] โ ANSI bytes โ
Terminalchoose()]), form inputsCliTextInline] โ [PixelChar] array โ [PixelCharRenderer] โ ANSI
bytes โ TerminalPixelCharRenderer]Both rendering paths ultimately need to convert styled text into ANSI escape
sequences. The [PixelCharRenderer] handles this conversion in a unified way across
both paths:
PixelChar] (array of styled characters)This enables:
RenderOpOutputVec execution โ [PixelCharRenderer] โ bytesCliTextInline] โ [PixelChar] โ [PixelCharRenderer] โ bytesCliTextInline]: Styled Text FragmentsFor direct rendering paths, [CliTextInline] represents a fragment of text with
styling information:
When converted to a string (via the [FastStringify] trait), it automatically:
PixelChar] arrayPixelCharRenderer] to generate ANSI bytesThis hidden conversion enables ergonomic styling in interactive components without requiring explicit knowledge of the underlying rendering machinery.
OutputDevice]: Thread-Safe Terminal OutputInteractive components (Path 2) use [OutputDevice] for coordinated terminal output:
std::io::Stdout]This allows multiple components to safely write to the terminal without race conditions or interleaved output.
Here is an example of what a single row of rendered output might look like in a row of
the [OffscreenBuffer]. This diagram shows each [PixelChar] in row_index: 1 of
the [OffscreenBuffer]. In this example, there are 80 columns in the terminal screen.
This actual log output generated by the TUI engine when logging is enabled.
row_index: 1
000 S โโโโโโโโณโโโโโโโโ001 P 'j'โfgโbg 002 P 'a'โfgโbg 003 P 'l'โfgโbg 004 P 'd'โfgโbg 005 P 'k'โfgโbg
006 P 'f'โfgโbg 007 P 'j'โfgโbg 008 P 'a'โfgโbg 009 P 'l'โfgโbg 010 P 'd'โfgโbg 011 P 'k'โfgโbg
012 P 'f'โfgโbg 013 P 'j'โfgโbg 014 P 'a'โfgโbg 015 P 'โ'โrev 016 S โโโโโโโโณโโโโโโโโ017 S โโโโโโโโณโโโโโโโโ
018 S โโโโโโโโณโโโโโโโโ019 S โโโโโโโโณโโโโโโโโ020 S โโโโโโโโณโโโโโโโโ021 S โโโโโโโโณโโโโโโโโ022 S โโโโโโโโณโโโโโโโโ023 S โโโโโโโโณโโโโโโโโ
024 S โโโโโโโโณโโโโโโโโ025 S โโโโโโโโณโโโโโโโโ026 S โโโโโโโโณโโโโโโโโ027 S โโโโโโโโณโโโโโโโโ028 S โโโโโโโโณโโโโโโโโ029 S โโโโโโโโณโโโโโโโโ
030 S โโโโโโโโณโโโโโโโโ031 S โโโโโโโโณโโโโโโโโ032 S โโโโโโโโณโโโโโโโโ033 S โโโโโโโโณโโโโโโโโ034 S โโโโโโโโณโโโโโโโโ035 S โโโโโโโโณโโโโโโโโ
036 S โโโโโโโโณโโโโโโโโ037 S โโโโโโโโณโโโโโโโโ038 S โโโโโโโโณโโโโโโโโ039 S โโโโโโโโณโโโโโโโโ040 S โโโโโโโโณโโโโโโโโ041 S โโโโโโโโณโโโโโโโโ
042 S โโโโโโโโณโโโโโโโโ043 S โโโโโโโโณโโโโโโโโ044 S โโโโโโโโณโโโโโโโโ045 S โโโโโโโโณโโโโโโโโ046 S โโโโโโโโณโโโโโโโโ047 S โโโโโโโโณโโโโโโโโ
048 S โโโโโโโโณโโโโโโโโ049 S โโโโโโโโณโโโโโโโโ050 S โโโโโโโโณโโโโโโโโ051 S โโโโโโโโณโโโโโโโโ052 S โโโโโโโโณโโโโโโโโ053 S โโโโโโโโณโโโโโโโโ
054 S โโโโโโโโณโโโโโโโโ055 S โโโโโโโโณโโโโโโโโ056 S โโโโโโโโณโโโโโโโโ057 S โโโโโโโโณโโโโโโโโ058 S โโโโโโโโณโโโโโโโโ059 S โโโโโโโโณโโโโโโโโ
060 S โโโโโโโโณโโโโโโโโ061 S โโโโโโโโณโโโโโโโโ062 S โโโโโโโโณโโโโโโโโ063 S โโโโโโโโณโโโโโโโโ064 S โโโโโโโโณโโโโโโโโ065 S โโโโโโโโณโโโโโโโโ
066 S โโโโโโโโณโโโโโโโโ067 S โโโโโโโโณโโโโโโโโ068 S โโโโโโโโณโโโโโโโโ069 S โโโโโโโโณโโโโโโโโ070 S โโโโโโโโณโโโโโโโโ071 S โโโโโโโโณโโโโโโโโ
072 S โโโโโโโโณโโโโโโโโ073 S โโโโโโโโณโโโโโโโโ074 S โโโโโโโโณโโโโโโโโ075 S โโโโโโโโณโโโโโโโโ076 S โโโโโโโโณโโโโโโโโ077 S โโโโโโโโณโโโโโโโโ
078 S โโโโโโโโณโโโโโโโโ079 S โโโโโโโโณโโโโโโโโ080 S โโโโโโโโณโโโโโโโโspacer [ 0, 16-80 ]
When RenderOpIRVec are executed and used to create an [OffscreenBuffer] that
maps to the size of the terminal window, clipping is performed automatically. This
means that it isn't possible to move the caret outside of the bounds of the viewport
(terminal window size). And it isn't possible to paint text that is larger than the
size of the offscreen buffer. The buffer really represents the current state of the
viewport. Scrolling has to be handled by the component itself (an example of this is
the editor component).
Each [PixelChar] can be one of 4 things:
PixelChar::PlainText and is used to paint the screen via the diffing algorithm
which is smart enough to "stack" styles that appear beside each other for quicker
rendering in terminals.Here's a detailed overview of the complete rendering pipeline architecture used for complex, full-screen TUI applications (Path 1). This pipeline efficiently allows for rendering terminal UIs with minimal redraws by leveraging an offscreen buffer and diffing mechanism, along with algorithms to remove needless output and control commands being sent to the terminal as output.
App
โ
Component
โ
RenderOpIRVec
โ
RenderPipeline โ OffscreenBuffer
โ
RenderOpOutputVec
โ
Terminal
This is very much like a compiler pipeline with multiple stages.
RenderOpIRVec
(intermediate representation) which is output.RenderOpOutputVec (where redundant
operations have been removed).direct_to_ansi, crossterm, etc.) and the optimizations
are applied in a backend agnostic way.The R3BL TUI rendering system for Path 1 is organized into 6 distinct stages, each with a clear responsibility:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STAGE 1: Application/Component Layer (App Code) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Generates: RenderOpIRVec with built-in clipping info โ
โ Module: render_op - Contains type definitions โ
โ โ
โ Components produce draw commands describing *what* to render and *where*. โ
โ Each operation carries clipping information to ensure safe rendering. โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STAGE 2: Render Pipeline Collection (Organization Layer) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Collects RenderOpIRVec into organized structures by ZOrder โ
โ Module: render_pipeline โ
โ โ
โ The pipeline aggregates render operations from multiple components and โ
โ organizes them by Z-order (layer depth). This ensures correct visual stacking โ
โ when components overlap. No rendering happens yetโjust organization. โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STAGE 3: Compositor (Rendering to Offscreen Buffer) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Processes RenderOpIRVec โ writes to OffscreenBuffer โ
โ Module: compositor_render_ops_to_ofs_buf โ
โ โ
โ The Compositor is the rendering engine. It: โ
โ - Executes RenderOpIRVec operations sequentially โ
โ - Applies clipping and Unicode/emoji width handling โ
โ - Writes rendered PixelChars to an offscreen buffer โ
โ - Manages cursor position and color state โ
โ - Acts as an intermediate "virtual terminal" โ
โ โ
โ Output: A complete 2D grid (OffscreenBuffer) representing the rendered frame. โ
โ This buffer can be analyzed to determine what changed since the last frame. โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STAGE 4: Backend Converter (Diff & Optimization Layer) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Scans OffscreenBuffer โ generates RenderOpOutputVec โ
โ Module: crossterm_backend/offscreen_buffer_paint_impl โ
โ (Backend-specific implementation of OffscreenBufferPaint trait) โ
โ โ
โ The Backend Converter: โ
โ - Compares current OffscreenBuffer with previous frame (optional) โ
โ - Generates only the operations needed for selective redraw โ
โ - Converts PixelChar grid into optimized text painting operations โ
โ - Produces RenderOpOutputVec (no clipping neededโalready handled) โ
โ - Eliminates redundant operations for performance โ
โ โ
โ Input: OffscreenBuffer (what we rendered) โ
โ Output: RenderOpOutputVec (optimized operations to display it) โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STAGE 5: Backend Executor (Terminal Output Layer) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Executes RenderOpOutputVec via backend library (Crossterm/DirectToAnsi) โ
โ Module: crossterm_backend/paint_render_op_impl โ
โ (Backend-specific trait: PaintRenderOp) โ
โ โ
โ The Backend Executor: โ
โ - Translates RenderOpOutputVec to terminal escape sequences โ
โ - Manages raw mode, cursor visibility, colors, mouse events โ
โ - Handles terminal-specific optimizations (e.g., state tracking) โ
โ - Sends commands to Crossterm/DirectToAnsi for actual terminal manipulation โ
โ - Flushes output to ensure immediate display โ
โ โ
โ Uses: RenderOpsLocalData to avoid redundant state changes โ
โ (e.g., don't resend "set color to red" if already red) โ
โโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ STAGE 6: Terminal Output (User Visible) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Rendered content displayed in the terminal โ
โ โ
โ The final result: User sees the rendered UI with correct colors, text, โ
โ and cursor position, updated efficiently without full redraws. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key Design Benefits:
RenderOpIR] and RenderOpOutput enums ensure operations are
used in the correct contextdirect_to_ansi, etc.)The following diagram provides a high level overview of how apps (that contain components, which may contain components, and so on) are rendered to the terminal screen using the composed component pipeline (Path 1).
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Container โ
โ โ
โ โญโโโโโโโโโโโโโโฎ โญโโโโโโโโโโโโโโฎ โ
โ โ Col 1 โ โ Col 2 โ โ
โ โ โ โ โ โ
โ โ โ โ โโโโโโโโโผโโผโโโโโฉ RenderPipeline โโโโโโฎ
โ โ โ โ โ โ โ
โ โ โ โ โ โ โ
โ โ โโโโโโโโผโโโผโโโโโโโโโโโโโโผโโผโโโโโฉ RenderPipeline โโฎ โ
โ โ โ โ โ โ โ โ
โ โ โ โ โ โ โฉ โ โฉ
โ โ โ โ โ โ โญโโโโโโโโโโโโโโโโโโโโโโฎ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ โ โ
โ โ โ OffscreenBuffer โ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฏ โ โ
โฐโโโโโโโโโโโโโโโโโโโโโโฏ
Each component produces a [RenderPipeline], which is a map of [ZOrder] and
RenderOpIRVec. [RenderOpIR] are the instructions that are grouped together, such
as move the caret to a position, set a color, and paint some text.
Inside of each RenderOpIRVec the caret is stateful, meaning that the caret
position is remembered after each [RenderOpIR] is executed. However, once a new
RenderOpIRVec is executed, the caret position reset just for that
RenderOpIRVec. Caret position is not stored globally. You should read more about
"atomic paint operations" in the [RenderOpIR] documentation.
Once a set of these [RenderPipeline]s have been generated, typically after the user
enters some input event, and that produces a new state which then has to be rendered,
they are combined and painted into an [OffscreenBuffer].
The paint module contains the paint() function, which is the entry point for
all rendering in the composed component pipeline (Path 1). Once the first render
occurs, the [OffscreenBuffer] that is generated is saved to [GlobalData]. The
following table shows the various tasks that have to be performed in order to render
to an [OffscreenBuffer]. There is a different code path that is taken for ANSI text
and plain text (which includes [TuiStyledText] which is just plain text with a
color). Syntax highlighted text is also just [TuiStyledText].
| UTF-8 | Task |
|---|---|
| Y | convert [RenderPipeline] to List<List<[PixelChar]>> ([OffscreenBuffer]) |
| Y | paint each [PixelChar] in List<List<[PixelChar]>> to stdout using OffscreenBufferPaintImplCrossterm |
| Y | save the List<List<[PixelChar]>> to [GlobalData] |
Currently crossterm and direct_to_ansi are supported for actually painting to
the terminal. But this process is really simple making it very easy to swap out other
terminal libraries or even a GUI backend, or some other custom output driver.
Since the [OffscreenBuffer] is cached in [GlobalData], a diff can be performed for
subsequent renders. And only those diff chunks are painted to the screen. This ensures
that there is no flicker when the content of the screen changes. It also minimizes the
amount of work that the terminal or terminal emulator has to do in order to render the
[PixelChar]s on the screen. This diff-based optimization is what gives Path 1 its
high performance characteristics compared to Path 2.
R3BL TUI supports multiple terminal backends to balance cross-platform compatibility with platform-specific optimizations.
The backend is selected at compile time via the TERMINAL_LIB_BACKEND constant:
| Platform | Default Backend | Why |
|---|---|---|
| Linux | DirectToAnsi |
Pure Rust async I/O, ~18% better performance |
| macOS/Windows | Crossterm |
Mature cross-platform support |
Crossterm is a cross-platform terminal manipulation library. It provides:
direct_to_ansi backend (Linux-native)direct_to_ansi is a pure-Rust ANSI sequence generator that bypasses external
terminal libraries. It provides:
mio for async stdin polling (macOS kqueue
doesn't support PTY/tty polling)Performance benefits (measured on Linux with 8-second workload, 999Hz sampling):
SmallVec[16] for render operations (+0.47%)When to choose each:
direct_to_ansi: When targeting Linux and want maximum performanceBoth backends plug into Stage 5 of the 6-stage rendering pipeline:
Stages 1-4 (Shared) Stage 5 (Backend-Specific)
โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Component โ RenderPipeline โ Crossterm (cross-platform)
โ Compositor OR
โ OffscreenBuffer โ DirectToAnsi (Linux-native)
โ RenderOpOutput
The shared stages (1-4) produce RenderOpOutput operations. Stage 5 backends
translate these operations into terminal-specific commands. This architecture ensures
consistent behavior across backends while allowing platform-specific optimizations.
Functional equivalence: Both backends are verified to produce identical results
through comprehensive PTY-based compatibility tests. The backend_compat_tests
module spawns controlled processes in real PTYs and compares:
This ensures you can switch backends without changing application behavior โ only performance characteristics differ.
For backend implementation details, see:
terminal_lib_backends - Pipeline architecturedirect_to_ansi - Linux backendcrossterm_backend - Cross-platform backendThe RRT pattern provides generic infrastructure for managing dedicated worker threads
that block on I/O operations. This powers the direct_to_ansi backend's
mio_poller.
Async executors (like Tokio) use thread pools that shouldn't block. Terminal input requires blocking on stdin, which would starve other async tasks. RRT solves this by dedicating a thread to blocking I/O.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ RESILIENT REACTOR THREAD โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ
โ Worker Thread Async Consumers โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ mio::Poll โ โ broadcast โ โโโโโบ โ SubscriberGuard A โ โ
โ โ โ โ channel โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ (blocks โ โโโโโบ โ โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โ on I/O) โevents โ (clones to โ โโโโโบ โ SubscriberGuard B โ โ
โ โ โ โ all) โ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโ โ
โ โฒ โโโโโบ โ SubscriberGuard C โ โ
โ โ โโโโโโโโโโโฌโโโโโโโโโโโ โ
โ โ โ โ
โ โโโโโโโโโโโโโโโ wake() on drop โโโโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
For the type hierarchy and implementation details, see the Architecture Overview in
resilient_reactor_thread.
| Component | Purpose |
|---|---|
ThreadSafeGlobalState |
Thread-safe singleton for RRT instances |
ThreadLiveness |
Running state + generation tracking |
SubscriberGuard |
RAII guard managing subscription lifecycle |
ThreadWorker |
Trait for the blocking work loop |
ThreadWaker |
Trait for interrupting blocked threads |
For comprehensive documentation including I/O backend compatibility, io_uring
support, and implementation examples, see resilient_reactor_thread.
The TUI engine includes comprehensive VT100/ANSI escape sequence parsing for both terminal input (keyboard, mouse events) and terminal output (PTY child processes).
The vt_100_terminal_input_parser module converts raw terminal bytes into
structured input events:
Raw stdin bytes
โ
โ try_parse_input_event()
โผ
VT100InputEventIR (intermediate representation)
โ
โ convert_input_event()
โผ
InputEvent (keyboard, mouse, terminal events)
Supported input types:
| Module | What It Parses |
|---|---|
keyboard |
Arrow keys, function keys, modifiers (Shift/Ctrl/Alt) |
mouse |
SGR, X10, RXVT protocols; clicks, drags, scroll, motion |
terminal_events |
Window resize, focus gained/lost, bracketed paste |
utf8 |
UTF-8 text between ANSI sequences |
Design principle: The parser is IO-free โ it processes byte slices without any I/O operations, making it easy to test and reuse across different backends.
The vt_100_pty_output_parser module processes ANSI sequences from PTY child
processes (like bash, vim, etc.) and updates the terminal display state:
pty_mux (receives child process output)
โ
โผ
OffscreenBuffer::apply_ansi_bytes()
โ
โ Uses VTE state machine
โผ
AnsiToOfsBufPerformer (updates buffer state)
โ
โผ
OffscreenBuffer (cursor, text, styles)
This enables the terminal multiplexer to correctly render output from any VT100- compatible program running in a PTY.
[OffscreenBuffer] can function as a standalone in-memory terminal emulator. By
calling OffscreenBuffer::apply_ansi_bytes(), you can feed raw VT100 ANSI escape
sequences directly into the buffer โ no real terminal or PTY required:
let mut buffer = OffscreenBuffer::new(Size { col_count: 80, row_count: 24 });
// Feed ANSI bytes from any source (file, network, PTY, test data)
buffer.apply_ansi_bytes(b"\x1b[31mRed text\x1b[0m Normal text");
// Buffer now contains a pixel-perfect snapshot of what a real terminal would show
// - Cursor position tracked
// - Text styles (colors, bold, etc.) applied
// - Screen state (scrolling, clearing) handled
Use cases:
How r3bl_tui uses this for testing:
The backend_compat_tests use in-memory terminal emulation to verify that
crossterm and direct_to_ansi backends produce identical output. Tests spawn
controlled processes in real PTYs, capture their ANSI output, apply it to
[OffscreenBuffer]s, and compare the resulting screen state โ all without needing to
visually inspect terminal output.
This is the same mechanism that powers [PTYMux] โ each managed process gets its own
[OffscreenBuffer] that continuously receives and renders ANSI output, enabling
instant switching between processes with fully preserved screen state.
observe_terminal validation test captures real terminal sequences for
ground-truth verificationFor implementation details:
vt_100_terminal_input_parser - Input parsingvt_100_pty_output_parser - Output parsingRaw mode is essential for TUI applications โ it disables terminal line buffering and echo so the application can read individual keystrokes and escape sequences.
| Aspect | Cooked Mode (default) | Raw Mode |
|---|---|---|
| Input buffering | Line-buffered (waits for Enter) | Immediate byte-by-byte |
| Special characters | Interpreted (Ctrl+C sends SIGINT) |
Pass through as bytes |
| Echo | Typed characters appear on screen | No automatic echo |
| Use case | Normal terminal interaction | TUI apps, escape sequence parsing |
Linux/macOS (via rustix):
Uses Rust's rustix crate for type-safe termios
manipulation:
// rustix provides safe, ergonomic termios API
termios.make_raw(); // Equivalent to cfmakeraw()
termios::tcsetattr(&fd, OptionalActions::Now, &termios)?;
Why rustix over libc?
make_raw() encapsulate complex flag manipulationmacOS/Windows (via Crossterm):
Falls back to Crossterm's raw mode implementation for cross-platform compatibility.
The recommended approach uses RAII for automatic cleanup:
use r3bl_tui::RawModeGuard;
{
let _guard = RawModeGuard::new()?;
// Terminal is now in raw mode
// ... process input ...
} // Raw mode automatically disabled when guard drops
Raw mode settings are stored statically and restored on disable. The implementation handles:
/dev/ttyRawModeGuard ensures restoration even on panicenable_raw_mode() multiple timesFor implementation details and historical context (TTY, line discipline, stty):
terminal_raw_mode - Main documentationraw_mode_unix - Linux/macOS implTesting TUI applications is challenging because they interact with terminal I/O in complex ways. The PTY testing infrastructure provides controlled environments for accurate end-to-end testing.
Traditional unit tests can't verify:
PTY tests solve this by creating real pseudo-terminals where tests act as both the "terminal emulator" (controller) and the "application" (controlled).
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Test Function (entry point) โ
โ - Macro detects role via environment variable โ
โ - Routes to controller or controlled function โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ โ
Controller Path Controlled Path
โ โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโ
โ Macro: PTY Setup โ โ Controlled Function โ
โ - Creates PTY pair โ โ - Enable raw mode (if needed) โ
โ - Spawns controlled โโโโโโถ - Execute test logic โ
โ - Passes to controller โ โ - Output via stdout/stderr โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโ โโโโโโโโโโโโโโฒโโฌโโโโโโโโโโโโโโโโโ
โ โ โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโ โ โ
โ Controller Function โ โ โ PTY I/O
โ - Receives pty_pair โ โ โ stdin, stdout/stderr
โ - Receives child handle โ โ โ
โ - Writes input to child (opt) โโโโโโโโโโโโ โ
โ - Reads results from child โโโโโโโโโโโโโโ
โ - Verifies assertions โ
โ - Waits for child exit โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
generate_pty_test! macroUse this macro for single-feature PTY tests:
generate_pty_test! {
test_fn: test_raw_mode_enables_correctly,
controller: my_controller_function,
controlled: my_controlled_function
}
The macro handles:
Controller (runs in test process):
PtyPair and ControlledChildControlled (runs in spawned child):
std::process::exit(0) when done| Scenario | Tool |
|---|---|
| Testing a single feature in PTY environment | generate_pty_test! macro |
| Comparing two backends produce identical results | spawn_controlled_in_pty() |
| One test, one controlled process | generate_pty_test! macro |
| One test, multiple controlled processes | spawn_controlled_in_pty() |
# Run a specific PTY test
cargo test -p r3bl_tui test_pty_keyboard_modifiers -- --nocapture
# Run all PTY-based integration tests
cargo test -p r3bl_tui integration_tests -- --nocapture
Note: PTY tests run with --nocapture to see debug output from both controller
and controlled processes.
For complete implementations, see:
pty_test_fixtures - Test infrastructureintegration_tests - Input parsing testsbackend_compat_tests - Backend comparison testsThe [EditorComponent] struct can hold data in its own memory, in addition to relying
on the state.
EditorEngine] which holds syntax highlighting information, and
configuration options for the editor (such as multiline mode enabled or not, syntax
highlighting enabled or not, etc.). Note that this information lives outside of the
state.Component<S, AS>] trait.EditorBuffer]) and not inside of
the [EditorComponent] itself.
HasEditorBuffers] which is where
the document data is stored (the key is the id of the flex box in which the editor
component is placed).EditorBuffer] contains the text content in a [ZeroCopyGapBuffer]. This
provides efficient, zero-copy access to editor content. It also contains the
scroll offset, caret position, and file extension for syntax highlighting.In other words,
EditorEngine] -> This goes in [EditorComponent]
EditorBuffer] -> This goes in the State
Here are the connection points with the impl of [Component<S, AS>] in
[EditorComponent]:
Component::handle_event() - Relays input events to
EditorEngine::apply_event(), which processes the event with the current
[EditorBuffer] and returns an updated buffer. The result can be dispatched to the
store via an action.Component::render() - Relays rendering arguments to
EditorEngine::render_engine(), which takes the current [EditorBuffer] state
and generates a [RenderPipeline] for display.The editor uses a [ZeroCopyGapBuffer] for text storage, delivering exceptional
performance through careful memory management and zero-copy access patterns.
Zero-copy access: Read operations return &str slices directly into the buffer
without allocation or copying:
ZeroCopyGapBuffer::as_str() access: 0.19 ns (essentially free)ZeroCopyGapBuffer::get_line_content(): 0.37 ns (direct pointer return)Efficient Unicode handling: All text operations are grapheme-cluster aware:
Scalable line management: Dynamic growth with predictable performance:
Each line is stored as a null-padded byte array:
Line: [H][e][l][l][o][\\n][\\0][\\0]...[\\0] // 256 bytes
This enables:
&str access for syntax highlighting and renderingThe implementation uses a "validate once, trust thereafter" approach:
&str type guarantees UTF-8 at API boundariesunsafe { from_utf8_unchecked() } in hot paths for maximum
performanceThis provides both safety (through type system guarantees) and performance (zero validation overhead in production).
End-of-line append operations are detected and optimized:
This makes typing at the end of lines (the most common editing pattern) extremely fast.
For comprehensive implementation details including:
See the detailed and extensive zero_copy_gap_buffer module documentation.
The TUI includes a high-performance markdown parser built with nom that supports
both standard markdown syntax and R3BL-specific extensions.
Standard markdown support:
R3BL extensions for enhanced document metadata:
@title: <text> - Document title metadata@tags: <tag1>, <tag2> - Tag lists for categorization@authors: <name1>, <name2> - Author attribution@date: <date> - Publication dateSmart lists - Multi-line list items with automatic indentation:
- This is a list item that spans
multiple lines and maintains proper
indentation automatically
- Nested items work correctly
The parser uses a priority-based composition strategy where more specific parsers are attempted first:
parse_markdown() {
many0(
parse_title_value() โ MdBlock::Title
parse_tags_list() โ MdBlock::Tags
parse_authors_list() โ MdBlock::Authors
parse_date_value() โ MdBlock::Date
parse_heading() โ MdBlock::Heading
parse_smart_list_block() โ MdBlock::SmartList
parse_fenced_code_block() โ MdBlock::CodeBlock
parse_block_text() โ MdBlock::Text (catch-all)
)
}
Within each block, inline fragments are parsed with similar priority:
**text**), italic (_text_), inline code (`code`)), links ([text](url))[ ], [x])The parser works seamlessly with the editor's syntax highlighting through several key functions:
try_parse_and_highlight] - Main entry point for parsing and syntax highlightingparse_markdown()] - Core parser that produces the [MdDocument] ASTparse_smart_list] - Specialized parser for multi-line list handlingsyntect via
render_engine() for
syntax highlightingRenderPipeline]The parser was chosen after extensive benchmarking against alternatives (including
markdown-rs):
nom
(tutorial) for
efficient memory usager3bl_tuiFor comprehensive implementation details including:
See:
parse_markdown()] function entry pointmd_parser module documentationThe [PTYMux] module provides tmux-like functionality with universal
compatibility for all programs: TUI applications, interactive shells, and
command-line tools.
Per-process virtual terminals: Each process maintains its own [OffscreenBuffer]
that acts as a complete virtual terminal, enabling:
Universal program support:
r3bl_tui app)Advanced features:
โญโโโโโโโโโโโโโโฎ โญโโโโโโโโโโโฎ โญโโโโโโโโโโโโโฎ โญโโโโโโโโโโโโโโโโโโฎ
โ Child Proc โโโโโโบ PTY โโโโโโบ VTE Parser โโโโโโบ OffscreenBuffer โ
โ (vim, bash) โ โ (bytes) โ โ (ANSI) โ โ (virtual โ
โฐโโโโโฒโโโโโโโโโฏ โฐโโโโโโโโโโโฏ โฐโโโโโโโโโโโโโฏ โ terminal) โ
โ โ โฐโโโโโโโโโโโโโโโโโโฏ
โ โ โ
โ โโโโโโโโโโผโโโโโโโ โ
โ โ Perform Trait โ โ
โ โ Implementationโ โ
โ โโโโโโโโโโโโโโโโโ โ
โ โ
โ โญโโโโโโโโโโโโโโโโโฎ โ
โ โ RenderPipeline โโโโโโโโโโโโฏ
โฐโโโโโโโโโโโโโโโโโโโโโโโโโโโโ paint() โ
โฐโโโโโโโโโโโโโโโโโฏ
The parser provides comprehensive VT100 compliance using the vte crate (same as
Alacritty):
Supported sequences:
Three-layer architecture for maintainability:
Layer 1: SHIM โ Protocol delegation (vt_100_shim_char_ops)
Layer 2: IMPLEMENTATION โ Business logic (vt_100_impl_char_ops)
Layer 3: TESTS โ Conformance validation (vt_100_test_char_ops)
This naming convention enables predictable IDE navigation: searching for
char_ops shows you the shim, implementation, and tests all together.
VT100 specification compliance:
Intentionally unimplemented legacy features: Custom tab stops (HTS, TBC), legacy line control (NEL), and legacy terminal modes (IRM, DECOM) are not implemented as they're primarily used by mainframe terminals and very old applications.
use r3bl_tui::core::{pty_mux::{PTYMux, Process}, get_size};
#[tokio::main]
async fn main() -> miette::Result<()> {
let terminal_size = get_size()?;
let processes = vec![
Process::new("bash", "bash", vec![], terminal_size),
Process::new("editor", "nvim", vec![], terminal_size),
Process::new("monitor", "htop", vec![], terminal_size),
];
let multiplexer = PTYMux::builder()
.processes(processes)
.build()?;
multiplexer.run().await?; // F1/F2/F3 to switch, Ctrl+Q to quit
Ok(())
}
For comprehensive implementation details including:
See the detailed pty_mux module documentation and vt_100_pty_output_parser
documentation.
Definitions:
Caret - the block that is visually displayed in a terminal which represents the insertion point for whatever is in focus. While only one insertion point is editable for the local user, there may be multiple of them, in which case there has to be a way to distinguish a local caret from a remote one (this can be done with bg color).
Cursor - the global "thing" provided in terminals that shows by blinking usually where the cursor is. This cursor is moved around and then paint operations are performed on various different areas in a terminal window to paint the output of render operations.
There are two ways of showing cursors which are quite different (each with very different constraints).
Using a global terminal cursor (we don't use this).
MoveTo(col, row), SetColor, PaintText(...) sequence).Paint the character at the cursor with the colors inverted (or some other bg color) giving the visual effect of a cursor.
A modal dialog box is different than a normal reusable component. This is because:
ZOrder::Glass], and outside of any layouts using [FlexBox]es).So this activation trigger must be done at the App trait impl level (in the
app_handle_event() method). Also, when this trigger is detected it has to:
EventPropagation::ConsumedRender] which will re-render the UI with the dialog box
on top.There is a question about where does the response from the user (once a dialog is
shown) go? This seems as though it would be different in nature from an
[EditorComponent] but it is the same. Here's why:
EditorComponent] is always updating its buffer based on user input, and
there's no "handler" for when the user performs some action on the editor. The
editor needs to save all the changes to the buffer to the state. This requires the
trait bound [HasEditorBuffers] to be implemented by the state.HasDialogBuffers] trait bound. This will hold
stale data once the dialog is dismissed or accepted, but that's ok since the title
and text should always be set before it is shown.
ComponentRegistry::user_data. And it is possible for handle_event() to return
a [EventPropagation::ConsumedRender] to make sure that changes are re-rendered.
This approach may have other issues related to having both immutable and mutable
borrows at the same time to some portion of the component registry if one is not
careful.When creating a new dialog box component, two callback functions are passed in:
DialogComponentData::on_dialog_press_handler] - this will be called if the user
choose no, or yes (with their typed text).DialogComponentData::on_dialog_editor_change_handler] - this will be called if
the user types something into the editor.So far we have covered the use case for a simple modal dialog box. The dialog system
also supports async autocomplete capabilities through the
[DialogEngineConfigOptions] struct, which allows configuring the dialog in
autocomplete mode.
In autocomplete mode, you can provide an async autocomplete provider that performs long-running operations such as:
The autocomplete mode displays an extra "results panel" and uses a different layout (top of screen instead of centered). The same callback functions are used, but the provider can now perform async operations to populate the results.
An implementation of lolcat color wheel is provided. Here's an example.
use r3bl_tui::*;
let mut lolcat = LolcatBuilder::new()
.set_color_change_speed(ColorChangeSpeed::Rapid)
.set_seed(1.0)
.set_seed_delta(1.0)
.build();
let content = "Hello, world!";
let content_gcs = GCStringOwned::new(content);
let lolcat_mut = &mut lolcat;
let st = lolcat_mut.colorize_to_styled_texts(&content_gcs);
lolcat.next_color();
This [crate::Lolcat] that is returned by build() is safe to re-use.
render()
function of this component will produce the same generated colors over and over
again.crate::Lolcat] instance.Please report any issues to the issue tracker. And if you have any feature requests, feel free to add them there too ๐.
License: Apache-2.0