chore: make input options mutually exclusive

This commit is contained in:
Christian Rocha 2023-05-05 14:55:25 -04:00
parent 25022e9789
commit fcc805f3da
3 changed files with 56 additions and 16 deletions

View File

@ -37,18 +37,20 @@ func WithOutput(output io.Writer) ProgramOption {
} }
// WithInput sets the input which, by default, is stdin. In most cases you // WithInput sets the input which, by default, is stdin. In most cases you
// won't need to use this. // won't need to use this. To disable input entirely pass nil.
//
// p := NewProgram(model, WithInput(nil))
func WithInput(input io.Reader) ProgramOption { func WithInput(input io.Reader) ProgramOption {
return func(p *Program) { return func(p *Program) {
p.input = input p.input = input
p.startupOptions |= withCustomInput p.inputType = customInput
} }
} }
// WithInputTTY opens a new TTY for input (or console input device on Windows). // WithInputTTY opens a new TTY for input (or console input device on Windows).
func WithInputTTY() ProgramOption { func WithInputTTY() ProgramOption {
return func(p *Program) { return func(p *Program) {
p.startupOptions |= withInputTTY p.inputType = ttyInput
} }
} }

View File

@ -14,13 +14,13 @@ func TestOptions(t *testing.T) {
} }
}) })
t.Run("input", func(t *testing.T) { t.Run("custom input", func(t *testing.T) {
var b bytes.Buffer var b bytes.Buffer
p := NewProgram(nil, WithInput(&b)) p := NewProgram(nil, WithInput(&b))
if p.input != &b { if p.input != &b {
t.Errorf("expected input to custom, got %v", p.input) t.Errorf("expected input to custom, got %v", p.input)
} }
if p.startupOptions&withCustomInput == 0 { if p.inputType != customInput {
t.Errorf("expected startup options to have custom input set, got %v", p.input) t.Errorf("expected startup options to have custom input set, got %v", p.input)
} }
}) })
@ -49,6 +49,25 @@ func TestOptions(t *testing.T) {
} }
}) })
t.Run("input options", func(t *testing.T) {
exercise := func(t *testing.T, opt ProgramOption, expect inputType) {
p := NewProgram(nil, opt)
if p.inputType != expect {
t.Errorf("expected input type %s, got %s", expect, p.inputType)
}
}
t.Run("tty input", func(t *testing.T) {
exercise(t, WithInputTTY(), ttyInput)
})
t.Run("custom input", func(t *testing.T) {
var b bytes.Buffer
exercise(t, WithInput(&b), customInput)
})
})
t.Run("startup options", func(t *testing.T) { t.Run("startup options", func(t *testing.T) {
exercise := func(t *testing.T, opt ProgramOption, expect startupOptions) { exercise := func(t *testing.T, opt ProgramOption, expect startupOptions) {
p := NewProgram(nil, opt) p := NewProgram(nil, opt)
@ -57,10 +76,6 @@ func TestOptions(t *testing.T) {
} }
} }
t.Run("input tty", func(t *testing.T) {
exercise(t, WithInputTTY(), withInputTTY)
})
t.Run("alt screen", func(t *testing.T) { t.Run("alt screen", func(t *testing.T) {
exercise(t, WithAltScreen(), withAltScreen) exercise(t, WithAltScreen(), withAltScreen)
}) })
@ -100,10 +115,13 @@ func TestOptions(t *testing.T) {
t.Run("multiple", func(t *testing.T) { t.Run("multiple", func(t *testing.T) {
p := NewProgram(nil, WithMouseAllMotion(), WithAltScreen(), WithInputTTY()) p := NewProgram(nil, WithMouseAllMotion(), WithAltScreen(), WithInputTTY())
for _, opt := range []startupOptions{withMouseAllMotion, withAltScreen, withInputTTY} { for _, opt := range []startupOptions{withMouseAllMotion, withAltScreen} {
if !p.startupOptions.has(opt) { if !p.startupOptions.has(opt) {
t.Errorf("expected startup options have %v, got %v", opt, p.startupOptions) t.Errorf("expected startup options have %v, got %v", opt, p.startupOptions)
} }
if p.inputType != ttyInput {
t.Errorf("expected input to be %v, got %v", opt, p.startupOptions)
}
} }
}) })
} }

32
tea.go
View File

@ -60,6 +60,24 @@ type Cmd func() Msg
type handlers []chan struct{} type handlers []chan struct{}
type inputType int
const (
defaultInput inputType = iota
ttyInput
customInput
)
// String implements the stringer interface for [inputType]. It is inteded to
// be used in testing.
func (i inputType) String() string {
return [...]string{
"default input",
"tty input",
"custom input",
}[i]
}
// Options to customize the program during its initialization. These are // Options to customize the program during its initialization. These are
// generally set with ProgramOptions. // generally set with ProgramOptions.
// //
@ -74,8 +92,6 @@ const (
withAltScreen startupOptions = 1 << iota withAltScreen startupOptions = 1 << iota
withMouseCellMotion withMouseCellMotion
withMouseAllMotion withMouseAllMotion
withInputTTY
withCustomInput
withANSICompressor withANSICompressor
withoutSignalHandler withoutSignalHandler
@ -94,6 +110,8 @@ type Program struct {
// treated as bits. These options can be set via various ProgramOptions. // treated as bits. These options can be set via various ProgramOptions.
startupOptions startupOptions startupOptions startupOptions
inputType inputType
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
@ -141,7 +159,6 @@ type QuitMsg struct{}
func NewProgram(model Model, opts ...ProgramOption) *Program { func NewProgram(model Model, opts ...ProgramOption) *Program {
p := &Program{ p := &Program{
initialModel: model, initialModel: model,
input: os.Stdin,
msgs: make(chan Msg), msgs: make(chan Msg),
} }
@ -371,8 +388,11 @@ func (p *Program) Run() (Model, error) {
defer p.cancel() defer p.cancel()
switch { switch p.inputType {
case p.startupOptions.has(withInputTTY): case defaultInput:
p.input = os.Stdin
case ttyInput:
// Open a new TTY, by request // Open a new TTY, by request
f, err := openInputTTY() f, err := openInputTTY()
if err != nil { if err != nil {
@ -381,7 +401,7 @@ func (p *Program) Run() (Model, error) {
defer f.Close() //nolint:errcheck defer f.Close() //nolint:errcheck
p.input = f p.input = f
case !p.startupOptions.has(withCustomInput): case customInput:
// If the user hasn't set a custom input, and input's not a terminal, // If the user hasn't set a custom input, and input's not a terminal,
// open a TTY so we can capture input as normal. This will allow things // 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 // to "just work" in cases where data was piped or redirected into this