allow custom input and handle eof for os.input properly

Signed-off-by: Christoph Hartmann <chris@lollyrock.com>
This commit is contained in:
Christoph Hartmann 2020-12-23 18:56:13 +01:00 committed by Christian Rocha
parent bc06e8d2e0
commit e84314c622
2 changed files with 81 additions and 16 deletions

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.13
require ( require (
github.com/containerd/console v1.0.1 github.com/containerd/console v1.0.1
github.com/mattn/go-isatty v0.0.12
github.com/muesli/termenv v0.7.2 github.com/muesli/termenv v0.7.2
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634 golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634

70
tea.go
View File

@ -11,10 +11,12 @@ package tea
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"runtime/debug" "runtime/debug"
"sync" "sync"
isatty "github.com/mattn/go-isatty"
te "github.com/muesli/termenv" te "github.com/muesli/termenv"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )
@ -57,12 +59,56 @@ func Batch(cmds ...Cmd) Cmd {
} }
} }
// ProgramOption is used to set options when intializing 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
// won't need to use this.
func WithOutput(output *os.File) ProgramOption {
return func(m *Program) {
m.output = output
}
}
// WithInput sets the input which, by default, is stdin. In most cases you
// won't need to use this.
func WithInput(input io.Reader) ProgramOption {
return func(m *Program) {
m.input = input
}
}
// WithInputDisables disables input. Use this with caution: if you disable
// input users will not be able to exit your program until it exits itself.
func WithInputDisabled() ProgramOption {
return func(m *Program) {
m.input = nil
}
}
// WithoutCatchPanics disables the panic catching that Bubble Tea does by
// default. Note that if panic catching is disabled the terminal will be in a
// fairly unusable state after a panic because Bubble Tea will not perform its
// usual cleanup on exit.
func WithoutCatchPanics() ProgramOption {
return func(m *Program) {
m.CatchPanics = false
}
}
// Program is a terminal user interface. // Program is a terminal user interface.
type Program struct { type Program struct {
initialModel Model initialModel Model
mtx sync.Mutex mtx sync.Mutex
output *os.File // where to send output. this will usually be os.Stdout. output *os.File // where to send output. this will usually be os.Stdout.
input io.Reader // this will usually be os.Stdin.
renderer *renderer renderer *renderer
altScreenActive bool altScreenActive bool
@ -106,12 +152,20 @@ func HideCursor() Msg {
type hideCursorMsg struct{} type hideCursorMsg struct{}
// NewProgram creates a new Program. // NewProgram creates a new Program.
func NewProgram(model Model) *Program { func NewProgram(model Model, opts ...ProgramOption) *Program {
return &Program{ p := &Program{
initialModel: model, initialModel: model,
output: os.Stdout, output: os.Stdout,
input: os.Stdin,
CatchPanics: true, CatchPanics: true,
} }
// Apply all options to program
for _, opt := range opts {
opt(p)
}
return p
} }
// Start initializes the program. // Start initializes the program.
@ -136,11 +190,15 @@ func (p *Program) Start() error {
p.renderer = newRenderer(p.output, &p.mtx) p.renderer = newRenderer(p.output, &p.mtx)
// Check if output is a TTY before entering raw mode, hiding the cursor and
// so on.
if isatty.IsTerminal(p.output.Fd()) {
err := initTerminal(p.output) err := initTerminal(p.output)
if err != nil { if err != nil {
return err return err
} }
defer restoreTerminal(p.output) defer restoreTerminal(p.output)
}
// Initialize program // Initialize program
model := p.initialModel model := p.initialModel
@ -159,15 +217,21 @@ func (p *Program) Start() error {
p.renderer.write(model.View()) p.renderer.write(model.View())
// Subscribe to user input // Subscribe to user input
if p.input != nil {
go func() { go func() {
for { for {
msg, err := readInput(os.Stdin) msg, err := readInput(p.input)
if err != nil { if err != nil {
// If we get EOF just stop listening for input
if err == io.EOF {
break
}
errs <- err errs <- err
} }
msgs <- msg msgs <- msg
} }
}() }()
}
// Get initial terminal size // Get initial terminal size
go func() { go func() {