From db140c807c6b7b4619989132355ba83812186c75 Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Sun, 27 Feb 2022 21:25:34 -0500 Subject: [PATCH 01/12] merge Adjective-Object/tea_log_renderer into standard renderer --- screen.go | 8 +++++ screen_test.go | 12 ++++++++ standard_renderer.go | 69 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/screen.go b/screen.go index 63db7ae656..beff1db814 100644 --- a/screen.go +++ b/screen.go @@ -27,6 +27,14 @@ func cursorDown(w io.Writer) { fmt.Fprintf(w, te.CSI+te.CursorDownSeq, 1) } +func cursorUpBy(w io.Writer, count int) { + fmt.Fprintf(w, te.CSI+te.CursorUpSeq, count) +} + +func cursorDownBy(w io.Writer, count int) { + fmt.Fprintf(w, te.CSI+te.CursorDownSeq, count) +} + func insertLine(w io.Writer, numLines int) { fmt.Fprintf(w, te.CSI+"%dL", numLines) } diff --git a/screen_test.go b/screen_test.go index 7bfdf2e676..3073f2e4c6 100644 --- a/screen_test.go +++ b/screen_test.go @@ -47,6 +47,18 @@ func TestScreen(t *testing.T) { }) t.Run("down", func(t *testing.T) { + exercise(t, func(w io.Writer) { + cursorDownBy(w, 3) + }, []byte("\x1b[3B")) + }) + + t.Run("upBy", func(t *testing.T) { + exercise(t, func(w io.Writer) { + cursorUpBy(w, 3) + }, []byte("\x1b[3A")) + }) + + t.Run("downBy", func(t *testing.T) { exercise(t, cursorDown, []byte("\x1b[1B")) }) diff --git a/standard_renderer.go b/standard_renderer.go index 4e754033d6..40ac201e8e 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -2,6 +2,7 @@ package tea import ( "bytes" + "fmt" "io" "strings" "sync" @@ -25,6 +26,7 @@ const ( type standardRenderer struct { out io.Writer buf bytes.Buffer + queuedMessages []string framerate time.Duration ticker *time.Ticker mtx *sync.Mutex @@ -53,6 +55,7 @@ func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) rendere mtx: mtx, framerate: defaultFramerate, useANSICompressor: useANSICompressor, + queuedMessages: []string{}, } if r.useANSICompressor { r.out = &compressor.Writer{Forward: out} @@ -122,22 +125,35 @@ func (r *standardRenderer) flush() { out := new(bytes.Buffer) newLines := strings.Split(r.buf.String(), "\n") + numLinesThisFlush := len(newLines) oldLines := strings.Split(r.lastRender, "\n") skipLines := make(map[int]struct{}) + flushQueuedMessages := len(r.queuedMessages) > 0 && !r.altScreenActive + + // Add any queued messages to this render + if flushQueuedMessages { + newLines = append(r.queuedMessages, newLines...) + r.queuedMessages = []string{} + } // Clear any lines we painted in the last render. if r.linesRendered > 0 { + skippedLines := 0 for i := r.linesRendered - 1; i > 0; 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. if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { - skipLines[i] = struct{}{} - } else if _, exists := r.ignoreLines[i]; !exists { + skippedLines += 1 + } else { + cursorUpBy(out, skippedLines+1) + skippedLines = 0 + // otherwise, clear the line so the new rendering can write into it clearLine(out) } - - cursorUp(out) + } + if skippedLines >= 1 { + cursorUpBy(out, skippedLines) } if _, exists := r.ignoreLines[0]; !exists { @@ -163,11 +179,9 @@ func (r *standardRenderer) flush() { } } - r.linesRendered = 0 - // Paint new lines for i := 0; i < len(newLines); i++ { - if _, skip := skipLines[r.linesRendered]; skip { + if _, skip := skipLines[i]; skip { // Unless this is the last line, move the cursor down. if i < len(newLines)-1 { cursorDown(out) @@ -192,8 +206,8 @@ func (r *standardRenderer) flush() { _, _ = io.WriteString(out, "\r\n") } } - r.linesRendered++ } + r.linesRendered = numLinesThisFlush // Make sure the cursor is at the start of the last line to keep rendering // behavior consistent. @@ -383,6 +397,13 @@ func (r *standardRenderer) handleMessages(msg Msg) { case scrollDownMsg: r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary) + + case printLineMessage: + if !r.altScreenActive { + r.mtx.Lock() + r.queuedMessages = append(r.queuedMessages, msg.messageBody) + r.mtx.Unlock() + } } } @@ -460,3 +481,35 @@ func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd { } } } + +type printLineMessage struct { + messageBody string +} + +// Println writes a message above your application on the next flush. +// +// The message is not added to the area bubbletea controls, so it can be +// used to log messages to the terminal without making them a persistent +// part of application model. +// +// Does not work with altScreenMode +func Println(args ...interface{}) Msg { + return printLineMessage{ + messageBody: fmt.Sprint(args...), + } +} + +// Println writes a message at the top of the next flush. +// +// The message is not added to the area bubbletea controls, so it can be +// used to log messages to the terminal without making them a persistent +// part of application model. +// +// Note that unlike fmt.Printf, the message will be put on its own line. +// +// Does not work with altScreenMode +func Printf(template string, args ...interface{}) Msg { + return printLineMessage{ + messageBody: fmt.Sprintf(template, args...), + } +} From d7204d3091beaae3242b186f2bdfab83f8c67a03 Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Sun, 27 Feb 2022 21:35:13 -0500 Subject: [PATCH 02/12] rename queuedMessages -> queuedMessageLines & break apart strings during message processing --- standard_renderer.go | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/standard_renderer.go b/standard_renderer.go index 40ac201e8e..9760b0caf8 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -24,17 +24,17 @@ const ( // In cases where very high performance is needed the renderer can be told // to exclude ranges of lines, allowing them to be written to directly. type standardRenderer struct { - out io.Writer - buf bytes.Buffer - queuedMessages []string - framerate time.Duration - ticker *time.Ticker - mtx *sync.Mutex - done chan struct{} - lastRender string - linesRendered int - useANSICompressor bool - once sync.Once + out io.Writer + buf bytes.Buffer + queuedMessageLines []string + framerate time.Duration + ticker *time.Ticker + mtx *sync.Mutex + done chan struct{} + lastRender string + linesRendered int + useANSICompressor bool + once sync.Once // essentially whether or not we're using the full size of the terminal altScreenActive bool @@ -51,11 +51,11 @@ type standardRenderer struct { // with os.Stdout as the first argument. func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) renderer { r := &standardRenderer{ - out: out, - mtx: mtx, - framerate: defaultFramerate, - useANSICompressor: useANSICompressor, - queuedMessages: []string{}, + out: out, + mtx: mtx, + framerate: defaultFramerate, + useANSICompressor: useANSICompressor, + queuedMessageLines: []string{}, } if r.useANSICompressor { r.out = &compressor.Writer{Forward: out} @@ -128,12 +128,12 @@ func (r *standardRenderer) flush() { numLinesThisFlush := len(newLines) oldLines := strings.Split(r.lastRender, "\n") skipLines := make(map[int]struct{}) - flushQueuedMessages := len(r.queuedMessages) > 0 && !r.altScreenActive + flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive // Add any queued messages to this render if flushQueuedMessages { - newLines = append(r.queuedMessages, newLines...) - r.queuedMessages = []string{} + newLines = append(r.queuedMessageLines, newLines...) + r.queuedMessageLines = []string{} } // Clear any lines we painted in the last render. @@ -400,8 +400,9 @@ func (r *standardRenderer) handleMessages(msg Msg) { case printLineMessage: if !r.altScreenActive { + lines := strings.Split(msg.messageBody, "\n") r.mtx.Lock() - r.queuedMessages = append(r.queuedMessages, msg.messageBody) + r.queuedMessageLines = append(r.queuedMessageLines, lines...) r.mtx.Unlock() } } From 33fa6f50ddbaec07f499dee4862b3c497b4c89ed Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Sun, 27 Feb 2022 21:47:05 -0500 Subject: [PATCH 03/12] delete cursorDownBy --- screen.go | 4 ---- screen_test.go | 6 ------ 2 files changed, 10 deletions(-) diff --git a/screen.go b/screen.go index beff1db814..d05dbd10ba 100644 --- a/screen.go +++ b/screen.go @@ -31,10 +31,6 @@ func cursorUpBy(w io.Writer, count int) { fmt.Fprintf(w, te.CSI+te.CursorUpSeq, count) } -func cursorDownBy(w io.Writer, count int) { - fmt.Fprintf(w, te.CSI+te.CursorDownSeq, count) -} - func insertLine(w io.Writer, numLines int) { fmt.Fprintf(w, te.CSI+"%dL", numLines) } diff --git a/screen_test.go b/screen_test.go index 3073f2e4c6..335399df1d 100644 --- a/screen_test.go +++ b/screen_test.go @@ -46,12 +46,6 @@ func TestScreen(t *testing.T) { exercise(t, cursorUp, []byte("\x1b[1A")) }) - t.Run("down", func(t *testing.T) { - exercise(t, func(w io.Writer) { - cursorDownBy(w, 3) - }, []byte("\x1b[3B")) - }) - t.Run("upBy", func(t *testing.T) { exercise(t, func(w io.Writer) { cursorUpBy(w, 3) From c604d40f61b2d20ec61550ab5301203ce1d05da8 Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Mon, 28 Feb 2022 07:19:21 -0500 Subject: [PATCH 04/12] += 1 -> ++ to make the linter happy --- standard_renderer.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/standard_renderer.go b/standard_renderer.go index 9760b0caf8..71cf066731 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -144,7 +144,7 @@ func (r *standardRenderer) flush() { // new line is the same as the old line we can skip rendering for // this line as a performance optimization. if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { - skippedLines += 1 + skippedLines++ } else { cursorUpBy(out, skippedLines+1) skippedLines = 0 From d8bde7dbe8fd52ff065af2b6a2fb18da4586dc0b Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Mon, 28 Feb 2022 09:31:37 -0500 Subject: [PATCH 05/12] add skipLines[] tracking back to standard renderer, and add rename skippedLines local to jumpedLines to clarify they are separate comments --- standard_renderer.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/standard_renderer.go b/standard_renderer.go index 71cf066731..a0a5a90f43 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -138,22 +138,23 @@ func (r *standardRenderer) flush() { // Clear any lines we painted in the last render. if r.linesRendered > 0 { - skippedLines := 0 + jumpedLines := 0 for i := r.linesRendered - 1; i > 0; 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. if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { - skippedLines++ + skipLines[i] = struct{}{} + jumpedLines++ } else { - cursorUpBy(out, skippedLines+1) - skippedLines = 0 + cursorUpBy(out, jumpedLines+1) + jumpedLines = 0 // otherwise, clear the line so the new rendering can write into it clearLine(out) } } - if skippedLines >= 1 { - cursorUpBy(out, skippedLines) + if jumpedLines >= 1 { + cursorUpBy(out, jumpedLines) } if _, exists := r.ignoreLines[0]; !exists { From 3bd5e17ec3ab1cd1ea825907a48db871066fa74b Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Mon, 28 Feb 2022 10:40:31 -0500 Subject: [PATCH 06/12] request repaint when a message is recieved --- standard_renderer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/standard_renderer.go b/standard_renderer.go index a0a5a90f43..621c71231e 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -404,6 +404,7 @@ func (r *standardRenderer) handleMessages(msg Msg) { lines := strings.Split(msg.messageBody, "\n") r.mtx.Lock() r.queuedMessageLines = append(r.queuedMessageLines, lines...) + r.repaint() r.mtx.Unlock() } } From 794be049c4e2d238be633fd0a5762b8d1de1a950 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 28 Feb 2022 13:43:36 -0500 Subject: [PATCH 07/12] Convert Println and Printf to commands --- standard_renderer.go | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/standard_renderer.go b/standard_renderer.go index 621c71231e..ebf0d727fb 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -489,30 +489,33 @@ type printLineMessage struct { messageBody string } -// Println writes a message above your application on the next flush. +// Printf prints above the Program. This output is unmanaged by the program and +// will persist across renders by the Program. // -// The message is not added to the area bubbletea controls, so it can be -// used to log messages to the terminal without making them a persistent -// part of application model. +// Unlike fmt.Printf (but similar to log.Printf) the message will be print on +// its own line. // -// Does not work with altScreenMode -func Println(args ...interface{}) Msg { - return printLineMessage{ - messageBody: fmt.Sprint(args...), +// If the altscreen is active no output will be printed. +func Println(args ...interface{}) Cmd { + return func() Msg { + return printLineMessage{ + messageBody: fmt.Sprint(args...), + } } } -// Println writes a message at the top of the next flush. -// -// The message is not added to the area bubbletea controls, so it can be -// used to log messages to the terminal without making them a persistent -// part of application model. +// Printf prints above the Program. It takes a format template followed by +// values similar to fmt.Printf. This output is unmanaged by the program and +// will persist across renders by the Program. // -// Note that unlike fmt.Printf, the message will be put on its own line. +// Unlike fmt.Printf (but similar to log.Printf) the message will be print on +// its own line. // -// Does not work with altScreenMode -func Printf(template string, args ...interface{}) Msg { - return printLineMessage{ - messageBody: fmt.Sprintf(template, args...), +// If the altscreen is active no output will be printed. +func Printf(format string, args ...interface{}) Cmd { + return func() Msg { + return printLineMessage{ + messageBody: fmt.Sprintf(template, args...), + } } } From a8e1aac1ecb430a506077ce0365e14df1d8b2891 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 28 Feb 2022 13:45:52 -0500 Subject: [PATCH 08/12] Add package manager example demonstrating tea.Printf --- examples/package-manager/main.go | 139 +++++++++++++++++++++++++++ examples/package-manager/packages.go | 52 ++++++++++ standard_renderer.go | 2 +- 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 examples/package-manager/main.go create mode 100644 examples/package-manager/packages.go diff --git a/examples/package-manager/main.go b/examples/package-manager/main.go new file mode 100644 index 0000000000..0d16f9a0a1 --- /dev/null +++ b/examples/package-manager/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "fmt" + "math/rand" + "os" + "strings" + "time" + + "github.com/charmbracelet/bubbles/progress" + "github.com/charmbracelet/bubbles/spinner" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type model struct { + packages []string + index int + width int + height int + spinner spinner.Model + progress progress.Model + done bool +} + +var ( + currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) + subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239")) + doneStyle = lipgloss.NewStyle().Margin(1, 2) + checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") +) + +func newModel() model { + p := progress.New( + progress.WithDefaultGradient(), + progress.WithWidth(40), + progress.WithoutPercentage(), + ) + s := spinner.New() + s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) + return model{ + packages: getPackages(), + spinner: s, + progress: p, + } +} + +func (m model) Init() tea.Cmd { + return tea.Batch(downloadAndInstall(m.packages[m.index]), m.spinner.Tick) +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.width, m.height = msg.Width, msg.Height + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "esc", "q": + return m, tea.Quit + } + case installedPkgMsg: + if m.index >= len(m.packages)-1 { + // Everything's been installed. We're done! + m.done = true + return m, tea.Quit + } + + // Update progress bar + progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)-1)) + + m.index++ + return m, tea.Batch( + progressCmd, + tea.Printf("%s %s", checkMark, m.packages[m.index]), // print success message above our program + downloadAndInstall(m.packages[m.index]), // download the next package + ) + case spinner.TickMsg: + var cmd tea.Cmd + m.spinner, cmd = m.spinner.Update(msg) + return m, cmd + case progress.FrameMsg: + newModel, cmd := m.progress.Update(msg) + if newModel, ok := newModel.(progress.Model); ok { + m.progress = newModel + } + return m, cmd + } + return m, nil +} + +func (m model) View() string { + n := len(m.packages) + w := lipgloss.Width(fmt.Sprintf("%d", n)) + + if m.done { + return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n)) + } + + pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n-1) + + spin := m.spinner.View() + " " + prog := m.progress.View() + cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount)) + + pkgName := currentPkgNameStyle.Render(m.packages[m.index]) + info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName) + + cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount)) + gap := strings.Repeat(" ", cellsRemaining) + + return spin + info + gap + prog + pkgCount +} + +type installedPkgMsg string + +func downloadAndInstall(pkg string) tea.Cmd { + // This is where you'd do i/o stuff to download and install packages. In + // our case we're just pausing for a moment to simulate the process. + d := time.Millisecond * time.Duration(rand.Intn(500)) + return tea.Tick(d, func(t time.Time) tea.Msg { + return installedPkgMsg(pkg) + }) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func main() { + rand.Seed(time.Now().UnixMicro()) + + if err := tea.NewProgram(newModel()).Start(); err != nil { + fmt.Println("Error running program:", err) + os.Exit(1) + } +} diff --git a/examples/package-manager/packages.go b/examples/package-manager/packages.go new file mode 100644 index 0000000000..7ed8478a3c --- /dev/null +++ b/examples/package-manager/packages.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "math/rand" +) + +var packages = []string{ + "vegeutils", + "libgardening", + "currykit", + "spicerack", + "fullenglish", + "eggy", + "bad-kitty", + "chai", + "hojicha", + "libtacos", + "babys-monads", + "libpurring", + "currywurst-devel", + "xmodmeow", + "licorice-utils", + "cashew-apple", + "rock-lobster", + "standmixer", + "coffee-CUPS", + "libesszet", + "zeichenorientierte-benutzerschnittstellen", + "schnurrkit", + "old-socks-devel", + "jalapeño", + "molasses-utils", + "xkohlrabi", + "party-gherkin", + "snow-peas", + "libyuzu", +} + +func getPackages() []string { + pkgs := packages + copy(pkgs, packages) + + rand.Shuffle(len(pkgs), func(i, j int) { + pkgs[i], pkgs[j] = pkgs[j], pkgs[i] + }) + + for k := range pkgs { + pkgs[k] += fmt.Sprintf("-%d.%d.%d", rand.Intn(10), rand.Intn(10), rand.Intn(10)) + } + return pkgs +} diff --git a/standard_renderer.go b/standard_renderer.go index ebf0d727fb..9c4a944618 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -512,7 +512,7 @@ func Println(args ...interface{}) Cmd { // its own line. // // If the altscreen is active no output will be printed. -func Printf(format string, args ...interface{}) Cmd { +func Printf(template string, args ...interface{}) Cmd { return func() Msg { return printLineMessage{ messageBody: fmt.Sprintf(template, args...), From 54c29ad607abd7d3782b7b3dd14b46b58cbe9453 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 28 Feb 2022 14:00:19 -0500 Subject: [PATCH 09/12] Use Unix instead of UnixMicro for Go 1.13 support in CI --- examples/package-manager/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/package-manager/main.go b/examples/package-manager/main.go index 0d16f9a0a1..e6f20ecd1a 100644 --- a/examples/package-manager/main.go +++ b/examples/package-manager/main.go @@ -130,7 +130,7 @@ func max(a, b int) int { } func main() { - rand.Seed(time.Now().UnixMicro()) + rand.Seed(time.Now().Unix()) if err := tea.NewProgram(newModel()).Start(); err != nil { fmt.Println("Error running program:", err) From c4c903e20b68969b6ca8aedf8563e1b7ae5baa2b Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Mon, 28 Feb 2022 16:30:53 -0500 Subject: [PATCH 10/12] fix off by one in std renderer --- standard_renderer.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/standard_renderer.go b/standard_renderer.go index 9c4a944618..8efd56b822 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -147,14 +147,14 @@ func (r *standardRenderer) flush() { skipLines[i] = struct{}{} jumpedLines++ } else { - cursorUpBy(out, jumpedLines+1) - jumpedLines = 0 - // otherwise, clear the line so the new rendering can write into it + cursorUpBy(out, jumpedLines) clearLine(out) + + jumpedLines = 0 } } if jumpedLines >= 1 { - cursorUpBy(out, jumpedLines) + cursorUpBy(out, jumpedLines+1) } if _, exists := r.ignoreLines[0]; !exists { From c2ab77dacbf175705f5a80fae1461d3d7b180165 Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Mon, 28 Feb 2022 17:43:10 -0500 Subject: [PATCH 11/12] add Printf/Println to tea.go --- tea.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tea.go b/tea.go index fcbf822af4..ff587469d7 100644 --- a/tea.go +++ b/tea.go @@ -709,3 +709,30 @@ func (p *Program) RestoreTerminal() error { return nil } + +// Printf prints above the Program. This output is unmanaged by the program and +// will persist across renders by the Program. +// +// Unlike fmt.Printf (but similar to log.Printf) the message will be print on +// its own line. +// +// If the altscreen is active no output will be printed. +func (p *Program) Println(args ...interface{}) { + p.msgs <- printLineMessage{ + messageBody: fmt.Sprint(args...), + } +} + +// Printf prints above the Program. It takes a format template followed by +// values similar to fmt.Printf. This output is unmanaged by the program and +// will persist across renders by the Program. +// +// Unlike fmt.Printf (but similar to log.Printf) the message will be print on +// its own line. +// +// If the altscreen is active no output will be printed. +func (p *Program) Printf(template string, args ...interface{}) { + p.msgs <- printLineMessage{ + messageBody: fmt.Sprintf(template, args...), + } +} From a3d0fd380fb9dfbb3665dd68f81155af8b3f00a5 Mon Sep 17 00:00:00 2001 From: Maxwell Huang-Hobbs Date: Mon, 28 Feb 2022 18:31:04 -0500 Subject: [PATCH 12/12] revert attempt at sequence compression + cursorUpBy --- screen.go | 4 ---- screen_test.go | 8 +------- standard_renderer.go | 12 +++--------- 3 files changed, 4 insertions(+), 20 deletions(-) diff --git a/screen.go b/screen.go index d05dbd10ba..63db7ae656 100644 --- a/screen.go +++ b/screen.go @@ -27,10 +27,6 @@ func cursorDown(w io.Writer) { fmt.Fprintf(w, te.CSI+te.CursorDownSeq, 1) } -func cursorUpBy(w io.Writer, count int) { - fmt.Fprintf(w, te.CSI+te.CursorUpSeq, count) -} - func insertLine(w io.Writer, numLines int) { fmt.Fprintf(w, te.CSI+"%dL", numLines) } diff --git a/screen_test.go b/screen_test.go index 335399df1d..7bfdf2e676 100644 --- a/screen_test.go +++ b/screen_test.go @@ -46,13 +46,7 @@ func TestScreen(t *testing.T) { exercise(t, cursorUp, []byte("\x1b[1A")) }) - t.Run("upBy", func(t *testing.T) { - exercise(t, func(w io.Writer) { - cursorUpBy(w, 3) - }, []byte("\x1b[3A")) - }) - - t.Run("downBy", func(t *testing.T) { + t.Run("down", func(t *testing.T) { exercise(t, cursorDown, []byte("\x1b[1B")) }) diff --git a/standard_renderer.go b/standard_renderer.go index 8efd56b822..2d12271ff5 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -138,23 +138,17 @@ func (r *standardRenderer) flush() { // Clear any lines we painted in the last render. if r.linesRendered > 0 { - jumpedLines := 0 for i := r.linesRendered - 1; i > 0; 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. if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { skipLines[i] = struct{}{} - jumpedLines++ - } else { - cursorUpBy(out, jumpedLines) + } else if _, exists := r.ignoreLines[i]; !exists { clearLine(out) - - jumpedLines = 0 } - } - if jumpedLines >= 1 { - cursorUpBy(out, jumpedLines+1) + + cursorUp(out) } if _, exists := r.ignoreLines[0]; !exists {