diff --git a/spinner.go b/spinner.go index 8c06408..84186ec 100644 --- a/spinner.go +++ b/spinner.go @@ -21,10 +21,12 @@ import ( "io" "math" "os" + "os/signal" "runtime" "strconv" "strings" "sync" + "syscall" "time" "unicode/utf8" @@ -189,7 +191,7 @@ type Spinner struct { WriterFile *os.File // writer as file to allow terminal check active bool // active holds the state of the spinner enabled bool // indicates whether the spinner is enabled or not - stopChan chan struct{} // stopChan is a channel used to stop the indicator + stopChan chan os.Signal // stopChan is a channel used to stop the indicator HideCursor bool // hideCursor determines if the cursor is visible PreUpdate func(s *Spinner) // will be triggered before every spinner update PostUpdate func(s *Spinner) // will be triggered after every spinner update @@ -204,7 +206,7 @@ func New(cs []string, d time.Duration, options ...Option) *Spinner { mu: &sync.RWMutex{}, Writer: color.Output, WriterFile: os.Stdout, // matches color.Output - stopChan: make(chan struct{}, 1), + stopChan: make(chan os.Signal, 1), active: false, enabled: true, HideCursor: true, @@ -328,13 +330,29 @@ func (s *Spinner) Start() { } s.active = true + signal.Notify(s.stopChan, syscall.Signal(0x0), os.Interrupt) s.mu.Unlock() go func() { for { for i := 0; i < len(s.chars); i++ { select { - case <-s.stopChan: + case sig := <-s.stopChan: + s.mu.Lock() + defer s.mu.Unlock() + if sig != syscall.Signal(0x0) { + s.active = false + if s.HideCursor && !isWindowsTerminalOnWindows { + fmt.Fprint(s.Writer, "\033[?25h") + } + signal.Stop(s.stopChan) + if !isWindows { + process, _ := os.FindProcess(os.Getpid()) + _ = process.Signal(sig) + } + } else { + signal.Stop(s.stopChan) + } return default: s.mu.Lock() @@ -396,7 +414,8 @@ func (s *Spinner) Stop() { fmt.Fprint(s.Writer, s.FinalMSG) } } - s.stopChan <- struct{}{} + s.stopChan <- syscall.Signal(0x0) + signal.Stop(s.stopChan) } } diff --git a/spinner_test.go b/spinner_test.go index c37f48b..2e079fa 100644 --- a/spinner_test.go +++ b/spinner_test.go @@ -17,11 +17,13 @@ package spinner import ( "bytes" "fmt" - "io/ioutil" + "io" "os" + "os/signal" "reflect" "strings" "sync" + "syscall" "testing" "time" @@ -277,7 +279,7 @@ func TestColorError(t *testing.T) { } func TestWithWriter(t *testing.T) { - s := New(CharSets[9], time.Millisecond*400, WithWriter(ioutil.Discard)) + s := New(CharSets[9], time.Millisecond*400, WithWriter(io.Discard)) _ = s } @@ -318,6 +320,60 @@ func TestComputeNumberOfLinesNeededToPrintStringInternal(t *testing.T) { } } +// TestUnhideCursor verifies the cursor is unhidden before exiting +func TestUnhideCursor(t *testing.T) { + tests := map[string]struct { + interrupt os.Signal + }{ + "the spinner stops normally": {}, + "the process is interrupted": { + interrupt: os.Interrupt, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + s, out := withOutput(CharSets[1], 100*time.Millisecond) + interrupts := make(chan os.Signal, 1) + signal.Notify(interrupts, syscall.Signal(0x0), os.Interrupt) + defer signal.Stop(interrupts) + defer close(interrupts) + + s.Start() + time.Sleep(240 * time.Millisecond) + + eraser := bytes.Buffer{} + if test.interrupt != nil { + s.stopChan <- test.interrupt + if exit := <-interrupts; exit != test.interrupt { + t.Errorf("Unexpected signal was returned=%v", exit) + close(s.stopChan) + } + } else { + preStopOutput := s.lastOutputPlain + s.Stop() + s.Writer = &eraser + s.lastOutputPlain = preStopOutput + s.erase() + } + + if s.Active() { + t.Errorf("Cursor is still active after stopping\n") + } + if !strings.HasPrefix(out.String(), "\033[?25l") { + t.Errorf("Output does not start by hiding the cursor\n") + t.Logf("\tWanted: '%q'\n", bytes.NewBufferString("\033[?25l")) + t.Logf("\tFound: '%q'\n", out) + } + if !strings.HasSuffix(out.String(), "\033[?25h"+eraser.String()) { + t.Errorf("Output does not reset the cursor correctly\n") + t.Logf("\tWanted: '%q'\n", bytes.NewBufferString("\033[?25h"+eraser.String())) + t.Logf("\tFound: '%q'\n", out) + } + }) + } +} + /* Benchmarks */