From d1dbb84c163be64da8bd2df34303f8a8d3c9c4b0 Mon Sep 17 00:00:00 2001 From: sammyette Date: Tue, 16 Apr 2024 11:56:57 -0400 Subject: [PATCH] fix(readline): flickering on line refresh (#288) --- CHANGELOG.md | 1 + go.mod | 2 +- readline/codes.go | 2 ++ readline/cursor.go | 43 +++++++++++++++++++++++++++++++++++++++++++ readline/go.mod | 2 +- readline/instance.go | 5 +++++ readline/line.go | 17 +++++++++++------ readline/prompt.go | 6 +++--- readline/tab.go | 5 +++-- readline/update.go | 18 ++++++++++++++++-- 10 files changed, 86 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b730b24..0c12ab15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased ### Fixed +- Line refresh fixes (less flicker) - Do more checks for a TTY - Panic if ENOTTY is thrown from readline - use `x/term` function to check if a terminal diff --git a/go.mod b/go.mod index 7bf5efda..6753a171 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module hilbish -go 1.17 +go 1.18 require ( github.com/arnodel/golua v0.0.0-20220221163911-dfcf252b6f86 diff --git a/readline/codes.go b/readline/codes.go index 7037e457..28a9e604 100644 --- a/readline/codes.go +++ b/readline/codes.go @@ -76,6 +76,8 @@ const ( seqCursorTopLeft = "\x1b[H" // Clears screen and places cursor on top-left seqGetCursorPos = "\x1b6n" // response: "\x1b{Line};{Column}R" + seqHideCursor = "\x1b[?25l" + seqUnhideCursor = "\x1b[?25h" seqCtrlLeftArrow = "\x1b[1;5D" seqCtrlRightArrow = "\x1b[1;5C" diff --git a/readline/cursor.go b/readline/cursor.go index f313ef4b..9d68a5a2 100644 --- a/readline/cursor.go +++ b/readline/cursor.go @@ -1,6 +1,7 @@ package readline import ( +// "fmt" "os" "regexp" "strconv" @@ -68,6 +69,40 @@ func (rl *Instance) getCursorPos() (x int, y int) { // This means that they are not used to keep any reference point when // when we internally move around clearning and printing things +/* +func moveCursorUpBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dA", i) +} + +func moveCursorDownBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dB", i) +} + +func moveCursorForwardsBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dC", i) +} + +func moveCursorUpBuffered(i int) { + if i < 1 { + return + } + + fmt.Fprintf(rl.bufferedOut, "\x1b[%dD", i) +} +*/ + func moveCursorUp(i int) { if i < 1 { return @@ -100,6 +135,14 @@ func moveCursorBackwards(i int) { printf("\x1b[%dD", i) } +func hideCursor() { + print(seqHideCursor) +} + +func unhideCursor() { + print(seqUnhideCursor) +} + func (rl *Instance) backspace(forward bool) { if len(rl.line) == 0 || rl.pos == 0 { return diff --git a/readline/go.mod b/readline/go.mod index ab404cd8..d5322dc1 100644 --- a/readline/go.mod +++ b/readline/go.mod @@ -1,6 +1,6 @@ module github.com/maxlandon/readline -go 1.16 +go 1.18 require ( github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d diff --git a/readline/instance.go b/readline/instance.go index a4772466..163bffee 100644 --- a/readline/instance.go +++ b/readline/instance.go @@ -1,6 +1,7 @@ package readline import ( + "bufio" "os" "regexp" "sync" @@ -203,6 +204,8 @@ type Instance struct { ViActionCallback func(ViAction, []string) RawInputCallback func([]rune) // called on all input + + bufferedOut *bufio.Writer } // NewInstance is used to create a readline instance and initialise it with sane defaults. @@ -251,6 +254,8 @@ func NewInstance() *Instance { return suggs } + rl.bufferedOut = bufio.NewWriter(os.Stdout) + // Registers rl.initRegisters() diff --git a/readline/line.go b/readline/line.go index 2024bb04..3069ad86 100644 --- a/readline/line.go +++ b/readline/line.go @@ -33,19 +33,20 @@ func (rl *Instance) GetLine() []rune { func (rl *Instance) echo() { // Then we print the prompt, and the line, + hideCursor() switch { case rl.PasswordMask != 0: case rl.PasswordMask > 0: - print(strings.Repeat(string(rl.PasswordMask), len(rl.line)) + " ") + rl.bufprint(strings.Repeat(string(rl.PasswordMask), len(rl.line)) + " ") default: + // Go back to prompt position, and clear everything below moveCursorBackwards(GetTermWidth()) moveCursorUp(rl.posY) - print(seqClearScreenBelow) // Print the prompt - print(string(rl.realPrompt)) + rl.bufprint(string(rl.realPrompt)) // Assemble the line, taking virtual completions into account var line []rune @@ -57,11 +58,14 @@ func (rl *Instance) echo() { // Print the input line with optional syntax highlighting if rl.SyntaxHighlighter != nil { - print(rl.SyntaxHighlighter(line)) + rl.bufprint(rl.SyntaxHighlighter(line)) } else { - print(string(line)) + rl.bufprint(string(line)) } + rl.bufprint(seqClearScreenBelow) + } + rl.bufflush() // Update references with new coordinates only now, because // the new line may be longer/shorter than the previous one. @@ -72,6 +76,7 @@ func (rl *Instance) echo() { moveCursorUp(rl.fullY) moveCursorDown(rl.posY) moveCursorForwards(rl.posX) + unhideCursor() } func (rl *Instance) insert(r []rune) { @@ -159,7 +164,7 @@ func (rl *Instance) clearLine() { moveCursorForwards(rl.promptLen) // Clear everything after & below the cursor - print(seqClearScreenBelow) + //print(seqClearScreenBelow) // Real input line rl.line = []rune{} diff --git a/readline/prompt.go b/readline/prompt.go index 0f6ca5aa..d141cd63 100644 --- a/readline/prompt.go +++ b/readline/prompt.go @@ -48,7 +48,7 @@ func (rl *Instance) RefreshPromptLog(log string) (err error) { rl.stillOnRefresh = true moveCursorUp(rl.infoY + rl.tcUsedY) moveCursorBackwards(GetTermWidth()) - print("\r\n" + seqClearScreenBelow) + //print("\r\n" + seqClearScreenBelow) // Print the log fmt.Printf(log) @@ -97,7 +97,7 @@ func (rl *Instance) RefreshPromptInPlace(prompt string) (err error) { print(seqClearLine) moveCursorUp(rl.infoY + rl.tcUsedY) moveCursorBackwards(GetTermWidth()) - print("\r\n" + seqClearScreenBelow) + //print("\r\n" + seqClearScreenBelow) // Add a new line if needed if rl.Multiline { @@ -137,7 +137,7 @@ func (rl *Instance) RefreshPromptCustom(prompt string, offset int, clearLine boo moveCursorUp(offset) // Then clear everything below our new position - print(seqClearScreenBelow) + //print(seqClearScreenBelow) // Update the prompt if a special has been passed. if prompt != "" { diff --git a/readline/tab.go b/readline/tab.go index d00decc3..f2cc1408 100644 --- a/readline/tab.go +++ b/readline/tab.go @@ -276,13 +276,14 @@ func (rl *Instance) writeTabCompletion() { // than what their MaxLength allows them to, cycling sometimes occur, // but does not fully clears itself: some descriptions are messed up with. // We always clear the screen as a result, between writings. - print(seqClearScreenBelow) + //rl.bufprint(seqClearScreenBelow) // Crop the completions so that it fits within our MaxTabCompleterRows completions, rl.tcUsedY = rl.cropCompletions(completions) // Then we print all of them. - fmt.Printf(completions) + rl.bufprintF(completions) + rl.bufflush() } // cropCompletions - When the user cycles through a completion list longer diff --git a/readline/update.go b/readline/update.go index 8f85c6d5..66b3ba08 100644 --- a/readline/update.go +++ b/readline/update.go @@ -1,6 +1,7 @@ package readline import ( + "fmt" "strings" "golang.org/x/text/width" @@ -10,7 +11,7 @@ import ( // it should coordinate reprinting the input line, any Infos and completions // and manage to get back to the current (computed) cursor coordinates func (rl *Instance) updateHelpers() { - + print(seqHideCursor) // Load all Infos & completions before anything. // Thus overwrites anything having been dirtily added/forced/modified, like rl.SetInfoText() rl.getInfoText() @@ -27,6 +28,7 @@ func (rl *Instance) updateHelpers() { // We are at the prompt line (with the latter // not printed yet), then reprint everything rl.renderHelpers() + print(seqUnhideCursor) } const tabWidth = 4 @@ -119,7 +121,7 @@ func (rl *Instance) clearHelpers() { moveCursorForwards(rl.fullX) // Clear everything below - print(seqClearScreenBelow) + //print(seqClearScreenBelow) // Go back to current cursor position moveCursorBackwards(GetTermWidth()) @@ -194,3 +196,15 @@ func (rl *Instance) renderHelpers() { moveCursorUp(rl.fullY - rl.posY) moveCursorForwards(rl.posX) } + +func (rl *Instance) bufprintF(format string, a ...any) { + fmt.Fprintf(rl.bufferedOut, format, a...) +} + +func (rl *Instance) bufprint(text string) { + fmt.Fprint(rl.bufferedOut, text) +} + +func (rl *Instance) bufflush() { + rl.bufferedOut.Flush() +}