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 }