Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ func main() {
Rendering based on builtin [text/template](https://pkg.go.dev/text/template) package. You can use existing pb's elements or create you own.

All available elements are described in the [element.go](v3/element.go) file.
The `bar` element accepts five standard parts: left border, fill, current, empty, and right border. It also accepts optional sixth and seventh parts for the empty left border and finished right border.

Set `UNICODE_PROGRESS_BAR=true` to use the built-in Unicode progress bar glyphs for bars that use the default bar elements when the active terminal font supports them.

#### All in one example:

Expand Down
99 changes: 80 additions & 19 deletions v3/element.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"math"
"os"
"strings"
"sync"
"time"
Expand All @@ -15,7 +16,32 @@ const (
)

var (
defaultBarEls = [5]string{"[", "-", ">", "_", "]"}
asciiBarEls = [barElementCount]string{"[", "-", ">", "_", "]", "", ""}
firaBarEls = [barElementCount]string{"", "", "", "", "", "", ""}
defaultBarEls = asciiBarEls
)

func init() {
configureDefaultBarEls()
}

func configureDefaultBarEls() {
defaultBarEls = asciiBarEls
if os.Getenv(unicodeProgressBarEnv) == "true" {
// Only "true" is accepted; values like "1" are ignored to match the Fira Code README spec.
defaultBarEls = firaBarEls
}
}

const (
barLeft = iota
barFill
barCurrent
barEmpty
barRight
barLeftEmpty
barRightFinished
barElementCount
)

// Element is an interface for bar elements
Expand Down Expand Up @@ -122,9 +148,10 @@ const (
)

type bar struct {
eb [5][]byte // elements in bytes
cc [5]int // cell counts
buf *bytes.Buffer
eb [barElementCount][]byte // elements in bytes
cc [barElementCount]int // cell counts
enabled [barElementCount]bool
buf *bytes.Buffer
}

func (p *bar) write(state *State, eln, width int) int {
Expand All @@ -149,7 +176,29 @@ func getProgressObj(state *State, args ...string) (p *bar) {
}
argsH := argsHelper(args)
for i := range p.eb {
arg := argsH.getNotEmptyOr(i, defaultBarEls[i])
enabled := true

var arg string
switch {
case i < barLeftEmpty:
arg = argsH.getNotEmptyOr(i, defaultBarEls[i])
case len(args) == 0:
arg = defaultBarEls[i]
default:
arg = argsH.getOr(i, "")
}

if i >= barLeftEmpty {
enabled = arg != ""
}
if p.enabled[i] != enabled {
p.enabled[i] = enabled
p.eb[i] = nil
p.cc[i] = 0
}
if !enabled {
continue
}
if string(p.eb[i]) != arg {
p.cc[i] = CellCount(arg)
p.eb[i] = []byte(arg)
Expand All @@ -163,7 +212,11 @@ func getProgressObj(state *State, args ...string) (p *bar) {
}

// ElementBar make progress bar view [-->__]
// Optionally can take up to 5 string arguments. Defaults is "[", "-", ">", "_", "]"
// Optionally can take up to 7 string arguments.
// Defaults is "[", "-", ">", "_", "]".
// First five arguments are left border, fill, current, empty, and right border.
// Sixth argument overrides the left border when progress is empty.
// Seventh argument overrides the right border when progress is finished.
// In template use as follows: {{bar . }} or {{bar . "<" "oOo" "|" "~" ">"}}
// Color args: {{bar . (red "[") (green "-") ...
var ElementBar ElementFunc = func(state *State, args ...string) string {
Expand All @@ -190,20 +243,28 @@ var ElementBar ElementFunc = func(state *State, args ...string) string {
widthLeft = 30
}

leftEl, rightEl := barLeft, barRight
if (total <= 0 || value == 0) && p.enabled[barLeftEmpty] {
leftEl = barLeftEmpty
}
if total == value && state.IsFinished() && p.enabled[barRightFinished] {
rightEl = barRightFinished
}

// write left border
if p.cc[0] < widthLeft {
widthLeft -= p.write(state, 0, p.cc[0])
if p.cc[leftEl] < widthLeft {
widthLeft -= p.write(state, leftEl, p.cc[leftEl])
} else {
p.write(state, 0, widthLeft)
p.write(state, leftEl, widthLeft)
return p.buf.String()
}

// check right border size
if p.cc[4] < widthLeft {
if p.cc[rightEl] < widthLeft {
// write later
widthLeft -= p.cc[4]
widthLeft -= p.cc[rightEl]
} else {
p.write(state, 4, widthLeft)
p.write(state, rightEl, widthLeft)
return p.buf.String()
}

Expand All @@ -216,18 +277,18 @@ var ElementBar ElementFunc = func(state *State, args ...string) string {

// write bar
if total == value && state.IsFinished() {
widthLeft -= p.write(state, 1, curCount)
} else if toWrite := curCount - p.cc[2]; toWrite > 0 {
widthLeft -= p.write(state, 1, toWrite)
widthLeft -= p.write(state, 2, p.cc[2])
widthLeft -= p.write(state, barFill, curCount)
} else if toWrite := curCount - p.cc[barCurrent]; toWrite > 0 {
widthLeft -= p.write(state, barFill, toWrite)
widthLeft -= p.write(state, barCurrent, p.cc[barCurrent])
} else if curCount > 0 {
widthLeft -= p.write(state, 2, curCount)
widthLeft -= p.write(state, barCurrent, curCount)
}
if widthLeft > 0 {
widthLeft -= p.write(state, 3, widthLeft)
widthLeft -= p.write(state, barEmpty, widthLeft)
}
// write right border
p.write(state, 4, p.cc[4])
p.write(state, rightEl, p.cc[rightEl])
// cut result and return string
return p.buf.String()
}
Expand Down
11 changes: 11 additions & 0 deletions v3/element_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ func TestElementBar(t *testing.T) {
}
}

func TestElementBarBorderOverrides(t *testing.T) {
format := []string{"L", "=", "=", ".", "R", "l", "F"}

testElementBarString(t, testState(100, 0, 7, false, true), ElementBar, "l.....R", format...)
testElementBarString(t, testState(100, 50, 7, false, true), ElementBar, "L===..R", format...)

st := testState(100, 100, 7, false, true)
st.finished = true
testElementBarString(t, st, ElementBar, "L=====F", format...)
}

func TestElementSpeed(t *testing.T) {
var state = testState(1000, 0, 0, false)
state.time = time.Now()
Expand Down
2 changes: 2 additions & 0 deletions v3/pb.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import (
// Version of ProgressBar library
const Version = "3.0.8"

const unicodeProgressBarEnv = "UNICODE_PROGRESS_BAR"

type key int

const (
Expand Down
99 changes: 99 additions & 0 deletions v3/pb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -110,6 +111,8 @@ func TestAddTotal(t *testing.T) {
}

func TestPBTemplate(t *testing.T) {
defer setUnicodeProgressBarEnv("false")()

bar := new(ProgressBar)
result := bar.SetTotal(100).SetCurrent(50).SetWidth(40).String()
expected := "50 / 100 [------->________] 50.00% ? p/s"
Expand Down Expand Up @@ -148,6 +151,78 @@ func TestPBTemplate(t *testing.T) {
}
}

func TestUnicodeProgressBarEnvUsesFiraDefaultBarElements(t *testing.T) {
defer setUnicodeProgressBarEnv("true")()

for name, tmpl := range map[string]ProgressBarTemplate{
"Full": Full,
"Default": Default,
"Simple": Simple,
"Custom": `{{bar . }}`,
} {
result := tmpl.New(100).SetCurrent(0).SetWidth(60).String()
if !strings.Contains(result, "") {
t.Errorf("%s must use fira empty left border: %q", name, result)
}
if !strings.Contains(result, "") {
t.Errorf("%s must use fira right border: %q", name, result)
}
}
}

func TestUnicodeProgressBarEnvIgnoresOne(t *testing.T) {
defer setUnicodeProgressBarEnv("1")()

result := ProgressBarTemplate(`{{bar . }}`).New(100).SetCurrent(0).SetWidth(10).String()
if result != "[________]" {
t.Errorf("UNICODE_PROGRESS_BAR=1 must keep ascii defaults: %q", result)
}
}

func TestUnicodeProgressBarEnvDoesNotOverrideExplicitBarArgs(t *testing.T) {
defer setUnicodeProgressBarEnv("true")()

for _, test := range []struct {
name string
template ProgressBarTemplate
current int64
finished bool
expected string
}{
{
name: "five args empty",
template: `{{bar . "<" "=" ">" "." ">"}}`,
current: 0,
expected: "<........>",
},
{
name: "five args finished",
template: `{{bar . "<" "=" ">" "." ">"}}`,
current: 100,
finished: true,
expected: "<========>",
},
{
name: "explicit empty extras",
template: `{{bar . "<" "=" ">" "." ">" "" ""}}`,
current: 0,
expected: "<........>",
},
} {
t.Run(test.name, func(t *testing.T) {
bar := New64(100).SetTemplate(test.template).SetCurrent(test.current).SetWidth(10)
if test.finished {
bar.Finish()
}

result := bar.String()
if result != test.expected {
t.Errorf("explicit bar args must be used: %q; want %q", result, test.expected)
}
})
}
}

func TestPBStartFinish(t *testing.T) {
bar := ProgressBarTemplate(`{{counters . }}`).New(0)
for i := int64(0); i < 2; i++ {
Expand Down Expand Up @@ -232,6 +307,30 @@ func TestPBFlags(t *testing.T) {
}
}

func setEnv(key, value string) func() {
old, ok := os.LookupEnv(key)
os.Setenv(key, value)

return func() {
if ok {
os.Setenv(key, old)
return
}
os.Unsetenv(key)
}
}

func setUnicodeProgressBarEnv(value string) func() {
restoreEnv := setEnv(unicodeProgressBarEnv, value)
oldDefaultBarEls := defaultBarEls
configureDefaultBarEls()

return func() {
defaultBarEls = oldDefaultBarEls
restoreEnv()
}
}

func BenchmarkRender(b *testing.B) {
var formats = []string{
string(Simple),
Expand Down
Loading