A Rust port of Textual, a reactive TUI framework for building rich terminal applications with widgets, CSS styling, layout, and event-driven architecture.
Built on rich-rs for terminal rendering primitives and crossterm for terminal I/O.
Attribution. textual-rs is a derivative work: a Rust port of Textual, created by Will McGugan and the Textualize team. All credit for the original framework design, API, and concepts goes to them. This project exists only because of their work and aims to bring that experience to Rust. Published on crates.io as the
textualcrate.
[dependencies]
textual = "1.0.0-dev"- 56 widgets: buttons, inputs, text areas, data tables, trees, tabs, markdown viewer, select, checkboxes, progress bars, overlays, and more
- 108 CSS properties: type/id/class/pseudo-class selectors, descendant/child combinators, nested
&rules, cascade with specificity and!important, theme tokens - Full layout engine: vertical, horizontal, grid, dock, and absolute positioning with box model (margin, border, padding), scrolling, min/max constraints, fractional/percentage/viewport units
- Event system: capture/bubble phases, focus management, keyboard bindings with action maps, mouse hit-testing, message bus
- Reactive runtime: Tokio-based event loop, reactive state with watchers, workers for background tasks, CSS transitions with easing functions
- Hot-reloadable stylesheets: external
.tcssfiles withApp::watch_stylesheet() - Deterministic rendering: frame buffer with screen diffing, metadata-safe hit-testing across repaints
- 1,490 tests: unit, integration, snapshot (via
insta), and golden-file coverage unsafeforbidden: enforced by lint configuration
tools/run-doc-example.sh widgets buttons # Interactive button demo
tools/run-doc-example.sh widgets hello # Composed widget/layout showcase
tools/run-doc-example.sh widgets data_table # Data table widget
tools/run-doc-example.sh widgets input # Input fields
tools/run-doc-example.sh widgets text_area_example # Text editorWidget-focused docs parity examples live in a dedicated crate:
cargo run --manifest-path docs/examples/widgets/Cargo.toml --example tabbed_content
tools/run-doc-example.sh widgets tabbed_content_label_color
tools/run-doc-example.sh guide/screens modal01- Python docs demos (
../textual/docs/examples/**) map to our docs example lane (docs/examples/**).- Current crate-backed location:
docs/examples/widgets/examples/**anddocs/examples/guide/**.
- Current crate-backed location:
- Python app demos (
../textual/examples/**) map to app examples underexamples/**.
Interactive: Button, Input, MaskedInput, TextArea, Checkbox, RadioSet, Switch, Select, OptionList, SelectionList, ListView, DataTable, Tree, DirectoryTree, Tabs, TabbedContent, Collapsible, CommandPalette, Link
Display: Label/Static, Text, Markdown, Pretty, Digits, ProgressBar, LoadingIndicator, Sparkline, RichLog, Log, Toast, Rule, Spacer, Placeholder, HelpPanel, KeyPanel
Containers: Container, ScrollView, Frame, Panel, Overlay, Constrained, Styled, Node
Stylesheets use Textual's TCSS syntax with nested rules:
Button {
width: auto;
min-width: 16;
line-pad: 1;
text-align: center;
content-align: center middle;
&.-style-flat {
text-style: bold;
color: auto 90%;
background: $surface;
border: block $surface;
&:hover {
background: $primary;
border: block $primary;
}
}
}Supported selectors: type, #id, .class, pseudo-classes (:hover, :focus, :active, :disabled, :can-focus, :dark, :light, :even, :odd, :first-child, :last-child, and more), descendant ( ), child (>), grouping (,), universal (*).
Theme tokens ($primary, $surface, $error-darken-2, etc.) resolve against the active theme and support lighten/darken/muted derivations.
Five layout modes: vertical, horizontal, grid, dock, and absolute.
Size units: cells (20), auto, percentage (50%), fractions (1fr), viewport (100vw, 50vh).
Box model with margin collapsing, border-box sizing (default, matching Python Textual), padding, and border. Constraints via min-width, max-width, min-height, max-height. Overflow handling with scrollbars (overflow: auto | hidden | scroll).
Widget tree → rich-rs Segments (with metadata) → FrameBuffer (2D grid) → frame diff → ANSI output
- Event routing: capture phase (root → focused) then bubble phase (focused → root)
- Style resolution: CSS cascade with specificity, inheritance, and
!important - Rendering: dirty-flag driven; widgets call
ctx.request_repaint()to trigger re-render
cargo build # Build library
cargo test # Run all 1,490 tests
cargo clippy # Lint
cargo fmt # FormatEnvironment variables for targeted instrumentation:
TEXTUAL_DEBUG_STYLE_FILE=/tmp/style.log # Log CSS resolution
TEXTUAL_DEBUG_LAYOUT_FILE=/tmp/layout.log # Log layout calculations
TEXTUAL_DEBUG_INPUT_FILE=/tmp/input.log # Log input events
TEXTUAL_DEBUG_RENDER_FILE=/tmp/render.log # Log rendering
TEXTUAL_DEBUG_BORDER_FILE=/tmp/border.log # Log border painting
TEXTUAL_DEBUG_FOCUS=1 # Log focus changes to stderrFilters narrow output: TEXTUAL_DEBUG_STYLE_FILTER='type=Button,class=error'
Python Textual is the source of truth for behavior and default styling. The port aligns:
- Semantics first: event/focus/message behavior, layout/box-model rules
- Defaults second: all 16 widget default CSS files match Python Textual verbatim
- Visuals third: render-time composition, border painting, opacity blending
Rust idioms are used where appropriate (ownership, type safety, modular boundaries) while preserving behavioral parity.
See ROADMAP.md for detailed phase tracking. Current status:
- Phases 0–9.6: Complete (runtime, widgets, layout, styling, async, animations, debug tooling, input diagnostics, tabbed content parity)
- Phase 9.7: Active (core modularization)
- v0.2 execution: widget closure, invalidation improvements, broader test coverage
MIT