Read input regardless of whether or not it's a TTY

This commit also contains some refactors:

* Refactor away inputStatus type
* Refactor away program.inputIsTTY member
* Simplify how we setup and restore input when it's a TTY
This commit is contained in:
Christian Rocha 2021-07-29 16:47:13 -04:00
parent e110b5ca1b
commit 49a5d16579
No known key found for this signature in database
GPG Key ID: D6CC7A16E5878018
5 changed files with 32 additions and 83 deletions

View File

@ -24,7 +24,7 @@ func WithOutput(output io.Writer) ProgramOption {
func WithInput(input io.Reader) ProgramOption { func WithInput(input io.Reader) ProgramOption {
return func(m *Program) { return func(m *Program) {
m.input = input m.input = input
m.inputStatus = customInput m.startupOptions |= withCustomInput
} }
} }

51
tea.go
View File

@ -52,8 +52,8 @@ type Model interface {
// function. // function.
type Cmd func() Msg type Cmd func() Msg
// startupOptions contains configuration options to be run while the program // Options to customize the program during its initialization. These are
// is initializing. // generally set with ProgramOptions.
// //
// The options here are treated as bits. // The options here are treated as bits.
type startupOptions byte type startupOptions byte
@ -62,42 +62,14 @@ func (s startupOptions) has(option startupOptions) bool {
return s&option != 0 return s&option != 0
} }
// Available startup options.
const ( const (
withAltScreen startupOptions = 1 << iota withAltScreen startupOptions = 1 << iota
withMouseCellMotion withMouseCellMotion
withMouseAllMotion withMouseAllMotion
withInputTTY withInputTTY
withCustomInput
) )
// 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.
type inputStatus int
const (
// Generally this will be stdin.
//
// Lint ignore note: this is the implicit default value. While it's not
// checked explicitly, it's presence nullifies the other possible values
// of this type in logical statements.
defaultInput inputStatus = iota // nolint:golint,deadcode,unused,varcheck
// The user explicitly set the input.
customInput
// We've opened a TTY for input.
managedInput
)
func (i inputStatus) String() string {
return [...]string{
"default input",
"custom input",
"managed input",
}[i]
}
// Program is a terminal user interface. // Program is a terminal user interface.
type Program struct { type Program struct {
initialModel Model initialModel Model
@ -122,8 +94,6 @@ type Program struct {
// is on by default. // is on by default.
CatchPanics bool CatchPanics bool
inputStatus inputStatus
inputIsTTY bool
outputIsTTY bool outputIsTTY bool
console console.Console console console.Console
@ -311,22 +281,19 @@ func (p *Program) Start() error {
p.input = f p.input = f
} }
// Is input a terminal?
if f, ok := p.input.(*os.File); ok {
p.inputIsTTY = isatty.IsTerminal(f.Fd())
}
// If input is not a terminal, and the user hasn't set a custom input, open // If input is not a terminal, and the user hasn't set a custom input, open
// a TTY so we can capture input as normal. This will allow things to "just // a TTY so we can capture input as normal. This will allow things to "just
// work" in cases where data was piped or redirected into this application. // work" in cases where data was piped or redirected into this application.
if !p.inputIsTTY && p.inputStatus != customInput { if f, ok := p.input.(*os.File); ok {
inputIsTTY := isatty.IsTerminal(f.Fd())
if !inputIsTTY && !p.startupOptions.has(withCustomInput) {
f, err := openInputTTY() f, err := openInputTTY()
if err != nil { if err != nil {
return err return err
} }
p.input = f p.input = f
p.inputIsTTY = true }
p.inputStatus = managedInput
} }
// Listen for SIGINT. Note that in most cases ^C will not send an // Listen for SIGINT. Note that in most cases ^C will not send an
@ -394,7 +361,7 @@ 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.inputIsTTY { if p.input != nil {
go func() { go func() {
for { for {
msg, err := readInput(p.input) msg, err := readInput(p.input)

14
tty.go
View File

@ -12,10 +12,7 @@ func (p *Program) initTerminal() error {
return err return err
} }
if p.inputIsTTY { if p.console != nil {
if p.console == nil {
return errors.New("no console")
}
err = p.console.SetRaw() err = p.console.SetRaw()
if err != nil { if err != nil {
return err return err
@ -35,11 +32,6 @@ func (p Program) restoreTerminal() error {
showCursor(p.output) showCursor(p.output)
} }
if err := p.restoreInput(); err != nil {
return err
}
// Console will only be set if input is a TTY.
if p.console != nil { if p.console != nil {
err := p.console.Reset() err := p.console.Reset()
if err != nil { if err != nil {
@ -47,5 +39,9 @@ func (p Program) restoreTerminal() error {
} }
} }
if err := p.restoreInput(); err != nil {
return err
}
return nil return nil
} }

View File

@ -3,7 +3,6 @@
package tea package tea
import ( import (
"errors"
"io" "io"
"os" "os"
@ -11,39 +10,25 @@ import (
) )
func (p *Program) initInput() error { func (p *Program) initInput() error {
if !p.inputIsTTY { // If input's a file, use console to manage it
return nil if f, ok := p.input.(*os.File); ok {
}
// If input's a TTY this should always succeed.
f, ok := p.input.(*os.File)
if !ok {
return errInputIsNotAFile
}
c, err := console.ConsoleFromFile(f) c, err := console.ConsoleFromFile(f)
if err != nil { if err != nil {
return nil return nil
} }
p.console = c p.console = c
}
return nil return nil
} }
// On unix systems, RestoreInput closes any TTYs we opened for input. Note that // On unix systems, RestoreInput closes any TTYs we opened for input. Note that
// we don't do this on Windows as it causes the prompt to not be drawn until the // we don't do this on Windows as it causes the prompt to not be drawn until
// terminal receives a keypress rather than appearing promptly after the program // the terminal receives a keypress rather than appearing promptly after the
// exits. // program exits.
func (p *Program) restoreInput() error { func (p *Program) restoreInput() error {
if p.inputStatus == managedInput { if p.console != nil {
f, ok := p.input.(*os.File) return p.console.Close()
if !ok {
return errors.New("could not close input")
}
err := f.Close()
if err != nil {
return err
}
} }
return nil return nil
} }

View File

@ -47,6 +47,7 @@ func (p *Program) restoreInput() error {
return nil return nil
} }
// Open the Windows equivalent of a TTY.
func openInputTTY() (*os.File, error) { func openInputTTY() (*os.File, error) {
f, err := os.OpenFile("CONIN$", os.O_RDWR, 0644) f, err := os.OpenFile("CONIN$", os.O_RDWR, 0644)
if err != nil { if err != nil {