fetch_ml/cli/src/utils/progress.zig
Jeremie Fraeys ef7d19db9b
feat(cli): integrate ProgressBar into sync command
Update progress.zig and integrate into sync command:
- progress.zig: update import from colors.zig to io.zig
- sync.zig: add ProgressBar for multi-run sync operations
- Shows progress bar when syncing 2+ runs (not in JSON mode)
- Updates progress after each successful sync

Benefits:
- Better UX for long-running sync operations
- Visual feedback on sync progress
- Maintains clean output for single runs

All tests pass.
2026-03-04 21:23:16 -05:00

165 lines
4.4 KiB
Zig

const std = @import("std");
const io = @import("io.zig");
/// Progress bar for long-running operations
pub const ProgressBar = struct {
allocator: std.mem.Allocator,
total: u64,
current: u64,
width: usize,
start_time: i64,
label: []const u8,
const Self = @This();
pub fn init(allocator: std.mem.Allocator, total: u64, label: []const u8) Self {
return .{
.allocator = allocator,
.total = total,
.current = 0,
.width = 40,
.start_time = std.time.timestamp(),
.label = label,
};
}
/// Update progress and redraw
pub fn update(self: *Self, current: u64) void {
self.current = current;
self.draw();
}
/// Increment progress by amount
pub fn increment(self: *Self, amount: u64) void {
self.current += amount;
self.draw();
}
/// Draw progress bar to stdout
pub fn draw(self: *Self) void {
const percent = if (self.total > 0)
@min(100, @as(u64, self.current * 100 / self.total))
else
0;
const filled = if (self.total > 0)
@min(self.width, @as(usize, self.current * self.width / self.total))
else
0;
const elapsed = std.time.timestamp() - self.start_time;
const rate = if (elapsed > 0) self.current / @as(u64, @intCast(elapsed)) else 0;
// Build bar
var bar: [256]u8 = undefined;
var i: usize = 0;
// Clear line and move cursor to start
bar[i] = '\r';
i += 1;
// Add label
const label_len = @min(self.label.len, 30);
@memcpy(bar[i..][0..label_len], self.label[0..label_len]);
i += label_len;
if (label_len < 30) {
bar[i] = ' ';
i += 1;
}
// Opening bracket
bar[i] = '[';
i += 1;
// Filled portion
for (0..filled) |_| {
bar[i] = '█';
i += 1;
}
// Empty portion
for (filled..self.width) |_| {
bar[i] = '░';
i += 1;
}
// Closing bracket
bar[i] = ']';
i += 1;
// Percentage
const percent_str = std.fmt.bufPrint(bar[i..][0..20], " {d:>3}%", .{percent}) catch " ???%";
i += percent_str.len;
// Rate
if (rate > 0) {
const rate_str = std.fmt.bufPrint(bar[i..][0..30], " ({d} it/s)", .{rate}) catch "";
i += rate_str.len;
}
// Write to stdout
const stdout = std.io.getStdOut();
_ = stdout.write(bar[0..i]) catch {};
}
/// Finish and print newline
pub fn finish(self: *Self) void {
self.current = self.total;
self.draw();
const stdout = std.io.getStdOut();
_ = stdout.writeAll("\n") catch {};
}
};
/// Spinner for indeterminate operations
pub const Spinner = struct {
frames: []const []const u8,
frame_index: usize,
label: []const u8,
last_update: i64,
const Self = @This();
pub fn init(label: []const u8) Self {
return .{
.frames = &[_][]const u8{ "", "", "", "", "", "", "", "", "", "" },
.frame_index = 0,
.label = label,
.last_update = std.time.timestamp(),
};
}
/// Draw next frame
pub fn tick(self: *Self) void {
const now = std.time.timestamp();
if (now - self.last_update < 0) return; // Only update every 100ms
self.last_update = now;
self.frame_index = (self.frame_index + 1) % self.frames.len;
self.draw();
}
fn draw(self: *Self) void {
const frame = self.frames[self.frame_index];
const stdout = std.io.getStdOut();
var buf: [256]u8 = undefined;
const msg = std.fmt.bufPrint(&buf, "\r{s} {s}", .{ frame, self.label }) catch return;
_ = stdout.write(msg) catch {};
}
/// Finish and clear
pub fn finish(self: *Self) void {
const stdout = std.io.getStdOut();
_ = stdout.writeAll("\r\x1b[K") catch {}; // Clear line
_ = std.fmt.format(stdout.writer(), "✓ {s}\n", .{self.label}) catch {};
}
};
/// Convenience functions
pub fn showProgress(total: u64, label: []const u8) ProgressBar {
return ProgressBar.init(std.heap.c_allocator, total, label);
}
pub fn showSpinner(label: []const u8) Spinner {
return Spinner.init(label);
}