From 1946b9b3700598ce283c5d2f2ec43ecd02ab57df Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Sat, 7 Sep 2024 03:48:39 +0200 Subject: [PATCH 1/8] feat(render): remove flickering --- standard_renderer.go | 64 ++++++++++++-------------------------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/standard_renderer.go b/standard_renderer.go index 59448e1d41..7047fea8f9 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -168,6 +168,9 @@ func (r *standardRenderer) flush() { // Output buffer buf := &bytes.Buffer{} + // Moving to the begining of the section, that we rendered. + buf.WriteString(ansi.CursorUp(r.linesRendered-1) + ansi.CursorLeft(r.width)) + newLines := strings.Split(r.buf.String(), "\n") // If we know the output's height, we can use it to determine how many @@ -179,64 +182,25 @@ 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[ + + 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 { + if _, skip := r.ignoreLines[i]; skip { // Unless this is the last line, move the cursor down. if i < len(newLines)-1 { buf.WriteString(ansi.CursorDown1) @@ -250,6 +214,9 @@ func (r *standardRenderer) flush() { line := newLines[i] + // Removing left over content from previous render at the end of the 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. @@ -270,6 +237,9 @@ func (r *standardRenderer) flush() { } r.linesRendered = numLinesThisFlush + // Clearing left over content from last render (if our new frame contains less lines). + buf.WriteString(ansi.EraseDisplayRight) + // Make sure the cursor is at the start of the last line to keep rendering // behavior consistent. if r.altScreenActive { From d811f1924a4ad22fa66d6db84e827cfc7407387b Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Sun, 8 Sep 2024 00:35:11 +0200 Subject: [PATCH 2/8] feat(render): update tests for non-flick rendering + optimisations --- screen_test.go | 18 +++++++++--------- standard_renderer.go | 19 ++++++++++++------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/screen_test.go b/screen_test.go index 728cd9779f..db46bb6a9f 100644 --- a/screen_test.go +++ b/screen_test.go @@ -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", }, } diff --git a/standard_renderer.go b/standard_renderer.go index 7047fea8f9..512ef888aa 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -161,15 +161,17 @@ 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 begining of the section, that we rendered. - buf.WriteString(ansi.CursorUp(r.linesRendered-1) + ansi.CursorLeft(r.width)) + if r.linesRendered > 0 { + buf.WriteString(ansi.CursorUp(r.linesRendered - 1)) + } newLines := strings.Split(r.buf.String(), "\n") @@ -214,7 +216,7 @@ func (r *standardRenderer) flush() { line := newLines[i] - // Removing left over content from previous render at the end of the line. + // Removing previousy rendered content at the end of line. line = line + ansi.EraseLineRight // Truncate lines wider than the width of the window to avoid @@ -235,10 +237,13 @@ func (r *standardRenderer) flush() { } } } - r.linesRendered = numLinesThisFlush - // Clearing left over content from last render (if our new frame contains less lines). - buf.WriteString(ansi.EraseDisplayRight) + // Clearing left over content from last render. + if r.linesRendered > numLinesThisFlush { + buf.WriteString(ansi.EraseDisplayRight) + } + + r.linesRendered = numLinesThisFlush // Make sure the cursor is at the start of the last line to keep rendering // behavior consistent. From 4c662ec42773468514eceb6499ad83f5b5daa849 Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Sun, 8 Sep 2024 02:37:25 +0200 Subject: [PATCH 3/8] feat(render): fix typo --- standard_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard_renderer.go b/standard_renderer.go index 512ef888aa..21c3ccd7e6 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -168,7 +168,7 @@ func (r *standardRenderer) flush() { // Output buffer. buf := &bytes.Buffer{} - // Moving to the begining of the section, that we rendered. + // Moving to the beginning of the section, that we rendered. if r.linesRendered > 0 { buf.WriteString(ansi.CursorUp(r.linesRendered - 1)) } From 990a85acbda2065df001a56161718dbc11387b54 Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Sun, 15 Sep 2024 02:09:59 +0200 Subject: [PATCH 4/8] update golden output --- examples/simple/testdata/TestApp.golden | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/simple/testdata/TestApp.golden b/examples/simple/testdata/TestApp.golden index 413aee46d6..cb6dc1af96 100644 --- a/examples/simple/testdata/TestApp.golden +++ b/examples/simple/testdata/TestApp.golden @@ -1,5 +1,7 @@ -[?25l[?2004h Hi. 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 \ No newline at end of file +[?25l[?2004h Hi. 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. + +To quit sooner press ctrl-c, or press ctrl-z to suspend... + [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file From e7d8095dd2553e9b68eb6a9137b3c6b5b228a16e Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Mon, 16 Sep 2024 17:57:32 +0200 Subject: [PATCH 5/8] feat(render): upd EraseDisplayRight --- standard_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard_renderer.go b/standard_renderer.go index 9a5798bcf7..2ffeb83165 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -240,7 +240,7 @@ func (r *standardRenderer) flush() { // Clearing left over content from last render. if r.linesRendered > numLinesThisFlush { - buf.WriteString(ansi.EraseDisplayRight) + buf.WriteString(ansi.EraseDisplayBelow) } r.linesRendered = numLinesThisFlush From 123712607f3d7054cb345c13e630c622c22e7494 Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Wed, 18 Sep 2024 04:11:09 +0200 Subject: [PATCH 6/8] feat(render): skipping lines from previous frame --- examples/simple/testdata/TestApp.golden | 4 +- standard_renderer.go | 53 ++++++++++++++----------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/examples/simple/testdata/TestApp.golden b/examples/simple/testdata/TestApp.golden index cb6dc1af96..6664a0eed0 100644 --- a/examples/simple/testdata/TestApp.golden +++ b/examples/simple/testdata/TestApp.golden @@ -2,6 +2,4 @@  To quit sooner press ctrl-c, or press ctrl-z to suspend... Hi. This program will exit in 9 seconds. - -To quit sooner press ctrl-c, or press ctrl-z to suspend... - [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file + [?2004l[?25h[?1002l[?1003l[?1006l \ No newline at end of file diff --git a/standard_renderer.go b/standard_renderer.go index 2ffeb83165..36e2c894ec 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -174,6 +174,7 @@ func (r *standardRenderer) flush() { } 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 @@ -202,39 +203,43 @@ func (r *standardRenderer) flush() { // Paint new lines. for i := 0; i < len(newLines); i++ { - if _, skip := r.ignoreLines[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] + if i == 0 && r.lastRender == "" { + // On first render, reset the cursor to the start of the line + // before writing anything. + buf.WriteByte('\r') + } - // Removing previousy rendered content at the end of line. - line = line + ansi.EraseLineRight + 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, "") - } + // Removing previousy rendered content at the end of line. + line = line + ansi.EraseLineRight - _, _ = buf.WriteString(line) + // 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 < len(newLines)-1 { - _, _ = buf.WriteString("\r\n") - } + _, _ = buf.WriteString(line) + + if i < len(newLines)-1 { + _, _ = buf.WriteString("\r\n") } } From 8e3bbbcbdc9db9f6d59e2ca4dcc450f4b0cb1563 Mon Sep 17 00:00:00 2001 From: Konstantin Antonov Date: Tue, 15 Oct 2024 22:31:40 +0200 Subject: [PATCH 7/8] fix single-line CursorUp(0) --- standard_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard_renderer.go b/standard_renderer.go index 36e2c894ec..a85aeaa32d 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -169,7 +169,7 @@ func (r *standardRenderer) flush() { buf := &bytes.Buffer{} // Moving to the beginning of the section, that we rendered. - if r.linesRendered > 0 { + if r.linesRendered > 1 { buf.WriteString(ansi.CursorUp(r.linesRendered - 1)) } From 5d2bc060c1738fe053703bdc371344a5670dda7c Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Thu, 24 Oct 2024 12:42:46 -0400 Subject: [PATCH 8/8] fix: update standard_renderer.go --- standard_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard_renderer.go b/standard_renderer.go index 05e2f0e114..1121af88e5 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -245,7 +245,7 @@ func (r *standardRenderer) flush() { // Clearing left over content from last render. if r.linesRendered > numLinesThisFlush { - buf.WriteString(ansi.EraseDisplayBelow) + buf.WriteString(ansi.EraseScreenBelow) } r.linesRendered = numLinesThisFlush