Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fd9c0fc
Fixes #4730. AnsiDriver will only capture Ctrl+Z if we move the mouse…
BDisp Feb 19, 2026
d31a49f
Add unit test for AnsiInput Peek and Read methods
BDisp Feb 19, 2026
8986208
Remove Console and force app redraw upon returning from the Suspend m…
BDisp Feb 20, 2026
d625901
Remove all Console calls from the AnsiDriver and UnixDriver
BDisp Feb 20, 2026
e313515
Remove duplicate code
BDisp Feb 20, 2026
c6b5284
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
tig Feb 22, 2026
aba752f
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Feb 25, 2026
192aa96
Delegate Suspend to IOutput and add per-output implementations
BDisp Feb 25, 2026
a6618ee
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Feb 25, 2026
03954c0
Add Suspend unit tests
BDisp Feb 25, 2026
1b85f96
Remove UnixTerminalHelper from DotNetDriver
BDisp Feb 25, 2026
eef6054
Removing forgotten Console calls from Unix and Ansi
BDisp Feb 25, 2026
c47d14d
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
tig Feb 27, 2026
7837a62
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Mar 1, 2026
57a2f94
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
tig Mar 1, 2026
fb4ecc7
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Mar 1, 2026
d53d38b
Fixes #4776. DriverTests.AllDriverTests.All_Drivers_LayoutAndDraw_Cro…
BDisp Mar 1, 2026
4a21473
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Mar 1, 2026
24dd710
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Mar 4, 2026
40edc6a
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Mar 4, 2026
6387b7b
Fix merge errors
BDisp Mar 4, 2026
9cc2709
Per codecov
BDisp Mar 4, 2026
82ec103
Refactor logging to use Trace.Lifecycle; fix VT input bugs
tig Mar 4, 2026
f56ee5c
Refactor WindowsVTInputHelper input event handling
tig Mar 4, 2026
1771cb1
Fix Windows Ctrl+Z input bug workaround logic
tig Mar 5, 2026
74f5049
Merge branch 'v2_develop' into v2_4730_ansidriver-ctrl-z-fix
BDisp Mar 5, 2026
2e7285e
Fix logging levels: Trace.Lifecycle for init, Logging.Warning for errors
tig Mar 5, 2026
1969961
Merge remote-tracking branch 'bdisp/v2_4730_ansidriver-ctrl-z-fix' in…
tig Mar 5, 2026
19c4ba8
Moved Ansi-specific Windows helpers ot Ansi Driver folder.
tig Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions Terminal.Gui/App/Keyboard/ApplicationKeyboard.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,9 @@ internal void AddKeyBindings ()
{
App?.Driver?.Suspend ();

// When the app is resumed, we need to force a full redraw to clear out any artifacts from the suspended console.
App?.ClearScreenNextIteration = true;

return true;
});

Expand Down Expand Up @@ -345,11 +348,8 @@ internal void AddKeyBindings ()
// TODO: Refresh Key should be configurable
KeyBindings.ReplaceCommands (Key.F5, Command.Refresh);

// TODO: Suspend Key should be configurable
if (Environment.OSVersion.Platform == PlatformID.Unix)
{
KeyBindings.ReplaceCommands (Key.Z.WithCtrl, Command.Suspend);
}
// Each driver handles Suspend themselves
KeyBindings.ReplaceCommands (Key.Z.WithCtrl, Command.Suspend);
}

/// <summary>
Expand Down
52 changes: 14 additions & 38 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using Terminal.Gui.Tracing;

namespace Terminal.Gui.Drivers;

