Skip to content

Commit fc579c6

Browse files
sebgodclaude
andcommitted
Fix ASCII mode input — defer VT I/O to alternate screen, add WriteInPlace
- Move EnableVirtualTerminalIO from InitAsync to EnterAlternateScreen so Console.ReadKey works correctly in normal (non-alternate) mode - Make EnableVirtualTerminalIO idempotent with a guard flag - Use ReadKey(intercept: true) in normal mode — callers control echo - Add TerminalViewportExtensions.WriteInPlace for in-place line updates - MenuBase: show "> " prompt and echo selection in normal mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6080061 commit fc579c6

File tree

6 files changed

+50
-10
lines changed

6 files changed

+50
-10
lines changed

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,9 @@ The version is defined in two places — both must be updated together:
1717
The CI workflow composes the full package version from `VERSION_PREFIX`, `VERSION_REV`, and `VERSION_HASH`.
1818

1919
When bumping the version, update both files to keep them in sync.
20+
21+
## Key design notes
22+
23+
- **Windows VT I/O** (`WindowsConsoleInput.EnableVirtualTerminalIO`) is only activated when entering alternate screen mode, not during `InitAsync()`. This keeps `Console.ReadKey` working correctly in normal (non-alternate) mode for ASCII/text-based UIs.
24+
- **`TryReadInput`** uses `intercept: true` in normal mode — keystrokes are never echoed. Callers control display feedback (e.g., via `WriteInPlace`).
25+
- **`MenuBase<T>`** in normal mode shows a `> ` prompt and echoes the selected item on confirmation.

src/Console.Lib/ITerminalViewport.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,16 @@ public interface ITerminalViewport
2525
/// <summary>Color mode supported by this terminal. Defaults to SGR-16.</summary>
2626
ColorMode ColorMode => ColorMode.Sgr16;
2727
}
28+
29+
public static class TerminalViewportExtensions
30+
{
31+
/// <summary>
32+
/// Overwrites the current line with <paramref name="text"/> using carriage return,
33+
/// padding with spaces to erase any previous content. Does not advance to the next line.
34+
/// </summary>
35+
public static void WriteInPlace(this ITerminalViewport terminal, string text)
36+
{
37+
var padding = Math.Max(0, terminal.Size.Width - text.Length);
38+
terminal.Write($"\r{text}{new string(' ', padding)}\r{text}");
39+
}
40+
}

src/Console.Lib/MenuBase.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ protected async Task<int> ShowMenuAsync(
3636
terminal.WriteLine($" {i + 1}) {items[i]}");
3737
}
3838

39+
terminal.Write("> ");
40+
3941
while (!cancellationToken.IsCancellationRequested)
4042
{
4143
if (!terminal.HasInput())
@@ -49,7 +51,7 @@ protected async Task<int> ShowMenuAsync(
4951
var digit = input.Key - ConsoleKey.D1;
5052
if (digit >= 0 && digit < items.Length)
5153
{
52-
terminal.WriteLine(items[digit]);
54+
terminal.WriteLine($"{digit + 1}) {items[digit]}");
5355
return digit;
5456
}
5557
}

src/Console.Lib/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,16 @@ public interface ITerminalViewport
118118

119119
`TermCell` holds the pixel dimensions of a single terminal character cell, queried from the terminal during initialization via the `\e[16t` control sequence.
120120

121+
### TerminalViewportExtensions
122+
123+
Extension methods for `ITerminalViewport`:
124+
125+
```csharp
126+
// Overwrite the current line in-place using \r, padding with spaces to erase stale content.
127+
// Does not advance to the next line — ideal for status prompts and progress indicators.
128+
terminal.WriteInPlace("> waiting...");
129+
```
130+
121131
### IVirtualTerminal
122132

123133
Extends `ITerminalViewport` with full terminal lifecycle: initialization, input reading, alternate screen buffer, and Sixel capability detection.
@@ -141,9 +151,10 @@ public interface IVirtualTerminal : ITerminalViewport, IAsyncDisposable
141151
1. Sets UTF-8 encoding for stdin/stdout
142152
2. Sends a Device Attributes request (`\e[0c`) to detect terminal capabilities (including Sixel support)
143153
3. Sends a cell size query (`\e[16t`) to determine pixel dimensions per character cell
144-
4. On Windows, enables virtual terminal I/O and mouse input via `WindowsConsoleInput`
145154

146-
When entering the alternate screen, it enables VT200 mouse tracking with SGR extended coordinates (`\e[?1000h`, `\e[?1006h`), parses SGR mouse events from raw stdin, and normalizes cell coordinates to pixel coordinates using the cell size.
155+
When entering the alternate screen, it enables virtual terminal I/O and mouse input via `WindowsConsoleInput` (Windows only), then enables VT200 mouse tracking with SGR extended coordinates (`\e[?1000h`, `\e[?1006h`), parses SGR mouse events from raw stdin, and normalizes cell coordinates to pixel coordinates using the cell size.
156+
157+
In normal (non-alternate) screen mode, `TryReadInput()` uses `Console.ReadKey(intercept: true)` — keystrokes are not echoed, giving the caller full control over display feedback.
147158

148159
### TerminalViewport
149160

src/Console.Lib/VirtualTerminal.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@ public async Task InitAsync()
4646
}
4747
}
4848

