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.
165 lines
4.4 KiB
Zig
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);
|
|
}
|