Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(render): improve renderer; remove flickering #1132

Merged
merged 15 commits into from
Oct 30, 2024
Merged
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
8 changes: 4 additions & 4 deletions examples/simple/testdata/TestApp.golden
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[?25l[?2004hHi. This program will exit in 10 seconds.

To quit sooner press ctrl-c, or press ctrl-z to suspend...
Hi. This program will exit in 9 seconds.
[?25l[?2004hHi. This program will exit in 10 seconds.

To quit sooner press ctrl-c, or press ctrl-z to suspend...
Hi. This program will exit in 9 seconds.
[?2004l[?25h[?1002l[?1003l[?1006l
Expand Down
18 changes: 9 additions & 9 deletions screen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,47 +14,47 @@ func TestClearMsg(t *testing.T) {
{
name: "clear_screen",
cmds: []Cmd{ClearScreen},
expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[2J\x1b[1;1H\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "altscreen",
cmds: []Cmd{EnterAltScreen, ExitAltScreen},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\x1b[?1049l\x1b[?25l\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "altscreen_autoexit",
cmds: []Cmd{EnterAltScreen},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\rsuccess\r\n\x1b[2;0H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h",
expected: "\x1b[?25l\x1b[?2004h\x1b[?1049h\x1b[2J\x1b[1;1H\x1b[?25l\rsuccess\x1b[0K\r\n\x1b[0K\x1b[2;0H\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1049l\x1b[?25h",
},
{
name: "mouse_cellmotion",
cmds: []Cmd{EnableMouseCellMotion},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?1002h\x1b[?1006h\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "mouse_allmotion",
cmds: []Cmd{EnableMouseAllMotion},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "mouse_disable",
cmds: []Cmd{EnableMouseAllMotion, DisableMouse},
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?1003h\x1b[?1006h\x1b[?1002l\x1b[?1003l\x1b[?1006l\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "cursor_hide",
cmds: []Cmd{HideCursor},
expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "cursor_hideshow",
cmds: []Cmd{HideCursor, ShowCursor},
expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?25l\x1b[?25h\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
{
name: "bp_stop_start",
cmds: []Cmd{DisableBracketedPaste, EnableBracketedPaste},
expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\r\n\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
expected: "\x1b[?25l\x1b[?2004h\x1b[?2004l\x1b[?2004h\rsuccess\x1b[0K\r\n\x1b[0K\x1b[D\x1b[2K\r\x1b[?2004l\x1b[?25h\x1b[?1002l\x1b[?1003l\x1b[?1006l",
},
}

Expand Down
122 changes: 51 additions & 71 deletions standard_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,14 +161,20 @@ func (r *standardRenderer) flush() {
defer r.mtx.Unlock()

if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
// Nothing to do
// Nothing to do.
return
}

// Output buffer
// Output buffer.
buf := &bytes.Buffer{}

// Moving to the beginning of the section, that we rendered.
if r.linesRendered > 1 {
buf.WriteString(ansi.CursorUp(r.linesRendered - 1))
}

newLines := strings.Split(r.buf.String(), "\n")
oldLines := strings.Split(r.lastRender, "\n")

// If we know the output's height, we can use it to determine how many
// lines we can render. We drop lines from the top of the render buffer if
Expand All @@ -179,95 +185,69 @@ func (r *standardRenderer) flush() {
}

numLinesThisFlush := len(newLines)
oldLines := strings.Split(r.lastRender, "\n")
skipLines := make(map[int]struct{})
flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive

// Clear any lines we painted in the last render.
if r.linesRendered > 0 {
for i := r.linesRendered - 1; i > 0; i-- {
// if we are clearing queued messages, we want to clear all lines, since
// printing messages allows for native terminal word-wrap, we
// don't have control over the queued lines
if flushQueuedMessages {
buf.WriteString(ansi.EraseEntireLine)
} else if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) {
// If the number of lines we want to render hasn't increased and
// new line is the same as the old line we can skip rendering for
// this line as a performance optimization.
skipLines[i] = struct{}{}
} else if _, exists := r.ignoreLines[i]; !exists {
buf.WriteString(ansi.EraseEntireLine)
}

buf.WriteString(ansi.CursorUp1)
}

if _, exists := r.ignoreLines[0]; !exists {
// We need to return to the start of the line here to properly
// erase it. Going back the entire width of the terminal will
// usually be farther than we need to go, but terminal emulators
// will stop the cursor at the start of the line as a rule.
//
// We use this sequence in particular because it's part of the ANSI
// standard (whereas others are proprietary to, say, VT100/VT52).
// If cursor previous line (ESC[ + <n> + F) were better supported
// we could use that above to eliminate this step.
buf.WriteString(ansi.CursorLeft(r.width))
buf.WriteString(ansi.EraseEntireLine)
}
}

// Merge the set of lines we're skipping as a rendering optimization with
// the set of lines we've explicitly asked the renderer to ignore.
for k, v := range r.ignoreLines {
skipLines[k] = v
}

if flushQueuedMessages {
// Dump the lines we've queued up for printing
// Dump the lines we've queued up for printing.
for _, line := range r.queuedMessageLines {

// Removing previousy rendered content at the end of line.
line = line + ansi.EraseLineRight

_, _ = buf.WriteString(line)
_, _ = buf.WriteString("\r\n")
}
// clear the queued message lines
// Clear the queued message lines.
r.queuedMessageLines = []string{}
}

// Paint new lines
// Paint new lines.
for i := 0; i < len(newLines); i++ {
if _, skip := skipLines[i]; skip {
canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content.
len(oldLines) > i && oldLines[i] == newLines[i] // Previously rendered line is the same.

if _, ignore := r.ignoreLines[i]; ignore || canSkip {
// Unless this is the last line, move the cursor down.
if i < len(newLines)-1 {
buf.WriteString(ansi.CursorDown1)
}
} else {
if i == 0 && r.lastRender == "" {
// On first render, reset the cursor to the start of the line
// before writing anything.
buf.WriteByte('\r')
}
continue
}

line := newLines[i]

// Truncate lines wider than the width of the window to avoid
// wrapping, which will mess up rendering. If we don't have the
// width of the window this will be ignored.
//
// Note that on Windows we only get the width of the window on
// program initialization, so after a resize this won't perform
// correctly (signal SIGWINCH is not supported on Windows).
if r.width > 0 {
line = ansi.Truncate(line, r.width, "")
}
if i == 0 && r.lastRender == "" {
// On first render, reset the cursor to the start of the line
// before writing anything.
buf.WriteByte('\r')
}

_, _ = buf.WriteString(line)
line := newLines[i]

if i < len(newLines)-1 {
_, _ = buf.WriteString("\r\n")
}
// Removing previousy rendered content at the end of line.
line = line + ansi.EraseLineRight

// Truncate lines wider than the width of the window to avoid
// wrapping, which will mess up rendering. If we don't have the
// width of the window this will be ignored.
//
// Note that on Windows we only get the width of the window on
// program initialization, so after a resize this won't perform
// correctly (signal SIGWINCH is not supported on Windows).
if r.width > 0 {
line = ansi.Truncate(line, r.width, "")
}

_, _ = buf.WriteString(line)

if i < len(newLines)-1 {
_, _ = buf.WriteString("\r\n")
}
}

// Clearing left over content from last render.
if r.linesRendered > numLinesThisFlush {
buf.WriteString(ansi.EraseScreenBelow)
}

r.linesRendered = numLinesThisFlush

// Make sure the cursor is at the start of the last line to keep rendering
Expand Down
Loading