package bonfire import ( "fmt" "strings" "sync" "time" "unicode/utf8" ) var spinnerFrames = []rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'} const ( boxTopLeft = "┌" boxTopRight = "┐" boxBottomLeft = "└" boxBottomRight = "┘" boxHorizontal = "─" boxVertical = "│" boxTeeRight = "├" boxTeeLeft = "┤" boxTeeDown = "┬" boxTeeUp = "┴" boxCross = "┼" ) func (c *console) Spinner(message string, fn func() error) error { if fn == nil { fn = func() error { return nil } } if !c.tty { c.Printf("[...] %s\n", message) return fn() } done := make(chan struct{}) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() i := 0 ticker := time.NewTicker(80 * time.Millisecond) defer ticker.Stop() c.Printf("\r%s %s", c.style(ansiCyan, string(spinnerFrames[0])), message) for { select { case <-done: return case <-ticker.C: i++ frame := spinnerFrames[i%len(spinnerFrames)] c.Printf("\r%s %s", c.style(ansiCyan, string(frame)), message) } } }() err := fn() close(done) wg.Wait() if err != nil { c.Printf("\r%s %s\n", c.style(ansiRed, symCross), message) return err } c.Printf("\r%s %s\n", c.style(ansiGreen, symCheck), message) return nil } func (c *console) Progress(total int, fn func(Progress) error) error { if total < 1 { total = 1 } bar := &progressBar{c: c, total: total, lastPct: -1} if fn == nil { return nil } err := fn(bar) if c.tty && bar.current >= bar.total { c.Println() } return err } 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.c.tty { p.renderTTY(pct) return } 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) renderTTY(pct int) { width := 40 filled := 0 if p.total > 0 { filled = p.current * width / p.total } if filled > width { filled = width } bar := strings.Repeat("█", filled) + strings.Repeat(" ", width-filled) color := progressColor(pct) p.c.Printf("\r %s %5.1f%% %d/%d", p.c.style(color, bar), float64(pct), p.current, p.total) } func progressColor(pct int) string { switch { case pct >= 100: return "\x1b[38;5;34m" case pct >= 50: return "\x1b[38;5;36m" default: return "\x1b[38;5;37m" } } 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 } cols := len(headers) all := make([][]string, 0, 1+len(rows)) all = append(all, headers) for _, row := range rows { all = append(all, padRow(row, cols)) } widths := make([]int, cols) for _, row := range all { for i, cell := range row { if n := utf8.RuneCountInString(cell); n > widths[i] { widths[i] = n } } } c.Println(boxLine(widths, boxTopLeft, boxTeeDown, boxTopRight)) c.Println(boxRow(headers, widths, true, c)) c.Println(boxLine(widths, boxTeeRight, boxCross, boxTeeLeft)) for _, row := range rows { c.Println(boxRow(padRow(row, cols), widths, false, c)) } c.Println(boxLine(widths, boxBottomLeft, boxTeeUp, boxBottomRight)) } 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 } func boxLine(widths []int, left, mid, right string) string { var b strings.Builder b.WriteString(left) for i, w := range widths { if i > 0 { b.WriteString(mid) } b.WriteString(strings.Repeat(boxHorizontal, w+2)) } b.WriteString(right) return b.String() } func boxRow(cells []string, widths []int, header bool, c *console) string { var b strings.Builder b.WriteString(boxVertical) for i, cell := range cells { pad := widths[i] - utf8.RuneCountInString(cell) text := cell if header { text = c.style("\x1b[1m", cell) } fmt.Fprintf(&b, " %s%s %s", text, strings.Repeat(" ", pad), boxVertical) } return b.String() }