telar 0.2.1

A modular Rust UI framework with its own template language, reactive signals and a self-contained renderer.
docs.rs failed to build telar-0.2.1
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: telar-0.1.8

Telar

CI crates.io docs.rs license

A modular Rust UI framework with its own template language, reactive signals and a self-contained renderer.

Telar draws every pixel itself — there is no webview and no native widget toolkit underneath. Components are written in .rsx, an indentation-based template language that compiles to plain Rust at build time, so what ships is a single binary with no runtime interpreter.

Early days. Telar is at 0.2.1. The APIs work and are exercised by the apps in this repo, but they will keep moving before 1.0.

A component

[logic]
#[derive(Default)]
pub struct Props {
    pub icon: &'static str,
    pub title: &'static str,
    pub body: &'static str,
}

[view]
box fill:$theme.surface stroke:$theme.border radius:16 width:300 pad:24 gap:10 axis:col
    text "{props.icon}" font_size:32
    text "{props.title}" font_size:18 color:$theme.dark
    text "{props.body}" font_size:14 color:$theme.muted

[preview "Fast"]
feature_card icon:"⚡" title:"Fast" body:"Software and wgpu renderers with dirty tracking."

A .rsx file has up to four sections: [logic] for verbatim Rust (a pub struct Props declares the component's props), [style] for reusable style classes, [view] for the node tree, and [preview] blocks that the tooling can render in isolation.

Quick start

cargo install cargo-telar
cargo telar new my-app        # --target desktop | tui | web | android
cd my-app
cargo telar dev

That is the whole of the setup. cargo telar new writes the manifest, the build profiles, a theme, an app root and one .rsx component, with one target already named — there is nothing to wire up and no feature list to read first.

my-app/
  Cargo.toml      # one target under [features], and the build profiles
  telar.toml      # renderer backend, theme, catalogs, the dev window
  src/main.rs     # fn main() { my_app::run(); }
  src/lib.rs      # telar::app!(…) — theme, startup hook, config, root
  src/theme.rs    # the design tokens every component reads
  src/app.rs      # the root component
  src/home.rsx    # one page

src/lib.rs is the whole of the wiring:

telar::app!(
    theme::AppTheme,
    { telar::set_theme(theme::AppTheme::light()); },
    telar::AppConfig::default(),
    app::Root
);

and telar.toml sits next to Cargo.toml:

[telar]
backend = "auto"

[telar.dev.window]
title = "my-app"
width = 1000
height = 700

Then:

cargo telar dev        # run with hot reload
cargo telar preview    # render every [preview] block, hot-reloaded
cargo telar preview --png out/   # render each one to a PNG instead, with no window
cargo telar test       # render all previews headlessly and report failures
cargo telar check      # type-check, with .rsx errors on the lines you wrote
cargo telar build --format deb   # appimage | deb | dmg | nsis | apk | dir
cargo telar doctor     # check the toolchain

One target, one word

Telar is a set of small crates behind one facade, and a build carries the target it named and nothing else:

Your app runs in default = [...] Crates compiled
A desktop window (Linux, macOS, Windows) ["desktop"] 397
The terminal it was launched from ["tui"] 102
A browser, drawing as a document ["web-dom"] 77
A browser, document and WebGPU canvas ["web"] 210
Android ["android"] 299
Nothing — draw commands in, pixels out ["headless"] 174

Counted with cargo tree -p telar --no-default-features --features "<target>" -e normal --target all, so every platform's dependencies are in the figure at once. Every row is complete on its own: naming it is the whole of the choice, and no row pays for another — a desktop build is mostly wgpu and its shader toolchain, and a terminal build links neither. Switching later is one word in Cargo.toml.

Per-target guides, and how to ship two targets from one codebase, are in docs/targets.md.

One value grammar

An attribute is key — a flag that asserts itself — or key:<rust expression>, read to the next space at delimiter depth 0. Every value is Rust, evaluated in the generated scope, so a call, a path, a method chain, a macro or a closure is itself and rustc judges it against the line you wrote:

col gap:8 pad:(space::lg() * 2.0) align:center
    btn "Save" on_press:(|| draft.save()) fill:$theme.primary
    text "Hola {name}" font_size:14

Parenthesise an expression that holds a space — (a + b) is an expression, so nothing new is invented. Three sugars survive, because each is a token shape rather than a second language:

written means
50% SizeDimension::Percent(0.5)
#3d78fa Color::rgba(…)
$sig sig.get() — a read of anything reactive, including the theme handle the view binds

key(…) is reserved for the handful of directives that have a grammar of their own and are not Rust at all: transition(fill 250ms ease-out) is a clause list, hover_style(fill:$theme.accent) a nested attribute list. The spelling says which world you are in.

Reactivity is reading, not marking. A layout value that is not a literal is re-resolved whenever what it reads changes — pad:$theme.gutter follows a theme switch, and so does pad:gutter(). The $ is .get() sugar; it does not decide anything.

Components are Rust paths

A .rsx file is a module, and its component is the pub fn named after the file. Import what you call:

[logic]
use crate::ui::card::{card, CardProps};

[view]
card pad:20
    text "inside" font_size:14

A directory is a module too, and mod.rsx is that module's file — mod.rs in the other language. Its [logic] is Rust at module level, which is where a //! and a #![…] belong; it takes no [view], because a module is not callable. Give a directory one and telar declares it and everything under it, so nothing in it has to be placed by hand:

src/media/
  mod.rsx         # the module: docs, attributes, and Rust items
  mod.rs          # optional, and kept as it is: included into the same module
  media.rsx       # crate::media::media
  state.rs        # crate::media::state

Without a mod.rsx, a directory that holds both .rsx files and a hand-written mod.rs places them from that file with a telar::rsx_modules!(); of its own — only a module's own file can add items to it.

apps/sandbox in this repo is the reference app and covers most of the surface.

What's in the box

Everything here is either always present or one word away. Nothing is bundled.

  • Reactive signals — a fine-grained graph of signals, memos and effects; no virtual DOM, no diffing.
  • Flexbox and grid layout on top of Taffy, with reactive writing direction (LTR/RTL).
  • Motion — tweens and springs driven by one frame ticker, with colors interpolated in Oklch.
  • Theming — theme tokens plus light/dark mode that can follow the OS.
  • Internationalization — translation catalogs baked at build time; t! validates keys and arguments at compile time.
  • Two renderers — a CPU rasterizer on tiny-skia and a GPU one on wgpu, behind the same drawing vocabulary. desktop and android bring both, and backend = "auto" picks per machine.
  • A widget catalogue — buttons, fields, selects, menus, modals, tabs, sliders, and the rest. → components
  • Navigation — a reactive page stack with animated transitions. → navigate
  • Images and SVG — baked into the binary at build time out of src:"…", with no parser in the binary. → svg
  • Translation catalogs baked the same way, with t! validating keys and arguments at compile time.

Both are baked by the CLI, and so is the .rsx itself: build through cargo telar check/dev/build/test, or run cargo telar transpile first. A plain cargo build fails with a message naming that command rather than compiling something stale, which is what keeps the decoders, the parser and the code generator out of every project's own build. A project that will not install the CLI produces the artifact itself from a build.rs, which is one call into telar-transpiler and gets real cargo:rerun-if-changed out of it.

  • Assets that arrive later, behind a transport-agnostic reactive seam: a signal that advances LoadingReady/Failed, with the transport, the cache and the decoder each yours to choose. → async-assets
  • Decoders and transports for that seam — SVG, bitmaps, translation catalogs, over HTTP or from a directory — in the companion crate telar-dynamic, one feature each. Yours plugs in the same way.
  • Hot reload in cargo telar dev, and an in-app devtools overlay for inspecting the live component tree — or one of your own, through the same seam. (the CLI sets this one)
  • Packaging to native installers per platform, plus Android APKs. → cargo telar build --format …

The complete list, with what each feature pulls in and why, is on docs.rs.

Editor support

The VS Code extension provides syntax highlighting, snippets, diagnostics, completion and component preview, backed by the telar-analyzer language server. A component's attribute keys are completed from its props struct — names, types and doc comments — through an embedded rust-analyzer, and a diagnostic about a value lands on the .rsx line and column you wrote it on. The extension bundles a prebuilt server binary, so no extra install step is needed.

Build tuning

cargo telar new writes profiles that keep dev builds fast and release builds small. Adding Telar to an existing workspace, wanting a faster linker, or wanting to know why panic = "abort" must stay off: see docs/build-tuning.md.

Crates

You depend on one:

[dependencies]
telar = "0.2.1"

Everything behind it — the reactive graph, the layout engine, the renderers, the platform backends, the .rsx pipeline — is a separate telar-* crate. They are published because Cargo requires every dependency of a published crate to be published too, not because an application names them; the split is what lets a terminal build skip a GPU renderer. Reach for one directly only if you are writing a frontend or a tool against Telar's internals.

Three exceptions. cargo-telar is a binary you install rather than a dependency. telar-dynamic is a second dependency, for an application that decodes an asset at run time rather than baking it: the facade owns the seam and ships no implementation of it, so the decoders and transports live there, one feature each. And telar-plugin is a third, for hosting a separately-compiled Telar UI inside your own — or for being one.

Crate Purpose
telar-reactive-core Signals, memos, effects, batching
telar-geometry-core Points, rects, transforms, border radii, Oklch color
telar-layout-core · telar-layout-reactive Flexbox/grid engine and its reactive context
telar-motion-core Tweens, springs, the frame ticker
telar-theme-core Theme tokens, light/dark mode
telar-semantics-core What a thing in an interface is, for screen readers, documents and terminals
telar-ui-core · telar-ui-tree · telar-ui-components Widget kernel, component tree, widget catalogue
telar-renderer-core Draw commands, culling, dirty tracking
telar-renderer-software · telar-renderer-hardware CPU and wgpu backends
telar-renderer-tui · telar-renderer-dom · telar-renderer-web Terminal cells, browser elements, browser canvas
telar-renderer-text · telar-renderer-assets Text shaping and glyph atlas; SVG parsing and build-time asset baking
telar-dynamic · telar-plugin Runtime asset decoders and transports; embedding a separately-compiled UI — the two crates here an application depends on directly
telar-renderer-cache · telar-renderer-record The shared byte-budgeted cache; a backend that records instead of drawing
telar-platform-core and telar-platform-{winit,desktop,android,tui,web,headless} Window/event abstraction and its backends
telar-devtools The dev overlay: FPS counter, node inspector, build-error banner — pulled in by telar/dev, and by nothing else
telar-parser · telar-transpiler · telar-macros The .rsx pipeline
telar-project What a project is: telar.toml, source discovery, output paths, and the build artifacts a transpile leaves behind
telar-i18n-core · telar-navigate-core · telar-services-core i18n runtime, navigation, platform paths and DI
telar-reactive-local Per-surface thread-local slots, split out so platform-core need not link the reactive runtime

telar-analyzer and telar-diagnostics live in this repo but are distributed through GitHub Releases and the VS Code extension rather than crates.io.

Minimum supported Rust version

Rust 1.95. Bumping it is a minor-version change.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.