From ea36e19bee0d8ca0c53ffffa2991394f6485b651 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Tue, 4 Oct 2022 00:04:05 +0200 Subject: [PATCH] fix: move output handling to renderer --- nil_renderer.go | 21 +++++--- nil_renderer_test.go | 2 +- renderer.go | 26 ++++++++-- standard_renderer.go | 72 ++++++++++++++++++++++++--- tea.go | 113 ++++++++++++++----------------------------- 5 files changed, 139 insertions(+), 95 deletions(-) diff --git a/nil_renderer.go b/nil_renderer.go index 03dd949..37b3c45 100644 --- a/nil_renderer.go +++ b/nil_renderer.go @@ -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() {} diff --git a/nil_renderer_test.go b/nil_renderer_test.go index 7364484..9ab7b58 100644 --- a/nil_renderer_test.go +++ b/nil_renderer_test.go @@ -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") } diff --git a/renderer.go b/renderer.go index aedbb66..a975e33 100644 --- a/renderer.go +++ b/renderer.go @@ -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. diff --git a/standard_renderer.go b/standard_renderer.go index 6c1c50b..feafb82 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -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: diff --git a/tea.go b/tea.go index 9e5d87e..5533f63 100644 --- a/tea.go +++ b/tea.go @@ -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 }