spinner.go 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. package format
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "os"
  7. tea "github.com/charmbracelet/bubbletea/v2"
  8. "github.com/charmbracelet/crush/internal/tui/components/anim"
  9. "github.com/charmbracelet/crush/internal/tui/styles"
  10. "github.com/charmbracelet/x/ansi"
  11. )
  12. // Spinner wraps the bubbles spinner for non-interactive mode
  13. type Spinner struct {
  14. done chan struct{}
  15. prog *tea.Program
  16. }
  17. type model struct {
  18. cancel context.CancelFunc
  19. anim *anim.Anim
  20. }
  21. func (m model) Init() tea.Cmd { return m.anim.Init() }
  22. func (m model) View() string { return m.anim.View() }
  23. // Update implements tea.Model.
  24. func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  25. switch msg := msg.(type) {
  26. case tea.KeyPressMsg:
  27. switch msg.String() {
  28. case "ctrl+c", "esc":
  29. m.cancel()
  30. return m, tea.Quit
  31. }
  32. }
  33. mm, cmd := m.anim.Update(msg)
  34. m.anim = mm.(*anim.Anim)
  35. return m, cmd
  36. }
  37. // NewSpinner creates a new spinner with the given message
  38. func NewSpinner(ctx context.Context, cancel context.CancelFunc, message string) *Spinner {
  39. t := styles.CurrentTheme()
  40. model := model{
  41. anim: anim.New(anim.Settings{
  42. Size: 10,
  43. Label: message,
  44. LabelColor: t.FgBase,
  45. GradColorA: t.Primary,
  46. GradColorB: t.Secondary,
  47. CycleColors: true,
  48. }),
  49. cancel: cancel,
  50. }
  51. prog := tea.NewProgram(
  52. model,
  53. tea.WithOutput(os.Stderr),
  54. tea.WithContext(ctx),
  55. )
  56. return &Spinner{
  57. prog: prog,
  58. done: make(chan struct{}, 1),
  59. }
  60. }
  61. // Start begins the spinner animation
  62. func (s *Spinner) Start() {
  63. go func() {
  64. defer close(s.done)
  65. _, err := s.prog.Run()
  66. // ensures line is cleared
  67. fmt.Fprint(os.Stderr, ansi.EraseEntireLine)
  68. if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, tea.ErrInterrupted) {
  69. fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
  70. }
  71. }()
  72. }
  73. // Stop ends the spinner animation
  74. func (s *Spinner) Stop() {
  75. s.prog.Quit()
  76. <-s.done
  77. }