From b4442e0d3731e4a222cdc4f29e507583caf4d326 Mon Sep 17 00:00:00 2001 From: Joel Christner Date: Sun, 31 May 2026 15:18:15 -0700 Subject: [PATCH 1/2] Fix origin mode cursor positioning --- src/XTerm.NET.Tests/InputHandlerTests.cs | 79 ++++++++++++++++++++++++ src/XTerm.NET.Tests/ModeHandlingTests.cs | 18 ++++++ src/XTerm.NET/InputHandler.cs | 34 ++++++---- 3 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/XTerm.NET.Tests/InputHandlerTests.cs b/src/XTerm.NET.Tests/InputHandlerTests.cs index 4fd205f..0776e91 100644 --- a/src/XTerm.NET.Tests/InputHandlerTests.cs +++ b/src/XTerm.NET.Tests/InputHandlerTests.cs @@ -150,6 +150,26 @@ public void HandleCsi_CursorPosition_SetsCursorPosition() Assert.Equal(9, terminal.Buffer.Y); // 10 - 1 (1-based to 0-based) } + [Fact] + public void HandleCsi_CursorPosition_UsesScrollRegionOriginInOriginMode() + { + // Arrange + var terminal = CreateTerminal(); + var handler = new InputHandler(terminal); + terminal.Buffer.SetScrollRegion(4, 19); + terminal.OriginMode = true; + var params_ = new Params(); + params_.AddParam(3); + params_.AddParam(20); + + // Act + handler.HandleCsi("H", params_); + + // Assert + Assert.Equal(19, terminal.Buffer.X); + Assert.Equal(6, terminal.Buffer.Y); + } + [Fact] public void HandleCsi_EraseInDisplay_ClearBelow() { @@ -344,6 +364,45 @@ public void HandleCsi_SetScrollRegion_SetsRegion() Assert.Equal(19, terminal.Buffer.ScrollBottom); // 20 - 1 (1-based to 0-based) } + [Fact] + public void HandleCsi_SetScrollRegion_MovesCursorHome() + { + // Arrange + var terminal = CreateTerminal(); + var handler = new InputHandler(terminal); + terminal.Buffer.SetCursor(10, 10); + var params_ = new Params(); + params_.AddParam(5); + params_.AddParam(20); + + // Act + handler.HandleCsi("r", params_); + + // Assert + Assert.Equal(0, terminal.Buffer.X); + Assert.Equal(0, terminal.Buffer.Y); + } + + [Fact] + public void HandleCsi_SetScrollRegion_MovesCursorToTopMarginInOriginMode() + { + // Arrange + var terminal = CreateTerminal(); + var handler = new InputHandler(terminal); + terminal.OriginMode = true; + terminal.Buffer.SetCursor(10, 10); + var params_ = new Params(); + params_.AddParam(5); + params_.AddParam(20); + + // Act + handler.HandleCsi("r", params_); + + // Assert + Assert.Equal(0, terminal.Buffer.X); + Assert.Equal(4, terminal.Buffer.Y); + } + [Fact] public void HandleEsc_Index_MovesDownOrScrolls() { @@ -1115,6 +1174,26 @@ public void HandleCsi_LinePositionAbsolute_MovesToRow() Assert.Equal(11, terminal.Buffer.Y); // 12 - 1 = 11 (0-based) } + [Fact] + public void HandleCsi_LinePositionAbsolute_UsesScrollRegionOriginInOriginMode() + { + // Arrange + var terminal = CreateTerminal(); + var handler = new InputHandler(terminal); + terminal.Buffer.SetScrollRegion(4, 19); + terminal.OriginMode = true; + terminal.Buffer.SetCursor(15, 5); + var params_ = new Params(); + params_.AddParam(3); + + // Act + handler.HandleCsi("d", params_); + + // Assert + Assert.Equal(15, terminal.Buffer.X); + Assert.Equal(6, terminal.Buffer.Y); + } + [Fact] public void HandleCsi_SaveRestoreCursorAnsi_SavesAndRestoresPosition() { diff --git a/src/XTerm.NET.Tests/ModeHandlingTests.cs b/src/XTerm.NET.Tests/ModeHandlingTests.cs index e4c0d89..3b6d132 100644 --- a/src/XTerm.NET.Tests/ModeHandlingTests.cs +++ b/src/XTerm.NET.Tests/ModeHandlingTests.cs @@ -170,6 +170,24 @@ public void SetMode_OriginMode_EnablesOriginMode() Assert.Equal(0, terminal.Buffer.Y); } + [Fact] + public void SetMode_OriginMode_MovesCursorToTopMargin() + { + // Arrange + var terminal = CreateTerminal(); + terminal.Buffer.SetScrollRegion(4, 19); + terminal.Buffer.SetCursor(10, 10); + Assert.False(terminal.OriginMode); + + // Act - DEC private mode + terminal.Write($"\x1B[?{(int)TerminalMode.Origin}h"); + + // Assert + Assert.True(terminal.OriginMode); + Assert.Equal(0, terminal.Buffer.X); + Assert.Equal(4, terminal.Buffer.Y); + } + [Fact] public void ResetMode_OriginMode_DisablesOriginMode() { diff --git a/src/XTerm.NET/InputHandler.cs b/src/XTerm.NET/InputHandler.cs index 5e2daf7..8c9e2d3 100644 --- a/src/XTerm.NET/InputHandler.cs +++ b/src/XTerm.NET/InputHandler.cs @@ -847,6 +847,7 @@ private void CursorPosition(Params parameters) { var row = Math.Max(parameters.GetParam(0, 1), 1) - 1; var col = Math.Max(parameters.GetParam(1, 1), 1) - 1; + row = GetAbsoluteCursorRow(row); _buffer.SetCursor(col, row); } @@ -1009,17 +1010,7 @@ private void LinePositionAbsolute(Params parameters) { // VPA - Line Position Absolute (CSI d) var row = Math.Max(parameters.GetParam(0, 1), 1) - 1; - - // Respect origin mode - if (_terminal.OriginMode) - { - row = Math.Clamp(row, _buffer.ScrollTop, _buffer.ScrollBottom); - } - else - { - row = Math.Clamp(row, 0, _terminal.Rows - 1); - } - + row = GetAbsoluteCursorRow(row); _buffer.SetCursor(_buffer.X, row); } @@ -1283,6 +1274,23 @@ private void SetScrollRegion(Params parameters) var top = Math.Max(parameters.GetParam(0, 1), 1) - 1; var bottom = Math.Max(parameters.GetParam(1, _terminal.Rows), 1) - 1; _buffer.SetScrollRegion(top, bottom); + MoveCursorToHome(); + } + + private int GetAbsoluteCursorRow(int row) + { + if (_terminal.OriginMode) + { + return Math.Clamp(_buffer.ScrollTop + row, _buffer.ScrollTop, _buffer.ScrollBottom); + } + + return Math.Clamp(row, 0, _terminal.Rows - 1); + } + + private void MoveCursorToHome() + { + var row = _terminal.OriginMode ? _buffer.ScrollTop : 0; + _buffer.SetCursor(0, row); } private void WindowManipulation(Params parameters) @@ -1540,7 +1548,7 @@ private void SetCSIMode(int mode, bool isPrivate) case TerminalMode.Origin: _terminal.OriginMode = true; - _buffer.SetCursor(0, 0); + MoveCursorToHome(); break; case TerminalMode.Wraparound: @@ -1729,7 +1737,7 @@ private void ResetCSIMode(int mode, bool isPrivate) case TerminalMode.Origin: _terminal.OriginMode = false; - _buffer.SetCursor(0, 0); + MoveCursorToHome(); break; case TerminalMode.Wraparound: From e8977b579fbfae0b6001a69db7a2918c3b1acee3 Mon Sep 17 00:00:00 2001 From: Joel Christner Date: Sun, 31 May 2026 18:12:04 -0700 Subject: [PATCH 2/2] Document origin mode fixes --- FIXES.md | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 FIXES.md diff --git a/FIXES.md b/FIXES.md new file mode 100644 index 0000000..26723e4 --- /dev/null +++ b/FIXES.md @@ -0,0 +1,112 @@ +# Origin mode and scroll region fixes + +## Summary + +This branch fixes DEC origin-mode cursor positioning with scroll regions. These changes are core terminal emulator behavior and are not specific to Termrig, Avalonia, ConPTY, or any host renderer. + +The fixed behavior is: + +- `DECSTBM` / `CSI t;b r` moves the cursor to home after setting the scroll region. +- `CUP` / `CSI row;col H` and `HVP` / `CSI row;col f` treat row coordinates as relative to the scroll region when `DECOM` / origin mode is enabled. +- `VPA` / `CSI row d` applies the same origin-mode row translation. +- enabling origin mode moves the cursor to the top margin of the scroll region; disabling origin mode moves the cursor to absolute home. + +## Why + +Full-screen and prompt-oriented terminal applications often reserve a bottom input or status row by setting a scroll region for the output area. They then use origin-mode cursor addressing inside that region. + +If the emulator treats those row coordinates as absolute screen rows, application output can be written outside the intended scroll region. In real-world terminal UIs this can leave stale prompt/status rows in scrollback or place rewritten content on the wrong line. + +## Files changed + +- `src/XTerm.NET/InputHandler.cs` + - Added shared row translation for origin-mode cursor addressing. + - Applied that translation to `CUP` / `HVP` and `VPA`. + - Homed the cursor after `DECSTBM`. + - Homed to the top margin when origin mode is enabled. +- `src/XTerm.NET.Tests/InputHandlerTests.cs` + - Added regression coverage for scroll-region cursor homing. + - Added regression coverage for origin-relative `CUP` / `HVP`. + - Added regression coverage for origin-relative `VPA`. +- `src/XTerm.NET.Tests/ModeHandlingTests.cs` + - Added regression coverage for enabling origin mode with a non-zero top margin. + +## Minimal reproductions + +### Scroll region homes the cursor + +```csharp +var terminal = new Terminal(new TerminalOptions { Cols = 20, Rows = 5 }); +var handler = new InputHandler(terminal); +terminal.Buffer.SetCursor(10, 10); + +var parameters = new Params(); +parameters.AddParam(2); +parameters.AddParam(4); +handler.HandleCsi("r", parameters); + +Assert.Equal(0, terminal.Buffer.X); +Assert.Equal(0, terminal.Buffer.Y); +``` + +### Origin-mode `CUP` is relative to the scroll region + +```csharp +var terminal = new Terminal(new TerminalOptions { Cols = 20, Rows = 5 }); +var handler = new InputHandler(terminal); +terminal.Buffer.SetScrollRegion(1, 3); +terminal.OriginMode = true; + +var parameters = new Params(); +parameters.AddParam(3); +parameters.AddParam(20); +handler.HandleCsi("H", parameters); + +Assert.Equal(19, terminal.Buffer.X); +Assert.Equal(3, terminal.Buffer.Y); +``` + +### Origin-mode `VPA` is relative to the scroll region + +```csharp +var terminal = new Terminal(new TerminalOptions { Cols = 20, Rows = 5 }); +var handler = new InputHandler(terminal); +terminal.Buffer.SetScrollRegion(1, 3); +terminal.OriginMode = true; +terminal.Buffer.SetCursor(10, 1); + +var parameters = new Params(); +parameters.AddParam(3); +handler.HandleCsi("d", parameters); + +Assert.Equal(10, terminal.Buffer.X); +Assert.Equal(3, terminal.Buffer.Y); +``` + +## Not included + +This branch intentionally does not include: + +- host-rendering changes +- PTY or ConPTY line-ending policy +- Avalonia integration changes +- Termrig-specific output normalization +- Docker Compose cell-width fixes from the earlier Docker progress branch + +Those are separate concerns. This branch is limited to standard VT scroll-region and origin-mode semantics in XTerm.NET. + +## Validation + +Run from this repository root: + +```powershell +dotnet test src/XTerm.NET.slnx --no-restore +``` + +Result on this branch: + +```text +Passed: 589 +Failed: 0 +Skipped: 0 +```