diff --git a/twin/screen.go b/twin/screen.go index c8d0a4f..791fc1f 100644 --- a/twin/screen.go +++ b/twin/screen.go @@ -684,8 +684,9 @@ func withoutHiddenRunes(runes []StyledRune) []StyledRune { } // Returns the rendered line, plus how many information carrying cells went into -// it -func renderLine(row []StyledRune, terminalColorCount ColorCount) (string, int) { +// it. The width is used to decide whether or not to clear to EOL at the end of +// the line. +func renderLine(row []StyledRune, width int, terminalColorCount ColorCount) (string, int) { row = withoutHiddenRunes(row) // Strip trailing whitespace @@ -727,24 +728,30 @@ func renderLine(row []StyledRune, terminalColorCount ColorCount) (string, int) { builder.WriteRune(runeToWrite) } - // Clear to end of line - // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences - builder.WriteString(StyleDefault.RenderUpdateFrom(lastStyle, terminalColorCount)) - builder.WriteString("\x1b[K") + if len(row) < width { + // Clear to end of line + // https://en.wikipedia.org/wiki/ANSI_escape_code#CSI_(Control_Sequence_Introducer)_sequences + // + // Note that we can't do this if we're one the last screen column: + // https://github.com/microsoft/terminal/issues/18115#issuecomment-2448054645 + builder.WriteString(StyleDefault.RenderUpdateFrom(lastStyle, terminalColorCount)) + builder.WriteString("\x1b[K") + } return builder.String(), len(row) } func (screen *UnixScreen) Show() { - _, height := screen.Size() - screen.showNLines(height, true) + width, height := screen.Size() + screen.showNLines(width, height, true) } func (screen *UnixScreen) ShowNLines(height int) { - screen.showNLines(height, false) + width, _ := screen.Size() + screen.showNLines(width, height, false) } -func (screen *UnixScreen) showNLines(height int, clearFirst bool) { +func (screen *UnixScreen) showNLines(width int, height int, clearFirst bool) { var builder strings.Builder if clearFirst { @@ -754,7 +761,7 @@ func (screen *UnixScreen) showNLines(height int, clearFirst bool) { } for row := 0; row < height; row++ { - rendered, lineLength := renderLine(screen.cells[row], screen.terminalColorCount) + rendered, lineLength := renderLine(screen.cells[row], width, screen.terminalColorCount) builder.WriteString(rendered) wasLastLine := row == (height - 1) diff --git a/twin/screen_test.go b/twin/screen_test.go index 39f3c83..f4ad0b4 100644 --- a/twin/screen_test.go +++ b/twin/screen_test.go @@ -65,7 +65,7 @@ func TestRenderLine(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 2) reset := "" reversed := "" @@ -80,7 +80,7 @@ func TestRenderLine(t *testing.T) { func TestRenderLineEmpty(t *testing.T) { row := []StyledRune{} - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 0) // All lines are expected to stand on their own, so we always need to clear @@ -96,7 +96,7 @@ func TestRenderLineLastReversed(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -114,7 +114,7 @@ func TestRenderLineLastNonSpace(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 1) reset := "" clearToEol := "" @@ -135,7 +135,7 @@ func TestRenderLineLastReversedPlusTrailingSpace(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -157,7 +157,7 @@ func TestRenderLineOnlyTrailingSpaces(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 0) // All lines are expected to stand on their own, so we always need to clear @@ -173,7 +173,7 @@ func TestRenderLineLastReversedSpaces(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 1) reset := "" reversed := "" @@ -190,7 +190,7 @@ func TestRenderLineNonPrintable(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 1) reset := "" white := "" @@ -211,7 +211,7 @@ func TestRenderHyperlinkAtEndOfLine(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 1) assert.Equal(t, @@ -236,7 +236,7 @@ func TestMultiCharHyperlink(t *testing.T) { }, } - rendered, count := renderLine(row, ColorCount16) + rendered, count := renderLine(row, 33, ColorCount16) assert.Equal(t, count, 3) assert.Equal(t, @@ -244,6 +244,31 @@ func TestMultiCharHyperlink(t *testing.T) { `ESC[mESC]8;;`+url+`ESC\-X-ESC]8;;ESC\ESC[K`) } +func TestRenderLineFullWidth(t *testing.T) { + row := []StyledRune{ + { + Rune: 'x', + }, + { + Rune: 'y', + }, + } + + rendered, count := renderLine(row, 2, ColorCount16) + assert.Equal(t, count, 2) + + assert.Equal(t, + strings.ReplaceAll(rendered, "", "ESC"), + "ESC[mxy", "Expected no clear-to-EOL at the end of a full-width line") + + rendered, count = renderLine(row, 3, ColorCount16) + assert.Equal(t, count, 2) + + assert.Equal(t, + strings.ReplaceAll(rendered, "", "ESC"), + "ESC[mxyESC[K", "Expected clear-to-EOL at the end of a full-width line") +} + // Test the most basic form of interruptability. Interrupting and sending a byte // should make the reader return EOF. //