progress

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

commit cb56bb9caa517863be07b1afa6c2259077b2bec7
parent 319aa51b00067afe3413112d4332873241f8ca9c
Author: BrookJeynes <jeynesbrook@gmail.com>
Date:   Sun, 18 Aug 2024 21:17:07 +1000

Officially version 1.0.0!!
- Added spinners! Check out `examples/spinner` for api usage or check
  the docs `zig build docs`.
- Renamed `progress.zig` to `bar.zig`. This change came from the need
  of having two progress variants, `bar.zig -> Bar` and `spinner.zig ->
  Spinner`.
- Added `Bar.isCurrent()` as a thread safe way of checking the current
  progress of a progress bar.
- Updated and split up examples, `examples/bar` and `examples/spinner`.
- Updated `build.zig` to allow running both spinner and bar examples,
  e.g. `zig build run-bar-complex` vs `zig build run-spinner-complex`.
- Updated `README` with new examples.
- Updated docstrings in `bar.zig`
- `Bar.finish()` can now error. This was always the intent but I missed
  a `!`... whoops. This is now fixed.

Diffstat:
MREADME | 24+++++++++++++++++++++---
Mbuild.zig | 28++++++++++++++++++++++++----
Mbuild.zig.zon | 2+-
Aexamples/bar/complex.zig | 26++++++++++++++++++++++++++
Aexamples/bar/simple.zig | 14++++++++++++++
Aexamples/bar/thread.zig | 33+++++++++++++++++++++++++++++++++
Dexamples/complex.zig | 24------------------------
Dexamples/simple.zig | 14--------------
Aexamples/spinner/complex.zig | 23+++++++++++++++++++++++
Aexamples/spinner/simple.zig | 19+++++++++++++++++++
Dexamples/thread.zig | 33---------------------------------
Asrc/bar.zig | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dsrc/progress.zig | 210-------------------------------------------------------------------------------
Asrc/root.zig | 2++
Asrc/spinner.zig | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
15 files changed, 486 insertions(+), 289 deletions(-)

