From 1e574fe6662151edfb6d4c00df0f33de90a2e180 Mon Sep 17 00:00:00 2001 From: sha1n Date: Fri, 4 Jun 2021 18:28:23 +0300 Subject: [PATCH] improved spinner responsiveness to title updates --- spinner.go | 69 ++++++++++++++++++++++++++----------------------- spinner_test.go | 50 ++++++++++++++--------------------- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/spinner.go b/spinner.go index b6c6d42..ce60d95 100644 --- a/spinner.go +++ b/spinner.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "strings" "sync" "time" ) @@ -62,10 +63,10 @@ type Spinner interface { type spinner struct { writer io.Writer interval time.Duration - mx *sync.RWMutex - titleMx *sync.RWMutex + stateMx *sync.RWMutex active bool stopC chan bool + titleC chan string title string formatter SpinnerFormatter } @@ -75,10 +76,10 @@ func NewSpinner(writer io.Writer, title string, interval time.Duration, formatte return &spinner{ writer: writer, interval: interval, - mx: &sync.RWMutex{}, - titleMx: &sync.RWMutex{}, + stateMx: &sync.RWMutex{}, active: false, stopC: make(chan bool), + titleC: make(chan string), title: title, formatter: formatter, } @@ -95,8 +96,8 @@ func (s *spinner) writeString(str string) (n int, err error) { // Start starts the spinner in the background and returns a cancellation handle and an error in case the spinner is already running. func (s *spinner) Start() (cancel context.CancelFunc, err error) { - s.mx.Lock() - defer s.mx.Unlock() + s.stateMx.Lock() + defer s.stateMx.Unlock() if s.active { return nil, errors.New("spinner already active") @@ -115,27 +116,39 @@ func (s *spinner) Start() (cancel context.CancelFunc, err error) { defer s.setActiveSafe(false) + update := func(title string) { + indicatorValue := s.formatter.FormatIndicator(fmt.Sprintf("%v", spinring.Value)) + if title != "" { + _, _ = s.writeString(fmt.Sprintf("%s%s %s", TermControlEraseLine, indicatorValue, s.formatter.FormatTitle(title))) + } else { + _, _ = s.writeString(fmt.Sprintf("%s%s", TermControlEraseLine, indicatorValue)) + } + } + for { select { case <-context.Done(): timer.Stop() + close(s.titleC) + s.printExitMessage("Cancelled...") + return case <-s.stopC: timer.Stop() + close(s.titleC) return + case title := <-s.titleC: + // The title is only written by this routine, so we're safe. + s.title = title + update(title) + case <-timer.C: spinring = spinring.Next() - title := s.getTitle() - indicatorValue := s.formatter.FormatIndicator(fmt.Sprintf("%v", spinring.Value)) - if title != "" { - s.writeString(fmt.Sprintf("%s%s %s", TermControlEraseLine, indicatorValue, s.formatter.FormatTitle(title))) - } else { - s.writeString(fmt.Sprintf("%s%s", TermControlEraseLine, indicatorValue)) - } - + title := s.title + update(title) } } }() @@ -147,8 +160,8 @@ func (s *spinner) Start() (cancel context.CancelFunc, err error) { // Stop stops the spinner and displays the specified message func (s *spinner) Stop(message string) (err error) { - s.mx.Lock() - defer s.mx.Unlock() + s.stateMx.Lock() + defer s.stateMx.Unlock() if !s.active { err = errors.New("spinner not active") @@ -163,22 +176,12 @@ func (s *spinner) Stop(message string) (err error) { // SetTitle updates the spinner text. func (s *spinner) SetTitle(title string) { - s.titleMx.Lock() - defer s.titleMx.Unlock() - - s.title = title -} - -func (s *spinner) getTitle() string { - s.titleMx.RLock() - defer s.titleMx.RUnlock() - - return s.title + s.titleC <- strings.TrimSpace(title) } func (s *spinner) printExitMessage(message string) { - s.writeString(TermControlEraseLine) - s.writeString(message) + _, _ = s.writeString(TermControlEraseLine) + _, _ = s.writeString(message) } func (s *spinner) createSpinnerRing() *ring.Ring { @@ -193,15 +196,15 @@ func (s *spinner) createSpinnerRing() *ring.Ring { } func (s *spinner) isActiveSafe() bool { - s.mx.RLock() - defer s.mx.RUnlock() + s.stateMx.RLock() + defer s.stateMx.RUnlock() return s.active } func (s *spinner) setActiveSafe(active bool) { - s.mx.Lock() - defer s.mx.Unlock() + s.stateMx.Lock() + defer s.stateMx.Unlock() s.active = active } diff --git a/spinner_test.go b/spinner_test.go index 85cfa28..4b45135 100644 --- a/spinner_test.go +++ b/spinner_test.go @@ -134,45 +134,35 @@ func assertStoppedEventually(t *testing.T, outBuffer *bytes.Buffer, spinner *spi ) } -// TODO can this be simplified? func assertSpinnerCharSequence(t *testing.T, outBuffer *bytes.Buffer) { charSeq := DefaultSpinnerCharSeq() - readChars := make([]string, len(charSeq)) - readCharsCount := 0 + readChars := []string{} - readSequence := func() string { - startTime := time.Now() + scan := func() { for { - s, _ := outBuffer.ReadString(TermControlEraseLine[len(TermControlEraseLine)-1]) // read everything you got - if strippedString := strings.Trim(s, TermControlEraseLine+"\x00"); strippedString != "" { - return strippedString + r, _, e := outBuffer.ReadRune() + print(string(r), ",") + if e != nil { + continue } - - // guard again infinite loop - if time.Now().After(startTime.Add(time.Second * 30)) { - return "" + readChar := string(r) + if len(readChars) == 0 && readChar == charSeq[0] { + readChars = append(readChars, readChar) + } else if len(readChars) > 0 { + for _, ch := range charSeq { + if ch == readChar { + readChars = append(readChars, ch) + } + + if len(readChars) == len(charSeq) { + return + } + } } } } - // find the first character in the spinner sequence, so we can validate order properly - for { - strippedString := readSequence() - if strippedString != "" && strippedString == charSeq[0] { - readChars[0] = strippedString - break - } - // guard against infinite loop caused by bugs - readCharsCount++ - if readCharsCount > len(charSeq)*2 { - assert.Fail(t, "something went wrong...") - } - } - - for i := 1; i < len(charSeq); i++ { - readChars[i] = readSequence() - } + scan() assert.Equal(t, charSeq, readChars) - }