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
91 changes: 91 additions & 0 deletions src/XTerm.NET.Tests/LineExitedViewportTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using XTerm;
using XTerm.Buffer;
using XTerm.Common;
using XTerm.Events;
using XTerm.Options;

namespace XTerm.Tests;

public class LineExitedViewportTests
{
[Fact]
public void FullScreenScroll_RaisesBeforeAlternateBufferLineIsRecycled()
{
var terminal = new Terminal(new TerminalOptions { Rows = 2, Cols = 8, Scrollback = 1 });
terminal.SwitchToAltBuffer();
SetCell(terminal.Buffer.Lines[0]!, "old");

string? captured = null;
BufferType? buffer = null;
LineExitReason? reason = null;
terminal.LineExitedViewport += (_, args) =>
{
captured = args.Line.TranslateToString(trimRight: true);
buffer = args.Buffer;
reason = args.Reason;
};

terminal.Buffer.ScrollUp(1);

Assert.Equal("old", captured);
Assert.Equal(BufferType.Alternate, buffer);
Assert.Equal(LineExitReason.Scrolled, reason);
}

[Fact]
public void PartialScrollRegion_RaisesForTheLineRemovedFromTheRegion()
{
var terminal = new Terminal(new TerminalOptions { Rows = 4, Cols = 8 });
terminal.Buffer.SetScrollRegion(1, 2);
SetCell(terminal.Buffer.Lines[1]!, "gone");

string? captured = null;
terminal.LineExitedViewport += (_, args) => captured = args.Line.TranslateToString(trimRight: true);

terminal.Buffer.ScrollUp(1);

Assert.Equal("gone", captured);
}

[Fact]
public void NarrowedMargins_DoNotRaiseBecauseNoWholeLineLeaves()
{
var terminal = new Terminal(new TerminalOptions { Rows = 3, Cols = 8 });
terminal.Buffer.SetLeftRightMargins(1, 6);
var count = 0;
terminal.LineExitedViewport += (_, _) => count++;

terminal.Buffer.ScrollUp(1);

Assert.Equal(0, count);
}

[Fact]
public void BufferSwitch_RaisesMeaningfulRowsBeforeBufferChanged()
{
var terminal = new Terminal(new TerminalOptions { Rows = 3, Cols = 8 });
SetCell(terminal.Buffer.Lines[0]!, "first");
var events = new List<string>();
TerminalEvents.LineExitedViewportEventArgs? exited = null;
terminal.LineExitedViewport += (_, args) =>
{
exited = args;
events.Add("exit");
};
terminal.BufferChanged += (_, _) => events.Add("changed");

terminal.SwitchToAltBuffer();

Assert.NotNull(exited);
Assert.Equal("first", exited.Line.TranslateToString(trimRight: true));
Assert.Equal(BufferType.Normal, exited.Buffer);
Assert.Equal(LineExitReason.BufferDeactivated, exited.Reason);
Assert.Equal(["exit", "changed"], events);
}

private static void SetCell(BufferLine line, string content)
{
var cell = new BufferCell(content, 1, AttributeData.Default);
line.SetCell(0, ref cell);
}
}
9 changes: 9 additions & 0 deletions src/XTerm.NET/Buffer/TerminalBuffer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,11 @@ public int ViewportY
/// </summary>
public event Action<int>? Trimmed;

/// <summary>
/// Fired synchronously before a line scrolls out of the active viewport and can be recycled.
/// </summary>
internal event Action<BufferLine>? LineExitedViewport;

/// <summary>
/// Whether scrolling reuses the scrollback line it is about to discard instead of allocating a
/// new one. On by default. Turn it off if a consumer holds <see cref="BufferLine"/> references
Expand Down Expand Up @@ -256,6 +261,10 @@ public void ScrollUp(int lines, bool isWrapped = false)

for (int i = 0; i < lines; i++)
{
var exitingLine = _lines[_yBase + _scrollTop];
if (exitingLine is not null)
LineExitedViewport?.Invoke(exitingLine);

BufferLine newLine;

// Only the full-screen scroll region contributes to scrollback.
Expand Down
9 changes: 9 additions & 0 deletions src/XTerm.NET/Common/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ public enum BufferType
Alternate
}

/// <summary>
/// Identifies why a terminal line left the active viewport.
/// </summary>
public enum LineExitReason
{
Scrolled,
BufferDeactivated
}

