diff --git a/options.go b/options.go index 49cf378b59..7f0c240aae 100644 --- a/options.go +++ b/options.go @@ -194,6 +194,18 @@ func WithANSICompressor() ProgramOption { } } +// WithoutSkipRenderIdenticalLines disables an optimisation of the standard renderer +// where it will skip writing a line to the screen, +// if the same line written on the last render is identical. +// The optimisation is usually without consequences, but can be unwanted if an ANSI coloring tag +// is added on both sides of the unchanged line. If the line is not rewritten +// (even though identical) it will not be colored in some terminals +func WithoutSkipRenderIdenticalLines() ProgramOption { + return func(p *Program) { + p.startupOptions |= withoutSkipRenderIdenticalLines + } +} + // WithFilter supplies an event filter that will be invoked before Bubble Tea // processes a tea.Msg. The event filter can return any tea.Msg which will then // get handled by Bubble Tea instead of the original event. If the event filter diff --git a/options_test.go b/options_test.go index 60336c61b4..02f331a0a2 100644 --- a/options_test.go +++ b/options_test.go @@ -100,6 +100,10 @@ func TestOptions(t *testing.T) { exercise(t, WithANSICompressor(), withANSICompressor) }) + t.Run("without skip render identical lines", func(t *testing.T) { + exercise(t, WithoutSkipRenderIdenticalLines(), withANSICompressor) + }) + t.Run("without catch panics", func(t *testing.T) { exercise(t, WithoutCatchPanics(), withoutCatchPanics) }) diff --git a/standard_renderer.go b/standard_renderer.go index 969a58c292..7cb7a946ce 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -58,23 +58,28 @@ type standardRenderer struct { // lines explicitly set not to render ignoreLines map[int]struct{} + + // if a line is equal to the last render of the same line, it will not be re-rendered by default + // this setting disables that mechanism + dontSkipIdenticalLines bool } // newRenderer creates a new renderer. Normally you'll want to initialize it // with os.Stdout as the first argument. -func newRenderer(out io.Writer, useANSICompressor bool, fps int) renderer { +func newRenderer(out io.Writer, useANSICompressor bool, dontSkipIdenticalLines bool, fps int) renderer { if fps < 1 { fps = defaultFPS } else if fps > maxFPS { fps = maxFPS } r := &standardRenderer{ - out: out, - mtx: &sync.Mutex{}, - done: make(chan struct{}), - framerate: time.Second / time.Duration(fps), - useANSICompressor: useANSICompressor, - queuedMessageLines: []string{}, + out: out, + mtx: &sync.Mutex{}, + done: make(chan struct{}), + framerate: time.Second / time.Duration(fps), + useANSICompressor: useANSICompressor, + dontSkipIdenticalLines: dontSkipIdenticalLines, + queuedMessageLines: []string{}, } if r.useANSICompressor { r.out = &compressor.Writer{Forward: out} @@ -212,7 +217,9 @@ func (r *standardRenderer) flush() { // Paint new lines. for i := 0; i < len(newLines); i++ { canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content. - len(r.lastRenderedLines) > i && r.lastRenderedLines[i] == newLines[i] // Previously rendered line is the same. + len(r.lastRenderedLines) > i && + !r.dontSkipIdenticalLines && + r.lastRenderedLines[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. diff --git a/tea.go b/tea.go index db84343e3e..bfe80d96d8 100644 --- a/tea.go +++ b/tea.go @@ -105,6 +105,12 @@ const ( withoutCatchPanics withoutBracketedPaste withReportFocus + // The standard renderer is optimised to skip writing a line to the screen, + // if the same line written on the last render is identical. + // This is usually without consequences, but can be unwanted if an ANSI coloring tag + // is added on both sides of the unchanged line. If the line is not rewritten + // (even though identical) it will not be colored in some terminals + withoutSkipRenderIdenticalLines ) // channelHandlers manages the series of channels returned by various processes. @@ -642,7 +648,11 @@ func (p *Program) Run() (returnModel Model, returnErr error) { // If no renderer is set use the standard one. if p.renderer == nil { - p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor), p.fps) + p.renderer = newRenderer( + p.output, + p.startupOptions.has(withANSICompressor), + p.startupOptions.has(withoutSkipRenderIdenticalLines), + p.fps) } // Check if output is a TTY before entering raw mode, hiding the cursor and