termancy Guide Components Commands API

multiprogress

Several labelled progress bars, one per task

termancy tui add multiprogress

Also copies what it requires: progress

termancy ui demo multiprogress, recorded

Several labelled progress bars, one per task, each finishing on its own.

var tasks = [_]MultiProgress.Task{
    .{ .label = "download", .bar = .{ .total = 100 } },
    .{ .label = "unpack", .bar = .{ .total = 20 } },
};
var multi: MultiProgress = .{ .tasks = &tasks };
// as a program: send `.app = .{ .task = 0, .sample = .{ .current = 40, .at = now } }`

A finished task shows done in place of its estimate. With quit_when_done, the program ends when every task has.

multiprogress.zig

//! Several labelled progress bars, one per task, each finishing on its own.
//!
//! ```zig
//! var tasks = [_]MultiProgress.Task{
//!     .{ .label = "download", .bar = .{ .total = 100 } },
//!     .{ .label = "unpack", .bar = .{ .total = 20 } },
//! };
//! var multi: MultiProgress = .{ .tasks = &tasks };
//! // as a program: send `.app = .{ .task = 0, .sample = .{ .current = 40, .at = now } }`
//! ```
//!
//! A finished task shows `done` in place of its estimate. With `quit_when_done`, the
//! program ends when every task has.

const std = @import("std");
const tui = @import("tui");
const Writer = std.Io.Writer;
const Progress = @import("progress.zig");

const MultiProgress = @This();

/// The caller's; the bars are updated in place.
tasks: []Task,
quit_when_done: bool = false,
styles: Styles = .{},

pub const Task = struct {
    label: []const u8,
    bar: Progress,
};

pub const Styles = struct {
    label: tui.Style = .{ .bold = true },
    done: tui.Style = .{ .fg = .{ .indexed = 78 } },
};

/// Progress of one task.
pub const Message = struct { task: usize, sample: Progress.Sample };

pub fn update(multi: *MultiProgress, msg: tui.Message(Message)) tui.Cmd {
    switch (msg) {
        .app => |m| {
            if (m.task < multi.tasks.len) {
                multi.tasks[m.task].bar.set(m.sample.current, m.sample.at);
            }
            if (multi.quit_when_done and multi.done()) return .quit;
        },
        else => {},
    }
    return .none;
}

/// Whether every task is finished.
pub fn done(multi: *const MultiProgress) bool {
    for (multi.tasks) |task| if (task.bar.current < task.bar.total) return false;
    return true;
}

pub fn view(multi: *const MultiProgress, w: *Writer) Writer.Error!void {
    var label_width: usize = 0;
    for (multi.tasks) |task| label_width = @max(label_width, tui.width.of(task.label));
    for (multi.tasks, 0..) |task, i| {
        if (i > 0) try w.writeByte('\n');
        try multi.styles.label.paint(w, task.label);
        try w.splatByteAll(' ', label_width - tui.width.of(task.label) + 1);
        var bar = task.bar;
        const finished = bar.current >= bar.total;
        if (finished) bar.show_eta = false;
        try bar.view(w);
        if (finished) {
            try w.writeByte(' ');
            try multi.styles.done.paint(w, "done");
        }
    }
}

const testing = std.testing;

fn at(seconds: i64) std.Io.Timestamp {
    return std.Io.Timestamp.zero.addDuration(.fromSeconds(seconds));
}

test "each task fills on its own, and the program quits when all are done" {
    const plain: Progress.Styles = .{ .full = .{}, .empty = .{} };
    var tasks = [_]MultiProgress.Task{
        .{
            .label = "get",
            .bar = .{ .total = 2, .width = 4, .full = "#", .empty = ".", .styles = plain },
        },
        .{
            .label = "unpack",
            .bar = .{ .total = 1, .width = 4, .full = "#", .empty = ".", .styles = plain },
        },
    };
    var d: tui.testing.Driver(MultiProgress) = try .init(testing.allocator, .{
        .tasks = &tasks,
        .quit_when_done = true,
        .styles = .{ .label = .{}, .done = .{} },
    }, .{});
    defer d.deinit();
    try d.expectFrame("get    ....   0%\nunpack ....   0%");
    try d.send(.{ .app = .{ .task = 1, .sample = .{ .current = 1, .at = at(1) } } });
    try d.expectFrame("get    ....   0%\nunpack #### 100% done");
    try testing.expect(!d.done());
    try d.send(.{ .app = .{ .task = 0, .sample = .{ .current = 2, .at = at(2) } } });
    try testing.expect(d.done());
    try d.expectFrame("get    #### 100% done\nunpack #### 100% done");
}