| Crates.io | rustact |
| lib.rs | rustact |
| version | 0.1.0 |
| created_at | 2025-11-21 13:34:51.613122+00 |
| updated_at | 2025-11-21 13:34:51.613122+00 |
| description | Async terminal UI framework inspired by React, built on top of ratatui and tokio. |
| homepage | https://github.com/IllusiveBagel/rustact |
| repository | https://github.com/IllusiveBagel/rustact |
| max_upload_size | |
| id | 1943559 |
| size | 209,223 |
Rustact is an experimental, React-inspired framework for building async terminal UIs in Rust. It layers a component/hook system on top of ratatui, offering familiar primitives such as components, state, effects, context, and an event bus.
component("Name", |ctx| ...) and manage state with use_state, use_reducer, use_ref, use_effect, use_memo, use_callback, and provide_context / use_context.tokio, handling terminal IO, ticks, and effect scheduling without blocking the UI thread.App::with_driver(...) for deterministic tests or custom IO sources.tracing spans/logs around renders, events, and shutdown, so you can inspect behavior with any tracing subscriber.ctx.dispatcher().events().subscribe() and react within hooks or spawned tasks.ratatui widgets, handling layout primitives like blocks, vertical/horizontal stacks, styled text, list panels, gauges, tables, tree views, forms, and rich text inputs.use_text_input binds component state to editable fields (with cursor management, tab focus, and secure mode), while use_text_input_validation or handle.set_status(...) drive contextual border colors and helper text.TabsNode, ModalNode, LayeredNode, and ToastStackNode to build multi-pane dashboards with modal dialogs and notification stacks without wiring bespoke renderer code.styles/demo.css to recolor widgets, toggle button fills, rename gauge labels, change list highlight colors, resize form/table columns, or theme inputs without touching Rust code.RUSTACT_WATCH_STYLES=1 (or true/on) to have the runtime poll styles/demo.css and live-reload the stylesheet without restarting the app.src/main.rs now composes every hook (state, reducer, ref, memo, callback, effect, context) with all widgets (text, flex, gauge, buttons, lists, tables, trees, forms) so you can see each feature in one place.docs/guide.md – day-to-day developer guide (setup, workflows, hook primer).docs/tutorial.md – step-by-step tutorial for building a fresh Rustact app.docs/architecture.md – deep dive into the runtime, hooks, renderer, and events.docs/styling.md – CSS subset reference and theming tips.docs/roadmap.md – prioritized initiatives to steer ongoing work.docs/api-docs.md – publishing instructions for hosting cargo doc output.docs/template.md – outline for the upcoming cargo generate starter template.templates/rustact-app/ – ready-to-use project scaffold consumable via cargo generate.Spin up a fresh app via cargo-generate:
cargo install cargo-generate
cargo generate \
--git https://github.com/IllusiveBagel/rustact \
--branch main \
--path templates/rustact-app \
--name my-rustact-app
cd my-rustact-app
cargo run
The template mirrors the demo’s patterns (hooks, dispatcher events, text inputs) plus a default stylesheet and README.
The workflow .github/workflows/publish-docs.yml builds cargo doc --no-deps on every push to main and deploys the result via GitHub Pages. Enable Pages → "GitHub Actions" in repo settings to activate it. Manual steps and customization tips live in docs/api-docs.md.
Rustact emits tracing spans throughout the runtime. Enable them in your binary (or the demo) by adding a subscriber:
use tracing_subscriber::EnvFilter;
fn init_tracing() {
let _ = tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new("rustact=info"))
.try_init();
}
Then set RUST_LOG=info (or more specific filters) before running the app to see render requests, external events, and shutdown diagnostics.
cargo run
While running:
+, -, or r, or click the on-screen - / + buttons to interact with the counter (watch the progress gauge update as the count approaches ±10).Ctrl+C.RUSTACT_WATCH_STYLES=1 cargo run
When the env var is set (accepts 1, true, or on) and styles/demo.css exists next to the binary, Rustact will poll the file, re-parse it on change, and trigger a render so you can see your CSS edits immediately. The demo and ops dashboard both honor the flag; when the file is missing, the runtime logs a warning and keeps using the embedded stylesheet.
cargo run --bin ops_dashboard
While running:
1/2 to switch between the overview and streaming logs tabs.i to pop open the incident modal, Esc to close it.c to dismiss the oldest toast.LayeredNode, ModalNode, and ToastStackNode, so they are reusable in your own apps.Rustact loads styles/demo.css on startup. The CSS parser understands simple selectors (element, element#id, element.class) plus custom properties, so you can retheme the terminal UI without recompiling:
:root defines palette tokens such as --accent-color, --warning-color, and friends that the demo injects into its Theme context.button#counter-plus (and #counter-minus) set accent-color and --filled to control button appearance.gauge#counter-progress can override the bar color and --label text.list#stats exposes --highlight-color and --max-items, while table#services reads --column-widths and form#release reads --label-width.input, input#feedback-name, etc. configure accent/border/text/background colors, while tip.keyboard / .mouse / .context use the standard color property to tint each info card.Save the file and rerun cargo run to see your tweaks reflected immediately.
See docs/styling.md for a deeper dive into selectors, property types, and integration tips.
use rustact::{component, Element, Scope};
use rustact::runtime::Color;
fn greeting(ctx: &mut Scope) -> Element {
let color = ctx
.use_context::<Theme>()
.map(|theme| theme.accent)
.unwrap_or(Color::Green);
Element::colored_text("Hello from a component", color)
}
let app = App::new("Demo", component("Greeting", greeting));
Each render receives a Scope, giving access to hooks, the dispatcher, and context. Components can compose other components with .into():
Element::vstack(vec![
component("Greeting", greeting).into(),
Element::text("Static text"),
])
Tables, trees, and forms ship with builder-style APIs so you can fluently describe structured layouts:
use rustact::{Element, TableCellNode, TableNode, TableRowNode};
use rustact::runtime::Color;
let table = TableNode::new(vec![
TableRowNode::new(vec![
TableCellNode::new("api").bold(),
TableCellNode::new("Healthy").color(Color::Green),
]),
])
.header(TableRowNode::new(vec![
TableCellNode::new("Service").bold(),
TableCellNode::new("Status").bold(),
]))
.widths(vec![40, 60])
.highlight(0);
Element::table(table);
TreeNode/TreeItemNode let you model recursive hierarchies, while FormNode + FormFieldNode render labeled key/value pairs with validation state highlighting.
use rustact::{Element, Scope, TextInputNode};
use rustact::runtime::FormFieldStatus;
fn feedback(ctx: &mut Scope) -> Element {
let name = ctx.use_text_input("feedback:name", || String::new());
let name_status = ctx.use_text_input_validation(&name, |snapshot| {
if snapshot.value.trim().is_empty() {
FormFieldStatus::Warning
} else {
FormFieldStatus::Success
}
});
Element::text_input(
TextInputNode::new(name)
.label("Display name")
.placeholder("Rustacean in Residence")
.width(32)
.status(name_status),
)
}
use_text_input registers a focusable binding with the shared registry. The runtime tracks hitboxes, so clicking anywhere in the input focuses it, while Tab/Shift+Tab move between inputs in declaration order. A blinking cursor shows when the field is focused, and .secure(true) masks sensitive values. Use ctx.use_text_input_validation or handle.set_status(FormFieldStatus::Error) to tint the field, then read helper text from the same status to explain errors or warnings.
use rustact::{Element, Scope};
#[derive(Clone, Copy)]
enum Action {
Increment,
Reset,
}
fn counter(ctx: &mut Scope) -> Element {
let (value, dispatch) = ctx.use_reducer(|| 0i32, |state, action: Action| match action {
Action::Increment => *state += 1,
Action::Reset => *state = 0,
});
let last_event = ctx.use_ref(|| None::<String>);
Element::text(format!(
"Value: {value} (last event: {:?})",
last_event.with(|evt| evt.clone())
))
}
use_reducer returns the current value plus a dispatch handle that batches state updates through your reducer; use_ref stores mutable data without triggering re-renders (handy for metrics or imperative handles).
Every render builds a View tree, compares it with the previous frame, and only asks the renderer to draw when something actually changed. Hooks, effects, and state updates still run, but redundant terminal flushes are avoided—mirroring the virtual DOM approach.
Rustact is distributed under the MIT License. By contributing, you agree that your contributions will be licensed under the same terms.