Add nil renderer and combination TUI-daemon program example

The Nil Renderer essentially disables the Bubble Tea renderer sending
loggings and print statements to stdout. It can be enabled via the
ProgramOption WithoutRenderer.
This commit is contained in:
Christian Rocha 2021-02-09 13:33:25 -05:00
parent 2b8b95d8f8
commit b3f62af8b5
6 changed files with 561 additions and 393 deletions

View File

@ -8,6 +8,7 @@ require (
github.com/charmbracelet/glamour v0.2.0
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/mattn/go-isatty v0.0.12
github.com/mattn/go-runewidth v0.0.10
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68
github.com/muesli/termenv v0.7.4

View File

@ -0,0 +1,121 @@
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
"time"
"github.com/charmbracelet/bubbles/spinner"
tea "github.com/charmbracelet/bubbletea"
"github.com/mattn/go-isatty"
"github.com/muesli/reflow/indent"
)
func main() {
rand.Seed(time.Now().UTC().UnixNano())
var (
daemonMode bool
showHelp bool
opts []tea.ProgramOption
)
flag.BoolVar(&daemonMode, "d", false, "run as a daemon")
flag.BoolVar(&showHelp, "h", false, "show help")
flag.Parse()
if showHelp {
flag.Usage()
os.Exit(0)
}
if daemonMode || !isatty.IsTerminal(os.Stdout.Fd()) {
// If we're in daemon mode don't render the TUI
opts = []tea.ProgramOption{tea.WithoutRenderer()}
} else {
// If we're in TUI mode, discard log output
log.SetOutput(ioutil.Discard)
}
p := tea.NewProgram(newModel(), opts...)
if err := p.Start(); err != nil {
fmt.Println("Error starting Bubble Tea program:", err)
os.Exit(1)
}
}
type model struct {
spinner spinner.Model
results []time.Duration
quitting bool
}
func newModel() model {
const showLastResults = 5
return model{
spinner: spinner.NewModel(),
results: make([]time.Duration, showLastResults),
}
}
func (m model) Init() tea.Cmd {
log.Println("Starting work...")
return tea.Batch(
spinner.Tick,
runPretendProcess,
)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
m.quitting = true
return m, tea.Quit
case spinner.TickMsg:
var cmd tea.Cmd
m.spinner, cmd = m.spinner.Update(msg)
return m, cmd
case processFinishedMsg:
d := time.Duration(msg)
log.Printf("Finished job in %s", d)
m.results = append(m.results[1:], d)
return m, runPretendProcess
default:
return m, nil
}
}
func (m model) View() string {
s := "\n" + m.spinner.View() + " Doing some work...\n\n"
for _, dur := range m.results {
if dur == 0 {
s += ".....................\n"
} else {
s += fmt.Sprintf("Job finished in %s\n", dur)
}
}
s += "\nPress any key to exit\n"
if m.quitting {
s += "\n"
}
return indent.String(s, 1)
}
// processFinishedMsg is send when a pretend process completes.
type processFinishedMsg time.Duration
// pretendProcess simulates a long-running process.
func runPretendProcess() tea.Msg {
pause := time.Duration(rand.Int63n(899)+100) * time.Millisecond
time.Sleep(pause)
return processFinishedMsg(pause)
}

9
nil_renderer.go Normal file
View File

@ -0,0 +1,9 @@
package tea
type nilRenderer struct{}
func (n nilRenderer) start() {}
func (n nilRenderer) stop() {}
func (n nilRenderer) write(v string) {}
func (n nilRenderer) altScreen() bool { return false }
func (n nilRenderer) setAltScreen(v bool) {}

View File

