progress

progress
git clone git://brookjeynes.dev/bjeynes/progress.git
Log | Files | Refs | README | LICENSE

commit 2f1bb86c2a6a77e565b73b613725a7100c179dbf
parent 9e4b62e03223cec6611c1a4e13d29b07317e509d
Author: Brook J <jeynesbrook@gmail.com>
Date:   Fri,  9 Aug 2024 16:58:46 +1000

`add()` no longer calls render
- `render()` must now be called manually.
- `render()` is now thread-safe.
- Added new config options, `write_newline_on_finish` and
  `clear_on_finish`.
- Fixed issue where progress bar would finish but not restore cursor.
- Updated examples to call `render()`.
- Renamed `finish()` to `isFinished()`.
- Added private attribute `finished` to internally track a finished
  state. This is helpful when dealing with threaded calls to render.
- Updated doc comments.
- Made `clear()` public. Use this when you want to manually clear the
  progress bar. Note that this is different to `reset()` which resets
  the bar but does not clear the line.
- Fixed issue where `set()` was not thread safe.

Diffstat:
MREADME | 2++
Mexamples/complex.zig | 6+++++-
Mexamples/simple.zig | 4+++-
Mexamples/thread.zig | 2++
Msrc/escape_codes.zig | 4++++
Msrc/progress.zig | 69+++++++++++++++++++++++++++++++++++++++++++++++----------------------
6 files changed, 63 insertions(+), 24 deletions(-)

