@@ -0,0 +1,54 @@
+package main
+
+import (
+ "fmt"
+ "os"
+
+ tea "charm.land/bubbletea/v2"
+
+ "github.com/charmbracelet/crush/internal/ui/spinner"
+)
+
+// Model is the Bubble Tea model for the example program.
+type Model struct {
+ spinner spinner.Spinner
+ quitting bool
+}
+
+// Init initializes the model. It satisfies tea.Model.
+func (m Model) Init() tea.Cmd {
+ return m.spinner.Step()
+}
+
+// Update updates the model per on incoming messages. It satisfies tea.Model.
+func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ switch msg := msg.(type) {
+ case tea.KeyPressMsg:
+ switch msg.String() {
+ case "q", "ctrl+c":
+ m.quitting = true
+ return m, tea.Quit
+ }
+ }
+
+ var cmd tea.Cmd
+ m.spinner, cmd = m.spinner.Update(msg)
+ return m, cmd
+}
+
+// View renders the model to a string. It satisfies tea.Model.
+func (m Model) View() tea.View {
+ if m.quitting {
+ return tea.NewView("")
+ }
+ return tea.NewView(m.spinner.View())
+}
+
+func main() {
+ if _, err := tea.NewProgram(Model{
+ spinner: spinner.NewSpinner(),
+ }).Run(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error running program: %v\n", err)
+ os.Exit(1)
+ }
+}
@@ -0,0 +1,95 @@
+// Package spinner implements a spinner used to indicate processing is occurring.
+package spinner
+
+import (
+ "strings"
+ "sync/atomic"
+ "time"
+
+ tea "charm.land/bubbletea/v2"
+)
+
+const (
+ fps = 24
+ emptyChar = '░'
+)
+
+var blocks = []rune{
+ '▁',
+ '▂',
+ '▃',
+ '▄',
+ '▅',
+ '▆',
+ '▇',
+ '█',
+}
+
+// Internal ID management. Used during animating to ensure that frame messages
+// are received only by spinner components that sent them.
+var lastID int64
+
+func nextID() int {
+ return int(atomic.AddInt64(&lastID, 1))
+}
+
+type StepMsg struct{ ID int }
+
+type Config struct {
+ Width int
+}
+
+func DefaultConfig() Config {
+ return Config{Width: 12}
+}
+
+type Spinner struct {
+ Config Config
+ id int
+ index int
+}
+
+func NewSpinner() Spinner {
+ return Spinner{
+ id: nextID(),
+ Config: DefaultConfig(),
+ }
+}
+
+func (s Spinner) Init() tea.Cmd {
+ return nil
+}
+
+func (s Spinner) Update(msg tea.Msg) (Spinner, tea.Cmd) {
+ if _, ok := msg.(StepMsg); ok {
+ if msg.(StepMsg).ID != s.id {
+ // Reject events from other spinners.
+ return s, nil
+ }
+ s.index++
+ if s.index > s.Config.Width-1 {
+ s.index = 0
+ }
+ return s, s.Step()
+ }
+ return s, nil
+}
+
+func (s Spinner) Step() tea.Cmd {
+ return tea.Tick(time.Second/time.Duration(fps), func(t time.Time) tea.Msg {
+ return StepMsg{ID: s.id}
+ })
+}
+
+func (s Spinner) View() string {
+ var b strings.Builder
+ for i := range s.Config.Width {
+ if i == s.index {
+ b.WriteRune(blocks[len(blocks)-1])
+ continue
+ }
+ b.WriteRune(emptyChar)
+ }
+
+ return b.String()
+}