From e7d7af877d36244e02ab595fc17ffc2121fa22db Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 28 Nov 2025 01:06:54 +0000 Subject: [PATCH 01/26] Fixes #4004. Driver "windows" broken in conhost and cmd --- Terminal.Gui/App/ApplicationImpl.Driver.cs | 3 +++ Terminal.Gui/App/IApplication.cs | 5 +++++ .../Drivers/WindowsDriver/WindowsOutput.cs | 18 ++++-------------- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index edb6adcbd2..a55eabd614 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -7,6 +7,9 @@ public partial class ApplicationImpl /// public IDriver? Driver { get; set; } + /// + public bool IsVirtualTerminal { get; internal set; } = true; + /// public bool Force16Colors { get; set; } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index a4a8c902eb..143abd0371 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -597,6 +597,11 @@ public TView Run (Func? errorHandler = null, string? dri /// IClipboard? Clipboard { get; } + /// + /// Gets or sets whether support for virtualized terminal sequences. + /// + public bool IsVirtualTerminal { get; } + /// /// Gets or sets whether will be forced to output only the 16 colors defined in /// . The default is , meaning 24-bit (TrueColor) colors will be diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index b351696a20..199e29a468 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -147,10 +147,8 @@ public WindowsOutput () } // Force 16 colors if not in virtual terminal mode. - // BUGBUG: This is bad. It does not work if the app was crated without - // BUGBUG: Apis. - //ApplicationImpl.Instance.Force16Colors = true; - + (ApplicationImpl.Instance as ApplicationImpl)!.IsVirtualTerminal = false; + ApplicationImpl.Instance.Force16Colors = true; } GetSize (); @@ -264,10 +262,7 @@ private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX public override void Write (IOutputBuffer outputBuffer) { - // BUGBUG: This is bad. It does not work if the app was crated without - // BUGBUG: Apis. - //_force16Colors = ApplicationImpl.Instance.Driver!.Force16Colors; - _force16Colors = false; + _force16Colors = ApplicationImpl.Instance.Force16Colors; _everythingStringBuilder.Clear (); // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. @@ -355,12 +350,7 @@ protected override void Write (StringBuilder output) /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - // BUGBUG: This is bad. It does not work if the app was crated without - // BUGBUG: Apis. - // bool force16Colors = ApplicationImpl.Instance.Force16Colors; - bool force16Colors = false; - - if (force16Colors) + if (_force16Colors) { if (_isVirtualTerminal) { From ba2444bb69554ad45273e8608bfe3161c1b90375 Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 28 Nov 2025 01:30:44 +0000 Subject: [PATCH 02/26] Fix unit tests --- .../Drivers/WindowsDriver/WindowsOutput.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 199e29a468..e9fda6b53d 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -146,9 +146,16 @@ public WindowsOutput () throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); } - // Force 16 colors if not in virtual terminal mode. - (ApplicationImpl.Instance as ApplicationImpl)!.IsVirtualTerminal = false; - ApplicationImpl.Instance.Force16Colors = true; + try + { + // Force 16 colors if not in virtual terminal mode. + (ApplicationImpl.Instance as ApplicationImpl)!.IsVirtualTerminal = false; + ApplicationImpl.Instance.Force16Colors = true; + } + catch + { + // possible running in unit tests + } } GetSize (); @@ -262,7 +269,15 @@ private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX public override void Write (IOutputBuffer outputBuffer) { - _force16Colors = ApplicationImpl.Instance.Force16Colors; + try + { + _force16Colors = ApplicationImpl.Instance.Force16Colors; + } + catch + { + // possible running in unit tests + } + _everythingStringBuilder.Clear (); // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. From c7475099fc2813be0b9c0bb1892fff3243aa2dce Mon Sep 17 00:00:00 2001 From: BDisp Date: Fri, 28 Nov 2025 22:21:45 +0000 Subject: [PATCH 03/26] Remove IsVirtualTerminal from IApplication. Add IDriverInternal and IOutputInternal interfaces --- Terminal.Gui/App/ApplicationImpl.Driver.cs | 3 - Terminal.Gui/App/IApplication.cs | 5 -- .../Drawing/Sixel/SixelSupportDetector.cs | 4 +- .../Drivers/DotNetDriver/NetOutput.cs | 8 ++- Terminal.Gui/Drivers/DriverImpl.cs | 43 ++++++++++-- Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs | 8 ++- Terminal.Gui/Drivers/IDriverInternal.cs | 9 +++ Terminal.Gui/Drivers/IOutputInternal.cs | 14 ++++ Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs | 8 ++- .../Drivers/WindowsDriver/WindowsOutput.cs | 68 ++++++++----------- Tests/UnitTests/FakeDriverBase.cs | 12 ++-- .../Application/ApplicationImplTests.cs | 4 +- .../Drivers/DriverTests.cs | 51 ++++++++++++++ 13 files changed, 171 insertions(+), 66 deletions(-) create mode 100644 Terminal.Gui/Drivers/IDriverInternal.cs create mode 100644 Terminal.Gui/Drivers/IOutputInternal.cs diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index a55eabd614..edb6adcbd2 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -7,9 +7,6 @@ public partial class ApplicationImpl /// public IDriver? Driver { get; set; } - /// - public bool IsVirtualTerminal { get; internal set; } = true; - /// public bool Force16Colors { get; set; } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index 143abd0371..a4a8c902eb 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -597,11 +597,6 @@ public TView Run (Func? errorHandler = null, string? dri /// IClipboard? Clipboard { get; } - /// - /// Gets or sets whether support for virtualized terminal sequences. - /// - public bool IsVirtualTerminal { get; } - /// /// Gets or sets whether will be forced to output only the 16 colors defined in /// . The default is , meaning 24-bit (TrueColor) colors will be diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index a9e1ae8aa0..dcc3439618 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs @@ -155,9 +155,9 @@ private void QueueRequest (AnsiEscapeSequence req, Action responseCallba private static bool ResponseIndicatesSupport (string response) { return response.Split (';').Contains ("4"); } - private static bool IsVirtualTerminal () + private bool IsVirtualTerminal () { - return !string.IsNullOrWhiteSpace (Environment.GetEnvironmentVariable ("WT_SESSION")); + return (_driver as IDriverInternal)?.IsVirtualTerminal == true; } private static bool IsXtermWithTransparency () diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index 4f8ab1fc09..868f1df6c0 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -4,10 +4,16 @@ namespace Terminal.Gui.Drivers; /// Implementation of that uses native dotnet /// methods e.g. /// -public class NetOutput : OutputBase, IOutput +public class NetOutput : OutputBase, IOutputInternal { private readonly bool _isWinPlatform; + /// + public IDriver? Driver { get; set; } + + /// + public bool IsVirtualTerminal { get; init; } = true; + /// /// Creates a new instance of the class. /// diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 9aebea3dd8..341f121a47 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -26,7 +26,7 @@ namespace Terminal.Gui.Drivers; /// Applications interact with drivers through the class. /// /// -internal class DriverImpl : IDriver +internal class DriverImpl : IDriverInternal { private readonly IOutput _output; private readonly AnsiRequestScheduler _ansiRequestScheduler; @@ -50,6 +50,7 @@ ISizeMonitor sizeMonitor { InputProcessor = inputProcessor; _output = output; + IsVirtualTerminal = (_output as IOutputInternal)!.IsVirtualTerminal; OutputBuffer = outputBuffer; _ansiRequestScheduler = ansiRequestScheduler; @@ -72,6 +73,8 @@ ISizeMonitor sizeMonitor }; CreateClipboard (); + + (_output as IOutputInternal)!.Driver = this; } /// @@ -197,16 +200,48 @@ public int Top // TODO: Probably not everyone right? + private bool _isVirtualTerminal = true; + + /// + public bool IsVirtualTerminal + { + get => _isVirtualTerminal; + set + { + _isVirtualTerminal = value; + + if (!_isVirtualTerminal) + { + Force16Colors = true; + } + } + } + /// - public bool SupportsTrueColor => true; + public bool SupportsTrueColor => _isVirtualTerminal; /// public bool Force16Colors { - get => Application.Force16Colors || !SupportsTrueColor; - set => Application.Force16Colors = value || !SupportsTrueColor; + get => Application.Force16Colors; + set + { + if (!_isVirtualTerminal && !Application.Force16Colors) + { + Application.Force16Colors = true; + + return; + } + + if (!_isVirtualTerminal && !value) + { + return; + } + + Application.Force16Colors = value; + } } /// diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index 8fd790f197..ac1d609397 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -5,13 +5,19 @@ namespace Terminal.Gui.Drivers; /// /// Fake console output for testing that captures what would be written to the console. /// -public class FakeOutput : OutputBase, IOutput +public class FakeOutput : OutputBase, IOutputInternal { private readonly StringBuilder _output = new (); private int _cursorLeft; private int _cursorTop; private Size _consoleSize = new (80, 25); + /// + public IDriver? Driver { get; set; } + + /// + public bool IsVirtualTerminal { get; init; } = true; + /// /// /// diff --git a/Terminal.Gui/Drivers/IDriverInternal.cs b/Terminal.Gui/Drivers/IDriverInternal.cs new file mode 100644 index 0000000000..278112cf27 --- /dev/null +++ b/Terminal.Gui/Drivers/IDriverInternal.cs @@ -0,0 +1,9 @@ +namespace Terminal.Gui.Drivers; + +internal interface IDriverInternal : IDriver +{ + /// + /// Gets or sets whether support for virtualized terminal sequences. + /// + public bool IsVirtualTerminal { get; set; } +} diff --git a/Terminal.Gui/Drivers/IOutputInternal.cs b/Terminal.Gui/Drivers/IOutputInternal.cs new file mode 100644 index 0000000000..754e1c28a9 --- /dev/null +++ b/Terminal.Gui/Drivers/IOutputInternal.cs @@ -0,0 +1,14 @@ +namespace Terminal.Gui.Drivers; + +internal interface IOutputInternal : IOutput +{ + /// + /// Get or sets the instance associated with this output. + /// + IDriver? Driver { get; set; } + + /// + /// Gets or sets whether support for virtualized terminal sequences. + /// + bool IsVirtualTerminal { get; init; } +} diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs index dfbf63ead4..5feb37e3cf 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui.Drivers; -internal class UnixOutput : OutputBase, IOutput +internal class UnixOutput : OutputBase, IOutputInternal { [StructLayout (LayoutKind.Sequential)] private struct WinSize @@ -36,6 +36,12 @@ private struct WinSize [DllImport ("libc", SetLastError = true)] private static extern int dup (int fd); + /// + public IDriver? Driver { get; set; } + + /// + public bool IsVirtualTerminal { get; init; } = true; + /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index e9fda6b53d..5bc49e2953 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui.Drivers; -internal partial class WindowsOutput : OutputBase, IOutput +internal partial class WindowsOutput : OutputBase, IOutputInternal { [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs (UnmanagedType.Bool)] @@ -97,10 +97,15 @@ [In] ref WindowsConsole.SmallRect lpConsoleWindow private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; private readonly nint _outputHandle; private nint _screenBuffer; - private readonly bool _isVirtualTerminal; private readonly ConsoleColor _foreground; private readonly ConsoleColor _background; + /// + public IDriver? Driver { get; set; } + + /// + public bool IsVirtualTerminal { get; init; } = true; + public WindowsOutput () { Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); @@ -113,9 +118,9 @@ public WindowsOutput () // Get the standard output handle which is the current screen buffer. _outputHandle = GetStdHandle (STD_OUTPUT_HANDLE); GetConsoleMode (_outputHandle, out uint mode); - _isVirtualTerminal = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; + IsVirtualTerminal = (mode & ENABLE_VIRTUAL_TERMINAL_PROCESSING) != 0; - if (_isVirtualTerminal) + if (IsVirtualTerminal) { if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) { @@ -145,17 +150,6 @@ public WindowsOutput () { throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); } - - try - { - // Force 16 colors if not in virtual terminal mode. - (ApplicationImpl.Instance as ApplicationImpl)!.IsVirtualTerminal = false; - ApplicationImpl.Instance.Force16Colors = true; - } - catch - { - // possible running in unit tests - } } GetSize (); @@ -194,7 +188,7 @@ public void Write (ReadOnlySpan str) return; } - if (!WriteConsole (_isVirtualTerminal ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) + if (!WriteConsole (IsVirtualTerminal ? _outputHandle : _screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) { throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to write to console screen buffer."); } @@ -225,19 +219,19 @@ internal Size SetConsoleWindow (short cols, short rows) var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); - if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + if (!GetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } - WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (IsVirtualTerminal ? _outputHandle : _screenBuffer); short newCols = Math.Min (cols, maxWinSize.X); short newRows = Math.Min (rows, maxWinSize.Y); csbi.dwSize = new (newCols, Math.Max (newRows, (short)1)); csbi.srWindow = new (0, 0, newCols, newRows); csbi.dwMaximumWindowSize = new (newCols, newRows); - if (!SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + if (!SetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } @@ -257,11 +251,11 @@ internal Size SetConsoleWindow (short cols, short rows) private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi) { - if ((_isVirtualTerminal + if ((IsVirtualTerminal ? _outputHandle : _screenBuffer) != nint.Zero - && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + && !SetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { throw new Win32Exception (Marshal.GetLastWin32Error ()); } @@ -269,23 +263,15 @@ private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX public override void Write (IOutputBuffer outputBuffer) { - try - { - _force16Colors = ApplicationImpl.Instance.Force16Colors; - } - catch - { - // possible running in unit tests - } - + _force16Colors = Driver?.Force16Colors ?? false; _everythingStringBuilder.Clear (); - // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. + // for 16 color mode we will write to a backing buffer, then flip it to the active one at the end to avoid jitter. _consoleBuffer = 0; if (_force16Colors) { - if (_isVirtualTerminal) + if (IsVirtualTerminal) { _consoleBuffer = _outputHandle; } @@ -303,7 +289,7 @@ public override void Write (IOutputBuffer outputBuffer) try { - if (_force16Colors && !_isVirtualTerminal) + if (_force16Colors && !IsVirtualTerminal) { SetConsoleActiveScreenBuffer (_consoleBuffer); } @@ -351,7 +337,7 @@ protected override void Write (StringBuilder output) var str = output.ToString (); - if (_force16Colors && !_isVirtualTerminal) + if (_force16Colors && !IsVirtualTerminal) { char [] a = str.ToCharArray (); WriteConsole (_screenBuffer, a, (uint)a.Length, out _, nint.Zero); @@ -367,7 +353,7 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute { if (_force16Colors) { - if (_isVirtualTerminal) + if (IsVirtualTerminal) { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); @@ -443,7 +429,7 @@ public Size GetWindowSize (out WindowsConsole.Coord cursorPosition) var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); - if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + if (!GetConsoleScreenBufferInfoEx (IsVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); cursorPosition = default (WindowsConsole.Coord); @@ -473,7 +459,7 @@ private Size GetLargestConsoleWindowSize () try { - maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + maxWinSize = GetLargestConsoleWindowSize (IsVirtualTerminal ? _outputHandle : _screenBuffer); } catch { @@ -486,7 +472,7 @@ private Size GetLargestConsoleWindowSize () /// protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) { - if (_force16Colors && !_isVirtualTerminal) + if (_force16Colors && !IsVirtualTerminal) { SetConsoleCursorPosition (_screenBuffer, new ((short)screenPositionX, (short)screenPositionY)); } @@ -510,7 +496,7 @@ public override void SetCursorVisibility (CursorVisibility visibility) return; } - if (!_isVirtualTerminal) + if (!IsVirtualTerminal) { var info = new WindowsConsole.ConsoleCursorInfo { @@ -544,7 +530,7 @@ public void SetCursorPosition (int col, int row) _lastCursorPosition = new (col, row); - if (_isVirtualTerminal) + if (IsVirtualTerminal) { var sb = new StringBuilder (); EscSeqUtils.CSI_AppendCursorPosition (sb, row + 1, col + 1); @@ -575,7 +561,7 @@ public void Dispose () return; } - if (_isVirtualTerminal) + if (IsVirtualTerminal) { if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) { diff --git a/Tests/UnitTests/FakeDriverBase.cs b/Tests/UnitTests/FakeDriverBase.cs index 0e6011e343..657c5a5880 100644 --- a/Tests/UnitTests/FakeDriverBase.cs +++ b/Tests/UnitTests/FakeDriverBase.cs @@ -4,7 +4,7 @@ namespace UnitTests; /// Enables tests to create a FakeDriver for testing purposes. /// [Collection ("Global Test Setup")] -public abstract class FakeDriverBase /*: IDisposable*/ +public abstract class FakeDriverBase : IDisposable { /// /// Creates a new FakeDriver instance with the specified buffer size. @@ -30,9 +30,9 @@ protected static IDriver CreateFakeDriver (int width = 80, int height = 25) return driver; } - ///// - //public void Dispose () - //{ - // Application.ResetState (true); - //} + /// + public void Dispose () + { + Application.ResetState (true); + } } diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs index dd59e2eb5f..8b4c6a2478 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs @@ -18,14 +18,14 @@ public class ApplicationImplTests m.Setup (f => f.CreateInput ()).Returns (netInput.Object); m.Setup (f => f.CreateInputProcessor (It.IsAny> ())).Returns (Mock.Of ()); - Mock consoleOutput = new (); + Mock consoleOutput = new (); var size = new Size (80, 25); consoleOutput.Setup (o => o.SetSize (It.IsAny (), It.IsAny ())) .Callback ((w, h) => size = new (w, h)); consoleOutput.Setup (o => o.GetSize ()).Returns (() => size); m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object); - m.Setup (f => f.CreateSizeMonitor (It.IsAny (), It.IsAny ())).Returns (Mock.Of ()); + m.Setup (f => f.CreateSizeMonitor (It.IsAny (), It.IsAny ())).Returns (Mock.Of ()); return new ApplicationImpl (m.Object); } diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 254396df8f..59f9d40051 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -92,6 +92,57 @@ public void All_Drivers_LayoutAndDraw_Cross_Platform (string driverName) app.Shutdown (); } + + [Fact] + public void IsVirtualTerminal_Returns_Expected_Values () + { + IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + Assert.NotNull (driver?.IsVirtualTerminal); + Assert.True (driver.IsVirtualTerminal); + + driver.IsVirtualTerminal = false; + Assert.False (driver.IsVirtualTerminal); + } + + [Fact] + public void IsVirtualTerminal_True_Force16Colors_True_False () + { + IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + Assert.NotNull (driver?.IsVirtualTerminal); + Assert.True (driver.IsVirtualTerminal); + Assert.False (driver.Force16Colors); + + driver.Force16Colors = true; + Assert.True (driver.IsVirtualTerminal); + Assert.True (driver.Force16Colors); + } + + [Fact] + public void IsVirtualTerminal_False_Force16Colors_Is_Always_True () + { + IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + Assert.NotNull (driver?.IsVirtualTerminal); + Assert.True (driver.IsVirtualTerminal); + Assert.False (driver.Force16Colors); + + driver.IsVirtualTerminal = false; + Assert.True (driver.Force16Colors); + + driver.Force16Colors = false; + Assert.True (driver.Force16Colors); + } + + [Fact] + public void IsVirtualTerminal_True_False_SupportsTrueColor_Is_Always_True_False () + { + IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + Assert.NotNull (driver?.IsVirtualTerminal); + Assert.True (driver.IsVirtualTerminal); + Assert.True (driver.SupportsTrueColor); + + driver.IsVirtualTerminal = false; + Assert.False (driver.SupportsTrueColor); + } } public class TestTop : Toplevel From 34c862c56b4e4ccd026e3bf782054184f8ffff49 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 00:14:25 +0000 Subject: [PATCH 04/26] Fix result.IsSupported --- Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index dcc3439618..98388eccd2 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs @@ -33,7 +33,7 @@ public SixelSupportDetector (IDriver? driver) : this () public void Detect (Action resultCallback) { var result = new SixelSupportResult (); - result.SupportsTransparency = IsVirtualTerminal () || IsXtermWithTransparency (); + result.SupportsTransparency = result.IsSupported = IsVirtualTerminal () || IsXtermWithTransparency (); IsSixelSupportedByDar (result, resultCallback); } From cb11fbfd4cb91b430c1f90f15a435e9b54393ccd Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 13:10:34 +0000 Subject: [PATCH 05/26] Remove internal interfaces and add them in the implementations classes --- .../Drivers/DotNetDriver/NetOutput.cs | 10 ++--- Terminal.Gui/Drivers/DriverImpl.cs | 42 ++++++++++--------- Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs | 9 +--- Terminal.Gui/Drivers/OutputBase.cs | 10 +++++ Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs | 13 +++--- .../Drivers/WindowsDriver/WindowsOutput.cs | 8 +--- .../Application/ApplicationImplTests.cs | 4 +- .../Drivers/DriverTests.cs | 8 ++-- 8 files changed, 50 insertions(+), 54 deletions(-) diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index 868f1df6c0..5f1f965f7f 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -4,16 +4,10 @@ namespace Terminal.Gui.Drivers; /// Implementation of that uses native dotnet /// methods e.g. /// -public class NetOutput : OutputBase, IOutputInternal +public class NetOutput : OutputBase, IOutput { private readonly bool _isWinPlatform; - /// - public IDriver? Driver { get; set; } - - /// - public bool IsVirtualTerminal { get; init; } = true; - /// /// Creates a new instance of the class. /// @@ -36,6 +30,8 @@ public NetOutput () { _isWinPlatform = true; } + + IsVirtualTerminal = true; } /// diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 341f121a47..7904734679 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -26,7 +26,7 @@ namespace Terminal.Gui.Drivers; /// Applications interact with drivers through the class. /// /// -internal class DriverImpl : IDriverInternal +internal class DriverImpl : IDriver { private readonly IOutput _output; private readonly AnsiRequestScheduler _ansiRequestScheduler; @@ -50,7 +50,7 @@ ISizeMonitor sizeMonitor { InputProcessor = inputProcessor; _output = output; - IsVirtualTerminal = (_output as IOutputInternal)!.IsVirtualTerminal; + IsVirtualTerminal = (_output as OutputBase)!.IsVirtualTerminal; OutputBuffer = outputBuffer; _ansiRequestScheduler = ansiRequestScheduler; @@ -74,7 +74,26 @@ ISizeMonitor sizeMonitor CreateClipboard (); - (_output as IOutputInternal)!.Driver = this; + (_output as OutputBase)!.Driver = this; + } + + private bool _isVirtualTerminal = true; + + /// + /// Gets or sets whether support for virtualized terminal sequences. + /// + internal bool IsVirtualTerminal + { + get => _isVirtualTerminal; + set + { + _isVirtualTerminal = value; + + if (!_isVirtualTerminal) + { + Force16Colors = true; + } + } } /// @@ -200,23 +219,6 @@ public int Top // TODO: Probably not everyone right? - private bool _isVirtualTerminal = true; - - /// - public bool IsVirtualTerminal - { - get => _isVirtualTerminal; - set - { - _isVirtualTerminal = value; - - if (!_isVirtualTerminal) - { - Force16Colors = true; - } - } - } - /// public bool SupportsTrueColor => _isVirtualTerminal; diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index ac1d609397..bc28c74df1 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -5,19 +5,13 @@ namespace Terminal.Gui.Drivers; /// /// Fake console output for testing that captures what would be written to the console. /// -public class FakeOutput : OutputBase, IOutputInternal +public class FakeOutput : OutputBase, IOutput { private readonly StringBuilder _output = new (); private int _cursorLeft; private int _cursorTop; private Size _consoleSize = new (80, 25); - /// - public IDriver? Driver { get; set; } - - /// - public bool IsVirtualTerminal { get; init; } = true; - /// /// /// @@ -25,6 +19,7 @@ public FakeOutput () { LastBuffer = new OutputBufferImpl (); LastBuffer.SetSize (80, 25); + IsVirtualTerminal = true; } /// diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index ad1f4120e5..3fe778c739 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -5,6 +5,16 @@ namespace Terminal.Gui.Drivers; /// public abstract class OutputBase { + /// + /// Get or sets the instance associated with this output. + /// + internal IDriver? Driver { get; set; } + + /// + /// Gets or sets whether support for virtualized terminal sequences. + /// + internal bool IsVirtualTerminal { get; set; } + private CursorVisibility? _cachedCursorVisibility; // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). diff --git a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs index 5feb37e3cf..46faaeb307 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs @@ -7,7 +7,7 @@ namespace Terminal.Gui.Drivers; -internal class UnixOutput : OutputBase, IOutputInternal +internal class UnixOutput : OutputBase, IOutput { [StructLayout (LayoutKind.Sequential)] private struct WinSize @@ -36,16 +36,15 @@ private struct WinSize [DllImport ("libc", SetLastError = true)] private static extern int dup (int fd); - /// - public IDriver? Driver { get; set; } - - /// - public bool IsVirtualTerminal { get; init; } = true; + public UnixOutput () + { + IsVirtualTerminal = true; + } /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - if (Application.Force16Colors) + if (Driver?.Force16Colors == true) { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 5bc49e2953..786a402fab 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui.Drivers; -internal partial class WindowsOutput : OutputBase, IOutputInternal +internal partial class WindowsOutput : OutputBase, IOutput { [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs (UnmanagedType.Bool)] @@ -100,12 +100,6 @@ [In] ref WindowsConsole.SmallRect lpConsoleWindow private readonly ConsoleColor _foreground; private readonly ConsoleColor _background; - /// - public IDriver? Driver { get; set; } - - /// - public bool IsVirtualTerminal { get; init; } = true; - public WindowsOutput () { Logging.Logger.LogInformation ($"Creating {nameof (WindowsOutput)}"); diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs index 8b4c6a2478..dd59e2eb5f 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationImplTests.cs @@ -18,14 +18,14 @@ public class ApplicationImplTests m.Setup (f => f.CreateInput ()).Returns (netInput.Object); m.Setup (f => f.CreateInputProcessor (It.IsAny> ())).Returns (Mock.Of ()); - Mock consoleOutput = new (); + Mock consoleOutput = new (); var size = new Size (80, 25); consoleOutput.Setup (o => o.SetSize (It.IsAny (), It.IsAny ())) .Callback ((w, h) => size = new (w, h)); consoleOutput.Setup (o => o.GetSize ()).Returns (() => size); m.Setup (f => f.CreateOutput ()).Returns (consoleOutput.Object); - m.Setup (f => f.CreateSizeMonitor (It.IsAny (), It.IsAny ())).Returns (Mock.Of ()); + m.Setup (f => f.CreateSizeMonitor (It.IsAny (), It.IsAny ())).Returns (Mock.Of ()); return new ApplicationImpl (m.Object); } diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 59f9d40051..831f64193b 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -96,7 +96,7 @@ public void All_Drivers_LayoutAndDraw_Cross_Platform (string driverName) [Fact] public void IsVirtualTerminal_Returns_Expected_Values () { - IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + DriverImpl? driver = CreateFakeDriver () as DriverImpl; Assert.NotNull (driver?.IsVirtualTerminal); Assert.True (driver.IsVirtualTerminal); @@ -107,7 +107,7 @@ public void IsVirtualTerminal_Returns_Expected_Values () [Fact] public void IsVirtualTerminal_True_Force16Colors_True_False () { - IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + DriverImpl? driver = CreateFakeDriver () as DriverImpl; Assert.NotNull (driver?.IsVirtualTerminal); Assert.True (driver.IsVirtualTerminal); Assert.False (driver.Force16Colors); @@ -120,7 +120,7 @@ public void IsVirtualTerminal_True_Force16Colors_True_False () [Fact] public void IsVirtualTerminal_False_Force16Colors_Is_Always_True () { - IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + DriverImpl? driver = CreateFakeDriver () as DriverImpl; Assert.NotNull (driver?.IsVirtualTerminal); Assert.True (driver.IsVirtualTerminal); Assert.False (driver.Force16Colors); @@ -135,7 +135,7 @@ public void IsVirtualTerminal_False_Force16Colors_Is_Always_True () [Fact] public void IsVirtualTerminal_True_False_SupportsTrueColor_Is_Always_True_False () { - IDriverInternal? driver = CreateFakeDriver () as IDriverInternal; + DriverImpl? driver = CreateFakeDriver () as DriverImpl; Assert.NotNull (driver?.IsVirtualTerminal); Assert.True (driver.IsVirtualTerminal); Assert.True (driver.SupportsTrueColor); From d8205258724a78a0a35ac1f85475a55535a050f1 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 13:16:35 +0000 Subject: [PATCH 06/26] Move Sixel from IApplication to IDriver interface it's a characteristic of the driver --- Terminal.Gui/App/Application.Driver.cs | 6 +++--- Terminal.Gui/App/ApplicationImpl.Driver.cs | 3 --- Terminal.Gui/App/ApplicationImpl.Lifecycle.cs | 9 +++------ Terminal.Gui/App/IApplication.cs | 6 ------ .../Drawing/Sixel/SixelSupportDetector.cs | 2 +- Terminal.Gui/Drivers/DriverImpl.cs | 3 +++ Terminal.Gui/Drivers/IDriver.cs | 6 ++++++ Terminal.Gui/Drivers/IDriverInternal.cs | 9 --------- Terminal.Gui/Drivers/IOutputInternal.cs | 14 -------------- Terminal.Gui/Drivers/OutputBase.cs | 18 ++++++++---------- 10 files changed, 24 insertions(+), 52 deletions(-) delete mode 100644 Terminal.Gui/Drivers/IDriverInternal.cs delete mode 100644 Terminal.Gui/Drivers/IOutputInternal.cs diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 427ba4de5f..a691447f42 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -51,9 +51,9 @@ public static string ForceDriver /// Raised when changes. public static event EventHandler>? ForceDriverChanged; - /// - [Obsolete ("The legacy static Application object is going away.")] - public static List Sixel => ApplicationImpl.Instance.Sixel; + /// + [Obsolete ("The legacy static Application object is going away.")] + public static List Sixel => ApplicationImpl.Instance.Driver?.Sixel!; /// Gets a list of types and type names that are available. /// diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index edb6adcbd2..013ccac065 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -13,9 +13,6 @@ public partial class ApplicationImpl /// public string ForceDriver { get; set; } = string.Empty; - /// - public List Sixel { get; } = new (); - /// /// Creates the appropriate based on platform and driverName. /// diff --git a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs index e0a7390b71..e49d9f29c4 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -262,17 +262,14 @@ public void ResetState (bool ignoreDisposed = false) Initialized = false; MainThreadId = null; - // === 9. Clear graphics === - Sixel.Clear (); - - // === 10. Reset ForceDriver === + // === 9. Reset ForceDriver === // Note: ForceDriver and Force16Colors are reset // If they need to persist across Init/Shutdown cycles // then the user of the library should manage that state Force16Colors = false; ForceDriver = string.Empty; - // === 11. Reset synchronization context === + // === 10. Reset synchronization context === // IMPORTANT: Always reset sync context, even if not initialized // This ensures cleanup works correctly even if Shutdown is called without Init // Reset synchronization context to allow the user to run async/await, @@ -281,7 +278,7 @@ public void ResetState (bool ignoreDisposed = false) // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (null); - // === 12. Unsubscribe from Application static property change events === + // === 11. Unsubscribe from Application static property change events === UnsubscribeApplicationEvents (); } diff --git a/Terminal.Gui/App/IApplication.cs b/Terminal.Gui/App/IApplication.cs index a4a8c902eb..9a226c27e0 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -645,12 +645,6 @@ public TView Run (Func? errorHandler = null, string? dri /// bool ClearScreenNextIteration { get; set; } - /// - /// Collection of sixel images to write out to screen when updating. - /// Only add to this collection if you are sure terminal supports sixel format. - /// - List Sixel { get; } - #endregion Screen and Driver #region Layout and Drawing diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index 98388eccd2..a393dd0acb 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs @@ -157,7 +157,7 @@ private void QueueRequest (AnsiEscapeSequence req, Action responseCallba private bool IsVirtualTerminal () { - return (_driver as IDriverInternal)?.IsVirtualTerminal == true; + return (_driver as DriverImpl)?.IsVirtualTerminal == true; } private static bool IsXtermWithTransparency () diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 7904734679..a408a9dea7 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -246,6 +246,9 @@ public bool Force16Colors } } + /// + public List Sixel { get; } = []; + /// public Attribute CurrentAttribute diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 8616d8edf9..7d1fe23fb7 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -100,6 +100,12 @@ public interface IDriver /// bool Force16Colors { get; set; } + /// + /// Collection of sixel images to write out to screen when updating. + /// Only add to this collection if you are sure terminal supports sixel format. + /// + List Sixel { get; } + /// /// The that will be used for the next or /// call. diff --git a/Terminal.Gui/Drivers/IDriverInternal.cs b/Terminal.Gui/Drivers/IDriverInternal.cs deleted file mode 100644 index 278112cf27..0000000000 --- a/Terminal.Gui/Drivers/IDriverInternal.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Terminal.Gui.Drivers; - -internal interface IDriverInternal : IDriver -{ - /// - /// Gets or sets whether support for virtualized terminal sequences. - /// - public bool IsVirtualTerminal { get; set; } -} diff --git a/Terminal.Gui/Drivers/IOutputInternal.cs b/Terminal.Gui/Drivers/IOutputInternal.cs deleted file mode 100644 index 754e1c28a9..0000000000 --- a/Terminal.Gui/Drivers/IOutputInternal.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Terminal.Gui.Drivers; - -internal interface IOutputInternal : IOutput -{ - /// - /// Get or sets the instance associated with this output. - /// - IDriver? Driver { get; set; } - - /// - /// Gets or sets whether support for virtualized terminal sequences. - /// - bool IsVirtualTerminal { get; init; } -} diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 3fe778c739..deecf9a603 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -100,16 +100,14 @@ public virtual void Write (IOutputBuffer buffer) } } - // BUGBUG: The Sixel impl depends on the legacy static Application object - // BUGBUG: Disabled for now - //foreach (SixelToRender s in Application.Sixel) - //{ - // if (!string.IsNullOrWhiteSpace (s.SixelData)) - // { - // SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - // Console.Out.Write (s.SixelData); - // } - //} + foreach (SixelToRender s in Driver?.Sixel!) + { + if (!string.IsNullOrWhiteSpace (s.SixelData)) + { + SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + Console.Out.Write (s.SixelData); + } + } SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); _cachedCursorVisibility = savedVisibility; From 454f0f033fd189110a8ef9f9c11bda8bc9df5784 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 13:30:49 +0000 Subject: [PATCH 07/26] Only if IOutput is OutputBase then set the internal properties --- Terminal.Gui/Drivers/DriverImpl.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index a408a9dea7..d51a39c28a 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -50,7 +50,6 @@ ISizeMonitor sizeMonitor { InputProcessor = inputProcessor; _output = output; - IsVirtualTerminal = (_output as OutputBase)!.IsVirtualTerminal; OutputBuffer = outputBuffer; _ansiRequestScheduler = ansiRequestScheduler; @@ -74,7 +73,11 @@ ISizeMonitor sizeMonitor CreateClipboard (); - (_output as OutputBase)!.Driver = this; + if (_output is OutputBase outputBase) + { + IsVirtualTerminal = outputBase.IsVirtualTerminal; + outputBase.Driver = this; + } } private bool _isVirtualTerminal = true; From fec6c9673095d31a2cbc714960f1886673d6a076 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 14:17:32 +0000 Subject: [PATCH 08/26] Prevents driver windows error on Unix system --- Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs | 9 +++++++-- .../UnitTests/Application/SynchronizatonContextTests.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 786a402fab..a47456b54f 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -279,10 +279,10 @@ public override void Write (IOutputBuffer outputBuffer) _consoleBuffer = _outputHandle; } - base.Write (outputBuffer); - try { + base.Write (outputBuffer); + if (_force16Colors && !IsVirtualTerminal) { SetConsoleActiveScreenBuffer (_consoleBuffer); @@ -303,6 +303,7 @@ public override void Write (IOutputBuffer outputBuffer) return; } + if (err != 0) { throw new Win32Exception (err); @@ -310,6 +311,10 @@ public override void Write (IOutputBuffer outputBuffer) } } } + catch (DllNotFoundException) + { + // Running unit tests or in an environment where writing is not possible. + } catch (Exception e) { Logging.Logger.LogError ($"Error: {e.Message} in {nameof (WindowsOutput)}"); diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 7137389f74..5812740526 100644 --- a/Tests/UnitTests/Application/SynchronizatonContextTests.cs +++ b/Tests/UnitTests/Application/SynchronizatonContextTests.cs @@ -26,7 +26,7 @@ public void SynchronizationContext_CreateCopy () [InlineData ("fake")] [InlineData ("windows")] [InlineData ("dotnet")] - // [InlineData ("unix")] + [InlineData ("unix")] public void SynchronizationContext_Post (string driverName = null) { lock (_lockPost) From c72340ecd4c2ded6a1ef9915bf5b03617dcb42c7 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 14:30:35 +0000 Subject: [PATCH 09/26] Fix scenario sixel error --- Examples/UICatalog/Scenarios/Images.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index 5791166cb8..32c9c94b53 100644 --- a/Examples/UICatalog/Scenarios/Images.cs +++ b/Examples/UICatalog/Scenarios/Images.cs @@ -246,7 +246,7 @@ protected override void Dispose (bool disposing) _sixelSupported.Dispose (); _isDisposed = true; - Application.Sixel.Clear (); + Application.Driver?.Sixel.Clear (); } private void OpenImage (object sender, CommandEventArgs e) From 9115bad22a9136452cf7dd61d221dd73b1bc4a6a Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 14:37:24 +0000 Subject: [PATCH 10/26] Comment some tests because is keyboard layout dependent and shifted key is needed to produce them (Pt) --- .../Drivers/Windows/WindowsKeyConverterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs index f50ee88092..a32e38e257 100644 --- a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs @@ -269,15 +269,15 @@ public void ToKey_VKPacket_SurrogatePair_DocumentsCurrentLimitation () #region ToKey Tests - OEM Keys [Theory] - [InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')] + //[InlineData (';', ConsoleKey.Oem1, false, (KeyCode)';')] // Keyboard layout dependent and shifted key is needed to produce ';' (Pt) [InlineData (':', ConsoleKey.Oem1, true, (KeyCode)':')] - [InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')] + //[InlineData ('/', ConsoleKey.Oem2, false, (KeyCode)'/')] // Keyboard layout dependent and shifted key is needed to produce '/' (Pt) [InlineData ('?', ConsoleKey.Oem2, true, (KeyCode)'?')] [InlineData (',', ConsoleKey.OemComma, false, (KeyCode)',')] [InlineData ('<', ConsoleKey.OemComma, true, (KeyCode)'<')] [InlineData ('.', ConsoleKey.OemPeriod, false, (KeyCode)'.')] [InlineData ('>', ConsoleKey.OemPeriod, true, (KeyCode)'>')] - [InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Un-shifted OemPlus is '=' + //[InlineData ('=', ConsoleKey.OemPlus, false, (KeyCode)'=')] // Keyboard layout dependent and shifted key is needed to produce '=' (Pt) [InlineData ('+', ConsoleKey.OemPlus, true, (KeyCode)'+')] // Shifted OemPlus is '+' [InlineData ('-', ConsoleKey.OemMinus, false, (KeyCode)'-')] [InlineData ('_', ConsoleKey.OemMinus, true, (KeyCode)'_')] // Shifted OemMinus is '_' From 7742679eb8e7bf958f8b6da4e4641cf80f0e5217 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 17:55:06 +0000 Subject: [PATCH 11/26] =?UTF-8?q?Add=20=F0=9F=87=B5=F0=9F=87=B9=20regional?= =?UTF-8?q?=20indicators=20test=20proving=20they=20ca=20be=20joined=20as?= =?UTF-8?q?=20only=20one=20grapheme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/UnitTestsParallelizable/Drawing/CellTests.cs | 3 ++- Tests/UnitTestsParallelizable/Text/StringTests.cs | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs index b51ab37a92..d66cbc1b06 100644 --- a/Tests/UnitTestsParallelizable/Drawing/CellTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs @@ -23,6 +23,7 @@ public void Constructor_Defaults () [InlineData ("æ", new uint [] { 0x00E6 })] [InlineData ("a︠", new uint [] { 0x0061, 0xFE20 })] [InlineData ("e︡", new uint [] { 0x0065, 0xFE21 })] + [InlineData ("🇵🇹", new uint [] { 0x1F1F5, 0x1F1F9 })] public void Runes_From_Grapheme (string grapheme, uint [] expected) { // Arrange @@ -88,6 +89,7 @@ public static IEnumerable ToStringTestData () yield return ["👨‍👩‍👦‍👦", null, "[\"👨‍👩‍👦‍👦\":]"]; yield return ["A", new Attribute (Color.Red) { Style = TextStyle.Blink }, "[\"A\":[Red,Red,Blink]]"]; yield return ["\U0001F469\u200D\u2764\uFE0F\u200D\U0001F48B\u200D\U0001F468", null, "[\"👩‍❤️‍💋‍👨\":]"]; + yield return ["\uD83C\uDDF5\uD83C\uDDF9", null, "[\"🇵🇹\":]"]; } [Fact] @@ -176,5 +178,4 @@ public void Surrogate_Normalize_Throws_And_Cell_Setter_Throws () // And if your Grapheme setter normalizes, assignment should throw as well Assert.Throws (() => new Cell () { Grapheme = s }); } - } diff --git a/Tests/UnitTestsParallelizable/Text/StringTests.cs b/Tests/UnitTestsParallelizable/Text/StringTests.cs index a3c4e52bab..5b5d02b8f8 100644 --- a/Tests/UnitTestsParallelizable/Text/StringTests.cs +++ b/Tests/UnitTestsParallelizable/Text/StringTests.cs @@ -77,6 +77,7 @@ public void TestGetColumns_Zero_Width () [InlineData ("ힰ", 0, 1, 0)] // U+D7B0 ힰ Hangul Jungseong O-Yeo [InlineData ("ᄀힰ", 2, 1, 2)] // ᄀ U+1100 HANGUL CHOSEONG KIYEOK (consonant) with U+D7B0 ힰ Hangul Jungseong O-Yeo //[InlineData ("षि", 2, 1, 2)] // U+0937 ष DEVANAGARI LETTER SSA with U+093F ि COMBINING DEVANAGARI VOWEL SIGN I + [InlineData ("🇵🇹", 2, 1, 2)] // 🇵 U+1F1F5 — REGIONAL INDICATOR SYMBOL LETTER P with 🇹 U+1F1F9 — REGIONAL INDICATOR SYMBOL LETTER T (flag of Portugal) public void TestGetColumns_MultiRune_WideBMP_Graphemes (string str, int expectedRunesWidth, int expectedGraphemesCount, int expectedWidth) { Assert.Equal (expectedRunesWidth, str.EnumerateRunes ().Sum (r => r.GetColumns ())); @@ -165,6 +166,7 @@ public static IEnumerable GetStringConcatCases () yield return [new [] { "👩‍", "🧒" }, "👩‍🧒"]; // Grapheme sequence yield return [new [] { "α", "β", "γ" }, "αβγ"]; // Unicode letters yield return [new [] { "A", null, "B" }, "AB"]; // Null ignored by string.Concat + yield return [new [] { "🇵", "🇹" }, "🇵🇹"]; // Grapheme sequence } [Theory] From 954d5e6cd619a2e3be78fbe0a975b77f5f5415d1 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 19:18:40 +0000 Subject: [PATCH 12/26] SetConsoleActiveScreenBuffer is already called by the constructor and is only needed once --- .../Drivers/WindowsDriver/WindowsOutput.cs | 33 ++++++++----------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index a47456b54f..8532b1bb37 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -283,31 +283,24 @@ public override void Write (IOutputBuffer outputBuffer) { base.Write (outputBuffer); - if (_force16Colors && !IsVirtualTerminal) - { - SetConsoleActiveScreenBuffer (_consoleBuffer); - } - else - { - ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string - bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); + bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); - if (!result) - { - int err = Marshal.GetLastWin32Error (); + if (!result) + { + int err = Marshal.GetLastWin32Error (); - if (err == 1) - { - Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); + if (err == 1) + { + Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); - return; - } + return; + } - if (err != 0) - { - throw new Win32Exception (err); - } + if (err != 0) + { + throw new Win32Exception (err); } } } From 654f9d52c5256d78530edb790484b0433609f935 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 21:54:52 +0000 Subject: [PATCH 13/26] Finally fixed non virtual terminal in windows driver --- Terminal.Gui/Drivers/OutputBase.cs | 17 ++++++++++++----- .../Drivers/WindowsDriver/WindowsOutput.cs | 2 ++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index deecf9a603..64d0bbd87a 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -59,7 +59,7 @@ public virtual void Write (IOutputBuffer buffer) { if (!buffer.Contents! [row, col].IsDirty) { - if (output.Length > 0) + if (!IsVirtualTerminal && output.Length > 0) { WriteToConsole (output, ref lastCol, row, ref outputWidth); } @@ -92,11 +92,18 @@ public virtual void Write (IOutputBuffer buffer) if (output.Length > 0) { - SetCursorPositionImpl (lastCol, row); + if (IsVirtualTerminal) + { + SetCursorPositionImpl (lastCol, row); - // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker - StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); - Write (processed); + // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker + StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); + Write (processed); + } + else + { + Write (output); + } } } diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs index 8532b1bb37..4f81e3e9e8 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -353,6 +353,8 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute } else { + Write (output); + output.Clear (); var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4)); SetConsoleTextAttribute (_screenBuffer, as16ColorInt); } From 938a8c376ddd49860c2419609c276f0ebd8e095d Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 23:06:14 +0000 Subject: [PATCH 14/26] Add more Sixel unit tests --- .../Drawing/{ => Sixel}/SixelEncoderTests.cs | 123 +++++++++- .../Sixel/SixelSupportDetectorTests.cs | 94 ++++++++ .../Drawing/Sixel/SixelSupportResultTests.cs | 62 +++++ .../Drawing/Sixel/SixelToRenderTests.cs | 215 ++++++++++++++++++ 4 files changed, 488 insertions(+), 6 deletions(-) rename Tests/UnitTestsParallelizable/Drawing/{ => Sixel}/SixelEncoderTests.cs (74%) create mode 100644 Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs diff --git a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs similarity index 74% rename from Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs rename to Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs index ab97eed738..ab76925f40 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs @@ -1,4 +1,5 @@ - +#nullable enable + namespace UnitTests_Parallelizable.DrawingTests; public class SixelEncoderTests @@ -37,7 +38,7 @@ public void EncodeSixel_RedSquare12x12_ReturnsExpectedSixel () { for (var y = 0; y < 12; y++) { - pixels [x, y] = new (255, 0, 0); + pixels [x, y] = new (255, 0); } } @@ -48,7 +49,7 @@ public void EncodeSixel_RedSquare12x12_ReturnsExpectedSixel () // Since image is only red we should only have 1 color definition Color c1 = Assert.Single (encoder.Quantizer.Palette); - Assert.Equal (new (255, 0, 0), c1); + Assert.Equal (new (255, 0), c1); Assert.Equal (expected, result); } @@ -124,7 +125,7 @@ public void EncodeSixel_12x12GridPattern3x3_ReturnsExpectedSixel () // Create a 3x3 checkerboard by alternating the color based on pixel coordinates if ((x / 3 + y / 3) % 2 == 0) { - pixels [x, y] = new (0, 0, 0); // Black + pixels [x, y] = new (0, 0); // Black } else { @@ -142,7 +143,7 @@ public void EncodeSixel_12x12GridPattern3x3_ReturnsExpectedSixel () Color black = encoder.Quantizer.Palette.ElementAt (0); Color white = encoder.Quantizer.Palette.ElementAt (1); - Assert.Equal (new (0, 0, 0), black); + Assert.Equal (new (0, 0), black); Assert.Equal (new (255, 255, 255), white); // Compare the generated SIXEL string with the expected one @@ -213,7 +214,7 @@ public void EncodeSixel_VerticalMix_TransparentAndColor_ReturnsExpectedSixel () // For simplicity, we'll make every other row transparent if (y % 2 == 0) { - pixels [x, y] = new (255, 0, 0); // Red pixel + pixels [x, y] = new (255, 0); // Red pixel } else { @@ -229,4 +230,114 @@ public void EncodeSixel_VerticalMix_TransparentAndColor_ReturnsExpectedSixel () // Assert: Expect the result to match the expected sixel output Assert.Equal (expected, result); } + + [Fact] + public void EncodeSixel_OnePixel_ReturnsExpectedSequence () + { + // Arrange: 1x1 red pixel + Color [,] pixels = new Color [1, 1]; + pixels [0, 0] = new (255, 0); + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // Build expected output + string expected = "\u001bP" // start + + "0;0;0" + + "q" + + "\"1;1;1;1" // no-scaling + width;height + + "#0;2;100;0;0" // palette + + "#0@$" // single column, single row -> code 1 -> char(1+63) = '@', then $ terminator + + "\u001b\\"; + + Assert.Equal (expected, result); + } + + [Fact] + public void EncodeSixel_WidthRepeat_UsesSequenceRepeatSyntax () + { + // Arrange: width 5, height 1, all same color so sequence repeat > 3 + int width = 5; + Color [,] pixels = new Color [width, 1]; + + for (var x = 0; x < width; x++) + { + pixels [x, 0] = new (255, 0); + } + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // Assert contains the repeat sequence for 5 identical columns: "!5" + Assert.Contains ("!5", result); + + // And final payload for the color should include the palette definition + Assert.Contains ("#0;2;100;0;0", result); + } + + [Fact] + public void EncodeSixel_HeightNotMultipleOfSix_IncludesBandSeparator () + { + // Arrange: width 2, height 7 to force two bands (6 rows + 1 row) + Color [,] pixels = new Color [2, 7]; + + for (var x = 0; x < 2; x++) + { + for (var y = 0; y < 7; y++) + { + pixels [x, y] = new (0, 0, 255); + } + } + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // Assert: there must be a band separator '-' between the bands + Assert.Contains ("-", result); + } + + [Fact] + public void EncodeSixel_AnyTransparentPixel_SetsTransparencyFlagInHeader () + { + // Arrange: 2x2 with one fully transparent pixel + Color [,] pixels = new Color [2, 2]; + pixels [0, 0] = new (255, 0); + pixels [0, 1] = new (0, 0, 0, 0); // fully transparent + pixels [1, 0] = new (0, 255); + pixels [1, 1] = new (0, 0, 255); + + var encoder = new SixelEncoder (); + + // Act + string result = encoder.EncodeSixel (pixels); + + // defaultRatios should be "0;1;0" when any pixel has alpha == 0 + Assert.Contains ("\u001bP0;1;0q", result); + } + + [Fact] + public void EncodeSixel_MaxPaletteHonored_WhenReducedMaxColors () + { + // Arrange: create three distinct colors but restrict max palette to 2 + Color [,] pixels = new Color [3, 1]; + pixels [0, 0] = new (255, 0); + pixels [1, 0] = new (0, 255); + pixels [2, 0] = new (0, 0, 255); + + var encoder = new SixelEncoder (); + encoder.Quantizer.MaxColors = 2; + + // Act + string result = encoder.EncodeSixel (pixels); + + // Assert: palette count must respect MaxColors (<= 2) and encoding must not throw + Assert.True (encoder.Quantizer.Palette.Count <= 2); + Assert.False (string.IsNullOrEmpty (result)); + } } diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs new file mode 100644 index 0000000000..869f301ad0 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -0,0 +1,94 @@ +#nullable enable +using Moq; + +namespace UnitTests_Parallelizable.DrawingTests; + +public class SixelSupportDetectorTests +{ + [Fact] + public void Detect_SetsSupportedAndResolution_WhenDeviceAttributesContain4_AndResolutionResponds() + { + // Arrange + var driverMock = new Mock(MockBehavior.Strict); + + // Expect QueueAnsiRequest to be called at least twice: + // 1) CSI_SendDeviceAttributes (terminator "c") + // 2) CSI_RequestSixelResolution (terminator "t") + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4") + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + req.ResponseReceived.Invoke ("1;4;7c"); + } + else if (req.Request == EscSeqUtils.CSI_RequestSixelResolution.Request) + { + // Reply with a resolution response matching regex "\[\d+;(\d+);(\d+)t$" + // Group 1 -> ry, Group 2 -> rx. The detector constructs resolution as new(rx, ry) + req.ResponseReceived.Invoke ("[6;20;10t"); + } + else + { + // Any other request - call abandoned to avoid hanging + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.True (final.IsSupported); // Response contained "4" + // Resolution should be constructed as new(rx, ry) where rx=10, ry=20 from our reply "[6;20;10t" + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (2)); + } + + [Fact] + public void Detect_DoesNotSetSupported_WhenDeviceAttributesDoNotContain4() + { + // Arrange + var driverMock = new Mock(MockBehavior.Strict); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + // SendDeviceAttributes -> reply without "4" + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + req.ResponseReceived.Invoke ("1;0;7c"); + } + else + { + // Any other requests should be abandoned + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.False (final.IsSupported); + // On no support, the direct resolution request path isn't followed so resolution remains the default + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs new file mode 100644 index 0000000000..d4dbdd51c4 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs @@ -0,0 +1,62 @@ +#nullable enable + +namespace UnitTests_Parallelizable.DrawingTests; + +public class SixelSupportResultTests +{ + [Fact] + public void Defaults_AreCorrect () + { + // Arrange & Act + var result = new SixelSupportResult (); + + // Assert + Assert.False (result.IsSupported); + Assert.Equal (10, result.Resolution.Width); + Assert.Equal (20, result.Resolution.Height); + Assert.Equal (256, result.MaxPaletteColors); + Assert.False (result.SupportsTransparency); + } + + [Fact] + public void Properties_CanBeModified () + { + // Arrange + var result = new SixelSupportResult (); + + // Act + result.IsSupported = true; + result.Resolution = new Size (24, 48); + result.MaxPaletteColors = 16; + result.SupportsTransparency = true; + + // Assert + Assert.True (result.IsSupported); + Assert.Equal (24, result.Resolution.Width); + Assert.Equal (48, result.Resolution.Height); + Assert.Equal (16, result.MaxPaletteColors); + Assert.True (result.SupportsTransparency); + } + + [Fact] + public void Resolution_IsValueType_CopyDoesNotAffectOriginal () + { + // Arrange + var result = new SixelSupportResult (); + Size original = result.Resolution; + + // Act + // Mutate a local copy and ensure original remains unchanged + Size copy = original; + copy.Width = 123; + copy.Height = 456; + + // Assert + Assert.Equal (10, result.Resolution.Width); + Assert.Equal (20, result.Resolution.Height); + Assert.Equal (10, original.Width); + Assert.Equal (20, original.Height); + Assert.Equal (123, copy.Width); + Assert.Equal (456, copy.Height); + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs new file mode 100644 index 0000000000..cdfb3bd63a --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs @@ -0,0 +1,215 @@ +#nullable enable +using Moq; + +namespace UnitTests_Parallelizable.DrawingTests; + +public class SixelToRenderTests +{ + [Fact] + public void SixelToRender_Properties_AreGettableAndSettable () + { + var s = new SixelToRender (); + + s.SixelData = "SIXEL-DATA"; + s.ScreenPosition = new Point (3, 5); + + Assert.Equal ("SIXEL-DATA", s.SixelData); + Assert.Equal (3, s.ScreenPosition.X); + Assert.Equal (5, s.ScreenPosition.Y); + } + + [Fact] + public void SixelSupportResult_DefaultValues_AreExpected () + { + var r = new SixelSupportResult (); + + Assert.False (r.IsSupported); + Assert.Equal (10, r.Resolution.Width); + Assert.Equal (20, r.Resolution.Height); + Assert.Equal (256, r.MaxPaletteColors); + Assert.False (r.SupportsTransparency); + } + + [Fact] + public void Detect_WhenDeviceAttributesIndicateSupport_GetsResolutionDirectly () + { + // Arrange + var driverMock = new Mock (MockBehavior.Strict); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response contains "4" -> indicates sixel support + req.ResponseReceived.Invoke ("?1;4;7c"); + } + else if (req.Request == EscSeqUtils.CSI_RequestSixelResolution.Request) + { + // Return resolution: "[6;20;10t" (group1=20 -> ry, group2=10 -> rx) + req.ResponseReceived.Invoke ("[6;20;10t"); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.True (final.IsSupported); + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (2)); + } + + [Fact] + public void Detect_WhenDirectResolutionFails_ComputesResolutionFromWindowSizes () + { + // Arrange + var driverMock = new Mock (MockBehavior.Strict); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + switch (req.Request) + { + case var r when r == EscSeqUtils.CSI_SendDeviceAttributes.Request: + // Indicate sixel support so flow continues to try resolution + req.ResponseReceived.Invoke ("?1;4;7c"); + break; + + case var r when r == EscSeqUtils.CSI_RequestSixelResolution.Request: + // Simulate failure to return resolution directly + req.Abandoned?.Invoke (); + break; + + case var r when r == EscSeqUtils.CSI_RequestWindowSizeInPixels.Request: + // Pixel dimensions reply: [4;600;1200t -> pixelHeight=600; pixelWidth=1200 + req.ResponseReceived.Invoke ("[4;600;1200t"); + break; + + case var r when r == EscSeqUtils.CSI_ReportWindowSizeInChars.Request: + // Character dimensions reply: [8;30;120t -> charHeight=30; charWidth=120 + req.ResponseReceived.Invoke ("[8;30;120t"); + break; + + default: + req.Abandoned?.Invoke (); + break; + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.True (final.IsSupported); + // Expect cell width = round(1200 / 120) = 10, cell height = round(600 / 30) = 20 + Assert.Equal (10, final.Resolution.Width); + Assert.Equal (20, final.Resolution.Height); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (3)); + } + + [Fact] + public void Detect_WhenDeviceAttributesDoNotIndicateSupport_ReturnsNotSupported () + { + // Arrange + var driverMock = new Mock (MockBehavior.Strict); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response does NOT contain "4" + req.ResponseReceived.Invoke ("?1;0;7c"); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.False (final.IsSupported); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeastOnce ()); + } + + [Fact] + public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparencyEvenIfDAReturnsNo4 () + { + // Arrange - set XTERM_VERSION env var to indicate real xterm with transparency + string? prev = Environment.GetEnvironmentVariable ("XTERM_VERSION"); + Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); + + try + { + var driverMock = new Mock (MockBehavior.Strict); + + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response does NOT contain "4" (so DAR indicates no sixel) + req.ResponseReceived.Invoke ("?1;0;7c"); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + + // DAR did not indicate sixel support + Assert.False (final.IsSupported); + + // But because XTERM_VERSION >= 370 we expect SupportsTransparency to have been initially true and remain true + Assert.True (final.SupportsTransparency); + + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeastOnce ()); + } + finally + { + // Restore environment + Environment.SetEnvironmentVariable ("XTERM_VERSION", prev); + } + } +} \ No newline at end of file From fbf56e094e8c44e2ef22aec997b617de19a92e9d Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Nov 2025 23:39:18 +0000 Subject: [PATCH 15/26] Add unit tests for OutputBase class --- .../Drivers/OutputBaseTests.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs new file mode 100644 index 0000000000..c2f0ab6368 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -0,0 +1,58 @@ +#nullable enable + +using System.Text; + +namespace UnitTests_Parallelizable.DriverTests; + +public class OutputBaseTests +{ + [Fact] + public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline () + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (1, 1); + + // Act + buffer.AddStr ("A"); + string ansi = output.ToAnsi (buffer); + + // Assert: single grapheme plus newline (BuildAnsiForRegion appends a newline per row) + Assert.Equal ("\u001b[38;2;0;0;0m\u001b[48;2;0;0;0mA" + Environment.NewLine, ansi); + } + + [Fact] + public void ToAnsi_WithAttribute_AppendsAnsiColorSequence () + { + // Arrange + var output = new TestFakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (1, 1); + + // Set an RGB attribute; TestFakeOutput will always emit RGB CSI sequences regardless of global state + buffer.CurrentAttribute = new Attribute (new Color (1, 2, 3), new Color (4, 5, 6)); + buffer.AddStr ("X"); + + // Act + string ansi = output.ToAnsi (buffer); + + // Assert: foreground RGB sequence must be included (ESC[38;2;r;g;bm) + Assert.Contains ("\u001b[38;2;1;2;3m", ansi); + // and the grapheme and newline remain present + Assert.Contains ("X" + Environment.NewLine, ansi); + } + + // Derived FakeOutput that avoids reading/modifying any static Application state. + // It always emits RGB CSI sequences for attributes so tests remain instance-scoped and deterministic. + private class TestFakeOutput : FakeOutput + { + protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) + { + // Always append RGB sequences (same as non-force16 path) + EscSeqUtils.CSI_AppendForegroundColorRGB (output, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); + EscSeqUtils.CSI_AppendBackgroundColorRGB (output, attr.Background.R, attr.Background.G, attr.Background.B); + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } + } +} \ No newline at end of file From 2cd23363dfccb130a7ab7a775c51c4bf1d0ed1c0 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 30 Nov 2025 00:11:13 +0000 Subject: [PATCH 16/26] Avoid emit escape sequence --- Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index c2f0ab6368..b017afe770 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -19,7 +19,7 @@ public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline () string ansi = output.ToAnsi (buffer); // Assert: single grapheme plus newline (BuildAnsiForRegion appends a newline per row) - Assert.Equal ("\u001b[38;2;0;0;0m\u001b[48;2;0;0;0mA" + Environment.NewLine, ansi); + Assert.Contains ("A" + Environment.NewLine, ansi); } [Fact] From ca3b4ac172f61caf87a95a0a92142a5ee846b5db Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 30 Nov 2025 01:33:38 +0000 Subject: [PATCH 17/26] Fix assertion failure in UICatalog --- Terminal.Gui/App/Application.Driver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index a691447f42..70436402f8 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -39,11 +39,11 @@ public static bool Force16Colors [Obsolete ("The legacy static Application object is going away.")] public static string ForceDriver { - get => _forceDriver; + get => ApplicationImpl.Instance.ForceDriver; set { string oldValue = _forceDriver; - _forceDriver = value; + _forceDriver = ApplicationImpl.Instance.ForceDriver = value; ForceDriverChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _forceDriver)); } } From e926ddd0d74b74f5bc9804170be724f875915517 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 30 Nov 2025 01:34:40 +0000 Subject: [PATCH 18/26] Let each driver to deal with the Sixel write --- Terminal.Gui/Drivers/OutputBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 64d0bbd87a..6ca6e2c9fc 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -112,7 +112,7 @@ public virtual void Write (IOutputBuffer buffer) if (!string.IsNullOrWhiteSpace (s.SixelData)) { SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Out.Write (s.SixelData); + Write ((StringBuilder)new (s.SixelData)); } } From fc9f9b8fbd5af2ae449f424f116e820fd13b09bd Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 30 Nov 2025 12:08:26 +0000 Subject: [PATCH 19/26] When Shutdown is called by the static Application then the ApplicationImpl.ResetStateStatic should be also called --- Examples/UICatalog/UICatalog.cs | 12 ++++++++---- Terminal.Gui/App/Application.Driver.cs | 4 ++-- Terminal.Gui/App/Application.Lifecycle.cs | 8 +++++++- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 884e65fd18..fb9254bab1 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -434,8 +434,10 @@ private static void UICatalogMain (UICatalogCommandLineOptions options) // This call to Application.Shutdown brackets the Application.Init call // made by Scenario.Init() above - // TODO: Throw if shutdown was not called already - Application.Shutdown (); + if (Application.Driver is { }) + { + Application.Shutdown (); + } VerifyObjectsWereDisposed (); @@ -483,8 +485,10 @@ void ApplicationOnInitializedChanged (object? sender, EventArgs e) scenario.Dispose (); - // TODO: Throw if shutdown was not called already - Application.Shutdown (); + if (Application.Driver is { }) + { + Application.Shutdown (); + } return results; } diff --git a/Terminal.Gui/App/Application.Driver.cs b/Terminal.Gui/App/Application.Driver.cs index 70436402f8..a691447f42 100644 --- a/Terminal.Gui/App/Application.Driver.cs +++ b/Terminal.Gui/App/Application.Driver.cs @@ -39,11 +39,11 @@ public static bool Force16Colors [Obsolete ("The legacy static Application object is going away.")] public static string ForceDriver { - get => ApplicationImpl.Instance.ForceDriver; + get => _forceDriver; set { string oldValue = _forceDriver; - _forceDriver = ApplicationImpl.Instance.ForceDriver = value; + _forceDriver = value; ForceDriverChanged?.Invoke (null, new ValueChangedEventArgs (oldValue, _forceDriver)); } } diff --git a/Terminal.Gui/App/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index c3c3cdf16c..b6fa935033 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -50,7 +50,13 @@ public static int? MainThreadId /// [Obsolete ("The legacy static Application object is going away.")] - public static void Shutdown () => ApplicationImpl.Instance.Shutdown (); + public static void Shutdown () + { + ApplicationImpl.Instance.Shutdown (); + + // Use the static reset method to bypass the fence check + ApplicationImpl.ResetStateStatic (); + } /// [Obsolete ("The legacy static Application object is going away.")] From 9144e0fb5e112a97ca97f457647c9111c1dad4d4 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 30 Nov 2025 13:34:47 +0000 Subject: [PATCH 20/26] Add more OutputBase with Sixel unit tests --- Terminal.Gui/Drivers/OutputBase.cs | 11 +- .../Drivers/OutputBaseTests.cs | 163 ++++++++++++++++-- 2 files changed, 156 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index 6ca6e2c9fc..c7c037c5df 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -107,12 +107,15 @@ public virtual void Write (IOutputBuffer buffer) } } - foreach (SixelToRender s in Driver?.Sixel!) + if (Driver is { }) { - if (!string.IsNullOrWhiteSpace (s.SixelData)) + foreach (SixelToRender s in Driver?.Sixel!) { - SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - Write ((StringBuilder)new (s.SixelData)); + if (!string.IsNullOrWhiteSpace (s.SixelData)) + { + SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + Write ((StringBuilder)new (s.SixelData)); + } } } diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index b017afe770..b3715d9177 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -1,6 +1,8 @@ #nullable enable +using System.Reflection; using System.Text; +using Moq; namespace UnitTests_Parallelizable.DriverTests; @@ -22,37 +24,170 @@ public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline () Assert.Contains ("A" + Environment.NewLine, ansi); } - [Fact] - public void ToAnsi_WithAttribute_AppendsAnsiColorSequence () + [Theory] + [InlineData (true)] + [InlineData (false)] + public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnVirtualTerminal (bool isVirtualTerminal) { // Arrange - var output = new TestFakeOutput (); + var output = new TestFakeOutput (isVirtualTerminal); IOutputBuffer buffer = output.LastBuffer!; buffer.SetSize (1, 1); - // Set an RGB attribute; TestFakeOutput will always emit RGB CSI sequences regardless of global state - buffer.CurrentAttribute = new Attribute (new Color (1, 2, 3), new Color (4, 5, 6)); + // Use a known RGB color and attribute + var fg = new Color (1, 2, 3); + var bg = new Color (4, 5, 6); + buffer.CurrentAttribute = new Attribute (fg, bg); buffer.AddStr ("X"); // Act string ansi = output.ToAnsi (buffer); - // Assert: foreground RGB sequence must be included (ESC[38;2;r;g;bm) - Assert.Contains ("\u001b[38;2;1;2;3m", ansi); - // and the grapheme and newline remain present + // Assert: when true color expected, we should see the RGB CSI; otherwise we should see the 16-color CSI + if (isVirtualTerminal) + { + Assert.Contains ("\u001b[38;2;1;2;3m", ansi); + } + else + { + var expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ()); + Assert.Contains (expected16, ansi); + } + + // Grapheme and newline should always be present Assert.Contains ("X" + Environment.NewLine, ansi); } - // Derived FakeOutput that avoids reading/modifying any static Application state. - // It always emits RGB CSI sequences for attributes so tests remain instance-scoped and deterministic. + [Fact] + public void Write_WritesDirtyCellsAndClearsDirtyFlags () + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (2, 1); + + // Mark two characters as dirty by writing them into the buffer + buffer.AddStr ("AB"); + + // Sanity: ensure cells are dirty before calling Write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.True (buffer.Contents! [0, 1].IsDirty); + + // Act + output.Write (buffer); // calls OutputBase.Write via FakeOutput + + // Assert: content was written to the fake output and dirty flags cleared + Assert.Contains ("AB", output.Output); + Assert.False (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 1].IsDirty); + } + + [Fact] + public void Write_NonVirtual_UsesWriteToConsoleAndClearsDirtyFlags () + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (3, 1); + + // Write 'A' at col 0 and 'C' at col 2; leave col 1 untouched (not dirty) + buffer.Move (0, 0); + buffer.AddStr ("A"); + buffer.Move (2, 0); + buffer.AddStr ("C"); + + // Confirm some dirtiness before the write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.True (buffer.Contents! [0, 2].IsDirty); + + // Set IsVirtualTerminal = false (FakeOutput exposes this because it's in test scope) + output.IsVirtualTerminal = false; + + // Act + output.Write (buffer); + + // Assert: both characters were written (use Contains to avoid CI side-effects) + Assert.Contains ("A", output.Output); + Assert.Contains ("C", output.Output); + + // Dirty flags cleared for the written cells + Assert.False (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 2].IsDirty); + + // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column) + Assert.Equal (new Point (0, 0), output.GetCursorPosition ()); + } + + [Fact] + public void Write_EmitsSixelDataAndPositionsCursor () + { + // Arrange + var output = new FakeOutput (); + IOutputBuffer buffer = output.LastBuffer!; + buffer.SetSize (1, 1); + + // Ensure the buffer has some content so Write traverses rows + buffer.AddStr ("."); + + // Create a Sixel to render + var s = new SixelToRender + { + SixelData = "SIXEL-DATA", + ScreenPosition = new Point (4, 2) + }; + + // Mock IDriver and set Sixel list + var driverMock = new Mock (); + driverMock.SetupGet (d => d.Sixel).Returns (new List { s }); + + // Attach mock driver to output via internal property (use reflection if necessary) + var driverProp = output.GetType ().GetProperty ( + "Driver", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy + ); + Assert.NotNull (driverProp); + driverProp.SetValue (output, driverMock.Object); + + // Act + output.Write (buffer); + + // Assert: Sixel data was emitted (use Contains to avoid equality/side-effects) + Assert.Contains ("SIXEL-DATA", output.Output); + + // Cursor was moved to Sixel position + Assert.Equal (s.ScreenPosition, output.GetCursorPosition ()); + } + + /// + /// Test FakeOutput variant that lets the test opt into emitting true-color RGB CSI sequences + /// or 16-color SGR sequences for attributes without touching static state. + /// private class TestFakeOutput : FakeOutput { + private readonly bool _isVirtualTerminal; + + public TestFakeOutput (bool isVirtualTerminal) + { + _isVirtualTerminal = isVirtualTerminal; + IsVirtualTerminal = isVirtualTerminal; + } + protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - // Always append RGB sequences (same as non-force16 path) - EscSeqUtils.CSI_AppendForegroundColorRGB (output, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); - EscSeqUtils.CSI_AppendBackgroundColorRGB (output, attr.Background.R, attr.Background.G, attr.Background.B); - EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + if (_isVirtualTerminal) + { + // True color path (RGB CSI) + EscSeqUtils.CSI_AppendForegroundColorRGB (output, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); + EscSeqUtils.CSI_AppendBackgroundColorRGB (output, attr.Background.R, attr.Background.G, attr.Background.B); + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } + else + { + // 16-color SGR path (emit SGR codes for closest 16 colors) + output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); + output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } } } } \ No newline at end of file From 6a37e9f1ac38c4ce9d820d086f58a677ee8f406a Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 1 Dec 2025 01:47:39 +0000 Subject: [PATCH 21/26] Fix some issues with IsVirtualTerminal and Force16Colors with unit tests improvement --- Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs | 21 ++- Terminal.Gui/Drivers/OutputBase.cs | 33 +++- .../Drivers/OutputBaseTests.cs | 150 ++++++++++-------- 3 files changed, 129 insertions(+), 75 deletions(-) diff --git a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index bc28c74df1..48d8329a39 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -87,10 +87,21 @@ public void Dispose () /// protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - if (Application.Force16Colors) + if (Driver?.Force16Colors == true) { - output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); - output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + if (IsVirtualTerminal) + { + output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); + output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } + else + { + Write (output); + Console.ForegroundColor = (ConsoleColor)attr.Foreground.GetClosestNamedColor16 (); + Console.BackgroundColor = (ConsoleColor)attr.Background.GetClosestNamedColor16 (); + } } else { @@ -107,9 +118,9 @@ protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr.Background.G, attr.Background.B ); - } - EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } } /// diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index c7c037c5df..1b42b66c12 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -10,10 +10,24 @@ public abstract class OutputBase /// internal IDriver? Driver { get; set; } + private bool _isVirtualTerminal; + /// /// Gets or sets whether support for virtualized terminal sequences. /// - internal bool IsVirtualTerminal { get; set; } + internal bool IsVirtualTerminal + { + get => _isVirtualTerminal; + set + { + _isVirtualTerminal = value; + + if (Driver is DriverImpl driverImpl) + { + driverImpl.IsVirtualTerminal = _isVirtualTerminal; + } + } + } private CursorVisibility? _cachedCursorVisibility; @@ -59,7 +73,7 @@ public virtual void Write (IOutputBuffer buffer) { if (!buffer.Contents! [row, col].IsDirty) { - if (!IsVirtualTerminal && output.Length > 0) + if (output.Length > 0) { WriteToConsole (output, ref lastCol, row, ref outputWidth); } @@ -107,7 +121,7 @@ public virtual void Write (IOutputBuffer buffer) } } - if (Driver is { }) + if (Driver is { } && IsVirtualTerminal) { foreach (SixelToRender s in Driver?.Sixel!) { @@ -248,9 +262,16 @@ private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref { SetCursorPositionImpl (lastCol, row); - // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker - StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); - Write (processed); + if (IsVirtualTerminal) + { + // Wrap URLs with OSC 8 hyperlink sequences using the new Osc8UrlLinker + StringBuilder processed = Osc8UrlLinker.WrapOsc8 (output); + Write (processed); + } + else + { + Write (output); + } output.Clear (); lastCol += outputWidth; diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index b3715d9177..83fc2c5e9e 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -1,9 +1,5 @@ #nullable enable -using System.Reflection; -using System.Text; -using Moq; - namespace UnitTests_Parallelizable.DriverTests; public class OutputBaseTests @@ -25,12 +21,27 @@ public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline () } [Theory] - [InlineData (true)] - [InlineData (false)] - public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnVirtualTerminal (bool isVirtualTerminal) + [InlineData (true, false)] + [InlineData (true, true)] + [InlineData (false, false)] + [InlineData (false, true)] + public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnVirtualTerminal_And_Force16Colors (bool isVirtualTerminal, bool force16Colors) { // Arrange - var output = new TestFakeOutput (isVirtualTerminal); + var output = new FakeOutput { IsVirtualTerminal = isVirtualTerminal }; + + // Create DriverImpl and associate it with the FakeOutput to test Sixel output + DriverImpl driver = new ( + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new (new AnsiResponseParser ()), + new SizeMonitorImpl (output)); + + driver.Force16Colors = force16Colors; + + Assert.Equal (output.Driver, driver); + IOutputBuffer buffer = output.LastBuffer!; buffer.SetSize (1, 1); @@ -44,15 +55,21 @@ public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnVirtualTermi string ansi = output.ToAnsi (buffer); // Assert: when true color expected, we should see the RGB CSI; otherwise we should see the 16-color CSI - if (isVirtualTerminal) + if (isVirtualTerminal && !force16Colors) { Assert.Contains ("\u001b[38;2;1;2;3m", ansi); } - else + else if (isVirtualTerminal && force16Colors) { var expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ()); Assert.Contains (expected16, ansi); } + else + { + var expected16 = (ConsoleColor)fg.GetClosestNamedColor16 (); + Assert.Equal (ConsoleColor.Black, expected16); + Assert.DoesNotContain ('\u001b', ansi); + } // Grapheme and newline should always be present Assert.Contains ("X" + Environment.NewLine, ansi); @@ -82,11 +99,14 @@ public void Write_WritesDirtyCellsAndClearsDirtyFlags () Assert.False (buffer.Contents! [0, 1].IsDirty); } - [Fact] - public void Write_NonVirtual_UsesWriteToConsoleAndClearsDirtyFlags () + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags (bool isVirtualTerminal) { // Arrange - var output = new FakeOutput (); + // FakeOutput exposes this because it's in test scope + var output = new FakeOutput { IsVirtualTerminal = isVirtualTerminal }; IOutputBuffer buffer = output.LastBuffer!; buffer.SetSize (3, 1); @@ -96,17 +116,35 @@ public void Write_NonVirtual_UsesWriteToConsoleAndClearsDirtyFlags () buffer.Move (2, 0); buffer.AddStr ("C"); - // Confirm some dirtiness before the write + // Confirm some dirtiness before to write Assert.True (buffer.Contents! [0, 0].IsDirty); Assert.True (buffer.Contents! [0, 2].IsDirty); - // Set IsVirtualTerminal = false (FakeOutput exposes this because it's in test scope) - output.IsVirtualTerminal = false; - // Act output.Write (buffer); - // Assert: both characters were written (use Contains to avoid CI side-effects) + // Assert: both characters were written (use Contains to avoid CI side effects) + Assert.Contains ("A", output.Output); + Assert.Contains ("C", output.Output); + + // Dirty flags cleared for the written cells + Assert.False (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 2].IsDirty); + + // Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column) + Assert.Equal (new Point (0, 0), output.GetCursorPosition ()); + + // Now write 'X' at col 0 to verify subsequent writes also work + buffer.Move (0, 0); + buffer.AddStr ("X"); + + // Confirm dirtiness state before to write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.False (buffer.Contents! [0, 2].IsDirty); + + output.Write (buffer); + + // Assert: both characters were written (use Contains to avoid CI side effects) Assert.Contains ("A", output.Output); Assert.Contains ("C", output.Output); @@ -118,8 +156,10 @@ public void Write_NonVirtual_UsesWriteToConsoleAndClearsDirtyFlags () Assert.Equal (new Point (0, 0), output.GetCursorPosition ()); } - [Fact] - public void Write_EmitsSixelDataAndPositionsCursor () + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Write_EmitsSixelDataAndPositionsCursor (bool isVirtualTerminal) { // Arrange var output = new FakeOutput (); @@ -136,58 +176,40 @@ public void Write_EmitsSixelDataAndPositionsCursor () ScreenPosition = new Point (4, 2) }; - // Mock IDriver and set Sixel list - var driverMock = new Mock (); - driverMock.SetupGet (d => d.Sixel).Returns (new List { s }); + // Create DriverImpl and associate it with the FakeOutput to test Sixel output + DriverImpl driver = new ( + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new (new AnsiResponseParser ()), + new SizeMonitorImpl (output)); - // Attach mock driver to output via internal property (use reflection if necessary) - var driverProp = output.GetType ().GetProperty ( - "Driver", - BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy - ); - Assert.NotNull (driverProp); - driverProp.SetValue (output, driverMock.Object); + Assert.Equal (output.Driver, driver); - // Act - output.Write (buffer); + // Add the Sixel to the driver + driver.Sixel.Add (s); - // Assert: Sixel data was emitted (use Contains to avoid equality/side-effects) - Assert.Contains ("SIXEL-DATA", output.Output); + // FakeOutput exposes this because it's in test scope + output.IsVirtualTerminal = isVirtualTerminal; - // Cursor was moved to Sixel position - Assert.Equal (s.ScreenPosition, output.GetCursorPosition ()); - } - - /// - /// Test FakeOutput variant that lets the test opt into emitting true-color RGB CSI sequences - /// or 16-color SGR sequences for attributes without touching static state. - /// - private class TestFakeOutput : FakeOutput - { - private readonly bool _isVirtualTerminal; + // Act + output.Write (buffer); - public TestFakeOutput (bool isVirtualTerminal) + if (isVirtualTerminal) { - _isVirtualTerminal = isVirtualTerminal; - IsVirtualTerminal = isVirtualTerminal; - } + // Assert: Sixel data was emitted (use Contains to avoid equality/side-effects) + Assert.Contains ("SIXEL-DATA", output.Output); - protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) + // Cursor was moved to Sixel position + Assert.Equal (s.ScreenPosition, output.GetCursorPosition ()); + } + else { - if (_isVirtualTerminal) - { - // True color path (RGB CSI) - EscSeqUtils.CSI_AppendForegroundColorRGB (output, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); - EscSeqUtils.CSI_AppendBackgroundColorRGB (output, attr.Background.R, attr.Background.G, attr.Background.B); - EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); - } - else - { - // 16-color SGR path (emit SGR codes for closest 16 colors) - output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); - output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); - EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); - } + // Assert: Sixel data was NOT emitted + Assert.DoesNotContain ("SIXEL-DATA", output.Output); + + // Cursor was NOT moved to Sixel position + Assert.NotEqual (s.ScreenPosition, output.GetCursorPosition ()); } } } \ No newline at end of file From c50c8b89ccafb2797ca692df4526c71672d10589 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 1 Dec 2025 04:21:16 +0000 Subject: [PATCH 22/26] Add Sixel Detect method unit test --- .../Sixel/SixelSupportDetectorTests.cs | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs index 869f301ad0..79cb18ab65 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -91,4 +91,36 @@ public void Detect_DoesNotSetSupported_WhenDeviceAttributesDoNotContain4() driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); } -} \ No newline at end of file + + [Fact] + public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrue () + { + // Arrange + var abandoned = false; + var driverMock = new Mock (MockBehavior.Strict); + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + // Abandon all requests + req.Abandoned?.Invoke (); + abandoned = true; + }) + .Verifiable (); + var detector = new SixelSupportDetector (driverMock.Object); + // Mock IsVirtualTerminal to return true + var isVirtualTerminalMethod = typeof (SixelSupportDetector).GetMethod ("IsVirtualTerminal", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + Assert.NotNull (isVirtualTerminalMethod); + isVirtualTerminalMethod!.Invoke (detector, null); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + // Not a real VT, so should be supported + Assert.False (final.IsSupported); + Assert.True (abandoned); + driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); + } +} From 62ef64d2e616a631d8d5ee9dee803ba3a9c9c7cb Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 2 Dec 2025 14:59:43 +0000 Subject: [PATCH 23/26] Make Sixel IsSupported and SupportsTransparency consistent with more unit tests --- .../Drawing/Sixel/SixelSupportDetector.cs | 2 +- Terminal.Gui/Drivers/DriverImpl.cs | 2 +- .../Sixel/SixelSupportDetectorTests.cs | 221 ++++++++++++++++-- .../Drawing/Sixel/SixelToRenderTests.cs | 41 +++- 4 files changed, 243 insertions(+), 23 deletions(-) diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index a393dd0acb..50f48abf5b 100644 --- a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs +++ b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs @@ -33,7 +33,7 @@ public SixelSupportDetector (IDriver? driver) : this () public void Detect (Action resultCallback) { var result = new SixelSupportResult (); - result.SupportsTransparency = result.IsSupported = IsVirtualTerminal () || IsXtermWithTransparency (); + result.SupportsTransparency = IsVirtualTerminal () && IsXtermWithTransparency (); IsSixelSupportedByDar (result, resultCallback); } diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index d51a39c28a..15d82702da 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -405,7 +405,7 @@ public Attribute SetAttribute (Attribute newAttribute) public void EnqueueKeyEvent (Key key) { InputProcessor.EnqueueKeyDownEvent (key); } /// - public void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); } + public virtual void QueueAnsiRequest (AnsiEscapeSequenceRequest request) { _ansiRequestScheduler.SendOrSchedule (this, request); } /// public AnsiRequestScheduler GetRequestScheduler () => _ansiRequestScheduler; diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs index 79cb18ab65..962d49c6a4 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -92,35 +92,228 @@ public void Detect_DoesNotSetSupported_WhenDeviceAttributesDoNotContain4() driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); } - [Fact] - public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrue () + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueAndResponseContain4OrFalse (bool isVirtualTerminal) { // Arrange - var abandoned = false; - var driverMock = new Mock (MockBehavior.Strict); + var responseReceived = false; + var output = new FakeOutput (); + output.IsVirtualTerminal = isVirtualTerminal; + + Mock driverMock = new ( + MockBehavior.Strict, + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) .Callback (req => { - // Abandon all requests - req.Abandoned?.Invoke (); - abandoned = true; + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + responseReceived = true; + + if (isVirtualTerminal) + { + // Response does contain "4" (so DAR indicates has sixel) + req.ResponseReceived.Invoke ("?1;4;0;7c"); + } + else + { + // Response does NOT contain "4" (so DAR indicates no sixel) + req.ResponseReceived.Invoke (""); + } + } + else + { + // Abandon all requests + req.Abandoned?.Invoke (); + } }) .Verifiable (); + var detector = new SixelSupportDetector (driverMock.Object); - // Mock IsVirtualTerminal to return true - var isVirtualTerminalMethod = typeof (SixelSupportDetector).GetMethod ("IsVirtualTerminal", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - Assert.NotNull (isVirtualTerminalMethod); - isVirtualTerminalMethod!.Invoke (detector, null); SixelSupportResult? final = null; // Act detector.Detect (r => final = r); // Assert + Assert.Equal (isVirtualTerminal, driverMock.Object.IsVirtualTerminal); Assert.NotNull (final); - // Not a real VT, so should be supported - Assert.False (final.IsSupported); - Assert.True (abandoned); + + if (isVirtualTerminal) + { + Assert.True (final.IsSupported); + } + else + { + // Not a real VT, so should be supported + Assert.False (final.IsSupported); + } + Assert.True (responseReceived); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueAndResponseContain4OrFalse_WithoutMock (bool isVirtualTerminal) + { + // Arrange + var responseReceived = false; + var output = new FakeOutput (); + output.IsVirtualTerminal = isVirtualTerminal; + + DriverImplProxy driver = new ( + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); + + driver.Callback += req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + responseReceived = true; + + if (isVirtualTerminal) + { + // Response does contain "4" (so DAR indicates has sixel) + req.ResponseReceived.Invoke ("?1;4;0;7c"); + } + else + { + // Response does NOT contain "4" (so DAR indicates no sixel) + req.ResponseReceived.Invoke (""); + } + + return true; + } + + // Abandon all requests + req.Abandoned?.Invoke (); + + return false; + }; + + var detector = new SixelSupportDetector (driver); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.Equal (isVirtualTerminal, driver.IsVirtualTerminal); + Assert.NotNull (final); + + if (isVirtualTerminal) + { + Assert.True (final.IsSupported); + Assert.False (final.SupportsTransparency); + } + else + { + // Not a real VT, so shouldn't be supported + Assert.False (final.IsSupported); + Assert.False (final.SupportsTransparency); + } + Assert.True (responseReceived); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueOrFalse_With_Response (bool isVirtualTerminal) + { + // Arrange + var responseReceived = false; + var output = new FakeOutput (); + output.IsVirtualTerminal = isVirtualTerminal; + + DriverImplProxy driver = new ( + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); + + driver.Callback += req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + responseReceived = true; + + // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4") + // Respond to the SendDeviceAttributes request with an empty value that indicates non-support + req.ResponseReceived.Invoke (driver.IsVirtualTerminal ? "1;4;7c" : ""); + } + + // Abandon all requests + req.Abandoned?.Invoke (); + + return true; + }; + + var detector = new SixelSupportDetector (driver); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.Equal (isVirtualTerminal, driver.IsVirtualTerminal); + Assert.NotNull (final); + + if (isVirtualTerminal) + { + Assert.True (final.IsSupported); + Assert.False (final.SupportsTransparency); + } + else + { + // Not a real VT, so shouldn't be supported + Assert.False (final.IsSupported); + Assert.False (final.SupportsTransparency); + } + + Assert.True (responseReceived); + } +} + +/// +internal class DriverImplProxy : DriverImpl +{ + /// + public DriverImplProxy (IInputProcessor inputProcessor, + IOutputBuffer outputBuffer, + IOutput output, + AnsiRequestScheduler ansiRequestScheduler, + ISizeMonitor sizeMonitor) : base (inputProcessor, outputBuffer, output, ansiRequestScheduler, sizeMonitor) + { } + + public Func? Callback { get; set; } + + /// + public override void QueueAnsiRequest (AnsiEscapeSequenceRequest request) + { + if (Callback is null) + { + throw new NullReferenceException ($"{nameof (Callback)} cannot be null."); + } + + if (!Callback (request)) + { + base.QueueAnsiRequest (request); + } + + Callback = null; + } } diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs index cdfb3bd63a..72a49c1565 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs @@ -162,16 +162,42 @@ public void Detect_WhenDeviceAttributesDoNotIndicateSupport_ReturnsNotSupported driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeastOnce ()); } - [Fact] - public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparencyEvenIfDAReturnsNo4 () + [Theory] + [InlineData ("", false, false, false, false)] + [InlineData ("", false, true, false, false)] + [InlineData ("?1;0;7c", true, false, false, false)] + [InlineData ("?1;0;7c", true, true, false, true)] + [InlineData ("?1;4;0;7c", true, false, true, false)] + [InlineData ("?1;4;0;7c", true, true, true, true)] + public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparencyEvenIfDAReturnsNo4 ( + string darResponse, + bool isVirtualTerminal, + bool isXtermWithTransparency, + bool expectedIsSupported, + bool expectedSupportsTransparency + ) { // Arrange - set XTERM_VERSION env var to indicate real xterm with transparency string? prev = Environment.GetEnvironmentVariable ("XTERM_VERSION"); - Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); + + if (isXtermWithTransparency) + { + Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); + } try { - var driverMock = new Mock (MockBehavior.Strict); + var output = new FakeOutput (); + output.IsVirtualTerminal = isVirtualTerminal; + + var driverMock = new Mock ( + MockBehavior.Strict, + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) .Callback (req => @@ -179,7 +205,7 @@ public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparenc if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) { // Response does NOT contain "4" (so DAR indicates no sixel) - req.ResponseReceived.Invoke ("?1;0;7c"); + req.ResponseReceived.Invoke (darResponse); } else { @@ -197,12 +223,13 @@ public void Detect_WhenXtermEnvironmentIndicatesTransparency_SupportsTransparenc // Assert Assert.NotNull (final); + Assert.Equal (isVirtualTerminal, driverMock.Object.IsVirtualTerminal); // DAR did not indicate sixel support - Assert.False (final.IsSupported); + Assert.Equal (expectedIsSupported, final.IsSupported); // But because XTERM_VERSION >= 370 we expect SupportsTransparency to have been initially true and remain true - Assert.True (final.SupportsTransparency); + Assert.Equal (expectedSupportsTransparency, final.SupportsTransparency); driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeastOnce ()); } From bc17291c07aae70e1ed39136318464dd50744106 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 2 Dec 2025 20:18:54 +0000 Subject: [PATCH 24/26] Fix namespaces and unit test --- .../Drawing/Sixel/SixelSupportDetectorTests.cs | 2 +- .../Drawing/Sixel/SixelSupportResultTests.cs | 2 +- .../Drawing/Sixel/SixelToRenderTests.cs | 12 ++++++------ .../Drivers/OutputBaseTests.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs index 962d49c6a4..f703bcaaef 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -1,7 +1,7 @@ #nullable enable using Moq; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SixelSupportDetectorTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs index d4dbdd51c4..6127bff2a3 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SixelSupportResultTests { diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs index 72a49c1565..617c8aed72 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs @@ -1,7 +1,7 @@ #nullable enable using Moq; -namespace UnitTests_Parallelizable.DrawingTests; +namespace DrawingTests; public class SixelToRenderTests { @@ -180,11 +180,6 @@ bool expectedSupportsTransparency // Arrange - set XTERM_VERSION env var to indicate real xterm with transparency string? prev = Environment.GetEnvironmentVariable ("XTERM_VERSION"); - if (isXtermWithTransparency) - { - Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); - } - try { var output = new FakeOutput (); @@ -218,6 +213,11 @@ bool expectedSupportsTransparency SixelSupportResult? final = null; + if (isXtermWithTransparency) + { + Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); + } + // Act detector.Detect (r => final = r); diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index 83fc2c5e9e..fc895e5672 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -1,6 +1,6 @@ #nullable enable -namespace UnitTests_Parallelizable.DriverTests; +namespace DriverTests; public class OutputBaseTests { From ff69a2db3be1769a1bdaf27b161e61cf6f7eb309 Mon Sep 17 00:00:00 2001 From: BDisp Date: Tue, 2 Dec 2025 21:41:01 +0000 Subject: [PATCH 25/26] Covering more ApplicationImpl Sixel unit test --- Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index fc895e5672..5abbc76c77 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -211,5 +211,12 @@ public void Write_EmitsSixelDataAndPositionsCursor (bool isVirtualTerminal) // Cursor was NOT moved to Sixel position Assert.NotEqual (s.ScreenPosition, output.GetCursorPosition ()); } + + IApplication app = Application.Create (); + app.Driver = driver; + + Assert.Equal (driver.Sixel, app.Driver.Sixel); + + app.Dispose (); } } \ No newline at end of file From 4f18becd3dce03e7e6e2d1c645657e2653d6602d Mon Sep 17 00:00:00 2001 From: BDisp Date: Wed, 3 Dec 2025 00:16:50 +0000 Subject: [PATCH 26/26] Remove DriverImplProxy because sometimes fails in parallel unit tests --- .../Sixel/SixelSupportDetectorTests.cs | 145 +++--------------- 1 file changed, 24 insertions(+), 121 deletions(-) diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs index f703bcaaef..05b4f384e8 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -159,74 +159,6 @@ public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueAndResponseContain4O driverMock.Verify (d => d.QueueAnsiRequest (It.IsAny ()), Times.AtLeast (1)); } - [Theory] - [InlineData (true)] - [InlineData (false)] - public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueAndResponseContain4OrFalse_WithoutMock (bool isVirtualTerminal) - { - // Arrange - var responseReceived = false; - var output = new FakeOutput (); - output.IsVirtualTerminal = isVirtualTerminal; - - DriverImplProxy driver = new ( - new FakeInputProcessor (null!), - new OutputBufferImpl (), - output, - new (new AnsiResponseParser ()), - new SizeMonitorImpl (output) - ); - - driver.Callback += req => - { - if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) - { - responseReceived = true; - - if (isVirtualTerminal) - { - // Response does contain "4" (so DAR indicates has sixel) - req.ResponseReceived.Invoke ("?1;4;0;7c"); - } - else - { - // Response does NOT contain "4" (so DAR indicates no sixel) - req.ResponseReceived.Invoke (""); - } - - return true; - } - - // Abandon all requests - req.Abandoned?.Invoke (); - - return false; - }; - - var detector = new SixelSupportDetector (driver); - SixelSupportResult? final = null; - - // Act - detector.Detect (r => final = r); - - // Assert - Assert.Equal (isVirtualTerminal, driver.IsVirtualTerminal); - Assert.NotNull (final); - - if (isVirtualTerminal) - { - Assert.True (final.IsSupported); - Assert.False (final.SupportsTransparency); - } - else - { - // Not a real VT, so shouldn't be supported - Assert.False (final.IsSupported); - Assert.False (final.SupportsTransparency); - } - Assert.True (responseReceived); - } - [Theory] [InlineData (true)] [InlineData (false)] @@ -237,39 +169,40 @@ public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueOrFalse_With_Respons var output = new FakeOutput (); output.IsVirtualTerminal = isVirtualTerminal; - DriverImplProxy driver = new ( - new FakeInputProcessor (null!), - new OutputBufferImpl (), - output, - new (new AnsiResponseParser ()), - new SizeMonitorImpl (output) - ); - - driver.Callback += req => - { - if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) - { - responseReceived = true; + Mock driverMock = new ( + MockBehavior.Strict, + new FakeInputProcessor (null!), + new OutputBufferImpl (), + output, + new AnsiRequestScheduler (new AnsiResponseParser ()), + new SizeMonitorImpl (output) + ); - // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4") - // Respond to the SendDeviceAttributes request with an empty value that indicates non-support - req.ResponseReceived.Invoke (driver.IsVirtualTerminal ? "1;4;7c" : ""); - } + driverMock.Setup (d => d.QueueAnsiRequest (It.IsAny ())) + .Callback (req => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + responseReceived = true; - // Abandon all requests - req.Abandoned?.Invoke (); + // Respond to the SendDeviceAttributes request with a value that indicates support (contains "4") + // Respond to the SendDeviceAttributes request with an empty value that indicates non-support + req.ResponseReceived.Invoke (driverMock.Object.IsVirtualTerminal ? "1;4;7c" : ""); + } - return true; - }; + // Abandon all requests + req.Abandoned?.Invoke (); + }) + .Verifiable (); - var detector = new SixelSupportDetector (driver); + var detector = new SixelSupportDetector (driverMock.Object); SixelSupportResult? final = null; // Act detector.Detect (r => final = r); // Assert - Assert.Equal (isVirtualTerminal, driver.IsVirtualTerminal); + Assert.Equal (isVirtualTerminal, driverMock.Object.IsVirtualTerminal); Assert.NotNull (final); if (isVirtualTerminal) @@ -287,33 +220,3 @@ public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueOrFalse_With_Respons Assert.True (responseReceived); } } - -/// -internal class DriverImplProxy : DriverImpl -{ - /// - public DriverImplProxy (IInputProcessor inputProcessor, - IOutputBuffer outputBuffer, - IOutput output, - AnsiRequestScheduler ansiRequestScheduler, - ISizeMonitor sizeMonitor) : base (inputProcessor, outputBuffer, output, ansiRequestScheduler, sizeMonitor) - { } - - public Func? Callback { get; set; } - - /// - public override void QueueAnsiRequest (AnsiEscapeSequenceRequest request) - { - if (Callback is null) - { - throw new NullReferenceException ($"{nameof (Callback)} cannot be null."); - } - - if (!Callback (request)) - { - base.QueueAnsiRequest (request); - } - - Callback = null; - } -}