| 
   | 
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. |