···11-// Package spin is a placeholder for something cool.
11+// Package spin provides a simple, easy to use terminal spinner with configurable styles
22+// to provide progress information in command line applications.
23package spin
3444-// Hello returns a welcome message for the project.
55-func Hello() string {
66- return "Hello spin"
55+import (
66+ "fmt"
77+ "io"
88+ "sync"
99+ "sync/atomic"
1010+ "time"
1111+)
1212+1313+const (
1414+ // frameRate is the rate at which spinner frames are rendered.
1515+ frameRate = 100 * time.Millisecond
1616+1717+ // erase is the ANSI code for erasing the current line and resetting the cursor
1818+ // position back to the start of the line.
1919+ //
2020+ // See https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797#erase-functions.
2121+ erase = "\r\x1b[K"
2222+)
2323+2424+// Spinner contains the spinner state.
2525+type Spinner struct {
2626+ w io.Writer // Where to draw the spinner
2727+ stop chan struct{} // Signal for the spinner to stop
2828+ msg string // Message to display during spinning e.g. "Loading"
2929+ wg sync.WaitGroup // Manages the rendering goroutine
3030+ running atomic.Bool // Whether the spinner is currently running
3131+}
3232+3333+// New returns a new [Spinner].
3434+func New(w io.Writer, msg string) *Spinner {
3535+ return &Spinner{
3636+ w: w,
3737+ stop: make(chan struct{}),
3838+ msg: msg,
3939+ }
4040+}
4141+4242+// Start starts the spinner animation.
4343+func (s *Spinner) Start() {
4444+ if s.running.Load() {
4545+ // If it's already running, we have nothing to do
4646+ return
4747+ }
4848+4949+ s.running.Store(true)
5050+5151+ s.wg.Add(1)
5252+ go func() {
5353+ // Store the frames and the index locally so no need for synchronisation
5454+ frames := [...]rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}
5555+ current := 0
5656+ defer s.wg.Done()
5757+ for {
5858+ select {
5959+ case <-s.stop:
6060+ return
6161+ case <-time.Tick(frameRate):
6262+ fmt.Fprintf(s.w, "%s%c %s...", erase, frames[current], s.msg)
6363+ current = (current + 1) % len(frames)
6464+ }
6565+ }
6666+ }()
6767+}
6868+6969+// Stop halts the spinner animation.
7070+func (s *Spinner) Stop() {
7171+ if !s.running.Load() {
7272+ // If it's not already running, there's nothing to do
7373+ return
7474+ }
7575+7676+ s.stop <- struct{}{} // Signal the stop
7777+ s.wg.Wait() // Wait for the render goroutine to return
7878+ s.running.Store(false)
7979+8080+ fmt.Fprint(s.w, erase) // Erase the line
781}