2020-06-05 12:40:44 -04:00
|
|
|
package tea
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"io"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/muesli/termenv"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2020-06-16 11:05:04 -04:00
|
|
|
// BlankSymbol, in this case, is used to signal to the renderer to skip
|
|
|
|
// over a given cell and not perform any rendering on it. The const is
|
|
|
|
// literally the Unicode "BLANK SYMBOL" (U+2422).
|
|
|
|
//
|
|
|
|
// This character becomes useful when handing of portions of the screen to
|
|
|
|
// a separate renderer.
|
|
|
|
BlankSymbol = "␢"
|
|
|
|
|
|
|
|
// defaultFramerate specifies the maximum interval at which we should
|
|
|
|
// update the view.
|
2020-06-15 20:01:18 -04:00
|
|
|
defaultFramerate = time.Second / 60
|
2020-06-05 12:40:44 -04:00
|
|
|
)
|
|
|
|
|
2020-06-16 11:05:04 -04:00
|
|
|
// renderer is a timer-based renderer, updating the view at a given framerate
|
|
|
|
// to avoid overloading the terminal emulator.
|
2020-06-05 12:40:44 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-06-11 21:38:27 -04:00
|
|
|
// We have an opportunity here to limit the rendering to the terminal width
|
|
|
|
// and height, but this would mean a few things:
|
2020-06-08 12:43:24 -04:00
|
|
|
//
|
|
|
|
// 1) We'd need to maintain the terminal dimensions internally and listen
|
|
|
|
// for window size changes.
|
|
|
|
//
|
2020-06-16 11:05:04 -04:00
|
|
|
// 2) We'd need to measure the width of lines, accounting for multi-cell
|
|
|
|
// rune widths, commonly found in Chinese, Japanese, Korean, emojis and so
|
|
|
|
// on. We'd use something like go-runewidth
|
2020-06-08 12:43:24 -04:00
|
|
|
// (http://github.com/mattn/go-runewidth).
|
|
|
|
//
|
|
|
|
// 3) We'd need to measure the width of lines excluding ANSI escape
|
|
|
|
// sequences and break lines in the right places accordingly.
|
|
|
|
//
|
|
|
|
// Because of the way this would complicate the renderer, this may not be
|
|
|
|
// the place to do that.
|
|
|
|
|
2020-06-05 12:40:44 -04:00
|
|
|
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 {
|
|
|
|
_, _ = 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()
|
2020-06-05 14:00:39 -04:00
|
|
|
w.buf.Reset()
|
2020-06-05 12:40:44 -04:00
|
|
|
w.buf.WriteString(s)
|
|
|
|
}
|