From 6c449e55bf0033e3d7176f48832e83d1470566d3 Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Sat, 4 Jun 2022 15:14:03 +0200 Subject: [PATCH] feat: use Termenv.Output to write to tty --- examples/go.mod | 2 +- examples/go.sum | 9 +++-- go.mod | 3 +- go.sum | 11 +++--- options.go | 24 +++++++------ options_test.go | 4 +-- screen.go | 53 ----------------------------- screen_test.go | 65 ----------------------------------- standard_renderer.go | 80 ++++++++++++++++++++++++-------------------- tea.go | 45 ++++++++++++++++++------- tty.go | 4 +-- tty_windows.go | 19 ----------- tutorials/go.sum | 11 +++--- 13 files changed, 111 insertions(+), 219 deletions(-) delete mode 100644 screen.go delete mode 100644 screen_test.go diff --git a/examples/go.mod b/examples/go.mod index 4e68c8b..042b22f 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -11,7 +11,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-isatty v0.0.16 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 + github.com/muesli/termenv v0.13.0 ) replace github.com/charmbracelet/bubbletea => ../ diff --git a/examples/go.sum b/examples/go.sum index 8b5d157..65731b4 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -39,8 +39,9 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY642CTFQb5Y= github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= @@ -53,8 +54,8 @@ github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKt github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898 h1:0j+cbZdhLgpNxjg0nWCasHUA82fgWOXxxGgWNVOLS1I= -github.com/muesli/termenv v0.12.1-0.20220615005108-4e9068de9898/go.mod h1:bN6sPNtkiahdhHv2Xm6RGU16LSCxfbIZvMfqjOCfrR4= +github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -81,7 +82,6 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -91,7 +91,6 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.mod b/go.mod index fedb6b0..d3a0c97 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,7 @@ require ( 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.11.1-0.20220212125758-44cd13922739 - golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab + github.com/muesli/termenv v0.13.0 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/text v0.3.7 // indirect ) diff --git a/go.sum b/go.sum index 270f8f5..76e0059 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,30 @@ +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/options.go b/options.go index b60d23c..6ede540 100644 --- a/options.go +++ b/options.go @@ -1,6 +1,10 @@ package tea -import "io" +import ( + "io" + + "github.com/muesli/termenv" +) // ProgramOption is used to set options when initializing a Program. Program can // accept a variable number of options. @@ -13,17 +17,17 @@ type ProgramOption func(*Program) // WithOutput sets the output which, by default, is stdout. In most cases you // won't need to use this. func WithOutput(output io.Writer) ProgramOption { - return func(m *Program) { - m.output = output + return func(p *Program) { + p.output = termenv.NewOutput(output, termenv.WithColorCache(true)) } } // WithInput sets the input which, by default, is stdin. In most cases you // won't need to use this. func WithInput(input io.Reader) ProgramOption { - return func(m *Program) { - m.input = input - m.startupOptions |= withCustomInput + return func(p *Program) { + p.input = input + p.startupOptions |= withCustomInput } } @@ -39,8 +43,8 @@ func WithInputTTY() ProgramOption { // unusable state after a panic because Bubble Tea will not perform its usual // cleanup on exit. func WithoutCatchPanics() ProgramOption { - return func(m *Program) { - m.CatchPanics = false + return func(p *Program) { + p.CatchPanics = false } } @@ -114,8 +118,8 @@ func WithMouseAllMotion() ProgramOption { // programs. For example, your program could behave like a daemon if output is // not a TTY. func WithoutRenderer() ProgramOption { - return func(m *Program) { - m.renderer = &nilRenderer{} + return func(p *Program) { + p.renderer = &nilRenderer{} } } diff --git a/options_test.go b/options_test.go index 1b77977..6648ddd 100644 --- a/options_test.go +++ b/options_test.go @@ -9,8 +9,8 @@ func TestOptions(t *testing.T) { t.Run("output", func(t *testing.T) { var b bytes.Buffer p := NewProgram(nil, WithOutput(&b)) - if p.output != &b { - t.Errorf("expected output to custom, got %v", p.output) + if p.output.TTY() != nil { + t.Errorf("expected output to custom, got %v", p.output.TTY().Fd()) } }) diff --git a/screen.go b/screen.go deleted file mode 100644 index 63db7ae..0000000 --- a/screen.go +++ /dev/null @@ -1,53 +0,0 @@ -package tea - -import ( - "fmt" - "io" - - te "github.com/muesli/termenv" -) - -func hideCursor(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.HideCursorSeq) -} - -func showCursor(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.ShowCursorSeq) -} - -func clearLine(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.EraseLineSeq, 2) -} - -func cursorUp(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.CursorUpSeq, 1) -} - -func cursorDown(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.CursorDownSeq, 1) -} - -func insertLine(w io.Writer, numLines int) { - fmt.Fprintf(w, te.CSI+"%dL", numLines) -} - -func moveCursor(w io.Writer, row, col int) { - fmt.Fprintf(w, te.CSI+te.CursorPositionSeq, row, col) -} - -func changeScrollingRegion(w io.Writer, top, bottom int) { - fmt.Fprintf(w, te.CSI+te.ChangeScrollingRegionSeq, top, bottom) -} - -func cursorBack(w io.Writer, n int) { - fmt.Fprintf(w, te.CSI+te.CursorBackSeq, n) -} - -func enterAltScreen(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.AltScreenSeq) - moveCursor(w, 0, 0) -} - -func exitAltScreen(w io.Writer) { - fmt.Fprintf(w, te.CSI+te.ExitAltScreenSeq) -} diff --git a/screen_test.go b/screen_test.go deleted file mode 100644 index 93cb50d..0000000 --- a/screen_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package tea - -import ( - "bytes" - "io" - "testing" -) - -func TestScreen(t *testing.T) { - exercise := func(t *testing.T, fn func(io.Writer), expect []byte) { - var w bytes.Buffer - fn(&w) - if !bytes.Equal(w.Bytes(), expect) { - t.Errorf("expected %q, got %q", expect, w.Bytes()) - } - } - - t.Run("change scrolling region", func(t *testing.T) { - exercise(t, func(w io.Writer) { - changeScrollingRegion(w, 16, 22) - }, []byte("\x1b[16;22r")) - }) - - t.Run("line", func(t *testing.T) { - t.Run("clear", func(t *testing.T) { - exercise(t, clearLine, []byte("\x1b[2K")) - }) - - t.Run("insert", func(t *testing.T) { - exercise(t, func(w io.Writer) { - insertLine(w, 12) - }, []byte("\x1b[12L")) - }) - }) - - t.Run("cursor", func(t *testing.T) { - t.Run("hide", func(t *testing.T) { - exercise(t, hideCursor, []byte("\x1b[?25l")) - }) - - t.Run("show", func(t *testing.T) { - exercise(t, showCursor, []byte("\x1b[?25h")) - }) - - t.Run("up", func(t *testing.T) { - exercise(t, cursorUp, []byte("\x1b[1A")) - }) - - t.Run("down", func(t *testing.T) { - exercise(t, cursorDown, []byte("\x1b[1B")) - }) - - t.Run("move", func(t *testing.T) { - exercise(t, func(w io.Writer) { - moveCursor(w, 10, 20) - }, []byte("\x1b[10;20H")) - }) - - t.Run("back", func(t *testing.T) { - exercise(t, func(w io.Writer) { - cursorBack(w, 15) - }, []byte("\x1b[15D")) - }) - }) -} diff --git a/standard_renderer.go b/standard_renderer.go index d2bc387..6c1c50b 100644 --- a/standard_renderer.go +++ b/standard_renderer.go @@ -10,6 +10,7 @@ import ( "github.com/muesli/ansi/compressor" "github.com/muesli/reflow/truncate" + "github.com/muesli/termenv" ) const ( @@ -24,7 +25,7 @@ const ( // In cases where very high performance is needed the renderer can be told // to exclude ranges of lines, allowing them to be written to directly. type standardRenderer struct { - out io.Writer + out *termenv.Output buf bytes.Buffer queuedMessageLines []string framerate time.Duration @@ -49,7 +50,7 @@ type standardRenderer struct { // newRenderer creates a new renderer. Normally you'll want to initialize it // with os.Stdout as the first argument. -func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) renderer { +func newRenderer(out *termenv.Output, mtx *sync.Mutex, useANSICompressor bool) renderer { r := &standardRenderer{ out: out, mtx: mtx, @@ -58,7 +59,7 @@ func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) rendere queuedMessageLines: []string{}, } if r.useANSICompressor { - r.out = &compressor.Writer{Forward: out} + r.out = termenv.NewOutput(&compressor.Writer{Forward: out}) } return r } @@ -75,13 +76,13 @@ func (r *standardRenderer) start() { // stop permanently halts the renderer, rendering the final frame. func (r *standardRenderer) stop() { r.flush() - clearLine(r.out) + r.out.ClearLine() r.once.Do(func() { close(r.done) }) if r.useANSICompressor { - if w, ok := r.out.(io.WriteCloser); ok { + if w, ok := r.out.TTY().(io.WriteCloser); ok { _ = w.Close() } } @@ -89,7 +90,7 @@ func (r *standardRenderer) stop() { // kill halts the renderer. The final frame will not be rendered. func (r *standardRenderer) kill() { - clearLine(r.out) + r.out.ClearLine() r.once.Do(func() { close(r.done) }) @@ -122,7 +123,8 @@ func (r *standardRenderer) flush() { } // Output buffer - out := new(bytes.Buffer) + buf := &bytes.Buffer{} + out := termenv.NewOutput(buf) newLines := strings.Split(r.buf.String(), "\n") numLinesThisFlush := len(newLines) @@ -145,10 +147,10 @@ func (r *standardRenderer) flush() { if (len(newLines) <= len(oldLines)) && (len(newLines) > i && len(oldLines) > i) && (newLines[i] == oldLines[i]) { skipLines[i] = struct{}{} } else if _, exists := r.ignoreLines[i]; !exists { - clearLine(out) + out.ClearLine() } - cursorUp(out) + out.CursorUp(1) } if _, exists := r.ignoreLines[0]; !exists { @@ -161,8 +163,8 @@ func (r *standardRenderer) flush() { // standard (whereas others are proprietary to, say, VT100/VT52). // If cursor previous line (ESC[ + + F) were better supported // we could use that above to eliminate this step. - cursorBack(out, r.width) - clearLine(out) + out.CursorBack(r.width) + out.ClearLine() } } @@ -179,7 +181,7 @@ func (r *standardRenderer) flush() { if _, skip := skipLines[i]; skip { // Unless this is the last line, move the cursor down. if i < len(newLines)-1 { - cursorDown(out) + out.CursorDown(1) } } else { line := newLines[i] @@ -195,10 +197,10 @@ func (r *standardRenderer) flush() { line = truncate.String(line, uint(r.width)) } - _, _ = io.WriteString(out, line) + _, _ = out.WriteString(line) if i < len(newLines)-1 { - _, _ = io.WriteString(out, "\r\n") + _, _ = out.WriteString("\r\n") } } } @@ -210,12 +212,12 @@ func (r *standardRenderer) flush() { // This case fixes a bug in macOS terminal. In other terminals the // other case seems to do the job regardless of whether or not we're // using the full terminal window. - moveCursor(out, r.linesRendered, 0) + out.MoveCursor(r.linesRendered, 0) } else { - cursorBack(out, r.width) + out.CursorBack(r.width) } - _, _ = r.out.Write(out.Bytes()) + _, _ = r.out.Write(buf.Bytes()) r.lastRender = r.buf.String() r.buf.Reset() } @@ -270,15 +272,17 @@ func (r *standardRenderer) setIgnoredLines(from int, to int) { // Erase ignored lines if r.linesRendered > 0 { - out := new(bytes.Buffer) + buf := &bytes.Buffer{} + out := termenv.NewOutput(buf) + for i := r.linesRendered - 1; i >= 0; i-- { if _, exists := r.ignoreLines[i]; exists { - clearLine(out) + out.ClearLine() } - cursorUp(out) + out.CursorUp(1) } - moveCursor(out, r.linesRendered, 0) // put cursor back - _, _ = r.out.Write(out.Bytes()) + out.MoveCursor(r.linesRendered, 0) // put cursor back + _, _ = r.out.Write(buf.Bytes()) } } @@ -311,18 +315,19 @@ func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary r.mtx.Lock() defer r.mtx.Unlock() - b := new(bytes.Buffer) + buf := &bytes.Buffer{} + out := termenv.NewOutput(buf) - changeScrollingRegion(b, topBoundary, bottomBoundary) - moveCursor(b, topBoundary, 0) - insertLine(b, len(lines)) - _, _ = io.WriteString(b, strings.Join(lines, "\r\n")) - changeScrollingRegion(b, 0, r.height) + out.ChangeScrollingRegion(topBoundary, bottomBoundary) + out.MoveCursor(topBoundary, 0) + out.InsertLines(len(lines)) + _, _ = out.WriteString(strings.Join(lines, "\r\n")) + out.ChangeScrollingRegion(0, r.height) // Move cursor back to where the main rendering routine expects it to be - moveCursor(b, r.linesRendered, 0) + out.MoveCursor(r.linesRendered, 0) - _, _ = r.out.Write(b.Bytes()) + _, _ = r.out.Write(buf.Bytes()) } // insertBottom effectively scrolls down. It inserts lines at the bottom of @@ -338,17 +343,18 @@ func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBound r.mtx.Lock() defer r.mtx.Unlock() - b := new(bytes.Buffer) + buf := &bytes.Buffer{} + out := termenv.NewOutput(buf) - changeScrollingRegion(b, topBoundary, bottomBoundary) - moveCursor(b, bottomBoundary, 0) - _, _ = io.WriteString(b, "\r\n"+strings.Join(lines, "\r\n")) - changeScrollingRegion(b, 0, r.height) + out.ChangeScrollingRegion(topBoundary, bottomBoundary) + out.MoveCursor(bottomBoundary, 0) + _, _ = out.WriteString("\r\n" + strings.Join(lines, "\r\n")) + out.ChangeScrollingRegion(0, r.height) // Move cursor back to where the main rendering routine expects it to be - moveCursor(b, r.linesRendered, 0) + out.MoveCursor(r.linesRendered, 0) - _, _ = r.out.Write(b.Bytes()) + _, _ = r.out.Write(buf.Bytes()) } // handleMessages handles internal messages for the renderer. diff --git a/tea.go b/tea.go index 547e011..f25d55f 100644 --- a/tea.go +++ b/tea.go @@ -23,7 +23,7 @@ import ( "github.com/containerd/console" isatty "github.com/mattn/go-isatty" "github.com/muesli/cancelreader" - te "github.com/muesli/termenv" + "github.com/muesli/termenv" "golang.org/x/term" ) @@ -89,9 +89,10 @@ type Program struct { errs chan error readLoopDone chan struct{} - output io.Writer // where to send output. this will usually be os.Stdout. - input io.Reader // this will usually be os.Stdin. - cancelReader cancelreader.CancelReader + output *termenv.Output // where to send output. this will usually be os.Stdout. + restoreOutput func() error + input io.Reader // this will usually be os.Stdin. + cancelReader cancelreader.CancelReader renderer renderer altScreenActive bool @@ -253,7 +254,6 @@ func NewProgram(model Model, opts ...ProgramOption) *Program { p := &Program{ mtx: &sync.Mutex{}, initialModel: model, - output: os.Stdout, input: os.Stdin, msgs: make(chan Msg), CatchPanics: true, @@ -265,6 +265,16 @@ func NewProgram(model Model, opts ...ProgramOption) *Program { opt(p) } + // if no output was set, set it to stdout + if p.output == nil { + p.output = termenv.DefaultOutput() + + // cache detected color values + termenv.WithColorCache(true)(p.output) + } + + p.restoreOutput, _ = termenv.EnableVirtualTerminalProcessing(p.output) + return p } @@ -429,7 +439,7 @@ func (p *Program) StartReturningModel() (Model, error) { } defer p.cancelReader.Close() // nolint:errcheck - if f, ok := p.output.(*os.File); ok && isatty.IsTerminal(f.Fd()) { + if f, ok := p.output.TTY().(*os.File); ok && isatty.IsTerminal(f.Fd()) { // Get the initial terminal size and send it to the program. go func() { w, h, err := term.GetSize(int(f.Fd())) @@ -527,7 +537,7 @@ func (p *Program) StartReturningModel() (Model, error) { p.DisableMouseAllMotion() case hideCursorMsg: - hideCursor(p.output) + p.output.HideCursor() case execMsg: // NB: this blocks. @@ -606,6 +616,10 @@ func (p *Program) shutdown(kill bool) { p.DisableMouseCellMotion() p.DisableMouseAllMotion() _ = p.restoreTerminalState() + + if p.restoreOutput != nil { + _ = p.restoreOutput() + } } // EnterAltScreen enters the alternate screen buffer, which consumes the entire @@ -620,7 +634,8 @@ func (p *Program) EnterAltScreen() { return } - enterAltScreen(p.output) + p.output.AltScreen() + p.output.MoveCursor(0, 0) p.altScreenActive = true if p.renderer != nil { @@ -639,7 +654,7 @@ func (p *Program) ExitAltScreen() { return } - exitAltScreen(p.output) + p.output.ExitAltScreen() p.altScreenActive = false if p.renderer != nil { @@ -654,7 +669,8 @@ func (p *Program) ExitAltScreen() { func (p *Program) EnableMouseCellMotion() { p.mtx.Lock() defer p.mtx.Unlock() - fmt.Fprintf(p.output, te.CSI+te.EnableMouseCellMotionSeq) + + p.output.EnableMouseCellMotion() } // DisableMouseCellMotion disables Mouse Cell Motion tracking. This will be @@ -664,7 +680,8 @@ func (p *Program) EnableMouseCellMotion() { func (p *Program) DisableMouseCellMotion() { p.mtx.Lock() defer p.mtx.Unlock() - fmt.Fprintf(p.output, te.CSI+te.DisableMouseCellMotionSeq) + + p.output.DisableMouseCellMotion() } // EnableMouseAllMotion enables mouse click, release, wheel and motion events, @@ -675,7 +692,8 @@ func (p *Program) DisableMouseCellMotion() { func (p *Program) EnableMouseAllMotion() { p.mtx.Lock() defer p.mtx.Unlock() - fmt.Fprintf(p.output, te.CSI+te.EnableMouseAllMotionSeq) + + p.output.EnableMouseAllMotion() } // DisableMouseAllMotion disables All Motion mouse tracking. This will be @@ -685,7 +703,8 @@ func (p *Program) EnableMouseAllMotion() { func (p *Program) DisableMouseAllMotion() { p.mtx.Lock() defer p.mtx.Unlock() - fmt.Fprintf(p.output, te.CSI+te.DisableMouseAllMotionSeq) + + p.output.DisableMouseAllMotion() } // ReleaseTerminal restores the original terminal state and cancels the input diff --git a/tty.go b/tty.go index 17e508b..e4f75cf 100644 --- a/tty.go +++ b/tty.go @@ -20,14 +20,14 @@ func (p *Program) initTerminal() error { } } - hideCursor(p.output) + p.output.HideCursor() return nil } // restoreTerminalState restores the terminal to the state prior to running the // Bubble Tea program. func (p Program) restoreTerminalState() error { - showCursor(p.output) + p.output.ShowCursor() if p.console != nil { err := p.console.Reset() diff --git a/tty_windows.go b/tty_windows.go index 9cfc7d9..be415ae 100644 --- a/tty_windows.go +++ b/tty_windows.go @@ -4,11 +4,9 @@ package tea import ( - "io" "os" "github.com/containerd/console" - "golang.org/x/sys/windows" ) func (p *Program) initInput() error { @@ -26,8 +24,6 @@ func (p *Program) initInput() error { p.console = c } - enableAnsiColors(p.output) - return nil } @@ -49,18 +45,3 @@ func openInputTTY() (*os.File, error) { } return f, nil } - -// enableAnsiColors enables support for ANSI color sequences in Windows -// default console. Note that this only works with Windows 10. -func enableAnsiColors(w io.Writer) { - f, ok := w.(*os.File) - if !ok { - return - } - - stdout := windows.Handle(f.Fd()) - var originalMode uint32 - - _ = windows.GetConsoleMode(stdout, &originalMode) - _ = windows.SetConsoleMode(stdout, originalMode|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) -} diff --git a/tutorials/go.sum b/tutorials/go.sum index 270f8f5..76e0059 100644 --- a/tutorials/go.sum +++ b/tutorials/go.sum @@ -1,29 +1,30 @@ +github.com/aymanbagabas/go-osc52 v1.0.3 h1:DTwqENW7X9arYimJrPeGZcV0ln14sGMt3pHZspWD+Mg= +github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739 h1:QANkGiGr39l1EESqrE0gZw0/AJNYzIvoGLhIoVYtluI= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= +github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= +github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/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-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=