From fcc805f3da68f535f7c3c78b94b4b14cd22a047a Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Fri, 5 May 2023 14:55:25 -0400 Subject: [PATCH] chore: make input options mutually exclusive --- options.go | 8 +++++--- options_test.go | 32 +++++++++++++++++++++++++------- tea.go | 32 ++++++++++++++++++++++++++------ 3 files changed, 56 insertions(+), 16 deletions(-) diff --git a/options.go b/options.go index 7a63c67b1b..30f7e6c50b 100644 --- a/options.go +++ b/options.go @@ -37,18 +37,20 @@ func WithOutput(output io.Writer) ProgramOption { } // 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 { return func(p *Program) { p.input = input - p.startupOptions |= withCustomInput + p.inputType = customInput } } // WithInputTTY opens a new TTY for input (or console input device on Windows). func WithInputTTY() ProgramOption { return func(p *Program) { - p.startupOptions |= withInputTTY + p.inputType = ttyInput } } diff --git a/options_test.go b/options_test.go index a66483d713..fdbc5b2cc2 100644 --- a/options_test.go +++ b/options_test.go @@ -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 p := NewProgram(nil, WithInput(&b)) if p.input != &b { 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) } }) @@ -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) { exercise := func(t *testing.T, opt ProgramOption, expect startupOptions) { 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) { exercise(t, WithAltScreen(), withAltScreen) }) @@ -100,10 +115,13 @@ func TestOptions(t *testing.T) { t.Run("multiple", func(t *testing.T) { p := NewProgram(nil, WithMouseAllMotion(), WithAltScreen(), WithInputTTY()) - for _, opt := range []startupOptions{withMouseAllMotion, withAltScreen, withInputTTY} { + for _, opt := range []startupOptions{withMouseAllMotion, withAltScreen} { if !p.startupOptions.has(opt) { 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) + } } }) } diff --git a/tea.go b/tea.go index e61bb5a6ae..ca24eacf3a 100644 --- a/tea.go +++ b/tea.go @@ -60,6 +60,24 @@ type Cmd func() Msg 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 // generally set with ProgramOptions. // @@ -74,8 +92,6 @@ const ( withAltScreen startupOptions = 1 << iota withMouseCellMotion withMouseAllMotion - withInputTTY - withCustomInput withANSICompressor withoutSignalHandler @@ -94,6 +110,8 @@ type Program struct { // treated as bits. These options can be set via various ProgramOptions. startupOptions startupOptions + inputType inputType + ctx context.Context cancel context.CancelFunc @@ -141,7 +159,6 @@ type QuitMsg struct{} func NewProgram(model Model, opts ...ProgramOption) *Program { p := &Program{ initialModel: model, - input: os.Stdin, msgs: make(chan Msg), } @@ -371,8 +388,11 @@ func (p *Program) Run() (Model, error) { defer p.cancel() - switch { - case p.startupOptions.has(withInputTTY): + switch p.inputType { + case defaultInput: + p.input = os.Stdin + + case ttyInput: // Open a new TTY, by request f, err := openInputTTY() if err != nil { @@ -381,7 +401,7 @@ func (p *Program) Run() (Model, error) { defer f.Close() //nolint:errcheck 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, // 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