rataflow README

The project's README from github.com/furkankly/rataflow. As one markdown file: README.md.

rataflow

Interactive node-based UIs for the terminal.

crates.io docs.rs downloads build status minimum supported Rust version

rataflow.furkankly.dev · every example in your browser, the crate itself compiled to WASM

A terminal canvas with two nodes wired by a step edge: a shell running tail -f, and a live request-rate sparkline

rataflow is a library for building node-based UIs in the terminal, from a static diagram to a fully interactive editor. Built on ratatui, inspired by xyflow (React Flow).

rataflow demo

Installation

cargo add rataflow

Or add to your Cargo.toml:

[dependencies]
rataflow = "0.1"

Features

Graph model

Interaction

Rendering

Layout

Integration

Concepts

Quick Start

A list of edges is enough to get a graph on screen. Nodes come from the unique names, positions from the layout, handles from its direction. It's draggable, pannable and zoomable from the first frame.

use rataflow::{Flow, Sugiyama};

let mut flow: Flow = Flow::from_edges(
    &[("Start", "Process"), ("Process", "End")],
    Sugiyama::vertical(),
)?;

To say more than that, build the graph yourself. The defaults come apart into their pieces: your own positions, handles, content types and edge kinds.

use rataflow::{Flow, Node, Edge, StepEdge};

// Create nodes with auto-sized text content
let nodes = vec![
    Node::from_text("a", (10.0, 10.0), "Node A"),
    Node::from_text("b", (40.0, 10.0), "Node B"),
];

// Create edges
let edges: Vec<Edge<StepEdge>> = vec![
    Edge::new("e1", "a", "b"),
];

// Create flow (`?` here assumes an enclosing `fn main() -> Result<..>`)
let mut flow = Flow::with_graph(nodes, edges)?;

// Request fit-view (applied at render time)
flow.request_fit_view();

// Render in your draw loop
terminal.draw(|f| {
    f.render_widget(&mut flow, f.area());
})?;

The examples/ directory has a runnable demo for every feature. A few starting points:

Run any of them with cargo run --example <name>.

Event Handling

Event handlers return an EventResponse: NotHandled, Handled, or Event(Vec<FlowEvent>). A single interaction can produce several events, for example NodeClicked followed by SelectionChanged:

use rataflow::FlowEvent;

for event in flow.handle_mouse_event(mouse.into()).into_events() {
    match event {
        FlowEvent::NodeClicked { node_id } => {
            // Show details, fetch data, etc.
        }
        FlowEvent::ConnectionCompleted(conn) => {
            // Add the edge, then persist to backend, validate, etc.
            flow.add_edge_from_connection(conn, StepEdge::default());
        }
        FlowEvent::SelectionChanged { node_ids, .. } => {
            // Update sidebar with current selection
        }
        _ => {}
    }
}

Under the Hood

A terminal cell grid doesn't give you what a browser does. There's no compositor, no stacking contexts, and no coordinates for anything drawn past the screen edge. A few of the pieces this library fills in:

See docs/ARCHITECTURE.md for the design rationale, and docs/INTERNALS.md for how it is implemented.

I've written this up as a series, Node-based UIs in the terminal. The first post covers the whole surface, and the other four each go one level down:

Event Loop

One operational gotcha: terminal backends deliver every raw mouse event individually (125-1000Hz), unlike browsers, which coalesce mouse moves between frames. During a drag the unprocessed events queue up and the input visibly lags.

Drain all pending events before each render:

'main: loop {
    terminal.draw(|f| {
        f.render_widget(&mut flow, area);
    })?;

    // Wait up to 16ms (~60 FPS) for the first event, then drain the rest
    if event::poll(Duration::from_millis(16))? {
        loop {
            match event::read()? {
                Event::Key(key) => {
                    if key.code == KeyCode::Char('q') { break 'main; }
                    flow.handle_key_event(key.into());
                }
                Event::Mouse(mouse) => {
                    for event in flow.handle_mouse_event(mouse.into()).into_events() {
                        match event {
                            FlowEvent::NodeClicked { node_id } => { /* ... */ }
                            _ => {}
                        }
                    }
                }
                _ => {}
            }
            if !event::poll(Duration::ZERO)? { break; }
        }
    }
}

All examples use this pattern. See examples/basic_async.rs for the tokio equivalent.

Performance

Benchmarks measure node dragging, the hardest sustained operation and the one where frame time turns into visible jank. Each test runs 20 consecutive move-and-render frames. Selection and mounting get no benchmarks of their own, because they are single-frame operations and dragging already covers the sustained case.

