progress

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

commit 47dc583fc49c7ecc230193144fb6473abbd2af3d
parent c328dcc207abba74d6b66bc38b6cb97f847cf3c8
Author: BrookJeynes <jeynesbrook@gmail.com>
Date:   Thu,  9 Jan 2025 21:05:15 +1000

feat: added the ability to colour progress bar.
feat: added option to show background character for progress bar.
chore: updated version number.
chore: updated examples to showcase new colour options.
refactor: renamed escape_codes.zig to ansi-term.zig and added correct copyright attribution

Diffstat:
Mbuild.zig.zon | 2+-
Mexamples/bar/complex.zig | 14++++++++++----
Asrc/ansi-term.zig | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/bar.zig | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Dsrc/escape_codes.zig | 24------------------------
Msrc/spinner.zig | 14+++++++-------
6 files changed, 174 insertions(+), 48 deletions(-)

diff --git a/build.zig.zon b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = "progress", - .version = "1.1.0", + .version = "1.2.0", .minimum_zig_version = "0.13.0", diff --git a/examples/bar/complex.zig b/examples/bar/complex.zig @@ -8,20 +8,26 @@ pub fn main() !void { .bar_prefix = '[', .bar_suffix = ']', .bar_fill_char = '=', - .description = "A complex progress bar", + .description = "Bar 1", .show_iterations = true, .show_percentage = true, .width = 60, }); + pb1.setColour(.Blue); + var pb2 = ProgressBar.init(15, stdout.any(), .{ .bar_prefix = '|', .bar_suffix = '|', - .bar_fill_char = '█', - .description = "A second complex progress bar", + .bar_fill_char = '━', + .description = "Bar 2", .show_iterations = true, .show_percentage = true, .width = 100, + .show_background = true, }); + pb2.setColour(.{ .RGB = .{ .r = 255, .g = 0, .b = 127 } }); + pb2.setBgColour(.{ .RGB = .{ .r = 120, .g = 0, .b = 127 } }); + const progress_bars: [2]*ProgressBar = .{ &pb1, &pb2 }; for (progress_bars, 0..) |pb, i| { @@ -30,7 +36,7 @@ pub fn main() !void { try pb.render(); if (i == 0) { - if (pb.current_progress == 14) pb.updateDescription("Half way there"); + if (pb.current_progress == 14) pb.updateDescription("Bar 1: nearly there"); } std.time.sleep(std.time.ns_per_ms * 150); diff --git a/src/ansi-term.zig b/src/ansi-term.zig @@ -0,0 +1,93 @@ +// MIT License +// +// Copyright (c) 2019 joachimschmidt557 +// +// 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/ziglibs/ansi-term + +const std = @import("std"); + +pub const RGB = struct { + r: u8, + g: u8, + b: u8, +}; + +pub const Colour = union(enum) { + Default, + Black, + White, + Red, + Green, + Blue, + Yellow, + Magenta, + Cyan, + Fixed: u8, + Grey: u8, + RGB: RGB, +}; + +const esc = "\x1B"; +const csi = esc ++ "["; +const reset = csi ++ "0m"; + +pub fn clearCurrentLine(writer: anytype) !void { + try writer.writeAll(csi ++ "2K"); +} + +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}); +} + +pub fn cursorForward(writer: anytype, columns: usize) !void { + try writer.print(csi ++ "{d}C", .{columns}); +} + +pub fn writeColour(writer: anytype, colour: Colour) !void { + try writer.writeAll(csi); + _ = switch (colour) { + .Default => try writer.writeAll("39"), + .Black => try writer.writeAll("30"), + .Red => try writer.writeAll("31"), + .Green => try writer.writeAll("32"), + .Yellow => try writer.writeAll("33"), + .Blue => try writer.writeAll("34"), + .Magenta => try writer.writeAll("35"), + .Cyan => try writer.writeAll("36"), + .White => try writer.writeAll("37"), + .Fixed => |fixed| try writer.print("48;5;{}", .{fixed}), + .Grey => |grey| try writer.print("48;2;{};{};{}", .{ grey, grey, grey }), + .RGB => |rgb| try writer.print("38;2;{};{};{}", .{ rgb.r, rgb.g, rgb.b }), + }; + try writer.writeAll("m"); +} + +pub fn resetColour(writer: anytype) !void { + try writer.writeAll(reset); +} diff --git a/src/bar.zig b/src/bar.zig @@ -1,6 +1,6 @@ const std = @import("std"); const termsize = @import("termsize.zig"); -const escape_codes = @import("escape_codes.zig"); +const ansi_term = @import("ansi-term.zig"); const Error = error{ FailedToRender, @@ -22,6 +22,8 @@ const Config = struct { bar_fill_char: u21 = '#', ///Show the iteration count. show_iterations: bool = false, + ///Show the progress bar background. + show_background: bool = false, ///Show the percentage. show_percentage: bool = false, ///Clear the line when the progress bar finishes. @@ -44,6 +46,10 @@ 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, +///The progress bar foreground colour. +colour: ansi_term.Colour = .Default, +///The progress bar background colour. +bg_colour: ansi_term.Colour = .Default, pub fn init(max_progress: usize, writer: std.io.AnyWriter, config: Config) Bar { return Bar{ @@ -104,7 +110,7 @@ pub fn render(self: *Bar) !void { 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 ansi_term.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 }); } @@ -117,25 +123,38 @@ pub fn render(self: *Bar) !void { if (self.config.description) |desc| { _ = try self.bw.write(desc); - try escape_codes.cursorForward(self.bw.writer(), 1); + try ansi_term.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 ansi_term.cursorForward(self.bw.writer(), 1); } const prefix_bytes = try std.unicode.utf8Encode(self.config.bar_prefix, &unicode_conversion_buf); _ = try self.bw.write(unicode_conversion_buf[0..prefix_bytes]); - const range: usize = @intFromFloat(percentage * @as(f32, @floatFromInt(std.math.sub(usize, width, extra_front_chars + extra_back_chars) catch 0))); - for (0..range) |_| { + const max_percentage_pos: usize = std.math.sub(usize, width, extra_front_chars + extra_back_chars) catch 0; + const current_percentage_pos: usize = @intFromFloat(percentage * @as(f32, @floatFromInt(std.math.sub(usize, width, extra_front_chars + extra_back_chars) catch 0))); + for (0..max_percentage_pos) |write_pos| { + if (write_pos > current_percentage_pos) { + if (self.config.show_background) { + try ansi_term.writeColour(self.bw.writer(), self.bg_colour); + const fill_char_bytes = try std.unicode.utf8Encode(self.config.bar_fill_char, &unicode_conversion_buf); + _ = try self.bw.write(unicode_conversion_buf[0..fill_char_bytes]); + try ansi_term.resetColour(self.bw.writer()); + } + continue; + } + + try ansi_term.writeColour(self.bw.writer(), self.colour); const fill_char_bytes = try std.unicode.utf8Encode(self.config.bar_fill_char, &unicode_conversion_buf); _ = try self.bw.write(unicode_conversion_buf[0..fill_char_bytes]); + try ansi_term.resetColour(self.bw.writer()); } - try escape_codes.hideCursor(self.bw.writer()); - try escape_codes.setCursorColumn(self.bw.writer(), width - extra_back_chars); + try ansi_term.hideCursor(self.bw.writer()); + try ansi_term.setCursorColumn(self.bw.writer(), width - extra_back_chars); const suffix_bytes = try std.unicode.utf8Encode(self.config.bar_suffix, &unicode_conversion_buf); _ = try self.bw.write(unicode_conversion_buf[0..suffix_bytes]); @@ -145,12 +164,44 @@ pub fn render(self: *Bar) !void { 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 ansi_term.showCursor(self.bw.writer()); } try self.bw.flush(); } +///Set the progress bar colour. +pub fn setColour(self: *Bar, colour: ansi_term.Colour) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.colour = colour; +} + +///Set the progress bar background colour. +pub fn setBgColour(self: *Bar, colour: ansi_term.Colour) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.bg_colour = colour; +} + +///Show the progress bar background. +pub fn showBg(self: *Bar) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.config.show_background = true; +} + +///Hide the progress bar background. +pub fn hideBg(self: *Bar) void { + self.mutex.lock(); + defer self.mutex.unlock(); + + self.config.show_background = false; +} + ///Returns `true` if the progress bar is finished and `false` otherwise. pub fn isFinished(self: *Bar) bool { self.mutex.lock(); @@ -175,7 +226,7 @@ pub fn finish(self: *Bar) !void { self.finished = true; self.current_progress = self.max_progress; - try escape_codes.showCursor(self.bw.writer()); + try ansi_term.showCursor(self.bw.writer()); try self.bw.flush(); } @@ -217,7 +268,7 @@ pub fn updateDescription(self: *Bar, description: []const u8) void { /// ///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 ansi_term.clearCurrentLine(self.bw.writer()); + try ansi_term.setCursorColumn(self.bw.writer(), 0); try self.bw.flush(); } diff --git a/src/escape_codes.zig b/src/escape_codes.zig @@ -1,24 +0,0 @@ -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 showCursor(writer: anytype) !void { - try writer.writeAll(csi ++ "?25h"); -} - -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/spinner.zig b/src/spinner.zig @@ -1,6 +1,6 @@ const std = @import("std"); const termsize = @import("termsize.zig"); -const escape_codes = @import("escape_codes.zig"); +const ansi_term = @import("ansi-term.zig"); pub const PredefinedSymbols = enum { ///- \ | / @@ -50,7 +50,7 @@ fn renderComplete(self: *Spinner, completion_char: u21) !void { _ = try self.bw.write(buf[0..bytes]); if (self.config.description) |desc| { - try escape_codes.cursorForward(self.bw.writer(), 1); + try ansi_term.cursorForward(self.bw.writer(), 1); _ = try self.bw.write(desc); } @@ -67,14 +67,14 @@ pub fn render(self: *Spinner) !void { if (self.finished) return; try self.clear(); - try escape_codes.hideCursor(self.bw.writer()); + try ansi_term.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 ansi_term.cursorForward(self.bw.writer(), 1); _ = try self.bw.write(desc); } @@ -104,7 +104,7 @@ pub fn finish(self: *Spinner) !void { 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 ansi_term.showCursor(self.bw.writer()); try self.bw.flush(); } @@ -135,7 +135,7 @@ pub fn updateDescriptionNewline(self: *Spinner, description: []const u8) !void { /// ///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 ansi_term.clearCurrentLine(self.bw.writer()); + try ansi_term.setCursorColumn(self.bw.writer(), 0); try self.bw.flush(); }