UI-фреймворк для людей и AI-агентов: Rust, C, C++, ASM, 6 платформ, 900 КБ. Assembler.. Assembler. C++.. Assembler. C++. json.. Assembler. C++. json. mcp.. Assembler. C++. json. mcp. Open source.. Assembler. C++. json. mcp. Open source. rust.. Assembler. C++. json. mcp. Open source. rust. ui-фреймворк.. Assembler. C++. json. mcp. Open source. rust. ui-фреймворк. uison.. Assembler. C++. json. mcp. Open source. rust. ui-фреймворк. uison. интерфейсы.. Assembler. C++. json. mcp. Open source. rust. ui-фреймворк. uison. интерфейсы. искусственный интеллект.. Assembler. C++. json. mcp. Open source. rust. ui-фреймворк. uison. интерфейсы. искусственный интеллект. сезон код будущего.

Продолжаю воплощать свои самые смелые идеи, на которые ранее не было ресурсов и которые теперь можно реализовать благодаря появлению LLM. Я верю, что сейчас ценность самой идеи стала выше и именно она всё решает.

Сегодня хочу представить лёгкий UI-фреймворк. Он написан на Rust, работает на Windows (x64, ARM), macOS, Linux, Android, iOS/iPadOS и даже в браузере через WASM (онлайн-демо на GitHub Pages). Снабжён примерами использования на Rust, C, C++ и даже ASM.

Отличительная особенность: фреймворк имеет два формата – либо отображает графический UI для людей, либо же рендерит UI в JSON так, чтобы с ним могли взаимодействовать AI-агенты без снятия скриншотов. Эта же фишка позволила добавить возможность перебросить UI по SSH (подробнее ниже).

Ключевая идея – JSON во всём

Хотя перекладывание JSON-ов является притчей во языцех, в данном случае я посчитал его оправданным. Простота протокола, начиная с нижнего уровня, позволяет не тратить много времени на его изучение, и в данном случае это важнее скорости рендера (фреймворк предназначен для утилит, а не для сложной графики).

Нижний уровень фреймворка умеет рендерить только JSON-кадр из 9 примитивов. Вот они:

Примитив

Геометрия

Свойства

Описание

rect

[x, y, w, h]

fill, stroke, width, radius

прямоугольник, в том числе со скруглёнными углами

line

[x1, y1, x2, y2]

color, width, cap (butt / round)

отрезок

circle

[cx, cy, r]

fill, stroke, width

круг

tri

[x1, y1, x2, y2, x3, y3]

fill, stroke, width

треугольник (стрелки, галочки)

poly

[[x, y], [x, y], …]

fill, stroke, width

замкнутый многоугольник (секторы графиков, свои фигуры)

text

"одна строка"

at: [x, y], size, color, font, bold, italic

строка текста; перенос делает слой экранов

image

[x, y, w, h]

src, fit (cover / contain / fill), radius

картинка

clip

[x, y, w, h]

items: […]

группа, обрезанная по прямоугольнику (прокрутка)

hit

[x, y, w, h]

id, cursor

невидимая область для мыши и касаний (не рисуется)

Уровнем выше в JSON добавляются привычные нам элементы управления, такие как Row, Text, IconButton и прочие. JSON со сложными элементами при спуске на нижний уровень будет преобразован так, что останутся только эти 9 примитивов.

Всё это позволило быстро адаптировать фреймворк под все 6 платформ человечества:

платформа

бэкенд

macOS

AppKit, CoreGraphics, CoreText, ImageIO через рантайм Objective-C

iOS

UIKit, CADisplayLink, тот же код CoreGraphics/CoreText, что и на macOS

Android

JNI вручную, android.graphics.Canvas, прослойка на Java ~300 строк; APK собирается без Gradle

Windows

Win32, Direct2D, DirectWrite и WIC, COM вручную; DPI для каждого монитора, тёмный заголовок

Linux

свой код протокола Wayland и X11 (xcb), Cairo + Pango, xkbcommon – всё через dlopen, поэтому бинарник зависит только от libc

Браузер

wasm32-unknown-unknown без wasm-bindgen, canvas 2D; C/C++ через Emscripten

Для добавления платформы нужно реализовать 9 методов (по одному для каждого из примитивов) средствами платформы, а также добавить обработку событий.

Свои элементы управления

На самом высоком уровне пользователи могут добавлять в JSON свои элементы управления вот так:

#include <uison_widgets.hpp>

using namespace uison::widgets;

template <class F>
uison::Node Pill(std::string text, F on_click) {
    return Box({
        .padding = {8, 22},
        .fill = uison::json(R"({ state: {
            normal: "$theme.primary", hover: "$theme.primaryHover", pressed: "$theme.primaryPressed" } })"),
        .radius = 999,  // more than half the height: the ends become semicircles
        .align = "center",
        .interactive = true,
        .on_click = on_click,
        .child = Text(text, {.bold = true, .color = "$theme.onPrimary"}),
    });
}

int main() {
    auto screen = Column({.padding = 24, .gap = 16, .cross = "start", .children = {
        Text("Pressed {@n} times", {.size = 20}),
        Pill("Press me", [](uison::Cx& cx) {
            cx.data().set("n", cx.data().get<int>("n") + 1);
        }),
    }});
    uison::Ui ui(screen, {.size = {360, 140}, .data = uison::obj{{"n", 0}}});
    return ui.run("Pill");
}

Или прямо в JSON:

{
  Pill: {
    params: { text: "" },
    interactive: true,  // the node gets hover / pressed states and a click
    body: {
      Box: {
        padding: [8, 22],
        radius: 999,  // more than half the height: the ends become semicircles
        align: "center",
        fill: { state: {
          normal: "$theme.primary",
          hover: "$theme.primaryHover",
          pressed: "$theme.primaryPressed",
        } },
        child: { Text: { text: "$text", bold: true, color: "$theme.onPrimary" } },
      },
    },
  },
}