The graph topology and size (25x25 chain = 625 nodes, 624 edges) match xyflow's stress test. Frame durations measured via performance.now() (WASM/xyflow) and std::time::Instant (native). Only the 20 mousemove frames are reported.

cargo run --release --example stress_test -- --bench        # Headless benchmark (25x25 default)
cargo run --release --example stress_test                   # Interactive (t=drag, a=all, q=quit)

Native

Headless benchmark, 200x60 terminal buffer (a typical fullscreen terminal at 1080p, fixed so numbers compare across machines). Release build, chain topology.

Nodes Edges Drag Avg FPS
625 624 ~1.0ms ~1,000
10,000 9,999 ~6.6ms ~152
22,500 22,499 ~11.4ms ~88
40,000 39,999 ~18.1ms ~55

Grid topology (2 edges per node) roughly doubles render time: 37,500 nodes with 74,600 edges averages ~33ms.

Native vs WASM

At 625 nodes: ~1.0ms vs ~8ms. The ~8x overhead comes from the WebGL2 rendering pipeline and browser frame scheduling.

Full WASM scaling data
Nodes Edges Drag Avg Range
625 624 ~8ms 7-10ms
2,500 2,499 ~8ms 8-9ms
5,625 5,624 ~8ms 7-10ms
10,000 9,999 ~8ms 7-12ms
22,500 22,499 ~13ms 12-15ms
27,889 27,888 ~17ms 16-19ms

WASM vs xyflow (React Flow)

rataflow renders to a flat cell buffer on a WebGL2 canvas via ratzilla; xyflow renders to the DOM using React/Svelte. These are fundamentally different rendering architectures, so this isn't a "which is better". It's a concrete illustration of the tradeoffs each approach makes.

625 nodes, 624 edges. Same browser, same window.

Library Avg Frame Range Frames
rataflow WASM ~8ms 7-10ms 20/20
xyflow (React Flow) ~11ms 5-30ms 11-14/20

Scaling. How many nodes at equivalent frame time:

Library Nodes Edges Avg Frame Range
xyflow (React Flow) 625 624 ~11ms 5-30ms
rataflow WASM 10,000 9,999 ~8ms 7-12ms

16:1. rataflow WASM handles 10,000 nodes at the frame time xyflow needs for 625.

Feature Flags

Contributing

Pull requests are welcome.

License

MIT License

Acknowledgements

rataflow (furkankly/rataflow)

Searching Database…

What is this?rataflow is a Rust library for building node-based, interactive diagram editors directly inside a terminal, ranging from a static diagram to a fully interactive node editor.
Is this a standalone app or a library I embed in my own Rust project?It's a library built on top of the ratatui terminal UI framework, modeled after xyflow (React Flow) for the web, so you embed its Flow widget inside your own TUI application rather than running it as a program.
How do I add rataflow to a project?Add it as a dependency in your Cargo.toml, then build your graph from nodes and edges. The examples/ directory has a runnable demo for every feature, runnable with cargo run --example .
Which terminal backends does it support?It's backend-agnostic: it supports crossterm (the default), termion, and termwiz for native terminals, plus WebAssembly via ratzilla for running in a browser.
Any gotchas when wiring up the event loop?Terminal backends deliver every raw mouse event individually at 125-1000Hz instead of coalescing moves like browsers do, so during a drag you must drain all pending events before each render or the input will visibly lag.
How does rataflow's performance compare to xyflow (React Flow)?In WASM benchmarks, rataflow renders 625 nodes in about 8ms versus xyflow's ~11ms, and it can render roughly 10,000 nodes in the same frame time xyflow needs for just 625 nodes.
What companion widgets come built in?Flow ships with Controls, MiniMap, and Background widgets that wrap around a Flow instance, plus built-in node/edge types like TextContent, StepEdge, StraightEdge, and FloatingEdge.
Can I save and restore a graph, or implement undo/redo?Yes, graph snapshots can be serialized with serde, which the saverestore and undoredo examples demonstrate.
Does it include automatic graph layout?Yes, a Sugiyama-based layered layout is built in with configurable direction, spacing and margins, or you can supply positions from your own external layout algorithm instead.
What are similar projects?zoetrope, LogicFlow, neomacs, gitlogue, FTXUI, ratatui, sofka, abtop
Creator
furkankly
Size
4.2 MB
Stars
186
Age
1mo
Commits · 90 Days