From 553200356cbd44a1e3ce149eb21d597179b8414c Mon Sep 17 00:00:00 2001 From: Zack Scholl Date: Tue, 30 Jun 2026 11:18:11 -0700 Subject: [PATCH] fix: width issues --- go.mod | 8 +-- go.sum | 13 ++--- progressbar.go | 130 ++++++++++++++++++++++++++------------------ progressbar_test.go | 67 ++++++++++++++++++++--- 4 files changed, 144 insertions(+), 74 deletions(-) diff --git a/go.mod b/go.mod index dd2eaff..d348913 100644 --- a/go.mod +++ b/go.mod @@ -6,16 +6,16 @@ require ( github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db github.com/rivo/uniseg v0.4.7 github.com/stretchr/testify v1.9.0 - golang.org/x/term v0.28.0 + golang.org/x/term v0.44.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.29.0 // indirect + golang.org/x/sys v0.46.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -go 1.22 +go 1.25.0 diff --git a/go.sum b/go.sum index cdb3e25..cfeb592 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= +github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= @@ -17,11 +17,10 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= -golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/progressbar.go b/progressbar.go index f6c95ad..3715861 100644 --- a/progressbar.go +++ b/progressbar.go @@ -1123,6 +1123,74 @@ func getStringWidth(c config, str string) int { return stringWidth } +func renderDeterminateProgressBar(c config, s *state, bar, stats, leftBrac, rightBrac string) string { + if rightBrac == "" { + str := fmt.Sprintf("%4d%% %s %s", + s.currentPercent, + bar, + stats) + if (s.currentPercent == 100 && c.showElapsedTimeOnFinish) || c.elapsedTime { + str = fmt.Sprintf("%s [%s]", str, leftBrac) + } + + if c.showDescriptionAtLineEnd { + return fmt.Sprintf("\r%s %s ", str, c.description) + } + return fmt.Sprintf("\r%s%s ", c.description, str) + } + + if s.currentPercent == 100 { + str := fmt.Sprintf("%4d%% %s %s", + s.currentPercent, + bar, + stats) + + if c.showElapsedTimeOnFinish { + str = fmt.Sprintf("%s [%s]", str, leftBrac) + } + + if c.showDescriptionAtLineEnd { + return fmt.Sprintf("\r%s %s", str, c.description) + } + return fmt.Sprintf("\r%s%s", c.description, str) + } + + str := fmt.Sprintf("%4d%% %s %s [%s:%s]", + s.currentPercent, + bar, + stats, + leftBrac, + rightBrac) + + if c.showDescriptionAtLineEnd { + return fmt.Sprintf("\r%s %s", str, c.description) + } + return fmt.Sprintf("\r%s%s", c.description, str) +} + +func fitProgressBarWidth(c config, s *state, barStart, barEnd, stats, leftBrac, rightBrac string) int { + if c.fullWidth || c.ignoreLength || c.width <= 0 { + return c.width + } + + terminalWidth, err := termWidth(c.writer) + if err != nil || terminalWidth <= 0 { + return c.width + } + + bar := barStart + strings.Repeat(c.theme.SaucerPadding, c.width) + barEnd + lineWidth := getStringWidth(c, renderDeterminateProgressBar(c, s, bar, stats, leftBrac, rightBrac)) + if lineWidth <= terminalWidth { + return c.width + } + + fitWidth := c.width - (lineWidth - terminalWidth) + if fitWidth < 0 { + return 0 + } + return fitWidth +} + func renderProgressBar(c config, s *state) (int, error) { var sb strings.Builder @@ -1252,6 +1320,10 @@ func renderProgressBar(c config, s *state) (int, error) { if (s.currentSaucerSize > 0 || s.currentPercent > 0) && c.theme.BarStartFilled != "" { barStart = c.theme.BarStartFilled } + c.width = fitProgressBarWidth(c, s, barStart, barEnd, sb.String(), leftBrac, rightBrac) + if !c.ignoreLength { + s.currentSaucerSize = int(float64(s.currentPercent) / 100.0 * float64(c.width)) + } if s.currentSaucerSize > 0 { if c.ignoreLength { saucer = strings.Repeat(c.theme.SaucerPadding, s.currentSaucerSize-1) @@ -1335,61 +1407,11 @@ func renderProgressBar(c config, s *state) (int, error) { } } } else if rightBrac == "" { - str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", - s.currentPercent, - barStart, - saucer, - saucerHead, - strings.Repeat(c.theme.SaucerPadding, repeatAmount), - barEnd, - sb.String()) - if (s.currentPercent == 100 && c.showElapsedTimeOnFinish) || c.elapsedTime { - str = fmt.Sprintf("%s [%s]", str, leftBrac) - } - - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s ", str, c.description) - } else { - str = fmt.Sprintf("\r%s%s ", c.description, str) - } + bar := barStart + saucer + saucerHead + strings.Repeat(c.theme.SaucerPadding, repeatAmount) + barEnd + str = renderDeterminateProgressBar(c, s, bar, sb.String(), leftBrac, rightBrac) } else { - if s.currentPercent == 100 { - str = fmt.Sprintf("%4d%% %s%s%s%s%s %s", - s.currentPercent, - barStart, - saucer, - saucerHead, - strings.Repeat(c.theme.SaucerPadding, repeatAmount), - barEnd, - sb.String()) - - if c.showElapsedTimeOnFinish { - str = fmt.Sprintf("%s [%s]", str, leftBrac) - } - - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s", str, c.description) - } else { - str = fmt.Sprintf("\r%s%s", c.description, str) - } - } else { - str = fmt.Sprintf("%4d%% %s%s%s%s%s %s [%s:%s]", - s.currentPercent, - barStart, - saucer, - saucerHead, - strings.Repeat(c.theme.SaucerPadding, repeatAmount), - barEnd, - sb.String(), - leftBrac, - rightBrac) - - if c.showDescriptionAtLineEnd { - str = fmt.Sprintf("\r%s %s", str, c.description) - } else { - str = fmt.Sprintf("\r%s%s", c.description, str) - } - } + bar := barStart + saucer + saucerHead + strings.Repeat(c.theme.SaucerPadding, repeatAmount) + barEnd + str = renderDeterminateProgressBar(c, s, bar, sb.String(), leftBrac, rightBrac) } if c.colorCodes { diff --git a/progressbar_test.go b/progressbar_test.go index 34ee7b4..96c8fe8 100644 --- a/progressbar_test.go +++ b/progressbar_test.go @@ -9,6 +9,7 @@ import ( "fmt" "io" "log" + "net" "net/http" "os" "strings" @@ -1000,6 +1001,30 @@ func TestOptionFullWidth(t *testing.T) { } } +func TestFixedWidthBarFitsTerminal(t *testing.T) { + oldTermWidth := termWidth + termWidth = func(w io.Writer) (int, error) { + return 50, nil + } + defer func() { + termWidth = oldTermWidth + }() + + buf := strings.Builder{} + bar := NewOptions( + 100, + OptionSetWriter(&buf), + OptionSetDescription("really-long-file-name.bin "), + OptionSetWidth(20), + OptionShowCount(), + OptionSetPredictTime(false), + ) + bar.Add(1) + + assert.LessOrEqual(t, getStringWidth(bar.config, bar.String()), 50) + assert.NotContains(t, bar.String(), "| |") +} + func TestHumanizeBytesSI(t *testing.T) { amount, suffix := humanizeBytes(float64(12.34)*1000*1000, false) assert.Equal(t, "12 MB", fmt.Sprintf("%s%s", amount, suffix)) @@ -1142,18 +1167,45 @@ func TestOptionShowTotalTrueIndeterminate(t *testing.T) { } } +func freeTestHTTPAddr(t *testing.T) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("listen for free port: %v", err) + } + defer listener.Close() + + return listener.Addr().String() +} + +func getHTTPWithRetry(t *testing.T, url string) *http.Response { + t.Helper() + + var lastErr error + deadline := time.Now().Add(time.Second) + for time.Now().Before(deadline) { + resp, err := http.Get(url) + if err == nil { + return resp + } + lastErr = err + time.Sleep(10 * time.Millisecond) + } + + t.Fatalf("GET %s: %v", url, lastErr) + return nil +} + func TestStartHTTPServer(t *testing.T) { bar := Default(10, "test") bar.Add(1) - hostPort := "localhost:9696" + hostPort := freeTestHTTPAddr(t) svr := bar.StartHTTPServer(hostPort) // check plain text - resp, err := http.Get(fmt.Sprintf("http://%s/desc", hostPort)) - if err != nil { - t.Error(err) - } + resp := getHTTPWithRetry(t, fmt.Sprintf("http://%s/desc", hostPort)) got, err := io.ReadAll(resp.Body) if err != nil { t.Error(err) @@ -1163,10 +1215,7 @@ func TestStartHTTPServer(t *testing.T) { } // check json - resp, err = http.Get(fmt.Sprintf("http://%s/state", hostPort)) - if err != nil { - t.Error(err) - } + resp = getHTTPWithRetry(t, fmt.Sprintf("http://%s/state", hostPort)) got, err = io.ReadAll(resp.Body) if err != nil { t.Error(err)