summaryrefslogtreecommitdiffhomepage
path: root/pkg/tui/components/spinner
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/tui/components/spinner')
-rw-r--r--pkg/tui/components/spinner/spinner.go127
-rw-r--r--pkg/tui/components/spinner/spinner_test.go24
2 files changed, 151 insertions, 0 deletions
diff --git a/pkg/tui/components/spinner/spinner.go b/pkg/tui/components/spinner/spinner.go
new file mode 100644
index 000000000..5e1af8771
--- /dev/null
+++ b/pkg/tui/components/spinner/spinner.go
@@ -0,0 +1,127 @@
+package spinner
+
+import (
+ "context"
+ "fmt"
+ "os"
+
+ "github.com/charmbracelet/bubbles/spinner"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/lipgloss"
+)
+
+// Spinner wraps the bubbles spinner for both interactive and non-interactive mode
+type Spinner struct {
+ model spinner.Model
+ done chan struct{}
+ prog *tea.Program
+ ctx context.Context
+ cancel context.CancelFunc
+}
+
+// spinnerModel is the tea.Model for the spinner
+type spinnerModel struct {
+ spinner spinner.Model
+ message string
+ quitting bool
+}
+
+func (m spinnerModel) Init() tea.Cmd {
+ return m.spinner.Tick
+}
+
+func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyMsg:
+ m.quitting = true
+ return m, tea.Quit
+ case spinner.TickMsg:
+ var cmd tea.Cmd
+ m.spinner, cmd = m.spinner.Update(msg)
+ return m, cmd
+ case quitMsg:
+ m.quitting = true
+ return m, tea.Quit
+ default:
+ return m, nil
+ }
+}
+
+func (m spinnerModel) View() string {
+ if m.quitting {
+ return ""
+ }
+ return fmt.Sprintf("%s %s", m.spinner.View(), m.message)
+}
+
+// quitMsg is sent when we want to quit the spinner
+type quitMsg struct{}
+
+// NewSpinner creates a new spinner with the given message
+func NewSpinner(message string) *Spinner {
+ s := spinner.New()
+ s.Spinner = spinner.Dot
+ s.Style = s.Style.Foreground(s.Style.GetForeground())
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ model := spinnerModel{
+ spinner: s,
+ message: message,
+ }
+
+ prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
+
+ return &Spinner{
+ model: s,
+ done: make(chan struct{}),
+ prog: prog,
+ ctx: ctx,
+ cancel: cancel,
+ }
+}
+
+// NewThemedSpinner creates a new spinner with the given message and color
+func NewThemedSpinner(message string, color lipgloss.AdaptiveColor) *Spinner {
+ s := spinner.New()
+ s.Spinner = spinner.Dot
+ s.Style = s.Style.Foreground(color)
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ model := spinnerModel{
+ spinner: s,
+ message: message,
+ }
+
+ prog := tea.NewProgram(model, tea.WithOutput(os.Stderr), tea.WithoutCatchPanics())
+
+ return &Spinner{
+ model: s,
+ done: make(chan struct{}),
+ prog: prog,
+ ctx: ctx,
+ cancel: cancel,
+ }
+}
+
+// Start begins the spinner animation
+func (s *Spinner) Start() {
+ go func() {
+ defer close(s.done)
+ go func() {
+ <-s.ctx.Done()
+ s.prog.Send(quitMsg{})
+ }()
+ _, err := s.prog.Run()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error running spinner: %v\n", err)
+ }
+ }()
+}
+
+// Stop ends the spinner animation
+func (s *Spinner) Stop() {
+ s.cancel()
+ <-s.done
+} \ No newline at end of file
diff --git a/pkg/tui/components/spinner/spinner_test.go b/pkg/tui/components/spinner/spinner_test.go
new file mode 100644
index 000000000..065726e91
--- /dev/null
+++ b/pkg/tui/components/spinner/spinner_test.go
@@ -0,0 +1,24 @@
+package spinner
+
+import (
+ "testing"
+ "time"
+)
+
+func TestSpinner(t *testing.T) {
+ t.Parallel()
+
+ // Create a spinner
+ s := NewSpinner("Test spinner")
+
+ // Start the spinner
+ s.Start()
+
+ // Wait a bit to let it run
+ time.Sleep(100 * time.Millisecond)
+
+ // Stop the spinner
+ s.Stop()
+
+ // If we got here without panicking, the test passes
+} \ No newline at end of file