@@ -14,36 +14,43 @@ import (
1414var spinnerFrames = []string {"⢎ " , "⠎⠁" , "⠊⠑" , "⠈⠱" , " ⡱" , "⢀⡰" , "⢄⡠" , "⢆⡀" }
1515
1616type Spinner struct {
17- // Populated when output is a TTY
17+ // Populated when output is a terminal
1818 program * tea.Program
1919
20- // Populated when output is not a TTY
20+ // Populated when output is not a terminal
2121 output io.Writer
2222 model * spinnerModel
2323}
2424
25+ // NewSpinner creates and returns a new [Spinner] for displaying animated
26+ // status messages. If output is a terminal, it uses bubbletea to dynamically
27+ // update the spinner in place. If output is not a terminal, it prints each
28+ // message on a new line without animation. The message parameter supports
29+ // fmt.Sprintf-style formatting with optional args.
2530func NewSpinner (output io.Writer , message string , args ... any ) * Spinner {
26- model := spinnerModel {
27- message : fmt . Sprintf ( message , args ... ),
31+ if isTerminal ( output ) {
32+ return newAnimatedSpinner ( output , message , args ... )
2833 }
34+ return newManualSpinner (output , message , args ... )
35+ }
2936
30- // If output is not a TTY, print each message on a new line
31- if ! isTerminal (output ) {
32- s := & Spinner {
33- output : output ,
34- model : & model ,
35- }
36- s .println ()
37- return s
37+ // isTerminal is a helper method for detecting whether an [io.Writer] is a
38+ // terminal.
39+ func isTerminal (w io.Writer ) bool {
40+ if f , ok := w .(* os.File ); ok {
41+ return term .IsTerminal (int (f .Fd ()))
3842 }
43+ return false
44+ }
3945
40- // If output is a TTY, use bubbletea to dynamically update the message
46+ func newAnimatedSpinner ( output io. Writer , message string , args ... any ) * Spinner {
4147 program := tea .NewProgram (
42- model ,
48+ spinnerModel {
49+ message : fmt .Sprintf (message , args ... ),
50+ },
4351 tea .WithOutput (output ),
4452 )
4553
46- // Start the program in a goroutine
4754 go func () {
4855 if _ , err := program .Run (); err != nil {
4956 fmt .Fprintf (output , "Error displaying output: %s\n " , err )
@@ -55,6 +62,20 @@ func NewSpinner(output io.Writer, message string, args ...any) *Spinner {
5562 }
5663}
5764
65+ func newManualSpinner (output io.Writer , message string , args ... any ) * Spinner {
66+ s := & Spinner {
67+ output : output ,
68+ model : & spinnerModel {
69+ message : fmt .Sprintf (message , args ... ),
70+ },
71+ }
72+ s .println ()
73+ return s
74+ }
75+
76+ // Update changes the spinner's displayed message. If the output is a terminal,
77+ // the message is updated in place via bubbletea. Otherwise, the message is
78+ // printed on a new line if it differs from the previous one.
5879func (s * Spinner ) Update (message string , args ... any ) {
5980 message = fmt .Sprintf (message , args ... )
6081 if s .program != nil {
@@ -66,6 +87,8 @@ func (s *Spinner) Update(message string, args ...any) {
6687 }
6788}
6889
90+ // Stop terminates the spinner program and waits for it to finish.
91+ // This method is a no-op if the output is not a terminal.
6992func (s * Spinner ) Stop () {
7093 if s .program == nil {
7194 return
@@ -75,22 +98,20 @@ func (s *Spinner) Stop() {
7598 s .program .Wait ()
7699}
77100
101+ // println prints the current state of the model to the configured output on a
102+ // new line. It is used when the output is not a terminal, and we therefore
103+ // don't want to write terminal control characters.
78104func (s * Spinner ) println () {
79105 fmt .Fprintln (s .output , s .model .View ())
80106}
81107
82- func isTerminal (w io.Writer ) bool {
83- if f , ok := w .(* os.File ); ok {
84- return term .IsTerminal (int (f .Fd ()))
85- }
86- return false
87- }
88-
89- // Message types for the bubbletea model
90- type tickMsg struct {}
91- type updateMsg string
108+ // Message types for the [tea.Model].
109+ type (
110+ tickMsg struct {}
111+ updateMsg string
112+ )
92113
93- // spinnerModel is the bubbletea model for the spinner
114+ // spinnerModel is the [tea.Model] for the spinner.
94115type spinnerModel struct {
95116 message string
96117 frame int
0 commit comments