Buffer/ticker-based renderer

This commit is contained in:
Christian Rocha 2020-06-05 12:40:44 -04:00
parent 4ce9b4ea83
commit 87434a2569
No known key found for this signature in database
GPG Key ID: D6CC7A16E5878018
4 changed files with 116 additions and 36 deletions

View File

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

View File

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

99
renderer.go Normal file
View File

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

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