- Spinner, gradient progress bar and the multi-row process view ported from SSU to bubbletea v2 (input off, no signal handler, command context); boxed tables through lipgloss/table - Prompts use huh fields and a single-select port of SSU's selector with its confirmation screen on an interactive terminal; line prompts off a terminal and under --no-ansi are unchanged (piped passwords still work) - -q hides decoration but keeps data and errors; -n returns defaults and Secret fails with a hint on a terminal stdin; --no-ansi and non-TTY runs print today's plain lines - Helpers: GlobalsOf, VerbosityOf, IsQuiet, Interactive, ConfirmProceed, Process, Current, Failed, PrintAbove, ItemProgress, ErrAborted - diary's styled console lines print above a running live view
177 lines
3.8 KiB
Go
177 lines
3.8 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() {
|
|
c.Printf("[...] %s\n", message)
|
|
return fn()
|
|
}
|
|
return runLive(context.Background(), c.out, c, newSpinnerModel(c.palette, message), func(func(tea.Msg)) error {
|
|
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() {
|
|
return fn(&progressBar{c: c, total: total, lastPct: -1})
|
|
}
|
|
return runLive(context.Background(), c.out, c, newProgressModel(c.palette, total), func(send func(tea.Msg)) error {
|
|
return fn(&liveProgress{send: send, total: total})
|
|
})
|
|
}
|
|
|
|
// 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(failureLine(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(failureLine(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(printAboveMsg(failureLine(item, err)))
|
|
}
|
|
|
|
func failureLine(item string, err error) string {
|
|
if err == nil {
|
|
return item
|
|
}
|
|
return item + ": " + err.Error()
|
|
}
|
|
|
|
func (c *console) Table(headers []string, rows [][]string) {
|
|
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")
|
|
}
|
|
|
|
func padRow(row []string, n int) []string {
|
|
out := make([]string, n)
|
|
for i := 0; i < n; i++ {
|
|
if i < len(row) {
|
|
out[i] = row[i]
|
|
}
|
|
}
|
|
return out
|
|
}
|