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