From 7271110d72ee22ae5b0d2d516ee85a44821cbfe5 Mon Sep 17 00:00:00 2001 From: Joel Christner Date: Mon, 1 Jun 2026 21:45:39 -0700 Subject: [PATCH 1/3] Preserve rows below partial scroll regions --- src/XTerm.NET.Tests/Buffer/BufferTests.cs | 29 +++++++++++++++++++++++ src/XTerm.NET/Buffer/TerminalBuffer.cs | 9 ++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/XTerm.NET.Tests/Buffer/BufferTests.cs b/src/XTerm.NET.Tests/Buffer/BufferTests.cs index 5db96af..a524fac 100644 --- a/src/XTerm.NET.Tests/Buffer/BufferTests.cs +++ b/src/XTerm.NET.Tests/Buffer/BufferTests.cs @@ -989,6 +989,28 @@ public void AlternateBuffer_NoScrollback_ScrollUpAtTopOfScreen_YBaseRemainsZero( Assert.Equal(0, buffer.YDisp); } + [Fact] + public void ScrollUp_TopAnchoredPartialRegion_PreservesRowsBelowRegion() + { + var buffer = new TerminalBuffer(10, 5, 100); + + SetCell(buffer, 0, "A"); + SetCell(buffer, 1, "B"); + SetCell(buffer, 2, "C"); + SetCell(buffer, 3, "D"); + SetCell(buffer, 4, ">"); + + buffer.SetScrollRegion(0, 3); + buffer.ScrollUp(1); + + Assert.Equal(0, buffer.YBase); + Assert.Equal("B", buffer.GetLine(0)?[0].Content); + Assert.Equal("C", buffer.GetLine(1)?[0].Content); + Assert.Equal("D", buffer.GetLine(2)?[0].Content); + Assert.True(buffer.GetLine(3)?[0].IsSpace()); + Assert.Equal(">", buffer.GetLine(4)?[0].Content); + } + [Fact] public void AlternateBuffer_NoScrollback_MultipleScrollOperations_YBaseRemainsZero() { @@ -1092,5 +1114,12 @@ public void NormalBuffer_WithScrollback_ScrollUpAtTop_YBaseIncrements() Assert.Equal(5, buffer.YDisp); } + private static void SetCell(TerminalBuffer buffer, int row, string content) + { + var line = buffer.GetLine(row); + var cell = new BufferCell { Content = content, Width = 1 }; + line?.SetCell(0, ref cell); + } + #endregion } diff --git a/src/XTerm.NET/Buffer/TerminalBuffer.cs b/src/XTerm.NET/Buffer/TerminalBuffer.cs index 412c07c..ce5da2e 100644 --- a/src/XTerm.NET/Buffer/TerminalBuffer.cs +++ b/src/XTerm.NET/Buffer/TerminalBuffer.cs @@ -140,11 +140,10 @@ public void ScrollUp(int lines, bool isWrapped = false) // Create a new blank line that will be inserted at the bottom of the scroll region var newLine = GetBlankLine(AttributeData.Default, isWrapped); - // Only use the scrollback path if: - // 1. Scroll region starts at top (_scrollTop == 0) - // 2. We actually have scrollback capacity (MaxLength > _rows) - // For alternate buffer (no scrollback), always use the in-place scroll logic. - if (_scrollTop == 0 && _lines.MaxLength > _rows) + // Only the full-screen scroll region contributes to scrollback. + // Top-anchored partial regions reserve rows below the margin and + // must scroll in place so prompts/status rows are not promoted. + if (_scrollTop == 0 && _scrollBottom == _rows - 1 && _lines.MaxLength > _rows) { // When scrollTop is 0, the top line goes into scrollback. // In xterm.js: push new line first, then increment yBase and yDisp. From 2532dcecbb96a5e5c81177f99a0203403a751ae7 Mon Sep 17 00:00:00 2001 From: Joel Christner Date: Mon, 1 Jun 2026 22:05:54 -0700 Subject: [PATCH 2/3] Use active coordinates for line insert/delete --- src/XTerm.NET.Tests/Buffer/BufferTests.cs | 49 +++++++++++++++++++++++ src/XTerm.NET/InputHandler.cs | 4 +- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/XTerm.NET.Tests/Buffer/BufferTests.cs b/src/XTerm.NET.Tests/Buffer/BufferTests.cs index a524fac..c0c0a37 100644 --- a/src/XTerm.NET.Tests/Buffer/BufferTests.cs +++ b/src/XTerm.NET.Tests/Buffer/BufferTests.cs @@ -1,5 +1,6 @@ using XTerm.Buffer; using XTerm.Common; +using XTerm.Options; namespace XTerm.Tests.Buffer; @@ -1011,6 +1012,54 @@ public void ScrollUp_TopAnchoredPartialRegion_PreservesRowsBelowRegion() Assert.Equal(">", buffer.GetLine(4)?[0].Content); } + [Fact] + public void InsertLines_WithScrollback_UsesActiveBufferCoordinates() + { + var terminal = new Terminal(new TerminalOptions { Cols = 10, Rows = 5, Scrollback = 100 }); + + terminal.Write("s1\r\ns2\r\ns3\r\ns4\r\ns5\r\n"); + var yBase = terminal.Buffer.YBase; + + SetCell(terminal.Buffer, yBase + 0, "A"); + SetCell(terminal.Buffer, yBase + 1, "B"); + SetCell(terminal.Buffer, yBase + 2, "C"); + SetCell(terminal.Buffer, yBase + 3, "D"); + SetCell(terminal.Buffer, yBase + 4, ">"); + + terminal.Write("\x1b[1;4r\x1b[1;1H\x1b[1L"); + + Assert.Equal(yBase, terminal.Buffer.YBase); + Assert.True(terminal.Buffer.GetLine(yBase + 0)?[0].IsSpace()); + Assert.Equal("A", terminal.Buffer.GetLine(yBase + 1)?[0].Content); + Assert.Equal("B", terminal.Buffer.GetLine(yBase + 2)?[0].Content); + Assert.Equal("C", terminal.Buffer.GetLine(yBase + 3)?[0].Content); + Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content); + } + + [Fact] + public void DeleteLines_WithScrollback_UsesActiveBufferCoordinates() + { + var terminal = new Terminal(new TerminalOptions { Cols = 10, Rows = 5, Scrollback = 100 }); + + terminal.Write("s1\r\ns2\r\ns3\r\ns4\r\ns5\r\n"); + var yBase = terminal.Buffer.YBase; + + SetCell(terminal.Buffer, yBase + 0, "A"); + SetCell(terminal.Buffer, yBase + 1, "B"); + SetCell(terminal.Buffer, yBase + 2, "C"); + SetCell(terminal.Buffer, yBase + 3, "D"); + SetCell(terminal.Buffer, yBase + 4, ">"); + + terminal.Write("\x1b[1;4r\x1b[1;1H\x1b[1M"); + + Assert.Equal(yBase, terminal.Buffer.YBase); + Assert.Equal("B", terminal.Buffer.GetLine(yBase + 0)?[0].Content); + Assert.Equal("C", terminal.Buffer.GetLine(yBase + 1)?[0].Content); + Assert.Equal("D", terminal.Buffer.GetLine(yBase + 2)?[0].Content); + Assert.True(terminal.Buffer.GetLine(yBase + 3)?[0].IsSpace()); + Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content); + } + [Fact] public void AlternateBuffer_NoScrollback_MultipleScrollOperations_YBaseRemainsZero() { diff --git a/src/XTerm.NET/InputHandler.cs b/src/XTerm.NET/InputHandler.cs index 5e2daf7..1143383 100644 --- a/src/XTerm.NET/InputHandler.cs +++ b/src/XTerm.NET/InputHandler.cs @@ -915,7 +915,7 @@ private void InsertLines(Params parameters) for (int i = 0; i < count; i++) { - _buffer.Lines.Splice(_buffer.ScrollBottom, 1); + _buffer.Lines.Splice(_buffer.YBase + _buffer.ScrollBottom, 1); _buffer.Lines.Splice(_buffer.Y + _buffer.YBase, 0, _buffer.GetBlankLine(_curAttr)); } @@ -928,7 +928,7 @@ private void DeleteLines(Params parameters) for (int i = 0; i < count; i++) { _buffer.Lines.Splice(_buffer.Y + _buffer.YBase, 1); - _buffer.Lines.Splice(_buffer.ScrollBottom, 0, + _buffer.Lines.Splice(_buffer.YBase + _buffer.ScrollBottom, 0, _buffer.GetBlankLine(_curAttr)); } } From 6d28d7195b71cec7301dfcc050b6ace2f29fbb2d Mon Sep 17 00:00:00 2001 From: Joel Christner Date: Mon, 1 Jun 2026 22:41:27 -0700 Subject: [PATCH 3/3] Ignore delete line outside scroll region --- src/XTerm.NET.Tests/Buffer/BufferTests.cs | 24 +++++++++++++++++++++++ src/XTerm.NET/InputHandler.cs | 3 +++ 2 files changed, 27 insertions(+) diff --git a/src/XTerm.NET.Tests/Buffer/BufferTests.cs b/src/XTerm.NET.Tests/Buffer/BufferTests.cs index c0c0a37..ba1d539 100644 --- a/src/XTerm.NET.Tests/Buffer/BufferTests.cs +++ b/src/XTerm.NET.Tests/Buffer/BufferTests.cs @@ -1060,6 +1060,30 @@ public void DeleteLines_WithScrollback_UsesActiveBufferCoordinates() Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content); } + [Fact] + public void DeleteLines_OutsideScrollRegion_PreservesReservedPromptRow() + { + var terminal = new Terminal(new TerminalOptions { Cols = 10, Rows = 5, Scrollback = 100 }); + + terminal.Write("s1\r\ns2\r\ns3\r\ns4\r\ns5\r\n"); + var yBase = terminal.Buffer.YBase; + + SetCell(terminal.Buffer, yBase + 0, "A"); + SetCell(terminal.Buffer, yBase + 1, "B"); + SetCell(terminal.Buffer, yBase + 2, "C"); + SetCell(terminal.Buffer, yBase + 3, "D"); + SetCell(terminal.Buffer, yBase + 4, ">"); + + terminal.Write("\x1b[1;4r\x1b[5;1H\x1b[1M"); + + Assert.Equal(yBase, terminal.Buffer.YBase); + Assert.Equal("A", terminal.Buffer.GetLine(yBase + 0)?[0].Content); + Assert.Equal("B", terminal.Buffer.GetLine(yBase + 1)?[0].Content); + Assert.Equal("C", terminal.Buffer.GetLine(yBase + 2)?[0].Content); + Assert.Equal("D", terminal.Buffer.GetLine(yBase + 3)?[0].Content); + Assert.Equal(">", terminal.Buffer.GetLine(yBase + 4)?[0].Content); + } + [Fact] public void AlternateBuffer_NoScrollback_MultipleScrollOperations_YBaseRemainsZero() { diff --git a/src/XTerm.NET/InputHandler.cs b/src/XTerm.NET/InputHandler.cs index 1143383..c811661 100644 --- a/src/XTerm.NET/InputHandler.cs +++ b/src/XTerm.NET/InputHandler.cs @@ -924,6 +924,9 @@ private void InsertLines(Params parameters) private void DeleteLines(Params parameters) { var count = Math.Max(parameters.GetParam(0, 1), 1); + // Only works in scroll region + if (_buffer.Y < _buffer.ScrollTop || _buffer.Y > _buffer.ScrollBottom) + return; for (int i = 0; i < count; i++) {