From ca37d898e8c95beee4ac9dd3df94f03c6907a6d3 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 15 Jul 2025 02:15:08 +0100 Subject: [PATCH 01/16] Consider width2 chars that are not IsBmp --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 27 +++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index fb8e26815f..fdf4c1cd42 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -146,16 +146,37 @@ public void Write (IOutputBuffer buffer) outputBuffer [position].Empty = false; - if (buffer.Contents [row, col].Rune.IsBmp) + var rune = buffer.Contents [row, col].Rune; + int width = rune.GetColumns (); + + if (rune.IsBmp) { - outputBuffer [position].Char = (char)buffer.Contents [row, col].Rune.Value; + if (width == 1) + { + // Single-width char, just encode first UTF-16 char + outputBuffer [position].Char = (char)rune.Value; + } + else if (width == 2 && col + 1 < buffer.Cols) + { + // Double-width char: encode to UTF-16 surrogate pair and write both halves + var utf16 = new char [2]; + rune.EncodeToUtf16 (utf16); + outputBuffer [position].Char = utf16 [0]; + outputBuffer [position].Empty = false; + + // Write second half into next cell + col++; + position = row * buffer.Cols + col; + outputBuffer [position].Char = utf16 [1]; + outputBuffer [position].Empty = false; + } } else { //outputBuffer [position].Empty = true; outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; - if (buffer.Contents [row, col].Rune.GetColumns () > 1 && col + 1 < buffer.Cols) + if (width > 1 && col + 1 < buffer.Cols) { // TODO: This is a hack to deal with non-BMP and wide characters. col++; From a37baf0dda7cc9496b86ab60999eec3381bc424f Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 15 Jul 2025 02:21:17 +0100 Subject: [PATCH 02/16] Apply same fix in WindowsDriver --- .../Drivers/WindowsDriver/WindowsDriver.cs | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs index 59941924fc..8de9290254 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs @@ -344,16 +344,37 @@ public override bool UpdateScreen () _outputBuffer [position].Empty = false; - if (Contents [row, col].Rune.IsBmp) + var rune = Contents [row, col].Rune; + int width = rune.GetColumns (); + + if (rune.IsBmp) { - _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value; + if (width == 1) + { + // Single-width char, just encode first UTF-16 char + _outputBuffer [position].Char = (char)rune.Value; + } + else if (width == 2 && col + 1 < Cols) + { + // Double-width char: encode to UTF-16 surrogate pair and write both halves + var utf16 = new char [2]; + rune.EncodeToUtf16 (utf16); + _outputBuffer [position].Char = utf16 [0]; + _outputBuffer [position].Empty = false; + + // Write second half into next cell + col++; + position = row * Cols + col; + _outputBuffer [position].Char = utf16 [1]; + _outputBuffer [position].Empty = false; + } } else { //_outputBuffer [position].Empty = true; _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; - if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols) + if (width > 1 && col + 1 < Cols) { // TODO: This is a hack to deal with non-BMP and wide characters. col++; From 1a91f8f8197baa992dfa95b83179afcbb85b080f Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 15 Jul 2025 02:26:56 +0100 Subject: [PATCH 03/16] Explicitly use type of local variable --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 2 +- Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index fdf4c1cd42..c21f592728 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -146,7 +146,7 @@ public void Write (IOutputBuffer buffer) outputBuffer [position].Empty = false; - var rune = buffer.Contents [row, col].Rune; + Rune rune = buffer.Contents [row, col].Rune; int width = rune.GetColumns (); if (rune.IsBmp) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs index 8de9290254..ac91db3f2a 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs @@ -344,7 +344,7 @@ public override bool UpdateScreen () _outputBuffer [position].Empty = false; - var rune = Contents [row, col].Rune; + Rune rune = Contents [row, col].Rune; int width = rune.GetColumns (); if (rune.IsBmp) From 788ce4653a21e010408fd7d0b8856f8decb7237d Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 17 Jul 2025 02:01:23 +0100 Subject: [PATCH 04/16] Revert changes to WindowsDriver --- .../Drivers/WindowsDriver/WindowsDriver.cs | 27 +++---------------- 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs index ac91db3f2a..59941924fc 100644 --- a/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver/WindowsDriver.cs @@ -344,37 +344,16 @@ public override bool UpdateScreen () _outputBuffer [position].Empty = false; - Rune rune = Contents [row, col].Rune; - int width = rune.GetColumns (); - - if (rune.IsBmp) + if (Contents [row, col].Rune.IsBmp) { - if (width == 1) - { - // Single-width char, just encode first UTF-16 char - _outputBuffer [position].Char = (char)rune.Value; - } - else if (width == 2 && col + 1 < Cols) - { - // Double-width char: encode to UTF-16 surrogate pair and write both halves - var utf16 = new char [2]; - rune.EncodeToUtf16 (utf16); - _outputBuffer [position].Char = utf16 [0]; - _outputBuffer [position].Empty = false; - - // Write second half into next cell - col++; - position = row * Cols + col; - _outputBuffer [position].Char = utf16 [1]; - _outputBuffer [position].Empty = false; - } + _outputBuffer [position].Char = (char)Contents [row, col].Rune.Value; } else { //_outputBuffer [position].Empty = true; _outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; - if (width > 1 && col + 1 < Cols) + if (Contents [row, col].Rune.GetColumns () > 1 && col + 1 < Cols) { // TODO: This is a hack to deal with non-BMP and wide characters. col++; From 6a59d034f8f9c4b8b4c50d7cb807c9c6cad0f59a Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 17 Jul 2025 02:49:25 +0100 Subject: [PATCH 05/16] Assume we are running in a terminal that supports true color by default unless user explicitly forces 16 --- Terminal.Gui/Drivers/V2/MainLoopCoordinator.cs | 7 ------- 1 file changed, 7 deletions(-) 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 (); From 4983e0c4909248fb28b44880250c7067ac571428 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 21 Jul 2025 02:53:09 +0100 Subject: [PATCH 06/16] Switch to SetAttribute and WriteConsole instead of WriteConsoleOutput for 16 color mode --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 182 +++++++++++++---------- 1 file changed, 103 insertions(+), 79 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index c21f592728..7cedadf398 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -3,7 +3,6 @@ using System.ComponentModel; using System.Runtime.InteropServices; using Microsoft.Extensions.Logging; -using static Terminal.Gui.Drivers.WindowsConsole; namespace Terminal.Gui.Drivers; @@ -59,6 +58,9 @@ private enum DesiredAccess : uint [DllImport ("kernel32.dll", SetLastError = true)] private static extern bool SetConsoleCursorInfo (nint hConsoleOutput, [In] ref WindowsConsole.ConsoleCursorInfo lpConsoleCursorInfo); + [DllImport ("kernel32.dll", SetLastError = true)] + public static extern bool SetConsoleTextAttribute (nint hConsoleOutput, ushort wAttributes); + private readonly nint _screenBuffer; // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). @@ -73,15 +75,31 @@ public WindowsOutput () return; } - _screenBuffer = CreateConsoleScreenBuffer ( - DesiredAccess.GenericRead | DesiredAccess.GenericWrite, - ShareMode.FileShareRead | ShareMode.FileShareWrite, - nint.Zero, - 1, - nint.Zero - ); + _screenBuffer = CreateScreenBuffer (); + + var backBuffer = CreateScreenBuffer (); + _doubleBuffer = [_screenBuffer, backBuffer]; + + if (!SetConsoleActiveScreenBuffer (_screenBuffer)) + { + throw new Win32Exception (Marshal.GetLastWin32Error ()); + } + } + + private int _activeDoubleBuffer = 0; + private nint [] _doubleBuffer = new nint[2]; - if (_screenBuffer == INVALID_HANDLE_VALUE) + private nint CreateScreenBuffer () + { + var buff = CreateConsoleScreenBuffer ( + DesiredAccess.GenericRead | DesiredAccess.GenericWrite, + ShareMode.FileShareRead | ShareMode.FileShareWrite, + nint.Zero, + 1, + nint.Zero + ); + + if (buff == INVALID_HANDLE_VALUE) { int err = Marshal.GetLastWin32Error (); @@ -91,10 +109,7 @@ public WindowsOutput () } } - if (!SetConsoleActiveScreenBuffer (_screenBuffer)) - { - throw new Win32Exception (Marshal.GetLastWin32Error ()); - } + return buff; } public void Write (ReadOnlySpan str) @@ -218,91 +233,72 @@ public void Write (IOutputBuffer buffer) public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [] charInfoBuffer, WindowsConsole.Coord bufferSize, WindowsConsole.SmallRect window, bool force16Colors) { + // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. + var buffer = force16Colors ? _doubleBuffer[_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2] : _screenBuffer; - //Debug.WriteLine ("WriteToConsole"); + var result = false; - //if (_screenBuffer == nint.Zero) - //{ - // ReadFromConsoleOutput (size, bufferSize, ref window); - //} + StringBuilder stringBuilder = new(); - var result = false; + stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); + EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0); - if (force16Colors) + Attribute? prev = null; + + foreach (WindowsConsole.ExtendedCharInfo info in charInfoBuffer) { - var i = 0; - WindowsConsole.CharInfo [] ci = new WindowsConsole.CharInfo [charInfoBuffer.Length]; + Attribute attr = info.Attribute; - foreach (WindowsConsole.ExtendedCharInfo info in charInfoBuffer) + if (attr != prev) { - ci [i++] = new () - { - Char = new () { UnicodeChar = info.Char }, - Attributes = - (ushort)((int)info.Attribute.Foreground.GetClosestNamedColor16 () | ((int)info.Attribute.Background.GetClosestNamedColor16 () << 4)) - }; + prev = attr; + AppendOrWrite (attr,force16Colors,stringBuilder,buffer); + _redrawTextStyle = attr.Style; } - 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); - - Attribute? prev = null; - - foreach (WindowsConsole.ExtendedCharInfo info in charInfoBuffer) + if (info.Char != '\x1b') { - Attribute attr = info.Attribute; - - if (attr != prev) + if (!info.Empty) { - 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 (' '); + AppendOrWrite (info.Char, force16Colors, stringBuilder, buffer); } } + else + { + stringBuilder.Append (' '); + } + } - stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); - stringBuilder.Append (EscSeqUtils.CSI_HideCursor); + if (force16Colors) + { + SetConsoleActiveScreenBuffer (buffer); + return true; + } - // 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); + stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); + stringBuilder.Append (EscSeqUtils.CSI_HideCursor); - // Supply console with the new content. - result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero); - } - finally - { - ArrayPool.Shared.Return (rentedWriteArray); - } + // 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); - 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); - } + // 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) + { + SetCursorPosition ((short)sixel.ScreenPosition.X, (short)sixel.ScreenPosition.Y); + WriteConsole (_screenBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero); } if (!result) @@ -318,6 +314,34 @@ public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [] charIn return result; } + private void AppendOrWrite (char infoChar, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) + { + + if (force16Colors) + { + WriteConsole (screenBuffer, [infoChar],1, out _, nint.Zero); + } + else + { + stringBuilder.Append (infoChar); + } + } + + private void AppendOrWrite (Attribute attr, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) + { + if (force16Colors) + { + var as16ColorInt = (ushort)((int)attr.Foreground.GetClosestNamedColor16 () | ((int)attr.Background.GetClosestNamedColor16 () << 4)); + SetConsoleTextAttribute (screenBuffer, as16ColorInt); + } + else + { + 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); + } + } + public Size GetWindowSize () { var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); From ebb3b0639f490e6f5afa716f17c8ca6ac774529c Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 21 Jul 2025 03:11:19 +0100 Subject: [PATCH 07/16] Fix some cursor issues (WIP) --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 7cedadf398..3c4d619c90 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -238,6 +238,14 @@ public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [] charIn var result = false; + GetWindowSize (out var cursorPosition); + + if (force16Colors) + { + SetConsoleCursorPosition (buffer, new (0, 0)); + + } + StringBuilder stringBuilder = new(); stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); @@ -274,6 +282,7 @@ public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [] charIn if (force16Colors) { SetConsoleActiveScreenBuffer (buffer); + SetConsoleCursorPosition (buffer, new (cursorPosition.X, cursorPosition.Y)); return true; } @@ -343,6 +352,10 @@ private void AppendOrWrite (Attribute attr, bool force16Colors, StringBuilder st } public Size GetWindowSize () + { + return GetWindowSize (out _); + } + public Size GetWindowSize (out WindowsConsole.Coord cursorPosition) { var csbi = new WindowsConsole.CONSOLE_SCREEN_BUFFER_INFOEX (); csbi.cbSize = (uint)Marshal.SizeOf (csbi); @@ -350,6 +363,7 @@ public Size GetWindowSize () if (!GetConsoleScreenBufferInfoEx (_screenBuffer, ref csbi)) { //throw new System.ComponentModel.Win32Exception (Marshal.GetLastWin32Error ()); + cursorPosition = default; return Size.Empty; } @@ -357,6 +371,7 @@ public Size GetWindowSize () csbi.srWindow.Right - csbi.srWindow.Left + 1, csbi.srWindow.Bottom - csbi.srWindow.Top + 1); + cursorPosition = csbi.dwCursorPosition; return sz; } From c74df745a1bbc7af43e87aea72c052ab8004f229 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 03:23:29 +0100 Subject: [PATCH 08/16] Remove concept of 'dirty rows' from v2 as its never actually used --- Terminal.Gui/Drivers/V2/IOutputBuffer.cs | 5 - Terminal.Gui/Drivers/V2/NetOutput.cs | 6 -- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 131 ++++++++++++----------- 3 files changed, 69 insertions(+), 73 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/IOutputBuffer.cs b/Terminal.Gui/Drivers/V2/IOutputBuffer.cs index 80f33a0c09..89891cbb18 100644 --- a/Terminal.Gui/Drivers/V2/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/V2/IOutputBuffer.cs @@ -9,11 +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/NetOutput.cs b/Terminal.Gui/Drivers/V2/NetOutput.cs index 32ae497645..65511442a2 100644 --- a/Terminal.Gui/Drivers/V2/NetOutput.cs +++ b/Terminal.Gui/Drivers/V2/NetOutput.cs @@ -80,17 +80,11 @@ public void Write (IOutputBuffer buffer) return; } - if (!buffer.DirtyLines [row]) - { - continue; - } - if (!SetCursorPositionImpl (0, row)) { return; } - buffer.DirtyLines [row] = false; output.Clear (); for (int col = left; col < cols; col++) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 3c4d619c90..02d1447c30 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -1,7 +1,9 @@ #nullable enable +using System; using System.Buffers; using System.ComponentModel; using System.Runtime.InteropServices; +using System.Text; using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; @@ -61,7 +63,7 @@ private enum DesiredAccess : uint [DllImport ("kernel32.dll", SetLastError = true)] public static extern bool SetConsoleTextAttribute (nint hConsoleOutput, ushort wAttributes); - private readonly nint _screenBuffer; + private nint _screenBuffer; // Last text style used, for updating style with EscSeqUtils.CSI_AppendTextStyleChange(). private TextStyle _redrawTextStyle = TextStyle.None; @@ -122,7 +124,7 @@ public void Write (ReadOnlySpan str) public void Write (IOutputBuffer buffer) { - WindowsConsole.ExtendedCharInfo [] outputBuffer = new WindowsConsole.ExtendedCharInfo [buffer.Rows * buffer.Cols]; + WindowsConsole.ExtendedCharInfo [][] outputBuffer = new WindowsConsole.ExtendedCharInfo[buffer.Rows] []; // TODO: probably do need this right? /* @@ -139,27 +141,14 @@ public void Write (IOutputBuffer buffer) for (var row = 0; row < buffer.Rows; row++) { - if (!buffer.DirtyLines [row]) - { - continue; - } + outputBuffer [row]= new WindowsConsole.ExtendedCharInfo[buffer.Cols]; - buffer.DirtyLines [row] = false; for (var col = 0; col < buffer.Cols; col++) { - int position = row * buffer.Cols + col; - outputBuffer [position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault (); - - if (buffer.Contents [row, col].IsDirty == false) - { - outputBuffer [position].Empty = true; - outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; - - continue; - } - - outputBuffer [position].Empty = false; + int position = col; + outputBuffer [row][position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault (); + outputBuffer [row][position].Empty = false; Rune rune = buffer.Contents [row, col].Rune; int width = rune.GetColumns (); @@ -169,35 +158,35 @@ public void Write (IOutputBuffer buffer) if (width == 1) { // Single-width char, just encode first UTF-16 char - outputBuffer [position].Char = (char)rune.Value; + outputBuffer [row][position].Char = (char)rune.Value; } else if (width == 2 && col + 1 < buffer.Cols) { // Double-width char: encode to UTF-16 surrogate pair and write both halves var utf16 = new char [2]; rune.EncodeToUtf16 (utf16); - outputBuffer [position].Char = utf16 [0]; - outputBuffer [position].Empty = false; + outputBuffer [row][position].Char = utf16 [0]; + outputBuffer [row][position].Empty = false; // Write second half into next cell col++; - position = row * buffer.Cols + col; - outputBuffer [position].Char = utf16 [1]; - outputBuffer [position].Empty = false; + position = col; + outputBuffer [row][position].Char = utf16 [1]; + outputBuffer [row][position].Empty = false; } } else { //outputBuffer [position].Empty = true; - outputBuffer [position].Char = (char)Rune.ReplacementChar.Value; + outputBuffer [row][position].Char = (char)Rune.ReplacementChar.Value; if (width > 1 && col + 1 < buffer.Cols) { // 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 = ' '; + position = col; + outputBuffer [row][position].Empty = false; + outputBuffer [row][position].Char = ' '; } } } @@ -231,58 +220,58 @@ public void Write (IOutputBuffer buffer) WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); } - public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [] charInfoBuffer, WindowsConsole.Coord bufferSize, WindowsConsole.SmallRect window, bool force16Colors) + public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] charInfoBuffer, WindowsConsole.Coord bufferSize, WindowsConsole.SmallRect window, bool force16Colors) { // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. var buffer = force16Colors ? _doubleBuffer[_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2] : _screenBuffer; - var result = false; + _screenBuffer = buffer; - GetWindowSize (out var cursorPosition); - - if (force16Colors) - { - SetConsoleCursorPosition (buffer, new (0, 0)); + var result = false; - } + GetWindowSize (out var originalCursorPosition); StringBuilder stringBuilder = new(); - stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); - EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 0, 0); Attribute? prev = null; - foreach (WindowsConsole.ExtendedCharInfo info in charInfoBuffer) + for (int row = 0; row < charInfoBuffer.Length; row++) { - Attribute attr = info.Attribute; + AppendOrWriteCursorPosition (new (0, row), force16Colors, stringBuilder, _screenBuffer); - if (attr != prev) + for(int col =0;col < charInfoBuffer [row].Length;col++) { - prev = attr; - AppendOrWrite (attr,force16Colors,stringBuilder,buffer); - _redrawTextStyle = attr.Style; - } + var info = charInfoBuffer [row] [col]; + Attribute attr = info.Attribute; + + if (attr != prev) + { + prev = attr; + AppendOrWrite (attr, force16Colors, stringBuilder, buffer); + _redrawTextStyle = attr.Style; + } - if (info.Char != '\x1b') - { - if (!info.Empty) + if (info.Char != '\x1b') { + if (!info.Empty) + { - AppendOrWrite (info.Char, force16Colors, stringBuilder, buffer); + AppendOrWrite (info.Char, force16Colors, stringBuilder, buffer); + } + } + else + { + stringBuilder.Append (' '); } - } - else - { - stringBuilder.Append (' '); } } if (force16Colors) { SetConsoleActiveScreenBuffer (buffer); - SetConsoleCursorPosition (buffer, new (cursorPosition.X, cursorPosition.Y)); + SetConsoleCursorPosition (buffer, new (originalCursorPosition.X, originalCursorPosition.Y)); return true; } @@ -351,6 +340,20 @@ private void AppendOrWrite (Attribute attr, bool force16Colors, StringBuilder st } } + private void AppendOrWriteCursorPosition (Point p, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) + { + if (force16Colors) + { + SetConsoleCursorPosition (screenBuffer, new ((short)p.X, (short)p.Y)); + } + else + { + // CSI codes are 1 indexed + stringBuilder.Append (EscSeqUtils.CSI_SaveCursorPosition); + EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, p.Y+1, p.X+1); + } + } + public Size GetWindowSize () { return GetWindowSize (out _); @@ -427,15 +430,19 @@ public void Dispose () return; } - if (_screenBuffer != nint.Zero) + for (int i = 0; i < 2; i++) { - try - { - CloseHandle (_screenBuffer); - } - catch (Exception e) + var buffer = _doubleBuffer [i]; + if (buffer != nint.Zero) { - Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method"); + try + { + CloseHandle (buffer); + } + catch (Exception e) + { + Logging.Logger.LogError (e, "Error trying to close screen buffer handle in WindowsOutput via interop method"); + } } } From b9dd4f5a242fc046ed825a9b4fd3374cdce168f3 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 03:24:58 +0100 Subject: [PATCH 09/16] Remove damageRegion as it does nothing --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 02d1447c30..95d93ed653 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -192,13 +192,6 @@ public void Write (IOutputBuffer buffer) } } - var damageRegion = new WindowsConsole.SmallRect - { - Top = 0, - Left = 0, - Bottom = (short)buffer.Rows, - Right = (short)buffer.Cols - }; //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window, if (!ConsoleDriver.RunningUnitTests @@ -206,7 +199,6 @@ public void Write (IOutputBuffer buffer) new (buffer.Cols, buffer.Rows), outputBuffer, bufferCoords, - damageRegion, Application.Driver!.Force16Colors)) { int err = Marshal.GetLastWin32Error (); @@ -216,11 +208,9 @@ public void Write (IOutputBuffer buffer) throw new Win32Exception (err); } } - - WindowsConsole.SmallRect.MakeEmpty (ref damageRegion); } - public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] charInfoBuffer, WindowsConsole.Coord bufferSize, WindowsConsole.SmallRect window, bool force16Colors) + public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] charInfoBuffer, WindowsConsole.Coord bufferSize, bool force16Colors) { // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. var buffer = force16Colors ? _doubleBuffer[_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2] : _screenBuffer; From 5501ada74dff0ec4e8eeae8560ac4c7d3f36e05d Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 03:28:32 +0100 Subject: [PATCH 10/16] Make string builder to console writing simpler --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 95d93ed653..9e5f9efbce 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -268,20 +268,8 @@ public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] char stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); stringBuilder.Append (EscSeqUtils.CSI_HideCursor); - // 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); - - // Supply console with the new content. - result = WriteConsole (_screenBuffer, writeBuffer, (uint)writeBuffer.Length, out uint _, nint.Zero); - } - finally - { - ArrayPool.Shared.Return (rentedWriteArray); - } + var span = stringBuilder.ToString ().AsSpan (); // still allocates the string + result = WriteConsole (_screenBuffer, span, (uint)span.Length, out _, nint.Zero); foreach (SixelToRender sixel in Application.Sixel) { From 6ce6b6b3599ccaf13a218439f395b2bcc27a4e3e Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 03:58:33 +0100 Subject: [PATCH 11/16] Radically simplify Write method --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 138 +++++------------------ 1 file changed, 26 insertions(+), 112 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 9e5f9efbce..d832968381 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -122,134 +122,43 @@ public void Write (ReadOnlySpan str) } } - public void Write (IOutputBuffer buffer) + public void Write (IOutputBuffer outputBuffer) { - WindowsConsole.ExtendedCharInfo [][] outputBuffer = new WindowsConsole.ExtendedCharInfo[buffer.Rows] []; + bool force16Colors = Application.Driver!.Force16Colors; - // TODO: probably do need this right? - /* - if (!windowSize.IsEmpty && (windowSize.Width != buffer.Cols || windowSize.Height != buffer.Rows)) - { - return; - }*/ - - var bufferCoords = new WindowsConsole.Coord - { - X = (short)buffer.Cols, //Clip.Width, - Y = (short)buffer.Rows //Clip.Height - }; - - for (var row = 0; row < buffer.Rows; row++) - { - outputBuffer [row]= new WindowsConsole.ExtendedCharInfo[buffer.Cols]; - - - for (var col = 0; col < buffer.Cols; col++) - { - int position = col; - outputBuffer [row][position].Attribute = buffer.Contents [row, col].Attribute.GetValueOrDefault (); - outputBuffer [row][position].Empty = false; - - Rune rune = buffer.Contents [row, col].Rune; - int width = rune.GetColumns (); - - if (rune.IsBmp) - { - if (width == 1) - { - // Single-width char, just encode first UTF-16 char - outputBuffer [row][position].Char = (char)rune.Value; - } - else if (width == 2 && col + 1 < buffer.Cols) - { - // Double-width char: encode to UTF-16 surrogate pair and write both halves - var utf16 = new char [2]; - rune.EncodeToUtf16 (utf16); - outputBuffer [row][position].Char = utf16 [0]; - outputBuffer [row][position].Empty = false; - - // Write second half into next cell - col++; - position = col; - outputBuffer [row][position].Char = utf16 [1]; - outputBuffer [row][position].Empty = false; - } - } - else - { - //outputBuffer [position].Empty = true; - outputBuffer [row][position].Char = (char)Rune.ReplacementChar.Value; - - if (width > 1 && col + 1 < buffer.Cols) - { - // TODO: This is a hack to deal with non-BMP and wide characters. - col++; - position = col; - outputBuffer [row][position].Empty = false; - outputBuffer [row][position].Char = ' '; - } - } - } - } - - - //size, ExtendedCharInfo [] charInfoBuffer, Coord , SmallRect window, - if (!ConsoleDriver.RunningUnitTests - && !WriteToConsole ( - new (buffer.Cols, buffer.Rows), - outputBuffer, - bufferCoords, - Application.Driver!.Force16Colors)) - { - int err = Marshal.GetLastWin32Error (); - - if (err != 0) - { - throw new Win32Exception (err); - } - } - } - - public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] charInfoBuffer, WindowsConsole.Coord bufferSize, bool force16Colors) - { // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. - var buffer = force16Colors ? _doubleBuffer[_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2] : _screenBuffer; + var consoleBuffer = force16Colors ? _doubleBuffer [_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2] : _screenBuffer; - _screenBuffer = buffer; + _screenBuffer = consoleBuffer; var result = false; GetWindowSize (out var originalCursorPosition); - StringBuilder stringBuilder = new(); - + StringBuilder stringBuilder = new (); Attribute? prev = null; - for (int row = 0; row < charInfoBuffer.Length; row++) + for (var row = 0; row < outputBuffer.Rows; row++) { AppendOrWriteCursorPosition (new (0, row), force16Colors, stringBuilder, _screenBuffer); - for(int col =0;col < charInfoBuffer [row].Length;col++) + for (var col = 0; col < outputBuffer.Cols; col++) { - var info = charInfoBuffer [row] [col]; - Attribute attr = info.Attribute; + var cell = outputBuffer.Contents [row, col]; + var attr = cell.Attribute ?? prev ?? new (); if (attr != prev) { prev = attr; - AppendOrWrite (attr, force16Colors, stringBuilder, buffer); + AppendOrWrite (attr!, force16Colors, stringBuilder, (nint)consoleBuffer); _redrawTextStyle = attr.Style; } - if (info.Char != '\x1b') + if (cell.Rune.Value != '\x1b') { - if (!info.Empty) - { - - AppendOrWrite (info.Char, force16Colors, stringBuilder, buffer); - } + AppendOrWrite (cell.Rune, force16Colors, stringBuilder, consoleBuffer); } else { @@ -260,9 +169,9 @@ public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] char if (force16Colors) { - SetConsoleActiveScreenBuffer (buffer); - SetConsoleCursorPosition (buffer, new (originalCursorPosition.X, originalCursorPosition.Y)); - return true; + SetConsoleActiveScreenBuffer (consoleBuffer); + SetConsoleCursorPosition (consoleBuffer, new (originalCursorPosition.X, originalCursorPosition.Y)); + return; } stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); @@ -286,20 +195,25 @@ public bool WriteToConsole (Size size, WindowsConsole.ExtendedCharInfo [][] char throw new Win32Exception (err); } } - - return result; } - private void AppendOrWrite (char infoChar, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) - { + private void AppendOrWrite (Rune rune, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) + { if (force16Colors) { - WriteConsole (screenBuffer, [infoChar],1, out _, nint.Zero); + char [] chars = new char [2]; // allocate maximum space (2 chars for surrogate pair) + int written = rune.EncodeToUtf16 (chars); + + // Slice array to actual size used + char [] result = new char [written]; + Array.Copy (chars, result, written); + + WriteConsole (screenBuffer,chars ,(uint)written, out _, nint.Zero); } else { - stringBuilder.Append (infoChar); + stringBuilder.Append (rune); } } From e25beff5f9b157a3f9373938a6177ee4f0dd709d Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 04:01:34 +0100 Subject: [PATCH 12/16] Simplify conditional logic --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index d832968381..5aea387f86 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -127,9 +127,16 @@ public void Write (IOutputBuffer outputBuffer) bool force16Colors = Application.Driver!.Force16Colors; // for 16 color mode we will write to a backing buffer then flip it to the active one at the end to avoid jitter. - var consoleBuffer = force16Colors ? _doubleBuffer [_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2] : _screenBuffer; - - _screenBuffer = consoleBuffer; + nint consoleBuffer; + if (force16Colors) + { + consoleBuffer = _doubleBuffer [_activeDoubleBuffer = (_activeDoubleBuffer + 1) % 2]; + _screenBuffer = consoleBuffer; + } + else + { + consoleBuffer = _screenBuffer; + } var result = false; From 77fcdefeb62c1a375ebffe0b9776794e8749e32f Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 04:20:39 +0100 Subject: [PATCH 13/16] Simplify restoring cursor position --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 5aea387f86..9eb2a4bfdc 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -174,15 +174,7 @@ public void Write (IOutputBuffer outputBuffer) } } - if (force16Colors) - { - SetConsoleActiveScreenBuffer (consoleBuffer); - SetConsoleCursorPosition (consoleBuffer, new (originalCursorPosition.X, originalCursorPosition.Y)); - return; - } - - stringBuilder.Append (EscSeqUtils.CSI_RestoreCursorPosition); - stringBuilder.Append (EscSeqUtils.CSI_HideCursor); + AppendOrWriteCursorPosition(new (originalCursorPosition.X, originalCursorPosition.Y),force16Colors,stringBuilder, consoleBuffer); var span = stringBuilder.ToString ().AsSpan (); // still allocates the string result = WriteConsole (_screenBuffer, span, (uint)span.Length, out _, nint.Zero); From e08b2d6efe00ee5548430b7d216cab26c42b7a40 Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 04:42:38 +0100 Subject: [PATCH 14/16] Reference local variable for console buffer --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 9eb2a4bfdc..25ee693083 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -1,9 +1,6 @@ #nullable enable -using System; -using System.Buffers; using System.ComponentModel; using System.Runtime.InteropServices; -using System.Text; using Microsoft.Extensions.Logging; namespace Terminal.Gui.Drivers; @@ -148,13 +145,20 @@ public void Write (IOutputBuffer outputBuffer) for (var row = 0; row < outputBuffer.Rows; row++) { - AppendOrWriteCursorPosition (new (0, row), force16Colors, stringBuilder, _screenBuffer); + AppendOrWriteCursorPosition (new (0, row), force16Colors, stringBuilder, consoleBuffer); for (var col = 0; col < outputBuffer.Cols; col++) { var cell = outputBuffer.Contents [row, col]; var attr = cell.Attribute ?? prev ?? new (); + // If we have 10 width and are at 9 it's ok + // But if it's width 2 we can't write it so must skip + if (cell.Rune.GetColumns () + col > outputBuffer.Cols) + { + continue; + } + if (attr != prev) { prev = attr; @@ -162,7 +166,6 @@ public void Write (IOutputBuffer outputBuffer) _redrawTextStyle = attr.Style; } - if (cell.Rune.Value != '\x1b') { AppendOrWrite (cell.Rune, force16Colors, stringBuilder, consoleBuffer); @@ -176,13 +179,19 @@ public void Write (IOutputBuffer outputBuffer) AppendOrWriteCursorPosition(new (originalCursorPosition.X, originalCursorPosition.Y),force16Colors,stringBuilder, consoleBuffer); + if (force16Colors) + { + SetConsoleActiveScreenBuffer (consoleBuffer); + return; + } + var span = stringBuilder.ToString ().AsSpan (); // still allocates the string - result = WriteConsole (_screenBuffer, span, (uint)span.Length, out _, nint.Zero); + result = WriteConsole (consoleBuffer, span, (uint)span.Length, out _, nint.Zero); 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); + WriteConsole (consoleBuffer, sixel.SixelData, (uint)sixel.SixelData.Length, out uint _, nint.Zero); } if (!result) From 711e4742d1f2343755291e8c4695d8f52225237d Mon Sep 17 00:00:00 2001 From: tznind Date: Tue, 22 Jul 2025 19:53:24 +0100 Subject: [PATCH 15/16] Reduce calls to ConsoleWrite by accumulating till attribute changes --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 44 ++++++++++++++++-------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 25ee693083..48e943847d 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -145,6 +145,8 @@ public void Write (IOutputBuffer outputBuffer) for (var row = 0; row < outputBuffer.Rows; row++) { + StringBuilder sbSinceLastAttrChange = new StringBuilder (); + AppendOrWriteCursorPosition (new (0, row), force16Colors, stringBuilder, consoleBuffer); for (var col = 0; col < outputBuffer.Cols; col++) @@ -162,19 +164,28 @@ public void Write (IOutputBuffer outputBuffer) if (attr != prev) { prev = attr; + + // write everything out up till now + AppendOrWrite (sbSinceLastAttrChange.ToString (), force16Colors, stringBuilder, consoleBuffer); + + // then change color/style etc AppendOrWrite (attr!, force16Colors, stringBuilder, (nint)consoleBuffer); + sbSinceLastAttrChange.Clear (); + sbSinceLastAttrChange.Append (cell.Rune); _redrawTextStyle = attr.Style; } - - if (cell.Rune.Value != '\x1b') - { - AppendOrWrite (cell.Rune, force16Colors, stringBuilder, consoleBuffer); - } else { - stringBuilder.Append (' '); + sbSinceLastAttrChange.Append (cell.Rune); } } + + // write trailing bits + if (sbSinceLastAttrChange.Length > 0) + { + AppendOrWrite (sbSinceLastAttrChange.ToString (), force16Colors, stringBuilder, consoleBuffer); + sbSinceLastAttrChange.Clear (); + } } AppendOrWriteCursorPosition(new (originalCursorPosition.X, originalCursorPosition.Y),force16Colors,stringBuilder, consoleBuffer); @@ -206,22 +217,25 @@ public void Write (IOutputBuffer outputBuffer) } - private void AppendOrWrite (Rune rune, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) + private void AppendOrWrite (string str, bool force16Colors, StringBuilder stringBuilder, nint screenBuffer) { - if (force16Colors) + if (str.Length == 0) { - char [] chars = new char [2]; // allocate maximum space (2 chars for surrogate pair) - int written = rune.EncodeToUtf16 (chars); + return; + } - // Slice array to actual size used - char [] result = new char [written]; - Array.Copy (chars, result, written); + // Replace escape characters with space + str = str.Replace ("\x1b", " "); - WriteConsole (screenBuffer,chars ,(uint)written, out _, nint.Zero); + + if (force16Colors) + { + var a = str.ToCharArray (); + WriteConsole (screenBuffer,a ,(uint)a.Length, out _, nint.Zero); } else { - stringBuilder.Append (rune); + stringBuilder.Append (str); } } From f6fbfc332940314e2dd04b38b82e7b605dd51af6 Mon Sep 17 00:00:00 2001 From: tznind Date: Sun, 27 Jul 2025 15:54:11 +0100 Subject: [PATCH 16/16] When resizing v2 16 color mode on windows, recreate the back buffer to match its size --- Terminal.Gui/Drivers/V2/WindowsOutput.cs | 42 +++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Drivers/V2/WindowsOutput.cs b/Terminal.Gui/Drivers/V2/WindowsOutput.cs index 48e943847d..4c9cc3024e 100644 --- a/Terminal.Gui/Drivers/V2/WindowsOutput.cs +++ b/Terminal.Gui/Drivers/V2/WindowsOutput.cs @@ -119,6 +119,26 @@ public void Write (ReadOnlySpan str) } } + public void RecreateBackBuffer () + { + int idx = (_activeDoubleBuffer + 1) % 2; + var inactiveBuffer = _doubleBuffer [idx]; + + DisposeBuffer (inactiveBuffer); + _doubleBuffer [idx] = CreateScreenBuffer (); + } + + private void DisposeBuffer (nint buffer) + { + if (buffer != 0 && buffer != INVALID_HANDLE_VALUE) + { + if (!CloseHandle (buffer)) + { + throw new Win32Exception (Marshal.GetLastWin32Error (), "Failed to close screen buffer handle."); + } + } + } + public void Write (IOutputBuffer outputBuffer) { bool force16Colors = Application.Driver!.Force16Colors; @@ -268,9 +288,29 @@ private void AppendOrWriteCursorPosition (Point p, bool force16Colors, StringBui } } + private Size? _lastSize = null; public Size GetWindowSize () { - return GetWindowSize (out _); + + var newSize = GetWindowSize (out _); + + if (_lastSize == null || _lastSize != newSize) + { + // Back buffers only apply to 16 color mode so if not in that just ignore + if (!Application.Force16Colors) + { + return newSize; + } + + // User is resizing the screen, they can only ever resize the active + // buffer since. We now however have issue because background offscreen + // buffer will be wrong size, recreate it to ensure it doesn't result in + // differing active and back buffer sizes (which causes flickering of window size) + RecreateBackBuffer (); + _lastSize = newSize; + } + + return newSize; } public Size GetWindowSize (out WindowsConsole.Coord cursorPosition) {