Blaeck

February 2, 2026 · View on GitHub

A terminal UI framework for Rust. Inline rendering, flexbox/grid layout, 35+ components.

Crates.io Documentation License

Showcase

Install

[dependencies]
blaeck = "0.3"

Inline Rendering

The killer feature: blaeck renders within your terminal flow. Output stays in scrollback. println!() works before and after.

Inline Demo

use blaeck::prelude::*;
use std::{thread, time::Duration};

fn main() -> std::io::Result<()> {
    println!("Starting build...\n");

    let mut blaeck = Blaeck::new(std::io::stdout())?;

    for i in 0..=10 {
        let bar = "█".repeat(i * 2) + &"░".repeat(20 - i * 2);
        blaeck.render(element! {
            Box(border_style: BorderStyle::Round, padding: 1.0) {
                Text(content: "Building...", bold: true)
                Text(content: bar, color: Color::Green)
            }
        })?;
        thread::sleep(Duration::from_millis(200));
    }

    blaeck.unmount()?;
    println!("Done! Terminal keeps working.");
    Ok(())
}

Interactive Apps

Use ReactiveApp for keyboard-driven UIs. State changes trigger automatic re-renders.

Form Demo

use blaeck::prelude::*;
use blaeck::reactive::*;
use crossterm::event::KeyCode;

fn form(cx: Scope) -> Element {
    let name = use_state(cx.clone(), || String::new());
    let env = use_state(cx.clone(), || 0usize); // 0=dev, 1=staging, 2=prod
    let confirmed = use_state(cx.clone(), || false);
    let focus = use_state(cx.clone(), || 0usize); // 0=name, 1=env, 2=confirm

    let envs = vec!["Development", "Staging", "Production"];

    let name_h = name.clone();
    let env_h = env.clone();
    let confirmed_h = confirmed.clone();
    let focus_h = focus.clone();

    use_input(cx, move |key| {
        let f = focus_h.get();
        match key.code {
            KeyCode::Tab => focus_h.set((f + 1) % 3),
            KeyCode::BackTab => focus_h.set(if f == 0 { 2 } else { f - 1 }),
            KeyCode::Up if f == 1 => env_h.set(if env_h.get() == 0 { 2 } else { env_h.get() - 1 }),
            KeyCode::Down if f == 1 => env_h.set((env_h.get() + 1) % 3),
            KeyCode::Char(' ') if f == 2 => confirmed_h.set(!confirmed_h.get()),
            KeyCode::Char(c) if f == 0 => {
                let mut s = name_h.get();
                s.push(c);
                name_h.set(s);
            }
            KeyCode::Backspace if f == 0 => {
                let mut s = name_h.get();
                s.pop();
                name_h.set(s);
            }
            _ => {}
        }
    });

    let f = focus.get();
    let name_val = name.get();
    let env_val = env.get();
    let conf_val = confirmed.get();

    element! {
        Box(flex_direction: FlexDirection::Column, border_style: BorderStyle::Round, padding: 1.0) {
            Text(content: "Deploy Configuration", bold: true, color: Color::Cyan)
            Newline

            // Name field
            Text(content: "Project name:", bold: f == 0)
            Box(flex_direction: FlexDirection::Row) {
                Text(content: if f == 0 { "> " } else { "  " }, color: Color::Green)
                Text(
                    content: if name_val.is_empty() { "(type here)".to_string() } else { name_val },
                    color: if f == 0 { Color::White } else { Color::DarkGray }
                )
                Text(content: if f == 0 { "_" } else { "" }, color: Color::Green)
            }
            Newline

            // Environment selector
            Text(content: "Environment:", bold: f == 1)
            #(Element::column(
                envs.iter().enumerate().map(|(i, e)| {
                    let selected = i == env_val;
                    let focused = f == 1;
                    let prefix = if selected { "● " } else { "○ " };
                    element! {
                        Text(
                            content: format!("  {}{}", prefix, e),
                            color: if selected && focused { Color::Green } else { Color::White },
                            bold: selected
                        )
                    }
                }).collect()
            ))
            Newline

            // Confirm checkbox
            Box(flex_direction: FlexDirection::Row) {
                Text(content: if f == 2 { "> " } else { "  " }, color: Color::Green)
                Text(
                    content: format!("[{}] I confirm this deployment", if conf_val { "x" } else { " " }),
                    bold: f == 2
                )
            }
            Newline

            Text(content: "Tab: next field | Space: toggle | Ctrl+C: exit", dim: true)
        }
    }
}

fn main() -> std::io::Result<()> {
    ReactiveApp::run(form)?;
    Ok(())
}

Key concepts: use_state creates reactive state. use_input registers keyboard handlers. When state changes, UI re-renders automatically.


When to Use Blaeck

Good for:

  • CLI tools with rich output (installers, build tools)
  • Interactive prompts with layout control
  • Progress displays, dashboards
  • Any UI that shouldn't take over the screen

Not for:


Components

CategoryComponents
LayoutBox, Spacer, Newline, Indent
TextText, Gradient, Markdown, SyntaxHighlight
InputTextInput, Select, MultiSelect, Checkbox, Confirm
DataTable, Tabs, TreeView, BarChart, Sparkline
FeedbackSpinner, Progress, Timer, Modal, Diff

Examples

cargo run --example demo_inline      # Inline rendering demo
cargo run --example form_demo        # Interactive form
cargo run --example reactive_counter # Counter with signals
cargo run --example reactive_list    # List navigation
cargo run --example dashboard        # Multi-panel layout
cargo run --example table            # Data tables
cargo run --example spinner_demo     # 15 spinner styles

See all examples or API docs.


Example Viewer

The example_viewer is an interactive tool for exploring all blaeck examples in a single interface. It provides a three-panel layout with live previews, source code viewing, and interactive testing.

Try it now (no Rust required):

curl -fsSL https://gustafeden.github.io/blaeck/run-example-viewer.sh | bash

Or run from source:

cargo run --example example_viewer

Features

  • Live Previews - See rendered output for all 44 examples in real-time
  • Interactive Mode - Press Enter to interact with any example in focus mode
  • Source Code Viewer - View both the example wrapper and UI implementation code
  • Background Execution - Build and run examples with captured output
  • Resizable Panels - Adjust layout with [ and ] keys
  • Example Management - Mark and batch-delete examples for cleanup

Controls

KeyAction
/ or k/jNavigate example list
EnterEnter live interactive preview (focus mode)
EscExit focus mode / Quit
[ / ]Resize preview panel (shrink/grow)
PageUp/PageDown or K/JScroll source code
rRun selected example (capture output)
sStop running example
bBuild selected example
dToggle mark for deletion
xExecute deletions (with confirmation)
qQuit

Perfect for:

  • Exploring the full component library
  • Testing examples without running them individually
  • Comparing UI implementations
  • Learning blaeck patterns

License

MIT OR Apache-2.0