The Jerusalem PostSwitzerland looking at Barak MX, SAMP/T NG for missile defensePunchNSCDC suspends officer over Abuja market shootingESPNTransfer rumors, news: Everton eye loan for Man United's RashfordBollywood HungamaBhumi Pednekar reunites with Akshay Kumar for Priyadarshan’s next comedyCNN TürkMuğla'da askeri gemiler ziyarete açıldıZDF heuteAktuelle Pressemitteilungen des ZDFSBS 뉴스'다문화 가정 정책' 논란 커지자…이 대통령 직접 반박NHK 社会浜岡原発地震データ問題で原子力規制委が不正防止対策導入決定سكاي نيوز عربيةالشرطة ترد على الليكود.. ما أدلة "سرقة الانتخابات"؟Golem.dePico11: Dieses Windows 11 passt auf eine CD-ROMn-tvErster World-Series-Titel: Brewers-Ekstase vor der großen Winter-Depression im BaseballGMA NewsMagnitude 5.6 earthquake hits off Surigao del Sur
The Daily Newsstand · Free, Always
Thursday, October 29, 2026

UI-фреймворк для людей и AI-агентов: Rust, C, C++, ASM, 6 платформ, 900 КБ

Translate

Продолжаю воплощать свои самые смелые идеи, на которые ранее не было ресурсов и которые теперь можно реализовать благодаря появлению 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":"ok\nchanges (+ added, - removed, ~ changed):\n+ checkbox \"Milk\" #4/0 unchecked\n+ button \"trash\" #del-4\n+ 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 focused\n"}],"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 в первую очередь рассчитан на утилиты, инструменты разработчика и обычные приложения с кнопками, списками, таблицами и формами. Для игр и сложной графики лучше использовать другие решения.

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

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.