From ccb1aaeb2b17fbd1db2051d47a908bcc7babb301 Mon Sep 17 00:00:00 2001 From: Tim Heckman Date: Thu, 30 Dec 2021 16:55:34 -0800 Subject: [PATCH] Add TerminalMode config; make Frequency optional when not TTY This change introduces a substantial change to how we handle the TTY discovery when constructing the spinner. It adds a new bitflag field to the Config struct, TerminalMode, that allows consumers to control whether the spinner tries to automatically disocver if it's within a TTY and/or a dumb terminal. It also allows you to override the behaviors, if you know the automatic mode won't do what you want. A side effect of this change is that you can now manually step the spinner animation by setting TerminalMode: ForceNoTTYMode, and then starting the spinner and calling the Message() method when you want to animate the spinner. To permit this, the Frequency field in the Config struct is now not required when calling the New() function. An error will be generated on Start() if it's within a TTY and the Frequency is 0. This also deprecates the NotTTY field in the Config struct. --- README.md | 24 +++++--- spinner.go | 156 +++++++++++++++++++++++++++++++++++++++--------- spinner_test.go | 150 ++++++++++++++++++++++++++++++++++++---------- 3 files changed, 264 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index f350197..c7c21b1 100644 --- a/README.md +++ b/README.md @@ -110,19 +110,27 @@ want to change a few configuration items via method calls, you can `Pause()` the spinner first. After making the changes you can call `Unpause()`, and it will continue rendering like normal with the newly applied configuration. -#### Supporting non-TTY Output Targets -`yacspin` also has native support for non-TTY output targets. This is detected -automatically within the constructor, or can be specified via the `NotTTY` -`Config` struct field, and results in a different mode of operation. +#### Supporting Non-Interactive (TTY) Output Targets +`yacspin` also has native support for non-interactive (TTY) output targets. By +default this is detected in the constructor, or can be overriden via the +`TerminalMode` `Config` struct field. When detecting the application is not +running withn a TTY session, the behavior of the spinner is different. -Specifically, when this is detected the spinner no longer uses colors, disables -the automatic spinner animation, and instead only animates the spinner when updating the -message. In addition, each animation is rendered on a new line instead of -overwriting the current line. +Specifically, when this is automatically detected the spinner no longer uses +colors, disables the automatic spinner animation, and instead only animates the +spinner when updating the message. In addition, each animation is rendered on a +new line instead of overwriting the current line. This should result in human-readable output without any changes needed by consumers, even when the system is writing to a non-TTY destination. +#### Manually Stepping Animation +If you'd like to manually animate the spinner, you can do so by setting the +`TerminalMode` to `ForceNoTTYMode | ForceSmartTerminalMode`. In this mode the +spinner will still use colors and other text stylings, but the animation only +happens when data is updated and on individual lines. You can accomplish this by +calling the `Message()` method with the same used previously. + ## Usage ``` go get github.com/theckman/yacspin diff --git a/spinner.go b/spinner.go index 547ed1f..dd96e5f 100644 --- a/spinner.go +++ b/spinner.go @@ -89,6 +89,48 @@ func setToCharSlice(ss []string) ([]character, int) { return c, maxWidth } +// TerminalMode is a type to represent the bit flag controlling the terminal +// mode of the spinner, accepted as a field on the Config struct. See the +// comments on the exported constants for more info. +type TerminalMode uint32 + +const ( + // AutomaticMode configures the constructor function to try and determine if + // the application using yacspin is being executed within a interactive + // (teletype [TTY]) session. + AutomaticMode TerminalMode = 1 << iota + + // ForceTTYMode configures the spinner to operate as if it's running within + // a TTY session. + ForceTTYMode + + // ForceNoTTYMode configures the spinner to operate as if it's not running + // within a TTY session. This mode causes the spinner to only animate when + // data is being updated. Each animation is rendered on a new line. You can + // trigger an animation by calling the Message() method, including with the + // last value it was called with. + ForceNoTTYMode + + // ForceDumbTerminalMode configures the spinner to operate as if it's + // running within a dumb terminal. This means the spinner will not use ANSI + // escape sequences to print colors or to erase each line. Line erasure to + // animate the spinner is accomplished by overwriting the line with space + // characters. + ForceDumbTerminalMode + + // ForceSmartTerminalMode configures the spinner to operate as if it's + // running within a terminal that supports ANSI escape sequences (VT100). + // This includes printing of stylized text, and more better line erasure to + // animate the spinner. + ForceSmartTerminalMode +) + +func termModeAuto(t TerminalMode) bool { return t&AutomaticMode > 0 } +func termModeForceTTY(t TerminalMode) bool { return t&ForceTTYMode > 0 } +func termModeForceNoTTY(t TerminalMode) bool { return t&ForceNoTTYMode > 0 } +func termModeForceDumb(t TerminalMode) bool { return t&ForceDumbTerminalMode > 0 } +func termModeForceSmart(t TerminalMode) bool { return t&ForceSmartTerminalMode > 0 } + // Config is the configuration structure for the Spinner type, which you provide // to the New() function. Some of the fields can be updated after the *Spinner // is constructed, others can only be set when calling the constructor. Please @@ -96,8 +138,6 @@ func setToCharSlice(ss []string) ([]character, int) { type Config struct { // Frequency specifies how often to animate the spinner. Optimal value // depends on the character set you use. - // - // Note: This is a required value (cannot be 0). Frequency time.Duration // Writer is the place where we are outputting the spinner, and can't be @@ -200,10 +240,38 @@ type Config struct { // respects the ColorAll field. StopFailColors []string + // TerminalMode is a bitflag field to control how the internal TTY / "dumb + // terminal" detection works, to allow consumers to override the internal + // behaviors. To set this value, it's recommended to use the TerminalMode + // constants exported by this package. + // + // If not set, the New() function implicitly sets it to AutomaticMode. The + // New() function also returns an error if you have conflicting flags, such + // as setting ForceTTYMode and ForceNoTTYMode, or if you set AutomaticMode + // and any other flags set. + // + // When in AutomaticMode, the New() function attempts to determine if the + // current application is running within an interactive (teletype [TTY]) + // session. If it does not appear to be within a TTY, it sets this field + // value to ForceNoTTYMode | ForceDumbTerminalMode. + // + // If this does appear to be a TTY, the ForceTTYMode bitflag will bet set. + // Similarly, if it's a TTY and the TERM environment variable isn't set to + // "dumb" the ForceSmartTerminalMode bitflag will also be set. + // + // If the deprecated NoTTY Config struct field is set to true, and this + // field is AutomaticMode, the New() function sets field to the value of + // ForceNoTTYMode | ForceDumbTerminalMode. + TerminalMode TerminalMode + // NotTTY tells the spinner that the Writer should not be treated as a TTY. // This results in the animation being disabled, with the animation only // happening whenever the data is updated. This mode also renders each // update on new line, versus reusing the current line. + // + // Deprecated: use TerminalMode field instead by setting it to: + // ForceNoTTYMode | ForceDumbTerminalMode. This will be removed in a future + // release. NotTTY bool } @@ -224,8 +292,7 @@ type Spinner struct { colorAll bool cursorHidden bool suffixAutoColon bool - isDumbTerm bool - isNotTTY bool + termMode TerminalMode spinnerAtEnd bool status *uint32 @@ -269,20 +336,50 @@ const ( // New creates a new unstarted spinner. If stdout does not appear to be a TTY, // this constructor implicitly sets cfg.NotTTY to true. func New(cfg Config) (*Spinner, error) { - if cfg.Frequency < 1 { - return nil, errors.New("cfg.Frequency must be greater than 0") - } - if cfg.ShowCursor && cfg.HideCursor { return nil, errors.New("cfg.ShowCursor and cfg.HideCursor cannot be true") } + if cfg.TerminalMode == 0 { + cfg.TerminalMode = AutomaticMode + } + + // AutomaticMode flag has been set, but so have others + if termModeAuto(cfg.TerminalMode) && cfg.TerminalMode != AutomaticMode { + return nil, errors.New("cfg.TerminalMode cannot have AutomaticMode flag set if others are set") + } + + if termModeForceTTY(cfg.TerminalMode) && termModeForceNoTTY(cfg.TerminalMode) { + return nil, errors.New("cfg.TerminalMode cannot have both ForceTTYMode and ForceNoTTYMode flags set") + } + + if termModeForceDumb(cfg.TerminalMode) && termModeForceSmart(cfg.TerminalMode) { + return nil, errors.New("cfg.TerminalMode cannot have both ForceDumbTerminalMode and ForceSmartTerminalMode flags set") + } + if cfg.HideCursor { cfg.ShowCursor = false } - if !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) { - cfg.NotTTY = true + // cfg.NotTTY compatibility + if cfg.TerminalMode == AutomaticMode && cfg.NotTTY { + cfg.TerminalMode = ForceNoTTYMode | ForceDumbTerminalMode + } + + // is this a dumb terminal / not a TTY? + if cfg.TerminalMode == AutomaticMode && !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) { + cfg.TerminalMode = ForceNoTTYMode | ForceDumbTerminalMode + } + + // if cfg.TerminalMode is still equal to AutomaticMode, this is a TTY + if cfg.TerminalMode == AutomaticMode { + cfg.TerminalMode = ForceTTYMode + + if os.Getenv("TERM") == "dumb" { + cfg.TerminalMode |= ForceDumbTerminalMode + } else { + cfg.TerminalMode |= ForceSmartTerminalMode + } } buf := bytes.NewBuffer(make([]byte, 2048)) @@ -300,7 +397,7 @@ func New(cfg Config) (*Spinner, error) { cursorHidden: !cfg.ShowCursor, spinnerAtEnd: cfg.SpinnerAtEnd, suffixAutoColon: cfg.SuffixAutoColon, - isDumbTerm: os.Getenv("TERM") == "dumb", + termMode: cfg.TerminalMode, colorFn: fmt.Sprintf, stopColorFn: fmt.Sprintf, stopFailColorFn: fmt.Sprintf, @@ -325,9 +422,9 @@ func New(cfg Config) (*Spinner, error) { // can only error if the charset is empty, and we prevent that above _ = s.CharSet(cfg.CharSet) - if cfg.NotTTY { - s.isNotTTY = true - s.isDumbTerm = true + if termModeForceNoTTY(s.termMode) { + // hack to prevent the animation from running if not a TTY + s.frequency = time.Duration(math.MaxInt64) } if cfg.Writer == nil { @@ -442,6 +539,10 @@ func (s *Spinner) Start() error { s.mu.Lock() + if s.frequency < 1 && termModeForceTTY(s.termMode) { + return errors.New("spinner Frequency duration must be greater than 0 when used within a TTY") + } + if len(s.chars) == 0 { s.mu.Unlock() @@ -456,11 +557,6 @@ func (s *Spinner) Start() error { s.frequencyUpdateCh = make(chan time.Duration, 4) s.dataUpdateCh, s.cancelCh = make(chan struct{}, 1), make(chan struct{}, 1) - if s.isNotTTY { - // hack to prevent the animation from running if not a TTY - s.frequency = time.Duration(math.MaxInt64) - } - s.mu.Unlock() // because of the atomic swap above, we know it's safe to mutate these @@ -643,7 +739,7 @@ func (s *Spinner) painter(cancel, dataUpdate, pause <-chan struct{}, done chan<- case <-dataUpdate: // if this is not a TTY: animate the spinner on the data update - s.paintUpdate(timer, s.isNotTTY) + s.paintUpdate(timer, termModeForceNoTTY(s.termMode)) case frequency := <-frequencyUpdate: handleFrequencyUpdate(frequency, timer, lastTick) @@ -692,7 +788,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, animate bool) { defer s.buffer.Reset() - if !s.isDumbTerm { + if termModeForceSmart(s.termMode) { if err := erase(s.buffer); err != nil { panic(fmt.Sprintf("failed to erase line: %v", err)) } @@ -703,7 +799,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, animate bool) { } } - if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, s.isNotTTY, cFn); err != nil { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, termModeForceNoTTY(s.termMode), cFn); err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } } else { @@ -711,7 +807,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, animate bool) { panic(fmt.Sprintf("failed to erase line: %v", err)) } - n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, s.isNotTTY, fmt.Sprintf) + n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, termModeForceNoTTY(s.termMode), fmt.Sprintf) if err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } @@ -755,7 +851,7 @@ func (s *Spinner) paintStop(chanOk bool) { defer s.buffer.Reset() - if !s.isDumbTerm { + if termModeForceSmart(s.termMode) { if err := erase(s.buffer); err != nil { panic(fmt.Sprintf("failed to erase line: %v", err)) } @@ -768,7 +864,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, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, s.isNotTTY, cFn); err != nil { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, termModeForceNoTTY(s.termMode), cFn); err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } } @@ -778,7 +874,7 @@ func (s *Spinner) paintStop(chanOk bool) { } if c.Size > 0 || len(m) > 0 { - if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, s.isNotTTY, fmt.Sprintf); err != nil { + if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, termModeForceNoTTY(s.termMode), fmt.Sprintf); err != nil { panic(fmt.Sprintf("failed to paint line: %v", err)) } } @@ -801,7 +897,9 @@ func erase(w io.Writer) error { // eraseDumbTerm clears the line on dumb terminals func (s *Spinner) eraseDumbTerm(w io.Writer) error { - if s.isNotTTY { + if termModeForceNoTTY(s.termMode) { + // non-TTY outputs use \n instead of line erasure, + // so return early return nil } @@ -882,7 +980,9 @@ func (s *Spinner) Frequency(d time.Duration) error { return errors.New("duration must be greater than 0") } - if s.isNotTTY { + if termModeForceNoTTY(s.termMode) { + // when output target is not a TTY, we don't animate spinner + // so there is no need to update the frequency return nil } diff --git a/spinner_test.go b/spinner_test.go index 930fc1e..c6b6a4d 100644 --- a/spinner_test.go +++ b/spinner_test.go @@ -17,6 +17,8 @@ import ( "github.com/mattn/go-runewidth" ) +const termModeTTY = ForceTTYMode | ForceSmartTerminalMode + // testErrCheck looks to see if errContains is a substring of err.Error(). If // not, this calls t.Fatal(). It also calls t.Fatal() if there was an error, but // errContains is empty. Returns true if you should continue running the test, @@ -48,24 +50,21 @@ func testErrCheck(t *testing.T, name string, errContains string, err error) bool func TestNew(t *testing.T) { tests := []struct { - name string - writer io.Writer - maxWidth int - cfg Config - charSet []character - err string + name string + writer io.Writer + maxWidth int + overrideFreq time.Duration + cfg Config + charSet []character + err string }{ - { - name: "empty_config", - writer: os.Stdout, - err: "cfg.Frequency must be greater than 0", - }, { name: "config_with_frequency_and_default_writer", maxWidth: 1, writer: os.Stdout, cfg: Config{ - Frequency: 100 * time.Millisecond, + Frequency: 100 * time.Millisecond, + TerminalMode: termModeTTY, }, }, { @@ -105,6 +104,30 @@ func TestNew(t *testing.T) { }, err: "cfg.ShowCursor and cfg.HideCursor cannot be true", }, + { + name: "config_with_conflicting_TerminalMode_Auto", + cfg: Config{ + Frequency: 100 * time.Millisecond, + TerminalMode: AutomaticMode | ForceTTYMode, + }, + err: "cfg.TerminalMode cannot have AutomaticMode flag set if others are set", + }, + { + name: "config_with_conflicting_TerminalMode_TTY", + cfg: Config{ + Frequency: 100 * time.Millisecond, + TerminalMode: ForceTTYMode | ForceNoTTYMode, + }, + err: "cfg.TerminalMode cannot have both ForceTTYMode and ForceNoTTYMode flags set", + }, + { + name: "config_with_conflicting_TerminalMode_Term", + cfg: Config{ + Frequency: 100 * time.Millisecond, + TerminalMode: ForceDumbTerminalMode | ForceSmartTerminalMode, + }, + err: "cfg.TerminalMode cannot have both ForceDumbTerminalMode and ForceSmartTerminalMode flags set", + }, { name: "full_config_with_deprecated_hidden_cursor", writer: os.Stderr, @@ -126,6 +149,7 @@ func TestNew(t *testing.T) { StopFailCharacter: "✗", StopFailColors: []string{"fgHiRed"}, SpinnerAtEnd: true, + TerminalMode: termModeTTY, }, }, { @@ -149,6 +173,19 @@ func TestNew(t *testing.T) { StopFailCharacter: "✗", StopFailColors: []string{"fgHiRed"}, SpinnerAtEnd: true, + TerminalMode: termModeTTY, + }, + }, + { + name: "not_tty", + writer: os.Stderr, + maxWidth: 3, + overrideFreq: 9223372036854775807, + cfg: Config{ + Frequency: 100 * time.Millisecond, + Writer: os.Stderr, + CharSet: CharSets[59], + NotTTY: true, }, }, } @@ -193,8 +230,14 @@ func TestNew(t *testing.T) { t.Fatal("spinner.frequencyUpdateCh is nil") } - if spinner.frequency != tt.cfg.Frequency { - t.Errorf("spinner.frequency = %s, want %s", spinner.frequency, tt.cfg.Frequency) + if tt.overrideFreq > 0 { + if spinner.frequency != tt.overrideFreq { + t.Errorf("spinner.frequency = %d (%s), want %d (%s)", spinner.frequency, spinner.frequency, tt.overrideFreq, tt.overrideFreq) + } + } else { + if spinner.frequency != tt.cfg.Frequency { + t.Errorf("spinner.frequency = %d (%s), want %d (%s)", spinner.frequency, spinner.frequency, tt.cfg.Frequency, tt.cfg.Frequency) + } } if spinner.writer == nil { @@ -221,6 +264,16 @@ func TestNew(t *testing.T) { t.Errorf("spinner.stopMsg = %q, want %q", spinner.stopMsg, tt.cfg.StopMessage) } + if tt.cfg.NotTTY { + if spinner.termMode != ForceDumbTerminalMode|ForceNoTTYMode { + t.Error("spinner.termMode != ForceDumbTerminalMode | ForceNoTTYMode") + } + + if d := time.Duration(math.MaxInt64); spinner.frequency != d { + t.Errorf("spinner.frequency = %d (%s), want %d (%s)", spinner.frequency, spinner.frequency, d, d) + } + } + sc := character{Value: tt.cfg.StopCharacter, Size: runewidth.StringWidth(tt.cfg.StopCharacter)} if spinner.stopChar != sc { t.Errorf("spinner.stopChar = %#v, want %#v", spinner.stopChar, sc) @@ -342,8 +395,8 @@ func TestNew_dumbTerm(t *testing.T) { spinner, err := New(cfg) testErrCheck(t, "New()", "", err) - if !spinner.isDumbTerm { - t.Fatal("spinner.isDumbTerm = false, want true") + if !termModeForceDumb(spinner.termMode) { + t.Fatal("spinner.termMode does not contain ForceDumbTerminalMode flag") } } @@ -482,7 +535,10 @@ func TestSpinner_Frequency(t *testing.T) { mu: &sync.Mutex{}, frequency: 0, frequencyUpdateCh: tt.ch, - isNotTTY: tt.isNotTTY, + } + + if tt.isNotTTY { + spinner.termMode = ForceDumbTerminalMode | ForceNoTTYMode } tmr := time.NewTimer(2 * time.Second) @@ -784,6 +840,19 @@ func TestSpinner_Start(t *testing.T) { err string }{ + { + name: "invalid_frequency_when_tty", + spinner: &Spinner{ + status: uint32Ptr(statusStopped), + mu: &sync.Mutex{}, + frequency: 0, + colorFn: fmt.Sprintf, + stopColorFn: fmt.Sprintf, + stopFailColorFn: fmt.Sprintf, + termMode: ForceTTYMode, + }, + err: "spinner Frequency duration must be greater than 0 when used within a TTY", + }, { name: "running_spinner", spinner: &Spinner{ @@ -846,13 +915,13 @@ func TestSpinner_Start(t *testing.T) { buffer: &bytes.Buffer{}, status: uint32Ptr(statusStopped), mu: &sync.Mutex{}, - frequency: time.Millisecond, + frequency: 9223372036854775807, colorFn: fmt.Sprintf, stopColorFn: fmt.Sprintf, stopFailColorFn: fmt.Sprintf, stopMsg: "stop msg", stopFailMsg: "stop fail msg", - isNotTTY: true, + termMode: ForceNoTTYMode, maxWidth: 3, chars: []character{ character{ @@ -899,7 +968,7 @@ func TestSpinner_Start(t *testing.T) { t.Fatal("painter did not write data") } - if max := time.Duration(math.MaxInt64); tt.spinner.isNotTTY && tt.spinner.frequency != max { + if max := time.Duration(math.MaxInt64); termModeForceNoTTY(tt.spinner.termMode) && tt.spinner.frequency != max { t.Fatalf("tt.spinner.duration = %s, want %s", tt.spinner.frequency, max) } }) @@ -1188,6 +1257,7 @@ func TestSpinner_paintUpdate(t *testing.T) { colorFn: fmt.Sprintf, chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, frequency: 10, + termMode: termModeTTY, }, want: "\r\033[K\ray msg\r\033[K\raz msg\r\033[K\raz msg\r\033[K\ray msg", }, @@ -1204,6 +1274,7 @@ func TestSpinner_paintUpdate(t *testing.T) { chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, frequency: 10, spinnerAtEnd: true, + termMode: termModeTTY, }, want: "\r\033[K\rmsg ay \r\033[K\rmsg az \r\033[K\rmsg az \r\033[K\rmsg ay ", }, @@ -1220,6 +1291,7 @@ func TestSpinner_paintUpdate(t *testing.T) { chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, frequency: 10, suffixAutoColon: true, + termMode: termModeTTY, }, want: "\r\033[K\ray msg\r\033[K\raz msg\r\033[K\raz msg\r\033[K\ray msg", }, @@ -1236,6 +1308,7 @@ func TestSpinner_paintUpdate(t *testing.T) { chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, frequency: 10, suffixAutoColon: true, + termMode: termModeTTY, }, want: "\r\033[K\ray foo: msg\r\033[K\raz foo: msg\r\033[K\raz foo: msg\r\033[K\ray foo: msg", }, @@ -1252,6 +1325,7 @@ func TestSpinner_paintUpdate(t *testing.T) { colorFn: fmt.Sprintf, chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, frequency: 10, + termMode: termModeTTY, }, want: "\r\033[K\r\r\033[?25l\ray msg\r\033[K\r\r\033[?25l\raz msg\r\033[K\r\r\033[?25l\raz msg\r\033[K\r\r\033[?25l\ray msg", }, @@ -1268,7 +1342,8 @@ func TestSpinner_paintUpdate(t *testing.T) { colorFn: fmt.Sprintf, chars: []character{{Value: "y", Size: 1}, {Value: "z", Size: 1}}, frequency: 10, - isDumbTerm: true, + // TODO(theckman): verify + termMode: ForceDumbTerminalMode, }, want: "\r\ray msg\r \raz msg\r \raz msg\r \ray msg", }, @@ -1281,6 +1356,7 @@ func TestSpinner_paintUpdate(t *testing.T) { colorFn: fmt.Sprintf, chars: []character{{Value: "", Size: 0}}, frequency: 10, + termMode: termModeTTY, }, want: "\r\033[K\r\r\033[K\r\r\033[K\r\r\033[K\r", }, @@ -1327,6 +1403,7 @@ func TestSpinner_paintStop(t *testing.T) { stopColorFn: fmt.Sprintf, stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", + termMode: termModeTTY, }, want: "\r\033[K\rax stop\n", }, @@ -1343,6 +1420,7 @@ func TestSpinner_paintStop(t *testing.T) { spinnerAtEnd: true, stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", + termMode: termModeTTY, }, want: "\r\033[K\rstop ax \n", }, @@ -1360,6 +1438,7 @@ func TestSpinner_paintStop(t *testing.T) { suffixAutoColon: true, stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", + termMode: termModeTTY, }, want: "\r\033[K\rstop ax \n", }, @@ -1376,6 +1455,7 @@ func TestSpinner_paintStop(t *testing.T) { stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", suffixAutoColon: true, + termMode: termModeTTY, }, want: "\r\033[K\rax stop\n", }, @@ -1392,6 +1472,7 @@ func TestSpinner_paintStop(t *testing.T) { stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", suffixAutoColon: true, + termMode: termModeTTY, }, want: "\r\033[K\rax foo: stop\n", }, @@ -1408,6 +1489,7 @@ func TestSpinner_paintStop(t *testing.T) { stopChar: character{Value: "x", Size: 1}, stopMsg: "", suffixAutoColon: true, + termMode: termModeTTY, }, want: "\r\033[K\rax \n", }, @@ -1424,6 +1506,7 @@ func TestSpinner_paintStop(t *testing.T) { stopColorFn: fmt.Sprintf, stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", + termMode: termModeTTY, }, want: "\r\033[K\r\r\033[?25h\rax stop\n", }, @@ -1440,7 +1523,8 @@ func TestSpinner_paintStop(t *testing.T) { stopColorFn: fmt.Sprintf, stopChar: character{Value: "x", Size: 1}, stopMsg: "stop", - isDumbTerm: true, + // TODO(theckman): verify + termMode: ForceDumbTerminalMode, lastPrintLen: 10, }, want: "\r \rax stop\n", @@ -1456,6 +1540,7 @@ func TestSpinner_paintStop(t *testing.T) { stopFailColorFn: fmt.Sprintf, stopFailChar: character{Value: "y", Size: 1}, stopFailMsg: "stop", + termMode: termModeTTY, }, want: "\r\033[K\ray stop\n", }, @@ -1468,18 +1553,20 @@ func TestSpinner_paintStop(t *testing.T) { suffix: " ", maxWidth: 1, stopFailColorFn: fmt.Sprintf, + termMode: termModeTTY, }, want: "\r\033[K\r", }, { name: "fail_no_char_no_msg_dumb_term", spinner: &Spinner{ - buffer: &bytes.Buffer{}, - mu: &sync.Mutex{}, - prefix: "a", - suffix: " ", - maxWidth: 1, - isDumbTerm: true, + buffer: &bytes.Buffer{}, + mu: &sync.Mutex{}, + prefix: "a", + suffix: " ", + maxWidth: 1, + // TODO(theckman): verify + termMode: ForceDumbTerminalMode, stopFailColorFn: fmt.Sprintf, }, want: "\r\r", @@ -1498,6 +1585,7 @@ func TestSpinner_paintStop(t *testing.T) { stopFailChar: character{Value: "y", Size: 1}, stopFailMsg: "stop", colorAll: true, + termMode: termModeTTY, }, want: "\r\033[K\rfullColor: ay stop\n", }, @@ -1516,6 +1604,7 @@ func TestSpinner_paintStop(t *testing.T) { stopFailMsg: "stop", colorAll: true, spinnerAtEnd: true, + termMode: termModeTTY, }, want: "\r\033[K\rfullColor: stop ay \n", }, @@ -1533,6 +1622,7 @@ func TestSpinner_paintStop(t *testing.T) { stopFailChar: character{Value: "", Size: 0}, stopFailMsg: "stop", colorAll: true, + termMode: termModeTTY, }, want: "\r\033[K\rfullColor: stop\n", }, @@ -1669,6 +1759,7 @@ func TestSpinner_painter(t *testing.T) { doneCh: done, dataUpdateCh: dataUpdate, frequencyUpdateCh: frequencyUpdate, + termMode: termModeTTY, } go spinner.painter(cancel, dataUpdate, pause, done, frequencyUpdate) @@ -1744,8 +1835,7 @@ func TestSpinner_painter(t *testing.T) { doneCh: done, dataUpdateCh: dataUpdate, frequencyUpdateCh: frequencyUpdate, - isNotTTY: true, - isDumbTerm: true, + termMode: ForceDumbTerminalMode | ForceNoTTYMode, } go spinner.painter(cancel, dataUpdate, pause, done, frequencyUpdate)