DEV Community

Cover image for UI Framework for Humans and AI Agents: 6 Platforms, 900 KB, Rust/C/C++/ASM
Dmitry
Dmitry

Posted on AI-assisted

UI Framework for Humans and AI Agents: 6 Platforms, 900 KB, Rust/C/C++/ASM

I've been working on uison, a lightweight UI framework in Rust, and wanted to share it:

  • Tiny footprint: About 900 KB in size.
  • Cross-platform: Runs on Windows (x64, ARM), macOS, Linux, Android, iOS/iPadOS, and WASM.
  • Dual-mode rendering: Renders a graphical UI for humans or exposes the UI structure and actions via MCP (Model Context Protocol) as JSON, allowing AI agents to interact with it without screenshots.
  • SSH UI Forwarding: Thanks to the JSON representation, you can easily pipe and forward the UI over SSH.
  • Polyglot examples: Comes with usage examples in Rust, C, C++, and even ASM.

GitHub logo uisonlab / uison

A tiny (<900 KB) cross-platform Rust UI framework with C/C++/ASM bindings that compiles JSON layout diffs to native pixels, featuring MCP integration for AI agents and SSH UI forwarding.

uison

uison (UI + JSON) is a tiny, fast UI framework written in Rust with no dependencies at all — only the standard library, down to its own JSON parser. You describe a screen as a tree of elements: in a JSON file, with a Rust DSL, with C++ widgets in the style of Flutter or as JSON right inside C code. The logic is ordinary code in Rust, C or C++: handlers, data, threads. uison takes care of the window, layout, drawing and input, and the same program runs on macOS, Windows, Linux (Wayland and X11), iOS, Android…

How I Built It

I used Opus 5.5 in Visual Studio Code to build the framework. My role was to come up with the idea, give direction, and review and verify the results.

It took about three weekend days and roughly a week of evenings in my free time outside work. I tested it on all supported platforms, both automatically and manually.

The Key Idea: JSON Everywhere

Although passing JSON around has become something of a running joke, in this case I think it's justified. The protocol is simple from the lowest level up, so you don't have to spend much time learning it, and here that matters more than rendering speed (the framework is not for complex graphics).

The lowest layer of the framework can render only a JSON frame consisting of 9 primitives. Here they are:

Primitive Geometry Properties Description
rect [x, y, w, h] fill, stroke, width, radius Rectangle, including rounded corners
line [x1, y1, x2, y2] color, width, cap (butt / round) Line segment
circle [cx, cy, r] fill, stroke, width Circle
tri [x1, y1, x2, y2, x3, y3] fill, stroke, width Triangle (arrows, checkmarks)
poly [[x, y], [x, y], …] fill, stroke, width Closed polygon (chart sectors, custom shapes)
text "one line" at: [x, y], size, color, font, bold, italic Line of text; wrapping is handled by the screen layer
image [x, y, w, h] src, fit (cover / contain / fill), radius Image
clip [x, y, w, h] items: […] Group clipped to a rectangle (scrolling)
hit [x, y, w, h] id, cursor Invisible area for mouse and touch input (not drawn)

At the next level up, familiar controls such as Row, Text, IconButton, and others are added to JSON. As JSON containing complex elements passes down to the lowest level, it is transformed so that only these 9 primitives remain.

All of this made it possible to quickly adapt the framework to all 6 platforms known to humanity: Windows, macOS, Linux, Android, iOS and Browser (WASM).

Custom Controls

At the highest level, users can add their own controls in Rust or directly in JSON. At the lowest layer, the new controls become JSON consisting of the same 9 primitives.

Render Only What Changed (Damage Tracking)

Frames are compared element-by-element using LCS alignment to generate "damage" regions. The backend then redraws only those modified regions instead of the whole screen.

Thanks to built-in list virtualization, an inbox with 2,000 emails only ever renders the ~20 visible rows at any given time.

Building on this concept, I even made a lightweight web browser. It supports a subset of HTML and CSS, which is parsed into JSON and fed straight to the framework. uison knows nothing about HTML or CSS — it just receives standard JSON and renders it like any native UI.

lightweight web browser

UI over SSH

Since the interface is native JSON, your app can run completely headless. The program on the server knows nothing about SSH: it writes frames to stdout and reads user events from stdin. The server needs only sshd — no X11, VNC, or browser.

SSH player

The same screen as a raw JSON stream:

SSH player (JSON)

Taking It Further: Native Modes for AI Agents

The exact same binary runs in multiple modes with zero code changes: open a graphical window for humans (default), stream raw JSON over SSH (--console), or expose semantic state to AI agents and scripts (--agent / --mcp).

With --mcp, your app becomes a server for Claude Code or Cursor. Agents inspect the UI via snapshot, send actions like click or type, and receive only lightweight diffs, all while respecting UI rules like disabled buttons.

Size

Comparing two versions of a real open-source Disk Usage application for macOS, both written in Rust:

Both release builds use LTO, codegen-units = 1, panic = "abort" and strip = true. The uison version uses opt-level = "s", the egui version opt-level = 3. Part of the difference is that uison uses the system's text and graphics (CoreText, CoreGraphics on macOS) instead of bundling fonts and an OpenGL renderer.

Quick Start

Everything is covered in the tutorials: Rust, C, C++, and a brief introduction to ASM. Here I'll show how to get started with Rust.

Step 1. Create a project and add uison

cargo new hello && cd hello
cargo add uison
Enter fullscreen mode Exit fullscreen mode

Step 2. Replace src/main.rs

use uison::prelude::*;

fn main() {
    let screen = center(column().gap(12).cross("center").children([
        text("Pressed {@n} times").size(20),
        button("+1").key("inc").on_click(|cx| cx.data.edit("n", |n: &mut i32| *n += 1)),
    ]));
    let mut ui = Ui::from_node(screen).with_size(320.0, 200.0);
    ui.data.set("n", 0);
    run(ui, "Hello");
}
Enter fullscreen mode Exit fullscreen mode

Step 3. Run it

cargo run --release
Enter fullscreen mode Exit fullscreen mode

A window with a counter will open. {@n} is a data binding: the handler changes n, and the text updates automatically, without manual redrawing.

Status

The project is young (version 0.2.1) and the API may change. It's MIT licensed. Not done yet: accessibility (screen readers), IME for Chinese, Japanese and Korean on the desktop, right-to-left text, and more than one window per program.

Links

For anyone interested, everything you need can be found at these links:

I'd be glad to hear feedback on the API and the JSON format. Issues and stars are welcome!

Top comments (2)

Collapse
 
autenai profile image
Auten •

Question on the JSON format: are element ids stable across re-renders, or does the agent need to re-read the whole tree after every action? One thing worth considering: have each action return only what changed, so the agent can confirm the click landed without fetching the full UI again. (I'm on the Auten team; we build a computer-use MCP that drives existing apps through the screen, so stable references are the part we fight with most.)

Collapse
 
baturhanpro profile image
baturhan •

🚩