diff --git a/src/XTerm.NET.Tests/VtTestBehaviourTests.cs b/src/XTerm.NET.Tests/VtTestBehaviourTests.cs
index f28afb5..440dcf3 100644
--- a/src/XTerm.NET.Tests/VtTestBehaviourTests.cs
+++ b/src/XTerm.NET.Tests/VtTestBehaviourTests.cs
@@ -1,4 +1,4 @@
-using XTerm.Options;
+using XTerm.Options;
namespace XTerm.Tests;
@@ -370,6 +370,55 @@ public void A_full_erase_keeps_the_width_of_a_line_whose_text_survived()
Assert.Equal(XTerm.Buffer.LineAttribute.DoubleWidth, terminal.Buffer.Lines[0]!.LineAttribute);
}
+ ///
+ /// Erasing a line does not erase its DECDWL/DECDHL attribute. vttest menu 4.
+ ///
+ ///
+ /// The attribute belongs to the LINE, not to the text sitting on it, so clearing the text
+ /// does not end it. vttest's double-size screen is built to catch a terminal that thinks
+ /// otherwise: row 14 is the only line on that screen carrying an EL, and it arrives AFTER
+ /// the DECDHL and BEFORE the text -- ESC[14;2H ESC#6 ESC#5 ESC#4 ESC#3 ESC[2K, then the
+ /// line. Dropping the attribute there drew row 14 at normal size and left the double-height
+ /// bottom half on row 15 with no top half above it, which is what the screen looked like.
+ ///
+ /// ECH is the same case reached by a different sequence, and is here for the same reason:
+ /// at column 0 with a count covering the line it erases just as much as EL 2 does.
+ ///
+ [Theory]
+ [InlineData("[2K")] // EL, whole line
+ [InlineData("[0K")] // EL, to the right -- from column 0, the whole line
+ [InlineData("[1K")] // EL, to the left
+ [InlineData("[80X")] // ECH, wider than the line
+ public void Erasing_a_line_keeps_the_line_attribute(string erase)
+ {
+ var terminal = Sized(20, 3);
+
+ terminal.Write($"{Esc}[1;1H{Esc}#3{Esc}{erase}another such line");
+
+ Assert.Equal("another such line", terminal.GetLine(0));
+ Assert.Equal(XTerm.Buffer.LineAttribute.DoubleHeightTop, terminal.Buffer.Lines[0]!.LineAttribute);
+ }
+
+ ///
+ /// Erasing the DISPLAY does take the line attribute with it.
+ ///
+ ///
+ /// The other half of the pair above, and the reason the reset exists at all: vttest erases
+ /// the display between screens, so an attribute carried across ED doubled whatever the next
+ /// screen wrote. Pinned here because the EL fix works by narrowing the reset to ED, and a
+ /// narrowing is exactly the kind of change that overshoots.
+ ///
+ [Fact]
+ public void Erasing_the_display_clears_the_line_attribute()
+ {
+ var terminal = Sized(20, 3);
+
+ terminal.Write($"{Esc}[1;1H{Esc}#3doubled");
+ terminal.Write($"{Esc}[2J");
+
+ Assert.Equal(XTerm.Buffer.LineAttribute.Normal, terminal.Buffer.Lines[0]!.LineAttribute);
+ }
+
/// DECSCPP declines a width it does not define rather than rounding to one.
[Theory]
[InlineData(81)]
diff --git a/src/XTerm.NET/InputHandler.Csi.cs b/src/XTerm.NET/InputHandler.Csi.cs
index 13994fe..1e15eb9 100644
--- a/src/XTerm.NET/InputHandler.Csi.cs
+++ b/src/XTerm.NET/InputHandler.Csi.cs
@@ -1,4 +1,4 @@
-using System.Globalization;
+using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using XTerm.Buffer;
@@ -386,7 +386,8 @@ private void EraseInDisplay(Params parameters, bool selective = false)
EraseInLine(parameters, selective); // Current line from cursor
for (int i = _buffer.Y + 1; i < _terminal.Rows; i++)
{
- EraseLineCells(_buffer.Lines[_buffer.YBase + i], 0, _terminal.Cols, selective);
+ EraseLineCells(_buffer.Lines[_buffer.YBase + i], 0, _terminal.Cols, selective,
+ resetLineAttribute: true);
BreakWrapFromAbove(i);
if (hasBlocks)
EraseBlocksHangingOver(_buffer.YBase + i, 0, _terminal.Cols);
@@ -395,7 +396,8 @@ private void EraseInDisplay(Params parameters, bool selective = false)
case 1: // Erase above
for (int i = 0; i < _buffer.Y; i++)
{
- EraseLineCells(_buffer.Lines[_buffer.YBase + i], 0, _terminal.Cols, selective);
+ EraseLineCells(_buffer.Lines[_buffer.YBase + i], 0, _terminal.Cols, selective,
+ resetLineAttribute: true);
BreakWrapFromAbove(i);
if (hasBlocks)
EraseBlocksHangingOver(_buffer.YBase + i, 0, _terminal.Cols);
@@ -406,7 +408,8 @@ private void EraseInDisplay(Params parameters, bool selective = false)
case 2: // Erase all — the visible screen only; the scrollback is kept
for (int i = 0; i < _terminal.Rows; i++)
{
- EraseLineCells(_buffer.Lines[_buffer.YBase + i], 0, _terminal.Cols, selective);
+ EraseLineCells(_buffer.Lines[_buffer.YBase + i], 0, _terminal.Cols, selective,
+ resetLineAttribute: true);
BreakWrapFromAbove(i);
if (hasBlocks)
EraseBlocksHangingOver(_buffer.YBase + i, 0, _terminal.Cols);
diff --git a/src/XTerm.NET/InputHandler.Protection.cs b/src/XTerm.NET/InputHandler.Protection.cs
index 6468327..c12e54c 100644
--- a/src/XTerm.NET/InputHandler.Protection.cs
+++ b/src/XTerm.NET/InputHandler.Protection.cs
@@ -42,7 +42,8 @@ public partial class InputHandler
internal void EraseWholeScreen()
{
for (var row = 0; row < _terminal.Rows; row++)
- EraseLineCells(_buffer.Lines[_buffer.YBase + row], 0, _terminal.Cols, selective: false);
+ EraseLineCells(_buffer.Lines[_buffer.YBase + row], 0, _terminal.Cols, selective: false,
+ resetLineAttribute: true);
}
/// DECSCA. 1 protects what is written next; 0 and 2 stop protecting.
@@ -73,7 +74,12 @@ internal void StartProtectedArea()
/// , honouring whichever protection applies: guarded cells always
/// survive, and DECSCA-protected cells survive the SELECTIVE erases.
///
- private void EraseLineCells(BufferLine? line, int start, int end, bool selective)
+ ///
+ /// Whether an erase that takes the whole line may also take its DECDWL/DECDHL attribute.
+ /// Only ED asks for this; see the note on wholeLine below.
+ ///
+ private void EraseLineCells(BufferLine? line, int start, int end, bool selective,
+ bool resetLineAttribute = false)
{
if (line is null)
return;
@@ -81,18 +87,25 @@ private void EraseLineCells(BufferLine? line, int start, int end, bool selective
var blank = BufferCell.Space;
blank.Attributes = GetEraseAttributes();
- // A line erased in full goes back to single width. The attribute describes how the
- // line is DRAWN, and an erased line has nothing left to draw at double size -- so
- // carrying it forward only doubles whatever is written next. vttest's double-size
- // test made that visible: it erases the display between screens, so every screen
- // after it stayed doubled.
+ // A line erased in full by ED goes back to single width. The attribute describes how
+ // the line is DRAWN, and a line ED has cleared has nothing left to draw at double size
+ // -- so carrying it forward only doubles whatever is written next. vttest's double-size
+ // test made that visible: it erases the display between screens, so every screen after
+ // it stayed doubled.
+ //
+ // ED ONLY, which is what resetLineAttribute carries in. EL and ECH erase a line without
+ // ending it, and the attribute is a property of the line rather than of the text on it:
+ // DEC keeps it across both, and vttest's double-size test is built to catch a terminal
+ // that does not. It sets DECDHL on row 14, sends EL 2, and only then writes the text --
+ // so a terminal that dropped the attribute here drew that row at normal size, leaving
+ // the sheared bottom half on row 15 with no top half above it.
//
// Full and non-selective only, and only when nothing SURVIVED the erase. A partial
// erase leaves text that is still meant to be double; a selective erase exists to
// leave protected text standing; and under ISO protection a guarded cell survives
// even a plain erase, which is why this is decided after the walk below rather than
// before it -- resizing the line under surviving text is the same mistake in reverse.
- var wholeLine = !selective && start == 0 && end >= _terminal.Cols;
+ var wholeLine = resetLineAttribute && !selective && start == 0 && end >= _terminal.Cols;
if (!_protectionUsed || _protectionMode == ProtectionOff)
{