Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions tea_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"io"
"os"
"strings"
"sync"
"sync/atomic"
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
32 changes: 32 additions & 0 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package tea

import (
"fmt"
"io"
"os"
"strings"
"time"

uv "github.com/charmbracelet/ultraviolet"
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down