wip(spinner): stub out new spinner

Christian Rocha created

Change summary

internal/ui/spinner/example/main.go | 54 +++++++++++++++++
internal/ui/spinner/spinner.go      | 95 +++++++++++++++++++++++++++++++
2 files changed, 149 insertions(+)

Detailed changes

internal/ui/spinner/example/main.go 🔗

@@ -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)
+	}
+}

internal/ui/spinner/spinner.go 🔗

@@ -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()
+}