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) } }) }