fetch_ml/cli/src/ui/progress.zig
Jeremie Fraeys 7583932897
feat(cli): add progress UI and rsync assets
- Add progress.zig for sync progress display
- Add rsync placeholder and release binaries to assets/rsync/
2026-02-20 15:51:17 -05:00

151 lines
4.5 KiB
Zig

const std = @import("std");
const colors = @import("../utils/colors.zig");
/// ProgressBar provides visual feedback for long-running operations.
/// It displays progress as a percentage, item count, and throughput rate.
pub const ProgressBar = struct {
total: usize,
current: usize,
label: []const u8,
start_time: i64,
width: usize,
/// Initialize a new progress bar
pub fn init(total: usize, label: []const u8) ProgressBar {
return .{
.total = total,
.current = 0,
.label = label,
.start_time = std.time.milliTimestamp(),
.width = 40, // Default bar width
};
}
/// Update the progress bar with current progress
pub fn update(self: *ProgressBar, current: usize) void {
self.current = current;
self.render();
}
/// Increment progress by one step
pub fn increment(self: *ProgressBar) void {
self.current += 1;
self.render();
}
/// Render the progress bar to stderr
fn render(self: ProgressBar) void {
const percent = if (self.total > 0)
@divFloor(self.current * 100, self.total)
else
0;
const elapsed_ms = std.time.milliTimestamp() - self.start_time;
const rate = if (elapsed_ms > 0 and self.current > 0)
@as(f64, @floatFromInt(self.current)) / (@as(f64, @floatFromInt(elapsed_ms)) / 1000.0)
else
0.0;
// Build progress bar
const filled = if (self.total > 0)
@divFloor(self.current * self.width, self.total)
else
0;
const empty = self.width - filled;
var bar_buf: [64]u8 = undefined;
var bar_stream = std.io.fixedBufferStream(&bar_buf);
const bar_writer = bar_stream.writer();
// Write filled portion
var i: usize = 0;
while (i < filled) : (i += 1) {
_ = bar_writer.write("=") catch {};
}
// Write empty portion
i = 0;
while (i < empty) : (i += 1) {
_ = bar_writer.write("-") catch {};
}
const bar = bar_stream.getWritten();
// Clear line and print progress
const stderr = std.io.getStdErr().writer();
stderr.print("\r{s} [{s}] {d}/{d} {d}% ({d:.1} items/s)", .{
self.label,
bar,
self.current,
self.total,
percent,
rate,
}) catch {};
}
/// Finish the progress bar and print a newline
pub fn finish(self: ProgressBar) void {
self.render();
const stderr = std.io.getStdErr().writer();
stderr.print("\n", .{}) catch {};
}
/// Complete with a success message
pub fn success(self: ProgressBar, msg: []const u8) void {
self.current = self.total;
self.render();
colors.printSuccess("\n{s}\n", .{msg});
}
/// Get elapsed time in milliseconds
pub fn elapsedMs(self: ProgressBar) i64 {
return std.time.milliTimestamp() - self.start_time;
}
/// Get current throughput (items per second)
pub fn throughput(self: ProgressBar) f64 {
const elapsed_ms = self.elapsedMs();
if (elapsed_ms > 0 and self.current > 0) {
return @as(f64, @floatFromInt(self.current)) / (@as(f64, @floatFromInt(elapsed_ms)) / 1000.0);
}
return 0.0;
}
};
/// Spinner provides visual feedback for indeterminate operations
pub const Spinner = struct {
label: []const u8,
start_time: i64,
frames: []const u8,
frame_idx: usize,
const DEFAULT_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
pub fn init(label: []const u8) Spinner {
return .{
.label = label,
.start_time = std.time.milliTimestamp(),
.frames = DEFAULT_FRAMES,
.frame_idx = 0,
};
}
/// Render one frame of the spinner
pub fn tick(self: *Spinner) void {
const frame = self.frames[self.frame_idx % self.frames.len];
const stderr = std.io.getStdErr().writer();
stderr.print("\r{s} {c} ", .{ self.label, frame }) catch {};
self.frame_idx += 1;
}
/// Stop the spinner and print a newline
pub fn stop(self: Spinner) void {
_ = self; // Intentionally unused - for API consistency
const stderr = std.io.getStdErr().writer();
stderr.print("\n", .{}) catch {};
}
/// Get elapsed time in seconds
pub fn elapsedSec(self: Spinner) i64 {
return @divFloor(std.time.milliTimestamp() - self.start_time, 1000);
}
};