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 Mar 1, 2024
1 parent c9d109a commit 414f447
Show file tree
Hide file tree
Showing 10 changed files with 65 additions and 89 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
6 changes: 5 additions & 1 deletion examples/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@ github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
<<<<<<< HEAD
github.com/charmbracelet/x/exp/golden v0.0.0-20240222125807-0344fda748f8 h1:kyT+aGp1z5jwlus3OY0cP6FuT05jYeeExx/4TYxnyrs=
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/charmbracelet/x/exp/teatest v0.0.0-20230508155401-2bd6fa14c46a h1:GpNt24LKE8sH5G0SZUpu4Tg15sP5XSt1mnfIqE7fW34=
github.com/charmbracelet/x/exp/teatest v0.0.0-20230508155401-2bd6fa14c46a/go.mod h1:dpMo6PfGlhavn+ofggWhfFQmK9sZB2Ewljiz9bZtKVI=
>>>>>>> 45e81530c3a6 (feat: reduce console/term dependencies)
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 +92,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
8 changes: 5 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
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=
<<<<<<< HEAD
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
=======
>>>>>>> 45e81530c3a6 (feat: reduce console/term dependencies)
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 +31,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
2 changes: 0 additions & 2 deletions tutorials/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ go 1.18

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
4 changes: 3 additions & 1 deletion tutorials/go.sum
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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=
<<<<<<< HEAD
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
=======
>>>>>>> 45e81530c3a6 (feat: reduce console/term dependencies)
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 +31,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 414f447

Please sign in to comment.