From 0e691502924cb47e3c29937fee5bc89c5ee6f1d1 Mon Sep 17 00:00:00 2001 From: Ayman Bagabas Date: Tue, 9 Jan 2024 17:20:23 -0500 Subject: [PATCH] feat: reduce console/term dependencies Replace mattn/isatty and containerd/console with golang.org/x/term. This pretty much mostly affects Windows. On Windows, unlike Unix, the console (TTY) have different handles for input/output. Using the Console API, we need to enable VT input on the input handle (CONIN) and VT processing on the output handle (CONOUT). Doing so enables processing VT sequences on Windows i.e. ANSI colors, mouse sequences, cursor movements, etc. We already handle enabling VT processing for the program output using Termenv `EnableVirtualTerminalProcessing`. For the input side, we enable VT input right before setting the console to raw. By doing this, we can drop both containerd/console and mattn/isatty. --- go.mod | 2 +- tea.go | 22 +++++++--------------- tty.go | 28 +++++++++++---------------- tty_unix.go | 25 ++++++------------------- tty_windows.go | 51 +++++++++++++++++++++++++------------------------- 5 files changed, 51 insertions(+), 77 deletions(-) diff --git a/go.mod b/go.mod index bc7c9e8884..1a71d75839 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ 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 @@ -17,6 +16,7 @@ require ( 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 diff --git a/tea.go b/tea.go index f18cb87cf0..34f49d6adf 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 01f084d438..f4a295ae8a 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 a3a25b8fa6..e7945e2af1 100644 --- a/tty_unix.go +++ b/tty_unix.go @@ -7,35 +7,22 @@ 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) + // 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 + return fmt.Errorf("error entering raw mode: %w", err) } - 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 nil -} - func openInputTTY() (*os.File, error) { f, err := os.Open("/dev/tty") if err != nil { diff --git a/tty_windows.go b/tty_windows.go index be415aef79..372ae73048 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -4,37 +4,38 @@ 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 - - // Note: this will panic if it fails. - c := console.Current() - p.console = c +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.GetState(int(p.tty.Fd())) + if err != nil { + return err + } + + // 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) + } + + _, err = term.MakeRaw(int(p.tty.Fd())) } - 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.