diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml
index d41181adfe..b78293b9ca 100644
--- a/.github/workflows/integration-tests.yml
+++ b/.github/workflows/integration-tests.yml
@@ -18,6 +18,8 @@ jobs:
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
timeout-minutes: 15
+ env:
+ DisableRealDriverIO: "1"
steps:
- name: Checkout code
diff --git a/.github/workflows/stress-tests.yml b/.github/workflows/stress-tests.yml
index ffbb23e566..033f5c43d5 100644
--- a/.github/workflows/stress-tests.yml
+++ b/.github/workflows/stress-tests.yml
@@ -17,6 +17,8 @@ jobs:
os: [ ubuntu-latest ]
timeout-minutes: 70 # Allow some buffer time beyond the 1-hour test duration
+ env:
+ DisableRealDriverIO: "1"
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -58,6 +60,8 @@ jobs:
os: [ ubuntu-latest, windows-latest, macos-latest ]
timeout-minutes: 90
+ env:
+ DisableRealDriverIO: "1"
steps:
- name: Checkout code
uses: actions/checkout@v4
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index 181333709a..9cd63dfb0c 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -20,6 +20,8 @@ jobs:
os: [ ubuntu-latest, windows-latest, macos-latest ]
timeout-minutes: 15
+ env:
+ DisableRealDriverIO: "1"
steps:
- name: Checkout code
@@ -76,6 +78,8 @@ jobs:
os: [ ubuntu-latest, windows-latest, macos-latest ]
timeout-minutes: 60
+ env:
+ DisableRealDriverIO: "1"
steps:
- name: Checkout code
diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs
index 6d8231d6b8..5064c488bd 100644
--- a/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs
+++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiInput.cs
@@ -88,9 +88,9 @@ public AnsiInput ()
try
{
// Check if we have a real console first
- if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
+ if (!IsAttachedToTerminal)
{
- Trace.Lifecycle (nameof (AnsiInput), "Init", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
+ Trace.Lifecycle (nameof (AnsiInput), "Init", "Console is not attached to a terminal. Running in degraded mode.");
return;
}
@@ -105,7 +105,9 @@ public AnsiInput ()
_windowsVTInput.Dispose ();
_windowsVTInput = null;
- Trace.Lifecycle (nameof (AnsiInput), "Init", "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;
}
@@ -121,7 +123,9 @@ public AnsiInput ()
if (!_unixRawMode.TryEnable ())
{
- Trace.Lifecycle (nameof (AnsiInput), "Init", "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;
@@ -132,7 +136,9 @@ public AnsiInput ()
}
catch (DllNotFoundException ex)
{
- Trace.Lifecycle (nameof (AnsiInput), "Init", $"Failed to enable Unix raw input mode. libc not available: {ex.Message}. Running in degraded mode.");
+ Trace.Lifecycle (nameof (AnsiInput),
+ "Init",
+ $"Failed to enable Unix raw input mode. libc not available: {ex.Message}. Running in degraded mode.");
}
}
else
@@ -147,7 +153,9 @@ public AnsiInput ()
}
catch (Exception ex)
{
- Trace.Lifecycle (nameof (AnsiInput), "Init", $"Failed to initialize terminal: {ex.GetType ().Name}: {ex.Message}. Running in degraded mode. 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;
}
}
diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
index d7ceff9e0e..f3ef57d450 100644
--- a/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
+++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiOutput.cs
@@ -61,9 +61,9 @@ public AnsiOutput ()
try
{
// Check if we have a real console first
- if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
+ if (!IsAttachedToTerminal)
{
- Trace.Lifecycle (nameof (AnsiOutput), "Init", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
+ Trace.Lifecycle (nameof (AnsiOutput), "Init", "No real terminal attached. Running in degraded mode.");
return;
}
@@ -78,7 +78,9 @@ public AnsiOutput ()
_windowsVTOutput.Dispose ();
_windowsVTOutput = null;
- Trace.Lifecycle (nameof (AnsiOutput), "Init", "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;
}
@@ -178,14 +180,16 @@ protected override void Write (StringBuilder output)
///
public void Write (ReadOnlySpan text)
{
+ StringBuilder capturedOutput = new ();
+ capturedOutput.Append (text);
+ base.Write (capturedOutput);
+
try
{
switch (_platform)
{
case AnsiPlatform.WindowsVT:
- StringBuilder sb = new ();
- sb.Append (text);
- _windowsVTOutput!.Write (sb);
+ _windowsVTOutput!.Write (capturedOutput);
break;
@@ -310,13 +314,13 @@ public void HandleSizeQueryResponse (string? response)
///
public void Dispose ()
{
- if (_platform == AnsiPlatform.Degraded)
- {
- return;
- }
-
try
{
+ if (_platform == AnsiPlatform.Degraded)
+ {
+ return;
+ }
+
// Restore terminal state: disable mouse, restore buffer, show cursor
// TODO: Move Input related CSI sequences to AnsiInput
Write (EscSeqUtils.CSI_DisableMouseEvents);
diff --git a/Terminal.Gui/Drivers/AnsiDriver/AnsiTerminalHelper.cs b/Terminal.Gui/Drivers/AnsiDriver/AnsiTerminalHelper.cs
index 34ab711bfd..09742178f8 100644
--- a/Terminal.Gui/Drivers/AnsiDriver/AnsiTerminalHelper.cs
+++ b/Terminal.Gui/Drivers/AnsiDriver/AnsiTerminalHelper.cs
@@ -4,30 +4,6 @@ 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
@@ -85,9 +61,6 @@ private static void FlushWindows ()
}
// Unix
- [DllImport ("libc", SetLastError = true)]
- private static extern int isatty (int fd);
-
[DllImport ("libc", SetLastError = true)]
private static extern int tcdrain (int fd);
@@ -98,9 +71,6 @@ private static void FlushWindows ()
[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);
diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs
index b45996378e..0c9a04fb59 100644
--- a/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs
+++ b/Terminal.Gui/Drivers/DotNetDriver/NetInput.cs
@@ -1,4 +1,6 @@
#nullable disable
+using Terminal.Gui.Tracing;
+
namespace Terminal.Gui.Drivers;
///
@@ -11,19 +13,25 @@ public class NetInput : InputImpl, ITestableInput
/// Creates a new instance of the class. Implicitly sends
/// console mode settings that enable virtual input (mouse
- /// reporting etc).
+ /// reporting etc.).
///
public NetInput ()
{
- //Logging.Information ($"Creating {nameof (NetInput)}");
+ // Check if we have a real console first
+ if (!IsAttachedToTerminal)
+ {
+ Trace.Lifecycle (nameof (NetInput), "Init", "Console is not attached to a terminal. Running in degraded mode.");
+
+ return;
+ }
PlatformID p = Environment.OSVersion.Platform;
- if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+ if (p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows)
{
try
{
- _adjustConsole = new ();
+ _adjustConsole = new NetWinVTConsole ();
}
catch (ApplicationException ex)
{
@@ -84,8 +92,8 @@ public override void Dispose ()
}
}
- ///
- public void InjectInput (ConsoleKeyInfo input) { throw new NotImplementedException (); }
+ ///
+ public void InjectInput (ConsoleKeyInfo input) => throw new NotImplementedException ();
///
public override bool Peek ()
diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
index ae73fe2043..0474809d34 100644
--- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
+++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs
@@ -1,3 +1,5 @@
+using Terminal.Gui.Tracing;
+
namespace Terminal.Gui.Drivers;
///
@@ -15,6 +17,13 @@ public NetOutput ()
{
// Logging.Information ($"Creating {nameof (NetOutput)}");
+ if (!IsAttachedToTerminal)
+ {
+ Trace.Lifecycle (nameof (NetOutput), "Init", "No real terminal attached. Output operations will be no-op.");
+
+ return;
+ }
+
try
{
Console.OutputEncoding = Encoding.UTF8;
@@ -26,7 +35,7 @@ public NetOutput ()
PlatformID p = Environment.OSVersion.Platform;
- if (p == PlatformID.Win32NT || p == PlatformID.Win32S || p == PlatformID.Win32Windows)
+ if (p is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows)
{
_isWinPlatform = true;
}
@@ -35,6 +44,11 @@ public NetOutput ()
///
public Size GetSize ()
{
+ if (!IsAttachedToTerminal)
+ {
+ return new Size (80, 25);
+ }
+
try
{
if (Console.IsInputRedirected || Console.IsOutputRedirected)
@@ -58,10 +72,14 @@ public void SetSize (int width, int height)
// Do Nothing.
}
-
///
public void Write (ReadOnlySpan text)
{
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
try
{
Console.Out.Write (text);
@@ -77,6 +95,11 @@ protected override void Write (StringBuilder output)
{
base.Write (output);
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
try
{
Console.Out.Write (output);
@@ -89,14 +112,10 @@ protected override void Write (StringBuilder output)
private Cursor _currentCursor = new ();
- ///
- public Cursor GetCursor ()
- {
- return _currentCursor;
- }
-
+ ///
+ public Cursor GetCursor () => _currentCursor;
- ///
+ ///
public void SetCursor (Cursor cursor)
{
try
@@ -107,7 +126,7 @@ public void SetCursor (Cursor cursor)
}
else
{
- if (_currentCursor!.Style != cursor.Style)
+ if (_currentCursor.Style != cursor.Style)
{
Write (EscSeqUtils.CSI_SetCursorStyle (cursor.Style));
}
@@ -121,10 +140,7 @@ public void SetCursor (Cursor cursor)
}
finally
{
- SetCursorPositionImpl (
- cursor.Position?.X ?? 0,
- cursor.Position?.Y ?? 0
- );
+ SetCursorPositionImpl (cursor.Position?.X ?? 0, cursor.Position?.Y ?? 0);
_currentCursor = cursor;
}
@@ -133,7 +149,7 @@ public void SetCursor (Cursor cursor)
///
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;
}
@@ -148,6 +164,7 @@ protected override bool SetCursorPositionImpl (int col, int row)
{
// Could happen that the windows is still resizing and the col is bigger than Console.WindowWidth.
}
+
return true;
}
@@ -164,7 +181,7 @@ public void Dispose () { }
///
public void Suspend ()
{
- if (PlatformDetection.IsWindows ())
+ if (PlatformDetection.IsWindows () && !IsAttachedToTerminal)
{
return;
}
@@ -178,7 +195,11 @@ public void Suspend ()
// Check if we have a real console first
if (Console.IsInputRedirected || Console.IsOutputRedirected)
{
- Logging.Information ($"Console redirected (Output: {Console.IsOutputRedirected}, Input: {Console.IsInputRedirected}). Running in degraded mode.");
+ Logging.Information ($"Console redirected (Output: {
+ Console.IsOutputRedirected
+ }, Input: {
+ Console.IsInputRedirected
+ }). Running in degraded mode.");
return;
}
diff --git a/Terminal.Gui/Drivers/Driver.cs b/Terminal.Gui/Drivers/Driver.cs
index 02bc73a640..4d5a181314 100644
--- a/Terminal.Gui/Drivers/Driver.cs
+++ b/Terminal.Gui/Drivers/Driver.cs
@@ -1,12 +1,12 @@
-namespace Terminal.Gui.Drivers;
+using System.Runtime.InteropServices;
+
+namespace Terminal.Gui.Drivers;
///
-/// Holds global driver settings.
+/// Holds global driver settings and cross-driver utility methods.
///
public sealed class Driver
{
- private static bool _force16Colors = false; // Resources/config.json overrides
-
// NOTE: Force16Colors is a configuration property (Driver.Force16Colors).
// NOTE: IDriver also has a Force16Colors property, which is an instance property
// NOTE: set whenever this static property is set.
@@ -17,15 +17,72 @@ public sealed class Driver
[ConfigurationProperty (Scope = typeof (SettingsScope))]
public static bool Force16Colors
{
- get => _force16Colors;
+ get;
set
{
- bool oldValue = _force16Colors;
- _force16Colors = value;
- Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _force16Colors));
+ bool oldValue = field;
+ field = value;
+ Force16ColorsChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, field));
}
}
/// Raised when changes.
public static event EventHandler>? Force16ColorsChanged;
+
+ ///
+ /// Determines whether the process is attached to a real terminal (i.e. stdin/stdout
+ /// are connected to a console device rather than redirected or running inside a test harness). Set the environment
+ /// variable "DisableRealDriverIO=1" to skip real terminal detection and force this method to return false, which is
+ /// required for running in test harnesses that do not have a real terminal attached.
+ ///
+ ///
+ /// When this method returns, if standard input is connected to a console device;
+ /// otherwise .
+ ///
+ ///
+ /// When this method returns, if standard output is connected to a console device;
+ /// otherwise .
+ ///
+ /// if both input and output are attached to a terminal; otherwise .
+ public static bool IsAttachedToTerminal (out bool inputAttached, out bool outputAttached)
+ {
+ inputAttached = outputAttached = false;
+
+ // When the test harness sets DisableRealDriverIO, skip real terminal detection entirely.
+ if (string.Equals (Environment.GetEnvironmentVariable ("DisableRealDriverIO"), "1", StringComparison.Ordinal))
+ {
+ return 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;
+ }
+
+ // Unix
+ [DllImport ("libc", SetLastError = true)]
+ private static extern int isatty (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);
}
diff --git a/Terminal.Gui/Drivers/Input/InputImpl.cs b/Terminal.Gui/Drivers/Input/InputImpl.cs
index 6aa69cfc5b..d72af4a465 100644
--- a/Terminal.Gui/Drivers/Input/InputImpl.cs
+++ b/Terminal.Gui/Drivers/Input/InputImpl.cs
@@ -10,6 +10,17 @@ namespace Terminal.Gui.Drivers;
///
public abstract class InputImpl : IInput
{
+ ///
+ /// Initializes a new instance of the class and detects if we are attached to a
+ /// real terminal device. If not, the input implementation will run in a degraded mode where all operations are no-op.
+ ///
+ protected InputImpl () => IsAttachedToTerminal = Driver.IsAttachedToTerminal (out _, out _);
+
+ ///
+ /// Gets whether this input instance is attached to a real terminal device.
+ ///
+ protected bool IsAttachedToTerminal { get; }
+
private ConcurrentQueue? _inputQueue;
///
diff --git a/Terminal.Gui/Drivers/Output/OutputBase.cs b/Terminal.Gui/Drivers/Output/OutputBase.cs
index 0d42a9ace1..01e9b09653 100644
--- a/Terminal.Gui/Drivers/Output/OutputBase.cs
+++ b/Terminal.Gui/Drivers/Output/OutputBase.cs
@@ -7,12 +7,20 @@ namespace Terminal.Gui.Drivers;
///
public abstract class OutputBase
{
- private bool _force16Colors;
+ ///
+ /// Initializes a new instance of the class and detects whether the output is attached to a real terminal device.
+ ///
+ protected OutputBase () => IsAttachedToTerminal = Driver.IsAttachedToTerminal (out _, out _);
+
+ ///
+ /// Gets whether this output instance is attached to a real terminal device.
+ ///
+ protected bool IsAttachedToTerminal { get; }
///
public bool Force16Colors
{
- get => _force16Colors;
+ get;
set
{
if (IsLegacyConsole && !value)
@@ -20,19 +28,17 @@ public bool Force16Colors
return;
}
- _force16Colors = value;
+ field = value;
}
}
- private bool _isLegacyConsole;
-
///
public bool IsLegacyConsole
{
- get => _isLegacyConsole;
+ get;
set
{
- _isLegacyConsole = value;
+ field = value;
if (value) // If legacy console (true), force 16 colors
{
@@ -43,13 +49,13 @@ public bool IsLegacyConsole
private readonly ConcurrentQueue _sixels = [];
- /// >
+ ///
public ConcurrentQueue GetSixels () => _sixels;
// Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange().
private TextStyle _redrawTextStyle = TextStyle.None;
- StringBuilder _lastOutputStringBuilder = new ();
+ private readonly StringBuilder _lastOutputStringBuilder = new ();
private bool _clearLastOutputPending;
///
@@ -61,8 +67,8 @@ public virtual void Write (IOutputBuffer buffer)
{
_clearLastOutputPending = true;
StringBuilder outputStringBuilder = new ();
- int top = 0;
- int left = 0;
+ var top = 0;
+ var left = 0;
int rows = buffer.Rows;
int cols = buffer.Cols;
Attribute? redrawAttr = null;
@@ -130,20 +136,22 @@ public virtual void Write (IOutputBuffer buffer)
}
// Flush buffered output for row
- if (outputStringBuilder.Length > 0)
+ if (outputStringBuilder.Length <= 0)
{
- if (IsLegacyConsole)
- {
- Write (outputStringBuilder);
- }
- else
- {
- SetCursorPositionImpl (lastCol, row);
+ continue;
+ }
- // Wrap URLs with OSC 8 hyperlink sequences
- StringBuilder processed = Osc8UrlLinker.WrapOsc8 (outputStringBuilder);
- Write (processed);
- }
+ if (IsLegacyConsole)
+ {
+ Write (outputStringBuilder);
+ }
+ else
+ {
+ SetCursorPositionImpl (lastCol, row);
+
+ // Wrap URLs with OSC 8 hyperlink sequences
+ StringBuilder processed = Osc8UrlLinker.WrapOsc8 (outputStringBuilder);
+ Write (processed);
}
}
@@ -165,7 +173,7 @@ public virtual void Write (IOutputBuffer buffer)
}
}
- ///
+ ///
public virtual string GetLastOutput () => _lastOutputStringBuilder.ToString ();
///
@@ -250,19 +258,17 @@ protected virtual void Write (StringBuilder output)
/// The last attribute used, for optimization.
/// Predicate to determine which cells to include. If null, includes all cells.
/// Whether to add newlines between rows.
- protected void BuildAnsiForRegion (
- IOutputBuffer buffer,
- int startRow,
- int endRow,
- int startCol,
- int endCol,
- StringBuilder output,
- ref Attribute? lastAttr,
- Func? includeCellPredicate = null,
- bool addNewlines = true
- )
+ protected void BuildAnsiForRegion (IOutputBuffer buffer,
+ int startRow,
+ int endRow,
+ int startCol,
+ int endCol,
+ StringBuilder output,
+ ref Attribute? lastAttr,
+ Func? includeCellPredicate = null,
+ bool addNewlines = true)
{
- TextStyle redrawTextStyle = TextStyle.None;
+ var redrawTextStyle = TextStyle.None;
for (int row = startRow; row < endRow; row++)
{
@@ -296,7 +302,13 @@ protected void BuildAnsiForRegion (
/// The maximum column, used for wide character handling.
/// The current column, updated for wide characters.
/// The current output width, updated for wide characters.
- protected void AppendCellAnsi (Cell cell, StringBuilder output, ref Attribute? lastAttr, ref TextStyle redrawTextStyle, int maxCol, ref int currentCol, ref int outputWidth)
+ protected void AppendCellAnsi (Cell cell,
+ StringBuilder output,
+ ref Attribute? lastAttr,
+ ref TextStyle redrawTextStyle,
+ int maxCol,
+ ref int currentCol,
+ ref int outputWidth)
{
Attribute? attribute = cell.Attribute;
@@ -314,11 +326,12 @@ protected void AppendCellAnsi (Cell cell, StringBuilder output, ref Attribute? l
outputWidth++;
// Handle wide grapheme
- if (grapheme.GetColumns () > 1 && currentCol + 1 < maxCol)
+ if (grapheme.GetColumns () <= 1 || currentCol + 1 >= maxCol)
{
- currentCol++; // Skip next cell for wide character
- outputWidth++;
+ return;
}
+ currentCol++; // Skip next cell for wide character
+ outputWidth++;
}
///
@@ -335,9 +348,9 @@ public string ToAnsi (IOutputBuffer buffer)
{
StringBuilder output = new ();
- for (int row = 0; row < buffer.Rows; row++)
+ for (var row = 0; row < buffer.Rows; row++)
{
- for (int col = 0; col < buffer.Cols; col++)
+ for (var col = 0; col < buffer.Cols; col++)
{
Cell cell = buffer.Contents! [row, col];
string grapheme = cell.Grapheme;
diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixIOHelper.cs b/Terminal.Gui/Drivers/UnixDriver/UnixIOHelper.cs
index b2e9bb2e96..cf660584e6 100644
--- a/Terminal.Gui/Drivers/UnixDriver/UnixIOHelper.cs
+++ b/Terminal.Gui/Drivers/UnixDriver/UnixIOHelper.cs
@@ -78,7 +78,7 @@ public enum Condition : short
/// Timeout in milliseconds (0 = non-blocking, -1 = infinite)
/// Number of file descriptors with events, or -1 on error
[DllImport ("libc", SetLastError = true)]
- public static extern int poll ([In][Out] Pollfd [] ufds, uint nfds, int timeout);
+ public static extern int poll ([In] [Out] Pollfd [] ufds, uint nfds, int timeout);
///
/// Read bytes from a file descriptor.
@@ -157,10 +157,9 @@ public struct WinSize
/// Get window/terminal size using ioctl.
/// Platform-specific constant (different on Darwin/BSD vs Linux).
///
- public static readonly uint TIOCGWINSZ =
- RuntimeInformation.IsOSPlatform (OSPlatform.OSX) || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD)
- ? 0x40087468u // Darwin/BSD
- : 0x5413u; // Linux
+ public static readonly uint TIOCGWINSZ = RuntimeInformation.IsOSPlatform (OSPlatform.OSX) || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD)
+ ? 0x40087468u // Darwin/BSD
+ : 0x5413u; // Linux
///
/// I/O control operations on file descriptors.
@@ -173,8 +172,8 @@ public struct WinSize
public static extern int ioctl (int fd, uint request, out WinSize ws);
///
- /// ioctl definition for Darwin/FreeBSD on ARM64.
- /// See https://github.com/dotnet/runtime/issues/48796#issuecomment-3695794860.
+ /// ioctl definition for Darwin/FreeBSD on ARM64.
+ /// See https://github.com/dotnet/runtime/issues/48796#issuecomment-3695794860.
///
/// File descriptor
/// Request code (e.g., TIOCGWINSZ)
@@ -187,7 +186,15 @@ public struct WinSize
/// Window size structure (output)
/// 0 on success, -1 on error
[DllImport ("libc", EntryPoint = "ioctl", SetLastError = true)]
- public static extern int ioctl_arm64 (int fd, ulong request, nint r3, nint r4, nint r5, nint r6, nint r7, nint r8, out WinSize ws);
+ public static extern int ioctl_arm64 (int fd,
+ ulong request,
+ nint r3,
+ nint r4,
+ nint r5,
+ nint r6,
+ nint r7,
+ nint r8,
+ out WinSize ws);
#endregion
@@ -298,12 +305,21 @@ public static bool TryGetTerminalSize (out Size size)
{
try
{
- int ioctlResult = 0;
+ var ioctlResult = 0;
WinSize ws;
- if (RuntimeInformation.OSArchitecture == Architecture.Arm64 &&
- (RuntimeInformation.IsOSPlatform (OSPlatform.OSX) || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD)))
+
+ if (RuntimeInformation.OSArchitecture == Architecture.Arm64
+ && (RuntimeInformation.IsOSPlatform (OSPlatform.OSX) || RuntimeInformation.IsOSPlatform (OSPlatform.FreeBSD)))
{
- ioctlResult = ioctl_arm64 (STDOUT_FILENO, TIOCGWINSZ, 0, 0, 0, 0, 0, 0, out ws);
+ ioctlResult = ioctl_arm64 (STDOUT_FILENO,
+ TIOCGWINSZ,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ out ws);
}
else
{
@@ -312,9 +328,9 @@ public static bool TryGetTerminalSize (out Size size)
if (ioctlResult == 0)
{
- if (ws.ws_col > 0 && ws.ws_row > 0)
+ if (ws is { ws_col: > 0, ws_row: > 0 })
{
- size = new (ws.ws_col, ws.ws_row);
+ size = new Size (ws.ws_col, ws.ws_row);
return true;
}
@@ -325,7 +341,7 @@ public static bool TryGetTerminalSize (out Size size)
// ignore
}
- size = new (80, 25); // fallback
+ size = new Size (80, 25); // fallback
return false;
}
diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs
index d2726bc83f..d049a83655 100644
--- a/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs
+++ b/Terminal.Gui/Drivers/UnixDriver/UnixInput.cs
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
+using Terminal.Gui.Tracing;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@@ -20,7 +21,13 @@ internal class UnixInput : InputImpl, IUnixInput, ITestableInput
public UnixInput ()
{
- //Logging.Information ($"Creating {nameof (UnixInput)}");
+ // Check if we have a real console first
+ if (!IsAttachedToTerminal)
+ {
+ Trace.Lifecycle (nameof (UnixInput), "Init", "Console is not attached to a terminal. Running in degraded mode.");
+
+ return;
+ }
try
{
@@ -30,17 +37,18 @@ public UnixInput ()
// Enable raw mode using the helper
_terminalInitialized = _rawModeHelper.TryEnable ();
- if (_terminalInitialized)
+ if (!_terminalInitialized)
{
- WriteRaw (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
- WriteRaw (EscSeqUtils.CSI_HideCursor);
-
- // CSI_EnableMouseEvents enables
- // Mode 1003 (any-event) - Reports all mouse events including motion with/without buttons
- // Mode 1015 (URXVT) - UTF-8 coordinate encoding (fallback for older terminals)
- // Mode 1006 (SGR) - Modern decimal format with unlimited coordinates (preferred)
- WriteRaw (EscSeqUtils.CSI_EnableMouseEvents);
+ return;
}
+ WriteRaw (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll);
+ WriteRaw (EscSeqUtils.CSI_HideCursor);
+
+ // CSI_EnableMouseEvents enables
+ // Mode 1003 (any-event) - Reports all mouse events including motion with/without buttons
+ // Mode 1015 (URXVT) - UTF-8 coordinate encoding (fallback for older terminals)
+ // Mode 1006 (SGR) - Modern decimal format with unlimited coordinates (preferred)
+ WriteRaw (EscSeqUtils.CSI_EnableMouseEvents);
}
catch (DllNotFoundException ex)
{
@@ -102,7 +110,7 @@ public override IEnumerable Read ()
continue;
}
- byte [] buf = new byte [256];
+ var buf = new byte [256];
if (!UnixIOHelper.TryReadStdin (buf, out int bytesRead) || bytesRead <= 0)
{
@@ -146,7 +154,7 @@ private void FlushConsoleInput ()
}
///
- public void InjectInput (char input) { _testInput.Enqueue (input); }
+ public void InjectInput (char input) => _testInput.Enqueue (input);
///
public override void Dispose ()
diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs
index 286ec59ec0..619dddf416 100644
--- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs
+++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs
@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
+using Terminal.Gui.Tracing;
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
@@ -10,12 +11,33 @@ namespace Terminal.Gui.Drivers;
internal class UnixOutput : OutputBase, IOutput
{
+ public UnixOutput ()
+ {
+ if (!IsAttachedToTerminal)
+ {
+ Trace.Lifecycle (nameof (UnixOutput), "Init", "No real terminal attached. Output operations will be no-op.");
+ }
+ }
+
///
- public void Suspend () => UnixTerminalHelper.Suspend (this);
+ public void Suspend ()
+ {
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
+ UnixTerminalHelper.Suspend (this);
+ }
///
public void Write (ReadOnlySpan text)
{
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
byte [] utf8 = Encoding.UTF8.GetBytes (text.ToArray ());
UnixIOHelper.TryWriteStdout (utf8);
}
@@ -25,19 +47,21 @@ protected override void Write (StringBuilder output)
{
base.Write (output);
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
byte [] utf8 = Encoding.UTF8.GetBytes (output.ToString ());
UnixIOHelper.TryWriteStdout (utf8);
}
private Cursor _currentCursor = new ();
- ///
- public Cursor GetCursor ()
- {
- return _currentCursor;
- }
+ ///
+ public Cursor GetCursor () => _currentCursor;
- ///
+ ///
public void SetCursor (Cursor cursor)
{
try
@@ -48,7 +72,7 @@ public void SetCursor (Cursor cursor)
}
else
{
- if (_currentCursor!.Style != cursor.Style)
+ if (_currentCursor.Style != cursor.Style)
{
Write (EscSeqUtils.CSI_SetCursorStyle (cursor.Style));
}
@@ -62,10 +86,7 @@ public void SetCursor (Cursor cursor)
}
finally
{
- SetCursorPositionImpl (
- cursor.Position?.X ?? 0,
- cursor.Position?.Y ?? 0
- );
+ SetCursorPositionImpl (cursor.Position?.X ?? 0, cursor.Position?.Y ?? 0);
_currentCursor = cursor;
}
@@ -74,7 +95,7 @@ public void SetCursor (Cursor cursor)
///
protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY)
{
- if (_currentCursor!.Position is { } && _currentCursor.Position.Value.X == screenPositionX && _currentCursor.Position.Value.Y == screenPositionY)
+ if (_currentCursor.Position is { } && _currentCursor.Position.Value.X == screenPositionX && _currentCursor.Position.Value.Y == screenPositionY)
{
return false;
}
@@ -116,10 +137,7 @@ protected override bool SetCursorPositionImpl (int screenPositionX, int screenPo
// create FileStream from the safe handle
var stream = new FileStream (handle, FileAccess.Write);
- return new StreamWriter (stream)
- {
- AutoFlush = true
- };
+ return new StreamWriter (stream) { AutoFlush = true };
}
catch (Exception ex)
{
@@ -132,12 +150,17 @@ protected override bool SetCursorPositionImpl (int screenPositionX, int screenPo
///
public Size GetSize ()
{
+ if (!IsAttachedToTerminal)
+ {
+ return new Size (80, 25);
+ }
+
if (UnixIOHelper.TryGetTerminalSize (out Size size))
{
return size;
}
- return new (80, 25); // fallback
+ return new Size (80, 25); // fallback
}
///
diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixTerminalHelper.cs b/Terminal.Gui/Drivers/UnixDriver/UnixTerminalHelper.cs
index a2d8f57cf0..5c48fd33ef 100644
--- a/Terminal.Gui/Drivers/UnixDriver/UnixTerminalHelper.cs
+++ b/Terminal.Gui/Drivers/UnixDriver/UnixTerminalHelper.cs
@@ -101,9 +101,11 @@ public static void Suspend (IOutput output)
output.Write (EscSeqUtils.CSI_DisableMouseEvents);
// Check if we have a real console first
- if (!AnsiTerminalHelper.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
+ if (!Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached))
{
- Trace.Lifecycle (nameof (UnixTerminalHelper), "Suspend", $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
+ Trace.Lifecycle (nameof (UnixTerminalHelper),
+ "Suspend",
+ $"Console redirected (Output: {outputAttached}, Input: {inputAttached}). Running in degraded mode.");
return;
}
diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs
index 7b4e4f6603..16bb3c0a53 100644
--- a/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs
+++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsInput.cs
@@ -1,5 +1,5 @@
using System.Runtime.InteropServices;
-using Microsoft.Extensions.Logging;
+using Terminal.Gui.Tracing;
using static Terminal.Gui.Drivers.WindowsConsole;
namespace Terminal.Gui.Drivers;
@@ -9,20 +9,10 @@ internal class WindowsInput : InputImpl, IWindowsInput
private readonly nint _inputHandle;
[DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)]
- public static extern bool ReadConsoleInput (
- nint hConsoleInput,
- nint lpBuffer,
- uint nLength,
- out uint lpNumberOfEventsRead
- );
+ public static extern bool ReadConsoleInput (nint hConsoleInput, nint lpBuffer, uint nLength, out uint lpNumberOfEventsRead);
[DllImport ("kernel32.dll", EntryPoint = "PeekConsoleInputW", CharSet = CharSet.Unicode)]
- public static extern bool PeekConsoleInput (
- nint hConsoleInput,
- nint lpBuffer,
- uint nLength,
- out uint lpNumberOfEventsRead
- );
+ public static extern bool PeekConsoleInput (nint hConsoleInput, nint lpBuffer, uint nLength, out uint lpNumberOfEventsRead);
[DllImport ("kernel32.dll", SetLastError = true)]
private static extern nint GetStdHandle (int nStdHandle);
@@ -40,7 +30,13 @@ out uint lpNumberOfEventsRead
public WindowsInput ()
{
- //Logging.Information ($"Creating {nameof (WindowsInput)}");
+ // Check if we have a real console first
+ if (!IsAttachedToTerminal)
+ {
+ Trace.Lifecycle (nameof (WindowsInput), "Init", "Console is not attached to a terminal. Running in degraded mode.");
+
+ return;
+ }
try
{
@@ -101,19 +97,14 @@ public override IEnumerable Read ()
try
{
- ReadConsoleInput (
- _inputHandle,
- pRecord,
- BUFFER_SIZE,
- out uint numberEventsRead);
-
- return numberEventsRead == 0
- ? []
- : new [] { Marshal.PtrToStructure (pRecord) };
+ ReadConsoleInput (_inputHandle, pRecord, BUFFER_SIZE, out uint numberEventsRead);
+
+ return numberEventsRead == 0 ? [] : new [] { Marshal.PtrToStructure (pRecord) };
}
catch (Exception)
{
Logging.Error ($"Error reading console input, error code: {Marshal.GetLastWin32Error ()}.");
+
return [];
}
finally
diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs
index cc5ab8218d..c6f3d818c6 100644
--- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs
+++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs
@@ -1,5 +1,6 @@
using System.ComponentModel;
using System.Runtime.InteropServices;
+using Terminal.Gui.Tracing;
namespace Terminal.Gui.Drivers;
@@ -7,13 +8,11 @@ internal partial class WindowsOutput : OutputBase, IOutput
{
[LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
[return: MarshalAs (UnmanagedType.Bool)]
- private static partial bool WriteConsole (
- nint hConsoleOutput,
- ReadOnlySpan lpBuffer,
- uint numberOfCharsToWrite,
- out uint lpNumberOfCharsWritten,
- nint lpReserved
- );
+ private static partial bool WriteConsole (nint hConsoleOutput,
+ ReadOnlySpan lpBuffer,
+ uint numberOfCharsToWrite,
+ out uint lpNumberOfCharsWritten,
+ nint lpReserved);
[LibraryImport ("kernel32.dll", SetLastError = true)]
private static partial nint GetStdHandle (int nStdHandle);
@@ -23,13 +22,11 @@ nint lpReserved
private static partial bool CloseHandle (nint handle);
[LibraryImport ("kernel32.dll", SetLastError = true)]
- private static partial nint CreateConsoleScreenBuffer (
- DesiredAccess dwDesiredAccess,
- ShareMode dwShareMode,
- nint securityAttributes,
- uint flags,
- nint screenBufferData
- );
+ private static partial nint CreateConsoleScreenBuffer (DesiredAccess dwDesiredAccess,
+ ShareMode dwShareMode,
+ nint securityAttributes,
+ uint flags,
+ nint screenBufferData);
[DllImport ("kernel32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
@@ -76,9 +73,7 @@ private enum DesiredAccess : uint
private static partial bool SetConsoleMode (nint hConsoleHandle, uint dwMode);
[LibraryImport ("kernel32.dll", SetLastError = true)]
- private static partial WindowsConsole.Coord GetLargestConsoleWindowSize (
- nint hConsoleOutput
- );
+ private static partial WindowsConsole.Coord GetLargestConsoleWindowSize (nint hConsoleOutput);
[DllImport ("kernel32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
@@ -86,11 +81,7 @@ nint hConsoleOutput
[DllImport ("kernel32.dll", SetLastError = true)]
[return: MarshalAs (UnmanagedType.Bool)]
- private static extern bool SetConsoleWindowInfo (
- nint hConsoleOutput,
- bool bAbsolute,
- [In] ref WindowsConsole.SmallRect lpConsoleWindow
- );
+ private static extern bool SetConsoleWindowInfo (nint hConsoleOutput, bool bAbsolute, [In] ref WindowsConsole.SmallRect lpConsoleWindow);
private const int STD_OUTPUT_HANDLE = -11;
private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
@@ -103,6 +94,13 @@ public WindowsOutput ()
{
//Logging.Information ($"Creating {nameof (WindowsOutput)}");
+ if (!IsAttachedToTerminal)
+ {
+ Trace.Lifecycle (nameof (WindowsOutput), "Init", "No real terminal attached. Output operations will be no-op.");
+
+ return;
+ }
+
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
@@ -152,11 +150,8 @@ public WindowsOutput ()
private Cursor _currentCursor = new ();
- ///
- public Cursor GetCursor ()
- {
- return _currentCursor;
- }
+ ///
+ public Cursor GetCursor () => _currentCursor;
//
public void SetCursor (Cursor cursor)
@@ -177,15 +172,15 @@ public void SetCursor (Cursor cursor)
cursorInfo.bVisible = true;
cursorInfo.dwSize = cursor.Style switch
- {
- CursorStyle.BlinkingBlock => 100,
- CursorStyle.SteadyBlock => 100,
- CursorStyle.BlinkingUnderline => 15,
- CursorStyle.SteadyUnderline => 15,
- CursorStyle.BlinkingBar => 15,
- CursorStyle.SteadyBar => 15,
- _ => 100
- };
+ {
+ CursorStyle.BlinkingBlock => 100,
+ CursorStyle.SteadyBlock => 100,
+ CursorStyle.BlinkingUnderline => 15,
+ CursorStyle.SteadyUnderline => 15,
+ CursorStyle.BlinkingBar => 15,
+ CursorStyle.SteadyBar => 15,
+ _ => 100
+ };
}
SetConsoleCursorInfo (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref cursorInfo);
@@ -198,7 +193,7 @@ public void SetCursor (Cursor cursor)
}
else
{
- if (_currentCursor!.Style != cursor.Style)
+ if (_currentCursor.Style != cursor.Style)
{
Write (EscSeqUtils.CSI_SetCursorStyle (cursor.Style));
}
@@ -213,10 +208,7 @@ public void SetCursor (Cursor cursor)
}
finally
{
- SetCursorPositionImpl (
- cursor.Position?.X ?? 0,
- cursor.Position?.Y ?? 0
- );
+ SetCursorPositionImpl (cursor.Position?.X ?? 0, cursor.Position?.Y ?? 0);
_currentCursor = cursor;
}
}
@@ -226,7 +218,7 @@ protected override bool SetCursorPositionImpl (int screenPositionX, int screenPo
{
if (Force16Colors && IsLegacyConsole)
{
- SetConsoleCursorPosition (_screenBuffer, new ((short)screenPositionX, (short)screenPositionY));
+ SetConsoleCursorPosition (_screenBuffer, new WindowsConsole.Coord ((short)screenPositionX, (short)screenPositionY));
}
else
{
@@ -240,13 +232,11 @@ protected override bool SetCursorPositionImpl (int screenPositionX, int screenPo
private void CreateScreenBuffer ()
{
- _screenBuffer = CreateConsoleScreenBuffer (
- DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
+ _screenBuffer = CreateConsoleScreenBuffer (DesiredAccess.GenericRead | DesiredAccess.GenericWrite,
ShareMode.FileShareRead | ShareMode.FileShareWrite,
nint.Zero,
1,
- nint.Zero
- );
+ nint.Zero);
if (_screenBuffer == INVALID_HANDLE_VALUE)
{
@@ -266,6 +256,11 @@ private void CreateScreenBuffer ()
public void Write (ReadOnlySpan str)
{
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
{
return;
@@ -296,9 +291,9 @@ public Size ResizeBuffer (Size size)
internal Size SetConsoleWindow (short cols, short rows)
{
- if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows))
+ if (!RuntimeInformation.IsOSPlatform (OSPlatform.Windows) || !IsAttachedToTerminal)
{
- return new (cols, rows);
+ return new Size (cols, rows);
}
var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX ();
@@ -312,36 +307,31 @@ internal Size SetConsoleWindow (short cols, short rows)
// Use the requested size directly. GetLargestConsoleWindowSize can underreport
// in modern terminals with non-default font sizes, causing the buffer to be
// clamped smaller than the actual window (visible as a gap at the bottom/right).
- short newCols = cols;
- short newRows = rows;
- csbi.dwSize = new (newCols, Math.Max (newRows, (short)1));
- csbi.srWindow = new (0, 0, newCols, newRows);
- csbi.dwMaximumWindowSize = new (newCols, newRows);
+ csbi.dwSize = new WindowsConsole.Coord (cols, Math.Max (rows, (short)1));
+ csbi.srWindow = new WindowsConsole.SmallRect (0, 0, cols, rows);
+ csbi.dwMaximumWindowSize = new WindowsConsole.Coord (cols, rows);
if (!SetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi))
{
throw new Win32Exception (Marshal.GetLastWin32Error ());
}
- var winRect = new WindowsConsole.SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0));
+ var winRect = new WindowsConsole.SmallRect (0, 0, (short)(cols - 1), (short)Math.Max (rows - 1, 0));
if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect))
{
//throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ());
- return new (cols, rows);
+ return new Size (cols, rows);
}
SetConsoleOutputWindow (csbi);
- return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1);
+ return new Size (winRect.Right + 1, rows - 1 < 0 ? 0 : winRect.Bottom + 1);
}
private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi)
{
- if ((!IsLegacyConsole
- ? _outputHandle
- : _screenBuffer)
- != nint.Zero
+ if ((!IsLegacyConsole ? _outputHandle : _screenBuffer) != nint.Zero
&& !SetConsoleScreenBufferInfoEx (!IsLegacyConsole ? _outputHandle : _screenBuffer, ref csbi))
{
throw new Win32Exception (Marshal.GetLastWin32Error ());
@@ -368,25 +358,31 @@ public override void Write (IOutputBuffer outputBuffer)
{
base.Write (outputBuffer);
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string
bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero);
- if (!result)
+ if (result)
{
- int err = Marshal.GetLastWin32Error ();
+ return;
+ }
+ int err = Marshal.GetLastWin32Error ();
- if (err == 1)
- {
- Logging.Error ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}");
+ if (err == 1)
+ {
+ Logging.Error ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}");
- return;
- }
+ return;
+ }
- if (err != 0)
- {
- throw new Win32Exception (err);
- }
+ if (err != 0)
+ {
+ throw new Win32Exception (err);
}
}
catch (DllNotFoundException)
@@ -414,6 +410,11 @@ protected override void Write (StringBuilder output)
base.Write (output);
+ if (!IsAttachedToTerminal)
+ {
+ return;
+ }
+
var str = output.ToString ();
if (Force16Colors && IsLegacyConsole)
@@ -429,21 +430,22 @@ protected override void Write (StringBuilder output)
bool result = WriteConsole (_outputHandle, span, (uint)span.Length, out _, nint.Zero);
- if (!result)
+ if (result)
{
- int err = Marshal.GetLastWin32Error ();
+ return;
+ }
+ int err = Marshal.GetLastWin32Error ();
- if (err == 1)
- {
- Logging.Error ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}");
+ if (err == 1)
+ {
+ Logging.Error ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}");
- return;
- }
+ return;
+ }
- if (err != 0)
- {
- throw new Win32Exception (err);
- }
+ if (err != 0)
+ {
+ throw new Win32Exception (err);
}
}
catch (DllNotFoundException)
@@ -507,31 +509,40 @@ public Size GetSize ()
_lastWindowSizeBeforeMaximized = null;
}
- if (_lastSize == null || _lastSize != newSize)
+ if (_lastSize == newSize)
{
- // User is resizing the screen, they can only ever resize the active
- // buffer since. We now however have issue because background offscreen
- // buffer will be wrong size, recreate it to ensure it doesn't result in
- // differing active and back buffer sizes (which causes flickering of window size)
- Size? bufSize = null;
- int retries = 0;
+ return newSize;
+ }
- while (bufSize != newSize && retries < 5)
- {
- _lockResize = true;
- bufSize = ResizeBuffer (newSize);
- retries++;
- }
+ // User is resizing the screen, they can only ever resize the active
+ // buffer since. We now however have issue because background offscreen
+ // buffer will be wrong size, recreate it to ensure it doesn't result in
+ // differing active and back buffer sizes (which causes flickering of window size)
+ Size? bufSize = null;
+ var retries = 0;
- _lockResize = false;
- _lastSize = newSize;
+ while (bufSize != newSize && retries < 5)
+ {
+ _lockResize = true;
+ bufSize = ResizeBuffer (newSize);
+ retries++;
}
+ _lockResize = false;
+ _lastSize = newSize;
+
return newSize;
}
public Size GetWindowSize (out WindowsConsole.Coord cursorPosition)
{
+ if (!IsAttachedToTerminal)
+ {
+ cursorPosition = default (WindowsConsole.Coord);
+
+ return new Size (80, 25);
+ }
+
try
{
var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX ();
@@ -545,9 +556,7 @@ public Size GetWindowSize (out WindowsConsole.Coord cursorPosition)
return Size.Empty;
}
- Size sz = new (
- csbi.srWindow.Right - csbi.srWindow.Left + 1,
- csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
+ Size sz = new (csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1);
cursorPosition = csbi.dwCursorPosition;
@@ -558,11 +567,16 @@ public Size GetWindowSize (out WindowsConsole.Coord cursorPosition)
cursorPosition = default (WindowsConsole.Coord);
}
- return new (80, 25);
+ return new Size (80, 25);
}
private Size GetLargestConsoleWindowSize ()
{
+ if (!IsAttachedToTerminal)
+ {
+ return new Size (80, 25);
+ }
+
WindowsConsole.Coord maxWinSize;
try
@@ -571,13 +585,12 @@ private Size GetLargestConsoleWindowSize ()
}
catch
{
- maxWinSize = new (80, 25);
+ maxWinSize = new WindowsConsole.Coord (80, 25);
}
- return new (maxWinSize.X, maxWinSize.Y);
+ return new Size (maxWinSize.X, maxWinSize.Y);
}
-
///
public void SetSize (int width, int height)
{
@@ -596,6 +609,13 @@ public void Dispose ()
return;
}
+ if (!IsAttachedToTerminal)
+ {
+ _isDisposed = true;
+
+ return;
+ }
+
if (IsLegacyConsole)
{
if (_screenBuffer != nint.Zero)
diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings
index d065a9ba65..c7fcaeef2e 100644
--- a/Terminal.sln.DotSettings
+++ b/Terminal.sln.DotSettings
@@ -543,6 +543,7 @@
True
True
True
+ True
True
True
True
diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props
new file mode 100644
index 0000000000..9318d68b87
--- /dev/null
+++ b/Tests/Directory.Build.props
@@ -0,0 +1,7 @@
+
+
+
+
+ $(MSBuildThisFileDirectory)test.runsettings
+
+
diff --git a/Tests/IntegrationTests/FluentTests/TestContextTests.cs b/Tests/IntegrationTests/FluentTests/TestContextTests.cs
index 980acb760a..881efabcba 100644
--- a/Tests/IntegrationTests/FluentTests/TestContextTests.cs
+++ b/Tests/IntegrationTests/FluentTests/TestContextTests.cs
@@ -11,6 +11,34 @@ public class TestContextTests (ITestOutputHelper outputHelper) : TestsAllDrivers
{
private readonly TextWriter _out = new TestOutputWriter (outputHelper);
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void DisableRealDriverIO_EnvironmentVariable_IsSet ()
+ {
+ // Diagnostic: verify the env var actually reaches the test process.
+ string? value = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
+ outputHelper.WriteLine ($"DisableRealDriverIO = '{value ?? "(null)"}'");
+
+ Assert.Equal ("1", value);
+ }
+
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void IsAttachedToTerminal_ReturnsFalse_WhenDisableRealDriverIO_IsSet ()
+ {
+ // Arrange – the env var should already be "1" via the test harness.
+ string? value = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
+ outputHelper.WriteLine ($"DisableRealDriverIO = '{value ?? "(null)"}'");
+
+ // Act
+ bool result = Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached);
+
+ // Assert
+ Assert.False (result, "IsAttachedToTerminal should return false when DisableRealDriverIO=1");
+ Assert.False (inputAttached, "inputAttached should be false when DisableRealDriverIO=1");
+ Assert.False (outputAttached, "outputAttached should be false when DisableRealDriverIO=1");
+ }
+
[Theory]
[MemberData (nameof (GetAllDriverNames))]
public void Constructor_Sets_Application_Screen (string d)
diff --git a/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs
deleted file mode 100644
index 1b88fcb296..0000000000
--- a/Tests/UnitTests/Application/MainLoopCoordinatorTests.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System.Collections.Concurrent;
-using Microsoft.Extensions.Logging;
-using Moq;
-
-namespace UnitTests.ApplicationTests;
-
-public class MainLoopCoordinatorTests
-{
- [Fact]
- public async Task TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread ()
- {
- Mock mockLogger = new ();
-
- ILogger beforeLogger = Logging.Logger;
- Logging.Logger = mockLogger.Object;
-
- Mock> m = new ();
-
- // Runs on a separate thread (input thread)
- m.Setup (f => f.CreateInput ()).Throws (new Exception ("Crash on boot"));
-
- MainLoopCoordinator c = new (new TimedEvents (),
-
- // Rest runs on main thread
- new ConcurrentQueue (),
- Mock.Of> (),
- m.Object);
-
- // StartAsync boots the main loop and the input thread. But if the input class bombs
- // on startup it is important that the exception surface at the call site and not lost
- var ex = await Assert.ThrowsAsync (() => c.StartInputTaskAsync (null));
- Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message);
-
- // Restore the original null logger to be polite to other tests
- Logging.Logger = beforeLogger;
-
- // Logs should explicitly call out that input loop crashed.
- mockLogger.Verify (l => l.Log (LogLevel.Critical,
- It.IsAny (),
- It.Is ((v, t) => v.ToString ().Contains ("Input loop crashed")),
- It.IsAny (),
- It.IsAny> ()),
- Times.Once);
- }
- /*
- [Fact]
- public void TestMainLoopCoordinator_InputExitsImmediately_ExceptionRaisedInMainThread ()
- {
-
- // Runs on a separate thread (input thread)
- // But because it's just a mock it immediately exists
- var mockInputFactoryMethod = () => Mock.Of> ();
-
-
- var mockOutput = Mock.Of ();
- var mockInputProcessor = Mock.Of ();
- var inputQueue = new ConcurrentQueue ();
- var timedEvents = new TimedEvents ();
-
- var mainLoop = new MainLoop ();
- mainLoop.Initialize (timedEvents,
- inputQueue,
- mockInputProcessor,
- mockOutput
- );
-
- var c = new MainLoopCoordinator (timedEvents,
- mockInputFactoryMethod,
- inputQueue,
- mockInputProcessor,
- ()=>mockOutput,
- mainLoop
- );
-
- // TODO: This test has race condition
- //
- // * When the input loop exits it can happen
- // * - During boot
- // * - After boot
- // *
- // * If it happens in boot you get input exited
- // * If it happens after you get "Input loop exited early (stop not called)"
- //
-
- // Because the console input class does not block - i.e. breaks contract
- // We need to let the user know input has silently exited and all has gone bad.
- var ex = Assert.ThrowsAsync (c.StartAsync).Result;
- Assert.Equal ("Input loop exited during startup instead of entering read loop properly (i.e. and blocking)", ex.Message);
- }*/
-}
diff --git a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs
index c59dfe0ccd..ae52bb071f 100644
--- a/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs
+++ b/Tests/UnitTestsParallelizable/Application/MainLoopCoordinatorTests.cs
@@ -1,6 +1,9 @@
-#nullable enable
using System.Collections.Concurrent;
using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Terminal.Gui.Tests;
+
// ReSharper disable AccessToDisposedClosure
#pragma warning disable xUnit1031
@@ -11,10 +14,10 @@ namespace ApplicationTests;
/// These tests ensure that the input thread starts, runs, and stops correctly when applications
/// are created, initialized, and disposed.
///
-[Collection("Application Tests")]
-public class MainLoopCoordinatorTests : IDisposable
+[Collection ("Application Tests")]
+public class MainLoopCoordinatorTests (ITestOutputHelper outputHelper) : IDisposable
{
- private readonly List _createdApps = new ();
+ private readonly List _createdApps = [];
public void Dispose ()
{
@@ -139,11 +142,11 @@ public void Multiple_Applications_Dispose_Without_Thread_Leaks ()
public void InputLoop_Throttle_Limits_Poll_Rate ()
{
// Arrange - Create a ANSIInput and manually run it with throttling
- AnsiInput input = new AnsiInput ();
- ConcurrentQueue queue = new ConcurrentQueue ();
+ var input = new AnsiInput ();
+ ConcurrentQueue queue = new ();
input.Initialize (queue);
- CancellationTokenSource cts = new CancellationTokenSource ();
+ var cts = new CancellationTokenSource ();
// Act - Run the input loop for 500ms
// Short duration reduces test time while still proving throttle exists
@@ -210,4 +213,63 @@ public void Throttle_Prevents_CPU_Saturation_With_Leaked_Apps ()
// With the throttle, each thread does Task.Delay(20ms) and exits within ~20-40ms
Assert.True (sw.ElapsedMilliseconds < 2000, $"Disposing {COUNT} apps took {sw.ElapsedMilliseconds}ms - CPU may be saturated");
}
+
+ [Fact]
+ public async Task TestMainLoopCoordinator_InputCrashes_ExceptionSurfacesMainThread ()
+ {
+ using IDisposable logScope = TestLogging.BindTo (outputHelper, LogLevel.Critical);
+
+ Mock> m = new ();
+
+ m.Setup (f => f.CreateInput ()).Throws (new Exception ("Crash on boot"));
+
+ MainLoopCoordinator c = new (new TimedEvents (), new ConcurrentQueue (), Mock.Of> (), m.Object);
+
+ AggregateException ex = await Assert.ThrowsAsync (() => c.StartInputTaskAsync (null));
+ Assert.Equal ("Crash on boot", ex.InnerExceptions [0].Message);
+ }
+
+ private sealed class TestAnsiComponentFactory (TestAnsiInput input, AnsiOutput output) : ComponentFactoryImpl
+ {
+ public override string GetDriverName () => DriverRegistry.Names.ANSI;
+
+ public override IInput CreateInput () => input;
+
+ public override IInputProcessor CreateInputProcessor (ConcurrentQueue inputBuffer, ITimeProvider? timeProvider = null) =>
+ new AnsiInputProcessor (inputBuffer, timeProvider);
+
+ public override IOutput CreateOutput () => output;
+
+ public override ISizeMonitor CreateSizeMonitor (IOutput consoleOutput, IOutputBuffer outputBuffer) => new SizeMonitorImpl (consoleOutput);
+ }
+
+ private sealed class TestAnsiInput (string? response) : IInput
+ {
+ private ConcurrentQueue? _inputQueue;
+
+ public CancellationTokenSource? ExternalCancellationTokenSource { get; set; }
+
+ public bool ResponseSent { get; private set; }
+
+ public void Initialize (ConcurrentQueue inputQueue) => _inputQueue = inputQueue;
+
+ public void Run (CancellationToken runCancellationToken)
+ {
+ if (!ResponseSent && !string.IsNullOrEmpty (response) && _inputQueue is { } inputQueue)
+ {
+ foreach (char ch in response)
+ {
+ inputQueue.Enqueue (ch);
+ }
+
+ ResponseSent = true;
+ }
+
+ WaitHandle.WaitAny ([runCancellationToken.WaitHandle]);
+
+ throw new OperationCanceledException (runCancellationToken);
+ }
+
+ public void Dispose () { }
+ }
}
diff --git a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs
index 164993dc4b..916fbf272e 100644
--- a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiInputOutputTests.cs
@@ -106,4 +106,18 @@ public void AnsiComponentFactory_CreateOutput_DoesNotThrow ()
// Assert
Assert.Null (exception);
}
+
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void AnsiDriver_IsAttachedToTerminal_ReturnsFalse_InTestHarness ()
+ {
+ // Copilot - generated.
+ // Act — Driver.IsAttachedToTerminal is the shared entry point all drivers use.
+ bool result = Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached);
+
+ // Assert
+ Assert.False (result, "AnsiDriver: IsAttachedToTerminal should return false in test harness");
+ Assert.False (inputAttached);
+ Assert.False (outputAttached);
+ }
}
diff --git a/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiTerminalHelperTests.cs b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiTerminalHelperTests.cs
new file mode 100644
index 0000000000..ff4bf4d1b5
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Drivers/Ansi/AnsiTerminalHelperTests.cs
@@ -0,0 +1,38 @@
+#nullable enable
+
+namespace DriverTests;
+
+///
+/// Tests for environment-variable gating.
+/// Copilot - generated.
+///
+public class DriverIsAttachedToTerminalTests (ITestOutputHelper output)
+{
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void DisableRealDriverIO_EnvironmentVariable_IsSet ()
+ {
+ // Diagnostic: verify the env var actually reaches the test process.
+ string? value = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
+ output.WriteLine ($"DisableRealDriverIO = '{value ?? "(null)"}'");
+
+ Assert.Equal ("1", value);
+ }
+
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void IsAttachedToTerminal_ReturnsFalse_WhenDisableRealDriverIO_IsSet ()
+ {
+ // Arrange – the env var should already be "1" via the test harness.
+ string? value = Environment.GetEnvironmentVariable ("DisableRealDriverIO");
+ output.WriteLine ($"DisableRealDriverIO = '{value ?? "(null)"}'");
+
+ // Act
+ bool result = Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached);
+
+ // Assert
+ Assert.False (result, "IsAttachedToTerminal should return false when DisableRealDriverIO=1");
+ Assert.False (inputAttached, "inputAttached should be false when DisableRealDriverIO=1");
+ Assert.False (outputAttached, "outputAttached should be false when DisableRealDriverIO=1");
+ }
+}
diff --git a/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputOutputTests.cs
index 8c8c74a887..1d94ded003 100644
--- a/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputOutputTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/Dotnet/NetInputOutputTests.cs
@@ -287,4 +287,18 @@ public void NetComponentFactory_CreateOutput_DoesNotThrow ()
Assert.Null (exception);
}
-}
+
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void NetDriver_IsAttachedToTerminal_ReturnsFalse_InTestHarness ()
+ {
+ // Copilot - generated.
+ // Act — Driver.IsAttachedToTerminal is the shared entry point all drivers use.
+ bool result = Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached);
+
+ // Assert
+ Assert.False (result, "NetDriver: IsAttachedToTerminal should return false in test harness");
+ Assert.False (inputAttached);
+ Assert.False (outputAttached);
+ }
+}
\ No newline at end of file
diff --git a/Tests/UnitTestsParallelizable/Drivers/Unix/UnixInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/Unix/UnixInputOutputTests.cs
index f3de24895d..d91bcba517 100644
--- a/Tests/UnitTestsParallelizable/Drivers/Unix/UnixInputOutputTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/Unix/UnixInputOutputTests.cs
@@ -119,4 +119,18 @@ public void UnixOutput_Suspend_DoesNotThrow_WhenNoTerminalAvailable ()
// Assert
Assert.Null (exception);
}
-}
+
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void UnixDriver_IsAttachedToTerminal_ReturnsFalse_InTestHarness ()
+ {
+ // Copilot - generated.
+ // Act — Driver.IsAttachedToTerminal is the shared entry point all drivers use.
+ bool result = Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached);
+
+ // Assert
+ Assert.False (result, "UnixDriver: IsAttachedToTerminal should return false in test harness");
+ Assert.False (inputAttached);
+ Assert.False (outputAttached);
+ }
+}
\ No newline at end of file
diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputOutputTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputOutputTests.cs
index 6fb72c464b..c11de00789 100644
--- a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputOutputTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsInputOutputTests.cs
@@ -91,4 +91,18 @@ public void WindowsOutput_Suspend_DoesNotThrow_WhenNoTerminalAvailable ()
// Assert
Assert.Null (exception);
}
-}
+
+ [Fact]
+ [Trait ("Category", "LowLevelDriver")]
+ public void WindowsDriver_IsAttachedToTerminal_ReturnsFalse_InTestHarness ()
+ {
+ // Copilot - generated.
+ // Act — Driver.IsAttachedToTerminal is the shared entry point all drivers use.
+ bool result = Driver.IsAttachedToTerminal (out bool inputAttached, out bool outputAttached);
+
+ // Assert
+ Assert.False (result, "WindowsDriver: IsAttachedToTerminal should return false in test harness");
+ Assert.False (inputAttached);
+ Assert.False (outputAttached);
+ }
+}
\ No newline at end of file
diff --git a/Tests/test.runsettings b/Tests/test.runsettings
new file mode 100644
index 0000000000..0deb342f93
--- /dev/null
+++ b/Tests/test.runsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ 1
+
+
+