feat(cli): add progress UI and rsync assets
- Add progress.zig for sync progress display - Add rsync placeholder and release binaries to assets/rsync/
This commit is contained in:
parent
2258f60ade
commit
7583932897
3 changed files with 167 additions and 0 deletions
15
cli/src/assets/rsync/rsync_placeholder.bin
Executable file
15
cli/src/assets/rsync/rsync_placeholder.bin
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/bin/bash
|
||||
# Rsync wrapper for development builds
|
||||
# This calls the system's rsync instead of embedding a full binary
|
||||
# Keeps the dev binary small (152KB) while still functional
|
||||
|
||||
# Find rsync on the system
|
||||
RSYNC_PATH=$(which rsync 2>/dev/null || echo "/usr/bin/rsync")
|
||||
|
||||
if [ ! -x "$RSYNC_PATH" ]; then
|
||||
echo "Error: rsync not found on system. Please install rsync or use a release build with embedded rsync." >&2
|
||||
exit 127
|
||||
fi
|
||||
|
||||
# Pass all arguments to system rsync
|
||||
exec "$RSYNC_PATH" "$@"
|
||||
1
cli/src/assets/rsync/rsync_release.bin
Symbolic link
1
cli/src/assets/rsync/rsync_release.bin
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
rsync_placeholder.bin
|
||||
151
cli/src/ui/progress.zig
Normal file
151
cli/src/ui/progress.zig
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
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);
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue