diff --git a/FIXES.md b/FIXES.md
index 0f553c5..8771604 100644
--- a/FIXES.md
+++ b/FIXES.md
@@ -1,3 +1,39 @@
+# Resize reflow for the normal buffer
+
+## Summary
+
+This change ports xterm.js 5.5.0 resize reflow into XTerm.NET. Shrinking column width re-wraps long logical lines onto additional `IsWrapped` rows instead of truncating them; growing width merges wrapped groups back. The alternate buffer is excluded via an explicit `hasScrollback: false` constructor flag.
+
+A related latent bug is fixed: when the buffer was at capacity and row count shrank, `CircularList.Resize` ran before trimming and kept the oldest lines, silently discarding the live screen bottom. Resize now trims from the top (raising `Trimmed`) before shrinking capacity.
+
+## Why
+
+Without reflow, shrinking a terminal window and growing it back left every long line permanently truncated at the narrowest width — the primary defect tracked as ISS-007. Scrollback lines were also lost on capacity shrink because `CircularList.Resize` preserved the wrong end of the buffer.
+
+## Files changed
+
+- `src/XTerm.NET/Buffer/BufferReflow.cs` — pure reflow functions ported from `BufferReflow.ts`
+- `src/XTerm.NET/Buffer/TerminalBuffer.cs` — `Resize` restructure, `ReflowLarger`/`ReflowSmaller`, `hasScrollback` flag
+- `src/XTerm.NET/Buffer/BufferLine.cs` — `GetWidth`, `HasContent`, `ReplaceCells`; `GetTrimmedLength` wide-char width
+- `src/XTerm.NET/Buffer/CircularList.cs` — `SetLength` for reflow batching
+- `src/XTerm.NET/Terminal.cs` — alt buffer `hasScrollback: false`
+- `src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs` — pure-function tests
+- `src/XTerm.NET.Tests/Buffer/BufferTests.cs` — reflow integration tests
+
+## Validation
+
+```powershell
+dotnet test src/XTerm.NET.slnx
+```
+
+Result on this branch:
+
+```text
+Passed: 624
+Failed: 0
+Skipped: 0
+```
+
# Docker progress rendering fixes
## Summary
diff --git a/src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs b/src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs
new file mode 100644
index 0000000..8203575
--- /dev/null
+++ b/src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs
@@ -0,0 +1,112 @@
+using XTerm.Buffer;
+
+namespace XTerm.Tests.Buffer;
+
+public class BufferReflowTests
+{
+ [Fact]
+ public void ReflowSmallerGetNewLineLengths_SmallLineWithWideCharacters()
+ {
+ var line = new BufferLine(4);
+ SetCell(line, 0, "汉", 2);
+ SetCell(line, 1, "", 0);
+ SetCell(line, 2, "语", 2);
+ SetCell(line, 3, "", 0);
+
+ Assert.Equal("汉语", line.TranslateToString(trimRight: true));
+ Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 3));
+ Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 2));
+ }
+
+ [Fact]
+ public void ReflowSmallerGetNewLineLengths_LargeLineWithWideCharacters()
+ {
+ var line = new BufferLine(12);
+ for (var i = 0; i < 12; i += 4)
+ {
+ SetCell(line, i, "汉", 2);
+ SetCell(line, i + 2, "语", 2);
+ }
+ for (var i = 1; i < 12; i += 2)
+ {
+ SetCell(line, i, "", 0);
+ }
+
+ Assert.Equal("汉语汉语汉语", line.TranslateToString());
+ Assert.Equal(new[] { 10, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 11));
+ Assert.Equal(new[] { 10, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 10));
+ Assert.Equal(new[] { 8, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 9));
+ Assert.Equal(new[] { 8, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 8));
+ Assert.Equal(new[] { 6, 6 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 7));
+ Assert.Equal(new[] { 6, 6 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 6));
+ Assert.Equal(new[] { 4, 4, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 5));
+ Assert.Equal(new[] { 4, 4, 4 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 4));
+ Assert.Equal(new[] { 2, 2, 2, 2, 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 3));
+ Assert.Equal(new[] { 2, 2, 2, 2, 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 12, 2));
+ }
+
+ [Fact]
+ public void ReflowSmallerGetNewLineLengths_MixedWideAndSingleCharacters()
+ {
+ var line = new BufferLine(6);
+ SetCell(line, 0, "a", 1);
+ SetCell(line, 1, "汉", 2);
+ SetCell(line, 2, "", 0);
+ SetCell(line, 3, "语", 2);
+ SetCell(line, 4, "", 0);
+ SetCell(line, 5, "b", 1);
+
+ Assert.Equal("a汉语b", line.TranslateToString());
+ Assert.Equal(new[] { 5, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 5));
+ Assert.Equal(new[] { 3, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 4));
+ Assert.Equal(new[] { 3, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 3));
+ Assert.Equal(new[] { 1, 2, 2, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 6, 2));
+ }
+
+ [Fact]
+ public void ReflowSmallerGetNewLineLengths_WrappedLineWithWideAndSingleCharacters()
+ {
+ var line1 = new BufferLine(6);
+ SetCell(line1, 0, "a", 1);
+ SetCell(line1, 1, "汉", 2);
+ SetCell(line1, 2, "", 0);
+ SetCell(line1, 3, "语", 2);
+ SetCell(line1, 4, "", 0);
+ SetCell(line1, 5, "b", 1);
+
+ var line2 = new BufferLine(6) { IsWrapped = true };
+ SetCell(line2, 0, "a", 1);
+ SetCell(line2, 1, "汉", 2);
+ SetCell(line2, 2, "", 0);
+ SetCell(line2, 3, "语", 2);
+ SetCell(line2, 4, "", 0);
+ SetCell(line2, 5, "b", 1);
+
+ Assert.Equal(new[] { 5, 4, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 5));
+ Assert.Equal(new[] { 3, 4, 4, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 4));
+ Assert.Equal(new[] { 3, 3, 3, 3 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 3));
+ Assert.Equal(new[] { 1, 2, 2, 2, 2, 2, 1 }, BufferReflow.ReflowSmallerGetNewLineLengths([line1, line2], 6, 2));
+ }
+
+ [Fact]
+ public void ReflowSmallerGetNewLineLengths_LinesEndingInNullSpace()
+ {
+ var line = new BufferLine(5);
+ SetCell(line, 0, "汉", 2);
+ SetCell(line, 1, "", 0);
+ SetCell(line, 2, "语", 2);
+ SetCell(line, 3, "", 0);
+ var empty = BufferCell.Empty;
+ line.SetCell(4, ref empty);
+
+ Assert.Equal("汉语", line.TranslateToString(trimRight: true));
+ Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 3));
+ Assert.Equal(new[] { 2, 2 }, BufferReflow.ReflowSmallerGetNewLineLengths([line], 4, 2));
+ }
+
+ private static void SetCell(BufferLine line, int col, string content, int width = 1)
+ {
+ var cell = content == "" ? BufferCell.Empty : new BufferCell(content, width, AttributeData.Default);
+ line.SetCell(col, ref cell);
+ }
+}
diff --git a/src/XTerm.NET.Tests/Buffer/BufferTests.cs b/src/XTerm.NET.Tests/Buffer/BufferTests.cs
index 83053d6..fa911c5 100644
--- a/src/XTerm.NET.Tests/Buffer/BufferTests.cs
+++ b/src/XTerm.NET.Tests/Buffer/BufferTests.cs
@@ -1187,6 +1187,364 @@ public void NormalBuffer_WithScrollback_ScrollUpAtTop_YBaseIncrements()
Assert.Equal(5, buffer.YDisp);
}
+ #endregion
+
+ #region Reflow Tests
+
+ [Fact]
+ public void Reflow_DoesNotWrapEmptyLines()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ var initialLength = buffer.Lines.Length;
+
+ buffer.Resize(75, 24);
+
+ Assert.Equal(initialLength, buffer.Lines.Length);
+ }
+
+ [Fact]
+ public void Reflow_ShrinksRowLength()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(5, 10);
+
+ for (var i = 0; i < 10; i++)
+ {
+ Assert.Equal(5, buffer.Lines[i]!.Length);
+ }
+ }
+
+ [Fact]
+ public void Reflow_WrapsAndUnwrapsLines()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(5, 10);
+
+ var firstLine = buffer.Lines[0]!;
+ for (var i = 0; i < 5; i++)
+ {
+ SetCell(firstLine, i, ((char)('a' + i)).ToString());
+ }
+
+ buffer.SetCursorRaw(0, 1);
+ Assert.Equal("abcde", buffer.Lines[0]!.TranslateToString());
+
+ buffer.Resize(1, 10);
+ Assert.Equal("a", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("b", buffer.Lines[1]!.TranslateToString());
+ Assert.Equal("c", buffer.Lines[2]!.TranslateToString());
+ Assert.Equal("d", buffer.Lines[3]!.TranslateToString());
+ Assert.Equal("e", buffer.Lines[4]!.TranslateToString());
+
+ buffer.Resize(5, 10);
+ Assert.Equal("abcde", buffer.Lines[0]!.TranslateToString());
+ }
+
+ [Fact]
+ public void Reflow_RemovesCorrectRowsWhenGrowingLarger()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(10, 10);
+ buffer.SetCursorRaw(0, 2);
+
+ for (var i = 0; i < 10; i++)
+ {
+ SetCell(buffer.Lines[0]!, i, ((char)('a' + i)).ToString());
+ SetCell(buffer.Lines[1]!, i, ((char)('0' + i)).ToString());
+ }
+
+ buffer.Resize(2, 10);
+ Assert.Equal("ab", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("cd", buffer.Lines[1]!.TranslateToString());
+ Assert.Equal("ef", buffer.Lines[2]!.TranslateToString());
+ Assert.Equal("gh", buffer.Lines[3]!.TranslateToString());
+ Assert.Equal("ij", buffer.Lines[4]!.TranslateToString());
+ Assert.Equal("01", buffer.Lines[5]!.TranslateToString());
+ Assert.Equal("23", buffer.Lines[6]!.TranslateToString());
+ Assert.Equal("45", buffer.Lines[7]!.TranslateToString());
+ Assert.Equal("67", buffer.Lines[8]!.TranslateToString());
+ Assert.Equal("89", buffer.Lines[9]!.TranslateToString());
+
+ buffer.Resize(10, 10);
+ Assert.Equal("abcdefghij", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("0123456789", buffer.Lines[1]!.TranslateToString());
+ }
+
+ [Fact]
+ public void Reflow_TransfersCombinedCharData()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(4, 3);
+ buffer.SetCursorRaw(0, 2);
+
+ SetCell(buffer.Lines[0]!, 0, "a");
+ SetCell(buffer.Lines[0]!, 1, "b");
+ SetCell(buffer.Lines[0]!, 2, "c");
+ SetCell(buffer.Lines[0]!, 3, "😁");
+
+ buffer.Resize(2, 3);
+ Assert.Equal("ab", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("c😁", buffer.Lines[1]!.TranslateToString());
+ }
+
+ [Fact]
+ public void Reflow_WideCharactersWhenShrinking()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(12, 10);
+ buffer.SetCursorRaw(0, 2);
+
+ for (var i = 0; i < 12; i += 4)
+ {
+ SetWideCell(buffer.Lines[0]!, i, "汉");
+ SetWideCell(buffer.Lines[1]!, i, "汉");
+ }
+ for (var i = 2; i < 12; i += 4)
+ {
+ SetWideCell(buffer.Lines[0]!, i, "语");
+ SetWideCell(buffer.Lines[1]!, i, "语");
+ }
+ buffer.Lines[1]!.IsWrapped = true;
+
+ buffer.Resize(11, 10);
+ Assert.Equal("汉语汉语汉", buffer.Lines[0]!.TranslateToString(trimRight: true));
+ Assert.Equal("语汉语汉语", buffer.Lines[1]!.TranslateToString(trimRight: true));
+ Assert.Equal("汉语", buffer.Lines[2]!.TranslateToString(trimRight: true));
+
+ buffer.Resize(7, 10);
+ Assert.Equal("汉语汉", buffer.Lines[0]!.TranslateToString(trimRight: true));
+ Assert.Equal("语汉语", buffer.Lines[1]!.TranslateToString(trimRight: true));
+ Assert.Equal("汉语汉", buffer.Lines[2]!.TranslateToString(trimRight: true));
+ Assert.Equal("语汉语", buffer.Lines[3]!.TranslateToString(trimRight: true));
+ }
+
+ [Fact]
+ public void Reflow_SkipsGroupsWithNonNormalLineAttribute()
+ {
+ var buffer = new TerminalBuffer(10, 5, 100);
+ buffer.Resize(4, 5);
+
+ SetCell(buffer.Lines[0]!, 0, "a");
+ SetCell(buffer.Lines[0]!, 1, "b");
+ SetCell(buffer.Lines[0]!, 2, "c");
+ SetCell(buffer.Lines[0]!, 3, "d");
+ buffer.Lines[1]!.IsWrapped = true;
+ buffer.Lines[1]!.LineAttribute = LineAttribute.DoubleWidth;
+ SetCell(buffer.Lines[1]!, 0, "e");
+ SetCell(buffer.Lines[1]!, 1, "f");
+ SetCell(buffer.Lines[1]!, 2, "g");
+ SetCell(buffer.Lines[1]!, 3, "h");
+
+ buffer.Resize(2, 5);
+
+ Assert.Equal("ab", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("ef", buffer.Lines[1]!.TranslateToString(trimRight: false));
+ Assert.Equal(LineAttribute.DoubleWidth, buffer.Lines[1]!.LineAttribute);
+ }
+
+ [Fact]
+ public void Reflow_RaisesTrimmedWhenLinesRemovedFromTop()
+ {
+ var buffer = new TerminalBuffer(80, 5, 1);
+ buffer.Resize(10, 5);
+
+ for (var i = 0; i < 10; i++)
+ {
+ SetCell(buffer.Lines[3]!, i, ((char)('a' + i)).ToString());
+ }
+
+ buffer.SetCursorRaw(0, 4);
+ var trimmedTotal = 0;
+ buffer.Trimmed += amount => trimmedTotal += amount;
+
+ buffer.Resize(2, 5);
+
+ Assert.True(trimmedTotal > 0);
+ }
+
+ [Fact]
+ public void Reflow_CursorStaysOnSameCharacterThroughShrinkGrow()
+ {
+ var buffer = new TerminalBuffer(20, 5, 100);
+ buffer.Resize(20, 5);
+
+ for (var i = 0; i < 20; i++)
+ {
+ SetCell(buffer.Lines[0]!, i, ((char)('a' + i)).ToString());
+ }
+
+ buffer.SetCursorRaw(9, 2);
+ buffer.Resize(10, 5);
+ buffer.Resize(20, 5);
+
+ Assert.Equal(9, buffer.X);
+ Assert.Equal(2, buffer.Y);
+ Assert.Equal("abcdefghijklmnopqrst", buffer.Lines[0]!.TranslateToString(trimRight: true));
+ }
+
+ [Fact]
+ public void Reflow_PendingWrapXEqualsColsDoesNotCrash()
+ {
+ var buffer = new TerminalBuffer(10, 5, 100);
+ buffer.Resize(10, 5);
+ buffer.SetCursorRaw(10, 0);
+
+ buffer.Resize(5, 5);
+ buffer.Resize(10, 5);
+
+ Assert.True(buffer.X >= 0);
+ }
+
+ [Fact]
+ public void Reflow_AtCapacityShrinkingRows_KeepsNewestLines()
+ {
+ var buffer = new TerminalBuffer(10, 5, 5);
+ buffer.ScrollUp(5);
+ Assert.Equal(10, buffer.Lines.Length);
+
+ SetCell(buffer.Lines[buffer.YBase + 4]!, 0, ">");
+ buffer.SetCursorRaw(0, 4);
+
+ buffer.Resize(10, 3);
+
+ Assert.Equal(">", buffer.Lines[7]![0].Content);
+ }
+
+ [Fact]
+ public void Reflow_AltBufferTruncatesWithoutReflow()
+ {
+ var buffer = new TerminalBuffer(10, 5, 0, hasScrollback: false);
+ for (var i = 0; i < 10; i++)
+ {
+ SetCell(buffer.Lines[0]!, i, ((char)('a' + i)).ToString());
+ }
+
+ buffer.Resize(5, 5);
+
+ Assert.Equal("abcde", buffer.Lines[0]!.TranslateToString(trimRight: true));
+ Assert.Equal(5, buffer.Lines.Length);
+ }
+
+ [Fact]
+ public void Reflow_GrowWhenLastBufferRowIsWrappedContinuation()
+ {
+ var buffer = new TerminalBuffer(5, 4, 100);
+ buffer.Resize(5, 4);
+ for (var i = 0; i < 5; i++)
+ {
+ SetCell(buffer.Lines[2]!, i, ((char)('a' + i)).ToString());
+ SetCell(buffer.Lines[3]!, i, ((char)('A' + i)).ToString());
+ }
+ buffer.Lines[3]!.IsWrapped = true; // last buffer row is a continuation
+ buffer.SetCursorRaw(0, 0); // cursor parked away so the group is not skipped
+
+ buffer.Resize(10, 4);
+
+ Assert.Equal("abcdeABCDE", buffer.Lines[2]!.TranslateToString(trimRight: true));
+ }
+
+ [Fact]
+ public void Reflow_WideCharactersWhenGrowing()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(6, 12);
+
+ var glyphs = new[] { "汉", "语", "測", "試" };
+ for (var row = 0; row < 4; row++)
+ {
+ for (var col = 0; col < 6; col += 2)
+ {
+ var glyphIndex = (row * 3 + col / 2) % glyphs.Length;
+ SetWideCell(buffer.Lines[row]!, col, glyphs[glyphIndex]);
+ }
+ }
+ buffer.Lines[1]!.IsWrapped = true;
+ buffer.Lines[2]!.IsWrapped = true;
+ buffer.Lines[3]!.IsWrapped = true;
+
+ Assert.Equal("汉语測", buffer.Lines[0]!.TranslateToString(trimRight: true));
+ Assert.Equal("試汉语", buffer.Lines[1]!.TranslateToString(trimRight: true));
+ Assert.Equal("測試汉", buffer.Lines[2]!.TranslateToString(trimRight: true));
+ Assert.Equal("语測試", buffer.Lines[3]!.TranslateToString(trimRight: true));
+
+ buffer.SetCursorRaw(0, 5);
+
+ buffer.Resize(7, 12);
+
+ var combined = buffer.Lines[0]!.TranslateToString(trimRight: true)
+ + buffer.Lines[1]!.TranslateToString(trimRight: true)
+ + buffer.Lines[2]!.TranslateToString(trimRight: true)
+ + buffer.Lines[3]!.TranslateToString(trimRight: true);
+ Assert.Equal("汉语測試汉语測試汉语測試", combined);
+ }
+
+ [Fact]
+ public void ReflowSmaller_MovesCursorDownWhenViewportNotFilled()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(4, 10);
+
+ SetCell(buffer.Lines[0]!, 0, "a");
+ SetCell(buffer.Lines[0]!, 1, "b");
+ SetCell(buffer.Lines[0]!, 2, "c");
+ SetCell(buffer.Lines[0]!, 3, "d");
+ SetCell(buffer.Lines[1]!, 0, "e");
+ SetCell(buffer.Lines[1]!, 1, "f");
+ SetCell(buffer.Lines[1]!, 2, "g");
+ SetCell(buffer.Lines[1]!, 3, "h");
+ buffer.Lines[1]!.IsWrapped = true;
+ SetCell(buffer.Lines[2]!, 0, "i");
+ SetCell(buffer.Lines[2]!, 1, "j");
+ SetCell(buffer.Lines[2]!, 2, "k");
+ SetCell(buffer.Lines[2]!, 3, "l");
+
+ buffer.SetCursorRaw(0, 3);
+ buffer.Resize(2, 10);
+
+ Assert.Equal(6, buffer.Y);
+ Assert.Equal(0, buffer.YDisp);
+ Assert.Equal(0, buffer.YBase);
+ Assert.Equal("ab", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("cd", buffer.Lines[1]!.TranslateToString());
+ Assert.Equal("ef", buffer.Lines[2]!.TranslateToString());
+ Assert.Equal("gh", buffer.Lines[3]!.TranslateToString());
+ Assert.True(buffer.Lines[1]!.IsWrapped);
+ Assert.True(buffer.Lines[3]!.IsWrapped);
+ }
+
+ [Fact]
+ public void ReflowLarger_MovesCursorUpWhenViewportNotFilled()
+ {
+ var buffer = new TerminalBuffer(80, 24, 1000);
+ buffer.Resize(2, 10);
+
+ SetCell(buffer.Lines[0]!, 0, "a");
+ SetCell(buffer.Lines[0]!, 1, "b");
+ SetCell(buffer.Lines[1]!, 0, "c");
+ SetCell(buffer.Lines[1]!, 1, "d");
+ buffer.Lines[1]!.IsWrapped = true;
+ SetCell(buffer.Lines[2]!, 0, "e");
+ SetCell(buffer.Lines[2]!, 1, "f");
+ SetCell(buffer.Lines[3]!, 0, "g");
+ SetCell(buffer.Lines[3]!, 1, "h");
+ buffer.Lines[3]!.IsWrapped = true;
+ SetCell(buffer.Lines[4]!, 0, "i");
+ SetCell(buffer.Lines[4]!, 1, "j");
+ SetCell(buffer.Lines[5]!, 0, "k");
+ SetCell(buffer.Lines[5]!, 1, "l");
+ buffer.Lines[5]!.IsWrapped = true;
+
+ buffer.SetCursorRaw(0, 6);
+ buffer.Resize(4, 10);
+
+ Assert.Equal(3, buffer.Y);
+ Assert.Equal(0, buffer.YDisp);
+ Assert.Equal(0, buffer.YBase);
+ Assert.Equal("abcd", buffer.Lines[0]!.TranslateToString());
+ Assert.Equal("efgh", buffer.Lines[1]!.TranslateToString());
+ Assert.Equal("ijkl", buffer.Lines[2]!.TranslateToString());
+ }
+
private static void SetCell(TerminalBuffer buffer, int row, string content)
{
var line = buffer.GetLine(row);
@@ -1194,5 +1552,17 @@ private static void SetCell(TerminalBuffer buffer, int row, string content)
line?.SetCell(0, ref cell);
}
+ private static void SetCell(BufferLine line, int col, string content, int width = 1)
+ {
+ var cell = new BufferCell(content, width, AttributeData.Default);
+ line.SetCell(col, ref cell);
+ }
+
+ private static void SetWideCell(BufferLine line, int col, string content)
+ {
+ SetCell(line, col, content, 2);
+ SetCell(line, col + 1, "", 0);
+ }
+
#endregion
}
diff --git a/src/XTerm.NET/Buffer/BufferLine.cs b/src/XTerm.NET/Buffer/BufferLine.cs
index f8028f5..b69f4ee 100644
--- a/src/XTerm.NET/Buffer/BufferLine.cs
+++ b/src/XTerm.NET/Buffer/BufferLine.cs
@@ -199,6 +199,47 @@ public string TranslateToString(bool trimRight = false, int startCol = 0, int en
return sb.ToString();
}
+ ///
+ /// Gets the width of the cell at the given column.
+ ///
+ public int GetWidth(int index)
+ {
+ if (index < 0 || index >= _length)
+ return 1;
+ return _cells[index].Width;
+ }
+
+ ///
+ /// Returns whether the cell at the given column has content.
+ ///
+ public bool HasContent(int index)
+ {
+ if (index < 0 || index >= _length)
+ return false;
+ var cell = _cells[index];
+ return !cell.IsSpace() && !cell.IsEmpty();
+ }
+
+ ///
+ /// Replaces cells in the range [startCol, endCol) with the fill cell.
+ ///
+ public void ReplaceCells(int startCol, int endCol, BufferCell fillCell)
+ {
+ if (startCol > 0 && GetWidth(startCol - 1) == 2)
+ {
+ _cells[startCol - 1] = fillCell;
+ }
+ if (endCol < _length && GetWidth(endCol - 1) == 2)
+ {
+ _cells[endCol] = fillCell;
+ }
+ while (startCol < endCol && startCol < _length)
+ {
+ _cells[startCol++] = fillCell;
+ }
+ Cache = null;
+ }
+
///
/// Gets the last non-whitespace cell index.
///
@@ -207,7 +248,7 @@ public int GetTrimmedLength()
for (int i = _length - 1; i >= 0; i--)
{
if (!_cells[i].IsSpace() && !_cells[i].IsEmpty())
- return i + 1;
+ return i + Math.Max(_cells[i].Width, 1);
}
return 0;
}
diff --git a/src/XTerm.NET/Buffer/BufferReflow.cs b/src/XTerm.NET/Buffer/BufferReflow.cs
new file mode 100644
index 0000000..a70609b
--- /dev/null
+++ b/src/XTerm.NET/Buffer/BufferReflow.cs
@@ -0,0 +1,254 @@
+// Ported from xterm.js BufferReflow.ts, (c) The xterm.js authors, MIT
+
+namespace XTerm.Buffer;
+
+///
+/// Result of creating a new buffer layout after reflowing larger.
+///
+public readonly struct NewLayoutResult
+{
+ public int[] Layout { get; init; }
+ public int CountRemoved { get; init; }
+}
+
+///
+/// Pure reflow helpers ported from xterm.js.
+///
+public static class BufferReflow
+{
+ ///
+ /// Evaluates indexes of rows to remove after a reflow-larger operation.
+ ///
+ public static int[] ReflowLargerGetLinesToRemove(
+ CircularList lines,
+ int oldCols,
+ int newCols,
+ int bufferAbsoluteY,
+ BufferCell nullCell)
+ {
+ var toRemove = new List();
+
+ for (var y = 0; y < lines.Length - 1; y++)
+ {
+ var i = y;
+ var nextLine = lines[++i];
+ if (nextLine is not { IsWrapped: true })
+ {
+ continue;
+ }
+
+ var wrappedLines = new List { lines[y]! };
+ while (i < lines.Length && nextLine is { IsWrapped: true })
+ {
+ wrappedLines.Add(nextLine);
+ nextLine = ++i < lines.Length ? lines[i] : null;
+ }
+
+ if (HasNonNormalLineAttribute(wrappedLines))
+ {
+ y += wrappedLines.Count - 1;
+ continue;
+ }
+
+ if (bufferAbsoluteY >= y && bufferAbsoluteY < i)
+ {
+ y += wrappedLines.Count - 1;
+ continue;
+ }
+
+ var destLineIndex = 0;
+ var destCol = GetWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);
+ var srcLineIndex = 1;
+ var srcCol = 0;
+ while (srcLineIndex < wrappedLines.Count)
+ {
+ var srcTrimmedLineLength = GetWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);
+ var srcRemainingCells = srcTrimmedLineLength - srcCol;
+ var destRemainingCells = newCols - destCol;
+ var cellsToCopy = Math.Min(srcRemainingCells, destRemainingCells);
+
+ wrappedLines[destLineIndex].CopyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);
+
+ destCol += cellsToCopy;
+ if (destCol == newCols)
+ {
+ destLineIndex++;
+ destCol = 0;
+ }
+ srcCol += cellsToCopy;
+ if (srcCol == srcTrimmedLineLength)
+ {
+ srcLineIndex++;
+ srcCol = 0;
+ }
+
+ if (destCol == 0 && destLineIndex != 0)
+ {
+ if (wrappedLines[destLineIndex - 1].GetWidth(newCols - 1) == 2)
+ {
+ wrappedLines[destLineIndex].CopyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);
+ var nullAtEnd = nullCell;
+ wrappedLines[destLineIndex - 1].SetCell(newCols - 1, ref nullAtEnd);
+ }
+ }
+ }
+
+ wrappedLines[destLineIndex].ReplaceCells(destCol, newCols, nullCell);
+
+ var countToRemove = 0;
+ for (var removeIndex = wrappedLines.Count - 1; removeIndex > 0; removeIndex--)
+ {
+ if (removeIndex > destLineIndex || wrappedLines[removeIndex].GetTrimmedLength() == 0)
+ {
+ countToRemove++;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ if (countToRemove > 0)
+ {
+ toRemove.Add(y + wrappedLines.Count - countToRemove);
+ toRemove.Add(countToRemove);
+ }
+
+ y += wrappedLines.Count - 1;
+ }
+
+ return toRemove.ToArray();
+ }
+
+ ///
+ /// Creates the new layout for lines given indexes to remove.
+ ///
+ public static NewLayoutResult ReflowLargerCreateNewLayout(CircularList lines, int[] toRemove)
+ {
+ var layout = new List();
+ var nextToRemoveIndex = 0;
+ var nextToRemoveStart = toRemove.Length > 0 ? toRemove[nextToRemoveIndex] : -1;
+ var countRemovedSoFar = 0;
+
+ for (var i = 0; i < lines.Length; i++)
+ {
+ if (nextToRemoveStart == i)
+ {
+ var countToRemove = toRemove[++nextToRemoveIndex];
+ i += countToRemove - 1;
+ countRemovedSoFar += countToRemove;
+ nextToRemoveStart = ++nextToRemoveIndex < toRemove.Length ? toRemove[nextToRemoveIndex] : -1;
+ }
+ else
+ {
+ layout.Add(i);
+ }
+ }
+
+ return new NewLayoutResult
+ {
+ Layout = layout.ToArray(),
+ CountRemoved = countRemovedSoFar
+ };
+ }
+
+ ///
+ /// Applies a new layout to the buffer in a single pass.
+ ///
+ public static void ReflowLargerApplyNewLayout(CircularList lines, int[] newLayout)
+ {
+ var newLayoutLines = new BufferLine[newLayout.Length];
+ for (var i = 0; i < newLayout.Length; i++)
+ {
+ newLayoutLines[i] = lines[newLayout[i]]!;
+ }
+
+ for (var i = 0; i < newLayoutLines.Length; i++)
+ {
+ lines[i] = newLayoutLines[i];
+ }
+
+ lines.SetLength(newLayout.Length);
+ }
+
+ ///
+ /// Gets new line lengths for a wrapped line group when shrinking columns.
+ ///
+ public static int[] ReflowSmallerGetNewLineLengths(
+ IReadOnlyList wrappedLines,
+ int oldCols,
+ int newCols)
+ {
+ var newLineLengths = new List();
+ var cellsNeeded = 0;
+ for (var i = 0; i < wrappedLines.Count; i++)
+ {
+ cellsNeeded += GetWrappedLineTrimmedLength(wrappedLines, i, oldCols);
+ }
+
+ var srcCol = 0;
+ var srcLine = 0;
+ var cellsAvailable = 0;
+ while (cellsAvailable < cellsNeeded)
+ {
+ if (cellsNeeded - cellsAvailable < newCols)
+ {
+ newLineLengths.Add(cellsNeeded - cellsAvailable);
+ break;
+ }
+
+ srcCol += newCols;
+ var oldTrimmedLength = GetWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);
+ if (srcCol > oldTrimmedLength)
+ {
+ srcCol -= oldTrimmedLength;
+ srcLine++;
+ }
+
+ var endsWithWide = wrappedLines[srcLine].GetWidth(srcCol - 1) == 2;
+ if (endsWithWide)
+ {
+ srcCol--;
+ }
+
+ var lineLength = endsWithWide ? newCols - 1 : newCols;
+ newLineLengths.Add(lineLength);
+ cellsAvailable += lineLength;
+ }
+
+ return newLineLengths.ToArray();
+ }
+
+ ///
+ /// Gets the trimmed length of a row within a wrapped line group.
+ ///
+ public static int GetWrappedLineTrimmedLength(IReadOnlyList lines, int i, int cols)
+ {
+ if (i == lines.Count - 1)
+ {
+ return lines[i].GetTrimmedLength();
+ }
+
+ var endsInNull = !lines[i].HasContent(cols - 1) && lines[i].GetWidth(cols - 1) == 1;
+ var followingLineStartsWithWide = lines[i + 1].GetWidth(0) == 2;
+ if (endsInNull && followingLineStartsWithWide)
+ {
+ return cols - 1;
+ }
+
+ return cols;
+ }
+
+ internal static bool HasNonNormalLineAttribute(IReadOnlyList lines)
+ {
+ for (var i = 0; i < lines.Count; i++)
+ {
+ if (lines[i].LineAttribute != LineAttribute.Normal)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/XTerm.NET/Buffer/CircularList.cs b/src/XTerm.NET/Buffer/CircularList.cs
index 1c31044..dd87963 100644
--- a/src/XTerm.NET/Buffer/CircularList.cs
+++ b/src/XTerm.NET/Buffer/CircularList.cs
@@ -187,6 +187,16 @@ public void Clear()
_length = 0;
}
+ ///
+ /// Sets the logical length of the list. Used by buffer reflow batching.
+ ///
+ internal void SetLength(int newLength)
+ {
+ if (newLength < 0 || newLength > MaxLength)
+ throw new ArgumentOutOfRangeException(nameof(newLength));
+ _length = newLength;
+ }
+
///
/// Resizes the maximum length of the circular list.
///
diff --git a/src/XTerm.NET/Buffer/TerminalBuffer.cs b/src/XTerm.NET/Buffer/TerminalBuffer.cs
index ce5da2e..7c0b13c 100644
--- a/src/XTerm.NET/Buffer/TerminalBuffer.cs
+++ b/src/XTerm.NET/Buffer/TerminalBuffer.cs
@@ -9,6 +9,7 @@ namespace XTerm.Buffer;
public class TerminalBuffer
{
private readonly CircularList _lines;
+ private readonly bool _hasScrollback;
private int _yDisp;
private int _yBase;
private int _y;
@@ -91,8 +92,9 @@ public SavedCursor()
public SavedCursor SavedCursorState { get; set; }
- public TerminalBuffer(int cols, int rows, int scrollback)
+ public TerminalBuffer(int cols, int rows, int scrollback, bool hasScrollback = true)
{
+ _hasScrollback = hasScrollback;
_cols = cols;
_rows = rows;
_lines = new CircularList(rows + scrollback);
@@ -111,6 +113,8 @@ public TerminalBuffer(int cols, int rows, int scrollback)
}
}
+ private bool IsReflowEnabled => _hasScrollback && _lines.MaxLength > _rows;
+
///
/// Gets a line from the buffer.
///
@@ -290,30 +294,53 @@ public int GetAbsoluteY(int y)
///
public void Resize(int newCols, int newRows)
{
- // Calculate new max length keeping the same scrollback capacity
+ var nullCell = BufferCell.Space;
var newMaxLength = newRows + (_lines.MaxLength - _rows);
- // Resize max length of circular list (may drop oldest lines if shrinking)
- _lines.Resize(newMaxLength);
-
- // Resize existing lines to the new column count
- var fillCell = BufferCell.Space;
- for (int i = 0; i < _lines.Length; i++)
+ if (newMaxLength > _lines.MaxLength)
{
- _lines[i]?.Resize(newCols, fillCell);
+ _lines.Resize(newMaxLength);
}
- // Ensure we have at least viewport rows available
- while (_lines.Length < newRows)
+ if (_lines.Length > 0)
{
- _lines.Push(new BufferLine(newCols, fillCell));
- }
+ if (_cols < newCols)
+ {
+ for (int i = 0; i < _lines.Length; i++)
+ {
+ _lines[i]?.Resize(newCols, nullCell);
+ }
+ }
- // If we have fewer rows, ensure ybase/ydisp stay in range
- _yBase = Math.Min(_yBase, Math.Max(0, _lines.Length - newRows));
- _yDisp = Math.Clamp(_yDisp, 0, _yBase);
+ while (_lines.Length < newRows)
+ {
+ _lines.Push(new BufferLine(newCols, nullCell));
+ }
+
+ _yBase = Math.Min(_yBase, Math.Max(0, _lines.Length - newRows));
+ _yDisp = Math.Clamp(_yDisp, 0, _yBase);
+
+ if (IsReflowEnabled && newCols != _cols)
+ {
+ if (newCols > _cols)
+ {
+ ReflowLarger(newCols, newRows);
+ }
+ else
+ {
+ ReflowSmaller(newCols, newRows);
+ }
+ }
+
+ if (_cols > newCols)
+ {
+ for (int i = 0; i < _lines.Length; i++)
+ {
+ _lines[i]?.Resize(newCols, nullCell);
+ }
+ }
+ }
- // Update scroll region and dimensions
var oldRows = _rows;
_cols = newCols;
_rows = newRows;
@@ -328,9 +355,260 @@ public void Resize(int newCols, int newRows)
}
_scrollTop = Math.Min(_scrollTop, newRows - 1);
- // Clamp cursor within new bounds
- _x = Math.Clamp(_x, 0, _cols - 1);
- _y = Math.Clamp(_y, 0, _rows - 1);
+ _x = Math.Min(_x, newCols - 1);
+ _y = Math.Min(_y, newRows - 1);
+ SavedCursorState.X = Math.Min(SavedCursorState.X, newCols - 1);
+
+ if (newMaxLength < _lines.MaxLength)
+ {
+ var amountToTrim = _lines.Length - newMaxLength;
+ if (amountToTrim > 0)
+ {
+ _lines.TrimStart(amountToTrim);
+ Trimmed?.Invoke(amountToTrim);
+ _yBase = Math.Max(_yBase - amountToTrim, 0);
+ _yDisp = Math.Max(_yDisp - amountToTrim, 0);
+ SavedCursorState.Y = Math.Max(SavedCursorState.Y - amountToTrim, 0);
+ }
+ _lines.Resize(newMaxLength);
+ }
+ }
+
+ private void ReflowLarger(int newCols, int newRows)
+ {
+ var nullCell = BufferCell.Space;
+ var toRemove = BufferReflow.ReflowLargerGetLinesToRemove(
+ _lines, _cols, newCols, _yBase + _y, nullCell);
+ if (toRemove.Length > 0)
+ {
+ var newLayoutResult = BufferReflow.ReflowLargerCreateNewLayout(_lines, toRemove);
+ BufferReflow.ReflowLargerApplyNewLayout(_lines, newLayoutResult.Layout);
+ ReflowLargerAdjustViewport(newCols, newRows, newLayoutResult.CountRemoved);
+ }
+ }
+
+ private void ReflowLargerAdjustViewport(int newCols, int newRows, int countRemoved)
+ {
+ var nullCell = BufferCell.Space;
+ var viewportAdjustments = countRemoved;
+ while (viewportAdjustments-- > 0)
+ {
+ if (_yBase == 0)
+ {
+ if (_y > 0)
+ {
+ _y--;
+ }
+ if (_lines.Length < newRows)
+ {
+ _lines.Push(new BufferLine(newCols, nullCell));
+ }
+ }
+ else
+ {
+ if (_yDisp == _yBase)
+ {
+ _yDisp--;
+ }
+ _yBase--;
+ }
+ }
+ SavedCursorState.Y = Math.Max(SavedCursorState.Y - countRemoved, 0);
+ }
+
+ private void ReflowSmaller(int newCols, int newRows)
+ {
+ var nullCell = BufferCell.Space;
+ var toInsert = new List<(int Start, List NewLines)>();
+ var countToInsert = 0;
+
+ for (var y = _lines.Length - 1; y >= 0; y--)
+ {
+ var nextLine = _lines[y];
+ if (nextLine == null || (!nextLine.IsWrapped && nextLine.GetTrimmedLength() <= newCols))
+ {
+ continue;
+ }
+
+ var wrappedLines = new List { nextLine };
+ while (nextLine.IsWrapped && y > 0)
+ {
+ nextLine = _lines[--y]!;
+ wrappedLines.Insert(0, nextLine);
+ }
+
+ if (BufferReflow.HasNonNormalLineAttribute(wrappedLines))
+ {
+ continue;
+ }
+
+ var absoluteY = _yBase + _y;
+ if (absoluteY >= y && absoluteY < y + wrappedLines.Count)
+ {
+ continue;
+ }
+
+ var lastLineLength = wrappedLines[^1].GetTrimmedLength();
+ var destLineLengths = BufferReflow.ReflowSmallerGetNewLineLengths(wrappedLines, _cols, newCols);
+ var linesToAdd = destLineLengths.Length - wrappedLines.Count;
+ int trimmedLines;
+ if (_yBase == 0 && _y != _lines.Length - 1)
+ {
+ trimmedLines = Math.Max(0, _y - _lines.MaxLength + linesToAdd);
+ }
+ else
+ {
+ trimmedLines = Math.Max(0, _lines.Length - _lines.MaxLength + linesToAdd);
+ }
+
+ var newLines = new List();
+ for (var i = 0; i < linesToAdd; i++)
+ {
+ newLines.Add(GetBlankLine(AttributeData.Default, isWrapped: true));
+ }
+
+ if (newLines.Count > 0)
+ {
+ toInsert.Add((y + wrappedLines.Count + countToInsert, newLines));
+ countToInsert += newLines.Count;
+ }
+
+ wrappedLines.AddRange(newLines);
+
+ var destLineIndex = destLineLengths.Length - 1;
+ var destCol = destLineLengths[destLineIndex];
+ if (destCol == 0)
+ {
+ destLineIndex--;
+ destCol = destLineLengths[destLineIndex];
+ }
+
+ var srcLineIndex = wrappedLines.Count - linesToAdd - 1;
+ var srcCol = lastLineLength;
+ while (srcLineIndex >= 0)
+ {
+ var cellsToCopy = Math.Min(srcCol, destCol);
+ if (wrappedLines[destLineIndex] == null)
+ {
+ break;
+ }
+
+ wrappedLines[destLineIndex].CopyCellsFrom(
+ wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);
+ destCol -= cellsToCopy;
+ if (destCol == 0)
+ {
+ destLineIndex--;
+ if (destLineIndex < 0)
+ {
+ break;
+ }
+ destCol = destLineLengths[destLineIndex];
+ }
+ srcCol -= cellsToCopy;
+ if (srcCol == 0)
+ {
+ srcLineIndex--;
+ var wrappedLinesIndex = Math.Max(srcLineIndex, 0);
+ srcCol = BufferReflow.GetWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, _cols);
+ }
+ }
+
+ for (var i = 0; i < wrappedLines.Count && i < destLineLengths.Length; i++)
+ {
+ if (destLineLengths[i] < newCols)
+ {
+ wrappedLines[i].ReplaceCells(destLineLengths[i], newCols, nullCell);
+ }
+ }
+
+ var viewportAdjustments = linesToAdd - trimmedLines;
+ while (viewportAdjustments-- > 0)
+ {
+ if (_yBase == 0)
+ {
+ if (_y < newRows - 1)
+ {
+ _y++;
+ _lines.Pop();
+ }
+ else
+ {
+ _yBase++;
+ _yDisp++;
+ }
+ }
+ else
+ {
+ if (_yBase < Math.Min(_lines.MaxLength, _lines.Length + countToInsert) - newRows)
+ {
+ if (_yBase == _yDisp)
+ {
+ _yDisp++;
+ }
+ _yBase++;
+ }
+ }
+ }
+
+ SavedCursorState.Y = Math.Min(SavedCursorState.Y + linesToAdd, _yBase + newRows - 1);
+ }
+
+ if (toInsert.Count > 0)
+ {
+ var originalLines = new List(_lines.Length);
+ for (var i = 0; i < _lines.Length; i++)
+ {
+ originalLines.Add(_lines[i]!);
+ }
+
+ var originalLinesLength = originalLines.Count;
+ RebuildWithInsertions(originalLines, toInsert, countToInsert);
+
+ var amountToTrim = Math.Max(0, originalLinesLength + countToInsert - _lines.MaxLength);
+ if (amountToTrim > 0)
+ {
+ _yBase = Math.Max(_yBase - amountToTrim, 0);
+ _yDisp = Math.Max(_yDisp - amountToTrim, 0);
+ SavedCursorState.Y = Math.Max(SavedCursorState.Y - amountToTrim, 0);
+ Trimmed?.Invoke(amountToTrim);
+ }
+ }
+ }
+
+ private void RebuildWithInsertions(
+ IReadOnlyList originalLines,
+ IReadOnlyList<(int Start, List NewLines)> toInsert,
+ int countInserted)
+ {
+ var originalLinesLength = originalLines.Count;
+ _lines.SetLength(Math.Min(_lines.MaxLength, originalLinesLength + countInserted));
+
+ var originalLineIndex = originalLinesLength - 1;
+ var nextToInsertIndex = 0;
+ var nextToInsert = nextToInsertIndex < toInsert.Count ? toInsert[nextToInsertIndex] : ((int Start, List NewLines)?)null;
+ var countInsertedSoFar = 0;
+
+ for (var i = Math.Min(_lines.MaxLength - 1, originalLinesLength + countInserted - 1); i >= 0; i--)
+ {
+ if (nextToInsert.HasValue && nextToInsert.Value.Start > originalLineIndex + countInsertedSoFar)
+ {
+ var insert = nextToInsert.Value;
+ for (var nextI = insert.NewLines.Count - 1; nextI >= 0; nextI--)
+ {
+ _lines[i--] = insert.NewLines[nextI];
+ }
+ i++;
+
+ countInsertedSoFar += insert.NewLines.Count;
+ nextToInsertIndex++;
+ nextToInsert = nextToInsertIndex < toInsert.Count ? toInsert[nextToInsertIndex] : null;
+ }
+ else
+ {
+ _lines[i] = originalLines[originalLineIndex--];
+ }
+ }
}
///
diff --git a/src/XTerm.NET/Terminal.cs b/src/XTerm.NET/Terminal.cs
index a66ef2d..cb0bb71 100644
--- a/src/XTerm.NET/Terminal.cs
+++ b/src/XTerm.NET/Terminal.cs
@@ -178,7 +178,7 @@ public Terminal(TerminalOptions? options = null)
// Initialize buffers
_normalBuffer = new Buffer.TerminalBuffer(Cols, Rows, Options.Scrollback);
- _altBuffer = new Buffer.TerminalBuffer(Cols, Rows, 0); // Alt buffer has no scrollback
+ _altBuffer = new Buffer.TerminalBuffer(Cols, Rows, 0, hasScrollback: false);
_buffer = _normalBuffer;
_usingAltBuffer = false;