Skip to content

Commit 4a2bf41

Browse files
author
Guillaume Bouvignies
authored
Support for multi-line spinner strings (#146)
1 parent 329c376 commit 4a2bf41

File tree

5 files changed

+107
-2
lines changed

5 files changed

+107
-2
lines changed

_example/main.go

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33

44
import (
55
"log"
6+
"strings"
67
"time"
78

89
"github.com/briandowns/spinner"
@@ -22,6 +23,13 @@ func main() {
2223
s.Suffix = " :appended text" // Append text after the spinner
2324
time.Sleep(4 * time.Second)
2425

26+
s.Suffix = " :appended " + strings.Repeat("very long text ", 20) // Append very long text
27+
time.Sleep(4 * time.Second)
28+
29+
s.Suffix = " :appended multi \nline\nsuffix\ntext" // Append multi line text
30+
time.Sleep(4 * time.Second)
31+
32+
s.Suffix = " :appended text" // Append text after the spinner
2533
s.Prefix = "Colors: "
2634

2735
if err := s.Color("yellow"); err != nil {

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ require (
66
github.com/fatih/color v1.7.0
77
github.com/mattn/go-colorable v0.1.2 // indirect
88
github.com/mattn/go-isatty v0.0.8
9+
golang.org/x/term v0.1.0
910
)

go.sum

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,8 @@ github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx
44
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
55
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
66
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
7-
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
87
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
8+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
9+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
10+
golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw=
11+
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

spinner.go

+39-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"io"
22+
"math"
2223
"os"
2324
"runtime"
2425
"strconv"
@@ -29,6 +30,7 @@ import (
2930

3031
"github.com/fatih/color"
3132
"github.com/mattn/go-isatty"
33+
"golang.org/x/term"
3234
)
3335

3436
// errInvalidColor is returned when attempting to set an invalid color
@@ -439,13 +441,23 @@ func (s *Spinner) erase() {
439441
return
440442
}
441443

444+
numberOfLinesToErase := computeNumberOfLinesNeededToPrintString(s.lastOutputPlain)
445+
442446
// Taken from https://en.wikipedia.org/wiki/ANSI_escape_code:
443447
// \r - Carriage return - Moves the cursor to column zero
444448
// \033[K - Erases part of the line. If n is 0 (or missing), clear from
445449
// cursor to the end of the line. If n is 1, clear from cursor to beginning
446450
// of the line. If n is 2, clear entire line. Cursor position does not
447451
// change.
448-
fmt.Fprintf(s.Writer, "\r\033[K")
452+
// \033[F - Go to the beginning of previous line
453+
eraseCodeString := strings.Builder{}
454+
// current position is at the end of the last printed line. Start by erasing current line
455+
eraseCodeString.WriteString("\r\033[K") // start by erasing current line
456+
for i := 1; i < numberOfLinesToErase; i++ {
457+
// For each additional lines, go up one line and erase it.
458+
eraseCodeString.WriteString("\033[F\033[K")
459+
}
460+
fmt.Fprintf(s.Writer, eraseCodeString.String())
449461
s.lastOutputPlain = ""
450462
}
451463

@@ -473,3 +485,29 @@ func GenerateNumberSequence(length int) []string {
473485
func isRunningInTerminal() bool {
474486
return isatty.IsTerminal(os.Stdout.Fd())
475487
}
488+
489+
func computeNumberOfLinesNeededToPrintString(linePrinted string) int {
490+
terminalWidth := math.MaxInt // assume infinity by default to keep behaviour consistent with what we had before
491+
if term.IsTerminal(0) {
492+
if width, _, err := term.GetSize(0); err == nil {
493+
terminalWidth = width
494+
}
495+
}
496+
return computeNumberOfLinesNeededToPrintStringInternal(linePrinted, terminalWidth)
497+
}
498+
499+
func computeNumberOfLinesNeededToPrintStringInternal(linePrinted string, maxLineWidth int) int {
500+
if linePrinted == "" {
501+
// empty string will necessarily take one line
502+
return 1
503+
}
504+
idxOfNewline := strings.Index(linePrinted, "\n")
505+
if idxOfNewline < 0 {
506+
// we use utf8.RunCountInString() in place of len() because the string contains "complex" unicode chars that
507+
// might be represented by multiple individual bytes (typically spinner char)
508+
return int(math.Ceil(float64(utf8.RuneCountInString(linePrinted)) / float64(maxLineWidth)))
509+
} else {
510+
return computeNumberOfLinesNeededToPrintStringInternal(linePrinted[:idxOfNewline], maxLineWidth) +
511+
computeNumberOfLinesNeededToPrintStringInternal(linePrinted[idxOfNewline+1:], maxLineWidth)
512+
}
513+
}

spinner_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"io/ioutil"
2121
"os"
2222
"reflect"
23+
"strings"
2324
"sync"
2425
"testing"
2526
"time"
@@ -280,6 +281,60 @@ func TestWithWriter(t *testing.T) {
280281
_ = s
281282
}
282283

284+
func TestComputeNumberOfLinesNeededToPrintStringInternal_SingleLine(t *testing.T) {
285+
line := "Hello world"
286+
result := computeNumberOfLinesNeededToPrintStringInternal(line, 50)
287+
expectedResult := 1
288+
if result != expectedResult {
289+
t.Errorf("Line '%s' shoud be printed on '%d' line, got '%d'", line, expectedResult, result)
290+
}
291+
}
292+
293+
func TestComputeNumberOfLinesNeededToPrintStringInternal_MultiLine(t *testing.T) {
294+
line := "Hello\n world"
295+
result := computeNumberOfLinesNeededToPrintStringInternal(line, 50)
296+
expectedResult := 2
297+
if result != expectedResult {
298+
t.Errorf("Line '%s' shoud be printed on '%d' lines, got '%d'", line, expectedResult, result)
299+
}
300+
}
301+
302+
func TestComputeNumberOfLinesPrinted_LongString(t *testing.T) {
303+
line := "Hello world! I am a super long string that will be printed in 2 lines"
304+
result := computeNumberOfLinesNeededToPrintStringInternal(line, 50)
305+
expectedResult := 2
306+
if result != expectedResult {
307+
t.Errorf("Line '%s' shoud be printed on '%d' lines, got '%d'", line, expectedResult, result)
308+
}
309+
}
310+
311+
func TestComputeNumberOfLinesNeededToPrintStringInternal_LongStringWithNewlines(t *testing.T) {
312+
line := "Hello world!\nI am a super long string that will be printed in 2 lines.\nAnother new line"
313+
result := computeNumberOfLinesNeededToPrintStringInternal(line, 50)
314+
expectedResult := 4
315+
if result != expectedResult {
316+
t.Errorf("Line '%s' shoud be printed on '%d' lines, got '%d'", line, expectedResult, result)
317+
}
318+
}
319+
320+
func TestComputeNumberOfLinesNeededToPrintStringInternal_NewlineCharAtTheEnd(t *testing.T) {
321+
line := "Hello world!\n"
322+
result := computeNumberOfLinesNeededToPrintStringInternal(line, 50)
323+
expectedResult := 2
324+
if result != expectedResult {
325+
t.Errorf("Line '%s' shoud be printed on '%d' lines, got '%d'", line, expectedResult, result)
326+
}
327+
}
328+
329+
func TestComputeNumberOfLinesNeededToPrintStringInternal_StringExactlyTheSizeOfTheScreen(t *testing.T) {
330+
line := strings.Repeat("a", 50)
331+
result := computeNumberOfLinesNeededToPrintStringInternal(line, 50)
332+
expectedResult := 1
333+
if result != expectedResult {
334+
t.Errorf("Line '%s' shoud be printed on '%d' lines, got '%d'", line, expectedResult, result)
335+
}
336+
}
337+
283338
/*
284339
Benchmarks
285340
*/

0 commit comments

Comments
 (0)