-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathspinner.go
More file actions
143 lines (118 loc) · 3.33 KB
/
spinner.go
File metadata and controls
143 lines (118 loc) · 3.33 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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package cmd
import (
"fmt"
"io"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/timescale/tiger-cli/internal/tiger/util"
)
// spinnerFrames defines the animation frames for the spinner
var spinnerFrames = []string{"⢎ ", "⠎⠁", "⠊⠑", "⠈⠱", " ⡱", "⢀⡰", "⢄⡠", "⢆⡀"}
type Spinner interface {
// Update changes the spinner's displayed message.
Update(message string, args ...any)
// Stop terminates the spinner program and waits for it to finish.
Stop()
}
// NewSpinner creates and returns a new [Spinner] for displaying animated
// status messages. If output is a terminal, it uses bubbletea to dynamically
// update the spinner and message in place. If output is not a terminal, it
// prints each message on a new line without animation. The message parameter
// supports fmt.Sprintf-style formatting with optional args.
func NewSpinner(output io.Writer, message string, args ...any) Spinner {
if util.IsTerminal(output) {
return newAnimatedSpinner(output, message, args...)
}
return newManualSpinner(output, message, args...)
}
type animatedSpinner struct {
program *tea.Program
}
func newAnimatedSpinner(output io.Writer, message string, args ...any) *animatedSpinner {
program := tea.NewProgram(
spinnerModel{
message: fmt.Sprintf(message, args...),
},
tea.WithOutput(output),
)
go func() {
if _, err := program.Run(); err != nil {
fmt.Fprintf(output, "Error displaying output: %s\n", err)
}
}()
return &animatedSpinner{
program: program,
}
}
// Update changes the spinner's displayed message and triggers bubbletea to re-render.
func (s *animatedSpinner) Update(message string, args ...any) {
s.program.Send(updateMsg(fmt.Sprintf(message, args...)))
}
// Stop quits the [tea.Program] and waits for it to finish.
func (s *animatedSpinner) Stop() {
s.program.Quit()
s.program.Wait()
}
type manualSpinner struct {
output io.Writer
model *spinnerModel
}
func newManualSpinner(output io.Writer, message string, args ...any) *manualSpinner {
s := &manualSpinner{
output: output,
model: &spinnerModel{
message: fmt.Sprintf(message, args...),
},
}
s.printLine()
return s
}
// Update prints the message on a new line if it differs from the previous one.
func (s *manualSpinner) Update(message string, args ...any) {
message = fmt.Sprintf(message, args...)
if message == s.model.message {
return
}
s.model.message = message
s.model.incFrame()
s.printLine()
}
// Stop is a no-op for a manual spinner.
func (s *manualSpinner) Stop() {}
func (s *manualSpinner) printLine() {
fmt.Fprintln(s.output, s.model.View())
}
// Message types for the [tea.Model].
type (
tickMsg struct{}
updateMsg string
)
// spinnerModel is the [tea.Model] for the spinner.
type spinnerModel struct {
message string
frame int
}
func (m spinnerModel) Init() tea.Cmd {
return m.tick()
}
func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tickMsg:
m.incFrame()
return m, m.tick()
case updateMsg:
m.message = string(msg)
}
return m, nil
}
func (m spinnerModel) View() string {
return fmt.Sprintf("%s %s", spinnerFrames[m.frame], m.message)
}
func (m *spinnerModel) incFrame() {
m.frame = (m.frame + 1) % len(spinnerFrames)
}
func (m *spinnerModel) tick() tea.Cmd {
return tea.Tick(100*time.Millisecond, func(time.Time) tea.Msg {
return tickMsg{}
})
}