Expand Down Expand Up @@ -87,9 +88,9 @@ public AnsiInput ()
try
{
// Check if we have a real console first
if (Console.IsInputRedirected || Console.IsOutputRedirected)
if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
{
Logging.Information ($"Console redirected (Output: {Console.IsOutputRedirected}, Input: {Console.IsInputRedirected}). Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiInput), "Init", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");

return;
}
Expand All @@ -104,7 +105,7 @@ public AnsiInput ()
_windowsVTInput.Dispose ();
_windowsVTInput = null;

Logging.Warning ("Failed to enable Windows VT Input mode. Terminal input will not work. Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiInput), "Init", "Failed to enable Windows VT Input mode. Terminal input will not work. Running in degraded mode.");

return;
}
Expand All @@ -120,7 +121,7 @@ public AnsiInput ()

if (!_unixRawMode.TryEnable ())
{
Logging.Warning ("Failed to enable Unix raw input mode. Terminal input will not work. Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiInput), "Init", "Failed to enable Unix raw input mode. Terminal input will not work. Running in degraded mode.");
_pollMap = null;
_unixRawMode?.Dispose ();
_unixRawMode = null;
Expand All @@ -131,32 +132,12 @@ public AnsiInput ()
}
catch (DllNotFoundException ex)
{
Logging.Warning ($"Failed to enable Unix raw input mode. libc not available: {ex.Message}. Running in degraded mode.");

return;
Trace.Lifecycle (nameof (AnsiInput), "Init", $"Failed to enable Unix raw input mode. libc not available: {ex.Message}. Running in degraded mode.");
}
}
else
{
Logging.Warning ("Unknown OS platform. Terminal input will not work. Running in degraded mode.");

return;
}

// Try to disable Ctrl+C handling to allow raw input
try
{
// BUGBUG: This is not needed on Windows as we turn off ENABLE_PROCESSED_INPUT in _windowsVTInput.TryEnable () above
// BUGBUG: This does nothing if we're running Unix, because we are using raw mode

// All TreatConsoleCAsInput does is un-set ENABLE_PROCESSED_INPUT on the input handle
Console.TreatControlCAsInput = true;
}
catch (Exception ex)
{
Logging.Warning ($"Failed to set TreatControlCAsInput: {ex.Message}");

// Not supported in all environments - continue anyway
Trace.Lifecycle (nameof (AnsiInput), "Init", "Unknown OS platform. Terminal input will not work. Running in degraded mode.");
}

// NOTE: Output operations (alternate buffer, cursor visibility, mouse events)
Expand All @@ -166,8 +147,7 @@ public AnsiInput ()
}
catch (Exception ex)
{
Logging.Warning ($"Failed to initialize terminal: {ex.GetType ().Name}: {ex.Message}. Running in degraded mode.");
Logging.Warning ($"Stack trace: {ex.StackTrace}");
Trace.Lifecycle (nameof (AnsiInput), "Init", $"Failed to initialize terminal: {ex.GetType ().Name}: {ex.Message}. Running in degraded mode. Stack trace: {ex.StackTrace}");
_platform = AnsiPlatform.Degraded;
}
}
Expand Down Expand Up @@ -217,13 +197,9 @@ public override IEnumerable<char> Read ()
yield break;
}

// Convert UTF-8 bytes to characters
uint cp = WindowsVTInputHelper.GetConsoleCP ();
var enc = Encoding.GetEncoding ((int)cp);

string text = enc.GetString (buffer, 0, bytesRead);
string text = Encoding.UTF8.GetString (buffer, 0, bytesRead);

//Logging.Trace ($"AnsiInput.Read: read {bytesRead} text: {text}");
//Trace.Lifecycle (nameof (AnsiInput), "Read", $"Read {bytesRead} bytes from Windows VT Input: {text}");

foreach (char ch in text)
{
Expand Down Expand Up @@ -284,15 +260,15 @@ private IEnumerable<char> ReadUnixInput (byte [] buffer)
{
// Error
int errno = Marshal.GetLastWin32Error ();
Logging.Warning ($"Read: read() returned {readResult}, errno={errno}");
Logging.Warning ($"{nameof (AnsiInput)}: read() returned {readResult}, errno={errno}");

yield break;
}
}
}
else
{
Logging.Error ("Read: read() failed");
Logging.Warning ($"{nameof (AnsiInput)}: read() failed");

yield break;
}
Expand Down Expand Up @@ -364,7 +340,7 @@ private void FlushInput ()

