Last active
June 15, 2023 01:21
bubbletea multiprogress widget supporting new widgets, failure, reset, all controlled externally
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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