diff --git a/progress/indicator.go b/progress/indicator.go index 4d98261..dd50f97 100644 --- a/progress/indicator.go +++ b/progress/indicator.go @@ -2,6 +2,7 @@ package progress import ( "strings" + "sync/atomic" "time" "github.com/jedib0t/go-pretty/v6/text" @@ -56,6 +57,91 @@ func IndeterminateIndicatorPacMan(duration time.Duration) IndeterminateIndicator return timedIndeterminateIndicatorGenerator(indeterminateIndicatorPacMan(), duration) } +// IndeterminateIndicatorColoredDominoes simulates a bunch of colored dominoes falling back and +// forth. +func IndeterminateIndicatorColoredDominoes(duration time.Duration, slashColor, backslashColor text.Color) IndeterminateIndicatorGenerator { + baseGen := IndeterminateIndicatorDominoes(duration) + return func(maxLen int) IndeterminateIndicator { + base := baseGen(maxLen) + colored := strings.Builder{} + for _, ch := range base.Text { + switch ch { + case '/': + colored.WriteString(text.Colors{slashColor}.Sprint(string(ch))) + case '\\': + colored.WriteString(text.Colors{backslashColor}.Sprint(string(ch))) + default: + colored.WriteRune(ch) + } + } + return IndeterminateIndicator{ + Position: 0, + Text: colored.String(), + } + } +} + +// IndeterminateIndicatorPacManChomp simulates a Pac-Man character chomping through the progress +// bar back and forth. +func IndeterminateIndicatorPacManChomp(duration time.Duration) IndeterminateIndicatorGenerator { + return timedIndeterminateIndicatorGenerator(indeterminateIndicatorPacManChomp(), duration) +} + +func indeterminateIndicatorPacManChomp() IndeterminateIndicatorGenerator { + var frame int64 + + return func(maxLen int) IndeterminateIndicator { + i := atomic.AddInt64(&frame, 1) + cycle := i / int64(maxLen-1) + pos := int(i % int64(maxLen-1)) + + leftToRight := cycle%2 == 0 + if !leftToRight { + pos = (maxLen - 1) - pos + } + + // Alternate between open and closed mouth + mouthOpen := (i/3)%2 == 0 + pac := "c" + if !leftToRight { + pac = "ɔ" + } + if !mouthOpen { + pac = "●" + } + + trail := make([]string, maxLen) + for j := 0; j < maxLen; j++ { + trail[j] = "·" + } + + for j := 0; j < maxLen; j++ { + if (leftToRight && j < pos) || (!leftToRight && j > pos) { + trail[j] = " " + } + } + + trail[pos] = pac + + var line string + for j := 0; j < maxLen; j++ { + switch { + case j == pos: + line += text.Colors{text.FgHiYellow}.Sprint(trail[j]) + case trail[j] == "·": + line += text.Colors{text.FgWhite}.Sprint(trail[j]) + default: + line += trail[j] + } + } + + return IndeterminateIndicator{ + Position: 0, + Text: line, + } + } +} + func indeterminateIndicatorDominoes() IndeterminateIndicatorGenerator { direction := 1 // positive == left to right; negative == right to left nextPosition := 0 diff --git a/progress/indicator_test.go b/progress/indicator_test.go index 85d6694..5494208 100644 --- a/progress/indicator_test.go +++ b/progress/indicator_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "github.com/jedib0t/go-pretty/v6/text" "github.com/stretchr/testify/assert" ) @@ -58,6 +59,61 @@ func TestIndeterminateIndicatorDominoes(t *testing.T) { } } +func TestIndeterminateIndicatorColoredDominoes(t *testing.T) { + maxLen := 10 + colorize := func(s string) string { + s = strings.ReplaceAll(s, "/", text.FgHiGreen.Sprint("/")) + s = strings.ReplaceAll(s, "\\", text.FgHiBlack.Sprint("\\")) + return s + } + + expectedTexts := []string{ + colorize(`\\\\\\\\\\`), + colorize(`/\\\\\\\\\`), + colorize(`//\\\\\\\\`), + colorize(`///\\\\\\\`), + colorize(`////\\\\\\`), + colorize(`/////\\\\\`), + colorize(`//////\\\\`), + colorize(`///////\\\`), + colorize(`////////\\`), + colorize(`/////////\`), + colorize(`//////////`), + colorize(`/////////\`), + colorize(`////////\\`), + colorize(`///////\\\`), + colorize(`//////\\\\`), + colorize(`/////\\\\\`), + colorize(`////\\\\\\`), + colorize(`///\\\\\\\`), + colorize(`//\\\\\\\\`), + colorize(`/\\\\\\\\\`), + colorize(`\\\\\\\\\\`), + colorize(`/\\\\\\\\\`), + colorize(`//\\\\\\\\`), + colorize(`///\\\\\\\`), + colorize(`////\\\\\\`), + colorize(`/////\\\\\`), + colorize(`//////\\\\`), + colorize(`///////\\\`), + colorize(`////////\\`), + colorize(`/////////\`), + } + + out := strings.Builder{} + f := IndeterminateIndicatorColoredDominoes(time.Millisecond*10, text.FgHiGreen, text.FgHiBlack) + for idx, expectedText := range expectedTexts { + actual := f(maxLen) + assert.Equal(t, 0, actual.Position, fmt.Sprintf("expectedTexts[%d]", idx)) + assert.Equal(t, expectedText, actual.Text, fmt.Sprintf("expectedTexts[%d]", idx)) + out.WriteString(fmt.Sprintf("`%v`,\n", actual.Text)) + time.Sleep(time.Millisecond * 10) + } + if t.Failed() { + fmt.Println(out.String()) + } +} + func TestIndeterminateIndicatorMovingBackAndForth(t *testing.T) { maxLen := 10 indicator := "<=>" @@ -286,3 +342,57 @@ func TestIndeterminateIndicatorPacMan(t *testing.T) { fmt.Println(out.String()) } } + +func TestIndeterminateIndicatorPacManChomp(t *testing.T) { + maxLen := 10 + colorize := func(s string) string { + s = strings.ReplaceAll(s, "·", text.FgWhite.Sprint("·")) + s = strings.ReplaceAll(s, "●", text.FgHiYellow.Sprint("●")) + s = strings.ReplaceAll(s, "ɔ", text.FgHiYellow.Sprint("ɔ")) + s = strings.ReplaceAll(s, "c", text.FgHiYellow.Sprint("c")) + return s + } + + expectedTexts := []string{ + colorize(" c········"), + colorize(" c·······"), + colorize(" ●······"), + colorize(" ●·····"), + colorize(" ●····"), + colorize(" c···"), + colorize(" c··"), + colorize(" c·"), + colorize("·········●"), + colorize("········● "), + colorize("·······● "), + colorize("······ɔ "), + colorize("·····ɔ "), + colorize("····ɔ "), + colorize("···● "), + colorize("··● "), + colorize("·● "), + colorize("c·········"), + colorize(" c········"), + colorize(" c·······"), + colorize(" ●······"), + colorize(" ●·····"), + colorize(" ●····"), + colorize(" c···"), + colorize(" c··"), + colorize(" c·"), + colorize("·········●"), + colorize("········● "), + } + + out := strings.Builder{} + f := IndeterminateIndicatorPacManChomp(time.Millisecond * 10) + for idx, expectedText := range expectedTexts { + actual := f(maxLen) + assert.Equal(t, expectedText, actual.Text, fmt.Sprintf("expectedTexts[%d]", idx)) + out.WriteString(fmt.Sprintf("%#v,\n", actual.Text)) + time.Sleep(time.Millisecond * 10) + } + if t.Failed() { + fmt.Println(out.String()) + } +}