spinner.go 2.5 KB

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