···55import (
66 "fmt"
77 "io"
88+ "os"
89 "sync"
910 "sync/atomic"
1011 "time"
11121213 "github.com/FollowTheProcess/hue"
1414+ "golang.org/x/term"
1315)
14161517const (
···59616062// Start starts the spinner animation.
6163func (s *Spinner) Start() {
6262- if s.running.Load() {
6363- // If it's already running, we have nothing to do
6464+ if s.running.Load() || !isTerminal(s.w) {
6565+ // If it's already running, or if it's not hooked up to a terminal
6666+ // there's nothing for us to do
6467 return
6568 }
6669···104107 s.Start()
105108 defer s.Stop()
106109 fn()
110110+}
111111+112112+// isTerminal returns whether w points to an [*os.File] and whether
113113+// that is itself a tty.
114114+func isTerminal(w io.Writer) bool {
115115+ file, ok := w.(*os.File)
116116+ if !ok {
117117+ return false
118118+ }
119119+120120+ if file == nil {
121121+ return false
122122+ }
123123+124124+ return term.IsTerminal(int(file.Fd()))
107125}
-109
spin_test.go
···11-package spin_test
22-33-import (
44- "bytes"
55- "strings"
66- "testing"
77- "time"
88-99- "github.com/FollowTheProcess/hue"
1010- "github.com/FollowTheProcess/spin"
1111-)
1212-1313-var frames = [...]string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
1414-1515-func TestSpinner(t *testing.T) {
1616- buf := &bytes.Buffer{}
1717-1818- spinner := spin.New(buf, "Testing", spin.FrameStyle(hue.Yellow), spin.MessageStyle(hue.Blue))
1919- spinner.Start()
2020- time.Sleep(300 * time.Millisecond)
2121- spinner.Stop()
2222-2323- got := buf.String()
2424-2525- found := false
2626- for _, frame := range frames {
2727- if strings.Contains(got, frame) {
2828- found = true
2929- break
3030- }
3131- }
3232-3333- if !found {
3434- t.Fatalf("expected output to contain a spinner frame:\n\n%s\n", got)
3535- }
3636-}
3737-3838-func TestDo(t *testing.T) {
3939- buf := &bytes.Buffer{}
4040-4141- spinner := spin.New(buf, "Testing", spin.FrameStyle(hue.Yellow), spin.MessageStyle(hue.Blue))
4242- spinner.Do(func() {
4343- time.Sleep(300 * time.Millisecond)
4444- })
4545-4646- got := buf.String()
4747-4848- found := false
4949- for _, frame := range frames {
5050- if strings.Contains(got, frame) {
5151- found = true
5252- break
5353- }
5454- }
5555-5656- if !found {
5757- t.Fatalf("expected output to contain a spinner frame:\n\n%s\n", got)
5858- }
5959-}
6060-6161-func TestDoubleStart(t *testing.T) {
6262- buf := &bytes.Buffer{}
6363-6464- spinner := spin.New(buf, "Working")
6565- spinner.Start()
6666- spinner.Start() // This one shouldn't do anything
6767-6868- time.Sleep(300 * time.Millisecond)
6969- spinner.Stop()
7070-7171- got := buf.String()
7272-7373- found := false
7474- for _, frame := range frames {
7575- if strings.Contains(got, frame) {
7676- found = true
7777- break
7878- }
7979- }
8080-8181- if !found {
8282- t.Fatalf("expected output to contain a spinner frame:\n\n%s\n", got)
8383- }
8484-}
8585-8686-func TestDoubleStop(t *testing.T) {
8787- buf := &bytes.Buffer{}
8888-8989- spinner := spin.New(buf, "Working")
9090- spinner.Start()
9191-9292- time.Sleep(300 * time.Millisecond)
9393- spinner.Stop()
9494- spinner.Stop() // This one shouldn't do anything
9595-9696- got := buf.String()
9797-9898- found := false
9999- for _, frame := range frames {
100100- if strings.Contains(got, frame) {
101101- found = true
102102- break
103103- }
104104- }
105105-106106- if !found {
107107- t.Fatalf("expected output to contain a spinner frame:\n\n%s\n", got)
108108- }
109109-}