/// <summary>
/// Cursor style for the terminal.
/// </summary>
Expand Down
18 changes: 18 additions & 0 deletions src/XTerm.NET/Events/TerminalEvents.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using XTerm.Buffer;
using XTerm.Common;

namespace XTerm.Events;
Expand Down Expand Up @@ -204,6 +205,23 @@ public LineFeedEventArgs(string data)
}
}

/// <summary>
/// Line exit event - fired before a line leaves the active viewport.
/// </summary>
public class LineExitedViewportEventArgs : EventArgs
{
public BufferLine Line { get; }
public BufferType Buffer { get; }
public LineExitReason Reason { get; }

public LineExitedViewportEventArgs(BufferLine line, BufferType buffer, LineExitReason reason)
{
Line = line;
Buffer = buffer;
Reason = reason;
}
}

/// <summary>
/// Scroll event - fired when the terminal scrolls.
/// </summary>
Expand Down
32 changes: 32 additions & 0 deletions src/XTerm.NET/Terminal.cs
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,11 @@ internal bool TrySetUserVariable(string name, string value)
/// </summary>
public event EventHandler<TerminalEvents.LineFeedEventArgs>? LineFed;

/// <summary>
/// Fired before a line leaves the active viewport.
/// </summary>
public event EventHandler<TerminalEvents.LineExitedViewportEventArgs>? LineExitedViewport;

/// <summary>
/// Fired when the current directory changes.
/// </summary>
Expand Down Expand Up @@ -760,6 +765,8 @@ public Terminal(TerminalOptions? options = null)
// Initialize buffers
_normalBuffer = new Buffer.TerminalBuffer(Cols, Rows, Options.Scrollback);
_altBuffer = new Buffer.TerminalBuffer(Cols, Rows, 0, hasScrollback: false);
_normalBuffer.LineExitedViewport += line => RaiseLineExitedViewport(line, BufferType.Normal, LineExitReason.Scrolled);
_altBuffer.LineExitedViewport += line => RaiseLineExitedViewport(line, BufferType.Alternate, LineExitReason.Scrolled);
_buffer = _normalBuffer;
_usingAltBuffer = false;

Expand Down Expand Up @@ -2003,6 +2010,7 @@ public void SwitchToAltBuffer()
if (_statusLineActive)
SetActiveStatusDisplay(0);

RaiseBufferDeactivatedLines(_normalBuffer!);
var x = _buffer.X;
var y = _buffer.Y;
_buffer = _altBuffer!;
Expand Down Expand Up @@ -2039,6 +2047,7 @@ public void SwitchToNormalBuffer()
if (_statusLineActive)
SetActiveStatusDisplay(0);

RaiseBufferDeactivatedLines(_altBuffer!);
var x = _buffer.X;
var y = _buffer.Y;
_buffer = _normalBuffer!;
Expand Down Expand Up @@ -2142,6 +2151,28 @@ private void LineFeed()
LineFed?.Invoke(this, new TerminalEvents.LineFeedEventArgs("\n"));
}

/// <summary>Raises a synchronous snapshot opportunity before a row leaves the viewport.</summary>
private void RaiseLineExitedViewport(BufferLine line, BufferType buffer, LineExitReason reason)
{
LineExitedViewport?.Invoke(this, new TerminalEvents.LineExitedViewportEventArgs(line, buffer, reason));
}

/// <summary>Raises exit events for the meaningful rows of a buffer before deactivation.</summary>
private void RaiseBufferDeactivatedLines(Buffer.TerminalBuffer buffer)
{
var firstLine = buffer.BaseY;
var lastLine = Math.Min(firstLine + Rows, buffer.Lines.Length) - 1;
while (lastLine >= firstLine && buffer.Lines[lastLine]?.GetTrimmedLength() == 0)
lastLine--;

for (int i = firstLine; i <= lastLine; i++)
{
var line = buffer.Lines[i];
if (line is not null)
RaiseLineExitedViewport(line, ReferenceEquals(buffer, _altBuffer) ? BufferType.Alternate : BufferType.Normal, LineExitReason.BufferDeactivated);
}
}

/// <summary>Whether <see cref="Dispose"/> has run. A disposed terminal accepts no writes.</summary>
private bool _disposed;

Expand Down Expand Up @@ -2198,6 +2229,7 @@ public void Dispose()
Resized = null;
Scrolled = null;
LineFed = null;
LineExitedViewport = null;
DirectoryChanged = null;
HyperlinkChanged = null;
ShellIntegrationMarkReceived = null;
Expand Down