|
const std = @import("std"); const builtin = @import("builtin"); const windows = std.os.windows; const testing = std.testing; const assert = std.debug.assert; const Progress = @This(); terminal: ?std.fs.File = undefined, is_windows_terminal: bool = false, supports_ansi_escape_codes: bool = false, dont_print_on_dumb: bool = false, root: Node = undefined, timer: ?std.time.Timer = null, prev_refresh_timestamp: u64 = undefined, output_buffer: [100]u8 = undefined, refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, initial_delay_ns: u64 = 500 * std.time.ns_per_ms, done: bool = true, update_mutex: std.Thread.Mutex = .{}, columns_written: usize = undefined, |
Node |
pub const Node = struct { context: *Progress, parent: ?*Node, name: []const u8, unit: []const u8 = "", recently_updated_child: ?*Node = null, unprotected_estimated_total_items: usize, unprotected_completed_items: usize, |
start() Must be handled atomically to be thread-safe. Must be handled atomically to be thread-safe. 0 means null. Must be handled atomically to be thread-safe. Create a new child progress node. Thread-safe. Call |
pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node { return Node{ .context = self.context, .parent = self, .name = name, .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; } |
completeOne() This is the same as calling |
pub fn completeOne(self: *Node) void { if (self.parent) |parent| { @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); } _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .Monotonic); self.context.maybeRefresh(); } |
end() Finish a started |
pub fn end(self: *Node) void { self.context.maybeRefresh(); if (self.parent) |parent| { { self.context.update_mutex.lock(); defer self.context.update_mutex.unlock(); _ = @cmpxchgStrong(?*Node, &parent.recently_updated_child, self, null, .Monotonic, .Monotonic); } parent.completeOne(); } else { self.context.update_mutex.lock(); defer self.context.update_mutex.unlock(); self.context.done = true; self.context.refreshWithHeldLock(); } } |
activate()Tell the parent node that this node is actively being worked on. Thread-safe. |
pub fn activate(self: *Node) void { if (self.parent) |parent| { @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); self.context.maybeRefresh(); } } |
setName()Thread-safe. |
pub fn setName(self: *Node, name: []const u8) void { const progress = self.context; progress.update_mutex.lock(); defer progress.update_mutex.unlock(); self.name = name; if (self.parent) |parent| { @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); if (parent.parent) |grand_parent| { @atomicStore(?*Node, &grand_parent.recently_updated_child, parent, .Release); } if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); } } |
setUnit()Thread-safe. |
pub fn setUnit(self: *Node, unit: []const u8) void { const progress = self.context; progress.update_mutex.lock(); defer progress.update_mutex.unlock(); self.unit = unit; if (self.parent) |parent| { @atomicStore(?*Node, &parent.recently_updated_child, self, .Release); if (parent.parent) |grand_parent| { @atomicStore(?*Node, &grand_parent.recently_updated_child, parent, .Release); } if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); } } |
setEstimatedTotalItems()Thread-safe. 0 means unknown. |
pub fn setEstimatedTotalItems(self: *Node, count: usize) void { @atomicStore(usize, &self.unprotected_estimated_total_items, count, .Monotonic); } |
setCompletedItems()Thread-safe. |
pub fn setCompletedItems(self: *Node, completed_items: usize) void { @atomicStore(usize, &self.unprotected_completed_items, completed_items, .Monotonic); } }; |
start() Create a new progress node. Call |
pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node { const stderr = std.io.getStdErr(); self.terminal = null; if (stderr.supportsAnsiEscapeCodes()) { self.terminal = stderr; self.supports_ansi_escape_codes = true; } else if (builtin.os.tag == .windows and stderr.isTty()) { self.is_windows_terminal = true; self.terminal = stderr; } else if (builtin.os.tag != .windows) { // we are in a "dumb" terminal like in acme or writing to a file self.terminal = stderr; } self.root = Node{ .context = self, .parent = null, .name = name, .unprotected_estimated_total_items = estimated_total_items, .unprotected_completed_items = 0, }; self.columns_written = 0; self.prev_refresh_timestamp = 0; self.timer = std.time.Timer.start() catch null; self.done = false; return &self.root; } |
maybeRefresh()Updates the terminal if enough time has passed since last update. Thread-safe. |
pub fn maybeRefresh(self: *Progress) void { if (self.timer) |*timer| { if (!self.update_mutex.tryLock()) return; defer self.update_mutex.unlock(); maybeRefreshWithHeldLock(self, timer); } } fn maybeRefreshWithHeldLock(self: *Progress, timer: *std.time.Timer) void { const now = timer.read(); if (now < self.initial_delay_ns) return; // TODO I have observed this to happen sometimes. I think we need to follow Rust's // lead and guarantee monotonically increasing times in the std lib itself. if (now < self.prev_refresh_timestamp) return; if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return; return self.refreshWithHeldLock(); } |
refresh() Updates the terminal and resets |
pub fn refresh(self: *Progress) void { if (!self.update_mutex.tryLock()) return; defer self.update_mutex.unlock(); return self.refreshWithHeldLock(); } fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { const file = p.terminal orelse return; var end = end_ptr.*; if (p.columns_written > 0) { // restore the cursor position by moving the cursor // `columns_written` cells to the left, then clear the rest of the // line if (p.supports_ansi_escape_codes) { end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[{d}D", .{p.columns_written}) catch unreachable).len; end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; } else if (builtin.os.tag == .windows) winapi: { std.debug.assert(p.is_windows_terminal); var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { // stop trying to write to this file p.terminal = null; break :winapi; } var cursor_pos = windows.COORD{ .X = info.dwCursorPosition.X - @as(windows.SHORT, @intCast(p.columns_written)), .Y = info.dwCursorPosition.Y, }; if (cursor_pos.X < 0) cursor_pos.X = 0; const fill_chars = @as(windows.DWORD, @intCast(info.dwSize.X - cursor_pos.X)); var written: windows.DWORD = undefined; if (windows.kernel32.FillConsoleOutputAttribute( file.handle, info.wAttributes, fill_chars, cursor_pos, &written, ) != windows.TRUE) { // stop trying to write to this file p.terminal = null; break :winapi; } if (windows.kernel32.FillConsoleOutputCharacterW( file.handle, ' ', fill_chars, cursor_pos, &written, ) != windows.TRUE) { // stop trying to write to this file p.terminal = null; break :winapi; } if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE) { // stop trying to write to this file p.terminal = null; break :winapi; } } else { // we are in a "dumb" terminal like in acme or writing to a file p.output_buffer[end] = '\n'; end += 1; } p.columns_written = 0; } end_ptr.* = end; } fn refreshWithHeldLock(self: *Progress) void { const is_dumb = !self.supports_ansi_escape_codes and !self.is_windows_terminal; if (is_dumb and self.dont_print_on_dumb) return; const file = self.terminal orelse return; var end: usize = 0; clearWithHeldLock(self, &end); if (!self.done) { var need_ellipse = false; var maybe_node: ?*Node = &self.root; while (maybe_node) |node| { if (need_ellipse) { self.bufWrite(&end, "... ", .{}); } need_ellipse = false; const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic); const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .Monotonic); const current_item = completed_items + 1; if (node.name.len != 0 or eti > 0) { if (node.name.len != 0) { self.bufWrite(&end, "{s}", .{node.name}); need_ellipse = true; } if (eti > 0) { if (need_ellipse) self.bufWrite(&end, " ", .{}); self.bufWrite(&end, "[{d}/{d}{s}] ", .{ current_item, eti, node.unit }); need_ellipse = false; } else if (completed_items != 0) { if (need_ellipse) self.bufWrite(&end, " ", .{}); self.bufWrite(&end, "[{d}{s}] ", .{ current_item, node.unit }); need_ellipse = false; } } maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Acquire); } if (need_ellipse) { self.bufWrite(&end, "... ", .{}); } } _ = file.write(self.output_buffer[0..end]) catch { // stop trying to write to this file self.terminal = null; }; if (self.timer) |*timer| { self.prev_refresh_timestamp = timer.read(); } } |
log() |
pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { const file = self.terminal orelse { std.debug.print(format, args); return; }; self.refresh(); file.writer().print(format, args) catch { self.terminal = null; return; }; self.columns_written = 0; } |
lock_stderr()Allows the caller to freely write to stderr until unlock_stderr() is called. During the lock, the progress information is cleared from the terminal. |
pub fn lock_stderr(p: *Progress) void { p.update_mutex.lock(); if (p.terminal) |file| { var end: usize = 0; clearWithHeldLock(p, &end); _ = file.write(p.output_buffer[0..end]) catch { // stop trying to write to this file p.terminal = null; }; } std.debug.getStderrMutex().lock(); } |
unlock_stderr() |
pub fn unlock_stderr(p: *Progress) void { std.debug.getStderrMutex().unlock(); p.update_mutex.unlock(); } fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { const amt = written.len; end.* += amt; self.columns_written += amt; } else |err| switch (err) { error.NoSpaceLeft => { self.columns_written += self.output_buffer.len - end.*; end.* = self.output_buffer.len; const suffix = "... "; @memcpy(self.output_buffer[self.output_buffer.len - suffix.len ..], suffix); }, } } |
Test:basic functionality |
test "basic functionality" { var disable = true; if (disable) { // This test is disabled because it uses time.sleep() and is therefore slow. It also // prints bogus progress data to stderr. return error.SkipZigTest; } var progress = Progress{}; const root_node = progress.start("", 100); defer root_node.end(); const speed_factor = std.time.ns_per_ms; const sub_task_names = [_][]const u8{ "reticulating splines", "adjusting shoes", "climbing towers", "pouring juice", }; var next_sub_task: usize = 0; var i: usize = 0; while (i < 100) : (i += 1) { var node = root_node.start(sub_task_names[next_sub_task], 5); node.activate(); next_sub_task = (next_sub_task + 1) % sub_task_names.len; node.completeOne(); std.time.sleep(5 * speed_factor); node.completeOne(); node.completeOne(); std.time.sleep(5 * speed_factor); node.completeOne(); node.completeOne(); std.time.sleep(5 * speed_factor); node.end(); std.time.sleep(5 * speed_factor); } { var node = root_node.start("this is a really long name designed to activate the truncation code. let's find out if it works", 0); node.activate(); std.time.sleep(10 * speed_factor); progress.refresh(); std.time.sleep(10 * speed_factor); node.end(); } } |
Generated by zstd-browse2 on 2023-11-04 14:12:20 -0400. |