Skip to content

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: 0 fills 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, like term.show_cursor_on_exit() does. The at-exit hook still restores the cursor if your handler ends the program with exit(). You can also set MultiBarOptions{ hide_cursor: false }.
  • When it draws live. For stdout and stderr it uses V's own check (term.can_show_color_on_*): a terminal, TERM not dumb, and the VCOLORS=always|never override is honored. NO_COLOR does not apply, since it concerns colour and not cursor control. MultiBarOptions.force draws 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 #

@[heap]
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 #

@[params]
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 #

@[heap]
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 #

@[heap]
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 #

@[params]
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 #

@[heap]
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 #

@[params]
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().