A2UI — Rust impl of the A2UI protocol (ratatui terminal + Slint / egui / Bevy / Iced / Dioxus / GPUI desktop)
English | 中文
A Rust implementation of the A2UI (Agent to UI) v1.0 protocol — a JSON-based streaming UI protocol that lets AI Agents dynamically generate and update interfaces.
On top of a single framework-agnostic core (a2ui-base), it ships 7 rendering backends: the default terminal backend a2ui-tui (built on ratatui), plus six optional native-desktop backends — Slint, egui, Bevy, Iced, Dioxus, and GPUI (Zed's GPU UI framework, via the gpui-component widget set). See the Backend Support Matrix for each backend's rendering fidelity and real-input capability.
The project is organized as a Cargo workspace: a2ui-base (framework-agnostic core) + 6 in-workspace backends (a2ui-tui / a2ui-slint / a2ui-egui / a2ui-bevy / a2ui-iced / a2ui-dioxus) + a matching *-gallery demo app for each + a2ui (an umbrella that re-exports them, keeping use a2ui::core::... / use a2ui::tui::... paths working). The 7th backend, a2ui-gpui, lives in a separate workspace due to a dependency conflict (see GPUI backend and Known build limitations).
- ✅ Full A2UI v1.0 protocol support
- ✅ 18 TUI components: Text, Row, Column, Button, TextField, Card, Divider, List, CheckBox, Icon, Tabs, Modal, Slider, ChoicePicker, DateTimeInput, Image, Video, AudioPlayer
- Interactive: Text, Row, Column, Button, TextField, Slider, CheckBox, ChoicePicker, DateTimeInput (arrow keys adjust the value)
- Placeholders (default): Image / Video / AudioPlayer render only text placeholders (
[🖼 description],[▶ url],[♫ url]) — the terminal cannot decode pixels/audio/video. Enable real image/audio rendering via the Optional Features below.
- ✅ Capabilities negotiation:
ClientCapabilities/ServerCapabilitiestypes + a builder that derivessupportedCatalogIdsfrom registered catalogs. - ✅ Inline catalogs: the server can declare
acceptsInlineCatalogs; the client parses and validates inline catalog JSON (UAX#31 identifier checks) and registers schema-only functions at runtime. - ✅ Generic fallback renderer: unknown / inline-custom component types render as a visible labeled box (type + properties + children) instead of a bare "unknown" error.
- ✅ 14 client-side functions: required, regex, length, numeric, email, and/or/not, formatString, formatNumber, formatCurrency, formatDate, pluralize, openUrl
- ✅ Payload validation: integrity / topology / recursion & path checks plus a fault-tolerant
parse_and_fix(auto-heals malformed JSON like smart quotes and trailing commas), ported from the Python SDK. Opt-in onMessageProcessor(with_validation(cfg)+drain_validation()), OFF by default and never blocks component loading — for untrusted or LLM-generated payloads. - ✅ Modular Cargo workspace architecture (
a2ui-baseframework-agnostic /a2ui-tuiratatui backend /a2ui-gallerydemo app /a2uiumbrella) - ✅ JSON Pointer data binding with reactive state management
- ✅ Gallery App sample browser with progressive message rendering
- ✅ 235 unit/integration tests (core 127 + tui 61 + gallery e2e 21 + slint 14 + iced 12), including end-to-end tests with A2UI specification examples
Gallery Sample Browser
Login Form
Agent Chat (AI chat interface, 08_agent_chat example: multi-surface chat layout, streaming A2UI messages, rich components like Card / Column / Row / Divider)
Invitation Builder (spec sample 30_live-invitation-builder: a reactive form layout where TextField / Slider / ChoicePicker / DateTimeInput components work together to preview an invitation in real time)
Sci-fi HUD — backend comparison (same data, same updateDataModel protocol, different renderer; every live value — gauges, radar sweep, event log — is read from the a2ui data model)
Seven backends, one protocol payload — only data flows through the protocol; each renders its own way.
The ratatui version uses custom TuiComponents to draw ASCII gauges and a character-grid radar; the Slint version uses an inline slint! component with flex-bar gauges and an ASCII character-grid radar (echoing the ratatui original); the egui version uses native ProgressBar gauges and a Painter-drawn radar sweep (immediate mode: ui() rebuilds the whole widget tree from the data model every frame); the Iced version uses progress_bar gauges and a Canvas-drawn radar; the Dioxus version uses CSS-bar gauges and an SVG radar sweep, rendered into a system WebView; the Bevy version uses native Bevy UI nodes (retained ECS: the entity tree is spawned once, then mutated in place each frame — Text/Node/colors) with flex-bar gauges and an ASCII character-grid radar; the GPUI version (Zed's GPU UI framework + gpui-component) uses Progress gauges and an SVG radar sweep (hybrid immediate/retained mode: render rebuilds the widget tree each frame). The architecture is identical across all seven — only data flows through the protocol; the rendering layer is each backend's own.
The sci-fi HUD is now realized for all seven backends (ratatui terminal + Slint / egui / Iced / Dioxus / Bevy / GPUI desktop).
The GPUI screenshot is captured under
Xvfb(an X11 virtual display) withimport: GPUI's Wayland window is just as invisible to desktop screenshot tools (grim/gnome-screenshotare both blocked), and GPUI has no egui/Bevy-style built-in screenshot API, so the fallback is to run it underxvfb-run(forcing the X11 backend) and grab the root window withimport -window root, then crop.The Bevy screenshot is produced by
scripts/capture_bevy_screenshot.sh: on a locked-down GNOME Wayland session desktop screenshot tools are unavailable (org.gnome.Shell.ScreenshotD-Bus is denied, and X11 tools can't see Wayland-native windows), so the example ships an env-triggered self-screenshot mode that reads the window's render target directly (Screenshot::primary_window()+save_to_disk), compositor-independent. The Slint screenshot is produced byscripts/capture_slint_screenshot.sh: under the same Wayland constraint, the example installs a headless platform (MinimalSoftwareWindow) and the software renderer rasterizes straight into an in-memory pixel buffer (no window, no compositor) before encoding a PNG. The egui screenshot is produced byscripts/capture_egui_screenshot.sh: the example requests one screenshot via egui's built-inViewportCommand::Screenshot, and eframe's glow backend reads the GPU framebuffer directly after painting (read_screen_rgba) — no compositor involved, the egui analog of the Bevy/Slint in-app capture.
# Run the Gallery App
cargo run -p a2ui-gallery
# Install the Gallery App (provides the `a2ui_gallery` binary)
cargo install a2ui-gallery
# Run an example (lives in the umbrella crate)
cargo run -p a2ui --example 12_handshake| Key | Action |
|---|---|
↑/k, ↓/j |
Navigate sample list |
Enter |
Select and render current sample |
n |
Step through next message |
a |
Process all remaining messages |
r |
Reset and replay |
Tab |
Cycle focus |
Esc |
Back to list / Quit |
┌──────────────────────────────────────────────────────────────┐
│ apps: a2ui-gallery (TUI) a2ui-{slint,egui,bevy,iced,dioxus}-gallery (desktop)
├──────────────────────────────────────────────────────────────┤
│ umbrella: a2ui (re-export core + tui [+ slint] [+ egui] [+ bevy] [+ iced] [+ dioxus])
├──────────────────────────────────────────────────────────────┤
│ backends: a2ui-tui (ratatui) a2ui-slint (Slint, opt-in) a2ui-egui (egui, opt-in) a2ui-bevy (Bevy, opt-in) a2ui-iced (Iced, opt-in) a2ui-dioxus (Dioxus, opt-in)
├──────────────────────────────────────────────────────────────┤
│ a2ui-base (framework-agnostic: Protocol / Model / Catalog / Processor)
└──────────────────────────────────────────────────────────────┘
Dependencies flow upward: a2ui-base underpins six in-workspace backends — a2ui-tui (ratatui, default), a2ui-slint (Slint desktop, optional), a2ui-egui (egui desktop, optional), a2ui-bevy (Bevy ECS UI desktop, optional), a2ui-iced (Iced desktop, optional), and a2ui-dioxus (Dioxus WebView desktop, optional) — plus the 7th, a2ui-gpui (GPUI + gpui-component), which lives in its own workspace due to a dependency conflict (see GPUI backend). Each backend has a matching *-gallery app; the a2ui umbrella depends on core + tui (slint / egui / bevy / iced / dioxus each behind a same-named feature). a2ui-base has zero ratatui/slint/egui/bevy/iced/dioxus/gpui dependency and can be used standalone by other backends.
All seven backends share the same a2ui-base core (interaction logic / dispatch_event / apply_event_result), but rendering fidelity and "real input" capability vary by GUI framework:
✅ Full (rendered; interactive controls accept input) · 🟡 Best-effort (read-only / limited interaction) · ⬜ Placeholder
| Component | TUI (ratatui) | Slint | egui | Bevy | Iced | Dioxus | GPUI |
|---|---|---|---|---|---|---|---|
| Text / Row / Column / Card / List / Divider | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Button | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Modal | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| TextField | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| CheckBox | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Slider | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| ChoicePicker | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Tabs | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| DateTimeInput | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Icon | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Image | ✅² | ✅⁶ | ✅⁸ | ✅⁷ | ✅⁵ | ✅³ | ✅⁹ |
| Video | ⬜ | ⬜ | ⬜ | ⬜ | ⬜ | ✅⁴ | ⬜ |
| AudioPlayer | ✅¹ | ⬜ | ⬜ | ⬜ | ⬜ | ✅⁴ | ⬜ |
¹ Needs the audio feature.
² The TUI backend decodes and renders actual image pixels via ratatui-image (kitty / iTerm2 / Sixel / Halfblocks auto-degrade, local paths only).
³ The Dioxus backend renders images via the native WebView <img> (supports file:// / http(s) / data: URLs).
⁴ The Dioxus backend plays real media via the native WebView <audio> / <video> elements (the browser supplies full transport controls — play/pause/seek/volume/fullscreen) — something the terminal and other desktop backends cannot do.
⁵ Iced has no built-in URL image loader (its image widget only takes a local path or in-memory bytes), so an Image's URL is resolved by the shared a2ui-image crate: http(s) is fetched out-of-band with ureq, data: URIs (base64 / percent-decode) are decoded in place, and local paths (incl. file://) are read directly — the bytes go to Handle::from_bytes and are cached (cleared on sample switch). fit maps onto ContentFit.
⁶ The Slint backend renders actual image pixels via its native Image widget + the image crate: the URL is resolved by the shared a2ui-image crate — local paths (incl. file://) are read directly, http(s) URLs are fetched with ureq, and data: URIs (base64 / percent-decode) are decoded in place (cached and cleared on sample switch); only images that fail to decode render as a labeled placeholder.
⁷ The Bevy backend renders actual image pixels via the native ImageNode (a wgpu texture): the URL is resolved by a2ui-image and the image crate decodes to a bevy::image::Image whose Handle is cached (cleared on sample switch). Local paths (incl. file://) are read directly; http(s) URLs are fetched synchronously on the UI thread with ureq (few sample images, isomorphic with Slint); data: URIs (base64 / percent-decode) are decoded in place; only decode failures render as a labeled placeholder. Icons render as emoji via the embedded ~12 KB NotoEmoji subset font (same icon-name table as TUI / Iced; Bevy uses emoji codepoints).
⁸ The egui backend renders actual image pixels via the native Image widget (a wgpu/glow texture): the URL is resolved by a2ui-image and the image crate decodes to an egui::ColorImage → TextureHandle which is cached (cleared on sample switch). Local paths (incl. file://) are read directly; http(s) URLs are fetched synchronously with ureq once, just before the per-frame walk (few sample images, isomorphic with Bevy / Slint; a failed fetch is cached as a placeholder and not retried); data: URIs (base64 / percent-decode) are decoded in place; only decode failures render as a labeled placeholder. Icons likewise render as emoji via the embedded ~12 KB NotoEmoji subset font (same a2ui-icons.ttf as Bevy; emoji-codepoint mapping).
⁹ GPUI (Zed's GPU UI framework) renders pixels via img() + a gpui Image: the URL is resolved by a2ui-image, the ImageFormat is sniffed from the extension / data: mime / magic bytes, and Image::from_bytes is fed on gpui's background executor, cached as an Arc<Image> (cleared on sample switch). Local paths / http(s) / data: all flow through the same async resolve → decode → cache path. Icons render as emoji glyphs (same icon-name table as TUI / Iced). fit maps onto ObjectFit.
- The TUI backend is the reference implementation — all 18 components render fully; real images are on by default (
ratatui-image), audio needs theaudiofeature, video is always a placeholder. - Genuine input on the interactive widgets (TextField / Slider / CheckBox / ChoicePicker / DateTimeInput): full on Slint, egui, Bevy, Iced, Dioxus, GPUI, and TUI (Slint uses native
LineEdit/Slider/CheckBox/ComboBoxwidgets, with change callbacks writing straight back to the data model, isomorphic with Iced / egui). - Bevy's ChoicePicker is currently a text label (
[ChoicePicker: …]); not yet wired to a native picker. - Iced is the cleanest-mapping backend (no state bridge, no diffing); all five interactive widgets are native — ChoicePicker uses a native
pick_list(single-select) / checkbox group (multi-select), DateTimeInput is an editable text box bound to the data model, and Tabs has a clickable tab bar (a boundactiveTabwrites back to the data model; the gallery samples leave it unbound, so the selected tab is tracked locally and clicks still switch panels); Icon renders an emoji (the mapping matches the TUI); and Image renders for real (local paths decoded inline, remote URLs fetched + cached in the background, see footnote ⁵). Dioxus is the most architecturally distinct (reactive signals + WebView/CSS rendering), and thanks to the WebView, Image / Video / AudioPlayer render via native HTML media elements too — it is the only backend to cover all 18 A2UI components (even the TUI's Video is a placeholder, since a terminal cannot play video).
Each GUI backend is opt-in (cargo build -p a2ui-<backend> --features backend). Do not build/lint/test them together with cargo build/check/test/clippy --workspace or --all-features: iced 0.14 (→ wgpu 27 → naga 27) and bevy 0.19 (→ bevy_shader → naga_oil 0.22) both resolve codespan-reporting 0.12.0 but with conflicting feature needs — naga_oil requires its termcolor feature, while naga 27.0.3 only compiles with it off. Cargo unifies them onto a single codespan 0.12.0 build with termcolor ON, and naga 27 then fails to compile (String: WriteColor not satisfied).
The GPUI backend goes further — it can't share a Cargo.lock with any other backend at all, so it is excluded from the main workspace (crates/gpui and crates/gpui-gallery each have their own [workspace]). The reason: gpui 0.2.2 pins cocoa = "=0.26.0" (a macOS-only dep) while dioxus-desktop (which enters the main lockfile via the umbrella crate's optional a2ui-dioxus dep) needs cocoa ^0.26.1. Both are target.'cfg(target_os = "macos")' deps that never compile on Linux, but Cargo still resolves and unifies them to produce a portable lockfile — and no single cocoa version satisfies both =0.26.0 and ^0.26.1. So the GPUI backend must be built standalone via --manifest-path:
cargo build --manifest-path crates/gpui-gallery/Cargo.toml # gallery
cargo run --manifest-path crates/gpui/Cargo.toml --example 08_agent_chat --features backendEach backend builds, lints, and tests cleanly on its own (-p a2ui-<backend> --features backend); only bevy + iced in the same cargo invocation triggers it. The repo ships just recipes that iterate per-backend to avoid this:
just check # type-check the default stack + each GUI backend separately
just clippy # same, clippy per backend
just test # same, tests per backend
The default stack (cargo build, no -p) compiles only core / tui / gallery / umbrella and is unaffected. This is an upstream ecosystem conflict (it will disappear once iced moves to naga 29), not a bug in this repo's code.
Alongside the ratatui terminal backend, the project ships a2ui-slint, which renders A2UI component trees into a native desktop window (built on Slint, pinned to 1.16). The framework-agnostic interaction logic (focus traversal, event dispatch, EventResult application) is shared in a2ui-base, so both backends behave identically for keyboard / button interactions.
It is opt-in and heavy: a2ui-slint is a non-default workspace member (it pulls the Slint toolchain + GUI system libraries). A plain cargo build only compiles the ratatui stack. Build the Slint backend explicitly:
cargo build -p a2ui-slint --features backendThe umbrella crate also re-exports it as a2ui::slint behind a slint cargo feature.
a2ui-slint-gallery loads the same embedded A2UI samples as the ratatui gallery, in a window. It prints the full numbered sample list at startup:
cargo run -p a2ui-slint-gallery # first sample
cargo run -p a2ui-slint-gallery -- 3 # by 1-based index
cargo run -p a2ui-slint-gallery -- login # by case-insensitive name substringRenderer: renderer-software + backend-winit — it works without a GPU / OpenGL driver.
16 of the A2UI component types render natively (only Video / AudioPlayer remain placeholders — Slint ships no media-playback widgets):
- Containers / content: Text / Row / Column / Card / List / Divider / Modal (overlay) / Button (clicks dispatch through the shared
core::components::dispatch_event) - Interactive widgets (all native, genuine input written back to the data model): TextField (native
LineEdit) / CheckBox (nativeCheckBox) / Slider (nativeSlider) / ChoicePicker (a nativeComboBoxfor single-select, aCheckBoxgroup for multi-select) / DateTimeInput (an editable ISO text box bound to the data model) / Tabs (a clickable tab bar + only the active panel is rendered) - Icon: mapped to an emoji / unicode glyph (the mapping matches the TUI / Iced / Dioxus; unknown names fall back to
[first-two-chars]) - Image: renders for real — local paths (incl.
file://) are read directly,http(s)URLs are fetched withureq, all decoded into aslint::Imagevia theimagecrate and shown in a nativeImagewidget (cache cleared on sample switch; remote fetching runs synchronously on the UI thread, acceptable for the small sample-image set) - Placeholders: Video / AudioPlayer render as a labeled placeholder
Slint cannot express recursion (neither recursive structs nor self-referencing components — see slint-ui/slint#4218). So instead of a nested tree, live_tree flattens the component tree into a Vec<LiveNode> with index-based children, and build.rs code-generates a bounded-depth component chain Node0 (leaf) → … → Node7 (root). A2UI trees are shallow, so depth 7 covers realistic UIs; deeper subtrees truncate to a …. This is the key constraint a future contributor needs to know.
Slint's standard widget library (std-widgets.slint) supplies native LineEdit / Slider / CheckBox / ComboBox, but their changes are pointer-driven (drag / click / type), whereas the shared core::dispatch_event only models keyboard events (arrow / character keys for Button / CheckBox / Slider / TextField) and has no channel for pointer events. So the Slint backend (isomorphic with the Iced / egui backends) bypasses core dispatch and writes the data model directly: build.rs wires each widget's change callback (edited / changed / toggled / selected), which carries the node id up to the host via the Events global; the host uses the id to find the ComponentModel, resolves the value / activeTab DynamicString::Binding path (handling template-nested paths through ComponentContext::data_context.resolve_pointer), then calls data_model.set(path, value) and redraws. Only Button still goes through core's dispatch_event (Enter), since its action may trigger a server event / function call.
- Trees deeper than 7 levels truncate (the bounded-depth constraint of the flattening scheme);
- Remote
http(s)image fetching runs synchronously on the UI thread (theRc/RefCellstate can't cross theSendclosure required byinvoke_from_event_loop), acceptable for the small sample-image set;data:URLs render as a placeholder; - Video / AudioPlayer are placeholders (Slint has no media-playback widgets).
The project also ships a2ui-iced, which renders A2UI component trees into a native desktop window (built on Iced, pinned to 0.14). This is the cleanest mapping of the five backends — Iced is Elm: view(&state) returns an immutable Element tree and update(&mut state, msg) mutates state. So interactive widgets read straight from the data model in view and write back through a Message in update: no egui-style EditBuffers state bridge and no bevy-style reconciler. No state bridge, no diffing. Button presses reuse the shared core::components::dispatch_event + apply_event_result; Modals float as a centered overlay layered via a Stack.
It is an optional dependency: a2ui-iced is a non-default workspace member (it pulls wgpu + winit). Plain cargo build only compiles the ratatui stack — build it explicitly:
cargo build -p a2ui-iced --features backendThe umbrella crate re-exports the backend as a2ui::iced under the iced cargo feature. The renderer defaults to wgpu (GPU), with a tiny-skia software fallback.
16 of the A2UI component types render natively (only Video / AudioPlayer remain placeholders — Iced 0.14 ships no media-playback widgets):
- Containers / content: Text (h1/h2/h3 heading sizes) / Row / Column / Card / List / Divider / Modal (a
Stack-centered overlay with a dimmed scrim) / Button (primary/secondary/borderless styles; disabled when achecksrule fails) - Interactive widgets (all native, genuine input written back to the data model): TextField (
text_input) / CheckBox / Slider / ChoicePicker (a nativepick_listfor single-select, a checkbox group for multi-select; writes back ajson!([value])array) / DateTimeInput (an editable ISO text box bound to the data model — Iced 0.14 has no calendar widget, so it uses a text input + anenableDate/enableTimeformat hint) / Tabs (a clickable tab bar + content panel; a boundactiveTabwrites back to the data model, an unbound one is tracked locally) - Icon: mapped to an emoji / unicode glyph (the mapping matches the TUI / Dioxus; unknown names fall back to
[first-two-chars]) - Image: renders for real — a local path (incl.
file://) goes straight toHandle::from_path; anhttp(s)URL is fetched in the background withureqwhen the sample loads, decoded viaHandle::from_bytes, and cached (a placeholder chip shows while loading or on failure).fitmaps ontoContentFit. Iced has no built-in URL image loader, so this is done with theTaskreturned from boot /SelectSampleplus animage_cache, all within the Elm architecture. - Placeholders: Video / AudioPlayer render as a labeled chip badge
a2ui-iced-gallery loads the same embedded A2UI samples:
cargo run -p a2ui-iced-gallery # the first sample
cargo run -p a2ui-iced-gallery -- 3 # by 1-based index
cargo run -p a2ui-iced-gallery -- login # by case-insensitive name substringThe project also ships a2ui-dioxus, which renders A2UI component trees into a native desktop WebView window (built on Dioxus, pinned to 0.7). This is the most architecturally distinct of the seven backends:
- Reactive signals — Dioxus is React-like: runtime state lives in a
Signalat the root and the UI is a pure read of it. No Iced-styleMessageenum (Elm view/update) and no egui-styleEditBuffersbridge. No message enum, no state bridge — the signal is the interaction channel; any write automatically re-renders the components that subscribed to it. - Recursive components — the whole tree is one
A2uiNodecomponent that renders itself per node (Dioxus supports recursive components natively, unlike Slint's bounded-depth codegen). - WebView rendering — it renders to a system WebView (WebKitGTK on Linux), so the dark theme is a CSS stylesheet (
theme::STYLESHEET) rather than per-widget style functions, and A2UI component kinds map to ordinary HTML elements + classes.
Button presses reuse the shared core::components::dispatch_event + apply_event_result (handed up to the root via an Rc<dyn Fn(String)> callback); Modals float as a centered panel over a dimmed scrim.
Thanks to the WebView, the Dioxus backend is the only one of the six to cover all 18 A2UI components (even the TUI's Video is a placeholder). The interactive ones are native HTML elements that accept real input and write back to the data model — TextField (<input>) / CheckBox / Slider (<input type="range">) / ChoicePicker (a native <select> for single/mutuallyExclusive, a checkbox group for multipleSelection) / DateTimeInput (native <input type="date|time|datetime-local">); Tabs renders a clickable tab bar + content panel (reads the tabs property, click switches and writes back activeTab); Icon shows an emoji directly (the mapping matches the TUI); Image uses a native <img> (file:// / http(s) / data: URLs); Video / AudioPlayer use native <video> / <audio> (the browser supplies full transport controls — play/pause/seek/volume/fullscreen — which the terminal and other desktop backends cannot do).
It is an optional dependency: a2ui-dioxus is a non-default workspace member (it pulls the wry WebView + tao windowing stack). A plain cargo build only compiles the ratatui stack. Build it explicitly:
cargo build -p a2ui-dioxus --features backendThe umbrella crate re-exports it as a2ui::dioxus under the dioxus cargo feature. On Linux it requires WebKitGTK (webkit2gtk-4.1) + GTK 3 installed system-wide.
a2ui-dioxus-gallery loads the same embedded A2UI samples:
cargo run -p a2ui-dioxus-gallery # the first sample
cargo run -p a2ui-dioxus-gallery -- 3 # by 1-based index
cargo run -p a2ui-dioxus-gallery -- login # by case-insensitive name substringThe 7th backend, a2ui-gpui, renders A2UI component trees into a native desktop window on GPUI (Zed's GPU-accelerated UI framework), with the widget layer provided by gpui-component (longbridge's 60+ cross-platform component set). All versions pinned to the latest crates.io line: gpui 0.2.2 + gpui-component 0.5.1 + gpui-component-assets 0.5.1 (gpui-component 0.5.1 depends on gpui ^0.2.2, so they line up).
GPUI is hybrid immediate/retained mode: like egui it rebuilds the Render view each frame, but stateful widgets (TextField / Slider / ChoicePicker) own a persistent Entity<…State> that survives across frames — so (unlike Iced's stateless view, and unlike egui's EditBuffers string map) this backend caches Entity<InputState> / Entity<SliderState> / Entity<SelectState> keyed by component id, lazily creating them on first render, seeding their value from the data model, and subscribing to their change events for write-back. Stateless widgets (Button / CheckBox / TabBar / list rows) attach cx.listener closures that fire after the render borrow is released and mutate the runtime directly — no egui-style collect-then-apply buffer.
Because the GPUI backend lives in its own workspace (see Known build limitations), build it with --manifest-path:
cargo run --manifest-path crates/gpui-gallery/Cargo.toml # the first sample
cargo run --manifest-path crates/gpui-gallery/Cargo.toml -- 3 # by 1-based index
cargo run --manifest-path crates/gpui-gallery/Cargo.toml -- login # by case-insensitive name substringGNOME Wayland titlebar caveat: gpui defaults to server-side decorations, which GNOME Wayland ignores (its policy is CSD-only) — so the window opens with no titlebar, undraggable, no close button. The gallery (and both examples) force
WindowDecorations::Client+appears_transparentand draw a gpui-componentTitleBarat the top (providing drag + min/max/close; harmless on X11 / macOS).
The GPUI backend also ships the same two examples as the other backends, sharing the a2ui_tui::agent_chat scenarios and the a2ui-base MessageProcessor:
cargo run --manifest-path crates/gpui/Cargo.toml --example 08_agent_chat --features backend # AI chat (background Timer simulates SSE streaming)
cargo run --manifest-path crates/gpui/Cargo.toml --example 17_scifi_hud --features backend # cyberpunk HUD (reads values from the data model each frame; SVG radar sweep)A2UI uses a JSON streaming message format to drive UI rendering:
{"version":"v1.0","createSurface":{"surfaceId":"main","catalogId":"https://a2ui.org/.../catalog.json"}}
{"version":"v1.0","updateComponents":{"surfaceId":"main","components":[...]}}
{"version":"v1.0","updateDataModel":{"surfaceId":"main","path":"/user/name","value":"Alice"}}
{"version":"v1.0","deleteSurface":{"surfaceId":"main"}}| Example | Description | Run |
|---|---|---|
01_hello_world |
Simplest A2UI program | cargo run -p a2ui --example 01_hello_world |
02_jsonl_stream |
JSONL stream processing & progressive rendering | cargo run -p a2ui --example 02_jsonl_stream |
03_data_binding |
JSON Pointer reactive data binding | cargo run -p a2ui --example 03_data_binding |
04_login_form |
Full form: inputs, validation, focus, actions | cargo run -p a2ui --example 04_login_form |
05_custom_function |
Custom catalog function implementation | cargo run -p a2ui --example 05_custom_function |
06_call_function |
Server-initiated callFunction & functionResponse |
cargo run -p a2ui --example 06_call_function |
07_action_response |
actionResponse with responsePath reactive updates |
cargo run -p a2ui --example 07_action_response |
12_handshake |
Capabilities-negotiation handshake | cargo run -p a2ui --example 12_handshake |
13_image |
Real image rendering (kitty / iTerm2 / Sixel / Halfblocks auto-degrade) | cargo run -p a2ui --example 13_image |
14_audio |
Interactive AudioPlayer (needs the audio feature) |
cargo run -p a2ui --example 14_audio |
15_date_time_input |
Interactive DateTimeInput | cargo run -p a2ui --example 15_date_time_input |
16_custom_component |
Custom component — implementing the TuiComponent trait |
cargo run -p a2ui --example 16_custom_component |
17_scifi_hud |
a2ui-driven cyberpunk HUD (see screenshot above) | cargo run -p a2ui --example 17_scifi_hud |
18_validate |
Payload validation: integrity / topology / parse_and_fix, STRICT vs RELAXED |
cargo run -p a2ui --example 18_validate |
20 examples in total (including the
07b/07cdebug variants) — full list incrates/a2ui/examples/.
Image rendering is built-in and on by default: a plain cargo build renders real images via ratatui-image (auto-degrading kitty / iTerm2 / Sixel / Halfblocks), local file paths only, falling back to the placeholder when unloadable. The following are additional opt-in features, OFF by default:
The desktop GUI backend lives in its own Slint Desktop Backend section above (a separate workspace member, not a ratatui feature).
| Feature | Description | Enable | Limitation |
|---|---|---|---|
audio |
Real audio playback via rodio (background thread) |
--features audio |
LOCAL file paths only; requires the ALSA system dev library (alsa-lib-devel on Fedora / libasound2-dev on Debian); silently falls back to the placeholder on failure |
| — (Video) | No feature exists for video | — | There is no mature TUI video solution, so Video always renders a placeholder |
a2ui-base is fully framework-agnostic — usable on its own for non-ratatui scenarios, or as the foundation for other backends (the project already builds the Slint desktop backend on top of it):
# Option 1: depend directly (most minimal, recommended for libraries)
cargo add a2ui-base a2ui-tui
# Option 2: via the umbrella (keeps a2ui:: paths)
cargo add a2uiuse a2ui_base::message_processor::MessageProcessor;
use a2ui_base::catalog::Catalog;
use a2ui_tui::catalogs::basic::{build_basic_catalog, build_basic_registry};
use a2ui_tui::surface::SurfaceRenderer;
// Create processor with Basic Catalog
let catalog = build_basic_catalog();
let registry = build_basic_registry();
let mut processor = MessageProcessor::new(vec![catalog]);
// Parse and process messages
let msg = MessageProcessor::parse_message(r#"{"version":"v1.0","createSurface":{...}}"#)?;
processor.process_message(msg)?;
// Render (within a ratatui Frame)
let surface = processor.model.get_surface("main").unwrap();
let renderer = SurfaceRenderer::new(surface, ®istry, &catalog);
renderer.render(&mut frame, area);Via the umbrella, just swap
a2ui_base::/a2ui_tui::fora2ui::core::/a2ui::tui::— everything else stays the same.
MIT










