Skip to content

Commit

Permalink
Handle cut off lines on Windows
Browse files Browse the repository at this point in the history
Before this change, on Windows, the rightmost column of the screen would
be cut off and not visible.

With this change in place, that column should now be visible.

Fixes #250.

Ref:
microsoft/terminal#18115 (comment)
  • Loading branch information
walles committed Oct 30, 2024
1 parent fc59ade commit ba213b8
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 21 deletions.
29 changes: 18 additions & 11 deletions twin/screen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
45 changes: 35 additions & 10 deletions twin/screen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := ""
Expand All @@ -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
Expand All @@ -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 := ""
Expand All @@ -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 := ""
Expand All @@ -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 := ""
Expand All @@ -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
Expand All @@ -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 := ""
Expand All @@ -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 := ""
Expand All @@ -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,
Expand All @@ -236,14 +236,39 @@ func TestMultiCharHyperlink(t *testing.T) {
},
}

rendered, count := renderLine(row, ColorCount16)
rendered, count := renderLine(row, 33, ColorCount16)
assert.Equal(t, count, 3)

assert.Equal(t,
strings.ReplaceAll(rendered, "", "ESC"),
`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.
//
Expand Down

0 comments on commit ba213b8

Please sign in to comment.