1package spinner
2
3import (
4	"fmt"
5	"os"
6	"time"
7)
8
9var symbolList = []string{
10	" | ",
11	" / ",
12	" – ",
13	" \\ ",
14}
15
16var symbolMap = map[string]string{
17	" ⏳ ": "\033[37m",
18	" ⌛ ": "\033[38m",
19}
20
21type Spinner struct {
22	duration time.Duration
23	trigger  interface{}
24	sentinel interface{}
25	closed   bool
26	// sigChan: is the pipe that receives the start and stop
27	sigChan chan interface{}
28	// waitChan waits till the shutdown has fully propagated
29	waitChan chan interface{}
30}
31
32func New(freq int64) *Spinner {
33	if freq < 1 {
34		freq = 10
35	}
36	sp := Spinner{
37		duration: time.Duration(1e9 / freq),
38		sentinel: nil,
39		// sigChan will be created on .Start()
40		sigChan:  nil,
41		waitChan: make(chan interface{}),
42	}
43
44	sp.trigger = &sp
45	return &sp
46}
47
48func (s *Spinner) Start() error {
49	err := s.spin()
50	if err == nil {
51		s.sigChan <- s.trigger
52	}
53	return err
54}
55
56func (s *Spinner) Stop() {
57	if !s.closed && s.sigChan != nil {
58		s.sigChan <- s.sentinel
59		close(s.sigChan)
60		<-s.waitChan
61		s.closed = true
62	}
63}
64
65func (s *Spinner) Reset() {
66	s.Stop()
67	s.sigChan = nil
68	s.closed = true
69}
70
71func (s *Spinner) Duration() time.Duration {
72	return s.duration
73}
74
75func (s *Spinner) spin() error {
76	if s.sigChan != nil { // Already in use
77		return fmt.Errorf("already in use")
78	}
79	s.sigChan = make(chan interface{})
80	go func() {
81		// Block till the first symbol comes through
82		<-s.sigChan
83
84		throttle := time.Tick(s.duration)
85		running := true
86		for running {
87			for _, segment := range symbolList {
88				select {
89				case in := <-s.sigChan:
90					if in == s.sentinel {
91						os.Stderr.Sync()
92						running = false
93						s.waitChan <- s.sentinel
94						break
95					}
96				default:
97				}
98				// Print it to stderr to avoid symbol getting into piped content
99				fmt.Fprintf(os.Stderr, "%s\r", segment)
100				<-throttle
101			}
102		}
103	}()
104	return nil
105}
106