Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unhide the cursor before exiting for termination signals #155

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 26 additions & 4 deletions spinner.go
Expand Up @@ -21,10 +21,12 @@ import (
"io"
"math"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
"time"
"unicode/utf8"

Expand Down Expand Up @@ -190,7 +192,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
Expand All @@ -205,7 +207,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,
Expand Down Expand Up @@ -328,14 +330,33 @@ func (s *Spinner) Start() {
color.NoColor = true
}

interrupts := []os.Signal{
syscall.Signal(0x0),
syscall.SIGHUP,
syscall.SIGINT,
syscall.SIGQUIT,
syscall.SIGTERM,
}
signal.Notify(s.stopChan, interrupts...)

s.active = true
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)
syscall.Kill(os.Getpid(), sig.(syscall.Signal))
return
default:
s.mu.Lock()
Expand Down Expand Up @@ -397,7 +418,8 @@ func (s *Spinner) Stop() {
fmt.Fprint(s.Writer, s.FinalMSG)
}
}
s.stopChan <- struct{}{}
s.stopChan <- syscall.Signal(0x0)
signal.Stop(s.stopChan)
}
}

Expand Down
69 changes: 67 additions & 2 deletions spinner_test.go
Expand Up @@ -17,11 +17,13 @@ package spinner
import (
"bytes"
"fmt"
"io/ioutil"
"io"
"os"
"os/signal"
"reflect"
"strings"
"sync"
"syscall"
"testing"
"time"

Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -318,6 +320,69 @@ 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 terminal hangs up": {
interrupt: syscall.SIGHUP,
},
"the process is interrupted": {
interrupt: syscall.SIGINT,
},
"the process is quit": {
interrupt: syscall.SIGQUIT,
},
"the process is terminated": {
interrupt: syscall.SIGTERM,
},
}

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.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
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
*/
Expand Down
1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/pwd_go15_plan9.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/pwd_plan9.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/race.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/race0.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/str.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/syscall.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions vendor/golang.org/x/sys/plan9/syscall_plan9.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/zsyscall_plan9_386.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/zsyscall_plan9_amd64.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions vendor/golang.org/x/sys/plan9/zsyscall_plan9_arm.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion vendor/golang.org/x/sys/unix/README.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 54 additions & 0 deletions vendor/golang.org/x/sys/unix/asm_linux_loong64.s

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.