diff --git a/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs b/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs
index 59172cf..473b0d2 100644
--- a/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs
+++ b/src/XTerm.NET.Tests/ShellIntegrationMarkAnchorTests.cs
@@ -104,6 +104,55 @@ public void Erasing_the_line_leaves_the_mark_alone()
Assert.Equal(ShellIntegrationMark.PromptStart, MarksOn(t, 0)[0].Kind);
}
+ ///
+ /// What pwsh's Clear-Host sends, and what tput clear sends on any modern terminfo: the
+ /// scrollback discarded, the cursor homed, the screen blanked in place. The rows come back
+ /// empty and so must their marks, or a gutter drawn from them shows bars beside nothing.
+ ///
+ [Fact]
+ public void Clearing_the_screen_drops_the_marks_on_the_rows_it_blanks()
+ {
+ var t = Fresh();
+ t.Write(Mark("A") + "$ " + Mark("B") + "ls\r\n" + Mark("C") + "file\r\n" + Mark("D;0"));
+ Assert.NotEmpty(MarksOn(t, 0));
+ Assert.NotEmpty(MarksOn(t, 1));
+ Assert.NotEmpty(MarksOn(t, 2));
+
+ t.Write($"{Esc}[3J{Esc}[H{Esc}[2J");
+
+ for (var row = 0; row < 5; row++)
+ Assert.Empty(MarksOn(t, row));
+ }
+
+ ///
+ /// ED 0 erases the cursor row through EL, and EL keeps marks: a shell that homes the cursor,
+ /// erases below and then prints its prompt -- zsh redrawing -- keeps the prompt's mark. The
+ /// rows below were erased whole and lose theirs.
+ ///
+ [Fact]
+ public void Erasing_below_keeps_the_cursor_rows_mark_and_drops_the_rest()
+ {
+ var t = Fresh();
+ t.Write(Mark("A") + "$ ls\r\n" + Mark("C") + "file\r\n" + Mark("D;0"));
+ t.Write($"{Esc}[1;1H{Esc}[J");
+
+ Assert.Equal(ShellIntegrationMark.PromptStart, Assert.Single(MarksOn(t, 0)).Kind);
+ Assert.Empty(MarksOn(t, 1));
+ Assert.Empty(MarksOn(t, 2));
+ }
+
+ /// A selective erase exists to leave protected text standing; the marks stand with it.
+ [Fact]
+ public void A_selective_screen_erase_leaves_the_marks_alone()
+ {
+ var t = Fresh();
+ t.Write(Mark("A") + "$ ls\r\n" + Mark("C") + "file\r\n");
+ t.Write($"{Esc}[?2J");
+
+ Assert.Single(MarksOn(t, 0));
+ Assert.Single(MarksOn(t, 1));
+ }
+
[Fact]
public void Reflow_moves_a_mark_to_the_row_and_column_owning_its_position()
{
diff --git a/src/XTerm.NET/Buffer/BufferLine.cs b/src/XTerm.NET/Buffer/BufferLine.cs
index 7ef6afe..b1b5d4e 100644
--- a/src/XTerm.NET/Buffer/BufferLine.cs
+++ b/src/XTerm.NET/Buffer/BufferLine.cs
@@ -487,7 +487,11 @@ internal void AddMark(LineMark mark)
_marks.Add(mark);
}
- /// Drops every mark. Only line reuse does this; see .
+ ///
+ /// Drops every mark. Two things do this: line reuse, see , and a
+ /// whole-screen clear -- ED, DECCOLM, the alternate screen blanked on the way out -- erasing
+ /// the line in full. Reflow drops and re-anchors them too, but that is a move, not a loss.
+ ///
internal void ClearMarks() => _marks = null;
/// Whether this line carries any OSC 8 link span.
diff --git a/src/XTerm.NET/InputHandler.Protection.cs b/src/XTerm.NET/InputHandler.Protection.cs
index c12e54c..897c617 100644
--- a/src/XTerm.NET/InputHandler.Protection.cs
+++ b/src/XTerm.NET/InputHandler.Protection.cs
@@ -112,7 +112,7 @@ private void EraseLineCells(BufferLine? line, int start, int end, bool selective
line.Fill(blank, start, end);
if (wholeLine)
- line.LineAttribute = Buffer.LineAttribute.Normal;
+ ClearedInFull(line);
return;
}
@@ -136,6 +136,31 @@ private void EraseLineCells(BufferLine? line, int start, int end, bool selective
}
if (wholeLine && !survived)
- line.LineAttribute = Buffer.LineAttribute.Normal;
+ ClearedInFull(line);
+ }
+
+ ///
+ /// What a line loses when a display erase has cleared it whole: its double-size attribute,
+ /// and its shell integration marks. Reached from ED, and from
+ /// -- DECCOLM's clear, and the alternate screen blanked on the way out -- which are the
+ /// other two acts that empty every row.
+ ///
+ ///
+ /// The marks are the deliberate exception to "a mark survives the erasing of the cells it sits
+ /// among". That rule exists for EL, which shells use to redraw a prompt they have just marked.
+ /// A display erase is a different act: clear is CSI 3 J, CUP, CSI 2 J -- the scrollback
+ /// discarded and the screen blanked in place -- and the rows that come back blank still carried
+ /// the marks of the commands that were on them. A host drawing a gutter from those marks kept
+ /// painting prompt and exit bars beside empty rows. Kitty and Ghostty both drop a row's prompt
+ /// flag when ED clears it; a shell that clears the screen from inside its own prompt loses that
+ /// one mark under all three, and prints the prompt it marked onto a screen it just emptied
+ /// either way. The cursor row of ED 0 and ED 1 goes through EL and keeps its marks, so a shell
+ /// that homes the cursor and erases below -- zsh, after a redraw -- keeps the mark of the
+ /// prompt it is about to print there.
+ ///
+ private static void ClearedInFull(BufferLine line)
+ {
+ line.LineAttribute = Buffer.LineAttribute.Normal;
+ line.ClearMarks();
}
}