diff options
Diffstat (limited to 'lib/std')
| -rw-r--r-- | lib/std/Progress.zig | 282 |
1 files changed, 48 insertions, 234 deletions
diff --git a/lib/std/Progress.zig b/lib/std/Progress.zig index 7909557137..61c1ee2a32 100644 --- a/lib/std/Progress.zig +++ b/lib/std/Progress.zig @@ -1,30 +1,23 @@ -//! This is a non-allocating, non-fallible, and thread-safe API for printing -//! progress indicators to the terminal. +//! This API non-allocating, non-fallible, and thread-safe. //! The tradeoff is that users of this API must provide the storage //! for each `Progress.Node`. //! -//! This library purposefully keeps its output simple and is ASCII-compatible. -//! //! Initialize the struct directly, overriding these fields as desired: //! * `refresh_rate_ms` //! * `initial_delay_ms` -//! * `dont_print_on_dumb` -//! * `max_width` const std = @import("std"); const builtin = @import("builtin"); const windows = std.os.windows; const testing = std.testing; const assert = std.debug.assert; -const os = std.os; -const time = std.time; const Progress = @This(); /// `null` if the current node (and its children) should /// not print on update() terminal: ?std.fs.File = undefined, -/// Is this a Windows API terminal (note: this is not the same as being run on Windows +/// Is this a windows API terminal (note: this is not the same as being run on windows /// because other terminals exist like MSYS/git-bash) is_windows_terminal: bool = false, @@ -42,31 +35,21 @@ 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: ?time.Timer = null, +timer: ?std.time.Timer = null, /// When the previous refresh was written to the terminal. /// Used to compare with `refresh_rate_ms`. prev_refresh_timestamp: u64 = undefined, -/// This is the maximum number of bytes that can be written to the terminal each refresh. -/// Anything larger than this is truncated. -// we can bump this up if we need to -output_buffer: [256]u8 = undefined, -output_buffer_slice: []u8 = undefined, - -/// This is the maximum number of bytes written to the terminal with each refresh. -/// -/// It is recommended to leave this as `null` so that `start` can automatically decide an -/// optimal width for the terminal. -/// -/// Note that this will be clamped to at least 4 and output will appear malformed if it is < 4. -max_width: ?usize = null, +/// 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. -refresh_rate_ns: u64 = 50 * time.ns_per_ms, +refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, -/// How many nanoseconds to keep the output hidden. -initial_delay_ns: u64 = 500 * time.ns_per_ms, +/// How many nanoseconds to keep the output hidden +initial_delay_ns: u64 = 500 * std.time.ns_per_ms, done: bool = true, @@ -79,14 +62,11 @@ update_mutex: std.Thread.Mutex = .{}, /// we can move the cursor back later. columns_written: usize = undefined, -const truncation_suffix = "... "; - /// 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, - /// The name that will be displayed for this node. name: []const u8, /// Must be handled atomically to be thread-safe. recently_updated_child: ?*Node = null, @@ -159,13 +139,9 @@ pub const Node = struct { /// 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. /// `estimated_total_items` value of 0 means unknown. -/// -/// Note that as soon as work is started and progress output is printed, -/// `std.Progress` expects you to lean back and wait and not resize the terminal. -/// Resizing the terminal during progress output may result in malformed output. -// 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 { const stderr = std.io.getStdErr(); self.terminal = null; @@ -179,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N // we are in a "dumb" terminal like in acme or writing to a file self.terminal = stderr; } - self.calculateMaxWidth(); self.root = Node{ .context = self, .parent = null, @@ -189,83 +164,11 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *N }; self.columns_written = 0; self.prev_refresh_timestamp = 0; - self.timer = time.Timer.start() catch null; + self.timer = std.time.Timer.start() catch null; self.done = false; return &self.root; } -fn calculateMaxWidth(self: *Progress) void { - if (self.max_width == null) { - if (self.terminal) |terminal| { - // choose an optimal width and account for progress output that could have been printed - // before us by another `std.Progress` instance - const terminal_width = self.getTerminalWidth(terminal.handle) catch 100; - const chars_already_printed = self.getTerminalCursorColumn(terminal) catch 0; - self.max_width = terminal_width - chars_already_printed; - } else { - self.max_width = 100; - } - } - self.max_width = std.math.clamp( - self.max_width.?, - truncation_suffix.len, // make sure we can at least truncate - self.output_buffer.len - 1, - ); -} - -fn getTerminalWidth(self: Progress, file_handle: os.fd_t) !u16 { - if (builtin.os.tag == .linux) { - // TODO: figure out how to get this working on FreeBSD, macOS etc. too. - // they too should have capabilities to figure out the cursor column. - var winsize: os.linux.winsize = undefined; - switch (os.errno(os.linux.ioctl(file_handle, os.linux.T.IOCGWINSZ, @ptrToInt(&winsize)))) { - .SUCCESS => return winsize.ws_col, - else => return error.Unexpected, - } - } else if (builtin.os.tag == .windows) { - std.debug.assert(self.is_windows_terminal); - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file_handle, &info) != windows.TRUE) - return error.Unexpected; - return @intCast(u16, info.dwSize.X); - } else { - return error.Unsupported; - } -} - -fn getTerminalCursorColumn(self: Progress, file: std.fs.File) !u16 { - // TODO: figure out how to get this working on FreeBSD, macOS etc. too. - // they too should have termios or capabilities to figure out the terminal width. - if (builtin.os.tag == .linux and self.supports_ansi_escape_codes) { - // First, disable echo and enable non-canonical mode - // (so that no enter press required for us to read the output of the escape sequence below) - const original_termios = try os.tcgetattr(file.handle); - var new_termios = original_termios; - new_termios.lflag &= ~(os.linux.ECHO | os.linux.ICANON); - try os.tcsetattr(file.handle, .NOW, new_termios); - defer os.tcsetattr(file.handle, .NOW, original_termios) catch { - // Sorry for ruining your terminal - }; - - try file.writeAll("\x1b[6n"); - var buf: ["\x1b[65536;65536R".len]u8 = undefined; - const output = try file.reader().readUntilDelimiter(&buf, 'R'); - var splitter = std.mem.split(u8, output, ";"); - _ = splitter.next().?; // skip first half - const column_half = splitter.next() orelse return error.UnexpectedEnd; - const column = try std.fmt.parseUnsigned(u16, column_half, 10); - return column - 1; // it's one-based - } else if (builtin.os.tag == .windows) { - std.debug.assert(self.is_windows_terminal); - var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; - if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) - return error.Unexpected; - return @intCast(u16, info.dwCursorPosition.X); - } else { - return error.Unsupported; - } -} - /// Updates the terminal if enough time has passed since last update. Thread-safe. pub fn maybeRefresh(self: *Progress) void { if (self.timer) |*timer| { @@ -295,16 +198,14 @@ fn refreshWithHeldLock(self: *Progress) void { const file = self.terminal orelse return; - // prepare for printing unprintable characters - self.output_buffer_slice = &self.output_buffer; - var end: usize = 0; if (self.columns_written > 0) { // restore the cursor position by moving the cursor - // `columns_written` cells to the left, then clear the rest of the line + // `columns_written` cells to the left, then clear the rest of the + // line if (self.supports_ansi_escape_codes) { - end += (std.fmt.bufPrint(self.output_buffer_slice[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len; - end += (std.fmt.bufPrint(self.output_buffer_slice[end..], "\x1b[0K", .{}) catch unreachable).len; + end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len; + end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; } else if (builtin.os.tag == .windows) winapi: { std.debug.assert(self.is_windows_terminal); @@ -346,53 +247,47 @@ fn refreshWithHeldLock(self: *Progress) void { unreachable; } else { // we are in a "dumb" terminal like in acme or writing to a file - self.output_buffer_slice[end] = '\n'; + self.output_buffer[end] = '\n'; end += 1; } self.columns_written = 0; } - // from here on we will write printable characters. we also make sure the unprintable characters - // we possibly wrote previously don't affect whether we truncate the line in `bufWrite`. - const unprintables = end; - end = 0; - self.output_buffer_slice = self.output_buffer[unprintables..@min(self.output_buffer.len, unprintables + self.max_width.?)]; - if (!self.done) { - var need_ellipsis = false; + var need_ellipse = false; var maybe_node: ?*Node = &self.root; while (maybe_node) |node| { - if (need_ellipsis) { + if (need_ellipse) { self.bufWrite(&end, "... ", .{}); } - need_ellipsis = false; - const estimated_total_items = @atomicLoad(usize, &node.unprotected_estimated_total_items, .Monotonic); + 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 estimated_total_items > 0) { + if (node.name.len != 0 or eti > 0) { if (node.name.len != 0) { self.bufWrite(&end, "{s}", .{node.name}); - need_ellipsis = true; + need_ellipse = true; } - if (estimated_total_items > 0) { - if (need_ellipsis) self.bufWrite(&end, " ", .{}); - self.bufWrite(&end, "[{d}/{d}] ", .{ current_item, estimated_total_items }); - need_ellipsis = false; + if (eti > 0) { + if (need_ellipse) self.bufWrite(&end, " ", .{}); + self.bufWrite(&end, "[{d}/{d}] ", .{ current_item, eti }); + need_ellipse = false; } else if (completed_items != 0) { - if (need_ellipsis) self.bufWrite(&end, " ", .{}); + if (need_ellipse) self.bufWrite(&end, " ", .{}); self.bufWrite(&end, "[{d}] ", .{current_item}); - need_ellipsis = false; + need_ellipse = false; } } maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .Acquire); } - if (need_ellipsis) { + if (need_ellipse) { self.bufWrite(&end, "... ", .{}); } } - _ = file.write(self.output_buffer[0 .. end + unprintables]) catch { + _ = file.write(self.output_buffer[0..end]) catch { // Stop trying to write to this file once it errors. self.terminal = null; }; @@ -415,113 +310,32 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { } fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { - if (std.fmt.bufPrint(self.output_buffer_slice[end.*..], format, args)) |written| { + 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 => { - // truncate the line with a suffix. - // for example if we have "hello world" (len=11) and 10 is the limit, - // it would become "hello w... " - self.columns_written += self.output_buffer_slice.len - end.*; - end.* = self.output_buffer_slice.len; - std.mem.copy( - u8, - self.output_buffer_slice[self.output_buffer_slice.len - truncation_suffix.len ..], - truncation_suffix, - ); + self.columns_written += self.output_buffer.len - end.*; + end.* = self.output_buffer.len; + const suffix = "... "; + std.mem.copy(u8, self.output_buffer[self.output_buffer.len - suffix.len ..], suffix); }, } } -// By default these tests are disabled because they use time.sleep() -// and are therefore slow. They also prints bogus progress data to stderr. -const skip_tests = true; - -test "behavior on buffer overflow" { - if (skip_tests) - return error.SkipZigTest; - - // uncomment this to move the cursor - //std.debug.print("{s}", .{"A" ** 300}); - - var progress = Progress{}; - - const long_string = "A" ** 300; - var node = progress.start(long_string, 0); - - const speed_factor = time.ns_per_s / 4; - - time.sleep(speed_factor); - node.activate(); - time.sleep(speed_factor); - node.end(); -} - -test "multiple tasks with long names" { - if (skip_tests) - return error.SkipZigTest; - - var progress = Progress{}; - - const tasks = [_][]const u8{ - "A" ** 99, - "A" ** 100, - "A" ** 101, - "A" ** 102, - "A" ** 103, - }; - - const speed_factor = time.ns_per_s / 6; - - for (tasks) |task| { - var node = progress.start(task, 3); - time.sleep(speed_factor); - node.activate(); - - time.sleep(speed_factor); - node.completeOne(); - time.sleep(speed_factor); - node.completeOne(); - time.sleep(speed_factor); - node.completeOne(); - - node.end(); - } -} - -test "very short max width" { - if (skip_tests) - return error.SkipZigTest; - - var progress = Progress{ .max_width = 4 }; - - const task = "A" ** 300; - - const speed_factor = time.ns_per_s / 2; - - var node = progress.start(task, 3); - time.sleep(speed_factor); - node.activate(); - - time.sleep(speed_factor); - node.completeOne(); - time.sleep(speed_factor); - node.completeOne(); - - node.end(); -} - test "basic functionality" { - if (skip_tests) + 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 = time.ns_per_ms; + const speed_factor = std.time.ns_per_ms; const sub_task_names = [_][]const u8{ "reticulating splines", @@ -538,24 +352,24 @@ test "basic functionality" { next_sub_task = (next_sub_task + 1) % sub_task_names.len; node.completeOne(); - time.sleep(5 * speed_factor); + std.time.sleep(5 * speed_factor); node.completeOne(); node.completeOne(); - time.sleep(5 * speed_factor); + std.time.sleep(5 * speed_factor); node.completeOne(); node.completeOne(); - time.sleep(5 * speed_factor); + std.time.sleep(5 * speed_factor); node.end(); - time.sleep(5 * speed_factor); + 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(); - time.sleep(10 * speed_factor); + std.time.sleep(10 * speed_factor); progress.refresh(); - time.sleep(10 * speed_factor); + std.time.sleep(10 * speed_factor); node.end(); } } |
