# TUI Keyboard Handling — разбор и решение ## Проблема После внедрения RazorConsole TUI не работали два базовых взаимодействия: - **Tab** не переключал вкладки (Overview / Logs / Settings) - **Стрелки** не навигировали по спискам ## Диагностика ### Что проверяли Изучили исходную архитектуру: каждый дочерний компонент (`OverviewTab`, `LogsTab`, `SettingsTab`) рендерил `` не работает для служебных клавиш.** `Select` получает `@onkeydown` как часть `AdditionalAttributes`. Стрелки и Enter компонент обрабатывает внутренне (переключает `FocusedValue`), наружу через callback они не выходят. 2. **Tab перехватывается FocusManager до `` - Добавлен `FocusedValue="@GetNormalizedIndex()"` — чтобы индикатор `>` отображался рядом с текущим элементом, управляемым извне ## Проблема производительности (тормоза) Первая реализация `GlobalKeyboardService` использовала polling: ```csharp // ❌ Было — 100 вызовов/сек Console.KeyAvailable while (!stoppingToken.IsCancellationRequested) { if (Console.KeyAvailable) OnKeyPressed?.Invoke(Console.ReadKey(intercept: true)); else await Task.Delay(10, stoppingToken); } ``` `Console.KeyAvailable` под капотом захватывает консольный mutex каждые 10ms. RazorConsole для рендеринга тоже обращается к консоли — возникала конкуренция, UI тормозил. **Исправление** — блокирующий поток без polling: ```csharp // ✓ Стало — выделенный поток, спит на уровне ОС между нажатиями _thread = new Thread(ReadLoop) { IsBackground = true, Name = "KeyboardReader" }; void ReadLoop() { while (!_stopping) OnKeyPressed?.Invoke(Console.ReadKey(intercept: true)); // блокирует без mutex-а } ``` Между нажатиями поток блокируется на уровне ОС, консольный mutex свободен, рендеринг не конкурирует ни за какой ресурс. ## Итоговые изменения | Файл | Что сделано | |------|-------------| | `TUI/GlobalKeyboardService.cs` | Создан. Блокирующий ReadKey в выделенном потоке | | `Program.cs` | Регистрация `GlobalKeyboardService` как singleton + hosted service | | `TUI/Components/App.razor` | Убран FocusManager. Подписка на GlobalKeyboardService. Sync key handler | | `TUI/Components/OverviewTab.razor` | Убран OnKeyDown. Добавлен FocusedValue | | `TUI/Components/LogsTab.razor` | Убран OnKeyDown. Добавлен FocusedValue | | `TUI/Components/SettingsTab.razor` | Убран OnKeyDown. Добавлен FocusedValue | | `TUI/Components/_Imports.razor` | Убран `@using RazorConsole.Core.Focus` | | `InspectRazorConsole/` | Временный проект для инспекции DLL (можно удалить) | ## Вывод по RazorConsole 0.5.0 Фреймворк хорошо подходит для рендеринга (Spectre.Console под капотом), но клавишные события — слабое место. `@onkeydown` на интерактивных компонентах не даёт полного контроля: служебные клавиши перехватываются внутри. Для любого нетривиального TUI с собственной навигацией нужен независимый читатель консольного ввода — либо свой поток с `Console.ReadKey`, либо (если появится) публичный API глобального перехвата в будущих версиях фреймворка. --- ## Ключевые моменты при работе с RazorConsole (0.5.0) Справочник для написания кода и диагностики багов. ### Архитектура и рендеринг - **Рендеринг — Spectre.Console под капотом.** Компоненты транслируются через VDOM-слой в `IRenderable` объекты Spectre. Всё, что умеет Spectre.Console (цвета, границы, таблицы, разметка), доступно через Razor-компоненты. - **`AutoClearConsole = true` в `ConsoleAppOptions`** — обязательно, иначе при перерисовке вывод накапливается и экран «растёт». Устанавливается через `services.Configure(...)` внутри `UseRazorConsole`. - **`EnableTerminalResizing = true`** — без этого изменение размера окна не триггерит перерисовку. Нужно для корректного поведения `Console.WindowHeight` в вычислениях viewport. - **`AfterRenderAsync`** — хук после каждого рендера. Единственное место, где гарантированно можно записать в stdout после того, как RazorConsole завершил свой вывод. Используется, например, чтобы скрыть курсор (`Console.CursorVisible = false` + ANSI `\e[?25l`). - **`StateHasChanged()` нужно вызывать через `InvokeAsync`** при вызове из фонового потока или подписчика события. Иначе — исключение о вызове не из диспетчера компонента: ```csharp // ✓ из любого потока InvokeAsync(() => { /* изменить стейт */ StateHasChanged(); }); ``` - **Частые `StateHasChanged` → видимые тормоза.** Каждый вызов — полная перерисовка терминала (очистка + вывод). При высокочастотных событиях (потоковые логи, таймеры) нужна дебаунс-логика или throttle: планировать один рендер на «пакет» событий, а не по одному на каждое. ### Компоненты #### `` не работает для служебных клавиш** (Tab, Arrow, Enter, Space). Эти клавиши потребляются компонентом или FocusManager до того, как попадают в callback. Не использовать `@onkeydown` на `