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

@ -8,4 +8,11 @@ func (n nilRenderer) kill() {}
func (n nilRenderer) write(v string) {} func (n nilRenderer) write(v string) {}
func (n nilRenderer) repaint() {} func (n nilRenderer) repaint() {}
func (n nilRenderer) altScreen() bool { return false } func (n nilRenderer) altScreen() bool { return false }
func (n nilRenderer) setAltScreen(v bool) {} 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.kill()
r.write("a") r.write("a")
r.repaint() r.repaint()
r.setAltScreen(true) r.enterAltScreen()
if r.altScreen() { if r.altScreen() {
t.Errorf("altScreen should always return false") 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. // Whether or not the alternate screen buffer is enabled.
altScreen() bool altScreen() bool
// Enable the alternate screen buffer.
enterAltScreen()
// Disable the alternate screen buffer.
exitAltScreen()
// Record internally that the alternate screen buffer is enabled. This // Show the cursor.
// does not actually toggle the alternate screen buffer. showCursor()
setAltScreen(bool) // 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. // 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 // 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. // to exclude ranges of lines, allowing them to be written to directly.
type standardRenderer struct { type standardRenderer struct {
mtx *sync.Mutex
out *termenv.Output out *termenv.Output
buf bytes.Buffer buf bytes.Buffer
queuedMessageLines []string queuedMessageLines []string
framerate time.Duration framerate time.Duration
ticker *time.Ticker ticker *time.Ticker
mtx *sync.Mutex
done chan struct{} done chan struct{}
lastRender string lastRender string
linesRendered int linesRendered int
@ -50,10 +51,10 @@ type standardRenderer struct {
// newRenderer creates a new renderer. Normally you'll want to initialize it // newRenderer creates a new renderer. Normally you'll want to initialize it
// with os.Stdout as the first argument. // 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{ r := &standardRenderer{
out: out, out: out,
mtx: mtx, mtx: &sync.Mutex{},
framerate: defaultFramerate, framerate: defaultFramerate,
useANSICompressor: useANSICompressor, useANSICompressor: useANSICompressor,
queuedMessageLines: []string{}, queuedMessageLines: []string{},
@ -248,11 +249,69 @@ func (r *standardRenderer) altScreen() bool {
return r.altScreenActive return r.altScreenActive
} }
func (r *standardRenderer) setAltScreen(v bool) { func (r *standardRenderer) enterAltScreen() {
r.altScreenActive = v r.mtx.Lock()
defer r.mtx.Unlock()
r.altScreenActive = true
r.out.AltScreen()
r.out.MoveCursor(1, 1)
r.repaint() 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 // setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
// renderer. // renderer.
func (r *standardRenderer) setIgnoredLines(from int, to int) { func (r *standardRenderer) setIgnoredLines(from int, to int) {
@ -371,6 +430,7 @@ func (r *standardRenderer) handleMessages(msg Msg) {
r.mtx.Lock() r.mtx.Lock()
r.width = msg.Width r.width = msg.Width
r.height = msg.Height r.height = msg.Height
r.repaint()
r.mtx.Unlock() r.mtx.Unlock()
case clearScrollAreaMsg: case clearScrollAreaMsg:

113
tea.go
View File

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