forked from Mirrors/bubbletea
Buffer/ticker-based renderer
This commit is contained in:
parent
4ce9b4ea83
commit
87434a2569
|
@ -8,5 +8,5 @@ require (
|
|||
github.com/charmbracelet/bubbles v0.0.0-20200526000837-87c7cd778f80
|
||||
github.com/charmbracelet/bubbletea v0.6.4-0.20200525234836-3b8b011b5a26
|
||||
github.com/fogleman/ease v0.0.0-20170301025033-8da417bf1776
|
||||
github.com/muesli/termenv v0.5.2
|
||||
github.com/muesli/termenv v0.5.3-0.20200526053627-d728968dcf83
|
||||
)
|
||||
|
|
|
@ -10,6 +10,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
|
|||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/muesli/termenv v0.5.2 h1:N1Y1dHRtx6OizOgaIQXd8SkJl4T/cCOV+YyWXiuLUEA=
|
||||
github.com/muesli/termenv v0.5.2/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
|
||||
github.com/muesli/termenv v0.5.3-0.20200526053627-d728968dcf83 h1:AfshZBlqAwhCZ27NJ1aPlMcPBihF1squ1GpaollhLQk=
|
||||
github.com/muesli/termenv v0.5.3-0.20200526053627-d728968dcf83/go.mod h1:O1/I6sw+6KcrgAmcs6uiUVr7Lui+DNVbHTzt9Lm/PlI=
|
||||
github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 h1:pd4YKIqCB0U7O2I4gWHgEUA2mCEOENmco0l/bM957bU=
|
||||
github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
|
@ -22,4 +24,6 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtD
|
|||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
package tea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/muesli/termenv"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultFramerate = time.Millisecond * 16
|
||||
)
|
||||
|
||||
type renderer struct {
|
||||
out io.Writer
|
||||
buf bytes.Buffer
|
||||
framerate time.Duration
|
||||
ticker *time.Ticker
|
||||
mtx sync.Mutex
|
||||
done chan struct{}
|
||||
lastRender string
|
||||
linesRendered int
|
||||
}
|
||||
|
||||
func newRenderer(out io.Writer) *renderer {
|
||||
return &renderer{
|
||||
out: out,
|
||||
framerate: defaultFramerate,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renderer) start() {
|
||||
if r.ticker == nil {
|
||||
r.ticker = time.NewTicker(r.framerate)
|
||||
}
|
||||
r.done = make(chan struct{})
|
||||
go r.listen()
|
||||
}
|
||||
|
||||
func (r *renderer) stop() {
|
||||
r.flush()
|
||||
r.done <- struct{}{}
|
||||
}
|
||||
|
||||
func (r *renderer) listen() {
|
||||
for {
|
||||
select {
|
||||
case <-r.ticker.C:
|
||||
if r.ticker != nil {
|
||||
r.flush()
|
||||
}
|
||||
case <-r.done:
|
||||
r.mtx.Lock()
|
||||
r.ticker.Stop()
|
||||
r.ticker = nil
|
||||
r.mtx.Unlock()
|
||||
close(r.done)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *renderer) flush() {
|
||||
if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
|
||||
// Nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
r.mtx.Lock()
|
||||
defer r.mtx.Unlock()
|
||||
|
||||
if r.linesRendered > 0 {
|
||||
termenv.ClearLines(r.linesRendered)
|
||||
}
|
||||
r.linesRendered = 0
|
||||
|
||||
var out bytes.Buffer
|
||||
for _, b := range r.buf.Bytes() {
|
||||
if b == '\n' {
|
||||
r.linesRendered++
|
||||
out.Write([]byte("\r\n"))
|
||||
} else {
|
||||
// TODO: don't write past the terminal width
|
||||
_, _ = out.Write([]byte{b})
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = r.out.Write(out.Bytes())
|
||||
r.lastRender = r.buf.String()
|
||||
r.buf.Reset()
|
||||
}
|
||||
|
||||
func (w *renderer) write(s string) {
|
||||
w.mtx.Lock()
|
||||
defer w.mtx.Unlock()
|
||||
w.buf.WriteString(s)
|
||||
}
|
47
tea.go
47
tea.go
|
@ -1,9 +1,7 @@
|
|||
package tea
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/muesli/termenv"
|
||||
|
@ -76,12 +74,13 @@ func NewProgram(init Init, update Update, view View) *Program {
|
|||
// Start initializes the program.
|
||||
func (p *Program) Start() error {
|
||||
var (
|
||||
model Model
|
||||
cmd Cmd
|
||||
cmds = make(chan Cmd)
|
||||
msgs = make(chan Msg)
|
||||
errs = make(chan error)
|
||||
done = make(chan struct{})
|
||||
model Model
|
||||
cmd Cmd
|
||||
cmds = make(chan Cmd)
|
||||
msgs = make(chan Msg)
|
||||
errs = make(chan error)
|
||||
done = make(chan struct{})
|
||||
mrRenderer = newRenderer(os.Stdout)
|
||||
)
|
||||
|
||||
err := initTerminal()
|
||||
|
@ -98,8 +97,11 @@ func (p *Program) Start() error {
|
|||
}()
|
||||
}
|
||||
|
||||
// Start renderer
|
||||
mrRenderer.start()
|
||||
|
||||
// Render initial view
|
||||
p.render(model)
|
||||
mrRenderer.write(p.view(model))
|
||||
|
||||
// Subscribe to user input
|
||||
go func() {
|
||||
|
@ -152,36 +154,11 @@ func (p *Program) Start() error {
|
|||
|
||||
model, cmd = p.update(msg, model) // run update
|
||||
cmds <- cmd // process command (if any)
|
||||
p.render(model) // render to terminal
|
||||
mrRenderer.write(p.view(model)) // send to renderer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render a view to the terminal. Returns the number of lines rendered.
|
||||
func (p *Program) render(model Model) {
|
||||
view := p.view(model)
|
||||
|
||||
// The view hasn't changed; no need to render
|
||||
if view == p.currentRender {
|
||||
return
|
||||
}
|
||||
|
||||
p.currentRender = view
|
||||
linesRendered := strings.Count(p.currentRender, "\n")
|
||||
|
||||
// Add carriage returns to ensure that the cursor travels to the start of a
|
||||
// column after a newline. Keep in mind that this means that in the rest
|
||||
// of the Tea program newlines should be a normal unix newline (\n).
|
||||
view = strings.Replace(view, "\n", "\r\n", -1)
|
||||
|
||||
p.mutex.Lock()
|
||||
if linesRendered > 0 {
|
||||
termenv.ClearLines(linesRendered)
|
||||
}
|
||||
_, _ = io.WriteString(os.Stdout, view)
|
||||
p.mutex.Unlock()
|
||||
}
|
||||
|
||||
// AltScreen exits the altscreen. This is just a wrapper around the termenv
|
||||
// function.
|
||||
func AltScreen() {
|
||||
|
|
Loading…
Reference in New Issue