Skip to content

Commit

Permalink
feat: reduce console/term dependencies (#897)
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 Mar 1, 2024
1 parent b655d7e commit 4624106
Show file tree
Hide file tree
Showing 10 changed files with 54 additions and 94 deletions.
1 change: 0 additions & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ require (
github.com/aymanbagabas/go-udiff v0.2.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
Expand Down
3 changes: 0 additions & 3 deletions examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 h1:kyT+
github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516 h1:7IZFEUZpEgjlTSd7P1MRRhGXs7t4F6mENeMw17TxnQs=
github.com/charmbracelet/x/exp/teatest v0.0.0-20240229115032-4b79243a3516/go.mod h1:SG24wGkG/mix5V2dZLXfQ6Bod43HGvk9CkTDxATwKN4=
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -87,7 +85,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down
3 changes: 1 addition & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ module github.com/charmbracelet/bubbletea
go 1.18

require (
github.com/containerd/console v1.0.4
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-localereader v0.0.1
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6
github.com/muesli/cancelreader v0.2.2
Expand All @@ -19,6 +17,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.18 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.6 // indirect
golang.org/x/text v0.3.8 // indirect
Expand Down
7 changes: 2 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
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 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
Expand All @@ -28,7 +26,6 @@ github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down
26 changes: 9 additions & 17 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 @@ -148,26 +147,19 @@ type Program struct {
renderer renderer

// where to read inputs from, this will usually be os.Stdin.
input io.Reader
cancelReader cancelreader.CancelReader
readLoopDone chan struct{}
console console.Console
input io.Reader
// tty is null if input is not a TTY.
tty *os.File
previousTtyState *term.State
cancelReader cancelreader.CancelReader
readLoopDone chan struct{}

// was the altscreen active before releasing the terminal?
altScreenWasActive bool
ignoreSignals uint32

bpWasActive bool // was the bracketed paste mode active before releasing the terminal?

// 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 @@ -257,7 +249,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 @@ -449,7 +441,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,24 +7,15 @@ import (
"os"
"time"

isatty "github.com/mattn/go-isatty"
"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.previousTtyState != nil {
if err := term.Restore(int(p.tty.Fd()), p.previousTtyState); err != nil {
return fmt.Errorf("error restoring console: %w", err)
}
}

return p.restoreInput()
return nil
}

// initCancelReader (re)commences reading inputs.
Expand Down Expand Up @@ -96,7 +90,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.previousTtyState, 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.previousTtyState, 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
1 change: 0 additions & 1 deletion tutorials/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require github.com/charmbracelet/bubbletea v0.25.0

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand Down
3 changes: 0 additions & 3 deletions tutorials/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 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand All @@ -28,7 +26,6 @@ github.com/rivo/uniseg v0.4.6/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/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.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
Expand Down

0 comments on commit 4624106

Please sign in to comment.