@ -1,390 +1,9 @@
package tea
import (
"bytes"
"io"
"strings"
"sync"
"time"
"github.com/muesli/reflow/truncate"
)
const (
// defaultFramerate specifies the maximum interval at which we should
// update the view.
defaultFramerate = time.Second / 60
)
// renderer is a timer-based renderer, updating the view at a given framerate
// to avoid overloading the terminal emulator.
//
// 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 renderer struct {
out io.Writer
buf bytes.Buffer
framerate time.Duration
ticker *time.Ticker
mtx *sync.Mutex
done chan struct{}
lastRender string
linesRendered int
// essentially whether or not we're using the full size of the terminal
altScreenActive bool
// renderer dimensions; usually the size of the window
width int
height int
// lines not to render
ignoreLines map[int]struct{}
}
// newRenderer creates a new renderer. Normally you'll want to initialize it
// with os.Stdout as the first argument.
func newRenderer(out io.Writer, mtx *sync.Mutex) *renderer {
return &renderer{
out: out,
mtx: mtx,
framerate: defaultFramerate,
}
}
// start starts the renderer.
func (r *renderer) start() {
if r.ticker == nil {
r.ticker = time.NewTicker(r.framerate)
}
r.done = make(chan struct{})
go r.listen()
}
// stop permanently halts the renderer.
func (r *renderer) stop() {
r.flush()
r.done <- struct{}{}
}
// listen waits for ticks on the ticker, or a signal to stop the renderer.
func (r *renderer) listen() {
for {
select {
case <-r.ticker.C:
if r.ticker != nil {
r.flush()
}
case <-r.done:
r.mtx.Lock()
r.ticker.Stop()
r.ticker = nil
r.mtx.Unlock()
close(r.done)
return
}
}
}
// flush renders the buffer.
func (r *renderer) flush() {
r.mtx.Lock()
defer r.mtx.Unlock()
if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
// Nothing to do
return
}
out := new(bytes.Buffer)
// Clear any lines we painted in the last render.
if r.linesRendered > 0 {
for i := r.linesRendered - 1; i > 0; i-- {
// Check if we should skip rendering for this line. Clearing the
// line before painting is part of the standard rendering routine.
if _, exists := r.ignoreLines[i]; !exists {
clearLine(out)
}
cursorUp(out)
}
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[ + <n> + F) were better supported
// we could use that above to eliminate this step.
cursorBack(out, r.width)
clearLine(out)
}
}
r.linesRendered = 0
lines := strings.Split(r.buf.String(), "\n")
// Paint new lines
for i := 0; i < len(lines); i++ {
if _, exists := r.ignoreLines[r.linesRendered]; exists {
cursorDown(out) // skip rendering for this line.
} else {
line := lines[i]
// Truncate lines wider than the width of the window to avoid
// rendering troubles. If we don't have the width of the window
// this will be ignored.
//
// Note that on Windows we can't get the width of the window
// (signal SIGWINCH is not supported), so this will be ignored.
if r.width > 0 {
line = truncate.String(line, uint(r.width))
}
_, _ = io.WriteString(out, line)
if i != len(lines)-1 {
_, _ = io.WriteString(out, "\r\n")
}
}
r.linesRendered++
}
// Make sure the cursor is at the start of the last line to keep rendering
// behavior consistent.
if r.altScreenActive {
// We need this case to fix a bug in macOS terminal. In other terminals
// the below case seems to do the job regardless of whether or not we're
// using the full terminal window.
moveCursor(out, r.linesRendered, 0)
} else {
cursorBack(out, r.width)
}
_, _ = r.out.Write(out.Bytes())
r.lastRender = r.buf.String()
r.buf.Reset()
}
// write writes to the internal buffer. The buffer will be outputted via the
// ticker which calls flush().
func (r *renderer) write(s string) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.buf.Reset()
_, _ = r.buf.WriteString(s)
}
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
// renderer.
func (r *renderer) setIgnoredLines(from int, to int) {
// Lock if we're going to be clearing some lines since we don't want
// anything jacking our cursor.
if r.linesRendered > 0 {
r.mtx.Lock()
defer r.mtx.Unlock()
}
if r.ignoreLines == nil {
r.ignoreLines = make(map[int]struct{})
}
for i := from; i < to; i++ {
r.ignoreLines[i] = struct{}{}
}
// Erase ignored lines
if r.linesRendered > 0 {
out := new(bytes.Buffer)
for i := r.linesRendered - 1; i >= 0; i-- {
if _, exists := r.ignoreLines[i]; exists {
clearLine(out)
}
cursorUp(out)
}
moveCursor(out, r.linesRendered, 0) // put cursor back
_, _ = r.out.Write(out.Bytes())
}
}
// clearIgnoredLines returns control of any ignored lines to the standard
// Bubble Tea renderer. That is, any lines previously set to be ignored can be
// rendered to again.
func (r *renderer) clearIgnoredLines() {
r.ignoreLines = nil
}
// insertTop effectively scrolls up. It inserts lines at the top of a given
// area designated to be a scrollable region, pushing everything else down.
// This is roughly how ncurses does it.
//
// To call this function use command ScrollUp().
//
// For this to work renderer.ignoreLines must be set to ignore the scrollable
// region since we are bypassing the normal Bubble Tea renderer here.
//
// Because this method relies on the terminal dimensions, it's only valid for
// full-window applications (generally those that use the alternate screen
// buffer).
//
// This method bypasses the normal rendering buffer and is philosophically
// different than the normal way we approach rendering in Bubble Tea. It's for
// use in high-performance rendering, such as a pager that could potentially
// be rendering very complicated ansi. In cases where the content is simpler
// standard Bubble Tea rendering should suffice.
func (r *renderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
r.mtx.Lock()
defer r.mtx.Unlock()
b := new(bytes.Buffer)
changeScrollingRegion(b, topBoundary, bottomBoundary)
moveCursor(b, topBoundary, 0)
insertLine(b, len(lines))
_, _ = io.WriteString(b, strings.Join(lines, "\r\n"))
changeScrollingRegion(b, 0, r.height)
// Move cursor back to where the main rendering routine expects it to be
moveCursor(b, r.linesRendered, 0)
_, _ = r.out.Write(b.Bytes())
}
// insertBottom effectively scrolls down. It inserts lines at the bottom of
// a given area designated to be a scrollable region, pushing everything else
// up. This is roughly how ncurses does it.
//
// To call this function use the command ScrollDown().
//
// See note in insertTop() for caveats, how this function only makes sense for
// full-window applications, and how it differs from the normal way we do
// rendering in Bubble Tea.
func (r *renderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
r.mtx.Lock()
defer r.mtx.Unlock()
b := new(bytes.Buffer)
changeScrollingRegion(b, topBoundary, bottomBoundary)
moveCursor(b, bottomBoundary, 0)
_, _ = io.WriteString(b, "\r\n"+strings.Join(lines, "\r\n"))
changeScrollingRegion(b, 0, r.height)
// Move cursor back to where the main rendering routine expects it to be
moveCursor(b, r.linesRendered, 0)
_, _ = r.out.Write(b.Bytes())
}
// handleMessages handles internal messages for the renderer.
func (r *renderer) handleMessages(msg Msg) {
switch msg := msg.(type) {
case WindowSizeMsg:
r.width = msg.Width
r.height = msg.Height
case clearScrollAreaMsg:
r.clearIgnoredLines()
// Force a repaint on the area where the scrollable stuff was in this
// update cycle
r.mtx.Lock()
r.lastRender = ""
r.mtx.Unlock()
case syncScrollAreaMsg:
// Re-render scrolling area
r.clearIgnoredLines()
r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
// Force non-scrolling stuff to repaint in this update cycle
r.mtx.Lock()
r.lastRender = ""
r.mtx.Unlock()
case scrollUpMsg:
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
case scrollDownMsg:
r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
}
}
// HIGH-PERFORMANCE RENDERING STUFF
type syncScrollAreaMsg struct {
lines []string
topBoundary int
bottomBoundary int
}
// SyncScrollArea performs a paint of the entire region designated to be the
// scrollable area. This is required to initialize the scrollable region and
// should also be called on resize (WindowSizeMsg).
//
// For high-performance, scroll-based rendering only.
func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
return func() Msg {
return syncScrollAreaMsg{
lines: lines,
topBoundary: topBoundary,
bottomBoundary: bottomBoundary,
}
}
}
type clearScrollAreaMsg struct{}
// ClearScrollArea deallocates the scrollable region and returns the control of
// those lines to the main rendering routine.
//
// For high-performance, scroll-based rendering only.
func ClearScrollArea() Msg {
return clearScrollAreaMsg{}
}
type scrollUpMsg struct {
lines []string
topBoundary int
bottomBoundary int
}
// ScrollUp adds lines to the top of the scrollable region, pushing existing
// lines below down. Lines that are pushed out the scrollable region disappear
// from view.
//
// For high-performance, scroll-based rendering only.
func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
return func() Msg {
return scrollUpMsg{
lines: newLines,
topBoundary: topBoundary,
bottomBoundary: bottomBoundary,
}
}
}
type scrollDownMsg struct {
lines []string
topBoundary int
bottomBoundary int
}
// ScrollDown adds lines to the bottom of the scrollable region, pushing
// existing lines above up. Lines that are pushed out of the scrollable region
// disappear from view.
//
// For high-performance, scroll-based rendering only.
func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
return func() Msg {
return scrollDownMsg{
lines: newLines,
topBoundary: topBoundary,
bottomBoundary: bottomBoundary,
}
}
type renderer interface {
start()
stop()
write(string)
altScreen() bool
setAltScreen(bool)
}

