rnk
A React-like declarative terminal UI framework for Rust, inspired by Ink and Bubbletea.

Version Status
The Crates.io badge above is the source of truth for published installs. This checkout currently declares 0.19.1; when the checkout is ahead of the latest published release, use the git dependency below for the latest source version.
Features
- React-like API: Familiar component model with hooks (
use_signal, use_state, use_ref, use_context, use_effect, use_layout_effect, use_input, use_cmd)
- Command System: Elm-inspired side effect management for async tasks, timers, file I/O
- Type-safe Commands:
Cmd<M> for compile-time message type checking
- Declarative Macros:
row!, col!, text! for concise UI building
- Declarative UI: Build TUIs with composable components
- Flexbox Layout: Powered by Taffy for flexible layouts
- Inline Mode (default): Output persists in terminal history (like Ink/Bubbletea)
- Fullscreen Mode: Uses alternate screen buffer (like vim)
- Line-level Diff Rendering: Only changed lines are redrawn for efficiency
- Persistent Output:
println() API for messages that persist above the UI
- Cross-thread Rendering:
request_render() for async/multi-threaded apps
- Rich Components: 45+ components including Box, Text, List, Table, Tree, Modal, LineChart, Calendar, CodeEditor, and more
- Animation System: Keyframe animations with 28 easing functions
- Chainable Style API: CSS-like fluent styling with
.fg(), .bg(), .bold(), .p(), .m()
- Mouse Support: Full mouse event handling
- Bracketed Paste: Distinguish between typed and pasted text
- Theme System: Centralized theming with semantic colors
- Cross-platform: Works on Linux, macOS, and Windows
Recent Improvements
- Added core hooks:
use_state, use_ref, create_context/use_context, use_layout_effect
- Integrated dirty-row rendering path in
Output::render()
- Improved command executor startup resilience (runtime creation failure now degrades safely)
- Added
docs/vibe/design-guard-and-fixflow.md for step-by-step design issue tracking and fixes
Quick Start
Install the latest published Crates.io release:
cargo add rnk
If you edit Cargo.toml manually, use the current version shown on the Crates.io badge or package page.
For the latest source version before the next release is published:
[dependencies]
rnk = { git = "https://github.com/majiayu000/rnk" }
Examples
Hello World
use rnk::prelude::*;
fn main() -> std::io::Result<()> {
render(app).run()
}
fn app() -> Element {
Box::new()
.padding(1)
.border_style(BorderStyle::Round)
.child(Text::new("Hello, rnk!").color(Color::Green).bold().into_element())
.into_element()
}
Using Declarative Macros
use rnk::prelude::*;
use rnk::{col, row, text, spacer};
fn app() -> Element {
col! {
text!("Header").bold(),
row! {
text!("Left"),
spacer!(),
text!("Right"),
},
text!("Footer").dim(),
}
}
Counter with Keyboard Input
use rnk::prelude::*;
fn main() -> std::io::Result<()> {
render(app).run()
}
fn app() -> Element {
let count = use_signal(|| 0i32);
let app = use_app();
use_input(move |input, key| {
if input == "q" {
app.exit();
} else if key.up_arrow {
count.update(|c| *c += 1);
} else if key.down_arrow {
count.update(|c| *c -= 1);
}
});
Box::new()
.flex_direction(FlexDirection::Column)
.padding(1)
.child(Text::new(format!("Count: {}", count.get())).bold().into_element())
.child(Text::new("↑/↓ to change, q to quit").dim().into_element())
.into_element()
}
Type-safe Commands with Cmd
use rnk::prelude::*;
use rnk::cmd::Cmd;
use std::time::Duration;
#[derive(Clone, Debug)]
enum Msg {
DataLoaded(Vec<String>),
Tick(u64),
}
fn load_data() -> Cmd<Msg> {
Cmd::perform(|| async { Msg::DataLoaded(vec!["Item 1".into(), "Item 2".into()]) })
}
fn start_tick() -> Cmd<Msg> {
Cmd::tick(Duration::from_secs(1), |_| Msg::Tick(1))
}
fn init() -> Cmd<Msg> {
Cmd::batch(vec![load_data(), start_tick()])
}
Render Modes
Inline Mode (Default)
Output appears at current cursor position and persists in terminal history.
render(app).run()?; render(app).inline().run()?;
Fullscreen Mode
Uses alternate screen buffer. Content is cleared on exit.
render(app).fullscreen().run()?;
Configuration Options
render(app)
.fullscreen() .fps(30) .exit_on_ctrl_c(false) .run()?;
Runtime Mode Switching
Switch between modes at runtime:
let app = use_app();
use_input(move |input, _key| {
if input == " " {
if rnk::is_alt_screen().unwrap_or(false) {
rnk::exit_alt_screen(); } else {
rnk::enter_alt_screen(); }
}
});
Components
Layout Components
| Component |
Description |
Box |
Flexbox container with full layout support |
Spacer |
Flexible space filler |
Newline |
Vertical space |
Transform |
Transform child text content |
Text & Display
| Component |
Description |
Text |
Styled text with colors and formatting |
Gradient |
Text with gradient colors |
Hyperlink |
Clickable terminal hyperlinks |
Spinner |
Animated loading indicator |
Cursor |
Blinking cursor component |
Data Display
| Component |
Description |
List |
Selectable list with keyboard navigation |
Table |
Data table with headers and styling |
Tree |
Hierarchical tree view |
Progress / Gauge |
Progress bars |
Sparkline |
Inline data visualization |
BarChart |
Horizontal/vertical bar charts |
Scrollbar |
Scrollbar indicator |
Input Components
| Component |
Description |
TextInput |
Single-line text input |
TextArea |
Multi-line text editor with vim keybindings |
SelectInput |
Dropdown-style selection |
MultiSelect |
Multiple item selection |
Confirm |
Yes/No confirmation dialog |
FilePicker |
File system browser |
Navigation
| Component |
Description |
Tabs |
Tab navigation |
Paginator |
Page navigation (dots, numbers, arrows) |
Viewport |
Scrollable content viewport |
Help |
Keyboard shortcut help display |
Feedback & Overlay
| Component |
Description |
Modal |
Modal overlay |
Dialog |
Dialog box with buttons |
Notification / Toast |
Notification messages |
Message |
Styled message boxes (info, success, warning, error) |
Static |
Permanent output above dynamic UI |
Theming
| Component |
Description |
Theme |
Centralized theme configuration |
ThemeBuilder |
Fluent theme construction |
Example: Box
Box::new()
.flex_direction(FlexDirection::Column)
.justify_content(JustifyContent::Center)
.align_items(AlignItems::Center)
.padding(1)
.margin(1.0)
.width(50)
.height(10)
.border_style(BorderStyle::Round)
.border_color(Color::Cyan)
.background(Color::Ansi256(236))
.child()
.into_element()
Example: Tree
let root = TreeNode::new("root", "Root")
.child(TreeNode::new("child1", "Child 1")
.child(TreeNode::new("grandchild", "Grandchild")))
.child(TreeNode::new("child2", "Child 2"));
Tree::new(root)
.expanded(&["root", "child1"])
.selected("grandchild")
.into_element()
Example: Modal
Modal::new()
.visible(show_modal.get())
.align(ModalAlign::Center)
.child(
Dialog::new()
.title("Confirm")
.message("Are you sure?")
.buttons(vec!["Yes", "No"])
.on_select(|idx| { })
.into_element()
)
.into_element()
Example: Notification
Notification::new()
.items(notifications.get())
.position(NotificationPosition::TopRight)
.max_visible(3)
.into_element()
Hooks
State Management
| Hook |
Description |
use_signal |
Reactive state management |
use_state |
Simplified (value, setter) state API |
use_ref |
Mutable persistent value without re-render |
create_context / use_context |
Context-based value sharing |
use_memo |
Memoized computation |
use_callback |
Memoized callback |
Effects & Commands
| Hook |
Description |
use_effect |
Side effects with dependencies |
use_effect_once |
One-time side effect |
use_layout_effect |
Layout effect API (currently aligned with use_effect) |
use_layout_effect_once |
One-time layout effect |
use_cmd |
Command execution with dependencies |
use_cmd_once |
One-time command execution |
Input
| Hook |
Description |
use_input |
Keyboard input handling |
use_mouse |
Mouse event handling |
use_paste |
Bracketed paste handling |
use_text_input |
Text input state management |
Focus & Navigation
| Hook |
Description |
use_focus |
Focus state for a component |
use_focus_manager |
Global focus management |
use_scroll |
Scroll state management |
Application
| Hook |
Description |
use_app |
Application control (exit, etc.) |
use_window_title |
Set terminal window title |
use_frame_rate |
Frame rate monitoring |
use_measure |
Measure element dimensions |
use_stdin / use_stdout / use_stderr |
Stdio access |
Example: use_paste
use_paste(move |event| {
match event {
PasteEvent::Start => { }
PasteEvent::Content(text) => {
input_buffer.update(|b| b.push_str(&text));
}
PasteEvent::End => { }
}
});
Example: use_memo
let items = use_signal(|| vec![1, 2, 3, 4, 5]);
let sum = use_memo(
move || items.get().iter().sum::<i32>(),
vec![items.get().len()],
);
Command System
Basic Commands
use rnk::cmd::Cmd;
Cmd::none()
Cmd::batch(vec![cmd1, cmd2, cmd3])
Cmd::sequence(vec![cmd1, cmd2, cmd3])
Cmd::sleep(Duration::from_secs(1))
Cmd::tick(Duration::from_secs(1), |timestamp| { })
Cmd::every(Duration::from_secs(1), |timestamp| { })
Cmd::perform(|| async { })
cmd.and_then(another_cmd)
Terminal Control Commands
Cmd::clear_screen()
Cmd::hide_cursor()
Cmd::show_cursor()
Cmd::set_window_title("My App")
Cmd::window_size()
Cmd::enter_alt_screen()
Cmd::exit_alt_screen()
Cmd::enable_mouse()
Cmd::disable_mouse()
Cmd::enable_bracketed_paste()
Cmd::disable_bracketed_paste()
External Process Execution
Cmd::exec_cmd("vim", &["file.txt"], |result| {
match result {
ExecResult::Success(code) => { }
ExecResult::Error(err) => { }
}
})
Type-safe Commands (Cmd)
use rnk::cmd::Cmd;
#[derive(Clone)]
enum Msg {
Loaded(String),
}
let cmd: Cmd<Msg> = Cmd::perform(|| async { Msg::Loaded("data".to_string()) });
Declarative Macros
use rnk::{col, row, text, styled_text, spacer, box_element, when, list};
col! {
text!("Line 1"),
text!("Line 2"),
}
row! {
text!("Left"),
spacer!(),
text!("Right"),
}
text!("Count: {}", count)
styled_text!("Error!", color: Color::Red)
when!(show_error => text!("Error occurred!"))
list!(items.iter(), |item| text!("{}", item))
list_indexed!(items.iter(), |idx, item| text!("[{}] {}", idx, item))
Theme System
use rnk::components::{Theme, ThemeBuilder, set_theme, get_theme, with_theme};
let theme = ThemeBuilder::new()
.primary(Color::Cyan)
.secondary(Color::Magenta)
.success(Color::Green)
.warning(Color::Yellow)
.error(Color::Red)
.build();
set_theme(theme);
let color = get_theme().semantic_color(SemanticColor::Primary);
with_theme(dark_theme, || {
});
Cross-thread Rendering
use std::thread;
fn main() -> std::io::Result<()> {
thread::spawn(|| {
loop {
rnk::request_render();
rnk::println("Background task completed");
thread::sleep(Duration::from_secs(1));
}
});
render(app).run()
}
Testing
use rnk::testing::{TestRenderer, assert_layout_valid};
#[test]
fn test_component() {
let element = my_component();
let renderer = TestRenderer::new(80, 24);
renderer.validate_layout(&element).expect("valid layout");
let output = rnk::render_to_string(&element, 80);
assert!(output.contains("expected text"));
}
Running Examples
cargo run --example hello
cargo run --example counter
cargo run --example todo_app
cargo run --example rnk_top cargo run --example rnk_git cargo run --example rnk_chat
cargo run --example tree_demo
cargo run --example multi_select_demo
cargo run --example notification_demo
cargo run --example textarea_demo
cargo run --example file_picker_demo
cargo run --example theme_demo
cargo run --example typed_cmd_demo
cargo run --example macros_demo
cargo run --example streaming_demo
cargo run --example glm_chat
Architecture
src/
├── animation/ # Keyframe animations, easing functions, spring physics
├── cmd/ # Command system (Cmd<M>, executor)
├── components/ # 45+ UI components
├── core/ # Element, Style, Color primitives
├── hooks/ # React-like hooks (use_signal, use_animation, use_transition)
├── layout/ # Taffy-based flexbox layout engine
├── macros.rs # Declarative UI macros
├── renderer/ # Terminal rendering, App runner
├── runtime/ # Signal handling, environment detection
└── testing/ # TestRenderer, TestHarness, assertions
Comparison with Ink/Bubbletea
| Feature |
rnk |
Ink |
Bubbletea |
| Language |
Rust |
JavaScript |
Go |
| Rendering |
Line-level diff |
Line-level diff |
Line-level diff |
| Layout |
Flexbox (Taffy) |
Flexbox (Yoga) |
Manual |
| State |
Hooks + Signals |
React hooks |
Model-Update |
| Type-safe Cmds |
Cmd |
N/A |
N/A |
| Declarative Macros |
row!/col!/text! |
JSX |
N/A |
| Components |
40+ |
~10 |
Bubbles lib |
| Inline mode |
✓ |
✓ |
✓ |
| Fullscreen |
✓ |
✓ |
✓ |
| Mouse support |
✓ |
✗ |
✓ |
| Bracketed paste |
✓ |
✗ |
✓ |
| Theme system |
✓ |
✗ |
Lipgloss |
| Println |
✓ |
Static |
tea.Println |
| Cross-thread |
request_render() |
- |
tea.Program.Send |
| Suspend/Resume |
✓ |
✗ |
✓ |
License
MIT