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)
}