progress

thread safe progress bar and spinner library
git clone git://brookjeynes.dev/bjeynes/progress.git
Log | Files | Refs | README | LICENSE

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 }