spinner.go

  1package format
  2
  3import (
  4	"context"
  5	"fmt"
  6	"os"
  7
  8	"github.com/charmbracelet/bubbles/v2/spinner"
  9	tea "github.com/charmbracelet/bubbletea/v2"
 10)
 11
 12// Spinner wraps the bubbles spinner for non-interactive mode
 13type Spinner struct {
 14	model  spinner.Model
 15	done   chan struct{}
 16	prog   *tea.Program
 17	ctx    context.Context
 18	cancel context.CancelFunc
 19}
 20
 21// spinnerModel is the tea.Model for the spinner
 22type spinnerModel struct {
 23	spinner  spinner.Model
 24	message  string
 25	quitting bool
 26}
 27
 28func (m spinnerModel) Init() tea.Cmd {
 29	return m.spinner.Tick
 30}
 31
 32func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 33	switch msg := msg.(type) {
 34	case tea.KeyPressMsg:
 35		m.quitting = true
 36		return m, tea.Quit
 37	case spinner.TickMsg:
 38		var cmd tea.Cmd
 39		m.spinner, cmd = m.spinner.Update(msg)
 40		return m, cmd
 41	case quitMsg:
 42		m.quitting = true
 43		return m, tea.Quit
 44	default:
 45		return m, nil
 46	}
 47}
 48
 49func (m spinnerModel) View() string {
 50	if m.quitting {
 51		return ""
 52	}
 53	return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
 54}
 55
 56// quitMsg is sent when we want to quit the spinner
 57type quitMsg struct{}
 58
 59// NewSpinner creates a new spinner with the given message
 60func NewSpinner(message string) *Spinner {
 61	s := spinner.New()
 62	s.Spinner = spinner.Dot
 63	s.Style = s.Style.Foreground(s.Style.GetForeground())
 64
 65	ctx, cancel := context.WithCancel(context.Background())
 66
 67	model := spinnerModel{
 68		spinner: s,
 69		message: message,
 70	}
 71
 72	prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
 73
 74	return &Spinner{
 75		model:  s,
 76		done:   make(chan struct{}),
 77		prog:   prog,
 78		ctx:    ctx,
 79		cancel: cancel,
 80	}
 81}
 82
 83// Start begins the spinner animation
 84func (s *Spinner) Start() {
 85	go func() {
 86		defer close(s.done)
 87		go func() {
 88			<-s.ctx.Done()
 89			s.prog.Send(quitMsg{})
 90		}()
 91		_, err := s.prog.Run()
 92		if err != nil {
 93			fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
 94		}
 95	}()
 96}
 97
 98// Stop ends the spinner animation
 99func (s *Spinner) Stop() {
100	s.cancel()
101	<-s.done
102}