An infinite canvas in the terminal. Boxes, arrows and text, placed and moved with the mouse — and written out as ASCII, so the drawing can live in a README or a comment rather than only on the screen it was made on.
Reads and writes JSON Canvas (.canvas), the format
Obsidian's Canvas uses, so a board can round-trip through Obsidian — negative
coordinates, groups, file and link cards included, every coordinate saved back
exactly where the other tool put it.
brew install azihsoyn/tap/ratslate # Homebrew (macOS/Linux)
cargo install ratslate # or from crates.io
ratslate board.canvas # then: drag empty space to draw a boxNeeds a terminal with mouse reporting (most modern ones: iTerm2, kitty, Alacritty, WezTerm, GNOME Terminal, Windows Terminal, tmux).
- Boxes — drag empty canvas to place one, type into it, double-click to
edit.
ccycles colors, or use the●button for a real color picker — a truecolor gradient field with a hue strip and live preview, plus the six JSON Canvas presets — border styles: plain, rounded, thick, double or dashed (xcycles them too) — and how the color is applied: outline and text only, a tinted background, or solid (fcycles; text goes black or white to stay readable). - Connectors — shift+drag from one box to another. Sides, arrowheads,
colors, labels, line styles (solid
──, rounded bends╭╯, thick━━, double══, dashed╌╌) and arrowhead glyphs (>▶▷●◆) all editable; JSON CanvasfromSide/toSide/fromEnd/toEndrespected. - Tables — a box whose text is a GFM markdown table renders as a real
grid. Double-click a cell to edit it spreadsheet-style (Tab/Enter/arrows,
+col -col +row -rowbuttons, Alt+Enter for a line break, Ctrl+Z inside the session). Still plain markdown in the file — Obsidian renders it as a table. Connectors can anchor to a table's row, column or single cell, and stay anchored when things move. - Groups — draw a box around things, press
g. Moving the fence moves everything inside it. - Stacking — boxes are opaque and stack in JSON Canvas node order.
[/]move a selected box one step back/forward,{/}all the way; the API spells itreorder. The order round-trips through Obsidian unchanged. - Duplicate —
Dclones the selection (a single box or a whole multi-selection), copies landing just offset from the originals and already selected to drag away. A connector between two copied boxes comes along. - Files & links — JSON Canvas file/link cards:
oopens them (creating the file if it doesn't exist yet),ycopies the path or URL. - Images — a file card pointing at an image (
.png,.jpg,.gif,.webp,.bmp) shows the actual picture, scaled to the box and moved or resized with the mouse like anything else — the same image cards an Obsidian canvas has. Needs a terminal with image support (kitty graphics or compatible: kitty, Ghostty, WezTerm, recent iTerm2); elsewhere the card falls back to a labeled box. Drop one on a board with--api '{"type":"set_file","id":"…","file":"photo.png"}', or just open a canvas that already has image cards.
Selecting a box lights up every connector wired to it, and selecting a connector lights up the two boxes it joins — so a board with many crossing lines stays legible: click a thing to see what it's tied to.
The canvas is an infinite plane: pan with arrow keys or the mouse wheel, and
the minimap in the corner (toggle m) shows where everything is — click or
drag it to jump. Press / to search: type to filter boxes by their text and
the view jumps to the first match, Enter steps through the rest.
Press l (or send {"type":"layout"} through --api) for auto-layout: a
layered left-to-right arrangement of whatever's connected to what — so an
agent can pour in nodes and edges without computing a single coordinate, and a
hand-drawn board can untangle itself. One undo step.
ratslate board.canvas --import graph.mmd # a Mermaid flowchart
ratslate board.canvas --import graph.dot # or a Graphviz DOT digraph
cat graph.mmd | ratslate board.canvas --import -A Mermaid flowchart (graph TD, flowchart LR, A -->|label| B, node
shapes) or a DOT digraph (A -> B [label="…"]) becomes an editable
board, laid out with the same pass the l key runs. So a graph that
already lives as text — in a doc, a README, an agent's output — opens as
boxes you can nudge with the mouse, then goes back out as --render
ASCII or a JSON Canvas for Obsidian.
The --import flag is sugar for one request: an agent can paste a graph
straight through the API with --api '{"type":"import","text":"graph TD\n A-->B"}'.
ratslate before.canvas after.canvasMultiple files open as tabs sharing a single camera — flip with
Tab/Shift+Tab (or click the bar) and the whole board swaps under an
unmoving view, so two versions drawn at the same coordinates compare
like frames of a flip-book. T clones the active board into the next
free <name>-2.canvas to start an "after" from a finished "before".
Every file stays a plain, standalone JSON Canvas.
ratslate board.canvas --renderprints the whole board — tables, connectors, groups — exactly as the TUI draws it, ready to paste into a doc, a PR, or a commit message.
Every mouse drag and keystroke is also a Request value, and there is
exactly one function that applies one: dispatch. --api is that
same function reached from the command line instead of a terminal —
not a second implementation of what a move or an edit means.
ratslate board.canvas --schema # what a request/response looks like
ratslate board.canvas --api '{"type":"place","x":2,"y":2}'
ratslate board.canvas --api '[{"type":"set_text","id":"n1a2b3c4d","text":"hello"},{"type":"save"}]'
ratslate board.canvas --api '{"type":"render"}' # the ASCII render, as JSON
ratslate board.canvas --api '{"type":"set_status","id":"n1a2b3c4d","status":"running"}' # ⠋ on the box; "ok" ✓, "failed" ✗, null clearsA JSON array runs as a batch, applied in order, replied to in order.
Nothing is written to disk until a save request says so — --api is
safe to use for a read-only state query.
ratslate board.canvas --watch 'some-command' --every 5runs the command on a timer and reads its stdout as a sync document:
a list of things to show, each with a stable key, and the links
between them. Ratslate matches items to boxes by key and updates them
in place — a box keeps wherever you dragged it when only its text or
status changed, a new key becomes a new box (and the board re-lays
out), a key that's gone is pruned if the document says so. Boxes you
placed by hand, which have no key, are never touched. Each document is
one sync request through --api, so a script can send the same thing
by hand, and the exact schema is in --schema under SyncDoc.
{
"items": [
{"key": "job:build", "text": "build\n21s", "status": "ok"},
{"key": "job:test", "text": "test", "status": "running",
"color": "4", "fill": "tint", "shape": "rounded", "width": 24, "height": 4}
],
"links": [
{"from": "job:build", "to": "job:test", "label": "needs",
"style": "dashed", "arrow": "triangle", "color": "2"}
],
"layout": "auto",
"prune": true
}Only key is required; an omitted field leaves the box as it was, so a
source that knows only statuses can send just keys and statuses. A box's
key is saved as ratslateKey — an extension key other JSON Canvas
readers ignore — so a watch picks the same boxes back up tomorrow.
layout is auto (lay out when the set of keys changes) or keep
(never move anything; new boxes land below the board). prune is off
by default, so a partial document is safe.
examples/gh-actions-board.py is a watch source: it prints a GitHub
Actions run as a document — jobs as items, needs: as links, each
job's status as its badge — and ratslate ci.canvas --watch examples/gh-actions-board.py --every 8 draws the run live as it goes.
It's a stateless script that prints JSON, which is the point — see
examples/README.md.
Ratslate's own emphasis color — selection, related connectors, the flow
pulse, the spinner — is cyan; if that sinks into your palette, set
RATSLATE_ACCENT=#ffd75f (any hex) and everything emphasized uses that.
Set RATSLATE_OPENER to any command and o hands it what's selected —
a box's text, a file's path, a link's URL, or the exact table cell
under the cursor. The opener decides what "open" means: jump to a
terminal pane, open a ticket, anything nameable in text. Unset, o
opens file and link boxes through the OS as usual.
Edits sync through a CRDT sidecar (.canvas.crdt, yrs):
a person dragging boxes in the TUI and an agent placing them through --api
merge live, field by field, instead of overwriting each other's saves. The
.canvas file itself stays clean JSON Canvas.
And if the .canvas is edited by another tool entirely — Obsidian, or a
script rewriting the file — ratslate notices and reloads it live, so the
board on screen keeps up with the file on disk.
A few things move so the board reads at a glance. Press a and a pulse
travels along every connector toward its arrowhead, so direction is
obvious even on a dense board. An agent can mark the box it's on with
set_status — running puts a spinner in the box's top border, ok a
check, failed a cross. And a box another writer just changed flashes
for a moment, so you see what the agent did and where. None of this
costs anything while nothing is moving: the screen only redraws on a
timer while something animates.
brew install azihsoyn/tap/ratslate # Homebrew (macOS/Linux)
cargo install ratslate # or from crates.ioMIT or Apache-2.0, at your option.
