Skip to content

UI Engine

Claude edited this page Sep 7, 2026 · 3 revisions

The UI Engine

Nirdosha ships a declarative UI DSL plus a UI generator that derives a full CRUD + dashboard web application from a program's struct declarations and its function-naming conventions — no UI syntax needed for the common case.

Zero-syntax inference

crates/compiler/src/ui_gen.rs looks for list_<struct>, create_<struct>, update_<struct>, delete_<struct>, get_<struct>, and stat_<name>/chart_<name> functions and generates a complete HTML/JS CRUD app + dashboard from them alone.

Optional screen / dashboard blocks

For what a naming convention can't express (a friendlier title, a relabeled field, a custom action), there is an additive DSL:

struct Product {
    id: i64,
    name: str,
    price_cents: i64,
    stock: i64,
}

fn list_product() -> Result(json, str) { ... }
fn create_product(p: Product) -> Result(i64, str) requires(role: "admin") { ... }
fn restock_product(id: i64) -> Result(i64, str) requires(role: "admin") { ... }

screen Product {
    title: "Catalog"
    field name { label: "Product Name" pattern: "^[A-Za-z0-9 ]+$" }
    field stock { min: 0 }
    action "Restock +10" -> restock_product {
        style: "outlined"
        confirm: "Restock this product by 10 units?"
    }
}

dashboard {
    tile "Products" -> stat_product_count
    chart "By Price" -> chart_products_by_price
}
  • screen/dashboard are real reserved keywords (top-level items like struct/fn); field/action/tile/chart are contextual keywords.
  • Typechecked: screen <Name> must name a real struct; field/action targets must resolve; view/edit must be role(...)/claim(...); pattern must compile as a regex and only apply to a str field; format must be one of "email"/"phone"/"date"/"url"/"uuid"; min/max must only apply to a numeric field.
  • Inert to native codegennirdosha build compiles a program containing screen/dashboard cleanly (codegen never inspects them). They're consumed only by nirdosha emit-ui.
  • view/edit (role/claim visibility) and pattern/format/min/max (format validation) are client-side hints only right now, not an enforced security boundary. Older versions of this page credited a serve.rs module with enforcing these server-side (redact_gated_fields/check_edit_gates/check_field_validations) — that file, and the live server that ran it, no longer exist (removed with the interpreter). emit-ui still emits the hide/disable hints into the generated page, but there's no server left behind them to enforce anything a curious user couldn't just bypass by reading the page source. This is genuinely different from the README's headline field-masking example (requires(role: ...) zeroing a struct field on function return, inside the compiled binary itself) — that mechanism is real and compiled, independent of emit-ui/screen entirely; the screen DSL's own view/edit gates are the part that lost server enforcement. See Honest Scope & Roadmap.
  • Tracked-but-not-wired (see crates/compiler/UI_DSL_TODO.md): paginate, searchable/sortable as DSL keys — real sorting/ search/pagination existed unconditionally per struct via a live server's own table route before the interpreter's removal; that route doesn't exist right now either.
  • Deliberately closed, not a fixed ceiling: four built-in chart shapes (inline-SVG bar_chart/graph/heatmap/timeline, no Recharts/D3/ Victory dependency) are joined by render: "chart" — a bounded grammar-of-graphics config, not a fifth hardcoded shape — and, if that still isn't enough, a Rust crate can contribute an entirely new layout widget kind. See "Extending the catalog" below. Four fixed built-in animations (fade-in/slide-up/scale-in/pop, no custom transitions or Framer-Motion-style gesture/physics motion) and a fixed seven-kind form-control set (text/number/checkbox/select/struct/ readonly/date — no rich text editor, color picker, drag-drop upload preview, autocomplete, calendar/scheduler, or signature pad) remain genuinely closed, not yet extensible the same way. See crates/compiler/UI_DSL_TODO.md's "Deliberate non-goals" section for the full rationale.

Extending the catalog

Two ways to grow the chart/component vocabulary above, both still fully closed and typechecked — no arbitrary markup, no runtime registration, nothing an agent or a served request can add on its own.

render: "chart" — a grammar-of-graphics config (mark × encode <channel>) for chart shapes the four fixed ones don't cover, additive alongside them (also wired into workspace panel { ... }, same grammar):

dashboard {
    visual "Revenue by month" -> chart_revenue_by_month {
        render: "chart"
        mark: "bar"
        encode x { field: "month" type: "temporal" }
        encode y { field: "amount" type: "quantitative" aggregate: "sum" }
    }
}

A Rust crate contributing a new layout widget kind — for something no chart shape covers at all. nirdosha emit-ui --manifest-path <Cargo.toml> (or an auto-detected Cargo.toml sitting next to the .nir file) discovers any dependency tagged [package.metadata.nirdosha] kind = "nir-ui-component" in the app's own Cargo.toml and links its JS automatically — no hand-written Rust glue needed for the common case:

layout {
    sparkline { source: recent_sales_totals field: "amount" }
}

Both resolve entirely at nirdosha build/emit-ui time — before the compiled artifact exists, let alone before an agent ever talks to it. There's no admin API, no hot-reload, no runtime negotiation that adds or changes a component after that point: growing the catalog is a human adding a reviewed Cargo dependency once, never something reachable from a served request or a chat turn. See rfcs/0009 for the full design, the real reference plugin crate (crates/ui-plugin-example-sparkline), and exactly what's shipped vs. still open — widening a component past a single layout widget kind to a full typed catalog entry, and Cargo-driven discovery for native builtins (a different, harder problem), both remain future work.

Design tokens: --theme

nirdosha emit-ui --theme theme.json layers a full design system on the baked-in Material Design 3 defaults — brand/neutral color ramps, fonts, radius, shadow, density, real entrance/hover/press animations, three dark-mode strategies, and CSS-only layout shell variants (LANGUAGE.md §11b). Every section is optional; a program with no --theme renders exactly as before this existed. This is a static, build-time layering — there's no live server to re-read the file on a TTL right now; a redeployed theme means re-running emit-ui.

Serving — not currently available

There is no nirdosha serve subcommand in this binary right now (removed along with the interpreter). A previous version of this page described one running a tiny_http server exposing the inferred functions as a JSON API (POST /api/<fn>) with OIDC JWKS/issuer/audience gating — that was real when the interpreter existed, and is documented as a real design target for a compiled replacement, but nothing in this repository implements it today. See Honest Scope & Roadmap and PUBLIC_ROADMAP.md for the current, exact status.

Why this matters for an agent

The zero-syntax path means an LLM asked for "a CRUD app over Product" doesn't need to also generate any UI code or HTML — it writes struct Product and five naming-convention functions, and a working, themeable static page falls out. What that page can honestly claim today is narrower than an older version of this section implied: it's real, generated, and — for whatever role-gated function-level logic the program itself compiles (requires(role: ...), field masking on return) — genuinely safe underneath, independent of any UI. What it isn't, right now, is a page whose own view/edit/validation hints are enforced by anything other than the client-side JS itself — a curious user reading the generated page's own source can see exactly what those hints do and don't stop them from doing, and that's an accurate read of the current state, not a bug in the generated code. See "Extending the catalog" above and Honest Scope & Roadmap for the fuller picture of what's compiled vs. not right now.

Clone this wiki locally