diff --git a/cli/src/assets/rsync/rsync_placeholder.bin b/cli/src/assets/rsync/rsync_placeholder.bin new file mode 100755 index 0000000..1db52e2 --- /dev/null +++ b/cli/src/assets/rsync/rsync_placeholder.bin @@ -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" "$@" diff --git a/cli/src/assets/rsync/rsync_release.bin b/cli/src/assets/rsync/rsync_release.bin new file mode 120000 index 0000000..5cd3bd9 --- /dev/null +++ b/cli/src/assets/rsync/rsync_release.bin @@ -0,0 +1 @@ +rsync_placeholder.bin \ No newline at end of file diff --git a/cli/src/ui/progress.zig b/cli/src/ui/progress.zig new file mode 100644 index 0000000..bee6c2f --- /dev/null +++ b/cli/src/ui/progress.zig @@ -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); + } +};