diff --git a/go.mod b/go.mod index bc7c9e8..9eef2fa 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,21 @@ module github.com/charmbracelet/bubbletea go 1.17 require ( - github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 - github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-localereader v0.0.1 github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b github.com/muesli/cancelreader v0.2.2 github.com/muesli/reflow v0.3.0 github.com/muesli/termenv v0.15.2 golang.org/x/sync v0.5.0 + golang.org/x/sys v0.9.0 golang.org/x/term v0.9.0 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/rivo/uniseg v0.2.0 // indirect - golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.3.8 // indirect ) diff --git a/go.sum b/go.sum index 549d542..a268ef3 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= @@ -41,7 +39,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= diff --git a/tea.go b/tea.go index f18cb87..34f49d6 100644 --- a/tea.go +++ b/tea.go @@ -21,11 +21,10 @@ import ( "sync/atomic" "syscall" - "github.com/containerd/console" - isatty "github.com/mattn/go-isatty" "github.com/muesli/cancelreader" "github.com/muesli/termenv" "golang.org/x/sync/errgroup" + "golang.org/x/term" ) // ErrProgramKilled is returned by [Program.Run] when the program got killed. @@ -147,24 +146,17 @@ type Program struct { renderer renderer // where to read inputs from, this will usually be os.Stdin. - input io.Reader + input io.Reader + // tty is null if input is not a TTY. + tty *os.File + ttyState *term.State cancelReader cancelreader.CancelReader readLoopDone chan struct{} - console console.Console // was the altscreen active before releasing the terminal? altScreenWasActive bool ignoreSignals uint32 - // Stores the original reference to stdin for cases where input is not a - // TTY on windows and we've automatically opened CONIN$ to receive input. - // When the program exits this will be restored. - // - // Lint ignore note: the linter will find false positive on unix systems - // as this value only comes into play on Windows, hence the ignore comment - // below. - windowsStdin *os.File //nolint:golint,structcheck,unused - filter func(Model, Msg) Msg // fps is the frames per second we should set on the renderer, if @@ -254,7 +246,7 @@ func (p *Program) handleSignals() chan struct{} { func (p *Program) handleResize() chan struct{} { ch := make(chan struct{}) - if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) { + if f, ok := p.output.TTY().(*os.File); ok && term.IsTerminal(int(f.Fd())) { // Get the initial terminal size and send it to the program. go p.checkResize() @@ -440,7 +432,7 @@ func (p *Program) Run() (Model, error) { if !isFile { break } - if isatty.IsTerminal(f.Fd()) { + if term.IsTerminal(int(f.Fd())) { break } diff --git a/tty.go b/tty.go index 01f084d..f4a295a 100644 --- a/tty.go +++ b/tty.go @@ -7,25 +7,16 @@ import ( "os" "time" - isatty "github.com/mattn/go-isatty" localereader "github.com/mattn/go-localereader" "github.com/muesli/cancelreader" "golang.org/x/term" ) func (p *Program) initTerminal() error { - err := p.initInput() - if err != nil { + if err := p.initInput(); err != nil { return err } - if p.console != nil { - err = p.console.SetRaw() - if err != nil { - return fmt.Errorf("error entering raw mode: %w", err) - } - } - p.renderer.hideCursor() return nil } @@ -45,14 +36,17 @@ func (p *Program) restoreTerminalState() error { } } - if p.console != nil { - err := p.console.Reset() - if err != nil { - return fmt.Errorf("error restoring terminal state: %w", err) + return p.restoreInput() +} + +// restoreInput restores the tty input to its original state. +func (p *Program) restoreInput() error { + if p.tty != nil && p.ttyState != nil { + if err := term.Restore(int(p.tty.Fd()), p.ttyState); err != nil { + return fmt.Errorf("error restoring console: %w", err) } } - - return p.restoreInput() + return nil } // initCancelReader (re)commences reading inputs. @@ -97,7 +91,7 @@ func (p *Program) waitForReadLoop() { // via a WindowSizeMsg. func (p *Program) checkResize() { f, ok := p.output.TTY().(*os.File) - if !ok || !isatty.IsTerminal(f.Fd()) { + if !ok || !term.IsTerminal(int(f.Fd())) { // can't query window size return } diff --git a/tty_unix.go b/tty_unix.go index a3a25b8..342e759 100644 --- a/tty_unix.go +++ b/tty_unix.go @@ -7,32 +7,19 @@ import ( "fmt" "os" - "github.com/containerd/console" + "golang.org/x/term" ) -func (p *Program) initInput() error { - // If input's a file, use console to manage it - if f, ok := p.input.(*os.File); ok { - c, err := console.ConsoleFromFile(f) +func (p *Program) initInput() (err error) { + // Check if input is a terminal + if f, ok := p.input.(*os.File); ok && term.IsTerminal(int(f.Fd())) { + p.tty = f + p.ttyState, err = term.MakeRaw(int(p.tty.Fd())) if err != nil { - return nil //nolint:nilerr // ignore error, this was just a test - } - p.console = c - } - - return nil -} - -// On unix systems, RestoreInput closes any TTYs we opened for input. Note that -// we don't do this on Windows as it causes the prompt to not be drawn until -// the terminal receives a keypress rather than appearing promptly after the -// program exits. -func (p *Program) restoreInput() error { - if p.console != nil { - if err := p.console.Reset(); err != nil { - return fmt.Errorf("error restoring console: %w", err) + return fmt.Errorf("error entering raw mode: %w", err) } } + return nil } diff --git a/tty_windows.go b/tty_windows.go index be415ae..2703624 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -4,37 +4,36 @@ package tea import ( + "fmt" "os" - "github.com/containerd/console" + "golang.org/x/sys/windows" + "golang.org/x/term" ) -func (p *Program) initInput() error { - // If input's a file, use console to manage it - if f, ok := p.input.(*os.File); ok { - // Save a reference to the current stdin then replace stdin with our - // input. We do this so we can hand input off to containerd/console to - // set raw mode, and do it in this fashion because the method - // console.ConsoleFromFile isn't supported on Windows. - p.windowsStdin = os.Stdin - os.Stdin = f +func (p *Program) initInput() (err error) { + // Save stdin state and enable VT input + // We enable VT processing using Termenv, but we also need to enable VT + // input here. + if f, ok := p.input.(*os.File); ok && term.IsTerminal(int(f.Fd())) { + p.tty = f + p.ttyState, err = term.MakeRaw(int(p.tty.Fd())) + if err != nil { + return err + } - // Note: this will panic if it fails. - c := console.Current() - p.console = c + // Enable VT input + var mode uint32 + if err := windows.GetConsoleMode(windows.Handle(p.tty.Fd()), &mode); err != nil { + return fmt.Errorf("error getting console mode: %w", err) + } + + if err := windows.SetConsoleMode(windows.Handle(p.tty.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil { + return fmt.Errorf("error setting console mode: %w", err) + } } - return nil -} - -// restoreInput restores stdout in the event that we placed it aside to handle -// input with CONIN$, above. -func (p *Program) restoreInput() error { - if p.windowsStdin != nil { - os.Stdin = p.windowsStdin - } - - return nil + return } // Open the Windows equivalent of a TTY.