Skip to content

Commit 8559812

Browse files
committed
refactor spinner
1 parent 8c64762 commit 8559812

6 files changed

Lines changed: 141 additions & 62 deletions

File tree

cmd/audio_inference.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"path/filepath"
99
"time"
1010

11-
"github.com/briandowns/spinner"
1211
"github.com/runware/runware-cli/internal/api"
1312
"github.com/runware/runware-cli/internal/config"
1413
"github.com/runware/runware-cli/internal/output"
@@ -142,30 +141,21 @@ func runAudioInference(cmd *cobra.Command, args []string) error {
142141
}
143142

144143
// Submit
145-
var s *spinner.Spinner
146-
if output.IsTTY() {
147-
s = spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr))
148-
s.Suffix = " Submitting audio generation task..."
149-
s.Start()
150-
}
144+
s := output.NewSpinner(" Submitting audio generation task...")
145+
s.Start()
146+
defer s.Stop()
151147

152148
client := api.NewClient(key, config.GetBaseURL(), flagVerbose)
153149
_, err := client.AudioInference(context.Background(), req)
154150
if err != nil {
155-
if s != nil {
156-
s.Stop()
157-
}
158151
if api.IsAuthError(err) {
159152
output.Error("Authentication failed. Run 'runware auth login' to set your API key.")
160153
return err
161154
}
162155
return err
163156
}
164157

165-
// Poll for completion
166-
if s != nil {
167-
s.Suffix = " Generating audio..."
168-
}
158+
s.Suffix(" Generating audio (this may take a few minutes)...")
169159

170160
taskUUID := req.TaskUUID
171161
deadline := time.Now().Add(time.Duration(timeout) * time.Second)
@@ -199,10 +189,6 @@ func runAudioInference(cmd *cobra.Command, args []string) error {
199189
time.Sleep(interval)
200190
}
201191

202-
if s != nil {
203-
s.Stop()
204-
}
205-
206192
if len(results) == 0 {
207193
output.Error("Audio generation timed out or returned no results")
208194
return fmt.Errorf("no audio results after %ds", timeout)
@@ -228,9 +214,15 @@ func runAudioInference(cmd *cobra.Command, args []string) error {
228214
}
229215
rows = append(rows, row)
230216
}
217+
218+
// Manually stop the spinner to ensure clean output of results.
219+
s.Stop()
220+
231221
return output.Print(format, results, headers, rows)
232222
}
233223

224+
s.Suffix(" Downloading audio results...")
225+
234226
// Download audio files
235227
if err := os.MkdirAll(outputDir, 0755); err != nil {
236228
return fmt.Errorf("failed to create output directory: %w", err)
@@ -262,5 +254,8 @@ func runAudioInference(cmd *cobra.Command, args []string) error {
262254
rows = append(rows, row)
263255
}
264256

257+
// Manually stop the spinner to ensure clean output of results.
258+
s.Stop()
259+
265260
return output.Print(format, results, headers, rows)
266261
}

cmd/image_inference.go

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"strings"
1111
"time"
1212

13-
"github.com/briandowns/spinner"
1413
"github.com/runware/runware-cli/internal/api"
1514
"github.com/runware/runware-cli/internal/config"
1615
"github.com/runware/runware-cli/internal/output"
@@ -244,16 +243,9 @@ func runImageInference(cmd *cobra.Command, args []string) error {
244243
}
245244

246245
// Start spinner
247-
var s *spinner.Spinner
248-
if output.IsTTY() {
249-
s = spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr))
250-
s.Suffix = " Generating image..."
251-
s.Start()
252-
}
253-
254-
if s != nil {
255-
s.Stop()
256-
}
246+
s := output.NewSpinner(" Generating image...")
247+
s.Start()
248+
defer s.Stop()
257249

258250
client := api.NewClient(key, config.GetBaseURL(), flagVerbose)
259251

@@ -284,9 +276,15 @@ func runImageInference(cmd *cobra.Command, args []string) error {
284276
for i, r := range results {
285277
rows = append(rows, []any{i + 1, r.ImageURL, r.Seed})
286278
}
279+
280+
// Manually stop the spinner to ensure clean output of results.
281+
s.Stop()
282+
287283
return output.Print(format, results, headers, rows)
288284
}
289285