49-
if (OperatingSystem.IsWindows())
50-
{
51-
WindowsConsoleInput.EnableVirtualTerminalIO();
52-
}
53-
5449
_initialized = true;
5550
}
5651

@@ -82,6 +77,11 @@ public bool HasColorSupport
8277
/// </summary>
8378
public void EnterAlternateScreen()
8479
{
80+
if (OperatingSystem.IsWindows())
81+
{
82+
WindowsConsoleInput.EnableVirtualTerminalIO();
83+
}
84+
8585
System.Console.Write("\e[?1049h"); // Enter alternate buffer
8686
System.Console.Write("\e[?25l"); // Hide cursor
8787
System.Console.Write("\e[?1000h"); // VT200 mouse tracking (basic button press/release and wheel)
@@ -138,7 +138,7 @@ public ConsoleInputEvent TryReadInput()
138138
}
139139
else
140140
{
141-
var first = System.Console.ReadKey(intercept: false);
141+
var first = System.Console.ReadKey(intercept: true);
142142

143143
if (first.Key == ConsoleKey.F1)
144144
{

src/Console.Lib/WindowsConsoleInput.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,20 @@ private enum ConsoleMode : uint
4141
private static nint _outputHandle;
4242
private static ConsoleMode _originalInputMode;
4343
private static ConsoleMode _originalOutputMode;
44+
private static bool _enabled;
4445

4546
/// <summary>
4647
/// Enables virtual terminal input and output processing.
48+
/// Only takes effect on the first call; subsequent calls are no-ops.
4749
/// </summary>
4850
/// <returns>True if virtual terminal input and output processing was enabled successfully.</returns>
4951
public static bool EnableVirtualTerminalIO()
5052
{
53+
if (_enabled)
54+
{
55+
return true;
56+
}
57+
5158
_inputHandle = GetStdHandle(STD_INPUT_HANDLE);
5259
if (_inputHandle == nint.Zero || _inputHandle == new nint(-1))
5360
{
@@ -73,8 +80,9 @@ public static bool EnableVirtualTerminalIO()
7380
| ConsoleMode.ExtendedFlags
7481
) & ~ConsoleMode.QuickEditMode;
7582

76-
return SetConsoleMode(_inputHandle, newInputMode)
83+
_enabled = SetConsoleMode(_inputHandle, newInputMode)
7784
&& SetConsoleMode(_outputHandle, _originalOutputMode | ConsoleMode.Processed | ConsoleMode.VirtualTerminalProcessing);
85+
return _enabled;
7886
}
7987

8088
/// <summary>

0 commit comments

Comments
 (0)