All checks were successful
continuous-integration/drone/push Build is passing
See #478
787 lines
20 KiB
Go
787 lines
20 KiB
Go
package tea
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/charmbracelet/x/ansi"
|
|
"github.com/muesli/ansi/compressor"
|
|
)
|
|
|
|
const (
|
|
// defaultFramerate specifies the maximum interval at which we should
|
|
// update the view.
|
|
defaultFPS = 60
|
|
maxFPS = 120
|
|
)
|
|
|
|
// standardRenderer is a framerate-based terminal renderer, updating the view
|
|
// at a given framerate to avoid overloading the terminal emulator.
|
|
//
|
|
// 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 {
|
|
mtx *sync.Mutex
|
|
out io.Writer
|
|
|
|
buf bytes.Buffer
|
|
queuedMessageLines []string
|
|
framerate time.Duration
|
|
ticker *time.Ticker
|
|
done chan struct{}
|
|
lastRender string
|
|
lastRenderedLines []string
|
|
linesRendered int
|
|
altLinesRendered int
|
|
useANSICompressor bool
|
|
once sync.Once
|
|
|
|
// cursor visibility state
|
|
cursorHidden bool
|
|
|
|
// essentially whether or not we're using the full size of the terminal
|
|
altScreenActive bool
|
|
|
|
// whether or not we're currently using bracketed paste
|
|
bpActive bool
|
|
|
|
// reportingFocus whether reporting focus events is enabled
|
|
reportingFocus bool
|
|
|
|
// renderer dimensions; usually the size of the window
|
|
width int
|
|
height int
|
|
|
|
// lines explicitly set not to render
|
|
ignoreLines map[int]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, useANSICompressor bool, fps int) renderer {
|
|
if fps < 1 {
|
|
fps = defaultFPS
|
|
} else if fps > maxFPS {
|
|
fps = maxFPS
|
|
}
|
|
r := &standardRenderer{
|
|
out: out,
|
|
mtx: &sync.Mutex{},
|
|
done: make(chan struct{}),
|
|
framerate: time.Second / time.Duration(fps),
|
|
useANSICompressor: useANSICompressor,
|
|
queuedMessageLines: []string{},
|
|
}
|
|
if r.useANSICompressor {
|
|
r.out = &compressor.Writer{Forward: out}
|
|
}
|
|
return r
|
|
}
|
|
|
|
// start starts the renderer.
|
|
func (r *standardRenderer) start() {
|
|
if r.ticker == nil {
|
|
r.ticker = time.NewTicker(r.framerate)
|
|
} else {
|
|
// If the ticker already exists, it has been stopped and we need to
|
|
// reset it.
|
|
r.ticker.Reset(r.framerate)
|
|
}
|
|
|
|
// Since the renderer can be restarted after a stop, we need to reset
|
|
// the done channel and its corresponding sync.Once.
|
|
r.once = sync.Once{}
|
|
|
|
go r.listen()
|
|
}
|
|
|
|
// stop permanently halts the renderer, rendering the final frame.
|
|
func (r *standardRenderer) stop() {
|
|
// Stop the renderer before acquiring the mutex to avoid a deadlock.
|
|
r.once.Do(func() {
|
|
r.done <- struct{}{}
|
|
})
|
|
|
|
// flush locks the mutex
|
|
r.flush()
|
|
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.EraseEntireLine)
|
|
// Move the cursor back to the beginning of the line
|
|
r.execute("\r")
|
|
|
|
if r.useANSICompressor {
|
|
if w, ok := r.out.(io.WriteCloser); ok {
|
|
_ = w.Close()
|
|
}
|
|
}
|
|
}
|
|
|
|
// execute writes a sequence to the terminal.
|
|
func (r *standardRenderer) execute(seq string) {
|
|
_, _ = io.WriteString(r.out, seq)
|
|
}
|
|
|
|
// kill halts the renderer. The final frame will not be rendered.
|
|
func (r *standardRenderer) kill() {
|
|
// Stop the renderer before acquiring the mutex to avoid a deadlock.
|
|
r.once.Do(func() {
|
|
r.done <- struct{}{}
|
|
})
|
|
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.EraseEntireLine)
|
|
// Move the cursor back to the beginning of the line
|
|
r.execute("\r")
|
|
}
|
|
|
|
// listen waits for ticks on the ticker, or a signal to stop the renderer.
|
|
func (r *standardRenderer) listen() {
|
|
for {
|
|
select {
|
|
case <-r.done:
|
|
r.ticker.Stop()
|
|
return
|
|
|
|
case <-r.ticker.C:
|
|
r.flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
// flush renders the buffer.
|
|
func (r *standardRenderer) flush() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
if r.buf.Len() == 0 || r.buf.String() == r.lastRender {
|
|
// Nothing to do.
|
|
return
|
|
}
|
|
|
|
// Output buffer.
|
|
buf := &bytes.Buffer{}
|
|
|
|
// Moving to the beginning of the section, that we rendered.
|
|
if r.altScreenActive {
|
|
buf.WriteString(ansi.CursorHomePosition)
|
|
} else if r.linesRendered > 1 {
|
|
buf.WriteString(ansi.CursorUp(r.linesRendered - 1))
|
|
}
|
|
|
|
newLines := strings.Split(r.buf.String(), "\n")
|
|
|
|
// If we know the output's height, we can use it to determine how many
|
|
// lines we can render. We drop lines from the top of the render buffer if
|
|
// necessary, as we can't navigate the cursor into the terminal's scrollback
|
|
// buffer.
|
|
if r.height > 0 && len(newLines) > r.height {
|
|
newLines = newLines[len(newLines)-r.height:]
|
|
}
|
|
|
|
flushQueuedMessages := len(r.queuedMessageLines) > 0 && !r.altScreenActive
|
|
|
|
if flushQueuedMessages {
|
|
// Dump the lines we've queued up for printing.
|
|
for _, line := range r.queuedMessageLines {
|
|
if ansi.StringWidth(line) < r.width {
|
|
// We only erase the rest of the line when the line is shorter than
|
|
// the width of the terminal. When the cursor reaches the end of
|
|
// the line, any escape sequences that follow will only affect the
|
|
// last cell of the line.
|
|
|
|
// Removing previously rendered content at the end of line.
|
|
line = line + ansi.EraseLineRight
|
|
}
|
|
|
|
_, _ = buf.WriteString(line)
|
|
_, _ = buf.WriteString("\r\n")
|
|
}
|
|
// Clear the queued message lines.
|
|
r.queuedMessageLines = []string{}
|
|
}
|
|
|
|
// Paint new lines.
|
|
for i := 0; i < len(newLines); i++ {
|
|
canSkip := !flushQueuedMessages && // Queuing messages triggers repaint -> we don't have access to previous frame content.
|
|
len(r.lastRenderedLines) > i && r.lastRenderedLines[i] == newLines[i] // Previously rendered line is the same.
|
|
|
|
if _, ignore := r.ignoreLines[i]; ignore || canSkip {
|
|
// Unless this is the last line, move the cursor down.
|
|
if i < len(newLines)-1 {
|
|
buf.WriteByte('\n')
|
|
}
|
|
continue
|
|
}
|
|
|
|
if i == 0 && r.lastRender == "" {
|
|
// On first render, reset the cursor to the start of the line
|
|
// before writing anything.
|
|
buf.WriteByte('\r')
|
|
}
|
|
|
|
line := newLines[i]
|
|
|
|
// Truncate lines wider than the width of the window to avoid
|
|
// wrapping, which will mess up rendering. If we don't have the
|
|
// width of the window this will be ignored.
|
|
//
|
|
// Note that on Windows we only get the width of the window on
|
|
// program initialization, so after a resize this won't perform
|
|
// correctly (signal SIGWINCH is not supported on Windows).
|
|
if r.width > 0 {
|
|
line = ansi.Truncate(line, r.width, "")
|
|
}
|
|
|
|
if ansi.StringWidth(line) < r.width {
|
|
// We only erase the rest of the line when the line is shorter than
|
|
// the width of the terminal. When the cursor reaches the end of
|
|
// the line, any escape sequences that follow will only affect the
|
|
// last cell of the line.
|
|
|
|
// Removing previously rendered content at the end of line.
|
|
line = line + ansi.EraseLineRight
|
|
}
|
|
|
|
_, _ = buf.WriteString(line)
|
|
|
|
if i < len(newLines)-1 {
|
|
_, _ = buf.WriteString("\r\n")
|
|
}
|
|
}
|
|
|
|
// Clearing left over content from last render.
|
|
if r.lastLinesRendered() > len(newLines) {
|
|
buf.WriteString(ansi.EraseScreenBelow)
|
|
}
|
|
|
|
if r.altScreenActive {
|
|
r.altLinesRendered = len(newLines)
|
|
} else {
|
|
r.linesRendered = len(newLines)
|
|
}
|
|
|
|
// Make sure the cursor is at the start of the last line to keep rendering
|
|
// behavior consistent.
|
|
if r.altScreenActive {
|
|
// 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.
|
|
buf.WriteString(ansi.CursorPosition(0, len(newLines)))
|
|
} else {
|
|
buf.WriteString(ansi.CursorBackward(r.width))
|
|
}
|
|
|
|
_, _ = r.out.Write(buf.Bytes())
|
|
r.lastRender = r.buf.String()
|
|
|
|
// Save previously rendered lines for comparison in the next render. If we
|
|
// don't do this, we can't skip rendering lines that haven't changed.
|
|
// See https://github.com/charmbracelet/bubbletea/pull/1233
|
|
r.lastRenderedLines = newLines
|
|
r.buf.Reset()
|
|
}
|
|
|
|
// lastLinesRendered returns the number of lines rendered lastly.
|
|
func (r *standardRenderer) lastLinesRendered() int {
|
|
if r.altScreenActive {
|
|
return r.altLinesRendered
|
|
}
|
|
return r.linesRendered
|
|
}
|
|
|
|
// write writes to the internal buffer. The buffer will be outputted via the
|
|
// ticker which calls flush().
|
|
func (r *standardRenderer) write(s string) {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
r.buf.Reset()
|
|
|
|
// If an empty string was passed we should clear existing output and
|
|
// rendering nothing. Rather than introduce additional state to manage
|
|
// this, we render a single space as a simple (albeit less correct)
|
|
// solution.
|
|
if s == "" {
|
|
s = " "
|
|
}
|
|
|
|
_, _ = r.buf.WriteString(s)
|
|
}
|
|
|
|
func (r *standardRenderer) repaint() {
|
|
r.lastRender = ""
|
|
r.lastRenderedLines = nil
|
|
}
|
|
|
|
func (r *standardRenderer) clearScreen() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.EraseEntireScreen)
|
|
r.execute(ansi.CursorHomePosition)
|
|
|
|
r.repaint()
|
|
}
|
|
|
|
func (r *standardRenderer) altScreen() bool {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
return r.altScreenActive
|
|
}
|
|
|
|
func (r *standardRenderer) enterAltScreen() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
if r.altScreenActive {
|
|
return
|
|
}
|
|
|
|
r.altScreenActive = true
|
|
r.execute(ansi.SetAltScreenSaveCursorMode)
|
|
|
|
// Ensure that the terminal is cleared, even when it doesn't support
|
|
// alt screen (or alt screen support is disabled, like GNU screen by
|
|
// default).
|
|
//
|
|
// Note: we can't use r.clearScreen() here because the mutex is already
|
|
// locked.
|
|
r.execute(ansi.EraseEntireScreen)
|
|
r.execute(ansi.CursorHomePosition)
|
|
|
|
// cmd.exe and other terminals keep separate cursor states for the AltScreen
|
|
// and the main buffer. We have to explicitly reset the cursor visibility
|
|
// whenever we enter AltScreen.
|
|
if r.cursorHidden {
|
|
r.execute(ansi.HideCursor)
|
|
} else {
|
|
r.execute(ansi.ShowCursor)
|
|
}
|
|
|
|
// Entering the alt screen resets the lines rendered count.
|
|
r.altLinesRendered = 0
|
|
|
|
r.repaint()
|
|
}
|
|
|
|
func (r *standardRenderer) exitAltScreen() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
if !r.altScreenActive {
|
|
return
|
|
}
|
|
|
|
r.altScreenActive = false
|
|
r.execute(ansi.ResetAltScreenSaveCursorMode)
|
|
|
|
// cmd.exe and other terminals keep separate cursor states for the AltScreen
|
|
// and the main buffer. We have to explicitly reset the cursor visibility
|
|
// whenever we exit AltScreen.
|
|
if r.cursorHidden {
|
|
r.execute(ansi.HideCursor)
|
|
} else {
|
|
r.execute(ansi.ShowCursor)
|
|
}
|
|
|
|
r.repaint()
|
|
}
|
|
|
|
func (r *standardRenderer) showCursor() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.cursorHidden = false
|
|
r.execute(ansi.ShowCursor)
|
|
}
|
|
|
|
func (r *standardRenderer) hideCursor() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.cursorHidden = true
|
|
r.execute(ansi.HideCursor)
|
|
}
|
|
|
|
func (r *standardRenderer) enableMouseCellMotion() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.SetButtonEventMouseMode)
|
|
}
|
|
|
|
func (r *standardRenderer) disableMouseCellMotion() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.ResetButtonEventMouseMode)
|
|
}
|
|
|
|
func (r *standardRenderer) enableMouseAllMotion() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.SetAnyEventMouseMode)
|
|
}
|
|
|
|
func (r *standardRenderer) disableMouseAllMotion() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.ResetAnyEventMouseMode)
|
|
}
|
|
|
|
func (r *standardRenderer) enableMouseSGRMode() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.SetSgrExtMouseMode)
|
|
}
|
|
|
|
func (r *standardRenderer) disableMouseSGRMode() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.ResetSgrExtMouseMode)
|
|
}
|
|
|
|
func (r *standardRenderer) enableBracketedPaste() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.SetBracketedPasteMode)
|
|
r.bpActive = true
|
|
}
|
|
|
|
func (r *standardRenderer) disableBracketedPaste() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.ResetBracketedPasteMode)
|
|
r.bpActive = false
|
|
}
|
|
|
|
func (r *standardRenderer) bracketedPasteActive() bool {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
return r.bpActive
|
|
}
|
|
|
|
func (r *standardRenderer) enableReportFocus() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.SetFocusEventMode)
|
|
r.reportingFocus = true
|
|
}
|
|
|
|
func (r *standardRenderer) disableReportFocus() {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
r.execute(ansi.ResetFocusEventMode)
|
|
r.reportingFocus = false
|
|
}
|
|
|
|
func (r *standardRenderer) reportFocus() bool {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
return r.reportingFocus
|
|
}
|
|
|
|
// setWindowTitle sets the terminal window title.
|
|
func (r *standardRenderer) setWindowTitle(title string) {
|
|
r.execute(ansi.SetWindowTitle(title))
|
|
}
|
|
|
|
// setIgnoredLines specifies lines not to be touched by the standard Bubble Tea
|
|
// renderer.
|
|
func (r *standardRenderer) setIgnoredLines(from int, to int) {
|
|
// Lock if we're going to be clearing some lines since we don't want
|
|
// anything jacking our cursor.
|
|
if r.lastLinesRendered() > 0 {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
}
|
|
|
|
if r.ignoreLines == nil {
|
|
r.ignoreLines = make(map[int]struct{})
|
|
}
|
|
for i := from; i < to; i++ {
|
|
r.ignoreLines[i] = struct{}{}
|
|
}
|
|
|
|
// Erase ignored lines
|
|
lastLinesRendered := r.lastLinesRendered()
|
|
if lastLinesRendered > 0 {
|
|
buf := &bytes.Buffer{}
|
|
|
|
for i := lastLinesRendered - 1; i >= 0; i-- {
|
|
if _, exists := r.ignoreLines[i]; exists {
|
|
buf.WriteString(ansi.EraseEntireLine)
|
|
}
|
|
buf.WriteString(ansi.CUU1)
|
|
}
|
|
buf.WriteString(ansi.CursorPosition(0, lastLinesRendered)) // put cursor back
|
|
_, _ = r.out.Write(buf.Bytes())
|
|
}
|
|
}
|
|
|
|
// clearIgnoredLines returns control of any ignored lines to the standard
|
|
// Bubble Tea renderer. That is, any lines previously set to be ignored can be
|
|
// rendered to again.
|
|
func (r *standardRenderer) clearIgnoredLines() {
|
|
r.ignoreLines = nil
|
|
}
|
|
|
|
// insertTop effectively scrolls up. It inserts lines at the top of a given
|
|
// area designated to be a scrollable region, pushing everything else down.
|
|
// This is roughly how ncurses does it.
|
|
//
|
|
// To call this function use command ScrollUp().
|
|
//
|
|
// For this to work renderer.ignoreLines must be set to ignore the scrollable
|
|
// region since we are bypassing the normal Bubble Tea renderer here.
|
|
//
|
|
// Because this method relies on the terminal dimensions, it's only valid for
|
|
// full-window applications (generally those that use the alternate screen
|
|
// buffer).
|
|
//
|
|
// This method bypasses the normal rendering buffer and is philosophically
|
|
// different than the normal way we approach rendering in Bubble Tea. It's for
|
|
// use in high-performance rendering, such as a pager that could potentially
|
|
// be rendering very complicated ansi. In cases where the content is simpler
|
|
// standard Bubble Tea rendering should suffice.
|
|
//
|
|
// Deprecated: This option is deprecated and will be removed in a future
|
|
// version of this package.
|
|
func (r *standardRenderer) insertTop(lines []string, topBoundary, bottomBoundary int) {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
|
|
buf.WriteString(ansi.CursorPosition(0, topBoundary))
|
|
buf.WriteString(ansi.InsertLine(len(lines)))
|
|
_, _ = buf.WriteString(strings.Join(lines, "\r\n"))
|
|
buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
|
|
|
|
// Move cursor back to where the main rendering routine expects it to be
|
|
buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
|
|
|
|
_, _ = r.out.Write(buf.Bytes())
|
|
}
|
|
|
|
// insertBottom effectively scrolls down. It inserts lines at the bottom of
|
|
// a given area designated to be a scrollable region, pushing everything else
|
|
// up. This is roughly how ncurses does it.
|
|
//
|
|
// To call this function use the command ScrollDown().
|
|
//
|
|
// See note in insertTop() for caveats, how this function only makes sense for
|
|
// full-window applications, and how it differs from the normal way we do
|
|
// rendering in Bubble Tea.
|
|
//
|
|
// Deprecated: This option is deprecated and will be removed in a future
|
|
// version of this package.
|
|
func (r *standardRenderer) insertBottom(lines []string, topBoundary, bottomBoundary int) {
|
|
r.mtx.Lock()
|
|
defer r.mtx.Unlock()
|
|
|
|
buf := &bytes.Buffer{}
|
|
|
|
buf.WriteString(ansi.SetTopBottomMargins(topBoundary, bottomBoundary))
|
|
buf.WriteString(ansi.CursorPosition(0, bottomBoundary))
|
|
_, _ = buf.WriteString("\r\n" + strings.Join(lines, "\r\n"))
|
|
buf.WriteString(ansi.SetTopBottomMargins(0, r.height))
|
|
|
|
// Move cursor back to where the main rendering routine expects it to be
|
|
buf.WriteString(ansi.CursorPosition(0, r.lastLinesRendered()))
|
|
|
|
_, _ = r.out.Write(buf.Bytes())
|
|
}
|
|
|
|
// handleMessages handles internal messages for the renderer.
|
|
func (r *standardRenderer) handleMessages(msg Msg) {
|
|
switch msg := msg.(type) {
|
|
case repaintMsg:
|
|
// Force a repaint by clearing the render cache as we slide into a
|
|
// render.
|
|
r.mtx.Lock()
|
|
r.repaint()
|
|
r.mtx.Unlock()
|
|
|
|
case WindowSizeMsg:
|
|
r.mtx.Lock()
|
|
r.width = msg.Width
|
|
r.height = msg.Height
|
|
r.repaint()
|
|
r.mtx.Unlock()
|
|
|
|
case clearScrollAreaMsg:
|
|
r.clearIgnoredLines()
|
|
|
|
// Force a repaint on the area where the scrollable stuff was in this
|
|
// update cycle
|
|
r.mtx.Lock()
|
|
r.repaint()
|
|
r.mtx.Unlock()
|
|
|
|
case syncScrollAreaMsg:
|
|
// Re-render scrolling area
|
|
r.clearIgnoredLines()
|
|
r.setIgnoredLines(msg.topBoundary, msg.bottomBoundary)
|
|
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
|
|
|
// Force non-scrolling stuff to repaint in this update cycle
|
|
r.mtx.Lock()
|
|
r.repaint()
|
|
r.mtx.Unlock()
|
|
|
|
case scrollUpMsg:
|
|
r.insertTop(msg.lines, msg.topBoundary, msg.bottomBoundary)
|
|
|
|
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()
|
|
}
|
|
}
|
|
}
|
|
|
|
// HIGH-PERFORMANCE RENDERING STUFF
|
|
|
|
type syncScrollAreaMsg struct {
|
|
lines []string
|
|
topBoundary int
|
|
bottomBoundary int
|
|
}
|
|
|
|
// SyncScrollArea performs a paint of the entire region designated to be the
|
|
// scrollable area. This is required to initialize the scrollable region and
|
|
// should also be called on resize (WindowSizeMsg).
|
|
//
|
|
// For high-performance, scroll-based rendering only.
|
|
//
|
|
// Deprecated: This option will be removed in a future version of this package.
|
|
func SyncScrollArea(lines []string, topBoundary int, bottomBoundary int) Cmd {
|
|
return func() Msg {
|
|
return syncScrollAreaMsg{
|
|
lines: lines,
|
|
topBoundary: topBoundary,
|
|
bottomBoundary: bottomBoundary,
|
|
}
|
|
}
|
|
}
|
|
|
|
type clearScrollAreaMsg struct{}
|
|
|
|
// ClearScrollArea deallocates the scrollable region and returns the control of
|
|
// those lines to the main rendering routine.
|
|
//
|
|
// For high-performance, scroll-based rendering only.
|
|
//
|
|
// Deprecated: This option will be removed in a future version of this package.
|
|
func ClearScrollArea() Msg {
|
|
return clearScrollAreaMsg{}
|
|
}
|
|
|
|
type scrollUpMsg struct {
|
|
lines []string
|
|
topBoundary int
|
|
bottomBoundary int
|
|
}
|
|
|
|
// ScrollUp adds lines to the top of the scrollable region, pushing existing
|
|
// lines below down. Lines that are pushed out the scrollable region disappear
|
|
// from view.
|
|
//
|
|
// For high-performance, scroll-based rendering only.
|
|
//
|
|
// Deprecated: This option will be removed in a future version of this package.
|
|
func ScrollUp(newLines []string, topBoundary, bottomBoundary int) Cmd {
|
|
return func() Msg {
|
|
return scrollUpMsg{
|
|
lines: newLines,
|
|
topBoundary: topBoundary,
|
|
bottomBoundary: bottomBoundary,
|
|
}
|
|
}
|
|
}
|
|
|
|
type scrollDownMsg struct {
|
|
lines []string
|
|
topBoundary int
|
|
bottomBoundary int
|
|
}
|
|
|
|
// ScrollDown adds lines to the bottom of the scrollable region, pushing
|
|
// existing lines above up. Lines that are pushed out of the scrollable region
|
|
// disappear from view.
|
|
//
|
|
// For high-performance, scroll-based rendering only.
|
|
//
|
|
// Deprecated: This option will be removed in a future version of this package.
|
|
func ScrollDown(newLines []string, topBoundary, bottomBoundary int) Cmd {
|
|
return func() Msg {
|
|
return scrollDownMsg{
|
|
lines: newLines,
|
|
topBoundary: topBoundary,
|
|
bottomBoundary: bottomBoundary,
|
|
}
|
|
}
|
|
}
|
|
|
|
type printLineMessage struct {
|
|
messageBody string
|
|
}
|
|
|
|
// Println prints above the Program. This output is unmanaged by the program and
|
|
// will persist across renders by the Program.
|
|
//
|
|
// Unlike fmt.Println (but similar to log.Println) 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...),
|
|
}
|
|
}
|
|
}
|