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 mostly affects Windows. On Windows, unlike Unix, the console (TTY)
has 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 e086d98
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 83 deletions.
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand Down Expand Up @@ -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=
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
27 changes: 7 additions & 20 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)
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
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 {
Expand Down
49 changes: 24 additions & 25 deletions tty_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

// 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.MakeRaw(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)
}
}

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 e086d98

Please sign in to comment.