This repository was archived by the owner on Mar 3, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathspin.go
More file actions
129 lines (114 loc) · 2.46 KB
/
spin.go
File metadata and controls
129 lines (114 loc) · 2.46 KB
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
package main
import (
"fmt"
"io"
"os"
"strings"
"sync"
"time"
)
// isTTY reports whether f is connected to a terminal.
// When false, the spinner is suppressed so piped output stays clean.
func isTTY(f *os.File) bool {
fi, err := f.Stat()
return err == nil && fi.Mode()&os.ModeCharDevice != 0
}
var spinFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
// Spinner renders a braille animation with a message to an io.Writer.
// All methods are safe to call concurrently. When isTTY is false every
// method is a no-op so piped output stays clean.
type Spinner struct {
out io.Writer
isTTY bool
mu sync.Mutex
msg string
live bool
done chan struct{}
}
func NewSpinner(out io.Writer, isTTY bool) *Spinner {
return &Spinner{out: out, isTTY: isTTY}
}
// Start begins the animation with msg.
func (s *Spinner) Start(msg string) {
if !s.isTTY {
return
}
s.mu.Lock()
s.msg = msg
s.live = true
s.done = make(chan struct{})
s.mu.Unlock()
go s.run()
}
func (s *Spinner) run() {
tick := time.NewTicker(80 * time.Millisecond)
defer tick.Stop()
for i := 0; ; i++ {
select {
case <-s.done:
return
case <-tick.C:
s.mu.Lock()
if s.live {
fmt.Fprintf(s.out, "\r\033[K%s %s", spinFrames[i%len(spinFrames)], s.msg)
}
s.mu.Unlock()
}
}
}
// Pause stops the animation and clears the spinner line so other output
// can be written without interleaving. Call Resume to restart.
func (s *Spinner) Pause() {
if !s.isTTY {
return
}
s.mu.Lock()
s.live = false
s.mu.Unlock()
s.clear()
}
// Resume restarts the animation with a new message.
func (s *Spinner) Resume(msg string) {
if !s.isTTY {
return
}
s.mu.Lock()
s.msg = msg
s.live = true
s.mu.Unlock()
}
// Stop halts the animation and clears the spinner line.
func (s *Spinner) Stop() {
if !s.isTTY {
return
}
close(s.done)
s.mu.Lock()
s.live = false
s.mu.Unlock()
s.clear()
}
// Update changes the spinner message without stopping the animation.
func (s *Spinner) Update(msg string) {
if !s.isTTY {
return
}
s.mu.Lock()
s.msg = msg
s.mu.Unlock()
}
func (s *Spinner) clear() {
fmt.Fprintf(s.out, "\r\033[K")
}
// spinnerWriter implements io.Writer by updating the spinner message.
// Used to bridge provisioning progress output into the spinner display.
type spinnerWriter struct {
spin *Spinner
}
func (w *spinnerWriter) Write(p []byte) (int, error) {
msg := strings.TrimSpace(string(p))
if msg != "" {
w.spin.Update(msg)
}
return len(p), nil
}