Все эти новые элементы, дойдя до нижнего слоя, будут преобразованы в JSON из 9 тех самых примитивов, которые приведены в таблице выше.

Рендерим только то, что изменилось

Новый кадр сравнивается со старым поэлементно (выравнивание LCS, группы clip с одинаковым прямоугольником сравниваются внутри). Изменённые, добавленные и удалённые элементы дают области “повреждения”, и бэкенд перерисовывает только их.

К примеру, благодаря виртуализации списков кадр почты из 2000 писем содержит пару десятков строк.

Насколько жизнеспособна идея?

На основе этой же идеи удалось реализовать даже простой браузер. HTML и CSS преобразуются в JSON, после чего результат просто скармливается нашему фреймворку.

Сам uison при этом ничего не знает про HTML и CSS. Он получает привычный JSON и отображает его точно так же, как интерфейс обычного приложения.

Таким образом, один и тот же механизм позволяет рисовать как обычные приложения с кнопками и таблицами, так и веб-страницы.

Wiki в JSON

Wiki в JSON

stdin и stdout вместо окна: UI по SSH

Поскольку в самом низу у нас JSON, его можно вообще не рендерить, а отдавать как есть – в консоль в голом виде. Это позволяет простым образом запустить программу на сервере и работать с ней по SSH так, как будто она работает на вашем компьютере.

Программа на сервере не знает про SSH. Она пишет кадры в stdout и читает события из stdin. Серверу нужен только sshd, ни X11, ни VNC, ни браузера.

Плеер умеет сам загрузить программу на сервер и запустить её с –console.

Плеер uison

Плеер uison

Плеер оформлен отдельным проектом. Его можно собрать даже для Android и управлять программой через телефон. В отдельной вкладке можно посмотреть JSON-события и кадры рендера, которые передаются между клиентом и сервером:

JSON рендер и события плеера

JSON рендер и события плеера

Развитие идеи: режим для AI-агентов

Одно и то же приложение может работать в нескольких режимах без изменения исходного кода. Оно может открыть обычное графическое окно для человека, передавать JSON-кадры внешнему рендереру или предоставлять семантическое описание интерфейса AI-агенту.

Запуск

Кто на том конце

Что идёт в stdout

todo

Человек

Ничего: открывается окно

todo --console

Плеер, который рисует

JSON-кадры: прямоугольники, текст, координаты

todo --agent

Скрипт или AI-агент

JSON со смыслом: роли, имена, состояния

todo --mcp

Claude Code и другие MCP-клиенты

Тот же смысл, но в протоколе MCP

С флагом –mcp программа становится MCP-сервером, то есть набором инструментов для Claude Code, Cursor и других AI-клиентов. Подключается одной командой claude mcp add todo – ./todo –mcp, кода писать не нужно. Агент вызывает snapshot и получает экран текстом, а потом действует инструментами click, type, choose. В ответ приходит только то, что изменилось. Например, агент вписывает «Milk» в поле и жмёт Enter:

→ {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"type","arguments":{"key":"draft/1/0","text":"Milk","submit":true}}}
← {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"oknchanges (+ added, - removed, ~ changed):n+ checkbox "Milk" #4/0 uncheckedn+ button "trash" #del-4n+ text "3"n+ text "1 of 4 done"n+ text "Remaining: 3"n- text "2"n- text "1 of 3 done"n- text "Remaining: 2"n~ #draft/1/0 focusedn"}],"isError":false}}

В списке появились пункт «Milk» и его кнопка удаления, а счётчик сменился с «1 of 3» на «1 of 4». Нажать неактивную кнопку агент не сможет, как не смог бы человек: программа ответит, что кнопка неактивна.

Размер и скорость

Покажу на примере реальной программы с открытым кодом – Disk Usage на macOS. Она создана в двух вариантах: на egui и на uison, оба на Rust. Вот результат тестов:

Показатель

egui (оригинал)

uison

Размер программы, macOS arm64

5,1 МБ (5 126 464 Б)

0,9 МБ (923 216 Б), −82 %

Размер с opt-level = "s"

4,5 МБ (4 450 560 Б)

0,9 МБ, примерно в 5 раз меньше

Зависимости в Cargo.lock

362

0 внешних (только крейты uison)

Память пустого окна, без сканирования

122 МБ

55 МБ

Память после сканирования

152 МБ

97 МБ, −36 %

Пик памяти во время сканирования

167–176 МБ

183–184 МБ

Строк Rust

3 198

3 547 + 479 строк JSON экрана

Быстрый старт

Всё есть в уроках на русском языке. Уроки для Rust, C, C++, а также краткая вводная для ASM.

Здесь приведу, как стартануть на Rust.

Шаг 1. Создаём проект и добавляем uison

cargo new hello && cd hello
cargo add uison

Шаг 2: Заменяем 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");
}

Шаг 3: запускаем

cargo run --release

Откроется окно со счётчиком. {@n} – привязка к данным: обработчик меняет n, и текст обновляется сам, без ручной перерисовки.

Чтобы программа была маленькой, добавьте в конец Cargo.toml профиль сборки:

[profile.release]
opt-level = "s"
lto = true
codegen-units = 1
panic = "abort"
strip = true

Итоги и ссылки

Фреймворк не ставит своей целью заменить Qt или другие полноценные GUI-фреймворки. uison в первую очередь рассчитан на утилиты, инструменты разработчика и обычные приложения с кнопками, списками, таблицами и формами. Для игр и сложной графики лучше использовать другие решения.

Кому интересно, все нужное можно найти по ссылкам:

Автор: codecity

Источник