feat: print unmanaged output above the application (#249)

* merge Adjective-Object/tea_log_renderer into standard renderer

* rename queuedMessages -> queuedMessageLines & break apart strings during message processing

* delete cursorDownBy

* += 1 -> ++ to make the linter happy

* add skipLines[] tracking back to standard renderer, and add rename skippedLines local to jumpedLines to clarify they are separate comments

* request repaint when a message is recieved

* Convert Println and Printf to commands

* Add package manager example demonstrating tea.Printf

* Use Unix instead of UnixMicro for Go 1.13 support in CI

* fix off by one in std renderer

* add Printf/Println to tea.go

* revert attempt at sequence compression + cursorUpBy

Co-authored-by: Maxwell Huang-Hobbs <mahuangh@microsoft.com>
Co-authored-by: Christian Rocha <christian@rocha.is>
This commit is contained in:
Max
2022-06-22 12:53:02 -04:00
committed by GitHub
parent a2d0ac9d38
commit ebabec7008
4 changed files with 289 additions and 18 deletions

View File

@@ -2,6 +2,7 @@ package tea
import (
"bytes"
"fmt"
"io"
"strings"
"sync"
@@ -23,16 +24,17 @@ 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
buf bytes.Buffer
framerate time.Duration
ticker *time.Ticker
mtx *sync.Mutex
done chan struct{}
lastRender string
linesRendered int
useANSICompressor bool
once sync.Once
out io.Writer
buf bytes.Buffer
queuedMessageLines []string
framerate time.Duration
ticker *time.Ticker
mtx *sync.Mutex
done chan struct{}
lastRender string
linesRendered int
useANSICompressor bool
once sync.Once
// essentially whether or not we're using the full size of the terminal
altScreenActive bool
@@ -49,10 +51,11 @@ type standardRenderer struct {
// with os.Stdout as the first argument.
func newRenderer(out io.Writer, mtx *sync.Mutex, useANSICompressor bool) renderer {
r := &standardRenderer{
out: out,
mtx: mtx,
framerate: defaultFramerate,
useANSICompressor: useANSICompressor,
out: out,
mtx: mtx,
framerate: defaultFramerate,
useANSICompressor: useANSICompressor,
queuedMessageLines: []string{},
}
if r.useANSICompressor {
r.out = &compressor.Writer{Forward: out}
@@ -122,8 +125,16 @@ func (r *standardRenderer) flush() {
out := new(bytes.Buffer)
newLines := strings.Split(r.buf.String(), "\n")
numLinesThisFlush := len(newLines)
oldLines := strings.Split(r.lastRender, "\n")
skipLines := make(map[int]struct{})
flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
// Add any queued messages to this render
if flushQueuedMessages {
newLines = append(r.queuedMessageLines, newLines...)
r.queuedMessageLines = []string{}
}
// Clear any lines we painted in the last render.
if r.linesRendered > 0 {
@@ -163,11 +174,9 @@ func (r *standardRenderer) flush() {
}
}
r.linesRendered = 0
// Paint new lines
for i := 0; i < len(newLines); i++ {
if _, skip := skipLines[r.linesRendered]; skip {
if _, skip := skipLines[i]; skip {
// Unless this is the last line, move the cursor down.
if i < len(newLines)-1 {
cursorDown(out)
@@ -192,8 +201,8 @@ func (r *standardRenderer) flush() {
_, _ = io.WriteString(out, "\r\n")
}
}
r.linesRendered++
}
r.linesRendered = numLinesThisFlush
// Make sure the cursor is at the start of the last line to keep rendering
// behavior consistent.
@@ -383,6 +392,15 @@ func (r *standardRenderer) handleMessages(msg Msg) {
case scrollDownMsg:
r.insertBottom(msg.lines, msg.topBoundary, msg.bottomBoundary)
case printLineMessage:
if !r.altScreenActive {
lines := strings.Split(msg.messageBody, "\n")
r.mtx.Lock()
r.queuedMessageLines = append(r.queuedMessageLines, lines...)
r.repaint()
r.mtx.Unlock()
}
}
}
@@ -460,3 +478,38 @@ func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
}
}
}
type printLineMessage struct {
messageBody string
}
// Printf prints above the Program. This output is unmanaged by the program and
// will persist across renders by the Program.
//
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
// its own line.
//
// If the altscreen is active no output will be printed.
func Println(args ...interface{}) Cmd {
return func() Msg {
return printLineMessage{
messageBody: fmt.Sprint(args...),
}
}
}
// Printf prints above the Program. It takes a format template followed by
// values similar to fmt.Printf. This output is unmanaged by the program and
// will persist across renders by the Program.
//
// Unlike fmt.Printf (but similar to log.Printf) the message will be print on
// its own line.
//
// If the altscreen is active no output will be printed.
func Printf(template string, args ...interface{}) Cmd {
return func() Msg {
return printLineMessage{
messageBody: fmt.Sprintf(template, args...),
}
}
}