- Добавить GlobalKeyboardService — выделенный поток с блокирующим Console.ReadKey, единственный источник клавишных событий для TUI - Убрать FocusManager из App.razor: перехватывал Tab до компонентов - Удалить @onkeydown с <Select>: RazorConsole не пробрасывает Tab/стрелки через этот механизм - Использовать FocusedValue вместо Value в Select для корректной подсветки - Обновить CLAUDE.md и AGENTS.md: архитектура TUI, RazorConsole gotchas - Добавить docs/tui_log.md: разбор проблемы и справочник по RazorConsole Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
75 lines
2.3 KiB
C#
75 lines
2.3 KiB
C#
using Microsoft.Extensions.Hosting;
|
||
|
||
namespace LazyBear.MCP.TUI;
|
||
|
||
/// <summary>
|
||
/// Фоновый сервис для глобального чтения клавиш консоли.
|
||
/// Единственный источник клавишных событий для всего TUI.
|
||
///
|
||
/// Использует выделенный поток с блокирующим Console.ReadKey — никакого
|
||
/// polling-а, нет обращений к Console.KeyAvailable, которые захватывают
|
||
/// консольный mutex и мешают рендерингу RazorConsole.
|
||
/// </summary>
|
||
public sealed class GlobalKeyboardService : IHostedService, IDisposable
|
||
{
|
||
public event Action<ConsoleKeyInfo>? OnKeyPressed;
|
||
|
||
private Thread? _thread;
|
||
private volatile bool _stopping;
|
||
|
||
public Task StartAsync(CancellationToken cancellationToken)
|
||
{
|
||
if (Console.IsInputRedirected)
|
||
{
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
_thread = new Thread(ReadLoop)
|
||
{
|
||
IsBackground = true,
|
||
Name = "KeyboardReader"
|
||
};
|
||
_thread.Start();
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
public Task StopAsync(CancellationToken cancellationToken)
|
||
{
|
||
_stopping = true;
|
||
// Console.ReadKey не поддерживает CancellationToken — поток
|
||
// завершится сам при выходе приложения (IsBackground = true).
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
private void ReadLoop()
|
||
{
|
||
while (!_stopping)
|
||
{
|
||
try
|
||
{
|
||
// Блокирующий вызов: не нагружает CPU и не трогает консольный
|
||
// mutex в паузах между нажатиями — рендеринг не страдает.
|
||
var key = Console.ReadKey(intercept: true);
|
||
if (!_stopping)
|
||
{
|
||
OnKeyPressed?.Invoke(key);
|
||
}
|
||
}
|
||
catch (InvalidOperationException)
|
||
{
|
||
// stdin стал недоступен (перенаправление и т.п.)
|
||
break;
|
||
}
|
||
catch
|
||
{
|
||
Thread.Sleep(100);
|
||
}
|
||
}
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_stopping = true;
|
||
}
|
||
}
|