398
standard_renderer.go Normal file
View File

@ -0,0 +1,398 @@
package tea
import (
"bytes"
"io"
"strings"
"sync"
"time"
"github.com/muesli/reflow/truncate"
)
const (
// defaultFramerate specifies the maximum interval at which we should
// update the view.
defaultFramerate = time.Second / 60
)
// renderer is a timer-based renderer, updating the view at a given framerate
// to avoid overloading the terminal emulator.
//
// 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
framerate time.Duration
ticker *time.Ticker
mtx *sync.Mutex
done chan struct{}
lastRender string
linesRendered int
// essentially whether or not we're using the full size of the terminal
altScreenActive bool
// renderer dimensions; usually the size of the window
width int
height int
// lines not to render
ignoreLines map[int]struct{}
}
// newRenderer creates a new renderer. Normally you'll want to initialize it
// with os.Stdout as the first argument.
func newRenderer(out io.Writer, mtx *sync.Mutex) renderer {
return &standardRenderer{
out: out,
mtx: mtx,
framerate: defaultFramerate,
}
}
// start starts the renderer.
func (r *standardRenderer) start() {
if r.ticker == nil {
r.ticker = time.NewTicker(r.framerate)
}
r.done = make(chan struct{})
go r.listen()
}
// stop permanently halts the renderer.
func (r *standardRenderer) stop() {
r.flush()
r.done <- struct{}{}
}
// listen waits for ticks on the ticker, or a signal to stop the renderer.
func (r *standardRenderer) listen() {
for {
select {
case <-r.ticker.C:
if r.ticker != nil {
r.flush()
}
case <-r.done:
r.mtx.Lock()
r.ticker.Stop()
r.ticker = nil
r.mtx.Unlock()
close(r.done)
return
}
}
}
// flush renders the buffer.
func (r *standardRenderer) flush() {
if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
// Nothing to do
return
}
out := new(bytes.Buffer)
r.mtx.Lock()
defer r.mtx.Unlock()
// Clear any lines we painted in the last render.
if r.linesRendered > 0 {
for i := r.linesRendered - 1; i > 0; i-- {
// Check if we should skip rendering for this line. Clearing the
// line before painting is part of the standard rendering routine.
if _, exists := r.ignoreLines[i]; !exists {
clearLine(out)
}
cursorUp(out)
}
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[ + <n> + F) were better supported
// we could use that above to eliminate this step.
cursorBack(out, r.width)
clearLine(out)
}
}
r.linesRendered = 0
lines := strings.Split(r.buf.String(), "\n")
// Paint new lines
for i := 0; i < len(lines); i++ {
if _, exists := r.ignoreLines[r.linesRendered]; exists {
cursorDown(out) // skip rendering for this line.
} else {
line := lines[i]
// Truncate lines wider than the width of the window to avoid
// rendering troubles. If we don't have the width of the window
// this will be ignored.
//
// Note that on Windows we can't get the width of the window
// (signal SIGWINCH is not supported), so this will be ignored.
if r.width > 0 {
line = truncate.String(line, uint(r.width))
}
_, _ = io.WriteString(out, line)
if i != len(lines)-1 {
_, _ = io.WriteString(out, "\r\n")
}
}
r.linesRendered++
}
// Make sure the cursor is at the start of the last line to keep rendering
// behavior consistent.
if r.altScreenActive {
// We need this case to fix a bug in macOS terminal. In other terminals
// the below case seems to do the job regardless of whether or not we're
// using the full terminal window.
moveCursor(out, r.linesRendered, 0)
} else {
cursorBack(out, r.width)
}
_, _ = r.out.Write(out.Bytes())
r.lastRender = r.buf.String()
r.buf.Reset()
}
// write writes to the internal buffer. The buffer will be outputted via the
// ticker which calls flush().
func (r *standardRenderer) write(s string) {
r.mtx.Lock()
defer r.mtx.Unlock()
r.buf.Reset()
_, _ = r.buf.WriteString(s)
}
func (r *standardRenderer) altScreen() bool {
return r.altScreenActive
}
func (r *standardRenderer) setAltScreen(v bool) {
r.altScreenActive = v
}
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
// renderer.
func (r *standardRenderer) setIgnoredLines(from int, to int) {
// Lock if we're going to be clearing some lines since we don't want
// anything jacking our cursor.
if r.linesRendered > 0 {
r.mtx.Lock()
defer r.mtx.Unlock()
}
if r.ignoreLines == nil {
r.ignoreLines = make(map[int]struct{})
}
for i := from; i < to; i++ {
r.ignoreLines[i] = struct{}{}
}
// Erase ignored lines
if r.linesRendered > 0 {
out := new(bytes.Buffer)
for i := r.linesRendered - 1; i >= 0; i-- {
if _, exists := r.ignoreLines[i]; exists {
clearLine(out)
}
cursorUp(out)
}
moveCursor(out, r.linesRendered, 0) // put cursor back
_, _ = r.out.Write(out.Bytes())
}
}
// clearIgnoredLines returns control of any ignored lines to the standard
// Bubble Tea renderer. That is, any lines previously set to be ignored can be
// rendered to again.
func (r *standardRenderer) clearIgnoredLines() {
r.ignoreLines = nil
}
// insertTop effectively scrolls up. It inserts lines at the top of a given
// area designated to be a scrollable region, pushing everything else down.
// This is roughly how ncurses does it.
//
// To call this function use command ScrollUp().
//
// For this to work renderer.ignoreLines must be set to ignore the scrollable
// region since we are bypassing the normal Bubble Tea renderer here.
//
// Because this method relies on the terminal dimensions, it's only valid for
// full-window applications (generally those that use the alternate screen
// buffer).
//
// This method bypasses the normal rendering buffer and is philosophically
// different than the normal way we approach rendering in Bubble Tea. It's for
// use in high-performance rendering, such as a pager that could potentially
// be rendering very complicated ansi. In cases where the content is simpler
// standard Bubble Tea rendering should suffice.
func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
r.mtx.Lock()
defer r.mtx.Unlock()
b := new(bytes.Buffer)
changeScrollingRegion(b, topBoundary, bottomBoundary)
moveCursor(b, topBoundary, 0)
insertLine(b, len(lines))
_, _ = io.WriteString(b, strings.Join(lines, "\r\n"))
changeScrollingRegion(b, 0, r.height)
// Move cursor back to where the main rendering routine expects it to be
moveCursor(b, r.linesRendered, 0)
_, _ = r.out.Write(b.Bytes())
}
// insertBottom effectively scrolls down. It inserts lines at the bottom of
// a given area designated to be a scrollable region, pushing everything else
// up. This is roughly how ncurses does it.
//
// To call this function use the command ScrollDown().
//
// See note in insertTop() for caveats, how this function only makes sense for
// full-window applications, and how it differs from the normal way we do
// rendering in Bubble Tea.
func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
r.mtx.Lock()
defer r.mtx.Unlock()
b := new(bytes.Buffer)
changeScrollingRegion(b, topBoundary, bottomBoundary)
moveCursor(b, bottomBoundary, 0)
_, _ = io.WriteString(b, "\r\n"+strings.Join(lines, "\r\n"))
changeScrollingRegion(b, 0, r.height)
// Move cursor back to where the main rendering routine expects it to be
moveCursor(b, r.linesRendered, 0)
_, _ = r.out.Write(b.Bytes())
}
// handleMessages handles internal messages for the renderer.
func (r *standardRenderer) handleMessages(msg Msg) {
switch msg := msg.(type) {
case WindowSizeMsg:
r.width = msg.Width
r.height = msg.Height
case clearScrollAreaMsg:
r.clearIgnoredLines()
// Force a repaint on the area where the scrollable stuff was in this
// update cycle
r.mtx.Lock()
r.lastRender = ""
r.mtx.Unlock()
case syncScrollAreaMsg:
// Re-render scrolling area
r.clearIgnoredLines()
r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
// Force non-scrolling stuff to repaint in this update cycle
r.mtx.Lock()
r.lastRender = ""
r.mtx.Unlock()
case scrollUpMsg:
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
case scrollDownMsg:
r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
}
}
// HIGH-PERFORMANCE RENDERING STUFF
type syncScrollAreaMsg struct {
lines []string
topBoundary int
bottomBoundary int
}
// SyncScrollArea performs a paint of the entire region designated to be the
// scrollable area. This is required to initialize the scrollable region and
// should also be called on resize (WindowSizeMsg).
//
// For high-performance, scroll-based rendering only.
func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
return func() Msg {
return syncScrollAreaMsg{
lines: lines,
topBoundary: topBoundary,
bottomBoundary: bottomBoundary,
}
}
}
type clearScrollAreaMsg struct{}
// ClearScrollArea deallocates the scrollable region and returns the control of
// those lines to the main rendering routine.
//
// For high-performance, scroll-based rendering only.
func ClearScrollArea() Msg {
return clearScrollAreaMsg{}
}
type scrollUpMsg struct {
lines []string
topBoundary int
bottomBoundary int
}
// ScrollUp adds lines to the top of the scrollable region, pushing existing
// lines below down. Lines that are pushed out the scrollable region disappear
// from view.
//
// For high-performance, scroll-based rendering only.
func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
return func() Msg {
return scrollUpMsg{
lines: newLines,
topBoundary: topBoundary,
bottomBoundary: bottomBoundary,
}
}
}
type scrollDownMsg struct {
lines []string
topBoundary int
bottomBoundary int
}
// ScrollDown adds lines to the bottom of the scrollable region, pushing
// existing lines above up. Lines that are pushed out of the scrollable region
// disappear from view.
//
// For high-performance, scroll-based rendering only.
func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
return func() Msg {
return scrollDownMsg{
lines: newLines,
topBoundary: topBoundary,
bottomBoundary: bottomBoundary,
}
}
}