286+
s.Suffix(" Downloading image results...")
287+
290288
// Download images
291289
if err := os.MkdirAll(outputDir, 0755); err != nil {
292290
return fmt.Errorf("failed to create output directory: %w", err)
@@ -311,6 +309,9 @@ func runImageInference(cmd *cobra.Command, args []string) error {
311309
rows = append(rows, []any{i + 1, destPath, r.Seed})
312310
}
313311

312+
// Manually stop the spinner to ensure clean output of results.
313+
s.Stop()
314+
314315
return output.Print(format, results, headers, rows)
315316
}
316317

cmd/text_inference.go

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7-
"os"
8-
"time"
97

10-
"github.com/briandowns/spinner"
118
"github.com/runware/runware-cli/internal/api"
129
"github.com/runware/runware-cli/internal/config"
1310
"github.com/runware/runware-cli/internal/output"
@@ -141,20 +138,12 @@ func runTextInference(cmd *cobra.Command, args []string) error {
141138
}
142139

143140
// Submit
144-
var s *spinner.Spinner
145-
if output.IsTTY() {
146-
s = spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr))
147-
s.Suffix = " Generating text..."
148-
s.Start()
149-
}
141+
s := output.NewSpinner(" Generating text...")
142+
s.Start()
143+
defer s.Stop()
150144

