diff --git a/Terminal.Gui/Drivers/NetDriver/NetWinVTConsole.cs b/Terminal.Gui/Drivers/NetDriver/NetWinVTConsole.cs index 4750a32e87..39dee104cb 100644 --- a/Terminal.Gui/Drivers/NetDriver/NetWinVTConsole.cs +++ b/Terminal.Gui/Drivers/NetDriver/NetWinVTConsole.cs @@ -96,6 +96,11 @@ public NetWinVTConsole () public void Cleanup () { + if (!FlushConsoleInputBuffer (_inputHandle)) + { + throw new ApplicationException ($"Failed to flush input buffer, error code: {GetLastError ()}."); + } + if (!SetConsoleMode (_inputHandle, _originalInputConsoleMode)) { throw new ApplicationException ($"Failed to restore input console mode, error code: {GetLastError ()}."); @@ -123,4 +128,7 @@ public void Cleanup () [DllImport ("kernel32.dll")] private static extern bool SetConsoleMode (nint hConsoleHandle, uint dwMode); + + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool FlushConsoleInputBuffer (nint hConsoleInput); } diff --git a/Terminal.Gui/Drivers/V2/IOutputBuffer.cs b/Terminal.Gui/Drivers/V2/IOutputBuffer.cs index 80f33a0c09..6f51ab1ad7 100644 --- a/Terminal.Gui/Drivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/V2/IOutputBuffer.cs @@ -9,12 +9,6 @@ namespace Terminal.Gui.Drivers; /// public interface IOutputBuffer { - /// - /// As performance is a concern, we keep track of the dirty lines and only refresh those. - /// This is in addition to the dirty flag on each cell. - /// - public bool [] DirtyLines { get; } - /// /// The contents of the application output. The driver outputs this buffer to the terminal when UpdateScreen is called. /// diff --git a/Terminal.Gui/Drivers/V2/InputProcessor.cs b/Terminal.Gui/Drivers/V2/InputProcessor.cs index 607ad3a23e..c860ba796e 100644 --- a/Terminal.Gui/Drivers/V2/InputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/InputProcessor.cs @@ -162,4 +162,43 @@ private IEnumerable ReleaseParserHeldKeysIfStale () /// /// protected abstract void ProcessAfterParsing (T input); + + internal char _highSurrogate = '\0'; + + internal bool IsValidInput (Key key, out Key result) + { + result = key; + + if (char.IsHighSurrogate ((char)key)) + { + _highSurrogate = (char)key; + + return false; + } + + if (_highSurrogate > 0 && char.IsLowSurrogate ((char)key)) + { + result = (KeyCode)new Rune (_highSurrogate, (char)key).Value; + _highSurrogate = '\0'; + + return true; + } + + if (char.IsSurrogate ((char)key)) + { + return false; + } + + if (_highSurrogate > 0) + { + _highSurrogate = '\0'; + } + + if (key.KeyCode == 0) + { + return false; + } + + return true; + } } diff --git a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs index a2cc34c497..a70b089567 100644 --- a/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs +++ b/Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs @@ -25,7 +25,6 @@ internal class MainLoopCoordinator : IMainLoopCoordinator private ConsoleDriverFacade _facade; private Task _inputTask; private readonly ITimedEvents _timedEvents; - private readonly bool _isWindowsTerminal; private readonly SemaphoreSlim _startupSemaphore = new (0, 1); @@ -61,7 +60,6 @@ IMainLoop loop _inputProcessor = inputProcessor; _outputFactory = outputFactory; _loop = loop; - _isWindowsTerminal = Environment.GetEnvironmentVariable ("WT_SESSION") is { } || Environment.GetEnvironmentVariable ("VSAPPIDNAME") != null; } /// @@ -162,11 +160,6 @@ private void BuildFacadeIfPossible () _loop.AnsiRequestScheduler, _loop.WindowSizeMonitor); - if (!_isWindowsTerminal) - { - Application.Force16Colors = _facade.Force16Colors = true; - } - Application.Driver = _facade; _startupSemaphore.Release (); diff --git a/Terminal.Gui/Drivers/V2/NetInput.cs b/Terminal.Gui/Drivers/V2/NetInput.cs index 518c77e557..10b5e1cf65 100644 --- a/Terminal.Gui/Drivers/V2/NetInput.cs +++ b/Terminal.Gui/Drivers/V2/NetInput.cs @@ -40,6 +40,12 @@ public NetInput () } } + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + + //Set cursor key to application. + Console.Out.Write (EscSeqUtils.CSI_HideCursor); + Console.Out.Write (EscSeqUtils.CSI_EnableMouseEvents); Console.TreatControlCAsInput = true; } @@ -68,8 +74,14 @@ protected override IEnumerable Read () public override void Dispose () { base.Dispose (); - _adjustConsole?.Cleanup (); - Console.Out.Write (EscSeqUtils.CSI_DisableMouseEvents); + + //Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + + //Set cursor key to cursor. + Console.Out.Write (EscSeqUtils.CSI_ShowCursor); + + _adjustConsole?.Cleanup (); } } diff --git a/Terminal.Gui/Drivers/V2/NetInputProcessor.cs b/Terminal.Gui/Drivers/V2/NetInputProcessor.cs index 4e11d402a4..36c885e355 100644 --- a/Terminal.Gui/Drivers/V2/NetInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/NetInputProcessor.cs @@ -41,8 +41,13 @@ protected override void Process (ConsoleKeyInfo consoleKeyInfo) protected override void ProcessAfterParsing (ConsoleKeyInfo input) { var key = KeyConverter.ToKey (input); - OnKeyDown (key); - OnKeyUp (key); + + // If the key is not valid, we don't want to raise any events. + if (IsValidInput (key, out key)) + { + OnKeyDown (key); + OnKeyUp (key); + } } /* For building test cases */ diff --git a/Terminal.Gui/Drivers/V2/NetOutput.cs b/Terminal.Gui/Drivers/V2/NetOutput.cs index 32ae497645..17956a3dfa 100644 --- a/Terminal.Gui/Drivers/V2/NetOutput.cs +++ b/Terminal.Gui/Drivers/V2/NetOutput.cs @@ -6,15 +6,10 @@ namespace Terminal.Gui.Drivers; /// Implementation of that uses native dotnet /// methods e.g. /// -public class NetOutput : IConsoleOutput +public class NetOutput : OutputBase, IConsoleOutput { private readonly bool _isWinPlatform; - private CursorVisibility? _cachedCursorVisibility; - - // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). - private TextStyle _redrawTextStyle = TextStyle.None; - /// /// Creates a new instance of the class. /// @@ -30,176 +25,10 @@ public NetOutput () { _isWinPlatform = true; } - - //Enable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); - - //Set cursor key to application. - Console.Out.Write (EscSeqUtils.CSI_HideCursor); } /// - public void Write (ReadOnlySpan text) - { - Console.Out.Write (text); - } - - /// - public void Write (IOutputBuffer buffer) - { - if (ConsoleDriver.RunningUnitTests) - { - return; - } - - if (Console.WindowHeight < 1 - || buffer.Contents.Length != buffer.Rows * buffer.Cols - || buffer.Rows != Console.WindowHeight) - { - // return; - } - - var top = 0; - var left = 0; - int rows = buffer.Rows; - int cols = buffer.Cols; - var output = new StringBuilder (); - Attribute? redrawAttr = null; - int lastCol = -1; - - CursorVisibility? savedVisibility = _cachedCursorVisibility; - SetCursorVisibility (CursorVisibility.Invisible); - - const int maxCharsPerRune = 2; - Span runeBuffer = stackalloc char[maxCharsPerRune]; - - for (int row = top; row < rows; row++) - { - if (Console.WindowHeight < 1) - { - return; - } - - if (!buffer.DirtyLines [row]) - { - continue; - } - - if (!SetCursorPositionImpl (0, row)) - { - return; - } - - buffer.DirtyLines [row] = false; - output.Clear (); - - for (int col = left; col < cols; col++) - { - lastCol = -1; - var outputWidth = 0; - - for (; col < cols; col++) - { - if (!buffer.Contents [row, col].IsDirty) - { - if (output.Length > 0) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - } - else if (lastCol == -1) - { - lastCol = col; - } - - if (lastCol + 1 < cols) - { - lastCol++; - } - - continue; - } - - if (lastCol == -1) - { - lastCol = col; - } - - Attribute attr = buffer.Contents [row, col].Attribute.Value; - - // Performance: Only send the escape sequence if the attribute has changed. - if (attr != redrawAttr) - { - redrawAttr = attr; - - 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); - - _redrawTextStyle = attr.Style; - } - - outputWidth++; - - // Avoid Rune.ToString() by appending the rune chars. - Rune rune = buffer.Contents [row, col].Rune; - int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer); - ReadOnlySpan runeChars = runeBuffer[..runeCharsWritten]; - output.Append (runeChars); - - if (buffer.Contents [row, col].CombiningMarks.Count > 0) - { - // AtlasEngine does not support NON-NORMALIZED combining marks in a way - // compatible with the driver architecture. Any CMs (except in the first col) - // are correctly combined with the base char, but are ALSO treated as 1 column - // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. - // - // For now, we just ignore the list of CMs. - //foreach (var combMark in Contents [row, col].CombiningMarks) { - // output.Append (combMark); - //} - // WriteToConsole (output, ref lastCol, row, ref outputWidth); - } - else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) - { - WriteToConsole (output, ref lastCol, row, ref outputWidth); - SetCursorPositionImpl (col - 1, row); - } - - buffer.Contents [row, col].IsDirty = false; - } - } - - if (output.Length > 0) - { - SetCursorPositionImpl (lastCol, row); - Console.Out.Write (output); - } - } - - foreach (SixelToRender s in Application.Sixel) - { - if (!string.IsNullOrWhiteSpace (s.SixelData)) - { - SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Out.Write (s.SixelData); - } - } - - SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); - _cachedCursorVisibility = savedVisibility; - } + public void Write (ReadOnlySpan text) { Console.Out.Write (text); } /// public Size GetWindowSize () @@ -213,23 +42,37 @@ public Size GetWindowSize () return new (Console.WindowWidth, Console.WindowHeight); } - private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) + /// + public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } + + private Point? _lastCursorPosition; + + /// + protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) { - SetCursorPositionImpl (lastCol, row); - Console.Out.Write (output); - output.Clear (); - lastCol += outputWidth; - outputWidth = 0; + 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); } /// - public void SetCursorPosition (int col, int row) { SetCursorPositionImpl (col, row); } + protected override void Write (StringBuilder output) { Console.Out.Write (output); } - private Point _lastCursorPosition; - - private bool SetCursorPositionImpl (int col, int row) + protected override bool SetCursorPositionImpl (int col, int row) { - if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row) + if (_lastCursorPosition is { } && _lastCursorPosition.Value.X == col && _lastCursorPosition.Value.Y == row) { return true; } @@ -259,21 +102,10 @@ private bool SetCursorPositionImpl (int col, int row) } /// - public void Dispose () - { - Console.ResetColor (); - - //Disable alternative screen buffer. - Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); - - //Set cursor key to cursor. - Console.Out.Write (EscSeqUtils.CSI_ShowCursor); - - Console.Out.Close (); - } + public void Dispose () { } /// - public void SetCursorVisibility (CursorVisibility visibility) + public override void SetCursorVisibility (CursorVisibility visibility) { Console.Out.Write (visibility == CursorVisibility.Default ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); } diff --git a/Terminal.Gui/Drivers/V2/OutputBase.cs b/Terminal.Gui/Drivers/V2/OutputBase.cs new file mode 100644 index 0000000000..b28551e4bd --- /dev/null +++ b/Terminal.Gui/Drivers/V2/OutputBase.cs @@ -0,0 +1,163 @@ +namespace Terminal.Gui.Drivers; + +public abstract class OutputBase +{ + private CursorVisibility? _cachedCursorVisibility; + + // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). + private TextStyle _redrawTextStyle = TextStyle.None; + + /// + public virtual void Write (IOutputBuffer buffer) + { + if (ConsoleDriver.RunningUnitTests) + { + return; + } + + if (Console.WindowHeight < 1 + || buffer.Contents.Length != buffer.Rows * buffer.Cols + || buffer.Rows != Console.WindowHeight) + { + // return; + } + + var top = 0; + var left = 0; + int rows = buffer.Rows; + int cols = buffer.Cols; + var output = new StringBuilder (); + Attribute? redrawAttr = null; + int lastCol = -1; + + CursorVisibility? savedVisibility = _cachedCursorVisibility; + SetCursorVisibility (CursorVisibility.Invisible); + + const int maxCharsPerRune = 2; + Span runeBuffer = stackalloc char [maxCharsPerRune]; + + for (int row = top; row < rows; row++) + { + if (Console.WindowHeight < 1) + { + return; + } + + if (!SetCursorPositionImpl (0, row)) + { + return; + } + + output.Clear (); + + for (int col = left; col < cols; col++) + { + lastCol = -1; + var outputWidth = 0; + + for (; col < cols; col++) + { + if (!buffer.Contents [row, col].IsDirty) + { + if (output.Length > 0) + { + WriteToConsole (output, ref lastCol, row, ref outputWidth); + } + else if (lastCol == -1) + { + lastCol = col; + } + + if (lastCol + 1 < cols) + { + lastCol++; + } + + continue; + } + + if (lastCol == -1) + { + lastCol = col; + } + + Attribute attr = buffer.Contents [row, col].Attribute.Value; + + // Performance: Only send the escape sequence if the attribute has changed. + if (attr != redrawAttr) + { + redrawAttr = attr; + + AppendOrWriteAttribute (output, attr, _redrawTextStyle); + + _redrawTextStyle = attr.Style; + } + + outputWidth++; + + // Avoid Rune.ToString() by appending the rune chars. + Rune rune = buffer.Contents [row, col].Rune; + int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer); + ReadOnlySpan runeChars = runeBuffer [..runeCharsWritten]; + output.Append (runeChars); + + if (buffer.Contents [row, col].CombiningMarks.Count > 0) + { + // AtlasEngine does not support NON-NORMALIZED combining marks in a way + // compatible with the driver architecture. Any CMs (except in the first col) + // are correctly combined with the base char, but are ALSO treated as 1 column + // width codepoints E.g. `echo "[e`u{0301}`u{0301}]"` will output `[é ]`. + // + // For now, we just ignore the list of CMs. + //foreach (var combMark in Contents [row, col].CombiningMarks) { + // output.Append (combMark); + //} + // WriteToConsole (output, ref lastCol, row, ref outputWidth); + } + else if (rune.IsSurrogatePair () && rune.GetColumns () < 2) + { + WriteToConsole (output, ref lastCol, row, ref outputWidth); + SetCursorPositionImpl (col - 1, row); + } + + buffer.Contents [row, col].IsDirty = false; + } + } + + if (output.Length > 0) + { + SetCursorPositionImpl (lastCol, row); + Write (output); + } + } + + foreach (SixelToRender s in Application.Sixel) + { + if (!string.IsNullOrWhiteSpace (s.SixelData)) + { + SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); + Console.Out.Write (s.SixelData); + } + } + + SetCursorVisibility (savedVisibility ?? CursorVisibility.Default); + _cachedCursorVisibility = savedVisibility; + } + + protected abstract void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle); + + private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) + { + SetCursorPositionImpl (lastCol, row); + Write (output); + output.Clear (); + lastCol += outputWidth; + outputWidth = 0; + } + + protected abstract void Write (StringBuilder output); + + protected abstract bool SetCursorPositionImpl (int screenPositionX, int screenPositionY); + + public abstract void SetCursorVisibility (CursorVisibility visibility); +} diff --git a/Terminal.Gui/Drivers/V2/WindowsInput.cs b/Terminal.Gui/Drivers/V2/WindowsInput.cs index 11d01cb608..6207e18864 100644 --- a/Terminal.Gui/Drivers/V2/WindowsInput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsInput.cs @@ -4,7 +4,7 @@ namespace Terminal.Gui.Drivers; -internal class WindowsInput : ConsoleInput, IWindowsInput +internal class WindowsInput : ConsoleInput, IWindowsInput { private readonly nint _inputHandle; @@ -35,6 +35,9 @@ out uint lpNumberOfEventsRead private readonly uint _originalConsoleMode; + [DllImport ("kernel32.dll", SetLastError = true)] + private static extern bool FlushConsoleInputBuffer (nint hConsoleInput); + public WindowsInput () { Logging.Logger.LogInformation ($"Creating {nameof (WindowsInput)}"); @@ -50,16 +53,16 @@ public WindowsInput () _originalConsoleMode = v; uint newConsoleMode = _originalConsoleMode; - newConsoleMode |= (uint)(WindowsConsole.ConsoleModes.EnableMouseInput | WindowsConsole.ConsoleModes.EnableExtendedFlags); - newConsoleMode &= ~(uint)WindowsConsole.ConsoleModes.EnableQuickEditMode; - newConsoleMode &= ~(uint)WindowsConsole.ConsoleModes.EnableProcessedInput; + newConsoleMode |= (uint)(ConsoleModes.EnableMouseInput | ConsoleModes.EnableExtendedFlags); + newConsoleMode &= ~(uint)ConsoleModes.EnableQuickEditMode; + newConsoleMode &= ~(uint)ConsoleModes.EnableProcessedInput; SetConsoleMode (_inputHandle, newConsoleMode); } protected override bool Peek () { const int bufferSize = 1; // We only need to check if there's at least one event - nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize); + nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize); try { @@ -89,10 +92,10 @@ protected override bool Peek () } } - protected override IEnumerable Read () + protected override IEnumerable Read () { const int bufferSize = 1; - nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize); + nint pRecord = Marshal.AllocHGlobal (Marshal.SizeOf () * bufferSize); try { @@ -104,7 +107,7 @@ protected override bool Peek () return numberEventsRead == 0 ? [] - : new [] { Marshal.PtrToStructure (pRecord) }; + : new [] { Marshal.PtrToStructure (pRecord) }; } catch (Exception) { @@ -123,6 +126,11 @@ public override void Dispose () return; } + if (!FlushConsoleInputBuffer (_inputHandle)) + { + throw new ApplicationException ($"Failed to flush input buffer, error code: {Marshal.GetLastWin32Error ()}."); + } + SetConsoleMode (_inputHandle, _originalConsoleMode); } } diff --git a/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs b/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs index cdd2e1dfe4..aa50b20b43 100644 --- a/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs +++ b/Terminal.Gui/Drivers/V2/WindowsInputProcessor.cs @@ -71,7 +71,8 @@ protected override void ProcessAfterParsing (InputRecord input) { var key = KeyConverter.ToKey (input); - if (key != (Key)0) + // If the key is not valid, we don't want to raise any events. + if (IsValidInput (key, out key)) { OnKeyDown (key!); OnKeyUp (key!); @@ -82,10 +83,29 @@ public MouseEventArgs ToDriverMouse (WindowsConsole.MouseEventRecord e) { var mouseFlags = MouseFlags.None; - mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, WindowsConsole.ButtonState.Button1Pressed, MouseFlags.Button1Pressed, MouseFlags.Button1Released, 0); - mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, WindowsConsole.ButtonState.Button2Pressed, MouseFlags.Button2Pressed, MouseFlags.Button2Released, 1); - mouseFlags = UpdateMouseFlags (mouseFlags, e.ButtonState, WindowsConsole.ButtonState.Button4Pressed, MouseFlags.Button4Pressed, MouseFlags.Button4Released, 3); - + mouseFlags = UpdateMouseFlags ( + mouseFlags, + e.ButtonState, + WindowsConsole.ButtonState.Button1Pressed, + MouseFlags.Button1Pressed, + MouseFlags.Button1Released, + 0); + + mouseFlags = UpdateMouseFlags ( + mouseFlags, + e.ButtonState, + WindowsConsole.ButtonState.Button2Pressed, + MouseFlags.Button2Pressed, + MouseFlags.Button2Released, + 1); + + mouseFlags = UpdateMouseFlags ( + mouseFlags, + e.ButtonState, + WindowsConsole.ButtonState.Button4Pressed, + MouseFlags.Button4Pressed, + MouseFlags.Button4Released, + 3); // Deal with button 3 separately because it is considered same as 'rightmost button' if (e.ButtonState.HasFlag (WindowsConsole.ButtonState.Button3Pressed) || e.ButtonState.HasFlag (WindowsConsole.ButtonState.RightmostButtonPressed)) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index fb8e26815f..5152d3a238 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -1,13 +1,12 @@ #nullable enable -using System.Buffers; using System.ComponentModel; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Extensions.Logging; -using static Terminal.Gui.Drivers.WindowsConsole; namespace Terminal.Gui.Drivers; -internal partial class WindowsOutput : IConsoleOutput +internal partial class WindowsOutput : OutputBase, IConsoleOutput { [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] [return: MarshalAs (UnmanagedType.Bool)] @@ -19,11 +18,15 @@ private static partial bool WriteConsole ( nint lpReserved ); - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool CloseHandle (nint handle); + [LibraryImport ("kernel32.dll", SetLastError = true)] + private static partial nint GetStdHandle (int nStdHandle); - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern nint CreateConsoleScreenBuffer ( + [LibraryImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool CloseHandle (nint handle); + + [LibraryImport ("kernel32.dll", SetLastError = true)] + private static partial nint CreateConsoleScreenBuffer ( DesiredAccess dwDesiredAccess, ShareMode dwShareMode, nint secutiryAttributes, @@ -32,6 +35,7 @@ nint screenBufferData ); [DllImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] private static extern bool GetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi); [Flags] @@ -50,19 +54,52 @@ private enum DesiredAccess : uint internal static nint INVALID_HANDLE_VALUE = new (-1); - [DllImport ("kernel32.dll", SetLastError = true)] - private static extern bool SetConsoleActiveScreenBuffer (nint handle); + [LibraryImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool SetConsoleActiveScreenBuffer (nint handle); - [DllImport ("kernel32.dll")] - private static extern bool SetConsoleCursorPosition (nint hConsoleOutput, WindowsConsole.Coord dwCursorPosition); + [LibraryImport ("kernel32.dll")] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool SetConsoleCursorPosition (nint hConsoleOutput, WindowsConsole.Coord dwCursorPosition); [DllImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref WindowsConsole.ConsoleCursorInfo lpConsoleCursorInfo); - private readonly nint _screenBuffer; + [LibraryImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + public static partial bool SetConsoleTextAttribute (nint hConsoleOutput, ushort wAttributes); + + [LibraryImport ("kernel32.dll")] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool GetConsoleMode (nint hConsoleHandle, out uint lpMode); + + [LibraryImport ("kernel32.dll")] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool SetConsoleMode (nint hConsoleHandle, uint dwMode); + + [LibraryImport ("kernel32.dll", SetLastError = true)] + private static partial WindowsConsole.Coord GetLargestConsoleWindowSize ( + nint hConsoleOutput + ); + + [DllImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool SetConsoleScreenBufferInfoEx (nint hConsoleOutput, ref WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX consoleScreenBufferInfo); + + [DllImport ("kernel32.dll", SetLastError = true)] + [return: MarshalAs (UnmanagedType.Bool)] + private static extern bool SetConsoleWindowInfo ( + nint hConsoleOutput, + bool bAbsolute, + [In] ref WindowsConsole.SmallRect lpConsoleWindow + ); - // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). - private TextStyle _redrawTextStyle = TextStyle.None; + private const int STD_OUTPUT_HANDLE = -11; + private const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004; + private readonly nint _outputHandle; + private nint _screenBuffer; + private readonly bool _isVirtualTerminal; public WindowsOutput () { @@ -73,13 +110,48 @@ public WindowsOutput () return; } + // 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; + + if (_isVirtualTerminal) + { + //Enable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_SaveCursorAndActivateAltBufferNoBackscroll); + } + else + { + CreateScreenBuffer (); + + if (!GetConsoleMode (_screenBuffer, out mode)) + { + throw new ApplicationException ($"Failed to get screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); + } + + const uint ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002; + + mode &= ~ENABLE_WRAP_AT_EOL_OUTPUT; // Disable wrap + + if (!SetConsoleMode (_screenBuffer, mode)) + { + throw new ApplicationException ($"Failed to set screenBuffer console mode, error code: {Marshal.GetLastWin32Error ()}."); + } + + // Force 16 colors if not in virtual terminal mode. + Application.Force16Colors = true; + } + } + + private void CreateScreenBuffer () + { _screenBuffer = CreateConsoleScreenBuffer ( - DesiredAccess.GenericRead | DesiredAccess.GenericWrite, - ShareMode.FileShareRead | ShareMode.FileShareWrite, - nint.Zero, - 1, - nint.Zero - ); + DesiredAccess.GenericRead | DesiredAccess.GenericWrite, + ShareMode.FileShareRead | ShareMode.FileShareWrite, + nint.Zero, + 1, + nint.Zero + ); if (_screenBuffer == INVALID_HANDLE_VALUE) { @@ -99,212 +171,229 @@ public WindowsOutput () public void Write (ReadOnlySpan str) { - if (!WriteConsole (_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."); } } - public void Write (IOutputBuffer buffer) + public Size ResizeBuffer (Size size) { - WindowsConsole.ExtendedCharInfo [] outputBuffer = new WindowsConsole.ExtendedCharInfo [buffer.Rows * buffer.Cols]; + Size newSize = SetConsoleWindow ( + (short)Math.Max (size.Width, 0), + (short)Math.Max (size.Height, 0)); - // TODO: probably do need this right? - /* - if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows)) + return newSize; + } + + 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)) { - return; - }*/ + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } - var bufferCoords = new WindowsConsole.Coord + 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)) { - X = (short)buffer.Cols, //Clip.Width, - Y = (short)buffer.Rows //Clip.Height - }; + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + + var winRect = new WindowsConsole.SmallRect (0, 0, (short)(newCols - 1), (short)Math.Max (newRows - 1, 0)); - for (var row = 0; row < buffer.Rows; row++) + if (!SetConsoleWindowInfo (_outputHandle, true, ref winRect)) { - if (!buffer.DirtyLines [row]) - { - continue; - } + //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + return new (cols, rows); + } - buffer.DirtyLines [row] = false; + SetConsoleOutputWindow (csbi); - for (var col = 0; col < buffer.Cols; col++) - { - int position = row * buffer.Cols + col; - outputBuffer [position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault (); + return new (winRect.Right + 1, newRows - 1 < 0 ? 0 : winRect.Bottom + 1); + } - if (buffer.Contents [row, col].IsDirty == false) - { - outputBuffer [position].Empty = true; - outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + private void SetConsoleOutputWindow (WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX csbi) + { + if ((_isVirtualTerminal + ? _outputHandle + : _screenBuffer) != nint.Zero && !SetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) + { + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + } - continue; - } + public override void Write (IOutputBuffer outputBuffer) + { + _force16Colors = Application.Driver!.Force16Colors; + _everythingStringBuilder = new StringBuilder (); + + // 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) + { + _consoleBuffer = _outputHandle; + } + else + { + _consoleBuffer = _screenBuffer; + } + } + else + { + _consoleBuffer = _outputHandle; + } - outputBuffer [position].Empty = false; + base.Write (outputBuffer); - if (buffer.Contents [row, col].Rune.IsBmp) - { - outputBuffer [position].Char = (char)buffer.Contents [row, col].Rune.Value; - } - else + try + { + if (_force16Colors && !_isVirtualTerminal) + { + SetConsoleActiveScreenBuffer (_consoleBuffer); + } + else + { + var span = _everythingStringBuilder.ToString ().AsSpan (); // still allocates the string + + var result = WriteConsole (_consoleBuffer, span, (uint)span.Length, out _, nint.Zero); + if (!result) { - //outputBuffer [position].Empty = true; - outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + int err = Marshal.GetLastWin32Error (); - if (buffer.Contents [row, col].Rune.GetColumns () > 1 && col + 1 < buffer.Cols) + if (err != 0) { - // TODO: This is a hack to deal with non-BMP and wide characters. - col++; - position = row * buffer.Cols + col; - outputBuffer [position].Empty = false; - outputBuffer [position].Char = ' '; + throw new Win32Exception (err); } } } } - - var damageRegion = new WindowsConsole.SmallRect + catch (Exception e) { - Top = 0, - Left = 0, - Bottom = (short)buffer.Rows, - Right = (short)buffer.Cols - }; + Logging.Logger.LogError ($"Error: {e.Message} in {nameof (WindowsOutput)}"); - //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window, - if (!ConsoleDriver.RunningUnitTests - && !WriteToConsole ( - new (buffer.Cols, buffer.Rows), - outputBuffer, - bufferCoords, - damageRegion, - Application.Driver!.Force16Colors)) - { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) + if (!ConsoleDriver.RunningUnitTests) { - throw new Win32Exception (err); + throw; } } - - WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); } - - public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [] charInfoBuffer, WindowsConsole.Coord bufferSize, WindowsConsole.SmallRect window, bool force16Colors) + /// + protected override void Write (StringBuilder output) { + if (output.Length == 0) + { + return; + } - //Debug.WriteLine ("WriteToConsole"); + var str = output.ToString (); - //if (_screenBuffer == nint.Zero) - //{ - // ReadFromConsoleOutput (size, bufferSize, ref window); - //} + if (_force16Colors && !_isVirtualTerminal) + { + var a = str.ToCharArray (); + WriteConsole (_screenBuffer,a ,(uint)a.Length, out _, nint.Zero); + } + else + { + _everythingStringBuilder.Append (str); + } + } - var result = false; + /// + protected override void AppendOrWriteAttribute (StringBuilder output, Attribute attr, TextStyle redrawTextStyle) + { + var force16Colors = Application.Force16Colors; if (force16Colors) { - var i = 0; - WindowsConsole.CharInfo [] ci = new WindowsConsole.CharInfo [charInfoBuffer.Length]; - - foreach (WindowsConsole.ExtendedCharInfo info in charInfoBuffer) + if (_isVirtualTerminal) { - ci [i++] = new () - { - Char = new () { UnicodeChar = info.Char }, - Attributes = - (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) - }; + output.Append (EscSeqUtils.CSI_SetForegroundColor (attr.Foreground.GetAnsiColorCode ())); + output.Append (EscSeqUtils.CSI_SetBackgroundColor (attr.Background.GetAnsiColorCode ())); + EscSeqUtils.CSI_AppendTextStyleChange (output, redrawTextStyle, attr.Style); + } + else + { + var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4)); + SetConsoleTextAttribute (_screenBuffer, as16ColorInt); } - - result = WriteConsoleOutput (_screenBuffer, ci, bufferSize, new () { X = window.Left, Y = window.Top }, ref window); } else { - StringBuilder stringBuilder = new(); - - stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); - EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0); + 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); + } + } - Attribute? prev = null; - foreach (WindowsConsole.ExtendedCharInfo info in charInfoBuffer) - { - Attribute attr = info.Attribute; + private Size? _lastSize; + private Size? _lastWindowSizeBeforeMaximized; + private bool _lockResize; - if (attr != prev) - { - prev = attr; - EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, attr.Foreground.R, attr.Foreground.G, attr.Foreground.B); - EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, attr.Background.R, attr.Background.G, attr.Background.B); - EscSeqUtils.CSI_AppendTextStyleChange (stringBuilder, _redrawTextStyle, attr.Style); - _redrawTextStyle = attr.Style; - } - - if (info.Char != '\x1b') - { - if (!info.Empty) - { - stringBuilder.Append (info.Char); - } - } - else - { - stringBuilder.Append (' '); - } - } - - stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); - stringBuilder.Append (EscSeqUtils.CSI_HideCursor); + public Size GetWindowSize () + { + if (_lockResize) + { + return _lastSize!.Value; + } - // TODO: Potentially could stackalloc whenever reasonably small (<= 8 kB?) write buffer is needed. - char [] rentedWriteArray = ArrayPool.Shared.Rent (minimumLength: stringBuilder.Length); - try - { - Span writeBuffer = rentedWriteArray.AsSpan(0, stringBuilder.Length); - stringBuilder.CopyTo (0, writeBuffer, stringBuilder.Length); + var newSize = GetWindowSize (out _); + Size largestWindowSize = GetLargestConsoleWindowSize (); - // Supply console with the new content. - result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero); - } - finally + if (_lastWindowSizeBeforeMaximized is null && newSize == largestWindowSize) + { + _lastWindowSizeBeforeMaximized = _lastSize; + } + else if (_lastWindowSizeBeforeMaximized is { } && newSize != largestWindowSize) + { + if (newSize != _lastWindowSizeBeforeMaximized) { - ArrayPool.Shared.Return (rentedWriteArray); + newSize = _lastWindowSizeBeforeMaximized.Value; } - foreach (SixelToRender sixel in Application.Sixel) - { - SetCursorPosition ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y); - WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero); - } + _lastWindowSizeBeforeMaximized = null; } - if (!result) + if (_lastSize == null || _lastSize != newSize) { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) + // User is resizing the screen, they can only ever resize the active + // buffer since. We now however have issue because background offscreen + // buffer will be wrong size, recreate it to ensure it doesn't result in + // differing active and back buffer sizes (which causes flickering of window size) + Size? bufSize = null; + while (bufSize != newSize) { - throw new Win32Exception (err); + _lockResize = true; + bufSize = ResizeBuffer (newSize); } + + _lockResize = false; + _lastSize = newSize; } - return result; + return newSize; } - public Size GetWindowSize () + public Size GetWindowSize (out WindowsConsole.Coord cursorPosition) { var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); - if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) + if (!GetConsoleScreenBufferInfoEx (_isVirtualTerminal ? _outputHandle : _screenBuffer, ref csbi)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + cursorPosition = default; return Size.Empty; } @@ -312,18 +401,45 @@ public Size GetWindowSize () csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + cursorPosition = csbi.dwCursorPosition; return sz; } + private Size GetLargestConsoleWindowSize () + { + WindowsConsole.Coord maxWinSize = GetLargestConsoleWindowSize (_isVirtualTerminal ? _outputHandle : _screenBuffer); + + return new (maxWinSize.X, maxWinSize.Y); + } + + /// + protected override bool SetCursorPositionImpl (int screenPositionX, int screenPositionY) + { + if (_force16Colors && !_isVirtualTerminal) + { + SetConsoleCursorPosition (_screenBuffer, new ((short)screenPositionX, (short)screenPositionY)); + } + else + { + // CSI codes are 1 indexed + _everythingStringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); + EscSeqUtils.CSI_AppendCursorPosition (_everythingStringBuilder, screenPositionY + 1, screenPositionX + 1); + } + + _lastCursorPosition = new (screenPositionX, screenPositionY); + + return true; + } + /// - public void SetCursorVisibility (CursorVisibility visibility) + public override void SetCursorVisibility (CursorVisibility visibility) { if (ConsoleDriver.RunningUnitTests) { return; } - if (Application.Driver!.Force16Colors) + if (!_isVirtualTerminal) { var info = new WindowsConsole.ConsoleCursorInfo { @@ -342,22 +458,34 @@ public void SetCursorVisibility (CursorVisibility visibility) } } - private Point _lastCursorPosition; + private Point? _lastCursorPosition; /// public void SetCursorPosition (int col, int row) { - if (_lastCursorPosition.X == col && _lastCursorPosition.Y == row) + if (_lastCursorPosition is { } && _lastCursorPosition.Value.X == col && _lastCursorPosition.Value.Y == row) { return; } _lastCursorPosition = new (col, row); - SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); + if (_isVirtualTerminal) + { + var sb = new StringBuilder (); + EscSeqUtils.CSI_AppendCursorPosition (sb, row + 1, col + 1); + Write (sb.ToString ()); + } + else + { + SetConsoleCursorPosition (_screenBuffer, new ((short)col, (short)row)); + } } private bool _isDisposed; + private bool _force16Colors; + private nint _consoleBuffer; + private StringBuilder _everythingStringBuilder; /// public void Dispose () @@ -367,16 +495,19 @@ public void Dispose () return; } - if (_screenBuffer != nint.Zero) + if (_isVirtualTerminal) + { + //Disable alternative screen buffer. + Console.Out.Write (EscSeqUtils.CSI_RestoreCursorAndRestoreAltBufferWithBackscroll); + } + else { - try + if (_screenBuffer != nint.Zero) { CloseHandle (_screenBuffer); } - catch (Exception e) - { - Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method"); - } + + _screenBuffer = nint.Zero; } _isDisposed = true;