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.
This commit is contained in:
Christian Rocha
2021-05-19 20:52:01 -04:00
parent ec2fbf024b
commit 85ab476698
4 changed files with 176 additions and 60 deletions

View File

@@ -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)
}
}

View File

@@ -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) {

View File

@@ -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)

214
tea.go
View File

@@ -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()