commit b0bcbb073ea80c00359a4648cc60e20d579bcbe4
Author: Brook J <jeynesbrook@gmail.com>
Date: Fri, 9 Aug 2024 00:09:19 +1000
0.1.0 release
Diffstat:
10 files changed, 445 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+.zig-cache/
+zig-out/
diff --git a/README b/README
@@ -0,0 +1,39 @@
+progress
+========
+
+A simple thread safe progress bar.
+
+Adding to your program
+----------------------
+1. Fetch the package.
+ `zig fetch --save git+https://github.com/BrookJeynes/progress`
+2. Add to your `build.zig`.
+ ```
+ const progress = b.dependency("progress", .{}).module("progress");
+ exe.root_module.addImport("progress", progress);
+ ```
+
+Minimal example
+---------------
+```zig
+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.finished()) {
+ try pb.add(1);
+ std.time.sleep(std.time.ns_per_ms * 150);
+ }
+}
+```
+
+You can find more examples in the `examples/` folder.
+For more information, see the source code or documentation (`zig build docs`).
+
+Contributing
+-----------
+Contributions, issues, and feature requests are always welcome! This project is
+using the latest stable release of Zig (0.13.0).
diff --git a/build.zig b/build.zig
@@ -0,0 +1,46 @@
+const std = @import("std");
+
+pub fn build(b: *std.Build) void {
+ const target = b.standardTargetOptions(.{});
+ const optimize = b.standardOptimizeOption(.{});
+
+ const progress = b.addModule("progress", .{
+ .root_source_file = b.path("src/progress.zig"),
+ .target = target,
+ .optimize = optimize,
+ });
+
+ const docs = b.addInstallDirectory(.{
+ .source_dir = b.addStaticLibrary(.{
+ .name = "progress",
+ .root_source_file = b.path("src/progress.zig"),
+ .target = target,
+ .optimize = optimize,
+ }).getEmittedDocs(),
+ .install_dir = .prefix,
+ .install_subdir = "docs",
+ });
+
+ const docs_step = b.step("docs", "Generate docs");
+ 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 exe = b.addExecutable(.{
+ .name = example,
+ .root_source_file = b.path(b.fmt("examples/{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);
+ }
+}
diff --git a/build.zig.zon b/build.zig.zon
@@ -0,0 +1,14 @@
+.{
+ .name = "progress",
+ .version = "0.1.0",
+
+ .minimum_zig_version = "0.13.0",
+
+ .dependencies = .{},
+
+ .paths = .{
+ "build.zig",
+ "build.zig.zon",
+ "src",
+ },
+}
diff --git a/examples/complex.zig b/examples/complex.zig
@@ -0,0 +1,19 @@
+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,
+ });
+
+ while (!pb.finished()) {
+ try pb.add(1);
+ std.time.sleep(std.time.ns_per_ms * 150);
+ }
+}
diff --git a/examples/simple.zig b/examples/simple.zig
@@ -0,0 +1,12 @@
+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.finished()) {
+ try pb.add(1);
+ std.time.sleep(std.time.ns_per_ms * 150);
+ }
+}
diff --git a/examples/thread.zig b/examples/thread.zig
@@ -0,0 +1,31 @@
+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) |_| {
+ try bar.add(1);
+ 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/escape_codes.zig b/src/escape_codes.zig
@@ -0,0 +1,20 @@
+const std = @import("std");
+
+const esc = "\x1B";
+const csi = esc ++ "[";
+
+pub fn clearCurrentLine(writer: anytype) !void {
+ try writer.writeAll(csi ++ "2K");
+}
+
+pub fn hideCursor(writer: anytype) !void {
+ try writer.writeAll(csi ++ "?25l");
+}
+
+pub fn setCursorColumn(writer: anytype, column: usize) !void {
+ try writer.print(csi ++ "{d}G", .{column});
+}
+
+pub fn cursorForward(writer: anytype, columns: usize) !void {
+ try writer.print(csi ++ "{d}C", .{columns});
+}
diff --git a/src/progress.zig b/src/progress.zig
@@ -0,0 +1,174 @@
+const std = @import("std");
+const termsize = @import("termsize.zig");
+const escape_codes = @import("escape_codes.zig");
+
+const Error = error{
+ MaxIsZero,
+ FailedToRender,
+ NumGreaterThanMax,
+};
+
+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 = '#',
+ /// Whether to show the iteration count.
+ show_iterations: bool = false,
+ /// Whether to show the percentage.
+ show_percentage: bool = false,
+};
+
+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{},
+
+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 the `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();
+
+ if (self.max_progress == 0) {
+ return Error.MaxIsZero;
+ }
+
+ if (self.current_progress + num < self.max_progress) {
+ self.current_progress += num;
+ } 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.
+///
+/// This function is not thread safe so it must be called with an acquired lock.
+pub fn render(self: *ProgressBar) !void {
+ if (self.max_progress == 0) {
+ return Error.MaxIsZero;
+ }
+
+ const winsize = try termsize.termSize(std.io.getStdOut());
+
+ try self.clearLine();
+
+ const percentage = @as(f32, @floatFromInt(self.current_progress)) / @as(f32, @floatFromInt(self.max_progress));
+
+ 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 += 5;
+
+ break :brk count;
+ };
+
+ const extra_back_chars = brk: {
+ var count: usize = 0;
+
+ if (self.config.show_iterations) {
+ 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(), winsize.?.width - count + 2);
+ _ = try self.bw.writer().print("[ {[curr]: >[padding]} / {[max]} ]\r", .{ .curr = self.current_progress, .padding = num_len, .max = self.max_progress });
+ }
+
+ break :brk count;
+ };
+
+ 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, winsize.?.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(), winsize.?.width - extra_back_chars);
+
+ _ = try self.bw.writer().writeByte(self.config.bar_suffix);
+
+ if (self.current_progress >= self.max_progress) {
+ _ = try self.bw.write("\n");
+ }
+
+ try self.bw.flush();
+}
+
+/// Returns `true` if the progress bar is finished and `false` otherwise.
+pub fn finished(self: *ProgressBar) bool {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ return self.current_progress >= self.max_progress;
+}
+
+/// Finish the progress bar.
+pub fn finish(self: *ProgressBar) void {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ self.current_progress = self.max_progress;
+}
+
+/// Reset the progress bar.
+pub fn reset(self: *ProgressBar) void {
+ self.mutex.lock();
+ defer self.mutex.unlock();
+
+ 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.
+pub fn set(self: *ProgressBar, num: usize) Error!void {
+ if (num > self.max_progress) return Error.NumGreaterThanMax;
+ 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;
+}
+
+fn clearLine(self: *ProgressBar) !void {
+ try escape_codes.clearCurrentLine(self.bw.writer());
+ try escape_codes.setCursorColumn(self.bw.writer(), 0);
+}
diff --git a/src/termsize.zig b/src/termsize.zig
@@ -0,0 +1,88 @@
+// Copyright (c) 2024 Doug Tangren
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// https://github.com/softprops/zig-termsize
+
+const std = @import("std");
+const builtin = @import("builtin");
+const io = std.io;
+const os = std.os;
+
+/// Terminal size dimensions
+pub const TermSize = struct {
+ /// Terminal width as measured number of characters that fit into a terminal horizontally
+ width: u16,
+ /// terminal height as measured number of characters that fit into terminal vertically
+ height: u16,
+};
+
+/// supports windows, linux, macos
+///
+/// ## example
+///
+/// ```zig
+/// const std = @import("std");
+/// const termSize = @import("termSize");
+///
+/// fn main() !void {
+/// std.debug.print(
+/// "{any}",
+/// termSize.termSize(std.os.getStdOut()),
+/// );
+/// }
+/// ```
+pub fn termSize(file: std.fs.File) !?TermSize {
+ if (!file.supportsAnsiEscapeCodes()) {
+ return null;
+ }
+ return switch (builtin.os.tag) {
+ .windows => blk: {
+ var buf: os.windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
+ break :blk switch (os.windows.kernel32.GetConsoleScreenBufferInfo(
+ file.handle,
+ &buf,
+ )) {
+ os.windows.TRUE => TermSize{
+ .width = @intCast(buf.srWindow.Right - buf.srWindow.Left + 1),
+ .height = @intCast(buf.srWindow.Bottom - buf.srWindow.Top + 1),
+ },
+ else => error.Unexpected,
+ };
+ },
+ .linux, .macos => blk: {
+ var buf: std.posix.system.winsize = undefined;
+ break :blk switch (std.posix.errno(
+ std.posix.system.ioctl(
+ file.handle,
+ std.posix.T.IOCGWINSZ,
+ @intFromPtr(&buf),
+ ),
+ )) {
+ .SUCCESS => TermSize{
+ .width = buf.ws_col,
+ .height = buf.ws_row,
+ },
+ else => error.IoctlError,
+ };
+ },
+ else => error.Unsupported,
+ };
+}