Skip to content

Commit

Permalink
Merge pull request #47 from theckman/spinner_at_end
Browse files Browse the repository at this point in the history
Add support for rendering animated spinner at the end of the line
  • Loading branch information
theckman committed Dec 12, 2021
2 parents 9775868 + 7217c76 commit 88324b5
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 26 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
90 changes: 68 additions & 22 deletions spinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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.
Expand All @@ -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:
//
// <prefix><spinner><suffix>: <message>
// <prefix><spinner><suffix>: <message>
//
// If SpinnerAtEnd is set to true, the printed line will instead look like
// this:
//
// <message><prefix><spinner><suffix>
//
// 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.
Expand Down Expand Up @@ -174,6 +196,7 @@ type Spinner struct {
cursorHidden bool
suffixAutoColon bool
isDumbTerm bool
spinnerAtEnd bool

status *uint32
lastPrintLen int
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -611,15 +635,15 @@ 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 {
if err := s.eraseDumbTerm(s.buffer); err != nil {
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))
}
Expand Down Expand Up @@ -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))
}
}
Expand All @@ -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))
}
}
Expand Down Expand Up @@ -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.
Expand Down
80 changes: 76 additions & 4 deletions spinner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ func TestNew(t *testing.T) {
StopFailMessage: "test stop fail message",
StopFailCharacter: "✗",
StopFailColors: []string{"fgHiRed"},
SpinnerAtEnd: true,
},
},
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
})
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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)
}
})
}
Expand Down

0 comments on commit 88324b5

Please sign in to comment.