fix: move output handling to renderer

This commit is contained in:
Christian Muehlhaeuser 2022-10-04 00:04:05 +02:00
parent 7cf0d54bd4
commit ea36e19bee
5 changed files with 139 additions and 95 deletions

View File

@ -2,10 +2,17 @@ package tea
type nilRenderer struct{}
func (n nilRenderer) start() {}
func (n nilRenderer) stop() {}
func (n nilRenderer) kill() {}
func (n nilRenderer) write(v string) {}
func (n nilRenderer) repaint() {}
func (n nilRenderer) altScreen() bool { return false }
func (n nilRenderer) setAltScreen(v bool) {}
func (n nilRenderer) start() {}
func (n nilRenderer) stop() {}
func (n nilRenderer) kill() {}
func (n nilRenderer) write(v string) {}
func (n nilRenderer) repaint() {}
func (n nilRenderer) altScreen() bool { return false }
func (n nilRenderer) enterAltScreen() {}
func (n nilRenderer) exitAltScreen() {}
func (n nilRenderer) showCursor() {}
func (n nilRenderer) hideCursor() {}
func (n nilRenderer) enableMouseCellMotion() {}
func (n nilRenderer) disableMouseCellMotion() {}
func (n nilRenderer) enableMouseAllMotion() {}
func (n nilRenderer) disableMouseAllMotion() {}

View File

