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:
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();
+}