diff --git a/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs b/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs new file mode 100644 index 0000000000..9ed6b328ca --- /dev/null +++ b/Benchmarks/ConsoleDrivers/EscSeqUtils/CSI_SetVsWrite.cs @@ -0,0 +1,31 @@ +using BenchmarkDotNet.Attributes; +using Tui = Terminal.Gui; + +namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils; + +[MemoryDiagnoser] +// Hide useless column from results. +[HideColumns ("writer")] +public class CSI_SetVsWrite +{ + [Benchmark (Baseline = true)] + [ArgumentsSource (nameof (TextWriterSource))] + public TextWriter Set (TextWriter writer) + { + writer.Write (Tui.EscSeqUtils.CSI_SetCursorPosition (1, 1)); + return writer; + } + + [Benchmark] + [ArgumentsSource (nameof (TextWriterSource))] + public TextWriter Write (TextWriter writer) + { + Tui.EscSeqUtils.CSI_WriteCursorPosition (writer, 1, 1); + return writer; + } + + public static IEnumerable TextWriterSource () + { + return [StringWriter.Null]; + } +} diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index 5bfdf039ee..adc463cc2c 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -1,5 +1,5 @@ #nullable enable -using Terminal.Gui.ConsoleDrivers; +using System.Globalization; using static Terminal.Gui.ConsoleDrivers.ConsoleKeyMapping; namespace Terminal.Gui; @@ -154,13 +154,13 @@ public enum ClearScreenOptions /// /// Control sequence for disabling mouse events. /// - public static string CSI_DisableMouseEvents { get; set; } = + public static readonly string CSI_DisableMouseEvents = CSI_DisableAnyEventMouse + CSI_DisableUrxvtExtModeMouse + CSI_DisableSgrExtModeMouse; /// /// Control sequence for enabling mouse events. /// - public static string CSI_EnableMouseEvents { get; set; } = + public static readonly string CSI_EnableMouseEvents = CSI_EnableAnyEventMouse + CSI_EnableUrxvtExtModeMouse + CSI_EnableSgrExtModeMouse; /// @@ -1688,6 +1688,32 @@ public static void CSI_AppendCursorPosition (StringBuilder builder, int row, int builder.Append ($"{CSI}{row};{col}H"); } + /// + /// ESC [ y ; x H - CUP Cursor Position - Cursor moves to x ; y coordinate within the viewport, where x is the column + /// of the y line + /// + /// TextWriter where to write the cursor position sequence. + /// Origin is (1,1). + /// Origin is (1,1). + public static void CSI_WriteCursorPosition (TextWriter writer, int row, int col) + { + const int maxInputBufferSize = + // CSI (2) + ';' + 'H' + 4 + + // row + col (2x int sign + int max value) + 2 + 20; + Span buffer = stackalloc char[maxInputBufferSize]; + if (!buffer.TryWrite (CultureInfo.InvariantCulture, $"{CSI}{row};{col}H", out int charsWritten)) + { + string tooLongCursorPositionSequence = $"{CSI}{row};{col}H"; + throw new InvalidOperationException ( + $"{nameof(CSI_WriteCursorPosition)} buffer (len: {buffer.Length}) is too short for cursor position sequence '{tooLongCursorPositionSequence}' (len: {tooLongCursorPositionSequence.Length})."); + } + + ReadOnlySpan cursorPositionSequence = buffer[..charsWritten]; + writer.Write (cursorPositionSequence); + } + //ESC [ ; f - HVP Horizontal Vertical Position* Cursor moves to; coordinate within the viewport, where is the column of the line //ESC [ s - ANSISYSSC Save Cursor – Ansi.sys emulation **With no parameters, performs a save cursor operation like DECSC //ESC [ u - ANSISYSRC Restore Cursor – Ansi.sys emulation **With no parameters, performs a restore cursor operation like DECRC diff --git a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs index 6004bf5e1b..cbd703ee89 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/IConsoleOutput.cs @@ -11,7 +11,7 @@ public interface IConsoleOutput : IDisposable /// overload. /// /// - void Write (string text); + void Write (ReadOnlySpan text); /// /// Write the contents of the to the console diff --git a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs index b7a9ea2806..69defb82e5 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/NetOutput.cs @@ -34,7 +34,10 @@ public NetOutput () } /// - public void Write (string text) { Console.Write (text); } + public void Write (ReadOnlySpan text) + { + Console.Out.Write (text); + } /// public void Write (IOutputBuffer buffer) @@ -57,6 +60,9 @@ public void Write (IOutputBuffer buffer) 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) @@ -115,26 +121,28 @@ public void Write (IOutputBuffer buffer) { redrawAttr = attr; - output.Append ( - EscSeqUtils.CSI_SetForegroundColorRGB ( - attr.Foreground.R, - attr.Foreground.G, - attr.Foreground.B - ) - ); - - output.Append ( - EscSeqUtils.CSI_SetBackgroundColorRGB ( - attr.Background.R, - attr.Background.G, - attr.Background.B - ) - ); + 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 + ); } outputWidth++; + + // Avoid Rune.ToString() by appending the rune chars. Rune rune = buffer.Contents [row, col].Rune; - output.Append (rune); + int runeCharsWritten = rune.EncodeToUtf16 (runeBuffer); + ReadOnlySpan runeChars = runeBuffer[..runeCharsWritten]; + output.Append (runeChars); if (buffer.Contents [row, col].CombiningMarks.Count > 0) { @@ -162,7 +170,7 @@ public void Write (IOutputBuffer buffer) if (output.Length > 0) { SetCursorPositionImpl (lastCol, row); - Console.Write (output); + Console.Out.Write (output); } } @@ -171,7 +179,7 @@ public void Write (IOutputBuffer buffer) if (!string.IsNullOrWhiteSpace (s.SixelData)) { SetCursorPositionImpl (s.ScreenPosition.X, s.ScreenPosition.Y); - Console.Write (s.SixelData); + Console.Out.Write (s.SixelData); } } @@ -185,7 +193,7 @@ public void Write (IOutputBuffer buffer) private void WriteToConsole (StringBuilder output, ref int lastCol, int row, ref int outputWidth) { SetCursorPositionImpl (lastCol, row); - Console.Write (output); + Console.Out.Write (output); output.Clear (); lastCol += outputWidth; outputWidth = 0; @@ -222,7 +230,7 @@ private bool SetCursorPositionImpl (int col, int row) // + 1 is needed because non-Windows is based on 1 instead of 0 and // Console.CursorTop/CursorLeft isn't reliable. - Console.Out.Write (EscSeqUtils.CSI_SetCursorPosition (row + 1, col + 1)); + EscSeqUtils.CSI_WriteCursorPosition (Console.Out, row + 1, col + 1); return true; } diff --git a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs index 382b01aa87..ba111c130f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/WindowsOutput.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Buffers; using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; @@ -6,12 +7,13 @@ namespace Terminal.Gui; -internal class WindowsOutput : IConsoleOutput +internal partial class WindowsOutput : IConsoleOutput { - [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool WriteConsole ( + [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool WriteConsole ( nint hConsoleOutput, - string lpbufer, + ReadOnlySpan lpbufer, uint numberOfCharsToWriten, out uint lpNumberOfCharsWritten, nint lpReserved @@ -84,7 +86,7 @@ public WindowsOutput () } } - public void Write (string str) + public void Write (ReadOnlySpan str) { if (!WriteConsole (_screenBuffer, str, (uint)str.Length, out uint _, nint.Zero)) { @@ -183,7 +185,6 @@ public void Write (IOutputBuffer buffer) public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord bufferSize, SmallRect window, bool force16Colors) { - var stringBuilder = new StringBuilder (); //Debug.WriteLine ("WriteToConsole"); @@ -213,10 +214,10 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord } else { - stringBuilder.Clear (); + StringBuilder stringBuilder = new(); stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); - stringBuilder.Append (EscSeqUtils.CSI_SetCursorPosition (0, 0)); + EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0); Attribute? prev = null; @@ -227,8 +228,8 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord if (attr != prev) { prev = attr; - stringBuilder.Append (EscSeqUtils.CSI_SetForegroundColorRGB (attr.Foreground.R, attr.Foreground.G, attr.Foreground.B)); - stringBuilder.Append (EscSeqUtils.CSI_SetBackgroundColorRGB (attr.Background.R, attr.Background.G, attr.Background.B)); + 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); } if (info.Char != '\x1b') @@ -247,14 +248,20 @@ public bool WriteToConsole (Size size, ExtendedCharInfo [] charInfoBuffer, Coord stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); stringBuilder.Append (EscSeqUtils.CSI_HideCursor); - var s = stringBuilder.ToString (); + // 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); - // TODO: requires extensive testing if we go down this route - // If console output has changed - //if (s != _lastWrite) - //{ - // supply console with the new content - result = WriteConsole (_screenBuffer, s, (uint)s.Length, out uint _, nint.Zero); + // Supply console with the new content. + result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero); + } + finally + { + ArrayPool.Shared.Return (rentedWriteArray); + } foreach (SixelToRender sixel in Application.Sixel) { @@ -297,9 +304,10 @@ public Size GetWindowSize () /// public void SetCursorVisibility (CursorVisibility visibility) { - var sb = new StringBuilder (); - sb.Append (visibility != CursorVisibility.Invisible ? EscSeqUtils.CSI_ShowCursor : EscSeqUtils.CSI_HideCursor); - Write (sb.ToString ()); + string cursorVisibilitySequence = visibility != CursorVisibility.Invisible + ? EscSeqUtils.CSI_ShowCursor + : EscSeqUtils.CSI_HideCursor; + Write (cursorVisibilitySequence); } private Point _lastCursorPosition; diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs index 4e93018cf5..f2fa41e25b 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsConsole.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; -internal class WindowsConsole +internal partial class WindowsConsole { private CancellationTokenSource? _inputReadyCancellationTokenSource; private readonly BlockingCollection _inputQueue = new (new ConcurrentQueue ()); @@ -926,10 +926,11 @@ public static extern bool WriteConsoleOutput ( ref SmallRect lpWriteRegion ); - [DllImport ("kernel32.dll", EntryPoint = "WriteConsole", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool WriteConsole ( + [LibraryImport ("kernel32.dll", EntryPoint = "WriteConsoleW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + [return: MarshalAs (UnmanagedType.Bool)] + private static partial bool WriteConsole ( nint hConsoleOutput, - string lpbufer, + ReadOnlySpan lpbufer, uint NumberOfCharsToWriten, out uint lpNumberOfCharsWritten, nint lpReserved diff --git a/Tests/UnitTests/Input/EscSeqUtilsTests.cs b/Tests/UnitTests/Input/EscSeqUtilsTests.cs index 10dd823237..73ec0998ea 100644 --- a/Tests/UnitTests/Input/EscSeqUtilsTests.cs +++ b/Tests/UnitTests/Input/EscSeqUtilsTests.cs @@ -1,4 +1,4 @@ -using JetBrains.Annotations; +using System.Text; using UnitTests; // ReSharper disable HeuristicUnreachableCode @@ -685,7 +685,7 @@ public void DecodeEscSeq_Multiple_Tests () top.Add (view); Application.Begin (top); - Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 }); + Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 }); ClearAll (); @@ -741,7 +741,7 @@ public void DecodeEscSeq_Multiple_Tests () // set Application.WantContinuousButtonPressedView to null view.WantContinuousButtonPressed = false; - Application.RaiseMouseEvent (new() { Position = new (0, 0), Flags = 0 }); + Application.RaiseMouseEvent (new () { Position = new (0, 0), Flags = 0 }); Application.RequestStop (); } @@ -1548,6 +1548,21 @@ public void InsertArray_Tests (string toInsert, string current, int? index, stri Assert.Equal (result, cki); } + [Theory] + [InlineData (0, 0, $"{EscSeqUtils.CSI}0;0H")] + [InlineData (int.MaxValue, int.MaxValue, $"{EscSeqUtils.CSI}2147483647;2147483647H")] + [InlineData (int.MinValue, int.MinValue, $"{EscSeqUtils.CSI}-2147483648;-2147483648H")] + public void CSI_WriteCursorPosition_ReturnsCorrectEscSeq (int row, int col, string expected) + { + StringBuilder builder = new(); + using StringWriter writer = new(builder); + + EscSeqUtils.CSI_WriteCursorPosition (writer, row, col); + + string actual = builder.ToString(); + Assert.Equal (expected, actual); + } + private void ClearAll () { EscSeqRequests.Clear ();