if (flushCount > 0)
{
Logging.Information ($"FlushInput: Flushed input buffer ({flushCount} read attempts)");
Trace.Lifecycle (nameof (AnsiInput), "FlushInput", $"Flushed input buffer ({flushCount} read attempts)");
}

break;
Expand All @@ -376,7 +352,7 @@ private void FlushInput ()
}
catch (Exception ex)
{
Logging.Warning ($"Error flushing input: {ex.Message}");
Logging.Warning ($"{nameof (AnsiInput)}: Error flushing input: {ex.Message}");
}
}

Expand Down
43 changes: 24 additions & 19 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.RegularExpressions;
using Terminal.Gui.Tracing;

namespace Terminal.Gui.Drivers;

Expand Down Expand Up @@ -59,10 +60,10 @@ public AnsiOutput ()

try
{
// Check if console is available (not redirected)
if (Console.IsOutputRedirected || Console.IsInputRedirected)
// Check if we have a real console first
if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
{
Logging.Information ($"Console redirected (Output: {Console.IsOutputRedirected}, Input: {Console.IsInputRedirected}). Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiOutput), "Init", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");

return;
}
Expand All @@ -77,7 +78,7 @@ public AnsiOutput ()
_windowsVTOutput.Dispose ();
_windowsVTOutput = null;

Logging.Information ("Failed to enable Windows VT Input mode. Terminal input will not work. Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiOutput), "Init", "Failed to enable Windows VT Input mode. Terminal input will not work. Running in degraded mode.");

return;
}
Expand All @@ -90,7 +91,7 @@ public AnsiOutput ()

if (fdCopy == -1)
{
Logging.Information ("Console output stream is not writable. Running in degraded mode.");
Trace.Lifecycle (nameof (AnsiOutput), "Init", "Console output stream is not writable. Running in degraded mode.");

return;
}
Expand All @@ -104,12 +105,12 @@ public AnsiOutput ()
Write (EscSeqUtils.CSI_ClearScreen (EscSeqUtils.ClearScreenOptions.EntireScreen));
Write (EscSeqUtils.CSI_SetCursorPosition (1, 1)); // Move to top-left
Write (EscSeqUtils.CSI_HideCursor);

// TODO: Move Input related CSI sequences to AnsiInput
Write (EscSeqUtils.CSI_EnableMouseEvents);

// Flush to ensure all sequences are sent
// NOTE: Default implementation of Flush does nothing.
Console.Out.Flush ();
AnsiTerminalHelper.FlushNative (_platform);

//Logging.Information ("ANSIOutput initialized successfully");

Expand All @@ -121,12 +122,14 @@ public AnsiOutput ()
}
catch (Exception ex)
{
Logging.Warning ($"Failed to initialize ANSIOutput: {ex.GetType ().Name}: {ex.Message}");
Logging.Warning ($"Stack trace: {ex.StackTrace}");
Trace.Lifecycle (nameof (AnsiOutput), "Init", $"Failed to initialize ANSIOutput: {ex.GetType ().Name}: {ex.Message}. Stack trace: {ex.StackTrace}");
_platform = AnsiPlatform.Degraded;
}
}

/// <inheritdoc/>
public void Suspend () => UnixTerminalHelper.Suspend (this);

