Skip to content

Commit

Permalink
Merge pull request #48 from theckman/not_a_tty
Browse files Browse the repository at this point in the history
Add support for non-TTY output targets
  • Loading branch information
theckman committed Dec 12, 2021
2 parents 88324b5 + 112b4fc commit 4b45330
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 80 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,19 @@ 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.

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.

This should result in human-readable output without any changes needed by
consumers, even when the system is writing to a non-TTY destination.

## Usage
```
go get github.com/theckman/yacspin
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ require (
github.com/fatih/color v1.13.0
github.com/google/go-cmp v0.5.6
github.com/mattn/go-colorable v0.1.12
github.com/mattn/go-isatty v0.0.14
github.com/mattn/go-runewidth v0.0.13
)

require (
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
Expand All @@ -15,7 +14,6 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand Down
57 changes: 45 additions & 12 deletions spinner.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@ import (
"errors"
"fmt"
"io"
"math"
"os"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth"
)

Expand Down Expand Up @@ -182,6 +184,12 @@ type Config struct {
// StopFailColors are the colors used for the StopFail() printed line. This
// respects the ColorAll field.
StopFailColors []string

// 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.
NotTTY bool
}

// Spinner is a type representing an animated CLi terminal spinner. It's
Expand All @@ -196,6 +204,7 @@ type Spinner struct {
cursorHidden bool
suffixAutoColon bool
isDumbTerm bool
isNotTTY bool
spinnerAtEnd bool

status *uint32
Expand Down Expand Up @@ -236,12 +245,17 @@ const (
statusUnpausing
)

// New creates a new unstarted spinner.
// 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 !isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()) {
cfg.NotTTY = true
}

s := &Spinner{
buffer: bytes.NewBuffer(make([]byte, 2048)),
mu: &sync.Mutex{},
Expand Down Expand Up @@ -279,6 +293,11 @@ 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 cfg.Writer == nil {
cfg.Writer = colorable.NewColorableStdout()
}
Expand Down Expand Up @@ -394,6 +413,11 @@ 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
Expand Down Expand Up @@ -568,14 +592,15 @@ func (s *Spinner) painter(cancel, dataUpdate, pause <-chan struct{}, done chan<-
case <-timer.C:
lastTick = time.Now()

s.paintUpdate(timer, false)
s.paintUpdate(timer, true)

case <-pause:
<-s.unpauseCh
close(s.unpausedCh)

case <-dataUpdate:
s.paintUpdate(timer, true)
// if this is not a TTY: animate the spinner on the data update
s.paintUpdate(timer, s.isNotTTY)

case frequency := <-frequencyUpdate:
handleFrequencyUpdate(frequency, timer, lastTick)
Expand All @@ -592,7 +617,7 @@ func (s *Spinner) painter(cancel, dataUpdate, pause <-chan struct{}, done chan<-
}
}

func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
func (s *Spinner) paintUpdate(timer *time.Timer, animate bool) {
s.mu.Lock()

p := s.prefix
Expand All @@ -603,7 +628,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
d := s.frequency
index := s.index

if !dataUpdate {
if animate {
s.index++

if s.index == len(s.chars) {
Expand Down Expand Up @@ -635,15 +660,15 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
}
}

if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, cFn); err != nil {
if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, false, s.isNotTTY, 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, s.spinnerAtEnd, false, fmt.Sprintf)
n, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, false, s.isNotTTY, fmt.Sprintf)
if err != nil {
panic(fmt.Sprintf("failed to paint line: %v", err))
}
Expand All @@ -657,7 +682,7 @@ func (s *Spinner) paintUpdate(timer *time.Timer, dataUpdate bool) {
}
}

if !dataUpdate {
if animate {
timer.Reset(d)
}
}
Expand Down Expand Up @@ -700,7 +725,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, cFn); err != nil {
if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, s.colorAll, s.spinnerAtEnd, true, s.isNotTTY, cFn); err != nil {
panic(fmt.Sprintf("failed to paint line: %v", err))
}
}
Expand All @@ -710,7 +735,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, fmt.Sprintf); err != nil {
if _, err := paint(s.buffer, mw, c, p, m, suf, s.suffixAutoColon, false, s.spinnerAtEnd, true, s.isNotTTY, fmt.Sprintf); err != nil {
panic(fmt.Sprintf("failed to paint line: %v", err))
}
}
Expand All @@ -733,6 +758,10 @@ func erase(w io.Writer) error {

// eraseDumbTerm clears the line on dumb terminals
func (s *Spinner) eraseDumbTerm(w io.Writer) error {
if s.isNotTTY {
return nil
}

clear := "\r" + strings.Repeat(" ", s.lastPrintLen) + "\r"

_, err := fmt.Fprint(w, clear)
Expand All @@ -758,7 +787,7 @@ 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, spinnerAtEnd, finalPaint bool, colorFn func(format string, a ...interface{}) string) (int, error) {
func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix string, suffixAutoColon, colorAll, spinnerAtEnd, finalPaint, notTTY bool, colorFn func(format string, a ...interface{}) string) (int, error) {
var output string

switch char.Size {
Expand Down Expand Up @@ -797,7 +826,7 @@ func paint(w io.Writer, maxWidth int, char character, prefix, message, suffix st
output = fmt.Sprintf("%s%s%s%s", prefix, colorFn(c), suffix, message)
}

if finalPaint {
if finalPaint || notTTY {
output += "\n"
}

Expand All @@ -810,6 +839,10 @@ func (s *Spinner) Frequency(d time.Duration) error {
return errors.New("duration must be greater than 0")
}

if s.isNotTTY {
return nil
}

s.mu.Lock()
defer s.mu.Unlock()

Expand Down
Loading

0 comments on commit 4b45330

Please sign in to comment.