diff --git a/options.go b/options.go index d1c04034b7..c4073e8dfc 100644 --- a/options.go +++ b/options.go @@ -166,6 +166,16 @@ func WithANSICompressor() ProgramOption { } } +// WithoutEmptyRenders makes bubbletea do nothing when the View method return +// an empty string. +// +// By default, it will render a space instead. +func WithoutEmptyRenders() ProgramOption { + return func(p *Program) { + p.startupOptions |= withoutEmptyRenders + } +} + // 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 fdbc5b2cc2..61aaae77a9 100644 --- a/options_test.go +++ b/options_test.go @@ -65,7 +65,6 @@ func TestOptions(t *testing.T) { var b bytes.Buffer exercise(t, WithInput(&b), customInput) }) - }) t.Run("startup options", func(t *testing.T) { @@ -92,6 +91,10 @@ func TestOptions(t *testing.T) { exercise(t, WithoutSignalHandler(), withoutSignalHandler) }) + t.Run("without empty renders", func(t *testing.T) { + exercise(t, WithoutEmptyRenders(), withoutEmptyRenders) + }) + t.Run("mouse cell motion", func(t *testing.T) { p := NewProgram(nil, WithMouseAllMotion(), WithMouseCellMotion()) if !p.startupOptions.has(withMouseCellMotion) { diff --git a/standard_renderer.go b/standard_renderer.go index c6a1b61017..0975e53950 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -37,6 +37,7 @@ type standardRenderer struct { lastRender string linesRendered int useANSICompressor bool + renderEmpty bool once sync.Once // cursor visibility state @@ -55,7 +56,7 @@ type standardRenderer struct { // newRenderer creates a new renderer. Normally you'll want to initialize it // with os.Stdout as the first argument. -func newRenderer(out *termenv.Output, useANSICompressor bool, fps int) renderer { +func newRenderer(out *termenv.Output, useANSICompressor, renderEmpty bool, fps int) renderer { if fps < 1 { fps = defaultFPS } else if fps > maxFPS { @@ -67,6 +68,7 @@ func newRenderer(out *termenv.Output, useANSICompressor bool, fps int) renderer done: make(chan struct{}), framerate: time.Second / time.Duration(fps), useANSICompressor: useANSICompressor, + renderEmpty: renderEmpty, queuedMessageLines: []string{}, } if r.useANSICompressor { @@ -271,7 +273,7 @@ func (r *standardRenderer) write(s string) { // rendering nothing. Rather than introduce additional state to manage // this, we render a single space as a simple (albeit less correct) // solution. - if s == "" { + if s == "" && r.renderEmpty { s = " " } diff --git a/tea.go b/tea.go index 34fb883e59..c2744ba4e3 100644 --- a/tea.go +++ b/tea.go @@ -98,6 +98,8 @@ const ( // recover from panics, print the stack trace, and disable raw mode. This // feature is on by default. withoutCatchPanics + + withoutEmptyRenders ) // handlers manages series of channels returned by various processes. It allows @@ -470,7 +472,12 @@ func (p *Program) Run() (Model, 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(withoutEmptyRenders), + p.fps, + ) } // Check if output is a TTY before entering raw mode, hiding the cursor and