2019-10-02 12:30:12 -07:00
|
|
|
const std = @import("std");
|
2020-04-22 03:58:02 -07:00
|
|
|
const windows = std.os.windows;
|
2019-10-02 12:30:12 -07:00
|
|
|
const testing = std.testing;
|
2019-10-17 17:20:22 -07:00
|
|
|
const assert = std.debug.assert;
|
|
|
|
|
|
|
|
/// This API is non-allocating and non-fallible. The tradeoff is that users of
|
|
|
|
/// this API must provide the storage for each `Progress.Node`.
|
|
|
|
/// Initialize the struct directly, overriding these fields as desired:
|
|
|
|
/// * `refresh_rate_ms`
|
|
|
|
/// * `initial_delay_ms`
|
|
|
|
pub const Progress = struct {
|
|
|
|
/// `null` if the current node (and its children) should
|
|
|
|
/// not print on update()
|
|
|
|
terminal: ?std.fs.File = undefined,
|
|
|
|
|
2020-04-29 09:49:02 -07:00
|
|
|
/// Whether the terminal supports ANSI escape codes.
|
|
|
|
supports_ansi_escape_codes: bool = false,
|
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
root: Node = undefined,
|
|
|
|
|
|
|
|
/// Keeps track of how much time has passed since the beginning.
|
|
|
|
/// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.
|
|
|
|
timer: std.time.Timer = undefined,
|
|
|
|
|
|
|
|
/// When the previous refresh was written to the terminal.
|
|
|
|
/// Used to compare with `refresh_rate_ms`.
|
|
|
|
prev_refresh_timestamp: u64 = undefined,
|
|
|
|
|
|
|
|
/// This buffer represents the maximum number of bytes written to the terminal
|
|
|
|
/// with each refresh.
|
|
|
|
output_buffer: [100]u8 = undefined,
|
|
|
|
|
|
|
|
/// How many nanoseconds between writing updates to the terminal.
|
2020-05-24 17:06:56 -07:00
|
|
|
refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,
|
2019-10-17 17:20:22 -07:00
|
|
|
|
|
|
|
/// How many nanoseconds to keep the output hidden
|
2020-05-24 17:06:56 -07:00
|
|
|
initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
|
2019-10-17 17:20:22 -07:00
|
|
|
|
|
|
|
done: bool = true,
|
|
|
|
|
2019-10-17 18:46:41 -07:00
|
|
|
/// Keeps track of how many columns in the terminal have been output, so that
|
|
|
|
/// we can move the cursor back later.
|
|
|
|
columns_written: usize = undefined,
|
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
/// Represents one unit of progress. Each node can have children nodes, or
|
|
|
|
/// one can use integers with `update`.
|
|
|
|
pub const Node = struct {
|
|
|
|
context: *Progress,
|
|
|
|
parent: ?*Node,
|
|
|
|
completed_items: usize,
|
|
|
|
name: []const u8,
|
|
|
|
recently_updated_child: ?*Node = null,
|
|
|
|
|
|
|
|
/// This field may be updated freely.
|
|
|
|
estimated_total_items: ?usize,
|
|
|
|
|
|
|
|
/// Create a new child progress node.
|
|
|
|
/// Call `Node.end` when done.
|
|
|
|
/// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this
|
|
|
|
/// API to set `self.parent.recently_updated_child` with the return value.
|
|
|
|
/// Until that is fixed you probably want to call `activate` on the return value.
|
|
|
|
pub fn start(self: *Node, name: []const u8, estimated_total_items: ?usize) Node {
|
|
|
|
return Node{
|
|
|
|
.context = self.context,
|
|
|
|
.parent = self,
|
|
|
|
.completed_items = 0,
|
|
|
|
.name = name,
|
|
|
|
.estimated_total_items = estimated_total_items,
|
|
|
|
};
|
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
/// This is the same as calling `start` and then `end` on the returned `Node`.
|
|
|
|
pub fn completeOne(self: *Node) void {
|
|
|
|
if (self.parent) |parent| parent.recently_updated_child = self;
|
|
|
|
self.completed_items += 1;
|
|
|
|
self.context.maybeRefresh();
|
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
pub fn end(self: *Node) void {
|
|
|
|
self.context.maybeRefresh();
|
|
|
|
if (self.parent) |parent| {
|
|
|
|
if (parent.recently_updated_child) |parent_child| {
|
|
|
|
if (parent_child == self) {
|
|
|
|
parent.recently_updated_child = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
parent.completeOne();
|
|
|
|
} else {
|
|
|
|
self.context.done = true;
|
|
|
|
self.context.refresh();
|
|
|
|
}
|
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
/// Tell the parent node that this node is actively being worked on.
|
|
|
|
pub fn activate(self: *Node) void {
|
|
|
|
if (self.parent) |parent| parent.recently_updated_child = self;
|
|
|
|
}
|
|
|
|
};
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
/// Create a new progress node.
|
|
|
|
/// Call `Node.end` when done.
|
|
|
|
/// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this
|
|
|
|
/// API to return Progress rather than accept it as a parameter.
|
|
|
|
pub fn start(self: *Progress, name: []const u8, estimated_total_items: ?usize) !*Node {
|
2019-11-12 17:36:07 -08:00
|
|
|
const stderr = std.io.getStdErr();
|
2020-04-22 03:58:02 -07:00
|
|
|
self.terminal = null;
|
|
|
|
if (stderr.supportsAnsiEscapeCodes()) {
|
|
|
|
self.terminal = stderr;
|
2020-04-29 09:49:02 -07:00
|
|
|
self.supports_ansi_escape_codes = true;
|
2020-04-22 03:58:02 -07:00
|
|
|
} else if (std.builtin.os.tag == .windows and stderr.isTty()) {
|
|
|
|
self.terminal = stderr;
|
|
|
|
}
|
2019-10-17 17:20:22 -07:00
|
|
|
self.root = Node{
|
|
|
|
.context = self,
|
|
|
|
.parent = null,
|
|
|
|
.completed_items = 0,
|
|
|
|
.name = name,
|
|
|
|
.estimated_total_items = estimated_total_items,
|
|
|
|
};
|
|
|
|
self.columns_written = 0;
|
2019-10-17 18:46:41 -07:00
|
|
|
self.prev_refresh_timestamp = 0;
|
2019-10-17 17:20:22 -07:00
|
|
|
self.timer = try std.time.Timer.start();
|
|
|
|
self.done = false;
|
|
|
|
return &self.root;
|
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
/// Updates the terminal if enough time has passed since last update.
|
|
|
|
pub fn maybeRefresh(self: *Progress) void {
|
|
|
|
const now = self.timer.read();
|
|
|
|
if (now < self.initial_delay_ns) return;
|
|
|
|
if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return;
|
|
|
|
self.refresh();
|
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
/// Updates the terminal and resets `self.next_refresh_timestamp`.
|
|
|
|
pub fn refresh(self: *Progress) void {
|
|
|
|
const file = self.terminal orelse return;
|
|
|
|
|
|
|
|
const prev_columns_written = self.columns_written;
|
|
|
|
var end: usize = 0;
|
|
|
|
if (self.columns_written > 0) {
|
2020-04-22 03:58:02 -07:00
|
|
|
// restore the cursor position by moving the cursor
|
|
|
|
// `columns_written` cells to the left, then clear the rest of the
|
|
|
|
// line
|
2020-04-29 09:49:02 -07:00
|
|
|
if (self.supports_ansi_escape_codes) {
|
2020-04-22 03:58:02 -07:00
|
|
|
end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{}D", .{self.columns_written}) catch unreachable).len;
|
|
|
|
end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;
|
2020-05-01 12:13:55 -07:00
|
|
|
} else if (std.builtin.os.tag == .windows) winapi: {
|
2020-04-22 03:58:02 -07:00
|
|
|
var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
|
|
|
|
if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
|
|
|
|
unreachable;
|
|
|
|
|
|
|
|
var cursor_pos = windows.COORD{
|
|
|
|
.X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written),
|
|
|
|
.Y = info.dwCursorPosition.Y,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (cursor_pos.X < 0)
|
|
|
|
cursor_pos.X = 0;
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2020-04-22 03:58:02 -07:00
|
|
|
const fill_chars = @intCast(windows.DWORD, info.dwSize.X - cursor_pos.X);
|
|
|
|
|
|
|
|
var written: windows.DWORD = undefined;
|
|
|
|
if (windows.kernel32.FillConsoleOutputAttribute(
|
|
|
|
file.handle,
|
|
|
|
info.wAttributes,
|
|
|
|
fill_chars,
|
|
|
|
cursor_pos,
|
|
|
|
&written,
|
2020-05-01 12:13:55 -07:00
|
|
|
) != windows.TRUE) {
|
|
|
|
// Stop trying to write to this file.
|
|
|
|
self.terminal = null;
|
|
|
|
break :winapi;
|
|
|
|
}
|
2020-04-22 03:58:02 -07:00
|
|
|
if (windows.kernel32.FillConsoleOutputCharacterA(
|
|
|
|
file.handle,
|
|
|
|
' ',
|
|
|
|
fill_chars,
|
|
|
|
cursor_pos,
|
|
|
|
&written,
|
|
|
|
) != windows.TRUE) unreachable;
|
|
|
|
|
|
|
|
if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE)
|
|
|
|
unreachable;
|
2020-04-29 10:07:23 -07:00
|
|
|
} else unreachable;
|
2020-04-22 03:58:02 -07:00
|
|
|
|
|
|
|
self.columns_written = 0;
|
2019-10-17 17:20:22 -07:00
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
|
2019-10-17 18:46:41 -07:00
|
|
|
if (!self.done) {
|
|
|
|
var need_ellipse = false;
|
|
|
|
var maybe_node: ?*Node = &self.root;
|
|
|
|
while (maybe_node) |node| {
|
|
|
|
if (need_ellipse) {
|
2019-12-08 19:53:51 -08:00
|
|
|
self.bufWrite(&end, "...", .{});
|
2019-10-17 18:46:41 -07:00
|
|
|
}
|
|
|
|
need_ellipse = false;
|
|
|
|
if (node.name.len != 0 or node.estimated_total_items != null) {
|
|
|
|
if (node.name.len != 0) {
|
2019-12-08 19:53:51 -08:00
|
|
|
self.bufWrite(&end, "{}", .{node.name});
|
2019-10-17 18:46:41 -07:00
|
|
|
need_ellipse = true;
|
|
|
|
}
|
|
|
|
if (node.estimated_total_items) |total| {
|
2019-12-08 19:53:51 -08:00
|
|
|
if (need_ellipse) self.bufWrite(&end, " ", .{});
|
|
|
|
self.bufWrite(&end, "[{}/{}] ", .{ node.completed_items + 1, total });
|
2019-10-17 18:46:41 -07:00
|
|
|
need_ellipse = false;
|
|
|
|
} else if (node.completed_items != 0) {
|
2019-12-08 19:53:51 -08:00
|
|
|
if (need_ellipse) self.bufWrite(&end, " ", .{});
|
|
|
|
self.bufWrite(&end, "[{}] ", .{node.completed_items + 1});
|
2019-10-17 18:46:41 -07:00
|
|
|
need_ellipse = false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
maybe_node = node.recently_updated_child;
|
|
|
|
}
|
|
|
|
if (need_ellipse) {
|
2019-12-08 19:53:51 -08:00
|
|
|
self.bufWrite(&end, "...", .{});
|
2019-10-17 18:46:41 -07:00
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
}
|
|
|
|
|
2019-10-17 17:20:22 -07:00
|
|
|
_ = file.write(self.output_buffer[0..end]) catch |e| {
|
|
|
|
// Stop trying to write to this file once it errors.
|
|
|
|
self.terminal = null;
|
2019-10-02 12:30:12 -07:00
|
|
|
};
|
2019-10-17 17:20:22 -07:00
|
|
|
self.prev_refresh_timestamp = self.timer.read();
|
2019-10-02 12:30:12 -07:00
|
|
|
}
|
|
|
|
|
2019-12-08 19:53:51 -08:00
|
|
|
pub fn log(self: *Progress, comptime format: []const u8, args: var) void {
|
2019-10-17 18:46:41 -07:00
|
|
|
const file = self.terminal orelse return;
|
|
|
|
self.refresh();
|
2020-03-10 13:31:04 -07:00
|
|
|
file.outStream().print(format, args) catch {
|
2019-10-17 18:46:41 -07:00
|
|
|
self.terminal = null;
|
|
|
|
return;
|
|
|
|
};
|
|
|
|
self.columns_written = 0;
|
2019-10-02 12:30:12 -07:00
|
|
|
}
|
|
|
|
|
2019-12-08 19:53:51 -08:00
|
|
|
fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: var) void {
|
2019-10-17 17:20:22 -07:00
|
|
|
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) {
|
2020-03-13 08:55:50 -07:00
|
|
|
error.NoSpaceLeft => {
|
2019-10-17 17:20:22 -07:00
|
|
|
self.columns_written += self.output_buffer.len - end.*;
|
|
|
|
end.* = self.output_buffer.len;
|
|
|
|
},
|
|
|
|
}
|
2020-04-22 03:58:02 -07:00
|
|
|
const bytes_needed_for_esc_codes_at_end = if (std.builtin.os.tag == .windows) 0 else 11;
|
2019-10-17 17:20:22 -07:00
|
|
|
const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end;
|
|
|
|
if (end.* > max_end) {
|
|
|
|
const suffix = "...";
|
|
|
|
self.columns_written = self.columns_written - (end.* - max_end) + suffix.len;
|
|
|
|
std.mem.copy(u8, self.output_buffer[max_end..], suffix);
|
|
|
|
end.* = max_end + suffix.len;
|
|
|
|
}
|
2019-10-02 12:30:12 -07:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
test "basic functionality" {
|
2019-10-17 18:46:41 -07:00
|
|
|
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;
|
|
|
|
}
|
2019-10-17 17:20:22 -07:00
|
|
|
var progress = Progress{};
|
|
|
|
const root_node = try progress.start("", 100);
|
|
|
|
defer root_node.end();
|
|
|
|
|
|
|
|
const sub_task_names = [_][]const u8{
|
|
|
|
"reticulating splines",
|
|
|
|
"adjusting shoes",
|
|
|
|
"climbing towers",
|
|
|
|
"pouring juice",
|
|
|
|
};
|
|
|
|
var next_sub_task: usize = 0;
|
2019-10-02 12:30:12 -07:00
|
|
|
|
|
|
|
var i: usize = 0;
|
2019-10-17 17:20:22 -07:00
|
|
|
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();
|
2020-05-24 17:06:56 -07:00
|
|
|
std.time.sleep(5 * std.time.ns_per_ms);
|
2019-10-17 17:20:22 -07:00
|
|
|
node.completeOne();
|
|
|
|
node.completeOne();
|
2020-05-24 17:06:56 -07:00
|
|
|
std.time.sleep(5 * std.time.ns_per_ms);
|
2019-10-17 17:20:22 -07:00
|
|
|
node.completeOne();
|
|
|
|
node.completeOne();
|
2020-05-24 17:06:56 -07:00
|
|
|
std.time.sleep(5 * std.time.ns_per_ms);
|
2019-10-17 17:20:22 -07:00
|
|
|
|
|
|
|
node.end();
|
|
|
|
|
2020-05-24 17:06:56 -07:00
|
|
|
std.time.sleep(5 * std.time.ns_per_ms);
|
2019-10-17 17:20:22 -07:00
|
|
|
}
|
|
|
|
{
|
|
|
|
var node = root_node.start("this is a really long name designed to activate the truncation code. let's find out if it works", null);
|
|
|
|
node.activate();
|
2020-05-24 17:06:56 -07:00
|
|
|
std.time.sleep(10 * std.time.ns_per_ms);
|
2019-10-17 18:46:41 -07:00
|
|
|
progress.refresh();
|
2020-05-24 17:06:56 -07:00
|
|
|
std.time.sleep(10 * std.time.ns_per_ms);
|
2019-10-17 17:20:22 -07:00
|
|
|
node.end();
|
2019-10-02 12:30:12 -07:00
|
|
|
}
|
|
|
|
}
|