flow is a single-header C library for building interactive node-graph editors in the
terminal — a C/terminal analog of xyflow (React Flow).
Dependency-free: pure ANSI escapes, links only -lm.
Try it:
make && ./demos/topo— a small network-topology editor (live pan, mouse drag, zoom, minimap)../demos/hello_flowis the minimal showcase (shown above).
Working today (each maps to a familiar xyflow concept):
- Nodes & edges with a custom-type system — measure/render vtables of function pointers
(
flow_register_node_type/flow_register_edge_type), the C analog ofnodeTypes/edgeTypes. - Handles, connect & reconnect, with a pluggable connection validator
(
flow_set_connection_validator, the analog ofisValidConnection). - Edge labels and straight / orthogonal (step) routing.
- Selection: single, multi, and marquee.
- Drag with auto-pan; per-element gates (draggable / selectable / deletable); an SE-corner resizer with explicit, persisted node sizes.
- Viewport: pan (arrows / drag / scroll), pointer-centered zoom with limits, fit-view, and level-of-detail collapsing.
- Minimap, controls, status bar, and node/edge toolbars.
- Subflows / groups (
flow_group/flow_ungroup/flow_set_parent). - Undo / redo via a coalescing command journal.
- Auto-layout: force-directed and layered (Sugiyama-style).
- JSON save / load (versioned format) and theming (DEFAULT / LIGHT / DARK, theme tokens, dot/line/cross backgrounds).
Honest terminal limits: one glyph = one cell (no sub-cell magnification — zoom changes spacing + level-of-detail), edges render as box-drawing corners (no bezier curves), and labels are measured one codepoint per cell (wide/CJK/emoji glyphs will misalign).
flow is a single header. Copy flow.h into your project — that's the whole install.
(A tagged release also attaches flow.h as a downloadable asset.)
In exactly one .c file, define FLOW_IMPLEMENTATION before the include to compile the
implementation. Every other file includes flow.h without the macro (declarations only).
#define FLOW_IMPLEMENTATION
#include "flow.h"
int main(void) {
flow_t *f = flow_new(80, 24);
flow_register_defaults(f);
int a = flow_add_node(f, "default", (flow_pt){4, 3}, (void*)"node A");
int b = flow_add_node(f, "default", (flow_pt){34, 12}, (void*)"node B");
flow_add_edge(f, a, b, "", "");
flow_run(f); /* arrow keys pan, q quits */
flow_free(f);
}Build (requires C99 or later; -lm is required on Linux):
cc app.c -std=c11 -lm -o appflow_run owns the terminal — it sets raw mode, polls stdin, and writes frames to stdout.
When you embed flow inside an app that already has an event loop (a game tick, a GUI
host, an ssh session, a test), skip flow_run and drive the model directly with two pure,
terminal-free calls:
flow_feed(f, bytes, n)— hand the model input bytes from whatever source you own (your ownread(), a socket, a scripted sequence). Same key/mouse parserflow_runuses.flow_render_diff(f)— render the model, diff it against the previous frame, advance the front buffer, and return a malloc'd escape string (absolute-positioned CSI/SGR). You write that string to whatever fd you own, thenfree()it. It returns""when nothing changed. (flow_presentis exactly this plusfputs(stdout)+fflush.)
flow_t *f = flow_new(80, 24); /* you pick the surface size — no terminal to query */
flow_register_defaults(f);
/* ... add nodes/edges ... */
for (;;) { /* YOUR loop, YOUR I/O */
char in[64];
int n = my_read_input(in, sizeof in); /* socket / pty / replay — your source */
flow_feed(f, in, n);
char *frame = flow_render_diff(f); /* "" if nothing moved */
my_write_output(frame, strlen(frame));/* your fd: pty, file, network */
free(frame);
}Only flow_run / flow_present / flow_term_* are POSIX/terminal-bound; everything else
(model, geometry, render-to-buffer, routing, layout, JSON) is portable C. For the full back
buffer instead of a diff, call flow_render(f, cells, cols, rows) and read the flow_cell
grid yourself. See examples/embed_headless.c for a complete,
TTY-free program (make examples).
Terminal safety. If you take the POSIX terminal path (flow_run, or flow_term_setup
directly), flow installs SIGINT/SIGTERM and crash (SIGSEGV/SIGABRT/…) handlers that
restore the terminal — raw mode, alt-screen, cursor, mouse tracking — and an atexit hook for
stray exit() calls, then chain to the previous handler so the process still dies with the
right status. The handlers are removed on flow_term_restore, and the headless path above
never installs them. If instead you put the terminal in raw mode yourself, restore it on
those signals yourself (async-signal-safely: write() the reset escape + tcsetattr, then
re-raise).
- C99 or later,
-lm. - C++: the header is
extern "C"-guarded and compiles as a C++ translation unit (clang/gcc,-std=c++17) — a C-compiledflow.olinks against C++ callers (make cpp). The color-preset table uses C99 array designators (accepted as an extension by clang/gcc), so MSVC's C++ frontend is not yet supported. - Linux / glibc: under strict
-std=c11, glibc hides the POSIX symbols the implementation needs (sigaction,ioctl,poll) behind a feature-test macro.flow.hrequests it (_DEFAULT_SOURCE) before its own includes, so in the implementation TU includeflow.hbefore any other system header — or compile with-D_DEFAULT_SOURCE(or-std=gnu11). macOS and the BSDs need none of this. - The interactive run-loop (
flow_run/flow_feed/flow_present/flow_term_*) needs a POSIX terminal (Linux / macOS). - The model, geometry, rendering, routing, layout, and JSON layers are portable C and can be embedded in your own event loop with your own I/O (Windows / non-TTY hosts can use these without the run-loop). Not yet validated on MSVC.
- Not thread-safe: serialize calls per
flow_t; separate instances are independent.
flow.h is generated from src/ by tools/amalgamate.sh — edit src/, never flow.h.
make # regenerate flow.h and build the demos + examples
make test # run the headless test suite (39 suites, snapshot goldens)| Doc | What's in it |
|---|---|
docs/API.md |
Per-function reference for every public symbol. |
docs/xyflow-mapping.md |
Coming from React Flow? Concept-by-concept mapping to the flow API, and the terminal divergences. |
docs/theming.md |
Color modes, the theme token struct, and the background grid. |
docs/ARCHITECTURE.md |
How flow.h is generated, the module map, and the portable-core / POSIX boundary. |
CONTRIBUTING.md |
Build/test workflow, the amalgamation rule, and conventions. |
SECURITY.md |
Security policy and the flow_load untrusted-input surface. |
CHANGELOG.md |
Release history. |
Concepts and API shape are modeled on xyflow /
React Flow (MIT, webkid GmbH) — flow is an independent C reimplementation and includes
no xyflow code. The terminal raw-mode and escape-sequence approach was inspired by
tuibox (Cubified) and implemented independently from
the standard termios idiom and ANSI/SGR escape sequences.
MIT — see LICENSE.
