From 6980bbebb3c38bed1ce3f0bdce20af97df7905ae Mon Sep 17 00:00:00 2001
From: lukasz buchmiet <56140308+buchmiet@users.noreply.github.com>
Date: Fri, 24 Jul 2026 00:02:06 +0100
Subject: [PATCH] Add resize reflow for the normal buffer
Ports xterm.js 5.5.0 resize reflow (BufferReflow.ts and the reflow
orchestration in Buffer.ts) into XTerm.NET.
Shrinking the column count now re-wraps long logical lines onto
additional IsWrapped rows instead of truncating them; growing merges
wrapped groups back upward. The alternate buffer is excluded via an
explicit hasScrollback: false flag, matching xterm.js, so full-screen
applications keep repainting themselves.
Also fixes an independent latent bug: when the buffer was at capacity
and the row count shrank, CircularList.Resize ran before any trimming
and kept the oldest lines, silently discarding the bottom of the live
screen including the cursor row. Resize now trims from the top
(raising Trimmed) before shrinking capacity.
Ported from xterm.js, (c) The xterm.js authors, MIT.
---
FIXES.md | 36 ++
.../Buffer/BufferReflowTests.cs | 112 ++++++
src/XTerm.NET.Tests/Buffer/BufferTests.cs | 370 ++++++++++++++++++
src/XTerm.NET/Buffer/BufferLine.cs | 43 +-
src/XTerm.NET/Buffer/BufferReflow.cs | 254 ++++++++++++
src/XTerm.NET/Buffer/CircularList.cs | 10 +
src/XTerm.NET/Buffer/TerminalBuffer.cs | 318 ++++++++++++++-
src/XTerm.NET/Terminal.cs | 2 +-
8 files changed, 1123 insertions(+), 22 deletions(-)
create mode 100644 src/XTerm.NET.Tests/Buffer/BufferReflowTests.cs
create mode 100644 src/XTerm.NET/Buffer/BufferReflow.cs
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;