@ -9,7 +9,7 @@ func TestNilRenderer(t *testing.T) {
r.kill()
r.write("a")
r.repaint()
r.setAltScreen(true)
r.enterAltScreen()
if r.altScreen() {
t.Errorf("altScreen should always return false")
}

View File

@ -23,10 +23,30 @@ type renderer interface {
// Whether or not the alternate screen buffer is enabled.
altScreen() bool
// Enable the alternate screen buffer.
enterAltScreen()
// Disable the alternate screen buffer.
exitAltScreen()
// Record internally that the alternate screen buffer is enabled. This
// does not actually toggle the alternate screen buffer.
setAltScreen(bool)
// Show the cursor.
showCursor()
// Hide the cursor.
hideCursor()
// enableMouseCellMotion enables mouse click, release, wheel and motion
// events if a mouse button is pressed (i.e., drag events).
enableMouseCellMotion()
// DisableMouseCellMotion disables Mouse Cell Motion tracking.
disableMouseCellMotion()
// EnableMouseAllMotion enables mouse click, release, wheel and motion
// events, regardless of whether a mouse button is pressed. Many modern
// terminals support this, but not all.
enableMouseAllMotion()
// DisableMouseAllMotion disables All Motion mouse tracking.
disableMouseAllMotion()
}
// repaintMsg forces a full repaint.

View File

@ -25,12 +25,13 @@ 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 *termenv.Output
mtx *sync.Mutex
out *termenv.Output
buf bytes.Buffer
queuedMessageLines []string
framerate time.Duration
ticker *time.Ticker
mtx *sync.Mutex
done chan struct{}
lastRender string
linesRendered int
@ -50,10 +51,10 @@ 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, mtx *sync.Mutex, useANSICompressor bool) renderer {
func newRenderer(out *termenv.Output, useANSICompressor bool) renderer {
r := &standardRenderer{
out: out,
mtx: mtx,
mtx: &sync.Mutex{},
framerate: defaultFramerate,
useANSICompressor: useANSICompressor,
queuedMessageLines: []string{},
@ -248,11 +249,69 @@ func (r *standardRenderer) altScreen() bool {
return r.altScreenActive
}
func (r *standardRenderer) setAltScreen(v bool) {
r.altScreenActive = v
func (r *standardRenderer) enterAltScreen() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.altScreenActive = true
r.out.AltScreen()
r.out.MoveCursor(1, 1)
r.repaint()
}
func (r *standardRenderer) exitAltScreen() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.altScreenActive = false
r.out.ExitAltScreen()
r.repaint()
}
func (r *standardRenderer) showCursor() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.ShowCursor()
}
func (r *standardRenderer) hideCursor() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.HideCursor()
}
func (r *standardRenderer) enableMouseCellMotion() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.EnableMouseCellMotion()
}
func (r *standardRenderer) disableMouseCellMotion() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.DisableMouseCellMotion()
}
func (r *standardRenderer) enableMouseAllMotion() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.EnableMouseAllMotion()
}
func (r *standardRenderer) disableMouseAllMotion() {
r.mtx.Lock()
defer r.mtx.Unlock()
r.out.DisableMouseAllMotion()
}
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
// renderer.
func (r *standardRenderer) setIgnoredLines(from int, to int) {
@ -371,6 +430,7 @@ func (r *standardRenderer) handleMessages(msg Msg) {
r.mtx.Lock()
r.width = msg.Width
r.height = msg.Height
r.repaint()
r.mtx.Unlock()
case clearScrollAreaMsg:

113
tea.go
View File

@ -16,7 +16,6 @@ import (
"os"
"os/signal"
"runtime/debug"
"sync"
"syscall"
"time"
@ -83,7 +82,6 @@ type Program struct {
startupOptions startupOptions
ctx context.Context
mtx *sync.Mutex
msgs chan Msg
errs chan error
@ -95,7 +93,6 @@ type Program struct {
cancelReader cancelreader.CancelReader
renderer renderer
altScreenActive bool
altScreenWasActive bool // was the altscreen active before releasing the terminal?
// CatchPanics is incredibly useful for restoring the terminal to a usable
@ -252,7 +249,6 @@ type hideCursorMsg struct{}
// NewProgram creates a new Program.
func NewProgram(model Model, opts ...ProgramOption) *Program {
p := &Program{
mtx: &sync.Mutex{},
initialModel: model,
input: os.Stdin,
msgs: make(chan Msg),
@ -389,17 +385,17 @@ func (p *Program) StartReturningModel() (Model, error) {
// If no renderer is set use the standard one.
if p.renderer == nil {
p.renderer = newRenderer(p.output, p.mtx, p.startupOptions.has(withANSICompressor))
p.renderer = newRenderer(p.output, p.startupOptions.has(withANSICompressor))
}
// Honor program startup options.
if p.startupOptions&withAltScreen != 0 {
p.EnterAltScreen()
p.renderer.enterAltScreen()
}
if p.startupOptions&withMouseCellMotion != 0 {
p.EnableMouseCellMotion()
p.renderer.enableMouseCellMotion()
} else if p.startupOptions&withMouseAllMotion != 0 {
p.EnableMouseAllMotion()
p.renderer.enableMouseAllMotion()
}
// Initialize the program.
@ -418,7 +414,6 @@ func (p *Program) StartReturningModel() (Model, error) {
// Start the renderer.
p.renderer.start()
p.renderer.setAltScreen(p.altScreenActive)
// Render the initial view.
p.renderer.write(model.View())
@ -503,40 +498,35 @@ func (p *Program) StartReturningModel() (Model, error) {
p.shutdown(false)
return model, nil
case enterAltScreenMsg:
p.renderer.enterAltScreen()
case exitAltScreenMsg:
p.renderer.exitAltScreen()
case enableMouseCellMotionMsg:
p.renderer.enableMouseCellMotion()
case enableMouseAllMotionMsg:
p.renderer.enableMouseAllMotion()
case disableMouseMsg:
p.renderer.disableMouseCellMotion()
p.renderer.disableMouseAllMotion()
case hideCursorMsg:
p.renderer.hideCursor()
case execMsg:
// NB: this blocks.
p.exec(msg.cmd, msg.fn)
case batchMsg:
for _, cmd := range msg {
cmds <- cmd
}
continue
case WindowSizeMsg:
p.mtx.Lock()
p.renderer.repaint()
p.mtx.Unlock()
case enterAltScreenMsg:
p.EnterAltScreen()
case exitAltScreenMsg:
p.ExitAltScreen()
case enableMouseCellMotionMsg:
p.EnableMouseCellMotion()
case enableMouseAllMotionMsg:
p.EnableMouseAllMotion()
case disableMouseMsg:
p.DisableMouseCellMotion()
p.DisableMouseAllMotion()
case hideCursorMsg:
p.output.HideCursor()
case execMsg:
// NB: this blocks.
p.exec(msg.cmd, msg.fn)
case sequenceMsg:
go func() {
// Execute commands one at a time, in order.
@ -621,19 +611,8 @@ func (p *Program) shutdown(kill bool) {
//
// Deprecated: Use the WithAltScreen ProgramOption instead.
func (p *Program) EnterAltScreen() {
p.mtx.Lock()
defer p.mtx.Unlock()
if p.altScreenActive {
return
}
p.output.AltScreen()
p.output.MoveCursor(1, 1)
p.altScreenActive = true
if p.renderer != nil {
p.renderer.setAltScreen(p.altScreenActive)
p.renderer.enterAltScreen()
}
}
@ -641,18 +620,8 @@ func (p *Program) EnterAltScreen() {
//
// Deprecated: The altscreen will exited automatically when the program exits.
func (p *Program) ExitAltScreen() {
p.mtx.Lock()
defer p.mtx.Unlock()
if !p.altScreenActive {
return
}
p.output.ExitAltScreen()
p.altScreenActive = false
if p.renderer != nil {
p.renderer.setAltScreen(p.altScreenActive)
p.renderer.exitAltScreen()
}
}
@ -661,10 +630,7 @@ func (p *Program) ExitAltScreen() {
//
// Deprecated: Use the WithMouseCellMotion ProgramOption instead.
func (p *Program) EnableMouseCellMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
p.output.EnableMouseCellMotion()
p.renderer.enableMouseCellMotion()
}
// DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be
@ -672,10 +638,7 @@ func (p *Program) EnableMouseCellMotion() {
//
// Deprecated: The mouse will automatically be disabled when the program exits.
func (p *Program) DisableMouseCellMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
p.output.DisableMouseCellMotion()
p.renderer.disableMouseCellMotion()
}
// EnableMouseAllMotion enables mouse click, release, wheel and motion events,
@ -684,10 +647,7 @@ func (p *Program) DisableMouseCellMotion() {
//
// Deprecated: Use the WithMouseAllMotion ProgramOption instead.
func (p *Program) EnableMouseAllMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
p.output.EnableMouseAllMotion()
p.renderer.enableMouseAllMotion()
}
// DisableMouseAllMotion disables All Motion mouse tracking. This will be
@ -695,10 +655,7 @@ func (p *Program) EnableMouseAllMotion() {
//
// Deprecated: The mouse will automatically be disabled when the program exits.
func (p *Program) DisableMouseAllMotion() {
p.mtx.Lock()
defer p.mtx.Unlock()
p.output.DisableMouseAllMotion()
p.renderer.disableMouseAllMotion()
}
// ReleaseTerminal restores the original terminal state and cancels the input
@ -708,8 +665,8 @@ func (p *Program) ReleaseTerminal() error {
p.cancelInput()
p.waitForReadLoop()
p.altScreenWasActive = p.altScreenActive
if p.altScreenActive {
p.altScreenWasActive = p.renderer.altScreen()
if p.renderer.altScreen() {
p.ExitAltScreen()
time.Sleep(time.Millisecond * 10) // give the terminal a moment to catch up
}