Skip to content

Instantly share code, notes, and snippets.

@flowchartsman
Last active June 15, 2023 01:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save flowchartsman/f109e15708a9736e5d88f501656398c6 to your computer and use it in GitHub Desktop.
Save flowchartsman/f109e15708a9736e5d88f501656398c6 to your computer and use it in GitHub Desktop.
bubbletea multiprogress widget supporting new widgets, failure, reset, all controlled externally
package main
import (
"fmt"
"math/rand"
"os"
"strconv"
"time"
tea "github.com/charmbracelet/bubbletea"
)
var p *tea.Program
// track the add so we don't loop (testing only)
var didAdd bool
func simulateTask(p *tea.Program, name string, steps int, simulate string) {
for i := 0; i < steps; i++ {
// pretend to do some work
time.Sleep(time.Duration(time.Duration(rand.Intn(800)+200) * time.Millisecond))
switch simulate {
case "fail":
// simulate failure on the next to last step
if i == steps-1 {
p.Send(taskErrMsg{
taskName: name,
err: fmt.Errorf("shit, I failed"),
})
}
case "reset":
// simulate reset halfway through
if i == steps/2 {
p.Send(taskMsg{
taskName: name,
action: reset,
})
// restart, and don't fail this time ;)
go simulateTask(p, name, steps, "")
return
}
case "cancel":
// simulate cancellation 1/4 of the way through
if i == steps/4 {
p.Send(taskMsg{
taskName: name,
action: cancel,
})
return
}
case "addstep":
// simulate adding additional steps on the next to last step
if i == steps-1 && !didAdd {
didAdd = true
p.Send(taskMsg{
taskName: name,
action: addstep,
})
steps++
}
}
p.Send(taskMsg{
taskName: name,
action: tick,
})
}
}
func main() {
rand.Seed(time.Now().UnixNano())
m := newModel()
// Start Bubble Tea
p = tea.NewProgram(m)
wait := make(chan struct{})
go func() {
if _, err := p.Run(); err != nil {
fmt.Println("error running program:", err)
os.Exit(1)
}
close(wait)
}()
for i := 1; i < 6; i++ {
taskName := "task " + strconv.Itoa(i)
// can also test randomization
// steps := rand.Intn(50) + 10
p.Send(taskNewMsg{
taskName: taskName,
steps: 30,
})
simulateAction := ""
// task 2 will error
switch i {
case 2:
// task 2 will fail
simulateAction = "fail"
case 3:
// task 3 will reset
simulateAction = "reset"
case 4:
// task 4 will cancel
simulateAction = "cancel"
case 5:
// task 5 will add an extra step at the end
simulateAction = "addstep"
}
go simulateTask(p, taskName, 30, simulateAction)
// stagger start the timers
time.Sleep(2 * time.Second)
}
<-wait
}
package main
import (
"fmt"
"strings"
"time"
"github.com/charmbracelet/bubbles/progress"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
var (
helpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#626262")).Render
errStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#dd0022")).Render
canceledStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#999")).Render
)
const (
padding = 2
maxWidth = 80
)
type taskAction int
const (
tick taskAction = iota
reset
addstep
cancel
)
type resizeMessage struct{}
type taskMsg struct {
taskName string
action taskAction
}
type taskNewMsg struct {
taskName string
steps int
}
type taskErrMsg struct {
taskName string
err error
}
func finalPause() tea.Cmd {
return tea.Tick(time.Millisecond*750, func(_ time.Time) tea.Msg {
return nil
})
}
func resize() tea.Cmd {
return func() tea.Msg {
return resizeMessage{}
}
}
type task struct {
name string
steps int
done int
bar progress.Model
err error
cancelled bool
}
type model struct {
complete int
taskNames map[string]int
tasks []task
width int
}
func newModel() model {
return model{
taskNames: map[string]int{},
tasks: []task{},
}
}
func (m model) Init() tea.Cmd {
return nil
}
func (m model) resize() {
for i := range m.tasks {
m.tasks[i].bar.Width = m.width - padding*2 - 4
if m.tasks[i].bar.Width > maxWidth {
m.tasks[i].bar.Width = maxWidth
}
}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m, tea.Quit
case tea.WindowSizeMsg:
m.width = msg.Width
m.resize()
return m, nil
case resizeMessage:
m.resize()
return m, nil
case taskNewMsg:
if _, found := m.taskNames[msg.taskName]; found {
return m, nil
}
m.tasks = append(m.tasks, task{
name: msg.taskName,
steps: msg.steps,
bar: progress.New(progress.WithDefaultGradient()),
})
m.taskNames[msg.taskName] = len(m.tasks) - 1
return m, resize()
case taskErrMsg:
if idx, found := m.taskNames[msg.taskName]; found {
m.tasks[idx].err = msg.err
}
m.complete++
// m.err = msg.err
// return m, tea.Quit
return m, nil
case taskMsg:
var cmds []tea.Cmd
idx, found := m.taskNames[msg.taskName]
if !found {
return m, nil
}
switch msg.action {
case tick:
if m.tasks[idx].bar.Percent() >= 1 {
// done already.
return m, nil
}
m.tasks[idx].done++
cmds = append(cmds, m.tasks[idx].bar.SetPercent(float64(m.tasks[idx].done)/float64(m.tasks[idx].steps)))
if m.tasks[idx].bar.Percent() >= 1 {
m.complete++
}
if m.complete >= len(m.tasks) {
cmds = append(cmds, tea.Sequence(finalPause(), tea.Quit))
}
case reset:
if m.tasks[idx].bar.Percent() >= 1 {
m.complete-- // remove it from the completion count
}
m.tasks[idx].done = 0
return m, m.tasks[idx].bar.SetPercent(0)
case cancel:
m.tasks[idx].cancelled = true
// you CAN delete it, but then you need to fix the other indices, so probably not fun
// m.tasks = slices.Delete(m.tasks, idx, idx+1)
// delete(m.taskNames, msg.taskName)
case addstep:
if m.tasks[idx].bar.Percent() >= 1 {
m.complete-- // remove it from the completion count
}
m.tasks[idx].steps++
cmds = append(cmds, m.tasks[idx].bar.SetPercent(float64(m.tasks[idx].done)/float64(m.tasks[idx].steps)))
}
return m, tea.Batch(cmds...)
case progress.FrameMsg:
for i := range m.tasks {
progressModel, cmd := m.tasks[i].bar.Update(msg)
if cmd != nil {
m.tasks[i].bar = progressModel.(progress.Model)
return m, cmd
}
}
return m, nil
default:
return m, nil
}
}
func (m model) View() string {
var sb strings.Builder
pad := strings.Repeat(" ", padding)
for _, t := range m.tasks {
sb.WriteString(pad + t.name)
switch {
case t.cancelled:
sb.WriteString("-" + canceledStyle("<canceled>") + "\n")
case t.err != nil:
sb.WriteString("-" + errStyle(t.err.Error()) + "\n")
default:
sb.WriteString(fmt.Sprintf("(%02d/%02d) ", t.done, t.steps))
sb.WriteString(t.bar.View() + "\n")
}
}
return "\n" +
sb.String() + "\n" +
pad + helpStyle("Press any key to quit")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment