-
Notifications
You must be signed in to change notification settings - Fork 0
/
spinner.go
218 lines (174 loc) · 5.1 KB
/
spinner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
package termite
import (
"container/ring"
"context"
"errors"
"fmt"
"io"
"strings"
"sync"
"time"
)
// DefaultSpinnerCharSeq returns the default character sequence of a spinner.
func DefaultSpinnerCharSeq() []string {
return []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
}
// DefaultSpinnerFormatter returns a default
func DefaultSpinnerFormatter() SpinnerFormatter {
return &SimpleSpinnerFormatter{}
}
// SpinnerFormatter a formatter to be used with a Spinner to customize its style.
type SpinnerFormatter interface {
// FormatTitle returns the input string with optional styling codes or anything else.
FormatTitle(s string) string
// FormatIndicator returns a string that contains one visible character - the one passed as input -
// and optionally additional styling charatcers such as color codes, background and other effects.
FormatIndicator(char string) string
// CharSeq the character sequence to use as indicators.
CharSeq() []string
}
// SimpleSpinnerFormatter a simple spinner formatter implementation that uses the default
// spinner character sequence and passes the title and the indicator setrings unchanged.
type SimpleSpinnerFormatter struct{}
// FormatTitle returns the input title as is
func (f *SimpleSpinnerFormatter) FormatTitle(s string) string {
return s
}
// FormatIndicator returns the input char as is
func (f *SimpleSpinnerFormatter) FormatIndicator(char string) string {
return char
}
// CharSeq returns the default character sequence.
func (f *SimpleSpinnerFormatter) CharSeq() []string {
return DefaultSpinnerCharSeq()
}
// Spinner a spinning progress indicator
type Spinner interface {
Start() (context.CancelFunc, error)
Stop(string) error
SetTitle(title string) error
}
type spinner struct {
writer io.Writer
interval time.Duration
stateMx *sync.RWMutex
active bool
stopC chan bool
titleC chan string
title string
formatter SpinnerFormatter
}
// NewSpinner creates a new Spinner with the specified update interval
func NewSpinner(writer io.Writer, title string, interval time.Duration, formatter SpinnerFormatter) Spinner {
return &spinner{
writer: writer,
interval: interval,
stateMx: &sync.RWMutex{},
active: false,
stopC: make(chan bool),
titleC: make(chan string),
title: title,
formatter: formatter,
}
}
// NewDefaultSpinner creates a new Spinner that writes to Stdout with a default update interval
func NewDefaultSpinner() Spinner {
return NewSpinner(StdoutWriter, "", time.Millisecond*100, DefaultSpinnerFormatter())
}
func (s *spinner) writeString(str string) (n int, err error) {
return io.WriteString(s.writer, str)
}
// Start starts the spinner in the background and returns a cancellation handle and an error in case the spinner is already running.
func (s *spinner) Start() (cancel context.CancelFunc, err error) {
s.stateMx.Lock()
defer s.stateMx.Unlock()
if s.active {
return nil, errors.New("spinner already active")
}
s.active = true
context, cancel := context.WithCancel(context.Background())
waitStart := &sync.WaitGroup{}
waitStart.Add(1)
go func() {
var spinring = s.createSpinnerRing()
timer := time.NewTicker(s.interval)
waitStart.Done()
defer s.setActiveSafe(false)
update := func(title string) {
indicatorValue := s.formatter.FormatIndicator(fmt.Sprintf("%v", spinring.Value))
if title != "" {
_, _ = s.writeString(fmt.Sprintf("%s%s %s", TermControlEraseLine, indicatorValue, s.formatter.FormatTitle(title)))
} else {
_, _ = s.writeString(fmt.Sprintf("%s%s", TermControlEraseLine, indicatorValue))
}
}
for {
select {
case <-context.Done():
timer.Stop()
close(s.titleC)
s.printExitMessage("Cancelled...")
return
case <-s.stopC:
timer.Stop()
close(s.titleC)
return
case title := <-s.titleC:
// The title is only written by this routine, so we're safe.
s.title = title
update(title)
case <-timer.C:
spinring = spinring.Next()
title := s.title
update(title)
}
}
}()
waitStart.Wait()
return cancel, err
}
// Stop stops the spinner and displays the specified message
func (s *spinner) Stop(message string) (err error) {
s.stateMx.Lock()
defer s.stateMx.Unlock()
if !s.active {
err = errors.New("spinner not active")
} else {
s.stopC <- true
s.active = false
s.printExitMessage(message)
}
return err
}
// SetTitle updates the spinner text.
func (s *spinner) SetTitle(title string) (err error) {
defer func() {
if recover() != nil {
err = errors.New("spinner not active")
}
}()
s.titleC <- strings.TrimSpace(title)
return err
}
func (s *spinner) printExitMessage(message string) {
_, _ = s.writeString(TermControlEraseLine)
_, _ = s.writeString(message)
}
func (s *spinner) createSpinnerRing() *ring.Ring {
r := ring.New(len(s.formatter.CharSeq()))
for _, ch := range s.formatter.CharSeq() {
r.Value = ch
r = r.Next()
}
return r
}
func (s *spinner) isActiveSafe() bool {
s.stateMx.RLock()
defer s.stateMx.RUnlock()
return s.active
}
func (s *spinner) setActiveSafe(active bool) {
s.stateMx.Lock()
defer s.stateMx.Unlock()
s.active = active
}