From 85ab47669894128e867ea6e4cc8c2b5842a13476 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Wed, 19 May 2021 20:52:01 -0400 Subject: [PATCH] Add ProgramOptions for entering the altscreen and enabling the mouse Because they run ansyncronously, the analogous commands, when run in a Model's Init() function, could fire after the renderer has starting writing to output, rendering artifacts to the commandline. --- examples/fullscreen/main.go | 3 +- examples/mouse/main.go | 5 +- examples/pager/main.go | 14 +-- tea.go | 214 +++++++++++++++++++++++++++--------- 4 files changed, 176 insertions(+), 60 deletions(-) diff --git a/examples/fullscreen/main.go b/examples/fullscreen/main.go index 62259c7..9c27d4b 100644 --- a/examples/fullscreen/main.go +++ b/examples/fullscreen/main.go @@ -16,7 +16,8 @@ type model int type tickMsg time.Time func main() { - if err := tea.NewProgram(model(5)).Start(); err != nil { + p := tea.NewProgram(model(5), tea.WithAltScreen()) + if err := p.Start(); err != nil { log.Fatal(err) } } diff --git a/examples/mouse/main.go b/examples/mouse/main.go index df2371e..f3ba0bd 100644 --- a/examples/mouse/main.go +++ b/examples/mouse/main.go @@ -11,7 +11,8 @@ import ( ) func main() { - if err := tea.NewProgram(model{}).Start(); err != nil { + p := tea.NewProgram(model{}, tea.WithAltScreen(), tea.WithMouseAllMotion()) + if err := p.Start(); err != nil { log.Fatal(err) } } @@ -22,7 +23,7 @@ type model struct { } func (m model) Init() tea.Cmd { - return tea.Batch(tea.EnterAltScreen, tea.EnableMouseAllMotion) + return nil } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { diff --git a/examples/pager/main.go b/examples/pager/main.go index ca5df3f..cd7038f 100644 --- a/examples/pager/main.go +++ b/examples/pager/main.go @@ -53,15 +53,15 @@ func main() { defer f.Close() } - p := tea.NewProgram(model{content: string(content)}) + p := tea.NewProgram( + model{content: string(content)}, - // Use the full size of the terminal in its "alternate screen buffer" - p.EnterAltScreen() - defer p.ExitAltScreen() + // Use the full size of the terminal in its "alternate screen buffer" + tea.WithAltScreen(), - // We also turn on mouse support so we can track the mouse wheel - p.EnableMouseCellMotion() - defer p.DisableMouseCellMotion() + // Also turn on mouse support so we can track the mouse wheel + tea.WithMouseCellMotion(), + ) if err := p.Start(); err != nil { fmt.Println("could not run program:", err) diff --git a/tea.go b/tea.go index cd742c5..4ca03c9 100644 --- a/tea.go +++ b/tea.go @@ -25,7 +25,7 @@ import ( "golang.org/x/term" ) -// Msg represents an action and is usually the result of an IO operation. It's +// Msg represents an action and is usually the result of an IO operation. It // triggers the Update function, and henceforth, the UI. type Msg interface{} @@ -52,23 +52,13 @@ type Model interface { // function. type Cmd func() Msg -// Batch performs a bunch of commands concurrently with no ordering guarantees -// about the results. -func Batch(cmds ...Cmd) Cmd { - if len(cmds) == 0 { - return nil - } - return func() Msg { - return batchMsg(cmds) - } -} - // ProgramOption is used to set options when initializing a Program. Program can // accept a variable number of options. // // Example usage: // // p := NewProgram(model, WithInput(someInput), WithOutput(someOutput)) +// type ProgramOption func(*Program) // WithOutput sets the output which, by default, is stdout. In most cases you @@ -98,6 +88,67 @@ func WithoutCatchPanics() ProgramOption { } } +// WithAltScreen starts the program with the alternate screen buffer enabled +// (i.e. the program starts in full window mode). Note that the altscreen will +// be automatically exited when the program quits. +// +// Example: +// +// p := tea.NewProgram(Model{}, tea.WithAltScreen()) +// if err := p.Start(); err != nil { +// fmt.Println("Error running program:", err) +// os.Exit(1) +// } +// +// To enter the altscreen once the program has already started running use the +// EnterAltScreen command. +func WithAltScreen() ProgramOption { + return func(p *Program) { + p.startupOptions |= withAltScreen + } +} + +// WithMouseCellMotion starts the program with the mouse enabled in "cell +// motion" mode. +// +// Cell motion mode enables mouse click, release, and wheel events. Mouse +// movement events are also captured if a mouse button is pressed (i.e., drag +// events). Cell motion mode is better supported than all motion mode. +// +// To enable mouse cell motion once the program has already started running use +// the EnableMouseCellMotion command. To disable the mouse when the program is +// running use the DisableMouse command. +// +// The mouse will be automatically disabled when the program exits. +func WithMouseCellMotion() ProgramOption { + return func(p *Program) { + p.startupOptions |= withMouseCellMotion // set + p.startupOptions &^= withMouseAllMotion // clear + } +} + +// WithMouseAllMotion starts the program with the mouse enabled in "all motion" +// mode. +// +// EnableMouseAllMotion is a special command that enables mouse click, release, +// wheel, and motion events, which are delivered regardless of whether a mouse +// button is pressed, effectively enabling support for hover interactions. +// +// Many modern terminals support this, but not all. If in doubt, use +// EnableMouseCellMotion instead. +// +// To enable the mouse once the program has already started running use the +// EnableMouseAllMotion command. To disable the mouse when the program is +// running use the DisableMouse command. +// +// The mouse will be automatically disabled when the program exits. +func WithMouseAllMotion() ProgramOption { + return func(p *Program) { + p.startupOptions |= withMouseAllMotion // set + p.startupOptions &^= withMouseCellMotion // clear + } +} + // 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 @@ -112,6 +163,19 @@ func WithoutRenderer() ProgramOption { } } +// startupOptions contains configuration options to be run while the program +// is initializing. +// +// The options here are treated as bits. +type startupOptions byte + +// Available startup options. +const ( + withAltScreen startupOptions = 1 << iota + withMouseCellMotion + withMouseAllMotion +) + // 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. @@ -144,6 +208,10 @@ func (i inputStatus) String() string { type Program struct { initialModel Model + // Configuration options that will set as the program is initializing, + // treated as bits. These options can be set via various ProgramOptions. + startupOptions startupOptions + mtx *sync.Mutex output io.Writer // where to send output. this will usually be os.Stdout. @@ -172,6 +240,28 @@ type Program struct { windowsStdin *os.File //nolint:golint,structcheck,unused } +// Batch performs a bunch of commands concurrently with no ordering guarantees +// about the results. Use a Batch to return several commands. +// +// Example: +// +// func (m model) Init() Cmd { +// return tea.Batch(someCommand, someOtherCommand) +// } +// +func Batch(cmds ...Cmd) Cmd { + if len(cmds) == 0 { + return nil + } + return func() Msg { + return batchMsg(cmds) + } +} + +// batchMsg is the internal message used to perform a bunch of commands. You +// can send a batchMsg with Batch. +type batchMsg []Cmd + // Quit is a special command that tells the Bubble Tea program to exit. func Quit() Msg { return quitMsg{} @@ -183,6 +273,10 @@ type quitMsg struct{} // EnterAltScreen is a special command that tells the Bubble Tea program to // enter alternate screen buffer. +// +// Because commands run asynchronously, this command should not be used in your +// model's Init function. To initialize your program with the altscreen enabled +// use the WithAltScreen ProgramOption instead. func EnterAltScreen() Msg { return enterAltScreenMsg{} } @@ -193,21 +287,45 @@ func EnterAltScreen() Msg { type enterAltScreenMsg struct{} // ExitAltScreen is a special command that tells the Bubble Tea program to exit -// alternate screen buffer. +// the alternate screen buffer. This command should be used to exit the +// alternate screen buffer while the program is running. +// +// Note that the alternate screen buffer will be automatically exited when the +// program quits. func ExitAltScreen() Msg { return exitAltScreenMsg{} } +// exitAltScreenMsg in an internal message signals that the program should exit +// alternate screen buffer. You can send a exitAltScreenMsg with ExitAltScreen. +type exitAltScreenMsg struct{} + +// EnableMouseCellMotion is a special command that enables mouse click, +// release, and wheel events. Mouse movement events are also captured if +// a mouse button is pressed (i.e., drag events). +// +// Because commands run asynchronously, this command should not be used in your +// model's Init function. Use the WithMouseCellMotion ProgramOption instead. +func EnableMouseCellMotion() Msg { + return enableMouseCellMotionMsg{} +} + // enableMouseCellMotionMsg is a special command that signals to start // listening for "cell motion" type mouse events (ESC[?1002l). To send an // enableMouseCellMotionMsg, use the EnableMouseCellMotion command. type enableMouseCellMotionMsg struct{} -// EnableMouseCellMotion is a special command that enables mouse click, -// release, wheel, events. Mouse movement events are also captured if a mouse -// button is pressed (i.e., drag events). -func EnableMouseCellMotion() Msg { - return enableMouseCellMotionMsg{} +// EnableMouseAllMotion is a special command that enables mouse click, release, +// wheel, and motion events, which are delivered regardless of whether a mouse +// button is pressed, effectively enabling support for hover interactions. +// +// Many modern terminals support this, but not all. If in doubt, use +// EnableMouseCellMotion instead. +// +// Because commands run asynchronously, this command should not be used in your +// model's Init function. Use the WithMouseAllMotion ProgramOption instead. +func EnableMouseAllMotion() Msg { + return enableMouseAllMotionMsg{} } // enableMouseAllMotionMsg is a special command that signals to start listening @@ -215,33 +333,19 @@ func EnableMouseCellMotion() Msg { // enableMouseAllMotionMsg, use the EnableMouseAllMotion command. type enableMouseAllMotionMsg struct{} -// EnableMouseAllMotion is a special command that enables mouse click, release, -// and wheel events. Mouse movement events are delivered regardless of whether -// a button is pressed. Many modern terminals support this, but not all. If in -// doubt, use EnableMouseCellMotion instead. -func EnableMouseAllMotion() Msg { - return enableMouseAllMotionMsg{} +// DisableMouse is a special command that stops listening for mouse events. +func DisableMouse() Msg { + return disableMouseMsg{} } // disableMouseMsg is an internal message that that signals to stop listening // for mouse events. To send a disableMouseMsg, use the DisableMouse command. type disableMouseMsg struct{} -// DisableMouse is a special command that stops listening for mouse events. -func DisableMouse() Msg { - return disableMouseMsg{} -} - -// exitAltScreenMsg in an internal message signals that the program should exit -// alternate screen buffer. You can send a exitAltScreenMsg with ExitAltScreen. -type exitAltScreenMsg struct{} - -// batchMsg is the internal message used to perform a bunch of commands. You -// can send a batchMsg with Batch. -type batchMsg []Cmd - -// WindowSizeMsg is used to report on the terminal size. It's sent to Update -// once initially and then on every terminal resize. +// WindowSizeMsg is used to report the terminal size. It's sent to Update once +// initially and then on every terminal resize. Note that Windows does not +// have support for reporting when resizes occur as it does not support the +// SIGWINCH signal. type WindowSizeMsg struct { Width int Height int @@ -347,11 +451,6 @@ 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. { @@ -364,6 +463,21 @@ 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) + } + + // Honor program startup options. + if p.startupOptions&withAltScreen != 0 { + p.EnterAltScreen() + } + if p.startupOptions&withMouseCellMotion != 0 { + p.EnableMouseCellMotion() + } else if p.startupOptions&withMouseAllMotion != 0 { + p.EnableMouseAllMotion() + } + // Initialize program model := p.initialModel initCmd := model.Init() @@ -490,7 +604,7 @@ func (p *Program) Start() error { // EnterAltScreen enters the alternate screen buffer, which consumes the entire // terminal window. ExitAltScreen will return the terminal to its former state. // -// Deprecated. Use the EnterAltScreen() command instead. +// Deprecated. Use the WithAltScreen ProgramOption instead. func (p *Program) EnterAltScreen() { p.mtx.Lock() defer p.mtx.Unlock() @@ -510,7 +624,7 @@ func (p *Program) EnterAltScreen() { // ExitAltScreen exits the alternate screen buffer. // -// Deprecated. Use the ExitAltScreen() command instead. +// Deprecated. The altscreen will exited automatically when the program exits. func (p *Program) ExitAltScreen() { p.mtx.Lock() defer p.mtx.Unlock() @@ -530,7 +644,7 @@ func (p *Program) ExitAltScreen() { // EnableMouseCellMotion enables mouse click, release, wheel and motion events // if a mouse button is pressed (i.e., drag events). // -// Deprecated. Use the EnableMouseCellMotion() command instead. +// Deprecated. Use the WithMouseCellMotion ProgramOption instead. func (p *Program) EnableMouseCellMotion() { p.mtx.Lock() defer p.mtx.Unlock() @@ -540,7 +654,7 @@ func (p *Program) EnableMouseCellMotion() { // DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be // called automatically when exiting a Bubble Tea program. // -// Deprecated. Use the DisableMouse() command instead. +// Deprecated. The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseCellMotion() { p.mtx.Lock() defer p.mtx.Unlock() @@ -551,7 +665,7 @@ func (p *Program) DisableMouseCellMotion() { // regardless of whether a mouse button is pressed. Many modern terminals // support this, but not all. // -// Deprecated. Use the EnableMouseAllMotion() command instead. +// Deprecated. Use the WithMouseAllMotion ProgramOption instead. func (p *Program) EnableMouseAllMotion() { p.mtx.Lock() defer p.mtx.Unlock() @@ -561,7 +675,7 @@ func (p *Program) EnableMouseAllMotion() { // DisableMouseAllMotion disables All Motion mouse tracking. This will be // called automatically when exiting a Bubble Tea program. // -// Deprecated. Use the DisableMouse() command instead. +// Deprecated. The mouse will automatically be disabled when the program exits. func (p *Program) DisableMouseAllMotion() { p.mtx.Lock() defer p.mtx.Unlock()