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.