From 49a5d1657938d57532a4e19fa517533b515e50d6 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Thu, 29 Jul 2021 16:47:13 -0400 Subject: [PATCH] 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 --- options.go | 2 +- tea.go | 59 +++++++++++--------------------------------------- tty.go | 14 +++++------- tty_unix.go | 39 ++++++++++----------------------- tty_windows.go | 1 + 5 files changed, 32 insertions(+), 83 deletions(-) diff --git a/options.go b/options.go index 3459b4d..944c1e5 100644 --- a/options.go +++ b/options.go @@ -24,7 +24,7 @@ func WithOutput(output io.Writer) ProgramOption { func WithInput(input io.Reader) ProgramOption { return func(m *Program) { m.input = input - m.inputStatus = customInput + m.startupOptions |= withCustomInput } } diff --git a/tea.go b/tea.go index 12046ad..37e9082 100644 --- a/tea.go +++ b/tea.go @@ -52,8 +52,8 @@ type Model interface { // function. type Cmd func() Msg -// startupOptions contains configuration options to be run while the program -// is initializing. +// Options to customize the program during its initialization. These are +// generally set with ProgramOptions. // // The options here are treated as bits. type startupOptions byte @@ -62,42 +62,14 @@ func (s startupOptions) has(option startupOptions) bool { return s&option != 0 } -// Available startup options. const ( withAltScreen startupOptions = 1 << iota withMouseCellMotion withMouseAllMotion 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. type Program struct { initialModel Model @@ -122,8 +94,6 @@ type Program struct { // is on by default. CatchPanics bool - inputStatus inputStatus - inputIsTTY bool outputIsTTY bool console console.Console @@ -311,22 +281,19 @@ func (p *Program) Start() error { 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 // 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. - if !p.inputIsTTY && p.inputStatus != customInput { - f, err := openInputTTY() - if err != nil { - return err + if f, ok := p.input.(*os.File); ok { + inputIsTTY := isatty.IsTerminal(f.Fd()) + + if !inputIsTTY && !p.startupOptions.has(withCustomInput) { + f, err := openInputTTY() + if err != nil { + 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 @@ -394,7 +361,7 @@ func (p *Program) Start() error { p.renderer.write(model.View()) // Subscribe to user input - if p.inputIsTTY { + if p.input != nil { go func() { for { msg, err := readInput(p.input) diff --git a/tty.go b/tty.go index fe4fc02..10d69d0 100644 --- a/tty.go +++ b/tty.go @@ -12,10 +12,7 @@ func (p *Program) initTerminal() error { return err } - if p.inputIsTTY { - if p.console == nil { - return errors.New("no console") - } + if p.console != nil { err = p.console.SetRaw() if err != nil { return err @@ -35,11 +32,6 @@ func (p Program) restoreTerminal() error { 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 { err := p.console.Reset() if err != nil { @@ -47,5 +39,9 @@ func (p Program) restoreTerminal() error { } } + if err := p.restoreInput(); err != nil { + return err + } + return nil } diff --git a/tty_unix.go b/tty_unix.go index 1fc7091..d34c44d 100644 --- a/tty_unix.go +++ b/tty_unix.go @@ -3,7 +3,6 @@ package tea import ( - "errors" "io" "os" @@ -11,39 +10,25 @@ import ( ) func (p *Program) initInput() error { - if !p.inputIsTTY { - return nil + // If input's a file, use console to manage it + if f, ok := p.input.(*os.File); ok { + c, err := console.ConsoleFromFile(f) + if err != nil { + return nil + } + p.console = c } - // If input's a TTY this should always succeed. - f, ok := p.input.(*os.File) - if !ok { - return errInputIsNotAFile - } - - c, err := console.ConsoleFromFile(f) - if err != nil { - return nil - } - p.console = c - return nil } // 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 -// terminal receives a keypress rather than appearing promptly after the program -// exits. +// we don't do this on Windows as it causes the prompt to not be drawn until +// the terminal receives a keypress rather than appearing promptly after the +// program exits. func (p *Program) restoreInput() error { - if p.inputStatus == managedInput { - f, ok := p.input.(*os.File) - if !ok { - return errors.New("could not close input") - } - err := f.Close() - if err != nil { - return err - } + if p.console != nil { + return p.console.Close() } return nil } diff --git a/tty_windows.go b/tty_windows.go index d255f3f..c9d43cf 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -47,6 +47,7 @@ func (p *Program) restoreInput() error { return nil } +// Open the Windows equivalent of a TTY. func openInputTTY() (*os.File, error) { f, err := os.OpenFile("CONIN$", os.O_RDWR, 0644) if err != nil {