forked from Mirrors/bubbletea
feat: reduce console/term dependencies
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.
This commit is contained in:
parent
cb1a1d79ea
commit
e086d98172
5
go.mod
5
go.mod
|
@ -3,22 +3,21 @@ module github.com/charmbracelet/bubbletea
|
||||||
go 1.17
|
go 1.17
|
||||||
|
|
||||||
require (
|
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/mattn/go-localereader v0.0.1
|
||||||
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
|
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b
|
||||||
github.com/muesli/cancelreader v0.2.2
|
github.com/muesli/cancelreader v0.2.2
|
||||||
github.com/muesli/reflow v0.3.0
|
github.com/muesli/reflow v0.3.0
|
||||||
github.com/muesli/termenv v0.15.2
|
github.com/muesli/termenv v0.15.2
|
||||||
golang.org/x/sync v0.5.0
|
golang.org/x/sync v0.5.0
|
||||||
|
golang.org/x/sys v0.9.0
|
||||||
golang.org/x/term v0.9.0
|
golang.org/x/term v0.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // 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/mattn/go-runewidth v0.0.14 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // 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
|
golang.org/x/text v0.3.8 // indirect
|
||||||
)
|
)
|
||||||
|
|
3
go.sum
3
go.sum
|
@ -1,7 +1,5 @@
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
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/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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
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=
|
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
@ -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-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-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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.7.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=
|
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
|
||||||
|
|
20
tea.go
20
tea.go
|
@ -21,11 +21,10 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/containerd/console"
|
|
||||||
isatty "github.com/mattn/go-isatty"
|
|
||||||
"github.com/muesli/cancelreader"
|
"github.com/muesli/cancelreader"
|
||||||
"github.com/muesli/termenv"
|
"github.com/muesli/termenv"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrProgramKilled is returned by [Program.Run] when the program got killed.
|
// ErrProgramKilled is returned by [Program.Run] when the program got killed.
|
||||||
|
@ -148,23 +147,16 @@ type Program struct {
|
||||||
|
|
||||||
// where to read inputs from, this will usually be os.Stdin.
|
// 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
|
cancelReader cancelreader.CancelReader
|
||||||
readLoopDone chan struct{}
|
readLoopDone chan struct{}
|
||||||
console console.Console
|
|
||||||
|
|
||||||
// was the altscreen active before releasing the terminal?
|
// was the altscreen active before releasing the terminal?
|
||||||
altScreenWasActive bool
|
altScreenWasActive bool
|
||||||
ignoreSignals uint32
|
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
|
filter func(Model, Msg) Msg
|
||||||
|
|
||||||
// fps is the frames per second we should set on the renderer, if
|
// 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{} {
|
func (p *Program) handleResize() chan struct{} {
|
||||||
ch := make(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.
|
// Get the initial terminal size and send it to the program.
|
||||||
go p.checkResize()
|
go p.checkResize()
|
||||||
|
|
||||||
|
@ -440,7 +432,7 @@ func (p *Program) Run() (Model, error) {
|
||||||
if !isFile {
|
if !isFile {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if isatty.IsTerminal(f.Fd()) {
|
if term.IsTerminal(int(f.Fd())) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
28
tty.go
28
tty.go
|
@ -7,25 +7,16 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
isatty "github.com/mattn/go-isatty"
|
|
||||||
localereader "github.com/mattn/go-localereader"
|
localereader "github.com/mattn/go-localereader"
|
||||||
"github.com/muesli/cancelreader"
|
"github.com/muesli/cancelreader"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Program) initTerminal() error {
|
func (p *Program) initTerminal() error {
|
||||||
err := p.initInput()
|
if err := p.initInput(); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
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()
|
p.renderer.hideCursor()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -45,14 +36,17 @@ func (p *Program) restoreTerminalState() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.console != nil {
|
return p.restoreInput()
|
||||||
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 nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// initCancelReader (re)commences reading inputs.
|
// initCancelReader (re)commences reading inputs.
|
||||||
|
@ -97,7 +91,7 @@ func (p *Program) waitForReadLoop() {
|
||||||
// via a WindowSizeMsg.
|
// via a WindowSizeMsg.
|
||||||
func (p *Program) checkResize() {
|
func (p *Program) checkResize() {
|
||||||
f, ok := p.output.TTY().(*os.File)
|
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
|
// can't query window size
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
27
tty_unix.go
27
tty_unix.go
|
@ -7,35 +7,22 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/containerd/console"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Program) initInput() error {
|
func (p *Program) initInput() (err error) {
|
||||||
// If input's a file, use console to manage it
|
// Check if input is a terminal
|
||||||
if f, ok := p.input.(*os.File); ok {
|
if f, ok := p.input.(*os.File); ok && term.IsTerminal(int(f.Fd())) {
|
||||||
c, err := console.ConsoleFromFile(f)
|
p.tty = f
|
||||||
|
p.ttyState, err = term.MakeRaw(int(p.tty.Fd()))
|
||||||
if err != nil {
|
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
|
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) {
|
func openInputTTY() (*os.File, error) {
|
||||||
f, err := os.Open("/dev/tty")
|
f, err := os.Open("/dev/tty")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -4,37 +4,36 @@
|
||||||
package tea
|
package tea
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/containerd/console"
|
"golang.org/x/sys/windows"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Program) initInput() error {
|
func (p *Program) initInput() (err error) {
|
||||||
// If input's a file, use console to manage it
|
// Save stdin state and enable VT input
|
||||||
if f, ok := p.input.(*os.File); ok {
|
// We enable VT processing using Termenv, but we also need to enable VT
|
||||||
// Save a reference to the current stdin then replace stdin with our
|
// input here.
|
||||||
// input. We do this so we can hand input off to containerd/console to
|
if f, ok := p.input.(*os.File); ok && term.IsTerminal(int(f.Fd())) {
|
||||||
// set raw mode, and do it in this fashion because the method
|
p.tty = f
|
||||||
// console.ConsoleFromFile isn't supported on Windows.
|
p.ttyState, err = term.MakeRaw(int(p.tty.Fd()))
|
||||||
p.windowsStdin = os.Stdin
|
if err != nil {
|
||||||
os.Stdin = f
|
return err
|
||||||
|
|
||||||
// Note: this will panic if it fails.
|
|
||||||
c := console.Current()
|
|
||||||
p.console = c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// restoreInput restores stdout in the event that we placed it aside to handle
|
if err := windows.SetConsoleMode(windows.Handle(p.tty.Fd()), mode|windows.ENABLE_VIRTUAL_TERMINAL_INPUT); err != nil {
|
||||||
// input with CONIN$, above.
|
return fmt.Errorf("error setting console mode: %w", err)
|
||||||
func (p *Program) restoreInput() error {
|
}
|
||||||
if p.windowsStdin != nil {
|
|
||||||
os.Stdin = p.windowsStdin
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Open the Windows equivalent of a TTY.
|
// Open the Windows equivalent of a TTY.
|
||||||
|
|
Loading…
Reference in New Issue