Render Tizen DALi UI C++ to a PNG and a structured JSON scene tree — built for AI agents and humans alike.
English | 한국어
Write a snippet of DALi (Tizen's Dynamic Animation Library) UI C++, and this CLI renders it headlessly inside a Docker container, then hands you back two things: a real PNG screenshot and a deterministic, machine-readable UI scene tree (every node's id, type, role, on-screen bounds, source line, and properties). You can then verify that render against a target image and/or tree and branch on the exit code. stdout is pure JSON, so it drops straight into an agent's parser.
LLM coding agents can write UI code, but they can't see whether it looks right. dali-ui-preview-cli closes that loop: an agent runs write → render → compare → rewrite, reading the structured tree first (cheap, exact, diffable) and the image second (for vision). No DALi SDK build is required on your machine — just Docker. The same loop is just as useful for a human eyeballing a layout in a terminal.
- Docker, usable by your user (the render preflight runs
docker info). - Node.js >= 18 (only to run the CLI itself).
- The runtime image auto-pulls on the first render (
ghcr.io/lwc0917/dali-preview-runtime, ~290 MB; DALi Toolkit + Xvfb for off-screen rendering).
Shared with the DALi Preview VS Code extension. This CLI uses the same runtime image and the same named volumes (
dali-preview-ccache,dali-preview-shader-cache) as the DALi Preview VS Code extension. If you already use the extension, the image and warm build caches are reused — no extra download, faster renders, and updating the image once benefits both.
The container is only needed for the render path. --version, --help, --list-versions, and the pure tree/overlay/diff logic do not require a live daemon.
Run it ad-hoc with npx (no install):
npx dali-ui-preview-cli <input.cpp> --image out.pngOr from source:
git clone https://github.com/lwc0917/dali-ui-preview
cd dali-ui-preview
npm install
npm run build
node out/cli.js <input.cpp>
# optional: expose it on your PATH as `dali-ui-preview-cli`
npm linkAll examples below use dali-ui-preview-cli; substitute node out/cli.js when running from a source checkout, or npx dali-ui-preview-cli.
Render a preview file and print its scene tree:
dali-ui-preview-cli samples/hello-dali.preview.dali.cppstdout is a single JSON line (pretty-printed and trimmed here):
{
"id": "0",
"type": "Layer",
"role": "panel",
"name": "RootLayer",
"mark": 1,
"bounds": { "x": 0, "y": 0, "w": 1024, "h": 600 },
"children": [
{
"id": "0/1",
"type": "FlexLayoutImpl",
"role": "container",
"mark": 3,
"bounds": { "x": 0, "y": 0, "w": 1024, "h": 600 },
"sourceLine": 13,
"flexProps": { "direction": "COLUMN", "alignItems": "CENTER", "justifyContent": "CENTER", "wrap": "NO_WRAP" },
"children": [
{
"id": "0/1/0",
"type": "LabelImpl",
"role": "label",
"text": "Hello, Dali!",
"mark": 4,
"bounds": { "x": 381, "y": 262, "w": 262, "h": 56 },
"sourceLine": 21,
"children": []
},
{
"id": "0/1/1",
"type": "LabelImpl",
"role": "label",
"text": "Edit this file to see the preview update",
"mark": 5,
"bounds": { "x": 251, "y": 322, "w": 522, "h": 22 },
"sourceLine": 25,
"children": []
}
]
}
],
"meta": { "resolution": { "w": 1024, "h": 600 }, "theme": "dark", "dpr": 1 }
}(The full tree also includes the two internal zero-area CameraActor siblings DALi inserts. A label's name is empty — its displayed text is in text.)
Also write the screenshot:
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --image out.png--image is optional and orthogonal to stdout: it writes the PNG but does not change the JSON.
The preview code can come from three sources (pass exactly one):
# 1. A FILE — a *.preview.dali.cpp file, or a regular .cpp/.h with
# @dali-preview-begin / @dali-preview-end markers delimiting the region.
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp
# 2. STDIN — a `-` positional, or just piped in (no positional).
cat samples/hello-dali.preview.dali.cpp | dali-ui-preview-cli
dali-ui-preview-cli - < samples/hello-dali.preview.dali.cpp
# 3. INLINE — a code block passed on the command line.
dali-ui-preview-cli --code 'return Label::New("Hello, Dali!");'Each group below is one labelled example: the exact command and what you get back. Most flags compose; the exceptions are noted in --help.
Write a "Set-of-Mark" PNG: each node gets a numbered magenta box matching its mark in the tree, so an agent (or a person) can refer to a control by number.
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --overlay overlay.pngYou get overlay.png with boxes labelled #1 Layer, #3 FlexLayoutImpl, #4 "Hello, Dali!", #5 subtitle, etc. The JSON tree is still printed to stdout.
Find the topmost node at a pixel:
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --at 500,290{ "id": "0/1/0", "mark": 4, "type": "LabelImpl", "role": "label", "bounds": { "x": 381, "y": 262, "w": 262, "h": 56 } }Or look up a node's region by id:
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --node 0/1/0Both print only their lookup JSON (the smallest box containing the pixel wins). They are mutually exclusive. A miss prints { "at": [x,y], "node": null } (for --at) or null (for --node).
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --format treeLayer "RootLayer" #1 [0] (1024x600 @ 0,0)
┠╴ CameraActor "" #2 [0/0] (0x0 @ 0,0)
┠╴ FlexLayoutImpl "" #3 [0/1] (1024x600 @ 0,0)
┃ ┠╴ LabelImpl "" #4 [0/1/0] (262x56 @ 381,262)
┃ ┖╴ LabelImpl "" #5 [0/1/1] (522x22 @ 251,322)
┖╴ CameraActor "" #6 [0/2] (0x0 @ 0,0)
The box-tree line shows the actor name (empty for labels); the displayed text lives in the JSON text field. --format json is the default.
Write an HTML or Markdown report (embedded PNG + box-tree + node table). The JSON tree is still printed to stdout; the file extension picks the format.
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --report report.html
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --report report.mdTrim the stdout JSON so it fits an agent's context window (a truncated marker shows where pruning stopped):
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --max-depth 1
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --max-nodes 3The agent loop is write → render → verify → branch on $?.
First, capture a baseline from a known-good render:
dali-ui-preview-cli good.cpp --update-baseline --baseline golden.png --baseline-tree golden.jsonThen verify a new render against it. stdout becomes a single verdict; the exit code is 0 on match, 20 on divergence (other codes still mean a tool failure):
dali-ui-preview-cli candidate.cpp --baseline golden.png --baseline-tree golden.json
echo "exit: $?"A passing verdict:
{
"match": true,
"image": { "dimsMatch": true, "diffPixels": 0, "totalPixels": 614400, "ratio": 0, "pass": true },
"tree": { "added": [], "removed": [], "changed": [] }
}A divergent one (exit 20) — e.g. a node whose bounds moved:
{
"match": false,
"image": { "dimsMatch": true, "diffPixels": 4673, "totalPixels": 614400, "ratio": 0.0076, "pass": false },
"tree": { "added": [], "removed": [], "changed": [{ "id": "0/1/0", "fields": ["bounds"] }] }
}You can verify either dimension alone (just --baseline for the image, just --baseline-tree for the tree). --threshold <ratio> (default 0.01) sets how many pixels may differ before the image fails; it requires --baseline.
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --resolution 800x480 --theme light --dpr 2--resolution WxH— logical render size (default1024x600).--theme dark|light— background theme (defaultdark).--dpr N— device-pixel ratio (default1); the actual render isresolution × dprdevice pixels.
The effective logical config is echoed on the root as root.meta = { resolution, theme, dpr }.
Re-render and re-emit on every change to the input file (FILE input only — not stdin or --code). One emission per render; Ctrl-C to stop.
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --watchThe render runs against ghcr.io/lwc0917/dali-preview-runtime. Its tags track DALi releases: one dali_<version> tag per release (e.g. dali_2.5.18) plus a rolling latest. The first render pulls a tag automatically; these commands manage which one you have and use. Because the image and caches are shared with the VS Code extension, updating the runtime once benefits both tools.
List the available versions (remote registry ∪ your local store) as JSON — does not render, exit 0:
dali-ui-preview-cli --list-versions{
"image": "ghcr.io/lwc0917/dali-preview-runtime",
"current": "latest",
"versions": [
{ "tag": "latest", "local": true, "current": true },
{ "tag": "dali_2.5.18", "local": false, "current": false }
]
}Pull a specific tag ahead of time (default latest); docker's progress streams to stderr, then a {"pulled":"<ref>","ok":true} line to stdout:
dali-ui-preview-cli --pull # pulls :latest
dali-ui-preview-cli --pull dali_2.5.18 # pulls a specific DALi releaseRender against a specific DALi version for this render with --image-tag:
dali-ui-preview-cli samples/hello-dali.preview.dali.cpp --image-tag dali_2.5.18Advanced: --runtime-image <name> overrides the image name itself (e.g. a private mirror). --list-versions / --pull take no input and cannot be combined with render or verify flags.
Every node in the tree has this shape (some fields are best-effort and may be absent):
| Field | Type | Meaning |
|---|---|---|
id |
string | Stable structural path (child-index), e.g. "0/1/0". |
mark |
number | 1-based ordinal; the number drawn on --overlay. |
type |
string | Concrete DALi type, e.g. "LabelImpl", "FlexLayoutImpl", "Layer". |
role |
string | Semantic role, e.g. "label", "container", "panel". |
name |
string | Actor name (often empty; the root is "RootLayer"). A label's displayed text is in text, not here. |
text |
string | The displayed text of a text control (Label / InputField). Present only when the control has non-empty text. |
bounds |
{x,y,w,h} |
On-screen box in image pixels (from CalculateCurrentScreenExtents). |
sourceLine |
number | 1-based line in your source the node maps to (when resolvable). |
semanticsSource |
string | "bridge" or "reconstructed" — where the semantics came from. |
visible |
boolean | The actor's VISIBLE property. |
opacity |
number | The actor's OPACITY (0..1). |
properties |
object | Exported DALi properties for the node (e.g. { "textColor": [r,g,b,a] }). |
flexProps |
object | Present on flex containers: the resolved flex layout, e.g. { "direction": "COLUMN", "alignItems": "CENTER", "justifyContent": "CENTER", "wrap": "NO_WRAP" }. |
children |
node[] | Child nodes, in child-index order. |
The root node additionally carries meta:
"meta": { "resolution": { "w": 1024, "h": 600 }, "theme": "dark", "dpr": 1 }Note: DALi inserts internal CameraActor siblings (zero-area boxes); --at/--node ignore degenerate boxes, so cameras never match a pixel query.
| Code | Meaning |
|---|---|
0 |
Success (also: a verify verdict that matched). |
1 |
Usage error, or empty input. |
10 |
Compile error in your code. |
11 |
Render / capture error. |
12 |
Docker unavailable (the docker info preflight failed). |
20 |
Verify diff mismatch (rendered, but diverged from the baseline). |
On a compile/render failure, a structured { "phase", "message", "sourceLine" } JSON is printed to stderr (stdout stays empty), e.g.:
{ "phase": "compile", "message": "'Banana' has not been declared", "sourceLine": 13 }- stdout is the machine contract. A bare render prints the full tree JSON;
--format treeprints a box-tree;--at/--nodeprint one lookup object; verify mode prints one verdict object;--list-versions/--pullprint one management object. Exactly one emission per invocation. - stderr is for diagnostics, including the structured compile/render error
{phase, message, sourceLine}. Parse stdout, watch the exit code, read stderr only on failure. - Deterministic. The same input renders byte-identical JSON, so tree diffs are meaningful and a
--baseline-treecomparison is exact. - Token caps. Use
--max-depth/--max-nodesto keep the tree within a context window. - Branchable exit codes. Distinguish "tool failed" (1/10/11/12) from "rendered but differs" (20) without parsing any text — ideal for the write→render→verify loop.
- Future option: wrap the CLI as an MCP server (a process that exposes tools to Claude/Cursor) so an agent can call
render_preview(code)directly instead of shelling out.
Apache-2.0.