Перейти к содержимому

Введение

Я не верю в GUI на Rust. Тем, кто хочет писать интерфейс, а не изобретать велосипед, скажу: берите Tauri или Electron.

Сам я начинал с вопроса «на чём писать GUI?» и, недолго думая, дошёл до Slint. Если нужно только нарисовать интерфейс, Rust — выбор ещё терпимый. Но эту потребность перерастаешь быстро, а за ней встаёт россыпь задач не менее важных: где хранить состояние, как обновляться, и ещё десяток мелочей. В Tauri они решены, а на нативном десктопе не решены вовсе.

Эта библиотека — мой ответ, один из десятка возможных, на вопрос «как сохранить ширину сайдбара между запусками?». Я читал исходники Halloy, Sniffnet и проектов помельче. Каждый решает этот вопрос на месте, под свои нужды, и, на мой взгляд, кривовато. Я решил поднапрячься — то есть поднапрячь видеокарты на серверах Anthropic — и решить задачу чуть системнее.

amethystate хранит состояние GUI-приложения на Rust — реактивное в памяти, сохранённое на диске. Вы описываете нужное состояние одной структурой; сохранение, реактивность и миграции — то, ради чего библиотека и существует.

Всё это держит store: файл на диске вместе с тем, что о нём знает память, — буфером записей и подписками. Store открывают при запуске, и структуры живут в нём.

#[amethystate(prefix = "network")]
pub struct NetworkState {
#[amestate(default = "127.0.0.1".to_string())]
pub host: String,
#[amestate(default = 8080u16)]
pub port: u16,
}

state.port().set(9090) возвращается к тому, кто уже может прочитать 9090, и к подписчикам, которые уже об этом узнали. Диск догоняет сам.

К подписчикам port — и больше ни к кому. Подписываются на поле, на ключ карты или на путь, но не на структуру и не на store. Запись в host не дойдёт до того, кто слушал port: вид перерисовывается, когда изменилось то, что он рисует, а не что-то рядом.

Куда лечь файлу. Store, открытый по имени, ложится туда, где система держит данные приложений, с тем расширением, которое хочет его движок — то, что пишет значения на диск: redb, SQLite или текстовый документ.

Когда писать. Так же, как ОС обходится с файлом через страничный кэш: запись ложится в память, читают её оттуда сразу, а на диск её потом переносит сброс. Сброс идёт раз в окно, отсчитанное от первой несохранённой записи: ползунок, протащенный по всей шкале, — сброс раз в несколько сотен миллисекунд, а не на каждом кадре. Есть и свой fsync — save_now: он ждёт, пока всё, что store держит, ляжет на диск. Закрываясь, store дописывает всё, что держал.

Чем значение станет на диске. Пять движков, три ответа: redb кладёт значение в MessagePack, SQLite — в JSON, а текстовые пишут его прямо в документ, на языке этого документа. Объявление для всех пяти одно и то же, а о том, чего формат не умеет хранить, вам скажут, а не оставят выяснять потом.

Где поправить значение. rule на поле стоит на каждом входе и выходе: значение прочитали при запуске, файл поправили руками, ваш код что-то записал. Правило не отказывает, а поправляет, и поправку записывают обратно, так что на экране и в файле не бывает разного. Инвариант между двумя полями держат там, где структуру открывают, — в собственном Open.

Что делать, если структура изменилась. Форма каждой структуры записана рядом с данными. Поднялась версия — структуру проводят через объявленные вами шаги. Поля изменились, а версия та же — это дрейф: о расхождении скажут с диффом, и запуск продолжится.

Что делать, если файл правили снаружи. Текстовые движки за ним следят. Правка со стороны приходит теми же событиями, что и запись, а сохранение, встретившее её, кладёт свои записи поверх того, что в файле сейчас, а не затирает его.

Различаются они одним — хендл поле или нет, — а хранение, схема и миграции у них общие.

  • Реактивное — поле это хендл: объект, через который поле читают, пишут и на него подписываются. Дальше книга подразумевает этот вид.
  • Persistent-only — обычные поля обычной структуры, сохраняются вручную. Для фреймворков с явным циклом отрисовки и для состояния, за которым некому следить. Об изменениях, сделанных где-то ещё, оно не узнаёт.

Это для состояния, которое приложение держит о самом себе: раскладка окна, открытая панель, фильтр, список недавних, кэш между запусками. Десятки ключей — его наименьший случай, но не единственный: кэш — это тысячи, записанные пачками и читаемые сканами, и на этот конец библиотека тоже рассчитана.

Это не база данных. Ни запросов, ни индексов, ни транзакций, которые вы могли бы открыть: путь и значение — вся модель данных. Store открывайте сколько угодно — движки расходятся не в этом, а в том, что делать с двумя на одном файле: redb второй не пускает, SQLite файл удерживает, а текстовые впускают обоих и разбираются при сохранении.

Быстрый старт — кратчайший путь от пустого места до работающего store.