30
tea.go
View File

@ -98,6 +98,20 @@ func WithoutCatchPanics() ProgramOption {
}
}
// WithoutRenderer disables the renderer. When this is set output and log
// statements will be plainly sent to stdout (or another output if one is set)
// without any rendering and redrawing logic. In other words, printing and
// logging will behave the same way it would in a non-TUI commandline tool.
// This can be useful if you want to use the Bubble Tea framework for a non-TUI
// application, or to provide an additional non-TUI mode to your Bubble Tea
// programs. For example, your program could behave like a daemon if output is
// not a TTY.
func WithoutRenderer() ProgramOption {
return func(m *Program) {
m.renderer = &nilRenderer{}
}
}
// inputStatus indicates the current state of the input. By default, input is
// stdin, however we'll change this if input's not a TTY. The user can also set
// the input.
@ -125,7 +139,7 @@ type Program struct {
output io.Writer // where to send output. this will usually be os.Stdout.
input io.Reader // this will usually be os.Stdin.
renderer *renderer
renderer renderer
altScreenActive bool
// CatchPanics is incredibly useful for restoring the terminal to a usable
@ -263,7 +277,10 @@ func (p *Program) Start() error {
}()
}
// If no renderer is set use the standard one.
if p.renderer == nil {
p.renderer = newRenderer(p.output, p.mtx)
}
// Check if output is a TTY before entering raw mode, hiding the cursor and
// so on.
@ -286,7 +303,7 @@ func (p *Program) Start() error {
// Start renderer
p.renderer.start()
p.renderer.altScreenActive = p.altScreenActive
p.renderer.setAltScreen(p.altScreenActive)
// Render initial view
p.renderer.write(model.View())
@ -365,7 +382,10 @@ func (p *Program) Start() error {
}
// Process internal messages for the renderer
p.renderer.handleMessages(msg)
if r, ok := p.renderer.(*standardRenderer); ok {
r.handleMessages(msg)
}
var cmd Cmd
model, cmd = model.Update(msg) // run update
cmds <- cmd // process command (if any)
@ -384,7 +404,7 @@ func (p *Program) EnterAltScreen() {
p.altScreenActive = true
if p.renderer != nil {
p.renderer.altScreenActive = p.altScreenActive
p.renderer.setAltScreen(p.altScreenActive)
}
}
@ -396,7 +416,7 @@ func (p *Program) ExitAltScreen() {
p.altScreenActive = false
if p.renderer != nil {
p.renderer.altScreenActive = p.altScreenActive
p.renderer.setAltScreen(p.altScreenActive)
}
}