progress

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

spinner.zig (4436B)


      1 const std = @import("std");
      2 const ansi_term = @import("ansi-term.zig");
      3 
      4 pub const PredefinedSymbols = struct {
      5     ///- \ | /
      6     pub const default: []const u21 = &[_]u21{ '-', '\\', '|', '/' };
      7     ///⎺ ⎻ ⎼ ⎽ ⎼ ⎻
      8     pub const line: []const u21 = &[_]u21{ '⎺', '⎻', '⎼', '⎽', '⎼', '⎻' };
      9     ///◑ ◒ ◐ ◓
     10     pub const moon: []const u21 = &[_]u21{ '◑', '◒', '◐', '◓' };
     11     ///◷ ◶ ◵ ◴
     12     pub const pie: []const u21 = &[_]u21{ '◷', '◶', '◵', '◴' };
     13     ///⣾ ⣷ ⣯ ⣟ ⡿ ⢿ ⣻ ⣽
     14     pub const pixel: []const u21 = &[_]u21{ '⣾', '⣷', '⣯', '⣟', '⡿', '⢿', '⣻', '⣽' };
     15 };
     16 
     17 const Config = struct {
     18     ///Progress spinner description.
     19     description: ?[]const u8 = null,
     20     symbols: []const u21 = PredefinedSymbols.default,
     21     ///Clear the line when the progress spinner finishes.
     22     clear_on_finish: bool = false,
     23     ///Write a newline when the progress spinner finishes.
     24     write_newline_on_finish: bool = true,
     25     ///Character to write on completion.
     26     completion_character: ?u21 = null,
     27 };
     28 
     29 const Spinner = @This();
     30 
     31 writer: *std.Io.File.Writer,
     32 config: Config,
     33 mutex: std.Io.Mutex = std.Io.Mutex.init,
     34 ///Direct access is not thread safe. Use `isFinished()` if you need thread safety.
     35 finished: bool = false,
     36 current_symbol_idx: usize = 0,
     37 
     38 pub fn init(writer: *std.Io.File.Writer, config: Config) Spinner {
     39     return Spinner{
     40         .writer = writer,
     41         .config = config,
     42     };
     43 }
     44 
     45 fn renderComplete(self: *Spinner, completion_char: u21) !void {
     46     try self.clear();
     47     var buf: [8]u8 = undefined;
     48     const bytes = try std.unicode.utf8Encode(completion_char, &buf);
     49     try self.writer.interface.writeAll(buf[0..bytes]);
     50 
     51     if (self.config.description) |desc| {
     52         try ansi_term.cursorForward(&self.writer.interface, 1);
     53         try self.writer.interface.writeAll(desc);
     54     }
     55 
     56     try self.writer.interface.flush();
     57 }
     58 
     59 ///Render the progress spinner and advance a visual cycle.
     60 pub fn render(self: *Spinner) !void {
     61     try self.mutex.lock(self.writer.io);
     62     defer self.mutex.unlock(self.writer.io);
     63 
     64     if (self.finished) return;
     65 
     66     try self.clear();
     67     try ansi_term.hideCursor(&self.writer.interface);
     68 
     69     var buf: [8]u8 = undefined;
     70     const bytes = try std.unicode.utf8Encode(self.config.symbols[self.current_symbol_idx], &buf);
     71     try self.writer.interface.writeAll(buf[0..bytes]);
     72 
     73     if (self.config.description) |desc| {
     74         try ansi_term.cursorForward(&self.writer.interface, 1);
     75         try self.writer.interface.writeAll(desc);
     76     }
     77 
     78     try self.writer.interface.flush();
     79     self.current_symbol_idx = (self.current_symbol_idx + 1) % self.config.symbols.len;
     80 }
     81 
     82 ///Returns `true` if the progress spinner is finished and `false` otherwise.
     83 pub fn isFinished(self: *Spinner) bool {
     84     self.mutex.lockUncancelable(self.writer.io);
     85     defer self.mutex.unlock(self.writer.io);
     86 
     87     return self.finished;
     88 }
     89 
     90 ///Finish the progress spinner.
     91 pub fn finish(self: *Spinner) !void {
     92     try self.mutex.lock(self.writer.io);
     93     defer self.mutex.unlock(self.writer.io);
     94 
     95     self.finished = true;
     96 
     97     if (self.config.completion_character) |char| {
     98         try self.renderComplete(char);
     99     }
    100 
    101     if (self.config.clear_on_finish) try self.clear();
    102     if (self.config.write_newline_on_finish) try self.writer.interface.writeAll("\n");
    103 
    104     try ansi_term.showCursor(&self.writer.interface);
    105     try self.writer.interface.flush();
    106 }
    107 
    108 ///Update the description.
    109 pub fn updateDescription(self: *Spinner, description: []const u8) void {
    110     self.mutex.lockUncancelable(self.writer.io);
    111     defer self.mutex.unlock(self.writer.io);
    112 
    113     self.config.description = description;
    114 }
    115 
    116 ///Update the description and continue the spinner on a newline.
    117 pub fn updateDescriptionNewline(self: *Spinner, description: []const u8) !void {
    118     try self.mutex.lock(self.writer.io);
    119     defer self.mutex.unlock(self.writer.io);
    120 
    121     if (self.config.completion_character) |char| {
    122         try self.renderComplete(char);
    123     }
    124 
    125     self.config.description = description;
    126 
    127     try self.writer.interface.writeAll("\n");
    128     try self.writer.interface.flush();
    129 }
    130 
    131 ///Clear the progress spinner.
    132 ///
    133 ///This function is not thread safe.
    134 pub fn clear(self: *Spinner) !void {
    135     try ansi_term.clearCurrentLine(&self.writer.interface);
    136     try ansi_term.setCursorColumn(&self.writer.interface, 0);
    137     try self.writer.interface.flush();
    138 }