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…

这是什么?rataflow 是一个 Rust 库,用于直接在终端里构建基于节点的交互式图表编辑器,从静态图表到完全交互式的节点编辑器都可以。
这是独立应用,还是我嵌入自己 Rust 项目的库?它是一个构建在 ratatui 终端 UI 框架之上的库,仿照 Web 端的 xyflow(React Flow)设计,所以你要把它的 Flow 组件嵌进自己的 TUI 应用,而不是把它当程序运行。
怎么把 rataflow 加进项目?在 Cargo.toml 里把它添加为依赖,然后用节点和边构建你的图。examples/ 目录里每个功能都有可运行的演示,用 cargo run --example 运行。
它支持哪些终端后端?它与后端无关:对原生终端支持 crossterm(默认)、termion 和 termwiz,还通过 ratzilla 支持 WebAssembly,可以在浏览器里运行。
接事件循环时有什么坑吗?终端后端会以 125-1000Hz 的频率逐个投递原始鼠标事件,而不像浏览器那样合并移动事件,所以拖拽过程中每次渲染前必须把所有待处理事件排空,否则输入会有明显的延迟。
rataflow 的性能与 xyflow(React Flow)相比如何?在 WASM 基准测试中,rataflow 渲染 625 个节点约需 8 毫秒,而 xyflow 约需 11 毫秒;xyflow 渲染 625 个节点所需的帧时间,rataflow 大约能渲染 10,000 个节点。
内置了哪些配套组件?Flow 自带 Controls、MiniMap 和 Background 组件,包裹在 Flow 实例外围,还内置 TextContent、StepEdge、StraightEdge 和 FloatingEdge 等节点/边类型。
我能保存和恢复图,或实现撤销/重做吗?可以。图快照可以用 serde 序列化,saverestore 和 undoredo 示例就演示了这一点。
它包含自动图布局吗?包含。它内置基于 Sugiyama 的分层布局,方向、间距和边距都可配置;你也可以改用自己的外部布局算法提供位置。
What are similar projects?zoetrope, LogicFlow, neomacs, gitlogue, FTXUI, ratatui, sofka, abtop
Creator
furkankly
Size
4.2 MB
Stars
187
Age
2mo
Commits · 90 Days