From 7217c76f1ac9f355127ce7b91c25dac4abd44d54 Mon Sep 17 00:00:00 2001 From: Tim Heckman Date: Sat, 11 Dec 2021 23:50:19 -0800 Subject: [PATCH] Add support for rendering animated spinner at the end of the line This change updates the internals of the spinner to support rendering the animation at the end of the line instead of the beginning. This is exposed via the Config.SpinnerAtEnd struct field, and related method. --- README.md | 6 ++++ spinner.go | 90 +++++++++++++++++++++++++++++++++++++------------ spinner_test.go | 80 ++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 150 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index b191be2..d0e1860 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,12 @@ list of tasks being executed serially. ##### StopFail ![Animation with Failure](https://raw.githubusercontent.com/theckman/yacspin-gifs/master/features/stop_fail.gif) +#### Animation At End of Line +The `SpinnerAtEnd` field of the `Config` struct allows you to specify whether +the spinner is rendered at the end of the line instead of the beginning. The +default value (`false`) results in the spinner being rendered at the beginning +of the line. + #### Concurrency The spinner is safe for concurrent use, so you can update any of its settings via methods whether the spinner is stopped or is currently animating. diff --git a/spinner.go b/spinner.go index a97cba8..4984f5b 100644 --- a/spinner.go +++ b/spinner.go @@ -39,6 +39,9 @@ // time.Sleep(2 * time.Second) // // spinner.Stop() +// +// Check out the Config struct to see all of the possible configuration options +// supported by the Spinner. package yacspin import ( @@ -105,6 +108,11 @@ type Config struct { // your terminal for the cursor to appear again. HideCursor bool + // SpinnerAtEnd configures the spinner to render the animation at the end of + // the line instead of the beginning. The default behavior is to render the + // animated spinner at the beginning of the line. + SpinnerAtEnd bool + // ColorAll describes whether to color everything (all) or just the spinner // character(s). This cannot be changed after the *Spinner has been // constructed. @@ -118,24 +126,38 @@ type Config struct { CharSet []string // Prefix is the string printed immediately before the spinner. + // + // If SpinnerAtEnd is set to true, it's recommended that this string start + // with a space character (` `). Prefix string // Suffix is the string printed immediately after the spinner and before the - // message. It's recommended that this string starts with an space ` ` - // character. + // message. + // + // If SpinnerAtEnd is set to false, it's recommended that this string starts + // with an space character (` `). Suffix string // SuffixAutoColon configures whether the spinner adds a colon after the // suffix automatically. If there is a message, a colon followed by a space // is added to the suffix. Otherwise, if there is no message the colon is // omitted. + // + // If SpinnerAtEnd is set to true, this option is ignored. SuffixAutoColon bool - // Message is the string printed after the suffix. If a suffix is present, - // `: ` is appended to the suffix before printing the message. It results in - // a message like: + // Message is the message string printed by the spinner. If SpinnerAtEnd is + // set to false and SuffixAutoColon is set to true, the printed line will + // look like: // - // : + // : + // + // If SpinnerAtEnd is set to true, the printed line will instead look like + // this: + // + // + // + // In this case, it may be preferred to set the Prefix to empty space (` `). Message string // StopMessage is the message used when Stop() is called. @@ -174,6 +196,7 @@ type Spinner struct { cursorHidden bool suffixAutoColon bool isDumbTerm bool + spinnerAtEnd bool status *uint32 lastPrintLen int @@ -229,6 +252,7 @@ func New(cfg Config) (*Spinner, error) { colorAll: cfg.ColorAll, cursorHidden: cfg.HideCursor, + spinnerAtEnd: cfg.SpinnerAtEnd, suffixAutoColon: cfg.SuffixAutoColon, isDumbTerm: os.Getenv("TERM") == "dumb", colorFn: fmt.Sprintf, @@ -611,7 +635,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) { } } - if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, cFn); err != nil { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, cFn); err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } } else { @@ -619,7 +643,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) { panic(fmt.Sprintf("failed to erase line: %v", err)) } - n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, fmt.Sprintf) + n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, fmt.Sprintf) if err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } @@ -676,7 +700,7 @@ func (s *Spinner) paintStop(chanOk bool) { if c.Size > 0 || len(m) > 0 { // paint the line with a newline as it's the final line - if _, err := paint(s.buffer, mw, c, p, m+"\n", suf, s.suffixAutoColon, s.colorAll, cFn); err != nil { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, cFn); err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } } @@ -686,7 +710,7 @@ func (s *Spinner) paintStop(chanOk bool) { } if c.Size > 0 || len(m) > 0 { - if _, err := paint(s.buffer, mw, c, p, m+"\n", suf, s.suffixAutoColon, false, fmt.Sprintf); err != nil { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, fmt.Sprintf); err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } } @@ -734,28 +758,50 @@ func padChar(char character, maxWidth int) string { // paint writes a single line to the w, using the provided character, message, // and color function -func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix string, suffixAutoColon, colorAll bool, colorFn func(format string, a ...interface{}) string) (int, error) { - if char.Size == 0 { +func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix string, suffixAutoColon, colorAll, spinnerAtEnd, finalPaint bool, colorFn func(format string, a ...interface{}) string) (int, error) { + var output string + + switch char.Size { + case 0: if colorAll { - return fmt.Fprint(w, colorFn(message)) + output = colorFn(message) + break } - return fmt.Fprint(w, message) - } + output = message - c := padChar(char, maxWidth) + default: + c := padChar(char, maxWidth) + + if spinnerAtEnd { + if colorAll { + output = colorFn("%s%s%s%s", message, prefix, c, suffix) + break + } + + output = fmt.Sprintf("%s%s%s%s", message, prefix, colorFn(c), suffix) + break + } - if suffixAutoColon { - if len(suffix) > 0 && len(message) > 0 && message != "\n" { - suffix += ": " + if suffixAutoColon { // also implicitly !spinnerAtEnd + if len(suffix) > 0 && len(message) > 0 && message != "\n" { + suffix += ": " + } } + + if colorAll { + output = colorFn("%s%s%s%s", prefix, c, suffix, message) + break + } + + output = fmt.Sprintf("%s%s%s%s", prefix, colorFn(c), suffix, message) } - if colorAll { - return fmt.Fprint(w, colorFn("%s%s%s%s", prefix, c, suffix, message)) + if finalPaint { + output += "\n" } - return fmt.Fprintf(w, "%s%s%s%s", prefix, colorFn(c), suffix, message) + return fmt.Fprint(w, output) } // Frequency updates the frequency of the spinner being animated. diff --git a/spinner_test.go b/spinner_test.go index a27d0b2..eab373a 100644 --- a/spinner_test.go +++ b/spinner_test.go @@ -114,6 +114,7 @@ func TestNew(t *testing.T) { StopFailMessage: "test stop fail message", StopFailCharacter: "✗", StopFailColors: []string{"fgHiRed"}, + SpinnerAtEnd: true, }, }, } @@ -138,6 +139,10 @@ func TestNew(t *testing.T) { t.Fatalf("spinner.cursorHiddenn = %t, want %t", spinner.cursorHidden, tt.cfg.HideCursor) } + if spinner.spinnerAtEnd != tt.cfg.SpinnerAtEnd { + t.Fatalf("spinner.spinnerAtEnd = %t, want %t", spinner.spinnerAtEnd, tt.cfg.SpinnerAtEnd) + } + if spinner.mu == nil { t.Fatal("spinner.mu is nil") } @@ -1073,6 +1078,22 @@ func TestSpinner_paintUpdate(t *testing.T) { }, want: "\r\033[K\ray msg\r\033[K\raz msg\r\033[K\raz msg\r\033[K\ray msg", }, + { + name: "spinner_no_hide_cursor_spinnerAtEnd", + spinner: &Spinner{ + buffer: &bytes.Buffer{}, + mu: &sync.Mutex{}, + prefix: " a", + message: "msg", + suffix: " ", + maxWidth: 1, + colorFn: fmt.Sprintf, + chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, + frequency: 10, + spinnerAtEnd: true, + }, + want: "\r\033[K\rmsg ay \r\033[K\rmsg az \r\033[K\rmsg az \r\033[K\rmsg ay ", + }, { name: "spinner_no_hide_cursor_auto_cursor", spinner: &Spinner{ @@ -1151,8 +1172,8 @@ func TestSpinner_paintUpdate(t *testing.T) { got := buf.String() - if got != tt.want { - t.Errorf("got = %#v, want %#v", got, tt.want) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Fatalf("output differs: (-want / +got)\n%s", diff) } }) } @@ -1180,6 +1201,39 @@ func TestSpinner_paintStop(t *testing.T) { }, want: "\r\033[K\rax stop\n", }, + { + name: "ok_spinnerAtEnd", + ok: true, + spinner: &Spinner{ + buffer: &bytes.Buffer{}, + mu: &sync.Mutex{}, + prefix: " a", + suffix: " ", + maxWidth: 1, + stopColorFn: fmt.Sprintf, + spinnerAtEnd: true, + stopChar: character{Value: "x", Size: 1}, + stopMsg: "stop", + }, + want: "\r\033[K\rstop ax \n", + }, + { + name: "ok_spinnerAtEnd_suffixAutoColon", + ok: true, + spinner: &Spinner{ + buffer: &bytes.Buffer{}, + mu: &sync.Mutex{}, + prefix: " a", + suffix: " ", + maxWidth: 1, + stopColorFn: fmt.Sprintf, + spinnerAtEnd: true, + suffixAutoColon: true, + stopChar: character{Value: "x", Size: 1}, + stopMsg: "stop", + }, + want: "\r\033[K\rstop ax \n", + }, { name: "ok_auto_colon", ok: true, @@ -1302,6 +1356,24 @@ func TestSpinner_paintStop(t *testing.T) { }, want: "\r\033[K\rfullColor: ay stop\n", }, + { + name: "fail_colorall_spinnerAtEnd", + spinner: &Spinner{ + buffer: &bytes.Buffer{}, + mu: &sync.Mutex{}, + prefix: " a", + suffix: " ", + maxWidth: 1, + stopFailColorFn: func(format string, a ...interface{}) string { + return fmt.Sprintf("fullColor: %s", fmt.Sprintf(format, a...)) + }, + stopFailChar: character{Value: "y", Size: 1}, + stopFailMsg: "stop", + colorAll: true, + spinnerAtEnd: true, + }, + want: "\r\033[K\rfullColor: stop ay \n", + }, { name: "fail_colorall_no_char", spinner: &Spinner{ @@ -1330,8 +1402,8 @@ func TestSpinner_paintStop(t *testing.T) { got := buf.String() - if got != tt.want { - t.Errorf("got = %#v, want %#v", got, tt.want) + if diff := cmp.Diff(tt.want, got); diff != "" { + t.Fatalf("output differs: (-want / +got)\n%s", diff) } }) }