feat: use Termenv.Output to write to tty

This commit is contained in:
Christian Muehlhaeuser 2022-06-04 15:14:03 +02:00
parent 5c4218e5f6
commit 6c449e55bf
13 changed files with 111 additions and 219 deletions

View File

@ -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 => ../

View File

@ -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=

3
go.mod
View File

@ -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
)

11
go.sum
View File

@ -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=

View File

@ -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{}
}
}

View File

@ -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())
}
})

View File

@ -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)
}

View File

@ -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"))
})
})
}

View File

@ -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[ + <n> + 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.

45
tea.go
View File

@ -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

4
tty.go
View File

@ -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()

View File

@ -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)
}

View File

@ -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=