Approach
Use a background async read pattern inside WindowsVTInputHelper: a background task calls Stream.ReadAsync in a loop, buffering results into a concurrent collection. The existing Peek()/TryRead() API continues to work but reads from the buffer instead of P/Invoke. This avoids needing to change InputImpl or IInput interfaces.
Current Architecture (for reference)
Input pipeline flow
- Input thread (
InputImpl.Run()): loops while (Peek()) { Read() } with 20ms delay
AnsiInput.Peek() → delegates to WindowsVTInputHelper.Peek() for WindowsVT platform
AnsiInput.Read() → calls WindowsVTInputHelper.TryRead(buffer, out bytesRead), converts bytes to UTF-8 chars, yields them
InputImpl.Run() enqueues yielded chars into InputQueue (a ConcurrentQueue<char>)
- Main thread (
ProcessQueue()): dequeues from InputQueue, calls AnsiInputProcessor.Process(char) → Parser.ProcessInput() → matches ANSI responses, raises keyboard/mouse events
ANSI request/response flow
AnsiSizeMonitor.SendSizeQuery() → AnsiRequestScheduler.SendOrSchedule() → writes ESC[18t to stdout via driver.WriteRaw()
- Terminal responds with ESC[8;height;widtht on stdin
- Response bytes flow through the input pipeline (steps 1-5 above)
AnsiResponseParser matches the response terminator/value and invokes ResponseReceived callback
- Scheduler has 100ms throttle, 1s stale timeout for unanswered requests
Initialization order (MainLoopCoordinator)
StartInputTaskAsync(app) is called
RunInput(app) starts on background thread (acquires lock):
- Creates
AnsiInput → WindowsVTInputHelper.TryEnable() (opens handle, sets console mode)
- Calls
BuildDriverIfPossible() (driver built only if output also exists)
- Releases lock, then calls
_input.Run() (starts Peek/Read loop)
BootMainLoop(app) runs on main thread (acquires lock):
- Creates
_output
- Calls
BuildDriverIfPossible() → if both input+output exist, builds driver
SizeMonitor.Initialize(_driver) → sends initial size query
_startupSemaphore.Release()
StartInputTaskAsync awaits semaphore, then returns
- Main event loop starts →
RunIteration() → ProcessQueue() begins processing
Key files
Terminal.Gui/Drivers/WindowsDriver/WindowsVTInputHelper.cs — the file to modify
Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs — calls Peek() (line 170) and TryRead() (line 195)
Terminal.Gui/Drivers/Input/InputImpl.cs — the Peek/Read/20ms-delay loop
Terminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs — processes chars through parser
Terminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs — manages ANSI request lifecycle
Terminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs — sends CSI_ReportWindowSizeInChars
Terminal.Gui/App/MainLoop/MainLoopCoordinator.cs — initialization and threading
VTTest Reference Implementation
VTTest/AsyncStreamInputReader.cs is the pattern to follow:
- Uses
Console.OpenStandardInput() on Windows
- Background
Task.Run(async () => { ReadAsync loop }) with CancellationToken
BlockingCollection<(byte[] Data, int Count)> (not ConcurrentQueue) — consumer blocks via _queue.Take()
- Consumer
Read() method BLOCKS until data available (unlike our TryRead which is non-blocking)
ReadAsync(buf.AsMemory(0, buf.Length), _cts.Token) — uses Memory<byte> overload
- Always copies buffer before enqueuing:
var copy = new byte[n]; Array.Copy(buf, 0, copy, 0, n);
- Enqueues 0-byte reads as-is (Ctrl+Z workaround handled by consumer)
- Calls
_queue.CompleteAdding() in finally block when loop exits
- Dispose: cancel → wait 1s → dispose stream → dispose queue → dispose CTS
Changes to WindowsVTInputHelper.cs
Remove:
ReadFile P/Invoke declaration
GetNumberOfConsoleInputEvents P/Invoke declaration
Keep:
GetStdHandle, GetConsoleMode, SetConsoleMode (still needed — no .NET equivalent for VT input mode flags)
- All console mode constants and setup logic
InputHandle property (used internally for Get/SetConsoleMode)
Add:
using System.Collections.Concurrent;
Stream? _stdinStream — opened via Console.OpenStandardInput()
CancellationTokenSource? _cts for clean shutdown
Task? _readTask for the background read loop
ConcurrentQueue<(byte[] Buffer, int Count)> _readQueue for buffering
ReadLoopAsync(CancellationToken ct) — background method:
- Calls
_stdinStream!.ReadAsync(buffer.AsMemory(0, buffer.Length), ct) in a loop
- Copies data before enqueuing
- For 0-byte reads (Ctrl+Z bug): enqueues
([0x1A], 1)
- Catches
OperationCanceledException for clean shutdown
Modify:
TryEnable() — after setting console mode, open stdin stream and start background task
Peek() — !_readQueue.IsEmpty instead of GetNumberOfConsoleInputEvents
TryRead() — _readQueue.TryDequeue() instead of ReadFile
Restore() — cancel background task, wait, dispose stream/CTS, then restore console mode
No changes to AnsiInput.cs, InputImpl.cs, or IInput.
Known Issues from First Attempt
The implementation compiled and passed all unit tests (14520 parallelizable + 1000 non-parallel) but broke ANSI request/response at runtime. Root cause unknown. Hypotheses:
-
Data truncation: Background reader used 1024-byte buffer but AnsiInput.Read() passes 256-byte buffer to TryRead(). If ReadAsync returns >256 bytes in one call, excess is silently dropped.
-
Timing/ordering: Background reader starts in TryEnable() before the ANSI request scheduler is set up. Unsolicited data arriving early could desync the parser.
-
Stream vs ReadFile behavioral difference: Console.OpenStandardInput() wraps the same handle but .NET's ConsoleStream.ReadAsync may have different buffering or blocking behavior. On .NET, ReadAsync on a non-seekable stream schedules synchronous Read on a thread pool thread.
-
ConcurrentQueue vs BlockingCollection: VTTest uses BlockingCollection with blocking Take(). Our ConcurrentQueue with non-blocking TryDequeue may miss data in edge cases, or the different consumer pattern may matter.
-
CancellationToken ineffective on console ReadFile: The Memory<byte> overload of ReadAsync on a Windows console stream may not support cancellation — the underlying ReadFile is a blocking kernel call.
No Unit Test Coverage for This Path
There is no unit test that exercises the ANSI request/response flow through WindowsVTInputHelper. All existing tests use either:
- Mocked
IAnsiResponseParser (AnsiRequestSchedulerTests, AnsiRequestSchedulerCollisionTests)
- Direct
parser.ProcessInput() calls (AnsiRequestSchedulerRaceTests)
InjectInput() test injection (AnsiInputTestableTests)
Prerequisites Before Re-attempting
- Write a unit test for ANSI request/response through the input pipeline
- Investigate the regression — add diagnostic logging to identify where response bytes are lost
- Consider
BlockingCollection instead of ConcurrentQueue (matching VTTest)
- Match buffer sizes — use 256-byte buffer in background reader to match caller
Approach
Use a background async read pattern inside
WindowsVTInputHelper: a background task callsStream.ReadAsyncin a loop, buffering results into a concurrent collection. The existingPeek()/TryRead()API continues to work but reads from the buffer instead of P/Invoke. This avoids needing to changeInputImplorIInputinterfaces.Current Architecture (for reference)
Input pipeline flow
InputImpl.Run()): loopswhile (Peek()) { Read() }with 20ms delayAnsiInput.Peek()→ delegates toWindowsVTInputHelper.Peek()for WindowsVT platformAnsiInput.Read()→ callsWindowsVTInputHelper.TryRead(buffer, out bytesRead), converts bytes to UTF-8 chars, yields themInputImpl.Run()enqueues yielded chars intoInputQueue(aConcurrentQueue<char>)ProcessQueue()): dequeues fromInputQueue, callsAnsiInputProcessor.Process(char)→Parser.ProcessInput()→ matches ANSI responses, raises keyboard/mouse eventsANSI request/response flow
AnsiSizeMonitor.SendSizeQuery()→AnsiRequestScheduler.SendOrSchedule()→ writes ESC[18t to stdout viadriver.WriteRaw()AnsiResponseParsermatches the response terminator/value and invokesResponseReceivedcallbackInitialization order (MainLoopCoordinator)
StartInputTaskAsync(app)is calledRunInput(app)starts on background thread (acquires lock):AnsiInput→WindowsVTInputHelper.TryEnable()(opens handle, sets console mode)BuildDriverIfPossible()(driver built only if output also exists)_input.Run()(starts Peek/Read loop)BootMainLoop(app)runs on main thread (acquires lock):_outputBuildDriverIfPossible()→ if both input+output exist, builds driverSizeMonitor.Initialize(_driver)→ sends initial size query_startupSemaphore.Release()StartInputTaskAsyncawaits semaphore, then returnsRunIteration()→ProcessQueue()begins processingKey files
Terminal.Gui/Drivers/WindowsDriver/WindowsVTInputHelper.cs— the file to modifyTerminal.Gui/Drivers/AnsiDriver/AnsiInput.cs— callsPeek()(line 170) andTryRead()(line 195)Terminal.Gui/Drivers/Input/InputImpl.cs— the Peek/Read/20ms-delay loopTerminal.Gui/Drivers/AnsiDriver/AnsiInputProcessor.cs— processes chars through parserTerminal.Gui/Drivers/AnsiHandling/AnsiRequestScheduler.cs— manages ANSI request lifecycleTerminal.Gui/Drivers/AnsiDriver/AnsiSizeMonitor.cs— sends CSI_ReportWindowSizeInCharsTerminal.Gui/App/MainLoop/MainLoopCoordinator.cs— initialization and threadingVTTest Reference Implementation
VTTest/AsyncStreamInputReader.csis the pattern to follow:Console.OpenStandardInput()on WindowsTask.Run(async () => { ReadAsync loop })withCancellationTokenBlockingCollection<(byte[] Data, int Count)>(notConcurrentQueue) — consumer blocks via_queue.Take()Read()method BLOCKS until data available (unlike ourTryReadwhich is non-blocking)ReadAsync(buf.AsMemory(0, buf.Length), _cts.Token)— usesMemory<byte>overloadvar copy = new byte[n]; Array.Copy(buf, 0, copy, 0, n);_queue.CompleteAdding()infinallyblock when loop exitsChanges to WindowsVTInputHelper.cs
Remove:
ReadFileP/Invoke declarationGetNumberOfConsoleInputEventsP/Invoke declarationKeep:
GetStdHandle,GetConsoleMode,SetConsoleMode(still needed — no .NET equivalent for VT input mode flags)InputHandleproperty (used internally for Get/SetConsoleMode)Add:
using System.Collections.Concurrent;Stream? _stdinStream— opened viaConsole.OpenStandardInput()CancellationTokenSource? _ctsfor clean shutdownTask? _readTaskfor the background read loopConcurrentQueue<(byte[] Buffer, int Count)> _readQueuefor bufferingReadLoopAsync(CancellationToken ct)— background method:_stdinStream!.ReadAsync(buffer.AsMemory(0, buffer.Length), ct)in a loop([0x1A], 1)OperationCanceledExceptionfor clean shutdownModify:
TryEnable()— after setting console mode, open stdin stream and start background taskPeek()—!_readQueue.IsEmptyinstead ofGetNumberOfConsoleInputEventsTryRead()—_readQueue.TryDequeue()instead ofReadFileRestore()— cancel background task, wait, dispose stream/CTS, then restore console modeNo changes to
AnsiInput.cs,InputImpl.cs, orIInput.Known Issues from First Attempt
The implementation compiled and passed all unit tests (14520 parallelizable + 1000 non-parallel) but broke ANSI request/response at runtime. Root cause unknown. Hypotheses:
Data truncation: Background reader used 1024-byte buffer but
AnsiInput.Read()passes 256-byte buffer toTryRead(). IfReadAsyncreturns >256 bytes in one call, excess is silently dropped.Timing/ordering: Background reader starts in
TryEnable()before the ANSI request scheduler is set up. Unsolicited data arriving early could desync the parser.Stream vs ReadFile behavioral difference:
Console.OpenStandardInput()wraps the same handle but .NET'sConsoleStream.ReadAsyncmay have different buffering or blocking behavior. On .NET,ReadAsyncon a non-seekable stream schedules synchronousReadon a thread pool thread.ConcurrentQueue vs BlockingCollection: VTTest uses
BlockingCollectionwith blockingTake(). OurConcurrentQueuewith non-blockingTryDequeuemay miss data in edge cases, or the different consumer pattern may matter.CancellationToken ineffective on console ReadFile: The
Memory<byte>overload ofReadAsyncon a Windows console stream may not support cancellation — the underlyingReadFileis a blocking kernel call.No Unit Test Coverage for This Path
There is no unit test that exercises the ANSI request/response flow through
WindowsVTInputHelper. All existing tests use either:IAnsiResponseParser(AnsiRequestSchedulerTests,AnsiRequestSchedulerCollisionTests)parser.ProcessInput()calls (AnsiRequestSchedulerRaceTests)InjectInput()test injection (AnsiInputTestableTests)Prerequisites Before Re-attempting
BlockingCollectioninstead ofConcurrentQueue(matching VTTest)