diff --git a/README b/README @@ -1,7 +1,7 @@ progress ======== -A simple thread safe progress bar. +A simple thread safe progress bar and spinner library. Adding to your program ---------------------- @@ -17,9 +17,10 @@ Minimal example --------------- ```zig const std = @import("std"); -const ProgressBar = @import("progress"); +const ProgressBar = @import("progress").Bar; +const ProgressSpinner = @import("progress").Spinner; -pub fn main() !void { +pub fn bar() !void { const stdout = std.io.getStdOut().writer(); var pb = ProgressBar.init(10, stdout.any(), .{}); @@ -30,6 +31,23 @@ pub fn main() !void { std.time.sleep(std.time.ns_per_ms * 150); } } + +pub fn spinner() !void { + const stdout = std.io.getStdOut().writer(); + var ps = ProgressSpinner.init(stdout.any(), .{ + .symbols = ProgressSpinner.PredefinedSymbols.default, + }); + + var iterations: usize = 0; + while (!ps.isFinished()) { + iterations += 1; + try ps.render(); + + if (iterations == 20) try ps.finish(); + + std.time.sleep(std.time.ns_per_ms * 150); + } +} ``` You can find more examples in the `examples/` folder. diff --git a/build.zig b/build.zig @@ -5,7 +5,7 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); const progress = b.addModule("progress", .{ - .root_source_file = b.path("src/progress.zig"), + .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }); @@ -13,7 +13,7 @@ pub fn build(b: *std.Build) void { const docs = b.addInstallDirectory(.{ .source_dir = b.addStaticLibrary(.{ .name = "progress", - .root_source_file = b.path("src/progress.zig"), + .root_source_file = b.path("src/root.zig"), .target = target, .optimize = optimize, }).getEmittedDocs(), @@ -25,10 +25,30 @@ pub fn build(b: *std.Build) void { docs_step.dependOn(&docs.step); for ([_][]const u8{ "simple", "complex", "thread" }) |example| { - const run_step = b.step(b.fmt("run-{s}", .{example}), b.fmt("Run {s}.zig example", .{example})); + const run_step = b.step(b.fmt("run-bar-{s}", .{example}), b.fmt("Run bar/{s}.zig example", .{example})); const exe = b.addExecutable(.{ .name = example, - .root_source_file = b.path(b.fmt("examples/{s}.zig", .{example})), + .root_source_file = b.path(b.fmt("examples/bar/{s}.zig", .{example})), + .target = target, + .optimize = optimize, + }); + exe.root_module.addImport("progress", progress); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(&b.addInstallArtifact(exe, .{}).step); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + run_step.dependOn(&run_cmd.step); + } + + for ([_][]const u8{ "simple", "complex" }) |example| { + const run_step = b.step(b.fmt("run-spinner-{s}", .{example}), b.fmt("Run spinner/{s}.zig example", .{example})); + const exe = b.addExecutable(.{ + .name = example, + .root_source_file = b.path(b.fmt("examples/spinner/{s}.zig", .{example})), .target = target, .optimize = optimize, }); diff --git a/build.zig.zon b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = "progress", - .version = "0.3.0", + .version = "1.0.0", .minimum_zig_version = "0.13.0", diff --git a/examples/bar/complex.zig b/examples/bar/complex.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const ProgressBar = @import("progress").Bar; + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + var pb = ProgressBar.init(28, stdout.any(), .{ + .bar_prefix = '[', + .bar_suffix = ']', + .bar_fill_char = '=', + .description = "A complex progress bar", + .show_iterations = true, + .show_percentage = true, + .write_newline_on_finish = false, + .width = 60, + }); + defer pb.clear() catch {}; + + while (!pb.isFinished()) { + pb.add(1); + try pb.render(); + + if (pb.current_progress == 14) pb.update_description("Half way there"); + + std.time.sleep(std.time.ns_per_ms * 150); + } +} diff --git a/examples/bar/simple.zig b/examples/bar/simple.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const ProgressBar = @import("progress").Bar; + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + var pb = ProgressBar.init(10, stdout.any(), .{}); + + while (!pb.isFinished()) { + pb.add(1); + try pb.render(); + + std.time.sleep(std.time.ns_per_ms * 150); + } +} diff --git a/examples/bar/thread.zig b/examples/bar/thread.zig @@ -0,0 +1,33 @@ +const std = @import("std"); +const ProgressBar = @import("progress").Bar; + +pub fn threadWorker(bar: *ProgressBar, seed: usize) !void { + var rand_impl = std.rand.DefaultPrng.init(seed); + const num = @mod(rand_impl.random().int(u64), 1000); + + for (0..5) |_| { + bar.add(1); + try bar.render(); + + std.time.sleep(std.time.ns_per_ms * num); + } +} + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + var pb = ProgressBar.init(100, stdout.any(), .{ + .description = "Progress bar with threads", + .show_iterations = true, + .show_percentage = true, + }); + + const thread_count = 20; + var threads: [thread_count]std.Thread = undefined; + for (0..thread_count) |i| { + threads[i] = try std.Thread.spawn(.{}, threadWorker, .{ &pb, i }); + } + + for (threads) |thread| { + thread.join(); + } +} diff --git a/examples/complex.zig b/examples/complex.zig @@ -1,24 +0,0 @@ -const std = @import("std"); -const ProgressBar = @import("progress"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - var pb = ProgressBar.init(28, stdout.any(), .{ - .bar_prefix = '[', - .bar_suffix = ']', - .bar_fill_char = '=', - .description = "A complex progress bar", - .show_iterations = true, - .show_percentage = true, - .write_newline_on_finish = false, - .width = 60, - }); - defer pb.clear() catch {}; - - while (!pb.isFinished()) { - 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 @@ -1,14 +0,0 @@ -const std = @import("std"); -const ProgressBar = @import("progress"); - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - var pb = ProgressBar.init(10, stdout.any(), .{}); - - while (!pb.isFinished()) { - pb.add(1); - try pb.render(); - - std.time.sleep(std.time.ns_per_ms * 150); - } -} diff --git a/examples/spinner/complex.zig b/examples/spinner/complex.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const ProgressSpinner = @import("progress").Spinner; + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + var ps = ProgressSpinner.init(stdout.any(), .{ + .description = "Progress spinner", + .symbols = &[_]u21{ '⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽' }, + .write_newline_on_finish = false, + }); + defer ps.clear() catch {}; + + var iterations: usize = 0; + while (!ps.isFinished()) { + iterations += 1; + try ps.render(); + + if (iterations == 10) ps.update_description("Half way there"); + if (iterations == 20) try ps.finish(); + + std.time.sleep(std.time.ns_per_ms * 150); + } +} diff --git a/examples/spinner/simple.zig b/examples/spinner/simple.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const ProgressSpinner = @import("progress").Spinner; + +pub fn main() !void { + const stdout = std.io.getStdOut().writer(); + var ps = ProgressSpinner.init(stdout.any(), .{ + .symbols = ProgressSpinner.PredefinedSymbols.default, + }); + + var iterations: usize = 0; + while (!ps.isFinished()) { + iterations += 1; + try ps.render(); + + if (iterations == 20) try ps.finish(); + + std.time.sleep(std.time.ns_per_ms * 150); + } +} diff --git a/examples/thread.zig b/examples/thread.zig @@ -1,33 +0,0 @@ -const std = @import("std"); -const ProgressBar = @import("progress"); - -pub fn threadWorker(bar: *ProgressBar, seed: usize) !void { - var rand_impl = std.rand.DefaultPrng.init(seed); - const num = @mod(rand_impl.random().int(u64), 1000); - - for (0..5) |_| { - bar.add(1); - try bar.render(); - - std.time.sleep(std.time.ns_per_ms * num); - } -} - -pub fn main() !void { - const stdout = std.io.getStdOut().writer(); - var pb = ProgressBar.init(100, stdout.any(), .{ - .description = "Progress bar with threads", - .show_iterations = true, - .show_percentage = true, - }); - - const thread_count = 20; - var threads: [thread_count]std.Thread = undefined; - for (0..thread_count) |i| { - threads[i] = try std.Thread.spawn(.{}, threadWorker, .{ &pb, i }); - } - - for (threads) |thread| { - thread.join(); - } -} diff --git a/src/bar.zig b/src/bar.zig @@ -0,0 +1,219 @@ +const std = @import("std"); +const termsize = @import("termsize.zig"); +const escape_codes = @import("escape_codes.zig"); + +const Error = error{ + FailedToRender, + ///Used when the progress bar is too small to render all the content. + BarTooSmall, +}; + +///Used when the terminal width cannot be retrieved. +const default_bar_width: u16 = 40; + +const Config = struct { + ///Progress bar description. + description: ?[]const u8 = null, + ///The progress bar prefix. + bar_prefix: u8 = '|', + ///The progress bar suffix. + bar_suffix: u8 = '|', + ///The charater to fill the progress bar with. + bar_fill_char: u8 = '#', + ///Show the iteration count. + show_iterations: bool = false, + ///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, + ///Custom progress bar width. + ///If the width is greater than the terminal width, the terminal width will be used. + ///It is up to you to ensure you provide enough space to render the complete bar. If the bar is too small, calls to `render()` will error. + width: ?usize = null, +}; + +const Bar = @This(); + +///Direct access is not thread safe. Use `currentProgress()` if you need thread safety. +current_progress: usize = 0, +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) Bar { + return Bar{ + .max_progress = max_progress, + .bw = std.io.bufferedWriter(writer), + .config = config, + }; +} + +///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: *Bar, num: usize) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if (self.current_progress + num < self.max_progress) { + self.current_progress += num; + } else { + self.current_progress = self.max_progress; + } +} + +///Render the progress bar. +pub fn render(self: *Bar) !void { + const winsize = try termsize.termSize(std.io.getStdOut()) orelse termsize.TermSize{ .width = default_bar_width, .height = 0 }; + const width = if (self.config.width) |w| @min(w, winsize.width) else winsize.width; + + 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)); + var padding: usize = 0; + + const extra_front_chars = brk: { + var count: usize = 0; + + if (self.config.description) |desc| { + count += (desc.len + 3); // "{desc} ||" + } else { + count += 2; // "||" + } + + if (self.config.show_percentage) count += 4; // "xxx%" + + break :brk count; + }; + + const extra_back_chars = brk: { + var count: usize = 0; + + if (self.config.show_iterations) { + padding += 2; // Add spacing after width. + const num_len: usize = @intFromFloat(@ceil(@log10(@as(f32, @floatFromInt(self.max_progress + 1))))); + + count += 8 + (num_len * 2); // "[ {num_len} / {num_len} ]" + try escape_codes.setCursorColumn(self.bw.writer(), std.math.sub(usize, width + padding, count) catch return Error.BarTooSmall); + + _ = try self.bw.writer().print("[ {[curr]: >[padding]} / {[max]} ]\r", .{ .curr = self.current_progress, .padding = num_len, .max = self.max_progress }); + } + + break :brk count; + }; + + const extra_chars = extra_front_chars + extra_back_chars + padding; + if (extra_chars > width) return Error.BarTooSmall; + + if (self.config.description) |desc| { + _ = try self.bw.write(desc); + try escape_codes.cursorForward(self.bw.writer(), 1); + } + + if (self.config.show_percentage) { + try self.bw.writer().print("{d: >3}%", .{@as(u32, @intFromFloat(percentage * 100))}); + try escape_codes.cursorForward(self.bw.writer(), 1); + } + + _ = try self.bw.writer().writeByte(self.config.bar_prefix); + + const range: usize = @intFromFloat(percentage * @as(f32, @floatFromInt(std.math.sub(usize, width, extra_front_chars + extra_back_chars) catch 0))); + for (0..range) |_| { + _ = try self.bw.writer().writeByte(self.config.bar_fill_char); + } + + try escape_codes.hideCursor(self.bw.writer()); + try escape_codes.setCursorColumn(self.bw.writer(), width - extra_back_chars); + + _ = try self.bw.writer().writeByte(self.config.bar_suffix); + + 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 isFinished(self: *Bar) bool { + self.mutex.lock(); + defer self.mutex.unlock(); + + return self.current_progress >= self.max_progress or self.finished; +} + +///Returns the current progress. +pub fn currentProgress(self: *Bar) usize { + self.mutex.lock(); + defer self.mutex.unlock(); + + return self.current_progress; +} + +///Finish the progress bar. +pub fn finish(self: *Bar) !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. +pub fn reset(self: *Bar) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.finished = false; + self.current_progress = 0; +} + +///Set the progress bar to `num`. +/// +///If `num` is equal to `max_progress`, `finished` is set true. +///If `num` is greater than `max_progress`, `current_progress` will be set to the max. +pub fn set(self: *Bar, num: usize) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if (num >= self.max_progress) { + self.finished = true; + self.current_progress = self.max_progress; + return; + } + + self.current_progress = num; +} + +///Update the description. +pub fn update_description(self: *Bar, description: []const u8) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.config.description = description; +} + +///Clear the progress bar. +/// +///This function is not thread safe. +pub fn clear(self: *Bar) !void { + try escape_codes.clearCurrentLine(self.bw.writer()); + try escape_codes.setCursorColumn(self.bw.writer(), 0); + try self.bw.flush(); +} diff --git a/src/progress.zig b/src/progress.zig @@ -1,210 +0,0 @@ -const std = @import("std"); -const termsize = @import("termsize.zig"); -const escape_codes = @import("escape_codes.zig"); - -const Error = error{ - FailedToRender, - ///Used when the progress bar is too small to render all the content. - BarTooSmall, -}; - -///Used when the terminal width cannot be retrieved. -const default_bar_width: u16 = 40; - -const Config = struct { - ///Progress bar description. - description: ?[]const u8 = null, - ///The progress bar prefix. - bar_prefix: u8 = '|', - ///The progress bar suffix. - bar_suffix: u8 = '|', - ///The charater to fill the progress bar with. - bar_fill_char: u8 = '#', - ///Show the iteration count. - show_iterations: bool = false, - ///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, - ///Custom progress bar width. - ///If the width is greater than the terminal width, the terminal width will be used. - ///It is up to you to ensure you provide enough space to render the complete bar. If the bar is too small, calls to `render()` will error. - width: ?usize = null, -}; - -const ProgressBar = @This(); - -current_progress: usize = 0, -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{ - .max_progress = max_progress, - .bw = std.io.bufferedWriter(writer), - .config = config, - }; -} - -///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) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (self.current_progress + num < self.max_progress) { - self.current_progress += num; - } else { - self.current_progress = self.max_progress; - } -} - -///Render the progress bar. The progress bar must have a `max_progress` greater than 0. -pub fn render(self: *ProgressBar) !void { - const winsize = try termsize.termSize(std.io.getStdOut()) orelse termsize.TermSize{ .width = default_bar_width, .height = 0 }; - const width = if (self.config.width) |w| @min(w, winsize.width) else winsize.width; - - 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)); - var padding: usize = 0; - - const extra_front_chars = brk: { - var count: usize = 0; - - if (self.config.description) |desc| { - count += (desc.len + 3); // "{desc} ||" - } else { - count += 2; // "||" - } - - if (self.config.show_percentage) count += 4; // "xxx%" - - break :brk count; - }; - - const extra_back_chars = brk: { - var count: usize = 0; - - if (self.config.show_iterations) { - padding += 2; // Add spacing after width. - const num_len: usize = @intFromFloat(@ceil(@log10(@as(f32, @floatFromInt(self.max_progress + 1))))); - - count += 8 + (num_len * 2); // "[ {num_len} / {num_len} ]" - try escape_codes.setCursorColumn(self.bw.writer(), std.math.sub(usize, width + padding, count) catch return Error.BarTooSmall); - - _ = try self.bw.writer().print("[ {[curr]: >[padding]} / {[max]} ]\r", .{ .curr = self.current_progress, .padding = num_len, .max = self.max_progress }); - } - - break :brk count; - }; - - const extra_chars = extra_front_chars + extra_back_chars + padding; - if (extra_chars > width) return Error.BarTooSmall; - - if (self.config.description) |desc| { - try self.bw.writer().print("{s}", .{desc}); - try escape_codes.cursorForward(self.bw.writer(), 1); - } - - if (self.config.show_percentage) { - try self.bw.writer().print("{d: >3}%", .{@as(u32, @intFromFloat(percentage * 100))}); - try escape_codes.cursorForward(self.bw.writer(), 1); - } - - _ = try self.bw.writer().writeByte(self.config.bar_prefix); - - const range: usize = @intFromFloat(percentage * @as(f32, @floatFromInt(std.math.sub(usize, width, extra_front_chars + extra_back_chars) catch 0))); - for (0..range) |_| { - _ = try self.bw.writer().writeByte(self.config.bar_fill_char); - } - - try escape_codes.hideCursor(self.bw.writer()); - try escape_codes.setCursorColumn(self.bw.writer(), width - extra_back_chars); - - _ = try self.bw.writer().writeByte(self.config.bar_suffix); - - 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 isFinished(self: *ProgressBar) bool { - self.mutex.lock(); - defer self.mutex.unlock(); - - return self.current_progress >= self.max_progress or self.finished; -} - -///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. -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 `num`. -/// -///If `num` is equal to `max_progress`, `finished` is set true. -///If `num` is greater than `max_progress`, `current_progress` will be set to the max. -pub fn set(self: *ProgressBar, num: usize) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - if (num >= self.max_progress) { - self.finished = true; - self.current_progress = self.max_progress; - return; - } - - self.current_progress = num; -} - -///Update the description. -pub fn update_description(self: *ProgressBar, description: []const u8) void { - self.mutex.lock(); - defer self.mutex.unlock(); - - self.config.description = description; -} - -///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(); -} diff --git a/src/root.zig b/src/root.zig @@ -0,0 +1,2 @@ +pub const Bar = @import("./bar.zig"); +pub const Spinner = @import("./spinner.zig"); diff --git a/src/spinner.zig b/src/spinner.zig @@ -0,0 +1,104 @@ +const std = @import("std"); +const termsize = @import("termsize.zig"); +const escape_codes = @import("escape_codes.zig"); + +pub const PredefinedSymbols = enum { + ///- \ | / + pub const default: []const u21 = &[_]u21{ '-', '\\', '|', '/' }; + ///⎺ ⎻ ⎼ ⎽ ⎼ ⎻ + pub const line: []const u21 = &[_]u21{ '⎺', '⎻', '⎼', '⎽', '⎼', '⎻' }; + ///◑ ◒ ◐ ◓ + pub const moon: []const u21 = &[_]u21{ '◑', '◒', '◐', '◓' }; + ///◷ ◶ ◵ ◴ + pub const pie: []const u21 = &[_]u21{ '◷', '◶', '◵', '◴' }; + ///⣾ ⣷ ⣯ ⣟ ⡿ ⢿ ⣻ ⣽ + pub const pixel: []const u21 = &[_]u21{ '⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽' }; +}; + +const Config = struct { + ///Progress spinner description. + description: ?[]const u8 = null, + symbols: []const u21 = PredefinedSymbols.default, + ///Clear the line when the progress spinner finishes. + clear_on_finish: bool = false, + ///Write a newline when the progress spinner finishes. + write_newline_on_finish: bool = true, +}; + +const Spinner = @This(); + +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, +current_symbol_idx: usize = 0, + +pub fn init(writer: std.io.AnyWriter, config: Config) Spinner { + return Spinner{ + .bw = std.io.bufferedWriter(writer), + .config = config, + }; +} + +///Render the progress spinner. +pub fn render(self: *Spinner) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + if (self.finished) return; + + try self.clear(); + try escape_codes.hideCursor(self.bw.writer()); + + var buf: [8]u8 = undefined; + const bytes = try std.unicode.utf8Encode(self.config.symbols[self.current_symbol_idx], &buf); + _ = try self.bw.write(buf[0..bytes]); + + if (self.config.description) |desc| { + try escape_codes.cursorForward(self.bw.writer(), 1); + _ = try self.bw.write(desc); + } + + try self.bw.flush(); + self.current_symbol_idx = (self.current_symbol_idx + 1) % self.config.symbols.len; +} + +///Returns `true` if the progress spinner is finished and `false` otherwise. +pub fn isFinished(self: *Spinner) bool { + self.mutex.lock(); + defer self.mutex.unlock(); + + return self.finished; +} + +///Finish the progress spinner. +pub fn finish(self: *Spinner) !void { + self.mutex.lock(); + defer self.mutex.unlock(); + + 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(); +} + +///Update the description. +pub fn update_description(self: *Spinner, description: []const u8) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.config.description = description; +} + +///Clear the progress spinner. +/// +///This function is not thread safe. +pub fn clear(self: *Spinner) !void { + try escape_codes.clearCurrentLine(self.bw.writer()); + try escape_codes.setCursorColumn(self.bw.writer(), 0); + try self.bw.flush(); +}