lay — локальный помощник ввода для Linux. Его главная задача: исправить
короткий хвост текста, набранный не в той раскладке, и сделать это без
clipboard, без облака и без попытки переписывать смысл текста.
Главный сценарий:
Набрал: ghbdtn
Нажал: Shift Shift
Стало: привет
Проект сейчас в статусе alpha: GNOME Wayland — основной проверенный путь, KDE/Plasma, Niri и X11 поддерживаются как более молодые backend-слои.
lay CLI
└─ разовая конвертация текста, explain/debug, smart CLI
lay-daemon
├─ слушает физическую клавиатуру через evdev
├─ держит короткий буфер слов
├─ ловит double Shift и другие триггеры
├─ вызывает decoder / typing pipeline
└─ выводит исправление через uinput или экспериментальный IME path
Desktop интеграции
├─ GNOME Shell extension: tray, DBus bridge, GNOME layout activation
├─ KDE tray/helper: Plasma menu и qdbus layout backend
├─ Niri backend: прямой IPC через niri-ipc
└─ X11 backend: native XKB через x11rb
lay-daemon не читает текст из активного приложения и не копирует его в буфер
обмена. Он восстанавливает хвост по собственному WordBuffer, который
собирается из физических key events.
Код делится на три слоя:
1. Core / decision
Распознаёт хвост, строит варианты, выбирает действие.
Не знает, что такое GNOME Shell, KDE, DBus или uinput.
2. Runtime / daemon
Слушает evdev, ведёт состояние, вызывает core и backend.
Не содержит словарных списков и не решает "на глаз".
3. Desktop / output backend
Переключает раскладку и выводит готовое действие в приложение.
Не принимает лингвистических решений.
Жёсткое правило: runtime-код не должен содержать пользовательские фразы как
правила. Лексика живёт в data/lexicon/*, тестовые сценарии — в
tests/fixtures/* и data/test_input/*, пользовательские правила — в
~/.config/lay/*.
src/core.rs— компактный facade общего ядра.src/config.rs— единая схема настроек daemon/tray/CLI.src/dict.rs— детерминированная RU/EN keyboard-map конвертация.src/decoder.rs— manual double-Shift decision layer.src/scoped_tail.rs— выбор и ранжирование хвоста из 1-3 слов.src/layout_autoswitch.rs— автоперекладка ASCII↔кириллица.src/typing_pipeline.rs— общий pipeline автопомощи после пробела.src/typing_rule_graph.rs— список правил, приоритеты и включение правил.src/typing_candidate.rs— scoring кандидатов автопомощи.src/typing_context.rs— контекст короткого хвоста вокруг слова.src/phrase_reader.rs— разбор склеенных/разрезанных русских слов.src/word_recognizer.rs— классификация токенов: RU, EN, protected, technical, mixed, number.src/lexicon.rs— загрузка словарных данных и пользовательских protected words.src/ru_typo.rs— семейства русских опечаток.src/russian_lexicon.rs— Hunspell и русские формы.src/lem.rs— Layout Error Metric для готовых вариантов.src/ngram.rs— char n-gram scorer.src/text_edit.rs— минимальные планы замены текста.src/keyboard.rs— key events, раскладочная карта и печать текста через uinput-события.
src/typing_assist.rs сейчас оставлен тонким публичным фасадом. Основная
логика уже вынесена в typing_pipeline, typing_rule_graph,
typing_candidate, typing_context, phrase_reader, layout_autoswitch и
ru_typo.
src/bin/lay_daemon.rs — только точка сборки модулей. Поведение разнесено по
src/bin/lay_daemon/*:
daemon_runtime.rs— главный event loop.daemon_state.rs— состояние процесса.keyboard_io.rs— evdev input.physical_input_grab.rs— временная изоляция физической клавиатуры при выводе.trigger_fsm.rsиmanual_trigger_runtime.rs— double Shift / multi-tap FSM.correction_runtime.rs— исполнение решения decoder.text_output.rs— uinput output contract.layout_controller.rs— переключение раскладки через выбранный backend.layout_kde.rs— KDE/Plasma через qdbus/qdbus6.layout_niri.rs— Niri через прямой IPC к compositor socket.layout_x11.rs— X11 через native XKB/fallback tools.focus_guard.rs— защита буфера между окнами/полями.pending_typing_assist.rs— отложенная автопомощь после пробела.learning_runtime.rs— feedback и продвижение пользовательских правок.action_log_runtime.rs— recent actions для tray.startup_runtime.rs— warmup, backend probes, service startup.
Цель такого разреза: daemon не должен снова превращаться в файл на тысячи строк, где смешаны ввод, вывод, правила, тесты и desktop-specific код.
На Wayland приложения изолированы. У обычного процесса нет универсального API:
- прочитать, что пользователь набирает в любом окне;
- получить слово под курсором;
- атомарно удалить и вставить surrounding text в любом toolkit.
Поэтому lay-daemon слушает /dev/input/event* через evdev. Это даёт
физические события клавиатуры до compositor/toolkit.
Для вывода используется /dev/uinput: виртуальная клавиатура ядра. Для
приложения это выглядит как обычный ввод с клавиатуры, поэтому работает шире,
чем xdotool/wtype.
Минусы этого пути:
- нужно членство пользователя в группе
input; - нужно право на
/dev/uinput; - вывод не является настоящим committed-text API;
- старые/особые окна могут терять часть событий, если backend ведёт себя нестандартно.
evdev key events
↓
WordBuffer
↓
trigger_fsm: press/release/press/release
↓
decoder
├─ replay all
├─ smart minimal replacement
└─ keep original
↓
correction_runtime
├─ при необходимости переключить layout
├─ удалить нужный хвост
├─ вставить/replay исправление
└─ синхронизировать WordBuffer с тем, что реально выведено
Ручной double Shift — явная команда пользователя. Поэтому последнее текущее слово/фрагмент можно переворачивать даже тогда, когда словарь считает его нормальным. Для соседних завершённых слов smart-режим действует осторожнее: нормальные слова оставляет, плохие меняет только при уверенном сигнале.
Автопомощь работает не на каждую клавишу, а после разделителя:
пользователь нажал Space
↓
daemon ждёт release Space
↓
строится snapshot завершённого хвоста
↓
typing_pipeline выбирает кандидата
↓
если кандидат безопасен — замена включает исходный separator
Контракт пробела жёсткий: если пользователь нажал пробел и автозамена сработала, итоговая замена обязана вернуть пробел в конец. Пробел не “компенсируется” произвольными задержками. Если пробел съеден, ошибка ищется в state/replacement contract.
Минимальная проверка этого класса:
html djn -> html вот
То есть после исправления первого токена буфер и экран должны оставаться синхронными, а следующий токен должен нормально отделяться пробелом.
Файл пользователя:
~/.config/lay/protected_words.txt
Слова из него являются стоп-сигналом для автоперекладки ASCII→RU. Это не “маленький вес” и не подсказка scorer-у. Если пользователь защитил токен, pipeline обязан оставить его как есть до любого LEM/LLM/ngram решения.
Пример:
cd -> cd
Даже если раскладочная замена cd -> св выглядит языково правдоподобной,
пользовательская защита сильнее scorer-а.
По умолчанию lay не является LLM-приложением.
Основной путь:
keyboard map
+ dictionaries
+ protected/technical-token recognizer
+ LEM/ngram scoring
+ conservative rule graph
LLM — optional arbiter для уже построенных вариантов. Она не должна свободно писать новый текст вместо пользователя. В штатном публичном поведении модель не загружается и не нужна для double Shift.
NANDA Wave — экспериментальный исследовательский слой поверх текущего детерминированного движка. Он не является включённым runtime-автокорректором и не заменяет safe replacement pipeline. Сейчас он нужен, чтобы изучать клетки, волновые признаки, ablation и ансамблевые режимы на реальном наборе кейсов.
Текущая схема:
fixtures / typing cases
↓
deterministic baseline
↓
NANDA Wave cells
↓
L1 sensors / L2 candidates / L3 consensus
↓
eval, ablation, status JSON
Базовая единица:
Cell32 = 32 KB wave cell
Текущий исследовательский статус можно посмотреть так:
lay-nanda-wave-eval --status-json
lay-nanda-wave-eval --trace "djn "
lay-nanda-wave-eval --real-suite --ablationПубличная UI-модель намеренно разделена:
Автоподмена— общий рубильник автоматических исправлений после пробела;NANDA ячейки— отдельное окно со статусом клеток, ролями и волновой схемой;- старого переключателя
Автокоррекция NANDAбольше нет: старый Expert64/ microbrain runtime удалён.
Клетки не печатают напрямую в приложение. В текущем виде они работают как исследовательский прибор:
- L1-сенсоры выделяют символьные, письменные, клавиатурные и граничные моды;
- L2-клетки строят кандидаты;
- L3-клетки согласуют или гасят решение;
- status JSON показывает gate, клетки, роли, delta и ablation.
Главный критерий развития NANDA — не “добавить ещё правил”, а доказать ансамблевую моду: связка клеток должна давать пользу, которую одиночная клетка не даёт, и эта польза должна исчезать при ablation ключевой клетки.
Настройка:
{
"layout_backend": "auto"
}Значения:
auto— выбрать backend по окружению;gnome— GNOME Shell extension + DBus bridge;kde— Plasma черезqdbus/qdbus6;niri— Niri Wayland через прямойniri-ipc;x11— native XKB черезx11rbи fallback tools.
GNOME Wayland — самый зрелый путь. KDE/Niri/X11 работают через те же core-модули, но имеют отдельные desktop edge cases, поэтому остаются менее зрелыми.
GNOME extension теперь разделён на модули:
extension.js— loader.lay-impl.js— tray indicator и сборка меню.tray_support.js— config, stats, update helpers, общие константы.dbus_service.js— session-local DBus service и layout helpers.recent_actions_menu.js— меню последних исправлений.prefs.js/settings.js— настройки.
Установщик и dev-reload копируют все *.js из extension-директории. CI делает
node --check для всех JS-модулей, чтобы новый файл не выпал из проверки.
GNOME Shell нужен только потому, что именно Shell умеет вызвать
inputSources[i].activate() внутри сессии GNOME Wayland.
Упрощённый путь:
lay-daemon
↓ zbus session call
GNOME extension DBus service
↓
GNOME Shell inputSources[i].activate()
DBus внутри user session не является защитой от процессов того же пользователя. Это локальный control channel между daemon и extension.
Production path — uinput:
delete tail with Backspace
switch layout if needed
type/replay replacement
sync WordBuffer
Если daemon успешно изолировал физическую клавиатуру (evdev grab), короткий
вывод можно делать быстрее: собственный виртуальный ввод не смешивается с
зажатым физическим Shift. Это заменило старую грубую задержку ожидания Shift.
Экспериментальный IME path существует и доступен через настройку Режим ввода,
но не является default:
{
"text_backend": "ime"
}IME нужен для committed-text замен и preedit-кандидатов прямо в поле ввода. Он остаётся экспериментальным backend-ом: быстрый uinput-путь по-прежнему основной и должен работать независимо от IME.
learning_log — opt-in.
Что пишется:
- явные ручные исправления double Shift;
- user-correction после того, как пользователь удалил результат auto/smart правки и ввёл свой вариант.
Что не пишется:
- полный поток набора;
- clipboard;
- приватный текст окон;
- обратные replay-toggle пары, которые только возвращают текст назад.
Локальные файлы пишутся приватно (0600), где это применимо:
~/.local/share/lay/corrections.jsonl;~/.local/share/lay/recent_actions.jsonl;~/.local/share/lay/learning_candidates.json;~/.local/share/lay/stats.json.
Production runtime не хранит большие списки в match/if по словам.
Основные данные:
data/lexicon/common_en_technical.txt;data/lexicon/common_ru.txt;data/lexicon/russian_*;data/test_input/builtin_scripts.tsv;tests/fixtures/*.
Если нужен новый класс поведения, сначала добавляется общий rule/scorer/guard, а конкретная фраза остаётся только в тестах.
Перед push минимальный gate:
cargo fmt --all
cargo test -q --all-targets
cargo clippy --all-targets -- -D warnings
cargo build --release --bins
git diff --check
for js in extension/lay@radislabus-star.github.io/*.js; do node --check "$js"; done
bash -n install.sh update.sh dev-reload.sh scripts/*.shДля KDE/Niri/X11 изменений дополнительно:
- clean-copy проверка в VM или отдельной папке;
- Rust tests/clippy/build на этой копии;
- smoke CLI/typing-assist кейсы;
- по возможности ручная проверка tray/backend в живой desktop-сессии.
- Поддержка языков сейчас рассчитана на RU/EN.
- Слово под курсором без предварительного набора
layне читает. - Выделенный текст не является текущим production path.
- IME/preedit уже есть как экспериментальный backend, но не должен становиться отдельным "мозгом": решение строит общий core, IME только отображает и применяет готовое действие.
- KDE/Niri/X11 моложе GNOME Wayland и требуют больше реальных отчётов.
- Автопомощь после пробела намеренно консервативна: лучше пропустить сомнительную правку, чем испортить нормальный текст.