diff --git a/README b/README @@ -25,6 +25,8 @@ pub fn main() !void { while (!pb.finished()) { try pb.add(1); + try pb.render(); + std.time.sleep(std.time.ns_per_ms * 150); } } diff --git a/examples/complex.zig b/examples/complex.zig @@ -10,10 +10,14 @@ pub fn main() !void { .description = "A complex progress bar", .show_iterations = true, .show_percentage = true, + .write_newline_on_finish = false, }); + defer pb.clear() catch {}; - while (!pb.finished()) { + while (!pb.isFinished()) { try pb.add(1); + try pb.render(); + std.time.sleep(std.time.ns_per_ms * 150); } } diff --git a/examples/simple.zig b/examples/simple.zig @@ -5,8 +5,10 @@ pub fn main() !void { const stdout = std.io.getStdOut().writer(); var pb = ProgressBar.init(10, stdout.any(), .{}); - while (!pb.finished()) { + while (!pb.isFinished()) { try pb.add(1); + try pb.render(); + std.time.sleep(std.time.ns_per_ms * 150); } } diff --git a/examples/thread.zig b/examples/thread.zig @@ -7,6 +7,8 @@ pub fn threadWorker(bar: *ProgressBar, seed: usize) !void { for (0..5) |_| { try bar.add(1); + try bar.render(); + std.time.sleep(std.time.ns_per_ms * num); } } diff --git a/src/escape_codes.zig b/src/escape_codes.zig @@ -11,6 +11,10 @@ pub fn hideCursor(writer: anytype) !void { try writer.writeAll(csi ++ "?25l"); } +pub fn showCursor(writer: anytype) !void { + try writer.writeAll(csi ++ "?25h"); +} + pub fn setCursorColumn(writer: anytype, column: usize) !void { try writer.print(csi ++ "{d}G", .{column}); } diff --git a/src/progress.zig b/src/progress.zig @@ -17,10 +17,14 @@ const Config = struct { bar_suffix: u8 = '|', /// The charater to fill the progress bar with. bar_fill_char: u8 = '#', - /// Whether to show the iteration count. + /// Show the iteration count. show_iterations: bool = false, - /// Whether to show the percentage. + /// Show the percentage. show_percentage: bool = false, + /// Clear the line when the progress bar finishes. + clear_on_finish: bool = false, + /// Write a newline when the progress bar finishes. + write_newline_on_finish: bool = true, }; const ProgressBar = @This(); @@ -30,6 +34,8 @@ max_progress: usize = 0, bw: std.io.BufferedWriter(4096, std.io.AnyWriter), config: Config, mutex: std.Thread.Mutex = std.Thread.Mutex{}, +///Direct access is not thread safe. Use `isFinished()` if you need thread safety. +finished: bool = false, pub fn init(max_progress: usize, writer: std.io.AnyWriter, config: Config) ProgressBar { return ProgressBar{ @@ -39,8 +45,7 @@ pub fn init(max_progress: usize, writer: std.io.AnyWriter, config: Config) Progr }; } -/// Add `num` to the progress bar. If `num` is greater than the `max_progress`, -/// `current_progress` will be set to the max. +///Add `num` to the progress bar. If `num` is greater than `max_progress`, `current_progress` will be set to the max. pub fn add(self: *ProgressBar, num: usize) Error!void { self.mutex.lock(); defer self.mutex.unlock(); @@ -54,14 +59,9 @@ pub fn add(self: *ProgressBar, num: usize) Error!void { } else { self.current_progress = self.max_progress; } - - self.render() catch return Error.FailedToRender; } -/// Render the progress bar. The progress bar must have a `max_progress` greater -/// than 0. -/// -/// Render does not need to be explicitly called as `add` will render the bar. +///Render the progress bar. The progress bar must have a `max_progress` greater than 0. pub fn render(self: *ProgressBar) !void { if (self.max_progress == 0) { return Error.MaxIsZero; @@ -69,7 +69,12 @@ pub fn render(self: *ProgressBar) !void { const winsize = try termsize.termSize(std.io.getStdOut()); - try self.clearLine(); + self.mutex.lock(); + defer self.mutex.unlock(); + + if (self.finished) return; + + try self.clear(); const percentage = @as(f32, @floatFromInt(self.current_progress)) / @as(f32, @floatFromInt(self.max_progress)); @@ -122,45 +127,61 @@ pub fn render(self: *ProgressBar) !void { _ = try self.bw.writer().writeByte(self.config.bar_suffix); - if (self.current_progress >= self.max_progress) { - _ = try self.bw.write("\n"); + if (self.current_progress >= self.max_progress and !self.finished) { + self.finished = true; + + if (self.config.clear_on_finish) try self.clear(); + if (self.config.write_newline_on_finish) _ = try self.bw.write("\n"); + try escape_codes.showCursor(self.bw.writer()); } try self.bw.flush(); } -/// Returns `true` if the progress bar is finished and `false` otherwise. -pub fn finished(self: *ProgressBar) bool { +///Returns `true` if the progress bar is finished and `false` otherwise. +pub fn isFinished(self: *ProgressBar) bool { self.mutex.lock(); defer self.mutex.unlock(); - return self.current_progress >= self.max_progress; + return self.current_progress >= self.max_progress or self.finished; } -/// Finish the progress bar. +///Finish the progress bar. pub fn finish(self: *ProgressBar) void { self.mutex.lock(); defer self.mutex.unlock(); + self.finished = true; self.current_progress = self.max_progress; + + try escape_codes.showCursor(self.bw.writer()); + try self.bw.flush(); } -/// Reset the progress bar. +///Reset the progress bar. pub fn reset(self: *ProgressBar) void { self.mutex.lock(); defer self.mutex.unlock(); + self.finished = false; self.current_progress = 0; } -/// Set the progress bar to the `num`. If `num` is greater than the `max_progress`, -/// a `NumGreaterThanMax` error will be returned. +///Set the progress bar to `num`. +/// +///If `num` is equal to `max_progress`, `finished` is set true. +///If `num` is greater than `max_progress`, `Error.NumGreaterThanMax` is returned. pub fn set(self: *ProgressBar, num: usize) Error!void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if (num == self.max_progress) self.finished = true; if (num > self.max_progress) return Error.NumGreaterThanMax; + self.current_progress = num; } -/// Update the description. +///Update the description. pub fn update_description(self: *ProgressBar, description: []const u8) void { self.mutex.lock(); defer self.mutex.unlock(); @@ -168,7 +189,11 @@ pub fn update_description(self: *ProgressBar, description: []const u8) void { self.config.description = description; } -fn clearLine(self: *ProgressBar) !void { +///Clear the progress bar. +/// +///This function is not thread safe. +pub fn clear(self: *ProgressBar) !void { try escape_codes.clearCurrentLine(self.bw.writer()); try escape_codes.setCursorColumn(self.bw.writer(), 0); + try self.bw.flush(); }