diff --git a/Examples/UICatalog/Scenarios/Images.cs b/Examples/UICatalog/Scenarios/Images.cs index 97c6126747..861dae4d57 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) diff --git a/Examples/UICatalog/UICatalog.cs b/Examples/UICatalog/UICatalog.cs index 21634ac0b5..19fdcfd794 100644 --- a/Examples/UICatalog/UICatalog.cs +++ b/Examples/UICatalog/UICatalog.cs @@ -433,8 +433,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 (); @@ -482,8 +484,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 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/Application.Lifecycle.cs b/Terminal.Gui/App/Application.Lifecycle.cs index 9fbc9fba15..64f60c704a 100644 --- a/Terminal.Gui/App/Application.Lifecycle.cs +++ b/Terminal.Gui/App/Application.Lifecycle.cs @@ -67,7 +67,13 @@ public static int? MainThreadId /// [Obsolete ("The legacy static Application object is going away.")] - public static void Shutdown () => ApplicationImpl.Instance.Dispose (); + public static void Shutdown () + { + ApplicationImpl.Instance.Dispose (); + + // Use the static reset method to bypass the fence check + ApplicationImpl.ResetStateStatic (); + } /// [Obsolete ("The legacy static Application object is going away.")] diff --git a/Terminal.Gui/App/ApplicationImpl.Driver.cs b/Terminal.Gui/App/ApplicationImpl.Driver.cs index 11fabb91a7..5c14ec2993 100644 --- a/Terminal.Gui/App/ApplicationImpl.Driver.cs +++ b/Terminal.Gui/App/ApplicationImpl.Driver.cs @@ -13,9 +13,6 @@ internal 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 acdd2a0cf1..8bc82e07a6 100644 --- a/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs +++ b/Terminal.Gui/App/ApplicationImpl.Lifecycle.cs @@ -310,17 +310,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, @@ -329,7 +326,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 4d0959a2f8..7f4158afc4 100644 --- a/Terminal.Gui/App/IApplication.cs +++ b/Terminal.Gui/App/IApplication.cs @@ -497,12 +497,6 @@ public IApplication Run (Func? errorHandler = null, /// 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 Keyboard diff --git a/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs b/Terminal.Gui/Drawing/Sixel/SixelSupportDetector.cs index a9e1ae8aa0..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 = IsVirtualTerminal () || IsXtermWithTransparency (); + result.SupportsTransparency = IsVirtualTerminal () && IsXtermWithTransparency (); IsSixelSupportedByDar (result, resultCallback); } @@ -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 DriverImpl)?.IsVirtualTerminal == true; } private static bool IsXtermWithTransparency () diff --git a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs index 4f8ab1fc09..5f1f965f7f 100644 --- a/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs +++ b/Terminal.Gui/Drivers/DotNetDriver/NetOutput.cs @@ -30,6 +30,8 @@ public NetOutput () { _isWinPlatform = true; } + + IsVirtualTerminal = true; } /// diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 9aebea3dd8..15d82702da 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -72,6 +72,31 @@ ISizeMonitor sizeMonitor }; CreateClipboard (); + + if (_output is OutputBase outputBase) + { + IsVirtualTerminal = outputBase.IsVirtualTerminal; + 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; + } + } } /// @@ -199,16 +224,34 @@ public int Top /// - 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; + } } + /// + public List Sixel { get; } = []; + /// public Attribute CurrentAttribute @@ -362,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/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs index 8fd790f197..48d8329a39 100644 --- a/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs +++ b/Terminal.Gui/Drivers/FakeDriver/FakeOutput.cs @@ -19,6 +19,7 @@ public FakeOutput () { LastBuffer = new OutputBufferImpl (); LastBuffer.SetSize (80, 25); + IsVirtualTerminal = true; } /// @@ -86,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 { @@ -106,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/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/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs index ad1f4120e5..1b42b66c12 100644 --- a/Terminal.Gui/Drivers/OutputBase.cs +++ b/Terminal.Gui/Drivers/OutputBase.cs @@ -5,6 +5,30 @@ namespace Terminal.Gui.Drivers; /// public abstract class OutputBase { + /// + /// Get or sets the instance associated with this output. + /// + internal IDriver? Driver { get; set; } + + private bool _isVirtualTerminal; + + /// + /// Gets or sets whether support for virtualized terminal sequences. + /// + internal bool IsVirtualTerminal + { + get => _isVirtualTerminal; + set + { + _isVirtualTerminal = value; + + if (Driver is DriverImpl driverImpl) + { + driverImpl.IsVirtualTerminal = _isVirtualTerminal; + } + } + } + private CursorVisibility? _cachedCursorVisibility; // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). @@ -82,24 +106,32 @@ 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); + } } } - // 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); - // } - //} + if (Driver is { } && IsVirtualTerminal) + { + foreach (SixelToRender s in Driver?.Sixel!) + { + if (!string.IsNullOrWhiteSpace (s.SixelData)) + { + SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + Write ((StringBuilder)new (s.SixelData)); + } + } + } SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); _cachedCursorVisibility = savedVisibility; @@ -230,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/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs index dfbf63ead4..46faaeb307 100644 --- a/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs +++ b/Terminal.Gui/Drivers/UnixDriver/UnixOutput.cs @@ -36,10 +36,15 @@ private struct WinSize [DllImport ("libc", SetLastError = true)] private static extern int dup (int fd); + 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 b351696a20..4f81e3e9e8 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsOutput.cs @@ -97,7 +97,6 @@ [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; @@ -113,9 +112,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,12 +144,6 @@ public WindowsOutput () { throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); } - - // 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; - } GetSize (); @@ -189,7 +182,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."); } @@ -220,19 +213,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 ()); } @@ -252,11 +245,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 ()); } @@ -264,18 +257,15 @@ 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 = 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; } @@ -289,37 +279,35 @@ public override void Write (IOutputBuffer outputBuffer) _consoleBuffer = _outputHandle; } - base.Write (outputBuffer); - try { - if (_force16Colors && !_isVirtualTerminal) - { - SetConsoleActiveScreenBuffer (_consoleBuffer); - } - else + base.Write (outputBuffer); + + ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + + bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); + + if (!result) { - ReadOnlySpan span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + int err = Marshal.GetLastWin32Error (); + + if (err == 1) + { + Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); - bool result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); + return; + } - if (!result) + if (err != 0) { - int err = Marshal.GetLastWin32Error (); - - if (err == 1) - { - Logging.Logger.LogError ($"Error: {Marshal.GetLastWin32Error ()} in {nameof (WindowsOutput)}"); - - return; - } - if (err != 0) - { - throw new Win32Exception (err); - } + throw new Win32Exception (err); } } } + 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)}"); @@ -341,7 +329,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); @@ -355,14 +343,9 @@ 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) + if (IsVirtualTerminal) { output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); @@ -370,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); } @@ -438,7 +423,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); @@ -468,7 +453,7 @@ private Size GetLargestConsoleWindowSize () try { - maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + maxWinSize = GetLargestConsoleWindowSize (IsVirtualTerminal ? _outputHandle : _screenBuffer); } catch { @@ -481,7 +466,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)); } @@ -505,7 +490,7 @@ public override void SetCursorVisibility (CursorVisibility visibility) return; } - if (!_isVirtualTerminal) + if (!IsVirtualTerminal) { var info = new WindowsConsole.ConsoleCursorInfo { @@ -539,7 +524,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); @@ -570,7 +555,7 @@ public void Dispose () return; } - if (_isVirtualTerminal) + if (IsVirtualTerminal) { if (Environment.GetEnvironmentVariable ("VSAPPIDNAME") is null) { diff --git a/Tests/UnitTests/Application/SynchronizatonContextTests.cs b/Tests/UnitTests/Application/SynchronizatonContextTests.cs index 39fee532fb..019c9ba6b6 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) 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/Drawing/CellTests.cs b/Tests/UnitTestsParallelizable/Drawing/CellTests.cs index f6da2e8524..9383e45926 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 void ToString_Override (string text, Attribute? attribute, string expecte 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/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 3a1ed881ab..bf48046c2e 100644 --- a/Tests/UnitTestsParallelizable/Drawing/SixelEncoderTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelEncoderTests.cs @@ -37,7 +37,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 +48,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 +124,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 +142,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 +213,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 +229,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..05b4f384e8 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportDetectorTests.cs @@ -0,0 +1,222 @@ +#nullable enable +using Moq; + +namespace 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)); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Detect_SetsSupported_WhenIsVirtualTerminalIsTrueAndResponseContain4OrFalse (bool isVirtualTerminal) + { + // Arrange + 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 => + { + 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); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.Equal (isVirtualTerminal, driverMock.Object.IsVirtualTerminal); + Assert.NotNull (final); + + 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_WhenIsVirtualTerminalIsTrueOrFalse_With_Response (bool isVirtualTerminal) + { + // Arrange + 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 => + { + 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 (driverMock.Object.IsVirtualTerminal ? "1;4;7c" : ""); + } + + // Abandon all requests + req.Abandoned?.Invoke (); + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + SixelSupportResult? final = null; + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.Equal (isVirtualTerminal, driverMock.Object.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); + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs new file mode 100644 index 0000000000..6127bff2a3 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelSupportResultTests.cs @@ -0,0 +1,62 @@ +#nullable enable + +namespace 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..617c8aed72 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Sixel/SixelToRenderTests.cs @@ -0,0 +1,242 @@ +#nullable enable +using Moq; + +namespace 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 ()); + } + + [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"); + + try + { + 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 => + { + if (req.Request == EscSeqUtils.CSI_SendDeviceAttributes.Request) + { + // Response does NOT contain "4" (so DAR indicates no sixel) + req.ResponseReceived.Invoke (darResponse); + } + else + { + req.Abandoned?.Invoke (); + } + }) + .Verifiable (); + + var detector = new SixelSupportDetector (driverMock.Object); + + SixelSupportResult? final = null; + + if (isXtermWithTransparency) + { + Environment.SetEnvironmentVariable ("XTERM_VERSION", "370"); + } + + // Act + detector.Detect (r => final = r); + + // Assert + Assert.NotNull (final); + Assert.Equal (isVirtualTerminal, driverMock.Object.IsVirtualTerminal); + + // DAR did not indicate sixel support + Assert.Equal (expectedIsSupported, final.IsSupported); + + // But because XTERM_VERSION >= 370 we expect SupportsTransparency to have been initially true and remain true + Assert.Equal (expectedSupportsTransparency, 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 diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 928dd923b2..3d56b9ec74 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.Dispose (); } + + [Fact] + public void IsVirtualTerminal_Returns_Expected_Values () + { + DriverImpl? driver = CreateFakeDriver () as DriverImpl; + Assert.NotNull (driver?.IsVirtualTerminal); + Assert.True (driver.IsVirtualTerminal); + + driver.IsVirtualTerminal = false; + Assert.False (driver.IsVirtualTerminal); + } + + [Fact] + public void IsVirtualTerminal_True_Force16Colors_True_False () + { + DriverImpl? driver = CreateFakeDriver () as DriverImpl; + 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 () + { + DriverImpl? driver = CreateFakeDriver () as DriverImpl; + 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 () + { + DriverImpl? driver = CreateFakeDriver () as DriverImpl; + Assert.NotNull (driver?.IsVirtualTerminal); + Assert.True (driver.IsVirtualTerminal); + Assert.True (driver.SupportsTrueColor); + + driver.IsVirtualTerminal = false; + Assert.False (driver.SupportsTrueColor); + } } public class TestTop : Runnable diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs new file mode 100644 index 0000000000..5abbc76c77 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -0,0 +1,222 @@ +#nullable enable + +namespace 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.Contains ("A" + Environment.NewLine, ansi); + } + + [Theory] + [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 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); + + // 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: when true color expected, we should see the RGB CSI; otherwise we should see the 16-color CSI + if (isVirtualTerminal && !force16Colors) + { + Assert.Contains ("\u001b[38;2;1;2;3m", ansi); + } + 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); + } + + [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); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags (bool isVirtualTerminal) + { + // Arrange + // FakeOutput exposes this because it's in test scope + var output = new FakeOutput { IsVirtualTerminal = isVirtualTerminal }; + 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 to write + Assert.True (buffer.Contents! [0, 0].IsDirty); + Assert.True (buffer.Contents! [0, 2].IsDirty); + + // 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 ()); + + // 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); + + // 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 ()); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void Write_EmitsSixelDataAndPositionsCursor (bool isVirtualTerminal) + { + // 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) + }; + + // 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)); + + Assert.Equal (output.Driver, driver); + + // Add the Sixel to the driver + driver.Sixel.Add (s); + + // FakeOutput exposes this because it's in test scope + output.IsVirtualTerminal = isVirtualTerminal; + + // Act + output.Write (buffer); + + if (isVirtualTerminal) + { + // 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 ()); + } + else + { + // 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 ()); + } + + IApplication app = Application.Create (); + app.Driver = driver; + + Assert.Equal (driver.Sixel, app.Driver.Sixel); + + app.Dispose (); + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs b/Tests/UnitTestsParallelizable/Drivers/Windows/WindowsKeyConverterTests.cs index 2cad545da8..0c4e022ea3 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 '_' diff --git a/Tests/UnitTestsParallelizable/Text/StringTests.cs b/Tests/UnitTestsParallelizable/Text/StringTests.cs index 1c6e848cd5..51525992c2 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]