Skip to content

Repository files navigation

flow

CI license: MIT single-header C99 release

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.

flow running in a terminal: web-server, cache, and database nodes joined by dashed orthogonal edges, with a keybinding header, a minimap-style devtools HUD reporting node/edge counts and undo depth, and a status bar of shortcuts.

Try it: make && ./demos/topo — a small network-topology editor (live pan, mouse drag, zoom, minimap). ./demos/hello_flow is the minimal showcase (shown above).

Features

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 of nodeTypes/edgeTypes.
  • Handles, connect & reconnect, with a pluggable connection validator (flow_set_connection_validator, the analog of isValidConnection).
  • 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).

Install

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.)

Quickstart

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 app

Embedding in your own event loop

flow_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 own read(), a socket, a scripted sequence). Same key/mouse parser flow_run uses.
  • 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, then free() it. It returns "" when nothing changed. (flow_present is exactly this plus fputs(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).

Requirements & platform

  • 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-compiled flow.o links 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.h requests it (_DEFAULT_SOURCE) before its own includes, so in the implementation TU include flow.h before 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.

Developing

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)

Documentation

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.

Credits

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.

License

MIT — see LICENSE.

About

xyflow for terminal

Resources

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages