Skip to content

Commit

Permalink
feat: reduce console/term dependencies
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
aymanbagabas committed Jan 10, 2024
1 parent cb1a1d7 commit 0e69150
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 77 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
22 changes: 7 additions & 15 deletions tea.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
}

Expand Down
28 changes: 11 additions & 17 deletions tty.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down
25 changes: 6 additions & 19 deletions tty_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))

Check failure on line 17 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (~1.17, ubuntu-latest)

undefined: err

Check failure on line 17 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (~1.17, ubuntu-latest)

cannot assign error to err in multiple assignment

Check failure on line 17 in tty_unix.go

View workflow job for this annotation

GitHub Actions / lint

undefined: err

Check failure on line 17 in tty_unix.go

View workflow job for this annotation

GitHub Actions / coverage (^1, ubuntu-latest)

undefined: err

Check failure on line 17 in tty_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

undefined: err

Check failure on line 17 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (^1, ubuntu-latest)

undefined: err
if err != nil {

Check failure on line 18 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (~1.17, ubuntu-latest)

undefined: err

Check failure on line 18 in tty_unix.go

View workflow job for this annotation

GitHub Actions / lint

undefined: err

Check failure on line 18 in tty_unix.go

View workflow job for this annotation

GitHub Actions / coverage (^1, ubuntu-latest)

undefined: err

Check failure on line 18 in tty_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

undefined: err

Check failure on line 18 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (^1, ubuntu-latest)

undefined: err
return nil //nolint:nilerr // ignore error, this was just a test
return fmt.Errorf("error entering raw mode: %w", err)

Check failure on line 19 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (~1.17, ubuntu-latest)

undefined: err

Check failure on line 19 in tty_unix.go

View workflow job for this annotation

GitHub Actions / lint

undefined: err (typecheck)

Check failure on line 19 in tty_unix.go

View workflow job for this annotation

GitHub Actions / coverage (^1, ubuntu-latest)

undefined: err

Check failure on line 19 in tty_unix.go

View workflow job for this annotation

GitHub Actions / lint-soft

undefined: err (typecheck)

Check failure on line 19 in tty_unix.go

View workflow job for this annotation

GitHub Actions / build (^1, ubuntu-latest)

undefined: 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 {
Expand Down
51 changes: 26 additions & 25 deletions tty_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down

0 comments on commit 0e69150

Please sign in to comment.