commit 49bcc8df9df3a0ed4b011a1fe6297faa490d3095
parent ce96ac1f3314284542eb28ea5e80be9e18fdd1a5
Author: pepe <91209772+pepedinho@users.noreply.github.com>
Date: Mon, 27 Apr 2026 02:37:39 +0200
feat: add multibar (#3)
---------
Co-authored-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
7 files changed, 238 insertions(+), 4 deletions(-)
diff --git a/build.zig b/build.zig
@@ -26,7 +26,7 @@ pub fn build(b: *std.Build) void {
const docs_step = b.step("docs", "Generate docs");
docs_step.dependOn(&docs.step);
- for ([_][]const u8{ "simple", "complex", "thread" }) |example| {
+ for ([_][]const u8{ "simple", "complex", "thread", "multibar", "multibar-static" }) |example| {
const run_step = b.step(b.fmt("run-bar-{s}", .{example}), b.fmt("Run bar/{s}.zig example", .{example}));
const exe = b.addExecutable(.{
.name = example,
diff --git a/examples/bar/multibar-static.zig b/examples/bar/multibar-static.zig
@@ -0,0 +1,50 @@
+const std = @import("std");
+const progress = @import("progress");
+const MultiBar = progress.MultiBar.MultiBar;
+
+pub fn threadWorker(manager: *MultiBar, index: usize, seed: usize) !void {
+ var rand_impl = std.Random.DefaultPrng.init(seed);
+ const delay = @mod(rand_impl.random().int(i64), 100) + 20;
+
+ const pb = try manager.bar(index);
+
+ while (!pb.isFinished()) {
+ pb.add(1);
+ try manager.render();
+
+ try std.Io.sleep(manager.writer.io, std.Io.Duration.fromMilliseconds(delay), .awake);
+ }
+}
+
+pub fn main(init: std.process.Init) !void {
+ var stdout_buf: [4096]u8 = undefined;
+ var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buf);
+
+ const num_bars = 5;
+ var desc_bufs: [num_bars][32]u8 = undefined;
+ var buf: [MultiBar.bufSize(num_bars)]u8 = undefined;
+ var fba = std.heap.FixedBufferAllocator.init(&buf);
+
+ var manager = MultiBar.init(fba.allocator(), &stdout_writer);
+ defer manager.deinit();
+
+ for (0..num_bars) |i| {
+ const desc = try std.fmt.bufPrint(&desc_bufs[i], "Task {d}", .{i + 1});
+
+ _ = try manager.addBar(100, .{
+ .description = desc,
+ .show_percentage = true,
+ .write_newline_on_finish = false,
+ .clear_on_finish = true,
+ });
+ }
+
+ var threads: [num_bars]std.Thread = undefined;
+ for (0..num_bars) |i| {
+ threads[i] = try std.Thread.spawn(.{}, threadWorker, .{ &manager, i, i * 100 });
+ }
+
+ for (threads) |thread| {
+ thread.join();
+ }
+}
diff --git a/examples/bar/multibar.zig b/examples/bar/multibar.zig
@@ -0,0 +1,53 @@
+const std = @import("std");
+const progress = @import("progress");
+const MultiBar = progress.MultiBar.MultiBar;
+
+pub fn threadWorker(manager: *MultiBar, index: usize, seed: usize) !void {
+ var rand_impl = std.Random.DefaultPrng.init(seed);
+ const delay = @mod(rand_impl.random().int(i64), 100) + 20;
+
+ const pb = try manager.bar(index);
+
+ while (!pb.isFinished()) {
+ pb.add(1);
+ try manager.render();
+
+ try std.Io.sleep(manager.writer.io, std.Io.Duration.fromMilliseconds(delay), .awake);
+ }
+}
+
+pub fn main(init: std.process.Init) !void {
+ var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer arena.deinit();
+ const allocator = arena.allocator();
+
+ var stdout_buf: [4096]u8 = undefined;
+ var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buf);
+
+ var manager = MultiBar.init(allocator, &stdout_writer);
+ defer manager.deinit();
+
+ const num_bars = 5;
+
+ for (0..num_bars) |i| {
+ var buf: [32]u8 = undefined;
+ const temp_desc = try std.fmt.bufPrint(&buf, "Task {d}", .{i + 1});
+ const desc = try allocator.dupe(u8, temp_desc);
+
+ _ = try manager.addBar(100, .{
+ .description = desc,
+ .show_percentage = true,
+ .write_newline_on_finish = false,
+ .clear_on_finish = false,
+ });
+ }
+
+ var threads: [num_bars]std.Thread = undefined;
+ for (0..num_bars) |i| {
+ threads[i] = try std.Thread.spawn(.{}, threadWorker, .{ &manager, i, i * 100 });
+ }
+
+ for (threads) |thread| {
+ thread.join();
+ }
+}
diff --git a/src/ansi-term.zig b/src/ansi-term.zig
@@ -65,6 +65,22 @@ pub fn setCursorColumn(writer: anytype, column: usize) !void {
try writer.print(csi ++ "{d}G", .{column});
}
+pub fn setCursorPos(writer: anytype, row: usize, column: usize) !void {
+ try writer.print(csi ++ "{d};{d}H", .{ row, column });
+}
+
+pub fn cursorUp(writer: anytype, rows: usize) !void {
+ if (rows > 0) {
+ try writer.print(csi ++ "{d}A", .{rows});
+ }
+}
+
+pub fn cursorDown(writer: anytype, rows: usize) !void {
+ if (rows > 0) {
+ try writer.print(csi ++ "{d}B", .{rows});
+ }
+}
+
pub fn cursorForward(writer: anytype, columns: usize) !void {
try writer.print(csi ++ "{d}C", .{columns});
}
diff --git a/src/bar.zig b/src/bar.zig
@@ -10,7 +10,7 @@ const Error = error{
///Used when the terminal width cannot be retrieved.
const default_bar_width: u16 = 40;
-const Config = struct {
+pub const Config = struct {
///Progress bar description.
description: ?[]const u8 = null,
///The progress bar prefix.
@@ -27,7 +27,7 @@ const Config = struct {
show_percentage: bool = false,
///Clear the line when the progress bar finishes.
clear_on_finish: bool = false,
- ///Write a newline when the progress bar finishes.
+ ///Write a newline when the progress bar finishes. In the case of a `MultiBar`, this will always be `false`.
write_newline_on_finish: bool = true,
///Custom progress bar width.
///If the width is greater than the terminal width, the terminal width will be used.
@@ -39,7 +39,7 @@ const Config = struct {
bg_colour: ansi_term.Colour = .Default,
};
-const Bar = @This();
+pub const Bar = @This();
///Direct access is not thread safe. Use `currentProgress()` if you need thread safety.
current_progress: usize = 0,
diff --git a/src/multi-bar.zig b/src/multi-bar.zig
@@ -0,0 +1,114 @@
+const std = @import("std");
+const bar = @import("bar.zig");
+const ansi_term = @import("ansi-term.zig");
+const Bar = bar.Bar;
+const Config = bar.Config;
+
+pub const MultiBar = struct {
+ bars: std.ArrayList(Bar),
+ writer: *std.Io.File.Writer,
+ mutex: std.Io.Mutex,
+ allocator: std.mem.Allocator,
+ active_line: usize = 0,
+
+ pub fn init(
+ allocator: std.mem.Allocator,
+ writer: *std.Io.File.Writer,
+ ) MultiBar {
+ return .{
+ .bars = .empty,
+ .writer = writer,
+ .mutex = std.Io.Mutex.init,
+ .allocator = allocator,
+ };
+ }
+
+ pub fn deinit(self: *MultiBar) void {
+ self.mutex.lockUncancelable(self.writer.io);
+ defer self.mutex.unlock(self.writer.io);
+
+ ansi_term.showCursor(&self.writer.interface) catch {};
+ self.writer.interface.flush() catch {};
+ self.bars.deinit(self.allocator);
+ }
+
+ /// Returns the minimum buffer size in bytes needed to hold `n` bars.
+ pub fn bufSize(n: usize) usize {
+ return n * @sizeOf(Bar) + @alignOf(Bar) - 1;
+ }
+
+ /// Add a new progress bar to the manager.
+ /// Overrides `write_newline_on_finish` to false to ensure terminal visual integrity.
+ /// When statically backed, `error.OutOfMemory` will be returned when the bar limit is exceeded.
+ /// Returns the index of the newly added bar.
+ pub fn addBar(self: *MultiBar, max_progress: usize, config: Config) !usize {
+ var safe_config = config;
+ safe_config.write_newline_on_finish = false;
+
+ const new_pb = Bar.init(max_progress, self.writer, safe_config);
+ const index = self.bars.items.len;
+
+ try self.bars.append(self.allocator, new_pb);
+ try self.writer.interface.writeAll("\n");
+ self.active_line += 1;
+ return index;
+ }
+
+ /// Returns a pointer to the bar at the given index.
+ /// Not thread safe. The caller is responsible for synchronization
+ /// if the bar is accessed concurrently with `render()` or other threads.
+ pub fn bar(self: *MultiBar, index: usize) !*Bar {
+ if (index >= self.bars.items.len) {
+ return error.IndexOutOfBounds;
+ }
+ return &self.bars.items[index];
+ }
+
+ /// Render all active progress bars.
+ /// Handles terminal cursor repositioning and dynamic reflowing if bars finish.
+ pub fn render(self: *MultiBar) !void {
+ self.mutex.lockUncancelable(self.writer.io);
+ defer self.mutex.unlock(self.writer.io);
+
+ if (self.active_line == 0) return;
+
+ try ansi_term.cursorUp(&self.writer.interface, self.active_line);
+
+ var newly_active: usize = 0;
+ var all_finished: bool = true;
+
+ for (self.bars.items) |*pb| {
+ if (!pb.isFinished()) {
+ all_finished = false;
+ }
+
+ if (pb.isFinished() and pb.config.clear_on_finish) {
+ continue;
+ }
+
+ try pb.render();
+ try ansi_term.cursorDown(&self.writer.interface, 1);
+ try ansi_term.setCursorColumn(&self.writer.interface, 0);
+
+ newly_active += 1;
+ }
+
+ if (newly_active < self.active_line) {
+ const diff = self.active_line - newly_active;
+ for (0..diff) |_| {
+ try ansi_term.clearCurrentLine(&self.writer.interface);
+ try ansi_term.cursorDown(&self.writer.interface, 1);
+ }
+ try ansi_term.cursorUp(&self.writer.interface, diff);
+ }
+
+ if (all_finished) {
+ try ansi_term.showCursor(&self.writer.interface);
+ self.active_line = 0;
+ } else {
+ self.active_line = newly_active;
+ }
+
+ try self.writer.interface.flush();
+ }
+};
diff --git a/src/root.zig b/src/root.zig
@@ -1,2 +1,3 @@
pub const Bar = @import("./bar.zig");
pub const Spinner = @import("./spinner.zig");
+pub const MultiBar = @import("multi-bar.zig");