x.progress #
x.progress
Terminal progress bars and spinners: one, or many at once.
Quickstart
import x.progress
fn main() {
mut bar := progress.Bar.new(1000, text: 'work ')
bar.start()
for _ in 0 .. 1000 {
bar.inc()
}
bar.wait()
}
All options are optional and are written inline, without naming a type: Bar.new(100), Bar.new(100, text: 'work ', show_rate: true). (The options structs are @[params].)
bar.start() begins drawing in a background thread, bar.inc() and bar.add(n) advance the bar (from any thread), and bar.wait() returns once the bar has completed and was drawn for the last time.
Several bars
A MultiBar draws any number of bars and spinners as one block, with a single render thread. Items are drawn in the order they were added. A finished item leaves the block, and the rest move up.
import x.progress
fn work(mut bar progress.Bar, n int) {
for _ in 0 .. n {
bar.inc()
}
}
fn main() {
mut mb := progress.MultiBar.new()
mut a := mb.add_bar(100, text: 'alpha ', on_end: 'alpha done')
mut b := mb.add_bar(250, text: 'bravo ', on_end: 'bravo done')
mb.start()
// advance the bars from any threads
ta := spawn work(mut a, 100)
tb := spawn work(mut b, 250)
ta.wait()
tb.wait()
mb.println('log lines go above the bars without tearing them')
mb.wait()
}
Bars must be held in mut variables, and passed to other threads as mut. More items can be added while the group is running, until wait() is called.
Spinners
A spinner is for work with no known size. It is driven exactly like a bar (start, finish, wait, set_text, println, is_done, elapsed) and shows the same text and elapsed time, but has no total, so there is no percentage, rate or ETA.
import time
import x.progress
fn main() {
mut sp := progress.Spinner.new(
text: 'resolving '
frames: progress.spinner_line() // the default is spinner_dots()
show_elapsed: true
leave: true // keep it on screen, with a done mark, when finished
)
sp.start()
time.sleep(2 * time.second) // the work
sp.finish()
sp.wait()
}
In a MultiBar, spinners and bars mix freely:
import time
import x.progress
fn main() {
mut mb := progress.MultiBar.new()
mut conn := mb.add_spinner(text: 'connecting ', on_end: 'connected')
mut dl := mb.add_bar(100, text: 'download ')
mb.start()
time.sleep(500 * time.millisecond) // connecting...
conn.finish()
dl.add(100)
mb.wait()
}
A bar finishes by itself when it reaches its maximum. A spinner finishes only when you call finish() (or MultiBar.stop()), so wait() blocks until you do.
The frames come from spinner_frames.v: 76 sets, taken from the Go library schollz/progressbar (MIT license, see the notice in that file) with the same numbering. Use a preset (spinner_dots(), spinner_line(), spinner_braille(), spinner_arrows(), spinner_circle(), spinner_quarters(), spinner_grow(), spinner_bounce(), spinner_pulse(), spinner_ellipsis(), spinner_earth(), spinner_moon()), any set by number with spinner_set(27) (spinner_set_count() of them), or your own []string. The presets are functions because V does not allow assigning a const array into a struct field.
The animation follows elapsed time (interval, 80ms by default), so it runs at the same speed however often the display is redrawn. Frames of different widths are padded, so the text does not jitter. The line is <frame> <text> <elapsed>; when it does not fit, the text is shortened with …, and the frame and the time are kept.
Units
With show_rate: true a bar shows its speed. By default that is items per second, scaled with SI prefixes (1.5kit/s). For bytes, pick a preset:
import x.progress
fn main() {
mut bar := progress.Bar.new(250_000_000,
text: 'download '
show_rate: true
rate_unit: progress.RateUnit.bytes_iec() // 12.3MiB/s
)
bar.start()
bar.add(250_000_000)
bar.wait()
}
| Preset | Prefix list | Looks like |
|---|---|---|
RateUnit.items() |
SI, 1000 | it/s, kit/s, Mit/s, ... |
RateUnit.bytes() |
SI, 1000 | B/s, kB/s, MB/s, ... |
RateUnit.bytes_iec() |
IEC, 1024 | B/s, KiB/s, MiB/s, ... |
The prefixes alone are UnitPrefixList.si() (the default) and UnitPrefixList.iec(), so any other unit combines them: RateUnit{ unit: 'frames', prefixes: progress.UnitPrefixList.iec() }. period and period_string change the time base, for example per minute.
Behaviour
- Output goes to stderr, so stdout stays clean for piping.
- Not a terminal (a CI log, a file, a pipe): nothing live is drawn and no escape codes are written, including any ANSI escapes in labels, completion messages, and log lines. A bar or spinner that finishes prints one summary line instead.
- Fits the terminal. A line never reaches the last column; the bar graphic shrinks first.
length: 0fills the available width. If there are more items than terminal rows, the rest are summarised as... and N more. ANSI styling is retained on live lines that fit; a shortened line is plain text to avoid splitting an escape or leaving a style active. - Nothing happens at import. The cursor is hidden, and an at-exit hook plus SIGINT/SIGTERM hooks are registered, only when a live display starts.
- Cursor and signals. The cursor is restored on normal exit, on
exit(), and on Ctrl+C or SIGTERM (the program then exits with 130 or 143). If your program already handles or ignores SIGINT/SIGTERM, that is left alone, liketerm.show_cursor_on_exit()does. The at-exit hook still restores the cursor if your handler ends the program withexit(). You can also setMultiBarOptions{ hide_cursor: false }. - When it draws live. For stdout and stderr it uses V's own check (
term.can_show_color_on_*): a terminal,TERMnotdumb, and theVCOLORS=always|neveroverride is honored.NO_COLORdoes not apply, since it concerns colour and not cursor control.MultiBarOptions.forcedraws regardless. - Windows (untested): V's runtime enables escape sequence processing when stdout is a terminal. If it is not enabled, the bars fall back to plain output. The terminal size is read from the stdout console handle.
- Thread-safe. Counters are atomic, and one thread draws. Increments saturate at the maximum, including very large or concurrent overshoots.
- Rate and ETA use a smoothed rate (an exponential moving average), not the overall average. Before a rate is known they show
--.-and--:--.
API summary
| Call | What it does |
|---|---|
Bar.new(max, ...) |
a bar that draws by itself |
Spinner.new(...) |
a spinner that draws by itself |
MultiBar.new(...) |
a group drawn together |
mb.add_bar(max, ...) |
add a bar to the group |
mb.add_spinner(...) |
add a spinner to the group |
.start(), .wait() |
begin drawing, and block until all is finished |
.finish() |
complete a bar early, or end a spinner |
mb.stop() |
finish every item where it stands, and return |
bar.inc(), bar.add(n), bar.set(v) |
advance a bar (thread-safe) |
.set_text(s) |
change the label (thread-safe) |
.println(s), mb.println(s) |
print a line above the display |
.is_done(), .elapsed() |
inspect |
bar.value() |
how many items are done |
BarOptions: text, on_end, leave, length, style, show_percent, show_rate, rate_unit, show_elapsed, elapsed_fmt, show_left, left_fmt, smoothing.
SpinnerOptions: text, on_end, leave, frames, interval, done_frame, show_elapsed, elapsed_fmt.
MultiBarOptions: delay, hide_cursor, output_fd, force, width.
When a bar or spinner completes, on_end (if set) replaces it. Otherwise leave: true keeps its final state, and otherwise it disappears.
Styles
A bar's look is its style: progress.ClassicStyle{} (the default) or progress.BlockStyle{}, which has 1/8 cell resolution. Both are structs with public fields (start, end, fill, ...). Implement the one method Style interface for your own look.
fn fmt_rate #
fn fmt_rate(per_sec f64, u RateUnit) string
(The standard library has no SI-prefix formatter, hence the prefix table here, nor an mm:ss duration formatter; time.Duration.str() prints 1m5s.)
fmt_rate formats per_sec (items per second) in the units described by u, choosing the prefix so the number stays readable: 0.5 -> 0.5it/s, 1_500_000 -> 1.5Mit/s; with IEC prefixes, 1536 bytes/s -> 1.5KiB/s.
fn fmt_rate_unknown #
fn fmt_rate_unknown(u RateUnit) string
fmt_rate_unknown is shown before enough samples exist to estimate a rate.
fn fmt_time #
fn fmt_time(d time.Duration, f TimeFormat) string
fmt_time formats d (negative durations are shown as zero).
fn fmt_time_unknown #
fn fmt_time_unknown(f TimeFormat) string
fmt_time_unknown is the placeholder shown when a time cannot be estimated yet.
fn spinner_arrows #
fn spinner_arrows() []string
spinner_arrows returns the frames of a rotating arrow.
fn spinner_bounce #
fn spinner_bounce() []string
spinner_bounce returns the frames of a dot bouncing between parentheses.
fn spinner_braille #
fn spinner_braille() []string
spinner_braille returns the frames of braille, eight frames.
fn spinner_circle #
fn spinner_circle() []string
spinner_circle returns the frames of a half-filled circle turning.
fn spinner_dots #
fn spinner_dots() []string
spinner_dots returns the frames of braille dots, the default.
fn spinner_earth #
fn spinner_earth() []string
spinner_earth returns the frames of emoji globe.
fn spinner_ellipsis #
fn spinner_ellipsis() []string
spinner_ellipsis returns the frames of dots filling and emptying.
fn spinner_grow #
fn spinner_grow() []string
spinner_grow returns the frames of a block growing and shrinking.
fn spinner_line #
fn spinner_line() []string
spinner_line returns the frames of the classic pipe, slash, dash, backslash.
fn spinner_moon #
fn spinner_moon() []string
spinner_moon returns the frames of emoji moon phases.
fn spinner_pulse #
fn spinner_pulse() []string
spinner_pulse returns the frames of a dot travelling across three.
fn spinner_quarters #
fn spinner_quarters() []string
spinner_quarters returns the frames of quarter arcs.
fn spinner_set #
fn spinner_set(n int) []string
spinner_set returns frame set n, a fresh copy. Panics if n is out of range, as that is a programming error.
fn spinner_set_count #
fn spinner_set_count() int
spinner_set_count is the number of frame sets: spinner_set(n) accepts 0 .. spinner_set_count() - 1.
fn Bar.new #
fn Bar.new(max i64, opts BarOptions) &Bar
Bar.new creates a bar for max items that draws by itself. Call start(), advance it with inc()/add(), then wait().
fn MultiBar.new #
fn MultiBar.new(opts MultiBarOptions) &MultiBar
MultiBar.new creates an empty group.
fn RateUnit.bytes #
fn RateUnit.bytes() RateUnit
RateUnit.bytes returns bytes per second with SI prefixes (B/s, kB/s, MB/s, GB/s, ...), with a step of 1000.
fn RateUnit.bytes_iec #
fn RateUnit.bytes_iec() RateUnit
RateUnit.bytes_iec returns bytes per second with IEC prefixes (B/s, KiB/s, MiB/s, GiB/s, ...), with a step of 1024.
fn RateUnit.items #
fn RateUnit.items() RateUnit
RateUnit.items returns the default unit: items per second (it/s, kit/s, ...).
fn Spinner.new #
fn Spinner.new(opts SpinnerOptions) &Spinner
Spinner.new creates a spinner that draws by itself. Call start(), do your work, then finish() and wait().
fn TimeFormat.from #
fn TimeFormat.from[W](input W) !TimeFormat
from returns the TimeFormat value of an integer or of a field name, or an error if there is none.
fn UnitPrefixList.iec #
fn UnitPrefixList.iec() UnitPrefixList
UnitPrefixList.iec returns the IEC (binary) prefixes: Ki, Mi, Gi, Ti, ... where each is 1024 times the previous one.
fn UnitPrefixList.si #
fn UnitPrefixList.si() UnitPrefixList
UnitPrefixList.si returns the SI (decimal) prefixes: k, M, G, T, ... where each is 1000 times the previous one. This is the default.
interface Style #
interface Style {
draw(mut sb strings.Builder, fraction f64, cells int)
}
Style draws the bar graphic itself (not the percent, rate or ETA).
draw must write start, then exactly cells terminal cells, then end. Implement it to make your own look; fraction is in 0.0..=1.0.
enum TimeFormat #
enum TimeFormat {
mmss // mm:ss
hhmm // hh:mm
hhmmss // hh:mm:ss
s // xs
sf // x.xxs
mmssf // mm:ss.xx
hhmmssf // hh:mm:ss.xx
}
TimeFormat selects how a duration is displayed.
struct Bar #
struct Bar {
Item
pub:
max i64
mut:
opts BarOptions
count &stdatomic.AtomicVal[i64] = stdatomic.new_atomic(i64(0))
est RateEstimator // touched only by the render thread
}
Bar is one progress bar. It is safe to advance from many threads at once.
Create it with Bar.new (a bar on its own) or MultiBar.add_bar (several bars drawn together). Hold it in a mut variable.
start(), wait(), finish(), is_done(), elapsed(), set_text() and println() come from the embedded Item, so a Spinner has exactly the same ones.
fn (Bar) inc #
fn (mut b Bar) inc()
inc advances the bar by one.
fn (Bar) add #
fn (mut b Bar) add(n i64)
add advances the bar by n. Non-positive n is ignored. Reaching the maximum completes the bar; the count saturates at max without overflowing.
fn (Bar) set #
fn (mut b Bar) set(v i64)
set moves the bar to an absolute position (clamped to 0..max).
fn (Bar) value #
fn (mut b Bar) value() i64
value is how many items are done so far (never more than max).
struct BarOptions #
struct BarOptions {
pub mut:
// text is printed before the percentage. Include your own trailing space.
text string
// on_end, when not empty, replaces the bar once it completes.
on_end string
// leave keeps the finished bar on screen (when on_end is empty).
// By default a finished bar disappears.
leave bool
// length is the width of the bar graphic in cells. 0 means "fill the
// terminal". It is shrunk automatically if the line would not fit.
length int = 30
// style draws the bar graphic: ClassicStyle{} (default), BlockStyle{}, or your own.
style Style = ClassicStyle{}
show_percent bool = true
show_rate bool
rate_unit RateUnit
show_elapsed bool
elapsed_fmt TimeFormat
show_left bool = true
left_fmt TimeFormat
// smoothing is the rate estimator's time constant in seconds (see
// RateEstimator.tau). It drives the rate and ETA displays.
smoothing f64 = 2.0
}
BarOptions configures one bar. Every field is optional, and it is a @[params] struct, so options are written inline without naming the type, or left out entirely: Bar.new(100), Bar.new(100, text: 'work ', show_rate: true).
struct BlockStyle #
struct BlockStyle {
pub mut:
start string = '▕'
end string = '▏'
fill rune = `█` // U+2588
empty rune = ` `
}
BlockStyle renders ▕█████▌ ▏ with 1/8-cell resolution, so even a short bar moves smoothly. fill should be a full-block glyph.
fn (BlockStyle) draw #
fn (s BlockStyle) draw(mut sb strings.Builder, fraction f64, cells int)
draw implements Style.
struct ClassicStyle #
struct ClassicStyle {
pub mut:
start string = ' ['
end string = ']'
fill rune = `=`
head rune = `>`
empty rune = ` `
}
ClassicStyle renders [=====> ].
fn (ClassicStyle) draw #
fn (s ClassicStyle) draw(mut sb strings.Builder, fraction f64, cells int)
draw implements Style.
struct Item #
struct Item {
mut:
text string
mu &sync.Mutex = sync.new_mutex() // guards text
t0 &stdatomic.AtomicVal[i64] = stdatomic.new_atomic(i64(0)) // start, monotonic ns
t_end &stdatomic.AtomicVal[i64] = stdatomic.new_atomic(i64(0)) // elapsed ns at completion; 0 while running
group &MultiBar = unsafe { nil }
}
Item is the part of a Bar and a Spinner that has nothing to do with what they show: a label, a clock, a completion time, and the MultiBar they belong to. Both embed it, so start, wait, finish, elapsed, set_text and println behave identically on either, which is the point: a spinner is driven exactly like a bar. You do not create an Item yourself.
fn (Item) start #
fn (mut c Item) start()
start begins drawing. For an item from MultiBar.add_bar / add_spinner this starts the whole MultiBar. Calling it more than once is harmless.
fn (Item) wait #
fn (mut c Item) wait()
wait blocks until every item in the group has finished and been drawn for the last time. It never returns while an item is unfinished: a bar finishes at its maximum, but a spinner only when you call finish(). Use finish() on the stragglers, or MultiBar.stop().
fn (Item) finish #
fn (mut c Item) finish()
finish completes the item where it stands: a bar at its current position, a spinner at whatever moment you decide the work is over.
fn (Item) is_done #
fn (mut c Item) is_done() bool
is_done reports whether the item has finished.
fn (Item) elapsed #
fn (mut c Item) elapsed() time.Duration
elapsed is the time since start(), frozen once the item finishes.
fn (Item) set_text #
fn (mut c Item) set_text(s string)
set_text changes the label. Thread-safe, so a worker can show what it is doing right now.
fn (Item) println #
fn (mut c Item) println(msg string)
println prints a line above the display without garbling it. See MultiBar.println.
struct MultiBar #
struct MultiBar {
mut:
opts MultiBarOptions
mu &sync.Mutex = sync.new_mutex() // guards the fields below
active []Drawable
pending []string // println() lines waiting to be drawn
started bool
running bool
closing bool // set by wait()/stop(): end once no bar is active
joined bool
t thread
// Render thread only (written before the thread starts, then private):
live bool // true: draw a live block; false: only emit final lines
drawn int // rows the live block currently occupies
last string
}
MultiBar draws any number of bars and spinners at once with one render thread, in the order they were added.
Items that are still running are redrawn in place as a block. A bar that completes leaves the block (printing its on_end text, or its final state if leave is set) and the remaining bars move up.
Typical use: create it, add bars and spinners, call start(), advance the bars from any threads (and finish() the spinners when their work is over), then wait(). More items may be added while it is running, up until wait() has been called.
fn (MultiBar) add_bar #
fn (mut g MultiBar) add_bar(max i64, opts BarOptions) &Bar
add_bar adds a bar for max items. Items are drawn in the order added.
fn (MultiBar) add_spinner #
fn (mut g MultiBar) add_spinner(opts SpinnerOptions) &Spinner
add_spinner adds a spinner, for work with no known size. It runs until you call its finish() (or stop() on the MultiBar).
fn (MultiBar) start #
fn (mut g MultiBar) start()
start begins drawing in a background thread. Calling it more than once is harmless.
fn (MultiBar) wait #
fn (mut g MultiBar) wait()
wait blocks until every bar has completed and the final frame is drawn. It never returns if some bar is never completed; call that bar's finish() or use stop(). Call it from one thread only.
fn (MultiBar) stop #
fn (mut g MultiBar) stop()
stop completes every bar and spinner where it stands, draws the last frame and returns. Use it to bail out early, e.g. after an error.
fn (MultiBar) println #
fn (mut g MultiBar) println(msg string)
println prints msg on its own line above the bars. Unlike a plain println(), it cannot tear the display, and it is safe from any thread.
struct MultiBarOptions #
struct MultiBarOptions {
pub mut:
// delay is the minimum time between redraws.
delay time.Duration = 50 * time.millisecond
// hide_cursor hides the cursor while a live display is running. It is
// restored on exit, Ctrl+C and SIGTERM.
hide_cursor bool = true
// output_fd is where bars and println() lines are written (2 = stderr).
output_fd int = 2
// force draws a live display even if output_fd is not a terminal.
force bool
// width overrides terminal-width detection (0 = detect).
width int
}
MultiBarOptions configures how a group of bars is drawn. Every field is optional, and it is a @[params] struct: MultiBar.new(), MultiBar.new(delay: 20 * time.millisecond).
struct RateUnit #
struct RateUnit {
pub mut:
period time.Duration = time.second
period_string string = 's'
unit string = 'it'
prefixes UnitPrefixList
}
RateUnit says how a rate is displayed: 1.5kit/s by default. The number is scaled with prefixes, then unit and /period_string are appended.
There are presets for the common cases: RateUnit.items() (the default), RateUnit.bytes() (kB/s, MB/s, ...) and RateUnit.bytes_iec() (KiB/s, MiB/s, ...).
struct Spinner #
struct Spinner {
Item
mut:
opts SpinnerOptions
frame_w int // widest frame, so the text does not jitter as frames change width
}
Spinner is an animation for work with no known size. It is driven exactly like a Bar: Spinner.new (or MultiBar.add_spinner), start(), finish(), wait(), plus set_text(), println(), is_done() and elapsed(), all from the embedded Item.
A bar finishes by itself when it reaches its maximum; a spinner has no maximum, so it runs until you call finish() (or MultiBar.stop()). Hold it in a mut variable.
struct SpinnerOptions #
struct SpinnerOptions {
pub mut:
// text is shown after the frame. Trailing spaces are trimmed (the
// renderer puts one space between the parts).
text string
// on_end, when not empty, replaces the spinner once it finishes.
on_end string
// leave keeps the finished spinner on screen (when on_end is empty),
// showing done_frame instead of an animation frame.
leave bool
// frames are the animation frames. See spinner_frames.v for ready-made
// sets: spinner_dots() (default), spinner_line(), spinner_set(N), ... or
// give your own. An empty list falls back to spinner_dots().
frames []string = spinner_dots()
// interval is how long each frame is shown. The display can only change
// as fast as MultiBarOptions.delay (50ms by default). Zero or negative
// falls back to 80ms.
interval time.Duration = 80 * time.millisecond
// done_frame replaces the animation once the spinner has finished (visible
// with `leave`, or in plain, non-terminal output).
done_frame string = '✓'
// show_elapsed shows the elapsed time after the text, like a bar.
show_elapsed bool
elapsed_fmt TimeFormat
}
SpinnerOptions configures one spinner. Every field is optional, and it is a @[params] struct: Spinner.new(), Spinner.new(text: 'working ').
A spinner has no total, so there is no percentage, rate or ETA; what it shares with a bar is the text, the elapsed time and what it leaves behind when it finishes.
struct UnitPrefixList #
struct UnitPrefixList {
pub mut:
sizes []string = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'R', 'Q']
base f64 = 1000.0
}
UnitPrefixList is the set of prefixes used to scale a number, and the factor between neighbouring ones. The zero value is the SI list (k, M, G, ... with a step of 1000), so a plain UnitPrefixList{} is the same as UnitPrefixList.si().
- README
- fn fmt_rate
- fn fmt_rate_unknown
- fn fmt_time
- fn fmt_time_unknown
- fn spinner_arrows
- fn spinner_bounce
- fn spinner_braille
- fn spinner_circle
- fn spinner_dots
- fn spinner_earth
- fn spinner_ellipsis
- fn spinner_grow
- fn spinner_line
- fn spinner_moon
- fn spinner_pulse
- fn spinner_quarters
- fn spinner_set
- fn spinner_set_count
- fn Bar.new
- fn MultiBar.new
- fn RateUnit.bytes
- fn RateUnit.bytes_iec
- fn RateUnit.items
- fn Spinner.new
- fn TimeFormat.from
- fn UnitPrefixList.iec
- fn UnitPrefixList.si
- interface Style
- enum TimeFormat
- struct Bar
- struct BarOptions
- struct BlockStyle
- struct ClassicStyle
- struct Item
- struct MultiBar
- struct MultiBarOptions
- struct RateUnit
- struct Spinner
- struct SpinnerOptions
- struct UnitPrefixList