Apps
Codegraf
# codegraf — visual node-graph playground
Blender-geometry-nodes meet Scratch: build a graph out of nodes that
reference functions, wire them with bezier links, press Run, and
save the whole thing as an executable Deft module.
```
┌──────────┐ ┌──────────┐ ┌──────────┐
│ n1 2 ├───────►│ n3 + ├───────►│ n4 output│
└──────────┘ │ │ └──────────┘
┌──────────┐ │ │
│ n2 3 ├───────►│ │
└──────────┘ └──────────┘
````
## Status
Engine (graph model, evaluator, code generator, node-pack loader) and
the GL canvas editor are implemented. Requires a GL host (`retro` or
`hydra`); `retra` (ANSI) shows a notice — the engine still runs
headless. Launch it with the app launcher, or copy
`workspace-example.dft` into `.deft/workspaces/` and open
`retro workspace codegraf`.
## Canvas controls
| Input | Action |
|-------|--------|
| click node | select (drag to move; snapped to a 10px grid) |
| drag a node's handles | resize it (stored as per-node `:w`/`:h`; handles snap to whole cells on real hosts) |
| Ctrl + click node | add/remove from the selection |
| drag a selected node | moves the whole selection |
| hover a port | the port under the cursor gets a yellow halo (focused internals, frame edges, and node ports) |
| double-click an input socket | remove that input from the group/function — frame edge socket, the input node's own socket, or a consumer port mapped to it; repeat/if inputs are structural and stay |
| double-click a node body | open the name editor (same as `n`); works on collapsed and expanded frames |
| drag from an output dot | start a wire; drop on an input to connect |
| drag from a wired input dot | detach and re-drag the wire |
| drag empty canvas | pan |
| Shift + drag empty | marquee select |
| wheel | zoom at the cursor |
| `a` / Shift+A | node palette (dialog search by category) |
| `F` | Functions dock — tiered function selector (palette → loaded defs → module prefixes; `/` drills, Tab cycles tier, Enter adds) |
| Tab / z | expand / collapse the selected group in place (Blender-style) |
| `l` | reflow the focused (or selected) group's internals left→right |
| `e` | edit a port/param value of the selected node |
| `i` | add a typed input to the selected group/region (or focused frame); collapsed groups open first. Type a name, Tab cycles the type, Enter adds |
| `B` | fold the focused function's boundary: inputs hook directly onto their consuming ports and the output directly onto its producer (removes the `input`/`output` nodes) |
| `r` | Run (per-node value/error badges appear; failures print in the red status bar at the bottom) |
| `f` / Home | fit view to the graph (uses the content area, so an open dock is excluded) |
| `n` | with one node selected: name it (or, for a group/region, rename its function; blank clears a plain node). With several selected: name the group — it becomes a function node |
| `N` | new graph |
| `x` / Delete / Backspace | delete the selected node |
| arrows | nudge the selected node |
| Shift + arrows | pan the view |
| Esc | clear selection / cancel drag |
| Ctrl+A | select all nodes |
| Ctrl+S / Ctrl+O | save / open a graph module |
| `?` | about |
Edited values render on the node body: dim text is the type default, bright text is your override (`value 5`, `fn str/upper`, `url …`). Ports show their wired name only when connected. String/function params take the text as typed; number/bool/list/map params parse Deft literals (bare-word typos on string ports are treated as text, not keywords). If the text isn't a literal it is built as an expression: `$y=2` on a condition port adds an `:eq` node fed by the node named `y` (or `$x` / another node's name), replaces any existing edge into that port, and wires the result in. `=` is accepted as `==`; calls, brackets, and other operators work too.
Menu ("Codegraf") and the pie menu mirror these commands. Graph files
default to `graphs/<name>.dft` (config `codegraf/graph-dir`). Middle-mouse
panning is not possible yet: the host's mouse events don't carry the
button, so canvas navigation uses left-drag / Shift+arrows / wheel.
## From code to graph (Action Graph)
In the editor, put the cursor inside a `def` (or select one) and choose
Graph ▸ Open Action Graph: the editor finds the codegraf panel (or
splits one open), and codegraf imports the def's source into a graph you
can inspect, rewire, run, and save. The same import is available as
`[app/open-def %{ :name … :source … :helpers … }]` (an `@rpc` method) and
headlessly as `[import/from-source $text %{ :name $def-name }]`.
The importer:
- turns each call into a node whose ports are the method's real
parameters, derived from the runtime (`info/props`): port names,
types, optionality, descriptions;
- turns `set` bindings into nodes and `$var` references into wires,
literals into node params, and the final expression / `return` into an
`output` node;
- turns the def's parameters into typed `input` nodes and wraps the whole
thing as a function (the canvas focuses it so the internals show);
- resolves sibling defs in the same file from their signatures, and
falls back to an `apply` node with a warning for anything unknown.
Anything needing a type no builtin/pack provides is stored in the
document's `:types` map as a serializable spec, so saved graphs reload
with full type information.
Control regions (Scratch-style): a block `if` and a collect-style `for`
(shown as repeat) import as an enclosing frame — the body nodes live inside the region,
reads/writes of outer variables stay implicit inside, and only the state
(and `items`) ports appear on the frame edge. The header shows the kind and
its slots (`if`, `repeat item in items`); the condition subgraph is drawn
inside. `repeat` takes either an integer count (repeat n times, the loop
variable is the 0-based index) or a list of items; `[range n]` in a source
def imports as a count with no range node. It recognizes the `set acc @{}`
+ `$acc << expr` idiom and emits `[collect [map … {| item | … }]]`; `if`
emits an inline conditional and threads state through its branches.
Current limits (v1): `while`/`match`/`try`, lambda arguments, and
bare-word values are reported in the import result's `:warnings` and
rendered as `note` nodes where possible, so the rest of the graph still
imports.
## What a node is
A node is a reference, not a copy of the code. Its identity is its
method — the callable it invokes (`:upper`, `:map`, `:math/sqrt`,
`:fs/read`) or a structural id (`:input`, `:output`, `:note`,
`:value/number`) — and its palette `:category` is separate metadata:
| Method | References |
|------------------|--------------------------------------------------|
| `:apply` | any function by name (`"str/upper"`, `"sheet/…"`)|
| `:rpc` | a live app's `@rpc` method (`[rpc runtime method]`, async) |
| `:sqlite/query` | a SQLite file + query |
| `:fs/read` | a file |
| `:get` | decode a map field (`:keys`, `:values` too) |
| `:http/request` | an HTTP request, async (`%{ :status :body }`) |
| `:input` | a function parameter (imported defs) |
| `:add` / `:eq` / `:not` … | infix operators (the symbol is the label) |
| built-ins | math/logic/string/list operations |
| node packs | functions contributed by a package (see below) |
Methods not in the palette resolve on demand from the runtime, so any
loaded function can back a node (add it from the Functions dock).
Wires carry values (a dataflow DAG). Each input port takes at most one
edge; an unwired port uses the node's own literal value. The graph
evaluates in topological order; cycles and `%Err` values become visible
per-node errors and block their downstream consumers.
Async nodes (`:http/request`, `:rpc`) return Futures. Run resolves
them in a spawned coroutine: the node and its consumers show `… running`
badges while pending, then fill in their values. The panel stays
responsive, and starting a new Run supersedes an in-flight one.
## Groups — reusable functions
Select several nodes (drag a box, or Ctrl+click to toggle) and press `n`:
name the group and it collapses into a single function node whose
inputs are the wires entering the selection and whose output is the one
edge leaving it (or the selection's output/sink). The function is saved
in the document (`:functions`) and appears in the add palette (`a`)
under `group`, ready to drop as many times as you like — at runtime it
evaluates its internals, and in generated modules it becomes a real
`def <name> {| in-… | … }` that other Deft code can call.
Expanded boxes: select a group node and press `Tab` (or `z`, Menu →
Expand/collapse group, or the pie) to expand it in place. The node
vanishes and a bounding frame takes its place — same position, and its
edge ports sit where the node's ports were, so existing wires link up
with the frame. The internals render inside; synthetic `in`/`out` boundary
nodes expose the function's inputs/output for mappings that don't already
target explicit input/output nodes (imported defs show just their own
`name`/`result` nodes, with dotted bezier hooks linking the frame's edge
sockets to those nodes). Any number of
groups can be expanded at once.
- Selecting among boxes: click a frame (or marquee over it) to
select + focus that group; the focused frame is highlighted. Clicking
an internal node of another box focuses that box and starts editing
there.
- Deleting: with a group selected (frame or box), `x`/Delete removes
it from the canvas; a function definition is pruned once no node
references it any more (other instances keep it alive).
- Moving/resizing: drag a frame's title bar to move the group (the
internals travel with it); drag any of the focused frame's eight
handles to resize it — the internal nodes' positions and sizes
scale with the frame (persisted as per-node `:w`/`:h` overrides).
- Editing: move nodes, rewire, add/delete internals (the `in`/`out`
ports themselves are fixed); edits commit straight back into the
function definition. Wires to/from the group attach to the frame's
edge ports (drag from a frame port to rewire externally).
- Reflow (`l`) auto-layouts the focused group's internals in layers
(columns by dependency depth, rows in order) and re-anchors the
frame; if a collapsed group is selected, `l` expands and reflows it.
- Fit (`f`/Home) frames the focused group when one is focused, else
everything visible (frames included). `Tab` collapses the focused
group; `Esc` unfocuses but keeps the box expanded; the status bar
shows the scope (`graph > function`).
- Run evaluates the top-level graph (badges are top-level), so run/see
badges from the top; palette adds (`a`) go to whichever scope is
focused.
Limits: a group exposes one output (wire the rest inside), nested groups
work (a group node can be part of a new group), grouping itself must
happen at the top level, and async nodes inside a group are not
supported yet.
## Graph documents are Deft modules
Saving writes `graphs/<name>.dft`:
%CodegrafGraph{
:codegraf 2
:name "pipeline"
:nodes %{
:n1 %CodegrafNode{ :id "n1" :params %{ :n 5 } :type :range … }
…
}
:edges @{ %CodegrafEdge{ :src "n1" :src-port "items" :dst "n2" :dst-port "items" } … } }
# ==== GENERATED — codegraf rewrites below on save ====
def n1 {|| [range 5] }
def n2 {| in-items | [map $in-items {| $it | [apply "str" $it]}] }
def run {|| … }
- The `%CodegrafGraph{}` block is the document (hand-editable; loading
only ever parses this block, never evaluates code).
- Deriving a node from a function the palette doesn't cover records a
`%CodegrafNodeTypeSpec{}` in the document's `:types` map, so the graph
stays self-describing even when that function isn't loaded later.
- Everything below the marker is regenerated from the in-memory model on
every save — deterministic, key-sorted, diff-friendly.
- Named nodes (`n` on a selected node) become their own `def`/variable in
the generated code (`def total {| in-a in-b | … }`, `set v-total …`);
names are sanitized and blanks fall back to `n<id>`.
- The module is importable: `run` returns the last output/sink value and
each node is its own `def n<id>`. Other Deft code can `source` or
`import` the module and call it.
- Async nodes are emitted with an inner `await` (`def n3 {|| [await [http/get …]] }`),
so calling `run` from an imported module still returns the value.
See `examples/pipeline.dft` for a complete example.
## Node packs — modules that provide functions
Any package tagged `codegraf-nodes` whose entry module implements the
`CodegrafNodesExtension` contract is discovered at startup
(`src/packs.dft`, on top of `deft/stdext/plugin`):
# package.dft %Package{ owner "deft" name "my-nodes" tags @{ "codegraf-nodes" } … } # src/my-nodes.dft import "deft/codegraf/types" as types implements CodegrafNodesExtension def key :my def label "My nodes" def version "0.1.0" def nodes "my-node-list" # name of an exported fn def my-node-list {|| @{ [types/node-type "my/double" "double" :math "#5e97f6" @{[types/port "value" "value" :number]} @{[types/port "value" "value" :number]} @{[types/param "value" "value" :number 1]} $ev-double $em-double] } } def ev-double {| inputs params | ([get $inputs "value"] * 2)} def em-double {| inputs params | [str "(" [get $inputs "value"] " * 2)"]}
`:eval` receives value maps; `:emit` receives code strings and
returns Deft source for the generated `def`. `types/node-type` takes the
method as its first argument. Packs can shadow built-in methods
deliberately. `packages/codegraf-nodes-demo` is the shipped
template.
## Module API (for scripts and tests)
| Module | Purpose |
|--------|---------|
| `deft/codegraf/graph` | pure document model: add/remove/connect/disconnect/move/set-param, `topo-order`, `validate` |
| `deft/codegraf/library`| built-in node types (`builtins`, `builtin-list`) |
| `deft/codegraf/packs` | discovery + `all-types` (builtins overlaid with packs), `type-list` |
| `deft/codegraf/eval` | `run` → `%CodegrafRun{ values status errors }`, `value`, `status-of`, `error` |
| `deft/codegraf/codegen`| `module-text` |
| `deft/codegraf/module` | `save`, `load`, `parse-text`, `read-block`, `coerce` |
| `deft/codegraf/literal`| deterministic data-source emission |
| `deft/codegraf/types` | node/port/param deftypes, specs, pack contract |
| `deft/codegraf/resolve`| derive types from `info/props`; `add-missing`, `used-specs` |
| `deft/codegraf/import` | code → graph: `from-source`, `from-file`, `local-type` |
| `deft/codegraf/fns` | Functions-dock data: tiered/scoped/filtered selector entries |
| `deft/codegraf/regions`| control-region eval/codegen (if, repeat) |
| `deft/codegraf/util` | small shared helpers |
## Configuration
| Key | Default | Purpose |
|-----|---------|---------|
| `codegraf/graph-dir` | `"graphs"` | default directory for saved graphs |
| `codegraf/auto-run` | `false` | re-run on every edit (UI milestone) |
| `codegraf/grid` | `true` | show the canvas grid |
| `codegraf/snap` | `true` | snap node positions to the grid |
## Tests
From the workspace root:
```
deft test packages/codegraf/test/test_codegraf.dft # engine + panel
deft test packages/codegraf/test/test_methods.dft # method types + specs
deft test packages/codegraf/test/test_import.dft # code → graph
deft test packages/codegraf/test/test_fns.dft # Functions-dock data
````
They cover the model, evaluator (defaults, errors, cycles, conditionals,
lists, function calls, async pending/resolve), literal escaping,
deterministic codegen, save/load round-trips (derived `:types` included),
generated-module execution, node pack discovery, view geometry, panel
state, runtime-derived method types, the importer, and the selector.