/// <summary>
/// Gets or sets the last output buffer written. The <see cref="IOutputBuffer.Contents"/> contains
/// a reference to the buffer last written with <see cref="Write(IOutputBuffer)"/>.
Expand Down Expand Up @@ -228,7 +231,7 @@ public void SetCursor (Cursor cursor)
}
else
{
if (_currentCursor!.Style != cursor.Style)
if (_currentCursor.Style != cursor.Style)
{
Write (EscSeqUtils.CSI_SetCursorStyle (cursor.Style));
}
Expand All @@ -251,7 +254,7 @@ public void SetCursor (Cursor cursor)
/// <inheritdoc/>
protected override bool SetCursorPositionImpl (int col, int row)
{
if (_currentCursor!.Position is { } && _currentCursor.Position.Value.X == col && _currentCursor.Position.Value.Y == row)
if (_currentCursor.Position is { } && _currentCursor.Position.Value.X == col && _currentCursor.Position.Value.Y == row)
{
return false;
}
Expand Down Expand Up @@ -286,19 +289,21 @@ public void HandleSizeQueryResponse (string? response)
// Example: "[8;25;80t"
Match match = Regex.Match (response, @"\[(\d+);(\d+);(\d+)t$");

if (match is { Success: true, Groups.Count: 4 })
if (match is not { Success: true, Groups.Count: 4 })
{
// Group 1 should be "8" (the response value)
// Group 2 is height, Group 3 is width
if (int.TryParse (match.Groups [2].Value, out int height) && int.TryParse (match.Groups [3].Value, out int width))
{
_consoleSize = new Size (width, height);
}
return;
}

// Group 1 should be "8" (the response value)
// Group 2 is height, Group 3 is width
if (int.TryParse (match.Groups [2].Value, out int height) && int.TryParse (match.Groups [3].Value, out int width))
{
_consoleSize = new Size (width, height);
}
}
catch (Exception ex)
{
Logging.Warning ($"Failed to parse size query response '{response}': {ex.Message}");
Trace.Lifecycle (nameof (AnsiOutput), "SizeQuery", $"Failed to parse size query response '{response}': {ex.Message}");
}
}

Expand Down
107 changes: 107 additions & 0 deletions Terminal.Gui/Drivers/AnsiDriver/AnsiTerminalHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Runtime.InteropServices;

namespace Terminal.Gui.Drivers;

internal static class AnsiTerminalHelper
{
public static bool IsAttachedToTerminal (out bool inputAttached, out bool outputAttached)
{
inputAttached = outputAttached = false;

if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
const int STD_INPUT_HANDLE = -10;
const int STD_OUTPUT_HANDLE = -11;
nint inH = GetStdHandle (STD_INPUT_HANDLE);
nint outH = GetStdHandle (STD_OUTPUT_HANDLE);

inputAttached = inH != nint.Zero && GetConsoleMode (inH, out _);
outputAttached = outH != nint.Zero && GetConsoleMode (outH, out _);

return inputAttached && outputAttached;
}
const int STDIN_FILENO = 0;
const int STDOUT_FILENO = 1;
inputAttached = isatty (STDIN_FILENO) == 1;
outputAttached = isatty (STDOUT_FILENO) == 1;

return inputAttached && outputAttached;
}

public static void FlushNative (AnsiPlatform platform)
{
try
{
switch (platform)
{
case AnsiPlatform.UnixRaw:
FlushUnix ();

break;

case AnsiPlatform.WindowsVT:
FlushWindows ();

break;
}
}
catch
{
// ignore any exceptions during flush, as we don't want to crash the app if the flush fails in unit tests.
}
}

/* Unix: wait until output has been transmitted to the terminal.
Prefer tcdrain(STDOUT_FILENO). If it fails, fall back to fsync. */
private static void FlushUnix ()
{
const int STDOUT_FILENO = 1;

if (tcdrain (STDOUT_FILENO) == 0)
{
return;
}

// fallback
try
{
fsync (STDOUT_FILENO);
}
catch
{ /* ignore */
}
}

/* Windows: flush the stdout handle. */
private static void FlushWindows ()
{
const int STD_OUTPUT_HANDLE = -11;
nint h = GetStdHandle (STD_OUTPUT_HANDLE);

if (h != nint.Zero && h != new nint (-1))
{
FlushFileBuffers (h); // returns false on failure
}
}

// Unix
[DllImport ("libc", SetLastError = true)]
private static extern int isatty (int fd);

[DllImport ("libc", SetLastError = true)]
private static extern int tcdrain (int fd);

[DllImport ("libc", SetLastError = true)]
private static extern int fsync (int fd);

// Windows
[DllImport ("kernel32.dll", SetLastError = true)]
private static extern nint GetStdHandle (int nStdHandle);

[DllImport ("kernel32.dll")]
private static extern bool GetConsoleMode (nint hConsoleHandle, out uint lpMode);

[DllImport ("kernel32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
private static extern bool FlushFileBuffers (nint hFile);
}
Loading
Loading