151145
client := api.NewClient(key, config.GetBaseURL(), flagVerbose)
152146
results, err := client.TextInference(context.Background(), req)
153-
154-
if s != nil {
155-
s.Stop()
156-
}
157-
158147
if err != nil {
159148
if api.IsAuthError(err) {
160149
output.Error("Authentication failed. Run 'runware auth login' to set your API key.")
@@ -163,6 +152,9 @@ func runTextInference(cmd *cobra.Command, args []string) error {
163152
return err
164153
}
165154

155+
// Manually stop the spinner to ensure clean output of results.
156+
s.Stop()
157+
166158
if len(results) == 0 {
167159
output.Error("No results returned")
168160
return fmt.Errorf("empty response from text inference")

cmd/video_inference.go

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"path/filepath"
99
"time"
1010

11-
"github.com/briandowns/spinner"
1211
"github.com/runware/runware-cli/internal/api"
1312
"github.com/runware/runware-cli/internal/config"
1413
"github.com/runware/runware-cli/internal/output"
@@ -191,37 +190,29 @@ func runVideoInference(cmd *cobra.Command, args []string) error {
191190
}
192191

193192
// Submit the video generation task
194-
var s *spinner.Spinner
195-
if output.IsTTY() {
196-
s = spinner.New(spinner.CharSets[14], 100*time.Millisecond, spinner.WithWriter(os.Stderr))
197-
s.Suffix = " Submitting video generation task..."
198-
s.Start()
199-
}
193+
s := output.NewSpinner(" Submitting video generation task...")
194+
s.Start()
195+
defer s.Stop()
200196

201197
client := api.NewClient(key, config.GetBaseURL(), flagVerbose)
202-
submitResults, err := client.VideoInference(context.Background(), req)
203198

199+
submitResults, err := client.VideoInference(context.Background(), req)
204200
if err != nil {
205-
if s != nil {
206-
s.Stop()
207-
}
208201
if api.IsAuthError(err) {
209202
output.Error("Authentication failed. Run 'runware auth login' to set your API key.")
210203
return err
211204
}
212205
return err
213206
}
214207

208+
s.Suffix(" Generating video (this may take a few minutes)...")
209+
215210
// Poll for completion
216211
taskUUID := req.TaskUUID
217212
if len(submitResults) > 0 && submitResults[0].TaskUUID != "" {
218213
taskUUID = submitResults[0].TaskUUID
219214
}
220215

221-
if s != nil {
222-
s.Suffix = " Generating video (this may take a few minutes)..."
223-
}
224-
225216
deadline := time.Now().Add(time.Duration(timeout) * time.Second)
226217
interval := time.Duration(pollInterval) * time.Second
227218
var results []api.VideoInferenceResult
@@ -256,10 +247,6 @@ func runVideoInference(cmd *cobra.Command, args []string) error {
256247
time.Sleep(interval)
257248
}
258249

259-
if s != nil {
260-
s.Stop()
261-
}
262-
263250
if len(results) == 0 {
264251
output.Error("Video generation timed out or returned no results")
265252
return fmt.Errorf("no video results after %ds", timeout)
@@ -289,9 +276,15 @@ func runVideoInference(cmd *cobra.Command, args []string) error {
289276
if includeCost {
290277
headers = append(headers, tableHeaderCost)
291278
}
279+
280+
// Manually stop the spinner to ensure clean output of results.
281+
s.Stop()
282+
292283
return output.Print(format, results, headers, rows)
293284
}
294285

286+
s.Suffix(" Downloading video results...")
287+
295288
// Download videos
296289
if err := os.MkdirAll(outputDir, 0755); err != nil {
297290
return fmt.Errorf("failed to create output directory: %w", err)
@@ -335,6 +328,9 @@ func runVideoInference(cmd *cobra.Command, args []string) error {
335328
return fmt.Errorf("all %d video downloads failed", len(results))
336329
}
337330

331+
// Manually stop the spinner to ensure clean output of results.
332+
s.Stop()
333+
338334
if err := output.Print(format, results, headers, rows); err != nil {
339335
return err
340336
}

internal/output/spinner.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package output
2+
3+
import (
4+
"os"
5+
"time"
6+
7+
"github.com/briandowns/spinner"
8+
)
9+
10+
// TTYSpinner wraps spinner.Spinner with transparent TTY detection.
11+
// All methods are no-ops when not running in a TTY.
12+
type TTYSpinner struct {
13+
s *spinner.Spinner
14+
}
15+
16+
// NewSpinner creates a TTYSpinner with the given suffix.
17+
// The underlying spinner is only initialised when output is a TTY.
18+
func NewSpinner(suffix string) *TTYSpinner {
19+
return newSpinner(suffix, IsTTY())
20+
}
21+
22+
func newSpinner(suffix string, tty bool) *TTYSpinner {
23+
t := &TTYSpinner{}
24+
if tty {
25+
t.s = spinner.New(
26+
spinner.CharSets[14],
27+
100*time.Millisecond,
28+
spinner.WithWriter(os.Stderr),
29+
spinner.WithSuffix(suffix),
30+
)
31+
}
32+
return t
33+
}
34+
35+
// Start starts the spinner. No-op when not a TTY.
36+
func (t *TTYSpinner) Start() {
37+
if t.s != nil {
38+
t.s.Start()
39+
}
40+
}
41+
42+
// Stop stops the spinner. No-op when not a TTY.
43+
func (t *TTYSpinner) Stop() {
44+
if t.s != nil {
45+
t.s.Stop()
46+
}
47+
}
48+
49+
// Suffix updates the spinner suffix text. No-op when not a TTY.
50+
func (t *TTYSpinner) Suffix(suffix string) {
51+
if t.s != nil {
52+
t.s.Suffix = suffix
53+
}
54+
}

internal/output/spinner_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package output
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestNewSpinner_NonTTY(t *testing.T) {
8+
s := newSpinner(" Generating...", false)
9+
if s.s != nil {
10+
t.Fatal("expected nil inner spinner for non-TTY")
11+
}
12+
}
13+
14+
func TestNewSpinner_TTY(t *testing.T) {
15+
s := newSpinner(" Generating...", true)
16+
if s.s == nil {
17+
t.Fatal("expected non-nil inner spinner for TTY")
18+
}
19+
}
20+
21+
func TestTTYSpinner_NoopWhenNonTTY(t *testing.T) {
22+
s := newSpinner(" Generating...", false)
23+
// Must not panic
24+
s.Start()
25+
s.Stop()
26+
s.Suffix(" Updated suffix")
27+
}
28+
29+
func TestTTYSpinner_Suffix_UpdatesInnerSpinner(t *testing.T) {
30+
s := newSpinner(" Initial", true)
31+
s.Suffix(" Updated")
32+
if s.s.Suffix != " Updated" {
33+
t.Fatalf("expected suffix %q, got %q", " Updated", s.s.Suffix)
34+
}
35+
}
36+
37+
func TestTTYSpinner_Suffix_NoopWhenNonTTY(t *testing.T) {
38+
s := newSpinner(" Initial", false)
39+
// Must not panic
40+
s.Suffix(" Updated")
41+
}

0 commit comments

Comments
 (0)