progressbar
progressbarGolang
一个非常简单的线程安全进度条,应该可以在每个操作系统上正常工作。作者需要一个特殊进度条,而我尝试的所有东西都有问题,所以作者又做了一个。为了与操作系统无关,作者不打算支持多线输出。
github的地址在这里。
安装
progressbar
go get -u github.com/schollz/progressbar/v3
用法
用法非常的多,这里有几个例子。
基本用法
bar := progressbar.Default(100)
for i := 0; i < 100; i++ {
bar.Add(1)
time.Sleep(40 * time.Millisecond)
}
看起来像:
输入输出操作
progressbaio.Writerio.Reader
req, _ := http.NewRequest("GET", "https://dl.google.com/go/go1.14.2.src.tar.gz", nil)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
f, _ := os.OpenFile("go1.14.2.src.tar.gz", os.O_CREATE|os.O_WRONLY, 0644)
defer f.Close()
bar := progressbar.DefaultBytes(
resp.ContentLength,
"downloading",
)
io.Copy(io.MultiWriter(f, bar), resp.Body)
长度未知的进度条
长度未知的进度条是微调器。任何长度为 -1 的条形都会自动将其转换为具有可自定义微调器类型的微调器。例如,可以运行上面的代码并将其设置resp.ContentLength为-1.
看起来像:
定制
您可以进行很多自定义 - 更改作者、颜色、宽度、描述、主题等。查看所有选项。
bar := progressbar.NewOptions(1000,
progressbar.OptionSetWriter(ansi.NewAnsiStdout()),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowBytes(true),
progressbar.OptionSetWidth(15),
progressbar.OptionSetDescription("[cyan][1/3][reset] Writing moshable file..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]=[reset]",
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}))
for i := 0; i < 1000; i++ {
bar.Add(1)
time.Sleep(5 * time.Millisecond)
}
代码分析
我们来看看是如何渲染出来的:
func (p *ProgressBar) State() State {
p.lock.Lock()
defer p.lock.Unlock()
s := State{}
s.CurrentPercent = float64(p.state.currentNum) / float64(p.config.max)
s.CurrentBytes = p.state.currentBytes
s.SecondsSince = time.Since(p.state.startTime).Seconds()
if p.state.currentNum > 0 {
s.SecondsLeft = s.SecondsSince / float64(p.state.currentNum) * (float64(p.config.max) - float64(p.state.currentNum))
}
s.KBsPerSecond = float64(p.state.currentBytes) / 1024.0 / s.SecondsSince
return s
}
// regex matching ansi escape codes
var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*[a-zA-Z]`)
func getStringWidth(c config, str string, colorize bool) int {
if c.colorCodes {
// convert any color codes in the progress bar into the respective ANSI codes
str = colorstring.Color(str)
}
// the width of the string, if printed to the console
// does not include the carriage return character
cleanString := strings.Replace(str, "\r", "", -1)
if c.colorCodes {
// the ANSI codes for the colors do not take up space in the console output,
// so they do not count towards the output string width
cleanString = ansiRegex.ReplaceAllString(cleanString, "")
}
// get the amount of runes in the string instead of the
// character count of the string, as some runes span multiple characters.
// see https://stackoverflow.com/a/12668840/2733724
stringWidth := runewidth.StringWidth(cleanString)
return stringWidth
}
func renderProgressBar(c config, s *state) (int, error) {
leftBrac := ""
rightBrac := ""
saucer := ""
saucerHead := ""
bytesString := ""
str := ""
averageRate := average(s.counterLastTenRates)
if len(s.counterLastTenRates) == 0 || s.finished {
// if no average samples, or if finished,
// then average rate should be the total rate
if t := time.Since(s.startTime).Seconds(); t > 0 {
averageRate = s.currentBytes / t
} else {
averageRate = 0
}
}
// show iteration count in "current/total" iterations format
if c.showIterationsCount {
if bytesString == "" {
bytesString += "("
} else {
bytesString += ", "
}
if !c.ignoreLength {
if c.showBytes {
currentHumanize, currentSuffix := humanizeBytes(s.currentBytes)
if currentSuffix == c.maxHumanizedSuffix {
bytesString += fmt.Sprintf("%s/%s%s", currentHumanize, c.maxHumanized, c.maxHumanizedSuffix)
} else {
bytesString += fmt.Sprintf("%s%s/%s%s", currentHumanize, currentSuffix, c.maxHumanized, c.maxHumanizedSuffix)
}
} else {
bytesString += fmt.Sprintf("%.0f/%d", s.currentBytes, c.max)
}
} else {
if c.showBytes {
currentHumanize, currentSuffix := humanizeBytes(s.currentBytes)
bytesString += fmt.Sprintf("%s%s", currentHumanize, currentSuffix)
} else {
bytesString += fmt.Sprintf("%.0f/%s", s.currentBytes, "-")
}
}
}
// show rolling average rate
if c.showBytes && averageRate > 0 && !math.IsInf(averageRate, 1) {
if bytesString == "" {
bytesString += "("
} else {
bytesString += ", "
}
currentHumanize, currentSuffix := humanizeBytes(averageRate)
bytesString += fmt.Sprintf("%s%s/s", currentHumanize, currentSuffix)
}
// show iterations rate
if c.showIterationsPerSecond {
if bytesString == "" {
bytesString += "("
} else {
bytesString += ", "
}
if averageRate > 1 {
bytesString += fmt.Sprintf("%0.0f %s/s", averageRate, c.iterationString)
} else {
bytesString += fmt.Sprintf("%0.0f %s/min", 60*averageRate, c.iterationString)
}
}
if bytesString != "" {
bytesString += ")"
}
// show time prediction in "current/total" seconds format
switch {
case c.predictTime:
rightBracNum := (time.Duration((1/averageRate)*(float64(c.max)-float64(s.currentNum))) * time.Second)
if rightBracNum.Seconds() < 0 {
rightBracNum = 0 * time.Second
}
rightBrac = rightBracNum.String()
fallthrough
case c.elapsedTime:
leftBrac = (time.Duration(time.Since(s.startTime).Seconds()) * time.Second).String()
}
if c.fullWidth && !c.ignoreLength {
width, _, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil {
width, _, err = term.GetSize(int(os.Stderr.Fd()))
if err != nil {
width = 80
}
}
c.width = width - getStringWidth(c, c.description, true) - 14 - len(bytesString) - len(leftBrac) - len(rightBrac)
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)
} else {
saucer = strings.Repeat(c.theme.Saucer, s.currentSaucerSize-1)
}
// Check if an alternate saucer head is set for animation
if c.theme.AltSaucerHead != "" && s.isAltSaucerHead {
saucerHead = c.theme.AltSaucerHead
s.isAltSaucerHead = false
} else if c.theme.SaucerHead == "" || s.currentSaucerSize == c.width {
// use the saucer for the saucer head if it hasn't been set
// to preserve backwards compatibility
saucerHead = c.theme.Saucer
} else {
saucerHead = c.theme.SaucerHead
s.isAltSaucerHead = true
}
saucer += saucerHead
}
/*
Progress Bar format
Description % |------ | (kb/s) (iteration count) (iteration rate) (predict time)
*/
repeatAmount := c.width - s.currentSaucerSize
if repeatAmount < 0 {
repeatAmount = 0
}
if c.ignoreLength {
if c.elapsedTime {
str = fmt.Sprintf("\r%s %s %s [%s] ",
spinners[c.spinnerType][int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(spinners[c.spinnerType])))))],
c.description,
bytesString,
leftBrac,
)
} else {
str = fmt.Sprintf("\r%s %s %s ",
spinners[c.spinnerType][int(math.Round(math.Mod(float64(time.Since(s.startTime).Milliseconds()/100), float64(len(spinners[c.spinnerType])))))],
c.description,
bytesString,
)
}
} else if rightBrac == "" {
str = fmt.Sprintf("\r%s%4d%% %s%s%s%s %s ",
c.description,
s.currentPercent,
c.theme.BarStart,
saucer,
strings.Repeat(c.theme.SaucerPadding, repeatAmount),
c.theme.BarEnd,
bytesString,
)
} else {
if s.currentPercent == 100 {
str = fmt.Sprintf("\r%s%4d%% %s%s%s%s %s",
c.description,
s.currentPercent,
c.theme.BarStart,
saucer,
strings.Repeat(c.theme.SaucerPadding, repeatAmount),
c.theme.BarEnd,
bytesString,
)
if c.showElapsedTimeOnFinish {
str = fmt.Sprintf("%s [%s]", str, leftBrac)
}
} else {
str = fmt.Sprintf("\r%s%4d%% %s%s%s%s %s [%s:%s]",
c.description,
s.currentPercent,
c.theme.BarStart,
saucer,
strings.Repeat(c.theme.SaucerPadding, repeatAmount),
c.theme.BarEnd,
bytesString,
leftBrac,
rightBrac,
)
}
}
if c.colorCodes {
// convert any color codes in the progress bar into the respective ANSI codes
str = colorstring.Color(str)
}
s.rendered = str
return getStringWidth(c, str, false), writeString(c, str)
}