bar.zig (9690B)
1 const std = @import("std"); 2 const termsize = @import("termsize.zig"); 3 const ansi_term = @import("ansi-term.zig"); 4 5 const Error = error{ 6 ///Used when the progress bar is too small to render all the content. 7 BarTooSmall, 8 }; 9 10 ///Used when the terminal width cannot be retrieved. 11 const default_bar_width: u16 = 40; 12 13 pub const Config = struct { 14 ///Progress bar description. 15 description: ?[]const u8 = null, 16 ///The progress bar prefix. 17 bar_prefix: u21 = '|', 18 ///The progress bar suffix. 19 bar_suffix: u21 = '|', 20 ///The charater to fill the progress bar with. 21 bar_fill_char: u21 = '#', 22 ///Show the iteration count. 23 show_iterations: bool = false, 24 ///Show the progress bar background. 25 show_background: bool = false, 26 ///Show the percentage. 27 show_percentage: bool = false, 28 ///Clear the line when the progress bar finishes. 29 clear_on_finish: bool = false, 30 ///Write a newline when the progress bar finishes. In the case of a `MultiBar`, this will always be `false`. 31 write_newline_on_finish: bool = true, 32 ///Custom progress bar width. 33 ///If the width is greater than the terminal width, the terminal width will be used. 34 ///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. 35 width: ?usize = null, 36 ///The progress bar foreground colour. 37 colour: ansi_term.Colour = .Default, 38 ///The progress bar background colour. 39 bg_colour: ansi_term.Colour = .Default, 40 }; 41 42 pub const Bar = @This(); 43 44 ///Direct access is not thread safe. Use `currentProgress()` if you need thread safety. 45 current_progress: usize = 0, 46 max_progress: usize = 0, 47 writer: *std.Io.File.Writer, 48 config: Config, 49 mutex: std.Io.Mutex = std.Io.Mutex.init, 50 ///Direct access is not thread safe. Use `isFinished()` if you need thread safety. 51 finished: bool = false, 52 53 pub fn init(max_progress: usize, writer: *std.Io.File.Writer, config: Config) Bar { 54 return Bar{ 55 .max_progress = max_progress, 56 .writer = writer, 57 .config = config, 58 }; 59 } 60 61 ///Add `num` to the progress bar. 62 ///If `num` is greater than `max_progress`, `current_progress` will be set to the max. 63 pub fn add(self: *Bar, num: usize) void { 64 self.mutex.lockUncancelable(self.writer.io); 65 defer self.mutex.unlock(self.writer.io); 66 67 if (self.current_progress + num < self.max_progress) { 68 self.current_progress += num; 69 } else { 70 self.current_progress = self.max_progress; 71 } 72 } 73 74 ///Render the progress bar. 75 pub fn render(self: *Bar) !void { 76 try self.mutex.lock(self.writer.io); 77 defer self.mutex.unlock(self.writer.io); 78 79 const winsize = try termsize.termSize(std.Io.File.stdout(), self.writer.io) orelse termsize.TermSize{ .width = default_bar_width, .height = 0 }; 80 const width = if (self.config.width) |w| @min(w, winsize.width) else winsize.width; 81 var unicode_conversion_buf: [8]u8 = undefined; 82 83 if (self.finished) return; 84 85 try self.clear(); 86 87 const percentage = @as(f32, @floatFromInt(self.current_progress)) / @as(f32, @floatFromInt(self.max_progress)); 88 var padding: usize = 0; 89 90 const prefix_len: usize = std.unicode.utf8CodepointSequenceLength(self.config.bar_prefix) catch 1; 91 const suffix_len: usize = std.unicode.utf8CodepointSequenceLength(self.config.bar_suffix) catch 1; 92 93 const extra_front_chars = brk: { 94 var count: usize = prefix_len + suffix_len; 95 96 if (self.config.description) |desc| { 97 count += desc.len + 1; // "{desc} + one space" 98 } 99 100 if (self.config.show_percentage) count += 4; // "xxx%" 101 102 break :brk count; 103 }; 104 105 const extra_back_chars = brk: { 106 var count: usize = 0; 107 108 if (self.config.show_iterations) { 109 padding += 2; // Add spacing after width. 110 const num_len: usize = @intFromFloat(@ceil(@log10(@as(f32, @floatFromInt(self.max_progress + 1))))); 111 112 count += 8 + (num_len * 2); // "[ {num_len} / {num_len} ]" 113 try ansi_term.setCursorColumn(&self.writer.interface, std.math.sub(usize, width + padding, count) catch return Error.BarTooSmall); 114 115 try self.writer.interface.print("[ {[curr]: >[padding]} / {[max]} ]\r", .{ .curr = self.current_progress, .padding = num_len, .max = self.max_progress }); 116 } 117 118 break :brk count; 119 }; 120 121 const extra_chars = extra_front_chars + extra_back_chars + padding; 122 if (extra_chars > width) return Error.BarTooSmall; 123 124 if (self.config.description) |desc| { 125 try self.writer.interface.writeAll(desc); 126 try ansi_term.cursorForward(&self.writer.interface, 1); 127 } 128 129 if (self.config.show_percentage) { 130 try self.writer.interface.print("{d: >3}%", .{@as(u32, @intFromFloat(percentage * 100))}); 131 try ansi_term.cursorForward(&self.writer.interface, 1); 132 } 133 134 const prefix_bytes = try std.unicode.utf8Encode(self.config.bar_prefix, &unicode_conversion_buf); 135 try self.writer.interface.writeAll(unicode_conversion_buf[0..prefix_bytes]); 136 137 const max_percentage_pos: usize = std.math.sub(usize, width, extra_front_chars + extra_back_chars) catch 0; 138 const current_percentage_pos: usize = @intFromFloat(percentage * @as(f32, @floatFromInt(max_percentage_pos))); 139 for (0..max_percentage_pos) |write_pos| { 140 if (write_pos > current_percentage_pos) { 141 if (self.config.show_background) { 142 try ansi_term.writeColour(&self.writer.interface, self.config.bg_colour); 143 const fill_char_bytes = try std.unicode.utf8Encode(self.config.bar_fill_char, &unicode_conversion_buf); 144 try self.writer.interface.writeAll(unicode_conversion_buf[0..fill_char_bytes]); 145 try ansi_term.resetColour(&self.writer.interface); 146 } 147 continue; 148 } 149 150 try ansi_term.writeColour(&self.writer.interface, self.config.colour); 151 const fill_char_bytes = try std.unicode.utf8Encode(self.config.bar_fill_char, &unicode_conversion_buf); 152 try self.writer.interface.writeAll(unicode_conversion_buf[0..fill_char_bytes]); 153 try ansi_term.resetColour(&self.writer.interface); 154 } 155 156 try ansi_term.hideCursor(&self.writer.interface); 157 try ansi_term.setCursorColumn( 158 &self.writer.interface, 159 std.math.sub(usize, width, extra_back_chars) catch return Error.BarTooSmall, 160 ); 161 162 const suffix_bytes = try std.unicode.utf8Encode(self.config.bar_suffix, &unicode_conversion_buf); 163 try self.writer.interface.writeAll(unicode_conversion_buf[0..suffix_bytes]); 164 165 if (self.current_progress >= self.max_progress and !self.finished) { 166 self.finished = true; 167 168 if (self.config.clear_on_finish) try self.clear(); 169 if (self.config.write_newline_on_finish) try self.writer.interface.writeAll("\n"); 170 try ansi_term.showCursor(&self.writer.interface); 171 } 172 173 try self.writer.interface.flush(); 174 } 175 176 ///Update the progress bar colour. 177 pub fn updateColour(self: *Bar, colour: ansi_term.Colour) void { 178 self.mutex.lockUncancelable(self.writer.io); 179 defer self.mutex.unlock(self.writer.io); 180 181 self.config.colour = colour; 182 } 183 184 ///Update the progress bar background colour. 185 pub fn updateBgColour(self: *Bar, colour: ansi_term.Colour) void { 186 self.mutex.lockUncancelable(self.writer.io); 187 defer self.mutex.unlock(self.writer.io); 188 189 self.config.bg_colour = colour; 190 } 191 192 ///Toggle the progress bar background. 193 pub fn setShowBg(self: *Bar, visible: bool) void { 194 self.mutex.lockUncancelable(self.writer.io); 195 defer self.mutex.unlock(self.writer.io); 196 self.config.show_background = visible; 197 } 198 199 ///Returns `true` if the progress bar is finished and `false` otherwise. 200 pub fn isFinished(self: *Bar) bool { 201 self.mutex.lockUncancelable(self.writer.io); 202 defer self.mutex.unlock(self.writer.io); 203 204 return self.current_progress >= self.max_progress or self.finished; 205 } 206 207 ///Returns the current progress. 208 pub fn currentProgress(self: *Bar) usize { 209 self.mutex.lockUncancelable(self.writer.io); 210 defer self.mutex.unlock(self.writer.io); 211 212 return self.current_progress; 213 } 214 215 ///Finish the progress bar. 216 pub fn finish(self: *Bar) !void { 217 try self.mutex.lock(self.writer.io); 218 defer self.mutex.unlock(self.writer.io); 219 220 self.finished = true; 221 self.current_progress = self.max_progress; 222 223 try ansi_term.showCursor(&self.writer.interface); 224 try self.writer.interface.flush(); 225 } 226 227 ///Reset the progress bar. 228 pub fn reset(self: *Bar) void { 229 self.mutex.lockUncancelable(self.writer.io); 230 defer self.mutex.unlock(self.writer.io); 231 232 self.finished = false; 233 self.current_progress = 0; 234 } 235 236 ///Set the progress bar to `num`. 237 /// 238 ///If `num` is equal to `max_progress`, `finished` is set true. 239 ///If `num` is greater than `max_progress`, `current_progress` will be set to the max. 240 pub fn set(self: *Bar, num: usize) void { 241 self.mutex.lockUncancelable(self.writer.io); 242 defer self.mutex.unlock(self.writer.io); 243 244 if (num >= self.max_progress) { 245 self.finished = true; 246 self.current_progress = self.max_progress; 247 return; 248 } 249 250 self.current_progress = num; 251 } 252 253 ///Update the description. 254 pub fn updateDescription(self: *Bar, description: []const u8) void { 255 self.mutex.lockUncancelable(self.writer.io); 256 defer self.mutex.unlock(self.writer.io); 257 258 self.config.description = description; 259 } 260 261 ///Clear the progress bar. 262 /// 263 ///This function is not thread safe. 264 pub fn clear(self: *Bar) !void { 265 try ansi_term.clearCurrentLine(&self.writer.interface); 266 try ansi_term.setCursorColumn(&self.writer.interface, 0); 267 try self.writer.interface.flush(); 268 }