From 9f04c936dab66b4d4bbe617912cb130f89cce0b4 Mon Sep 17 00:00:00 2001 From: Christian Rocha Date: Mon, 11 Jan 2021 16:33:14 -0500 Subject: [PATCH] Always listen for SIGINT --- tea.go | 54 +++++++++++++++++++++++++++++++++++++++++++----------- tty.go | 31 ++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/tea.go b/tea.go index 6b0d8bb..317e1e2 100644 --- a/tea.go +++ b/tea.go @@ -10,11 +10,14 @@ package tea import ( + "context" "fmt" "io" "os" + "os/signal" "runtime/debug" "sync" + "syscall" isatty "github.com/mattn/go-isatty" te "github.com/muesli/termenv" @@ -109,6 +112,9 @@ type Program struct { // from panics, print the stack trace, and disable raw mode. This feature // is on by default. CatchPanics bool + + inputIsTTY bool + outputIsTTY bool } // Quit is a special command that tells the Bubble Tea program to exit. @@ -167,17 +173,43 @@ func (p *Program) Start() error { msgs = make(chan Msg) errs = make(chan error) done = make(chan struct{}) + + // If output is a file (e.g. os.Stdout) then this will be set + // accordingly. Most of the time you should refer to p.outputIsTTY + // rather than do a nil check against the value here. + outputAsFile *os.File ) - // Is output a file? - outputAsFile, outputIsFile := p.output.(*os.File) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - // Is output a TTY? - var isTTY bool - if outputIsFile { - isTTY = isatty.IsTerminal(outputAsFile.Fd()) + // Is output a terminal? + if f, ok := p.output.(*os.File); ok { + outputAsFile = f + p.outputIsTTY = isatty.IsTerminal(f.Fd()) } + // Is input a terminal? + if f, ok := p.input.(*os.File); ok { + p.inputIsTTY = isatty.IsTerminal(f.Fd()) + } + + // Listen for SIGINT. Note that in most cases ^C will not send an + // interrupt because the terminal will be in raw mode and thus capture + // that keystroke and send it along to Program.Update. If input is not a + // TTY, however, ^C will be caught here. + go func() { + sig := make(chan os.Signal, 1) + signal.Notify(sig, syscall.SIGINT) + defer signal.Stop(sig) + + select { + case <-ctx.Done(): + case <-sig: + msgs <- quitMsg{} + } + }() + if p.CatchPanics { defer func() { if r := recover(); r != nil { @@ -193,12 +225,12 @@ func (p *Program) Start() error { // Check if output is a TTY before entering raw mode, hiding the cursor and // so on. - if isTTY { - err := initTerminal(p.output) + { + err := p.initTerminal() if err != nil { return err } - defer restoreTerminal(p.output) + defer p.restoreTerminal() } // Initialize program @@ -218,7 +250,7 @@ func (p *Program) Start() error { p.renderer.write(model.View()) // Subscribe to user input - if p.input != nil { + if p.inputIsTTY { go func() { for { msg, err := readInput(p.input) @@ -234,7 +266,7 @@ func (p *Program) Start() error { }() } - if isTTY { + if p.outputIsTTY { // Get initial terminal size go func() { w, h, err := terminal.GetSize(int(outputAsFile.Fd())) diff --git a/tty.go b/tty.go index 6e50141..ac8999d 100644 --- a/tty.go +++ b/tty.go @@ -1,26 +1,35 @@ package tea import ( - "io" - "github.com/containerd/console" ) var tty console.Console -func initTerminal(w io.Writer) error { - tty = console.Current() - err := tty.SetRaw() - if err != nil { - return err +func (p Program) initTerminal() error { + if p.outputIsTTY { + tty = console.Current() + } + + if p.inputIsTTY { + err := tty.SetRaw() + if err != nil { + return err + } + } + + if p.outputIsTTY { + enableAnsiColors(p.output) + hideCursor(p.output) } - enableAnsiColors(w) - hideCursor(w) return nil } -func restoreTerminal(w io.Writer) error { - showCursor(w) +func (p Program) restoreTerminal() error { + if !p.outputIsTTY { + return nil + } + showCursor(p.output) return tty.Reset() }