forked from Mirrors/bubbletea
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:
parent
a2d0ac9d38
commit
ebabec7008
|
@ -0,0 +1,139 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/bubbles/progress"
|
||||
"github.com/charmbracelet/bubbles/spinner"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type model struct {
|
||||
packages []string
|
||||
index int
|
||||
width int
|
||||
height int
|
||||
spinner spinner.Model
|
||||
progress progress.Model
|
||||
done bool
|
||||
}
|
||||
|
||||
var (
|
||||
currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211"))
|
||||
subtleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239"))
|
||||
doneStyle = lipgloss.NewStyle().Margin(1, 2)
|
||||
checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓")
|
||||
)
|
||||
|
||||
func newModel() model {
|
||||
p := progress.New(
|
||||
progress.WithDefaultGradient(),
|
||||
progress.WithWidth(40),
|
||||
progress.WithoutPercentage(),
|
||||
)
|
||||
s := spinner.New()
|
||||
s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63"))
|
||||
return model{
|
||||
packages: getPackages(),
|
||||
spinner: s,
|
||||
progress: p,
|
||||
}
|
||||
}
|
||||
|
||||
func (m model) Init() tea.Cmd {
|
||||
return tea.Batch(downloadAndInstall(m.packages[m.index]), m.spinner.Tick)
|
||||
}
|
||||
|
||||
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width, m.height = msg.Width, msg.Height
|
||||
case tea.KeyMsg:
|
||||
switch msg.String() {
|
||||
case "ctrl+c", "esc", "q":
|
||||
return m, tea.Quit
|
||||
}
|
||||
case installedPkgMsg:
|
||||
if m.index >= len(m.packages)-1 {
|
||||
// Everything's been installed. We're done!
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.packages)-1))
|
||||
|
||||
m.index++
|
||||
return m, tea.Batch(
|
||||
progressCmd,
|
||||
tea.Printf("%s %s", checkMark, m.packages[m.index]), // print success message above our program
|
||||
downloadAndInstall(m.packages[m.index]), // download the next package
|
||||
)
|
||||
case spinner.TickMsg:
|
||||
var cmd tea.Cmd
|
||||
m.spinner, cmd = m.spinner.Update(msg)
|
||||
return m, cmd
|
||||
case progress.FrameMsg:
|
||||
newModel, cmd := m.progress.Update(msg)
|
||||
if newModel, ok := newModel.(progress.Model); ok {
|
||||
m.progress = newModel
|
||||
}
|
||||
return m, cmd
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m model) View() string {
|
||||
n := len(m.packages)
|
||||
w := lipgloss.Width(fmt.Sprintf("%d", n))
|
||||
|
||||
if m.done {
|
||||
return doneStyle.Render(fmt.Sprintf("Done! Installed %d packages.\n", n))
|
||||
}
|
||||
|
||||
pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n-1)
|
||||
|
||||
spin := m.spinner.View() + " "
|
||||
prog := m.progress.View()
|
||||
cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount))
|
||||
|
||||
pkgName := currentPkgNameStyle.Render(m.packages[m.index])
|
||||
info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName)
|
||||
|
||||
cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount))
|
||||
gap := strings.Repeat(" ", cellsRemaining)
|
||||
|
||||
return spin + info + gap + prog + pkgCount
|
||||
}
|
||||
|
||||
type installedPkgMsg string
|
||||
|
||||
func downloadAndInstall(pkg string) tea.Cmd {
|
||||
// This is where you'd do i/o stuff to download and install packages. In
|
||||
// our case we're just pausing for a moment to simulate the process.
|
||||
d := time.Millisecond * time.Duration(rand.Intn(500))
|
||||
return tea.Tick(d, func(t time.Time) tea.Msg {
|
||||
return installedPkgMsg(pkg)
|
||||
})
|
||||
}
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().Unix())
|
||||
|
||||
if err := tea.NewProgram(newModel()).Start(); err != nil {
|
||||
fmt.Println("Error running program:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
var packages = []string{
|
||||
"vegeutils",
|
||||
"libgardening",
|
||||
"currykit",
|
||||
"spicerack",
|
||||
"fullenglish",
|
||||
"eggy",
|
||||
"bad-kitty",
|
||||
"chai",
|
||||
"hojicha",
|
||||
"libtacos",
|
||||
"babys-monads",
|
||||
"libpurring",
|
||||
"currywurst-devel",
|
||||
"xmodmeow",
|
||||
"licorice-utils",
|
||||
"cashew-apple",
|
||||
"rock-lobster",
|
||||
"standmixer",
|
||||
"coffee-CUPS",
|
||||
"libesszet",
|
||||
"zeichenorientierte-benutzerschnittstellen",
|
||||
"schnurrkit",
|
||||
"old-socks-devel",
|
||||
"jalapeño",
|
||||
"molasses-utils",
|
||||
"xkohlrabi",
|
||||
"party-gherkin",
|
||||
"snow-peas",
|
||||
"libyuzu",
|
||||
}
|
||||
|
||||
func getPackages() []string {
|
||||
pkgs := packages
|
||||
copy(pkgs, packages)
|
||||
|
||||
rand.Shuffle(len(pkgs), func(i, j int) {
|
||||
pkgs[i], pkgs[j] = pkgs[j], pkgs[i]
|
||||
})
|
||||
|
||||
for k := range pkgs {
|
||||
pkgs[k] += fmt.Sprintf("-%d.%d.%d", rand.Intn(10), rand.Intn(10), rand.Intn(10))
|
||||
}
|
||||
return pkgs
|
||||
}
|
|
@ -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...),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
tea.go
27
tea.go
|
@ -709,3 +709,30 @@ func (p *Program) RestoreTerminal() error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 (p *Program) Println(args ...interface{}) {
|
||||
p.msgs <- 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 (p *Program) Printf(template string, args ...interface{}) {
|
||||
p.msgs <- printLineMessage{
|
||||
messageBody: fmt.Sprintf(template, args...),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue