| Crates.io | blaeck |
| lib.rs | blaeck |
| version | 0.1.2 |
| created_at | 2026-01-23 11:37:19.636168+00 |
| updated_at | 2026-01-23 11:57:26.061128+00 |
| description | A component-based terminal UI framework for Rust |
| homepage | |
| repository | https://github.com/gustafeden/blaeck |
| max_upload_size | |
| id | 2064205 |
| size | 999,147 |
A component-based terminal UI framework for Rust — flexbox layout, async-first, inline rendering.

[dependencies]
blaeck = "0.1"
use blaeck::prelude::*;
use blaeck::Blaeck;
use std::io;
fn main() -> io::Result<()> {
let mut blaeck = Blaeck::new(io::stdout())?;
let ui = element! {
Box(border_style: BorderStyle::Round, padding: 1.0) {
Box(flex_direction: FlexDirection::Row, gap: 2.0) {
Text(content: "Status:", bold: true)
Text(content: "Ready", color: Color::Green)
}
Box(flex_direction: FlexDirection::Row, gap: 1.0) {
Text(content: "Progress:", dim: true)
Text(content: "[████████░░] 80%", color: Color::Cyan)
}
}
};
blaeck.render(ui)?;
blaeck.unmount()?;
Ok(())
}
Output:
╭────────────────────────────╮
│ Status: Ready │
│ Progress: [████████░░] 80% │
╰────────────────────────────╯
use blaeck::prelude::*;
use blaeck::{App, Key, match_key};
fn main() -> std::io::Result<()> {
let items = vec!["Build", "Test", "Deploy", "Rollback"];
let mut selected = 0;
App::new()?.run(
|_| element! {
Box(border_style: BorderStyle::Round, padding: 1.0) {
Text(content: "Select action:", bold: true)
// ... render items with selection indicator
}
},
|app, key| {
match_key(&key, &mut selected)
.on_arrow(Arrow::Up, |s| *s = s.saturating_sub(1))
.on_arrow(Arrow::Down, |s| *s = (*s + 1).min(items.len() - 1))
.on_enter(|_| { /* handle selection */ })
.on_char('q', |_| app.exit());
},
)
}
element! macro for declarative UI treeselement! {
Box(flex_direction: FlexDirection::Row, gap: 2.0) {
Box(flex_direction: FlexDirection::Column, border_style: BorderStyle::Single) {
Text(content: "Left Panel")
}
Spacer // Fills available space
Box(flex_direction: FlexDirection::Column, border_style: BorderStyle::Single) {
Text(content: "Right Panel")
}
}
}
// Text input with state
let mut input_state = TextInputState::new();
element! {
Box(flex_direction: FlexDirection::Column, gap: 1.0) {
Text(content: "Username:")
TextInput(state: input_state.clone(), placeholder: "Enter name...")
Confirm(prompt: "Save changes?", selected: true)
}
}
// Table with selection
element! {
Table(
headers: vec!["Name", "Status", "CPU"],
rows: vec![
Row::new(vec!["nginx", "running", "2.3%"]),
Row::new(vec!["postgres", "running", "5.1%"]),
],
border_style: BorderStyle::Round,
selected_row: Some(0)
)
}
// Tree view
element! {
TreeView(
root: TreeNode::new("src")
.child(TreeNode::leaf("main.rs"))
.child(TreeNode::new("components")
.child(TreeNode::leaf("mod.rs"))),
state: tree_state
)
}
| Category | Components | Notes |
|---|---|---|
| Layout | Box, Spacer, Newline, Indent, Transform |
Flexbox-based |
| Text | Text, Gradient, Markdown, SyntaxHighlight |
Full styling |
| Input | TextInput, Checkbox, Select, MultiSelect, Confirm, Autocomplete |
Interactive, focus-aware |
| Data | Table, Tabs, TreeView, BarChart, Sparkline |
Scrollable, selectable |
| Feedback | Spinner, Progress, Timer, LogBox, Diff |
Animated |
| Navigation | Breadcrumbs, StatusBar, KeyHints |
Context display |
| Overlay | Modal, Badge, Link, Divider |
Dialogs, alerts |
| Animation | AnimationTimer, blink, Easing |
10+ easing functions |
Blaeck uses inline rendering: it tracks how many lines were written and overwrites them on re-render. This means:
println!() works after unmount()Render throttling prevents excessive CPU usage:
blaeck.set_max_fps(30); // Limit to 30 renders/second
Uses Taffy for flexbox layout:
flex_direction: Row or Columnjustify_content, align_items: Standard flexbox alignmentgap, padding, margin: Spacingflex_grow, flex_shrink: Flexible sizinglet mut focus = FocusManager::new();
focus.register(FocusId(0));
focus.register(FocusId(1));
focus.on_focus_change(|event| {
// Handle focus/blur events
});
// In input handler
match_key(&key, &mut focus)
.on_tab(|f| f.focus_next())
.on_backtab(|f| f.focus_previous());
Enable with features = ["async"]:
use blaeck::{AsyncApp, AppEvent, channel};
#[tokio::main]
async fn main() -> io::Result<()> {
let (tx, mut rx) = channel::<Message>(10);
// Background task
tokio::spawn(async move {
tx.send(Message::Update).await.ok();
});
let mut app = AsyncApp::new(io::stdout())?;
loop {
tokio::select! {
event = app.next_event() => { /* handle input */ }
msg = rx.recv() => { /* handle background message */ }
}
app.render(build_ui())?;
}
}
# Basic
cargo run --example hello # Hello world
cargo run --example interactive # Keyboard input
# Components
cargo run --example spinner_demo # 15 spinner styles
cargo run --example progress # Progress bars
cargo run --example table # Data tables
cargo run --example tree # File tree view
cargo run --example syntax # Syntax highlighting
cargo run --example modal # Dialog boxes
cargo run --example barchart # Charts
cargo run --example markdown # Markdown rendering
# Advanced
cargo run --example dashboard # Multi-panel layout
cargo run --example timer # Stopwatch/countdown
cargo run --example logbox_command # Command output viewer
cargo run --example async_demo --features async # Async app
| Blaeck | Ratatui | inquire | dialoguer | |
|---|---|---|---|---|
| Rendering | Inline | Fullscreen | Inline | Inline |
| Layout | Flexbox | Constraint-based | None | None |
| Component model | Yes | No (immediate) | No | No |
| Async support | Native | Manual | No | No |
| Focus management | Built-in | Manual | Per-prompt | Per-prompt |
| Use case | Rich CLIs | Full TUIs | Prompts | Prompts |
# Build
cargo build --all-features
# Test (660 tests)
cargo test --all
# Lint
cargo clippy --all-targets --all-features
# Format
cargo fmt --all
PRs and issues welcome. For larger changes, open an issue first to discuss.
MIT OR Apache-2.0