Lini
Pretty diagrams from plain text, with fine-grained control.
cat -> dog -> bird
One line is a complete diagram: three boxes, two arrows, sensible spacing. You place the boxes; Lini routes the links. The same syntax scales to the polished scene below.
Thirty-odd lines of Lini (samples/hero.lini).
Why Lini
Lini handles the fiddly part of a diagram — drawing the connectors — and leaves the layout to you. Arrange nodes in rows, grids, or by anchor; name any two and Lini routes a clean orthogonal path between them, staying clear of everything in the way.
- You place, Lini connects. Routing is automatic, orthogonal, and rounded, with a clearance it won't cross. Force a side when you want to steer one.
- The look is yours. Sizes, anchors, strokes, shadows, rotation, gradients, and raw SVG paths render exactly as set — never filtered through a theme.
- Charts from data.
layout: chartplots bars, lines, areas, scatter, radar, and pie straight from numbers, working out the scales, ticks, and colours for you. - Sequence diagrams.
layout: sequencereads your wires as time — participants across the top, messages top-to-bottom, with activation bars,loop/opt/altframes, and notes, all from the links you already write. - Engineering drawings.
layout: drawingturns profiles drawn with a pen (draw: move(…) up(10) fillet(3) …) into dimensioned sheets: parts mate against each other, and every dimension's value is measured from the geometry — mirror a half-profile, pattern a hole, break a long bar, and the numbers stay true. - Small, and quick to learn.
{ }for style,[ ]for children, a few sigils, andcat -> dogis already a diagram. Parenthesized(…)expressions add compile-time math, baked to literals. - One fast binary. About 1.5 MB, no Node or browser, compiling a typical diagram in a couple of milliseconds — and byte-identically each run, so SVGs diff cleanly in CI. Hundreds of tests, property tests on the router included, keep it honest.
- Good colour for free. Eleven OKLCH-tuned hues in five tiers, gradients at a flattering angle, and automatic dark mode — every colour a
light-dark()variable, no hex to pick.
Install
|
Building from a clone instead? cargo install --path .
A tour of Lini
A diagram reads like a CSS file. A { } stylesheet at the top sets defaults, declares reusable classes, and extends nodes; then come the instances, then the links:
{ // the stylesheet — pure setup, draws nothing
clearance: 10; // clearance cascades to every link
|-| { stroke: --gray-deep; } // |-| styles every link's wire
.hot { fill: --red-wash; stroke: --red-ink; } // a node class
.loud { stroke: red; stroke-width: 2; } // a class — for a link's wire or a node's outline
|db::cyl| { fill: lightyellow; } // a new type from the cylinder primitive
}
|box#api| "API"
|box#queue| "Queue" .hot { radius: 8 } // a node wears its class after the label
|db#store| "Postgres"
api -> queue "enqueue"
queue -> store .loud "persist" // a link wears one after its endpoints
store ---> api "ack" // dotted arrow
Containers lay their children out. Style sits in { }, children in [ ]; pick a mode and they flow, grid, or anchor:
|group#services| "Services" { direction: row; gap: 24 } [
|box#api| "API"
|box#auth| "Auth"
]
A flow orients with direction: row or column; a grid is sized by columns / rows and placed with cell: / span:; plus pin and translate to lift a child out of the flow.
Nodes
|hex| "hex" { width: 82; height: 72 }
|cyl| "db" { width: 78; height: 78 }
|poly| { points: 0 -34, 32 11, 20 34, -20 34, -32 11; }
|path| { path: "M -34 6 C -34 -34 34 -34 34 6 C 20 34 -20 34 -34 6 Z"; }
Block (the bare frameless rectangle), oval, hex, slant, cylinder, diamond, polygon, line, icon (a Phosphor symbol — |icon| { symbol: heart }, painted like a node), and image, plus path for anything else. Text is not a primitive: a bare "…" is content — styleable in place ("x" { color: red }) — and |block| is the frameless box for a label that needs an id or a link. Templates (box, rect, group, caption, footnote, badge, row, column, grid, table, cell, header, footer, entity, sign) bundle common patterns over a base type, and you can define your own from any base: |panel::group| { stroke: --accent; }. A |table|'s first row is an automatic |header|; an |entity| is a two-column ER/database card (a |header| title + "field" "type" rows) wired with the crow's-foot operators (a -< b).
Charts
Give a node layout: chart and it becomes a plot, drawn from data instead of pixels. Hand it some numbers and it sorts out the scale, the ticks, the gridlines, and a colour per series, then lowers the whole thing to the same primitives as everything else — so a chart themes, bakes, and diffs exactly like the rest of a diagram.
|chart| "Revenue ($M)" { categories: "Q1" "Q2" "Q3" "Q4" } [
|bars| "2023" { data: 12 19 15 25 }
|bars| "2024" { data: 18 24 22 31 }
]
|bars|, |line|, |area|, |dots|, and |bubble| share one x/value plane; |slice| makes a pie or donut. direction: radial bends the plane into a radar and direction: row lays it on its side, with no change to the data. A series reads either data: (plain numbers, or x y points) or fn: — a formula sampled over the domain, using the language's own compile-time math. Axes auto-fit or take a range:, run linear or log, and you declare an |axis| only when you want to say something; shade a zone with |band|, drop a threshold or callout with |mark|. Label individual points with tags: and they place themselves without colliding — on the plot where they fit, on hover where they don't (tooltip: none | hover | auto | always); size a point for hovering with marker: circle. The whole chart language is in SPEC.md §14.
Sequences
Give the scene layout: sequence and the diagram reads on a time axis: named participants line up across the top, each drops a lifeline, and the messages — ordinary links — fall top-to-bottom in the order you write them. No new syntax: participants are nodes, messages are links, frames and notes are nodes. Like a chart it lowers to the same primitives, so it themes, bakes, and diffs like everything else.
{ layout: sequence }
|box#user| "User"
|box#api| "API"
user -> api "POST /login" // a call — solid arrow, opens an activation bar
api --> user "200 + token" // a return — dashed
|alt| "valid" [ // a branch frame; |else| "…" splits compartments
user ~> api "log event" // async — wavy
]
The operator picks the message: -> a call, --> a return, ~> async, and a -> a a self-message. A call opens an activation bar on its target and the matching return closes it — nesting stacks, automatically. Wrap a span of messages in |loop|, |opt|, or |alt| (with |else| compartments) to frame it, and a frame only groups — its messages still wire the outer participants. Drop a |note| "…" { over: a } (or { left: a } / { right: a }, or over: a b to span). The whole sequence language is §10 of SPEC.md.
Icons
Built-in Phosphor icons, drawn as inline SVG paths — no icon font, no external files. |icon| { symbol: heart } paints like any node: fill is the body, stroke the line, stroke-width counter-scaled so the weight stays even at any size. |sign| is a larger preset and an ordinary node — it carries a label, wears a colour class, and wires up like a box.
|icon| .teal { symbol: user } // two-tone, via a colour class
|icon| { symbol: cloud; fill: none; stroke: --sky-deep } // single-tone line
|icon| "bell" .amber [ "3" ] // symbol via the label, "3" a badge
|sign#svc| "gear" .purple [ "Service" ] // larger, labelled, and linkable
Only the symbols a diagram uses are embedded, so the full set never bloats a small file.
Links
Connect two nodes by id and Lini finds an orthogonal path through the free space, keeping a configurable clearance from every node and link, rounding the corners, and landing the arrowhead on the edge. One knob (clearance, default 16) sets spacing for the whole diagram.
The operator is the link's look, written [start][line][end] with no spaces:
| Line | Markers | ||
|---|---|---|---|
- solid |
-- dashed |
> arrow |
* dot |
--- dotted |
~ wavy |
< crow |
<> diamond |
So -> is a solid arrow, <-> is bidirectional, --* a dashed line ending in a dot, ~> a wavy arrow. Endpoints support fan-out, fan-in, and cartesian fans with &, and dot-paths into nested containers (closet.outlet -> fridge.inlet). Routing is automatic but steerable: name a side (a:right -> b:left) to force where a link leaves or arrives. Labels ride the link and slide to clear nodes; the link never moves for a label.
The full routing contract (crossings, priority, self-loops, starvation) lives in ROUTING.md.
Entities
An |entity| is an ER / database card — a titled field list built on |table|. Its label is the centred title; each row is "field" "type", or "key" "field" "type" when you add a third column for a PK / FK gutter ({ columns: auto auto auto }). Relationships are ordinary links drawn with the crow's-foot operators, so a whole schema is a handful of lines:
| Op | Reads | Op | Reads |
|---|---|---|---|
-+ |
one | -o+ |
zero-or-one |
-< |
many | -+< |
one-or-many |
-++ |
exactly one | -o< |
zero-or-many |
Each end-marker composes [min][max] — an optionality ring o (zero) or bar + (one) hugs the line, the crow < (many) or a bar + (one) sits at the entity — and it works on either end (a +-< b is one-to-many). Colour an entity like any node (stroke: --blue-ink; fill: --blue-wash), or leave it in the default palette.
Entities lay out in any container today — grid, flow, or free-positioned with pin / translate (as above). They'll pair best with layout: auto, an automatic graph layout on the roadmap.
Engineering drawings
Work in progress — the language is specified; the engine is landing incrementally.
layout: drawing turns a profile drawn with a pen (draw: move(…) up(10) fillet(3) …) into a dimensioned technical sheet: parts mate against each other, holes and patterns punch through, a half-profile revolves into a turned part (every shoulder drawing its edge line), a thread: dresses a surface with the ISO minor lines and composes its own M20×1.5 callout, a long bar breaks to fit — and every dimension's value is measured from the geometry, so the numbers stay true when the model changes. Dimensions live in the ( ) bracket — (-) a linear span, (o) a diameter or radius, (<) an angle — with leaders, datums, and hatched sections annotating the rest. An ISO 5457 |page| (frame, zone references, seated |title-block|) hosts the views at true millimetre scale. It lowers to plain SVG like every other layout. See SPEC.md §15.
Colour
Pretty by default. A curated palette of 11 named hues — red rose orange amber lime green teal sky blue purple gray — each in five job-named tiers, so the easy path is the flattering one:
{ |card::box| { fill: --teal-wash; stroke: --teal-ink } } // a soft card, one line
|box#n| { fill: --amber-soft }
|box#hero| { fill: gradient(--rose, --amber, --sky) } // a three-colour blend
- Five tiers per hue —
wash(palest, for backgrounds),soft, the bare name (the everyday pastel),deep(the strong tone, for borders and strokes), andink(for text and emphasis). The names hold across the dark flip:--teal-inkis the high-contrast tone in both modes, where alight/darkname would invert. - OKLCH under the hood, so the ramp is perceptually even and the eleven read as a family. Pick any colour the same way —
fill: oklch(0.7, 0.14, 200)— and conventional names still land (--yellow→ amber,--pink→ rose). - Gradients —
gradient(--rose, --sky)blends two hues at a flattering angle (any two look good); add stops for a multi-colour wash,linear-gradient(135, …)for a custom angle, orradial-gradient(…). Works on fill and stroke. - Everything flips and bakes. Hues and gradient stops are
light-dark()variables, so a colour follows dark/light like the rest and freezes to a literal under--bake-vars. Only the colours a diagram uses are emitted, so a big palette never bloats a small file.
Theming
One SVG, both palettes. Every colour is a light-dark() pair, so an exported SVG carries both and switches on its own — it follows the viewer's OS (prefers-color-scheme) with no script or @media, and a data-theme="dark"/"light" attribute on the SVG or any ancestor overrides it.
Defaults sit in @layer lini.defaults, so unlayered host CSS wins — no !important, no rebuild:
} /* recolour every diagram on the page */
Geometry is always baked in, so a theme only ever changes colour — layout never depends on the host.
Three built-in themes — light, dark, and high-contrast — pin a single palette at export time:
lini theme NAME prints any as a ready-to-edit --lini-* file; --bake-vars flattens it to literals for non-browser renderers (resvg, librsvg) and email. Every lini-* class is a stable styling hook.
The default font is a monospace stack (ui-monospace, "SF Mono", …, monospace): it reads crisp and keeps text sizing accurate. Swap it with --lini-font-family in the diagram, a theme, or the page's CSS.
The CLI
lini [options] <input.lini>
lini fmt [--check] [--stdout] <input.lini>
lini serve [--port N] [--bake-vars] [PATH]
lini desugar <input.lini>
lini theme [NAME]
| Flag | Meaning |
|---|---|
-o, --output FILE |
Output path (default: stdout). |
--format svg|html |
Raw SVG (default), or wrapped in a minimal HTML page. |
--bake-vars |
Inline var() references — for resvg, librsvg, raster, email. |
--theme NAME|FILE |
A built-in theme (dark, high-contrast, …), a CSS file, or a light/dark pair. |
--check |
Parse and validate only. |
--watch |
Recompile on every change (with -o). |
--no-warn / --strict |
Silence lint warnings, or promote them to errors. |
Errors are LSP-formatted (file:line:col: error: …) and suggest fixes: an unknown endpoint asks did you mean kitchen.counter.bowl?. lini serve runs a live preview at localhost:7700 — a single file, or a folder as a playground; lini desugar prints a file with its sugar expanded, for teaching and debugging.
Playground
lini serve is also a browser playground. Point it at a folder and it lists the .lini files inside; pick one from the dropdown to open it in a small editor — source on the left, the diagram rendering live on the right.
Syntax highlighting, a draggable split, and light/dark themes (it follows your system by default). Ctrl/Cmd-S renders the current buffer; Save writes it back to the file.
Performance
Measured end-to-end on a modern laptop, including process startup (--bake-vars, output discarded):
| Diagram | Time |
|---|---|
| One node | ~1.6 ms |
| Realistic service diagram (9 nodes, 5 links) | ~2.2 ms |
| Dense scene (100 nodes, 90 routed links) | ~50 ms |
A single-pass parser, bottom-up layout, and an orthogonal router. No browser to spin up.
Where Lini fits
| Lini | Auto-layout tools* | |
|---|---|---|
| Placement | you control (flex / grid / anchors) | automatic |
| Link routing | automatic, orthogonal — steerable sides | automatic |
| Visual control | full SVG (CSS vars + classes) | theme presets |
| Runtime | single native binary, written in Rust | varies (Node, browser, JVM, …) |
*the common auto-layout diagram generators that place nodes for you from a text description
Reach for Lini when you already have a layout in mind — a grid, a top-down flow, framed groups — and want it to look that way without drawing the connectors by hand.
Architecture
A linear pipeline, each stage independently testable:
lex → parse → resolve → layout → route → render
Parsing is recursive-descent over an LL(1) grammar; resolve applies CSS-like specificity (inline beats class beats default) and expands user types; layout sizes bottom-up; the router solves links orthogonally against a clearance contract; render emits semantic SVG. The full language is specified in SPEC.md.
Status
v0.17. The language (the box/text model in SPEC.md) is stable, and the pipeline is complete and tested: links route and render, layout and theming work, charts plot from data (SPEC.md §14), sequence diagrams read the wires as time (§13), ER entities wire with crow's-foot cardinality (§8), and the formatter and dev server ship in the same binary. Engineering drawings (§15) are built through the sheet: turned profiles (revolve:, thread:, hidden geometry), measured dimensions with ISO arrowheads, and the ISO 5457 |page| with its title block — sections and detail views are the next round (DRAWING-0.18.md).
Development
samples/ holds a .lini per feature area; tests/conformance.rs snapshots their SVG with insta, and tests/laws.rs asserts the router's laws on every scene.
License
MIT — see LICENSE.