Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 112 additions & 0 deletions FIXES.md
Original file line number Diff line number Diff line change
@@ -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
```
79 changes: 79 additions & 0 deletions src/XTerm.NET.Tests/InputHandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -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()
{
Expand Down
18 changes: 18 additions & 0 deletions src/XTerm.NET.Tests/ModeHandlingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
34 changes: 21 additions & 13 deletions src/XTerm.NET/InputHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down