diff --git a/tea_test.go b/tea_test.go index 8d81c77b08..6771bac411 100644 --- a/tea_test.go +++ b/tea_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "os" "strings" "sync" "sync/atomic" @@ -30,10 +31,16 @@ type testModel struct { counter atomic.Value } +type quitModel struct{} + func (m *testModel) Init() Cmd { return nil } +func (quitModel) Init() Cmd { + return Quit +} + func (m *testModel) Update(msg Msg) (Model, Cmd) { switch msg := msg.(type) { case ctxImplodeMsg: @@ -66,6 +73,14 @@ func (m *testModel) View() View { return NewView("success") } +func (quitModel) Update(msg Msg) (Model, Cmd) { + return quitModel{}, nil +} + +func (quitModel) View() View { + return NewView("") +} + func TestTeaModel(t *testing.T) { t.Parallel() var buf bytes.Buffer @@ -89,6 +104,27 @@ func TestTeaModel(t *testing.T) { } } +func TestTeaProgramRunsWithDevNullInput(t *testing.T) { + t.Parallel() + + devNull, err := os.Open(os.DevNull) + if err != nil { + t.Fatal(err) + } + defer devNull.Close() + + p := NewProgram( + quitModel{}, + WithInput(devNull), + WithOutput(io.Discard), + WithoutSignals(), + ) + + if _, err := p.Run(); err != nil { + t.Fatalf("expected /dev/null input to be accepted, got %v", err) + } +} + func TestTeaQuit(t *testing.T) { t.Parallel() var buf bytes.Buffer diff --git a/tty.go b/tty.go index 12b86493b3..6148dadd0e 100644 --- a/tty.go +++ b/tty.go @@ -2,7 +2,9 @@ package tea import ( "fmt" + "io" "os" + "strings" "time" uv "github.com/charmbracelet/ultraviolet" @@ -52,6 +54,33 @@ func (p *Program) restoreInput() error { return nil } +type fallbackInputReader struct { + io.Reader +} + +func shouldRetryCancelReaderWithoutFD(input io.Reader, err error) bool { + if err == nil || !strings.Contains(err.Error(), "add reader to epoll interest list") { + return false + } + + file, ok := input.(*os.File) + if !ok || term.IsTerminal(file.Fd()) { + return false + } + + info, statErr := file.Stat() + if statErr != nil { + return false + } + + devNullInfo, statErr := os.Stat(os.DevNull) + if statErr != nil { + return false + } + + return os.SameFile(info, devNullInfo) +} + // initInputReader (re)commences reading inputs. func (p *Program) initInputReader(cancel bool) error { if cancel && p.cancelReader != nil { @@ -67,6 +96,9 @@ func (p *Program) initInputReader(cancel bool) error { var err error p.cancelReader, err = uv.NewCancelReader(p.input) + if shouldRetryCancelReaderWithoutFD(p.input, err) { + p.cancelReader, err = uv.NewCancelReader(fallbackInputReader{Reader: p.input}) + } if err != nil { return fmt.Errorf("bubbletea: could not create cancelable reader: %w", err) }