- bonfire.Sanitize (additive), moved from diary's console channel: tab kept, CR and LF to a space, every other control character dropped - applied before styling to every error-block line (message, explanation, Try lines, -v chain), process-view item names and errors, the current item of spinner and progress, failure lines, and table headers and cells in both layouts - diary's console handler calls bonfire.Sanitize; README and the writing-commands page say so (WR-05)
192 lines
4.3 KiB
Go
192 lines
4.3 KiB
Go
package bonfire
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
"charm.land/lipgloss/v2/table"
|
|
)
|
|
|
|
// ItemProgress is the optional side of a Progress that knows about items:
|
|
// Current names the item being worked on, Fail records a failed item. The
|
|
// Progress handed to Output.Progress implements it; Current and Failed call
|
|
// it when present.
|
|
type ItemProgress interface {
|
|
Progress
|
|
Current(item string)
|
|
Fail(item string, err error)
|
|
}
|
|
|
|
func (c *console) Spinner(message string, fn func() error) error {
|
|
if fn == nil {
|
|
fn = func() error { return nil }
|
|
}
|
|
if c.globals.Quiet {
|
|
return fn()
|
|
}
|
|
if c.live() {
|
|
started, err := runLive(context.Background(), c.out, c, newSpinnerModel(c.palette, message), func(func(tea.Msg)) error {
|
|
return fn()
|
|
})
|
|
if started {
|
|
return err
|
|
}
|
|
}
|
|
c.Printf("[...] %s\n", message)
|
|
return fn()
|
|
}
|
|
|
|
func (c *console) Progress(total int, fn func(Progress) error) error {
|
|
if total < 1 {
|
|
total = 1
|
|
}
|
|
if fn == nil {
|
|
return nil
|
|
}
|
|
if c.globals.Quiet {
|
|
return fn(&quietProgress{c: c})
|
|
}
|
|
if c.live() {
|
|
started, err := runLive(context.Background(), c.out, c, newProgressModel(c.palette, total), func(send func(tea.Msg)) error {
|
|
return fn(&liveProgress{send: send, total: total})
|
|
})
|
|
if started {
|
|
return err
|
|
}
|
|
}
|
|
return fn(&progressBar{c: c, total: total, lastPct: -1})
|
|
}
|
|
|
|
// progressBar prints a line at every tenth of the work when widgets do not
|
|
// animate.
|
|
type progressBar struct {
|
|
c *console
|
|
total int
|
|
current int
|
|
lastPct int
|
|
}
|
|
|
|
func (p *progressBar) Advance(step int) {
|
|
if step < 1 {
|
|
step = 1
|
|
}
|
|
p.current += step
|
|
if p.current > p.total {
|
|
p.current = p.total
|
|
}
|
|
pct := 0
|
|
if p.total > 0 {
|
|
pct = p.current * 100 / p.total
|
|
}
|
|
if p.lastPct >= 0 && pct/10 == p.lastPct/10 && pct != 100 {
|
|
return
|
|
}
|
|
if pct == 0 && p.lastPct < 0 {
|
|
p.lastPct = 0
|
|
return
|
|
}
|
|
if pct%10 == 0 || pct == 100 {
|
|
p.c.Printf("[%d/%d] %d%%\n", p.current, p.total, pct)
|
|
p.lastPct = pct
|
|
}
|
|
}
|
|
|
|
func (p *progressBar) Current(string) {}
|
|
|
|
func (p *progressBar) Fail(item string, err error) {
|
|
p.c.Error(failureText(item, err))
|
|
}
|
|
|
|
// quietProgress prints nothing but failures (-q).
|
|
type quietProgress struct {
|
|
c *console
|
|
}
|
|
|
|
func (p *quietProgress) Advance(int) {}
|
|
|
|
func (p *quietProgress) Current(string) {}
|
|
|
|
func (p *quietProgress) Fail(item string, err error) {
|
|
p.c.Error(failureText(item, err))
|
|
}
|
|
|
|
// liveProgress drives the live progress model.
|
|
type liveProgress struct {
|
|
send func(tea.Msg)
|
|
total int
|
|
current int
|
|
item string
|
|
}
|
|
|
|
func (p *liveProgress) Advance(step int) {
|
|
if step < 1 {
|
|
step = 1
|
|
}
|
|
p.current = min(p.current+step, p.total)
|
|
p.send(itemMsg{done: p.current, total: p.total, item: p.item})
|
|
}
|
|
|
|
func (p *liveProgress) Current(item string) {
|
|
p.item = item
|
|
p.send(itemMsg{done: p.current, total: p.total, item: item})
|
|
}
|
|
|
|
func (p *liveProgress) Fail(item string, err error) {
|
|
p.send(itemMsg{done: p.current, total: p.total, item: item, err: err})
|
|
p.send(printLine(failureText(item, err)))
|
|
}
|
|
|
|
func failureLine(item string, err error) string {
|
|
if err == nil {
|
|
return item
|
|
}
|
|
return item + ": " + err.Error()
|
|
}
|
|
|
|
// failureText is the failure line as printed: one line without control
|
|
// characters.
|
|
func failureText(item string, err error) string {
|
|
return Sanitize(failureLine(item, err))
|
|
}
|
|
|
|
func (c *console) Table(headers []string, rows [][]string) {
|
|
headers = padRow(headers, len(headers))
|
|
if !c.tty {
|
|
c.Println(strings.Join(headers, "\t"))
|
|
for _, row := range rows {
|
|
c.Println(strings.Join(padRow(row, len(headers)), "\t"))
|
|
}
|
|
return
|
|
}
|
|
padded := make([][]string, 0, len(rows))
|
|
for _, row := range rows {
|
|
padded = append(padded, padRow(row, len(headers)))
|
|
}
|
|
header := c.palette.Header
|
|
t := table.New().
|
|
Border(lipgloss.NormalBorder()).
|
|
Headers(headers...).
|
|
Rows(padded...).
|
|
StyleFunc(func(row, _ int) lipgloss.Style {
|
|
if row == table.HeaderRow {
|
|
return header.Padding(0, 1)
|
|
}
|
|
return lipgloss.NewStyle().Padding(0, 1)
|
|
})
|
|
c.emit(c.out, t.String()+"\n")
|
|
}
|
|
|
|
// padRow returns n cells of row (missing cells empty), each passed through
|
|
// Sanitize so a cell cannot move the cursor or retitle the terminal.
|
|
func padRow(row []string, n int) []string {
|
|
out := make([]string, n)
|
|
for i := 0; i < n; i++ {
|
|
if i < len(row) {
|
|
out[i] = Sanitize(row[i])
|
|
}
|
|
}
|
|
return out
|
|
}
|