package spin import ( "context" "errors" "fmt" "github.com/charmbracelet/bubbles/spinner" tea "github.com/charmbracelet/bubbletea" ) type model struct { spinner spinner.Model text string quitting bool err error } type errMsg error type textMessage string func newModel(text string) model { s := spinner.New() s.Spinner = spinner.Dot return model{spinner: s, text: text} } func (m model) Init() tea.Cmd { return m.spinner.Tick } func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case errMsg: m.err = msg return m, nil case textMessage: m.text = string(msg) return m, nil default: var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } } func (m model) View() string { if m.err != nil { return m.err.Error() } str := fmt.Sprintf("%s %s\n", m.spinner.View(), m.text) if m.quitting { return str + "\n" } return str } func Spin(ctx context.Context, message string) (chan string, chan error) { p := tea.NewProgram(newModel(message), tea.WithContext(ctx)) msgChan := make(chan string, 10) errChan := make(chan error) go func() { for { m := <-msgChan p.Send(textMessage(m)) } }() go func() { _, err := p.Run() if errors.Is(err, context.Canceled) { err = nil } else if errors.Is(err, context.DeadlineExceeded) { err = nil } else if errors.Is(err, tea.ErrProgramKilled) { err = nil } errChan <- err }() return msgChan, errChan }