| author | |
| committer | |
| log | b2f8631a3c9b2cc04a4c78f38d164130be2fb1ae |
| tree | fa378fd8ba41bcefbf7a7f5fb8bb42e3b8b27a34 |
| parent | 32fd637e57c3b4391b3f2f4499c803e3d4e8f615 |
If this errdefer did get run it would constitute a race condition. So I
deleted the dead code for clarity.3 files changed, 310 insertions(+), 311 deletions(-)
lib/std/Progress.zig created+310| ... | @@ -0,0 +1,310 @@ | ||
| 1 | // SPDX-License-Identifier: MIT | ||
| 2 | // Copyright (c) 2015-2020 Zig Contributors | ||
| 3 | // This file is part of [zig](https://ziglang.org/), which is MIT licensed. | ||
| 4 | // The MIT license requires this copyright notice to be included in all copies | ||
| 5 | // and substantial portions of the software. | ||
| 6 | const std = @import("std"); | ||
| 7 | const windows = std.os.windows; | ||
| 8 | const testing = std.testing; | ||
| 9 | const assert = std.debug.assert; | ||
| 10 | |||
| 11 | /// This API is non-allocating and non-fallible. The tradeoff is that users of | ||
| 12 | /// this API must provide the storage for each `Progress.Node`. | ||
| 13 | /// Initialize the struct directly, overriding these fields as desired: | ||
| 14 | /// * `refresh_rate_ms` | ||
| 15 | /// * `initial_delay_ms` | ||
| 16 | pub const Progress = struct { | ||
| 17 | /// `null` if the current node (and its children) should | ||
| 18 | /// not print on update() | ||
| 19 | terminal: ?std.fs.File = undefined, | ||
| 20 | |||
| 21 | /// Whether the terminal supports ANSI escape codes. | ||
| 22 | supports_ansi_escape_codes: bool = false, | ||
| 23 | |||
| 24 | root: Node = undefined, | ||
| 25 | |||
| 26 | /// Keeps track of how much time has passed since the beginning. | ||
| 27 | /// Used to compare with `initial_delay_ms` and `refresh_rate_ms`. | ||
| 28 | timer: std.time.Timer = undefined, | ||
| 29 | |||
| 30 | /// When the previous refresh was written to the terminal. | ||
| 31 | /// Used to compare with `refresh_rate_ms`. | ||
| 32 | prev_refresh_timestamp: u64 = undefined, | ||
| 33 | |||
| 34 | /// This buffer represents the maximum number of bytes written to the terminal | ||
| 35 | /// with each refresh. | ||
| 36 | output_buffer: [100]u8 = undefined, | ||
| 37 | |||
| 38 | /// How many nanoseconds between writing updates to the terminal. | ||
| 39 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, | ||
| 40 | |||
| 41 | /// How many nanoseconds to keep the output hidden | ||
| 42 | initial_delay_ns: u64 = 500 * std.time.ns_per_ms, | ||
| 43 | |||
| 44 | done: bool = true, | ||
| 45 | |||
| 46 | /// Keeps track of how many columns in the terminal have been output, so that | ||
| 47 | /// we can move the cursor back later. | ||
| 48 | columns_written: usize = undefined, | ||
| 49 | |||
| 50 | /// Represents one unit of progress. Each node can have children nodes, or | ||
| 51 | /// one can use integers with `update`. | ||
| 52 | pub const Node = struct { | ||
| 53 | context: *Progress, | ||
| 54 | parent: ?*Node, | ||
| 55 | completed_items: usize, | ||
| 56 | name: []const u8, | ||
| 57 | recently_updated_child: ?*Node = null, | ||
| 58 | |||
| 59 | /// This field may be updated freely. | ||
| 60 | estimated_total_items: ?usize, | ||
| 61 | |||
| 62 | /// Create a new child progress node. | ||
| 63 | /// Call `Node.end` when done. | ||
| 64 | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this | ||
| 65 | /// API to set `self.parent.recently_updated_child` with the return value. | ||
| 66 | /// Until that is fixed you probably want to call `activate` on the return value. | ||
| 67 | pub fn start(self: *Node, name: []const u8, estimated_total_items: ?usize) Node { | ||
| 68 | return Node{ | ||
| 69 | .context = self.context, | ||
| 70 | .parent = self, | ||
| 71 | .completed_items = 0, | ||
| 72 | .name = name, | ||
| 73 | .estimated_total_items = estimated_total_items, | ||
| 74 | }; | ||
| 75 | } | ||
| 76 | |||
| 77 | /// This is the same as calling `start` and then `end` on the returned `Node`. | ||
| 78 | pub fn completeOne(self: *Node) void { | ||
| 79 | if (self.parent) |parent| parent.recently_updated_child = self; | ||
| 80 | self.completed_items += 1; | ||
| 81 | self.context.maybeRefresh(); | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn end(self: *Node) void { | ||
| 85 | self.context.maybeRefresh(); | ||
| 86 | if (self.parent) |parent| { | ||
| 87 | if (parent.recently_updated_child) |parent_child| { | ||
| 88 | if (parent_child == self) { | ||
| 89 | parent.recently_updated_child = null; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | parent.completeOne(); | ||
| 93 | } else { | ||
| 94 | self.context.done = true; | ||
| 95 | self.context.refresh(); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | /// Tell the parent node that this node is actively being worked on. | ||
| 100 | pub fn activate(self: *Node) void { | ||
| 101 | if (self.parent) |parent| parent.recently_updated_child = self; | ||
| 102 | } | ||
| 103 | }; | ||
| 104 | |||
| 105 | /// Create a new progress node. | ||
| 106 | /// Call `Node.end` when done. | ||
| 107 | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this | ||
| 108 | /// API to return Progress rather than accept it as a parameter. | ||
| 109 | pub fn start(self: *Progress, name: []const u8, estimated_total_items: ?usize) !*Node { | ||
| 110 | const stderr = std.io.getStdErr(); | ||
| 111 | self.terminal = null; | ||
| 112 | if (stderr.supportsAnsiEscapeCodes()) { | ||
| 113 | self.terminal = stderr; | ||
| 114 | self.supports_ansi_escape_codes = true; | ||
| 115 | } else if (std.builtin.os.tag == .windows and stderr.isTty()) { | ||
| 116 | self.terminal = stderr; | ||
| 117 | } | ||
| 118 | self.root = Node{ | ||
| 119 | .context = self, | ||
| 120 | .parent = null, | ||
| 121 | .completed_items = 0, | ||
| 122 | .name = name, | ||
| 123 | .estimated_total_items = estimated_total_items, | ||
| 124 | }; | ||
| 125 | self.columns_written = 0; | ||
| 126 | self.prev_refresh_timestamp = 0; | ||
| 127 | self.timer = try std.time.Timer.start(); | ||
| 128 | self.done = false; | ||
| 129 | return &self.root; | ||
| 130 | } | ||
| 131 | |||
| 132 | /// Updates the terminal if enough time has passed since last update. | ||
| 133 | pub fn maybeRefresh(self: *Progress) void { | ||
| 134 | const now = self.timer.read(); | ||
| 135 | if (now < self.initial_delay_ns) return; | ||
| 136 | if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return; | ||
| 137 | self.refresh(); | ||
| 138 | } | ||
| 139 | |||
| 140 | /// Updates the terminal and resets `self.next_refresh_timestamp`. | ||
| 141 | pub fn refresh(self: *Progress) void { | ||
| 142 | const file = self.terminal orelse return; | ||
| 143 | |||
| 144 | const prev_columns_written = self.columns_written; | ||
| 145 | var end: usize = 0; | ||
| 146 | if (self.columns_written > 0) { | ||
| 147 | // restore the cursor position by moving the cursor | ||
| 148 | // `columns_written` cells to the left, then clear the rest of the | ||
| 149 | // line | ||
| 150 | if (self.supports_ansi_escape_codes) { | ||
| 151 | end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{}D", .{self.columns_written}) catch unreachable).len; | ||
| 152 | end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; | ||
| 153 | } else if (std.builtin.os.tag == .windows) winapi: { | ||
| 154 | var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; | ||
| 155 | if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) | ||
| 156 | unreachable; | ||
| 157 | |||
| 158 | var cursor_pos = windows.COORD{ | ||
| 159 | .X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written), | ||
| 160 | .Y = info.dwCursorPosition.Y, | ||
| 161 | }; | ||
| 162 | |||
| 163 | if (cursor_pos.X < 0) | ||
| 164 | cursor_pos.X = 0; | ||
| 165 | |||
| 166 | const fill_chars = @intCast(windows.DWORD, info.dwSize.X - cursor_pos.X); | ||
| 167 | |||
| 168 | var written: windows.DWORD = undefined; | ||
| 169 | if (windows.kernel32.FillConsoleOutputAttribute( | ||
| 170 | file.handle, | ||
| 171 | info.wAttributes, | ||
| 172 | fill_chars, | ||
| 173 | cursor_pos, | ||
| 174 | &written, | ||
| 175 | ) != windows.TRUE) { | ||
| 176 | // Stop trying to write to this file. | ||
| 177 | self.terminal = null; | ||
| 178 | break :winapi; | ||
| 179 | } | ||
| 180 | if (windows.kernel32.FillConsoleOutputCharacterA( | ||
| 181 | file.handle, | ||
| 182 | ' ', | ||
| 183 | fill_chars, | ||
| 184 | cursor_pos, | ||
| 185 | &written, | ||
| 186 | ) != windows.TRUE) unreachable; | ||
| 187 | |||
| 188 | if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE) | ||
| 189 | unreachable; | ||
| 190 | } else unreachable; | ||
| 191 | |||
| 192 | self.columns_written = 0; | ||
| 193 | } | ||
| 194 | |||
| 195 | if (!self.done) { | ||
| 196 | var need_ellipse = false; | ||
| 197 | var maybe_node: ?*Node = &self.root; | ||
| 198 | while (maybe_node) |node| { | ||
| 199 | if (need_ellipse) { | ||
| 200 | self.bufWrite(&end, "... ", .{}); | ||
| 201 | } | ||
| 202 | need_ellipse = false; | ||
| 203 | if (node.name.len != 0 or node.estimated_total_items != null) { | ||
| 204 | if (node.name.len != 0) { | ||
| 205 | self.bufWrite(&end, "{}", .{node.name}); | ||
| 206 | need_ellipse = true; | ||
| 207 | } | ||
| 208 | if (node.estimated_total_items) |total| { | ||
| 209 | if (need_ellipse) self.bufWrite(&end, " ", .{}); | ||
| 210 | self.bufWrite(&end, "[{}/{}] ", .{ node.completed_items + 1, total }); | ||
| 211 | need_ellipse = false; | ||
| 212 | } else if (node.completed_items != 0) { | ||
| 213 | if (need_ellipse) self.bufWrite(&end, " ", .{}); | ||
| 214 | self.bufWrite(&end, "[{}] ", .{node.completed_items + 1}); | ||
| 215 | need_ellipse = false; | ||
| 216 | } | ||
| 217 | } | ||
| 218 | maybe_node = node.recently_updated_child; | ||
| 219 | } | ||
| 220 | if (need_ellipse) { | ||
| 221 | self.bufWrite(&end, "... ", .{}); | ||
| 222 | } | ||
| 223 | } | ||
| 224 | |||
| 225 | _ = file.write(self.output_buffer[0..end]) catch |e| { | ||
| 226 | // Stop trying to write to this file once it errors. | ||
| 227 | self.terminal = null; | ||
| 228 | }; | ||
| 229 | self.prev_refresh_timestamp = self.timer.read(); | ||
| 230 | } | ||
| 231 | |||
| 232 | pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { | ||
| 233 | const file = self.terminal orelse return; | ||
| 234 | self.refresh(); | ||
| 235 | file.outStream().print(format, args) catch { | ||
| 236 | self.terminal = null; | ||
| 237 | return; | ||
| 238 | }; | ||
| 239 | self.columns_written = 0; | ||
| 240 | } | ||
| 241 | |||
| 242 | fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { | ||
| 243 | if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { | ||
| 244 | const amt = written.len; | ||
| 245 | end.* += amt; | ||
| 246 | self.columns_written += amt; | ||
| 247 | } else |err| switch (err) { | ||
| 248 | error.NoSpaceLeft => { | ||
| 249 | self.columns_written += self.output_buffer.len - end.*; | ||
| 250 | end.* = self.output_buffer.len; | ||
| 251 | }, | ||
| 252 | } | ||
| 253 | const bytes_needed_for_esc_codes_at_end = if (std.builtin.os.tag == .windows) 0 else 11; | ||
| 254 | const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end; | ||
| 255 | if (end.* > max_end) { | ||
| 256 | const suffix = "... "; | ||
| 257 | self.columns_written = self.columns_written - (end.* - max_end) + suffix.len; | ||
| 258 | std.mem.copy(u8, self.output_buffer[max_end..], suffix); | ||
| 259 | end.* = max_end + suffix.len; | ||
| 260 | } | ||
| 261 | } | ||
| 262 | }; | ||
| 263 | |||
| 264 | test "basic functionality" { | ||
| 265 | var disable = true; | ||
| 266 | if (disable) { | ||
| 267 | // This test is disabled because it uses time.sleep() and is therefore slow. It also | ||
| 268 | // prints bogus progress data to stderr. | ||
| 269 | return error.SkipZigTest; | ||
| 270 | } | ||
| 271 | var progress = Progress{}; | ||
| 272 | const root_node = try progress.start("", 100); | ||
| 273 | defer root_node.end(); | ||
| 274 | |||
| 275 | const sub_task_names = [_][]const u8{ | ||
| 276 | "reticulating splines", | ||
| 277 | "adjusting shoes", | ||
| 278 | "climbing towers", | ||
| 279 | "pouring juice", | ||
| 280 | }; | ||
| 281 | var next_sub_task: usize = 0; | ||
| 282 | |||
| 283 | var i: usize = 0; | ||
| 284 | while (i < 100) : (i += 1) { | ||
| 285 | var node = root_node.start(sub_task_names[next_sub_task], 5); | ||
| 286 | node.activate(); | ||
| 287 | next_sub_task = (next_sub_task + 1) % sub_task_names.len; | ||
| 288 | |||
| 289 | node.completeOne(); | ||
| 290 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 291 | node.completeOne(); | ||
| 292 | node.completeOne(); | ||
| 293 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 294 | node.completeOne(); | ||
| 295 | node.completeOne(); | ||
| 296 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 297 | |||
| 298 | node.end(); | ||
| 299 | |||
| 300 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 301 | } | ||
| 302 | { | ||
| 303 | 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); | ||
| 304 | node.activate(); | ||
| 305 | std.time.sleep(10 * std.time.ns_per_ms); | ||
| 306 | progress.refresh(); | ||
| 307 | std.time.sleep(10 * std.time.ns_per_ms); | ||
| 308 | node.end(); | ||
| 309 | } | ||
| 310 | } | ||
lib/std/progress.zig deleted-310| ... | @@ -1,310 +0,0 @@ | ||
| 1 | // SPDX-License-Identifier: MIT | ||
| 2 | // Copyright (c) 2015-2020 Zig Contributors | ||
| 3 | // This file is part of [zig](https://ziglang.org/), which is MIT licensed. | ||
| 4 | // The MIT license requires this copyright notice to be included in all copies | ||
| 5 | // and substantial portions of the software. | ||
| 6 | const std = @import("std"); | ||
| 7 | const windows = std.os.windows; | ||
| 8 | const testing = std.testing; | ||
| 9 | const assert = std.debug.assert; | ||
| 10 | |||
| 11 | /// This API is non-allocating and non-fallible. The tradeoff is that users of | ||
| 12 | /// this API must provide the storage for each `Progress.Node`. | ||
| 13 | /// Initialize the struct directly, overriding these fields as desired: | ||
| 14 | /// * `refresh_rate_ms` | ||
| 15 | /// * `initial_delay_ms` | ||
| 16 | pub const Progress = struct { | ||
| 17 | /// `null` if the current node (and its children) should | ||
| 18 | /// not print on update() | ||
| 19 | terminal: ?std.fs.File = undefined, | ||
| 20 | |||
| 21 | /// Whether the terminal supports ANSI escape codes. | ||
| 22 | supports_ansi_escape_codes: bool = false, | ||
| 23 | |||
| 24 | root: Node = undefined, | ||
| 25 | |||
| 26 | /// Keeps track of how much time has passed since the beginning. | ||
| 27 | /// Used to compare with `initial_delay_ms` and `refresh_rate_ms`. | ||
| 28 | timer: std.time.Timer = undefined, | ||
| 29 | |||
| 30 | /// When the previous refresh was written to the terminal. | ||
| 31 | /// Used to compare with `refresh_rate_ms`. | ||
| 32 | prev_refresh_timestamp: u64 = undefined, | ||
| 33 | |||
| 34 | /// This buffer represents the maximum number of bytes written to the terminal | ||
| 35 | /// with each refresh. | ||
| 36 | output_buffer: [100]u8 = undefined, | ||
| 37 | |||
| 38 | /// How many nanoseconds between writing updates to the terminal. | ||
| 39 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, | ||
| 40 | |||
| 41 | /// How many nanoseconds to keep the output hidden | ||
| 42 | initial_delay_ns: u64 = 500 * std.time.ns_per_ms, | ||
| 43 | |||
| 44 | done: bool = true, | ||
| 45 | |||
| 46 | /// Keeps track of how many columns in the terminal have been output, so that | ||
| 47 | /// we can move the cursor back later. | ||
| 48 | columns_written: usize = undefined, | ||
| 49 | |||
| 50 | /// Represents one unit of progress. Each node can have children nodes, or | ||
| 51 | /// one can use integers with `update`. | ||
| 52 | pub const Node = struct { | ||
| 53 | context: *Progress, | ||
| 54 | parent: ?*Node, | ||
| 55 | completed_items: usize, | ||
| 56 | name: []const u8, | ||
| 57 | recently_updated_child: ?*Node = null, | ||
| 58 | |||
| 59 | /// This field may be updated freely. | ||
| 60 | estimated_total_items: ?usize, | ||
| 61 | |||
| 62 | /// Create a new child progress node. | ||
| 63 | /// Call `Node.end` when done. | ||
| 64 | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this | ||
| 65 | /// API to set `self.parent.recently_updated_child` with the return value. | ||
| 66 | /// Until that is fixed you probably want to call `activate` on the return value. | ||
| 67 | pub fn start(self: *Node, name: []const u8, estimated_total_items: ?usize) Node { | ||
| 68 | return Node{ | ||
| 69 | .context = self.context, | ||
| 70 | .parent = self, | ||
| 71 | .completed_items = 0, | ||
| 72 | .name = name, | ||
| 73 | .estimated_total_items = estimated_total_items, | ||
| 74 | }; | ||
| 75 | } | ||
| 76 | |||
| 77 | /// This is the same as calling `start` and then `end` on the returned `Node`. | ||
| 78 | pub fn completeOne(self: *Node) void { | ||
| 79 | if (self.parent) |parent| parent.recently_updated_child = self; | ||
| 80 | self.completed_items += 1; | ||
| 81 | self.context.maybeRefresh(); | ||
| 82 | } | ||
| 83 | |||
| 84 | pub fn end(self: *Node) void { | ||
| 85 | self.context.maybeRefresh(); | ||
| 86 | if (self.parent) |parent| { | ||
| 87 | if (parent.recently_updated_child) |parent_child| { | ||
| 88 | if (parent_child == self) { | ||
| 89 | parent.recently_updated_child = null; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | parent.completeOne(); | ||
| 93 | } else { | ||
| 94 | self.context.done = true; | ||
| 95 | self.context.refresh(); | ||
| 96 | } | ||
| 97 | } | ||
| 98 | |||
| 99 | /// Tell the parent node that this node is actively being worked on. | ||
| 100 | pub fn activate(self: *Node) void { | ||
| 101 | if (self.parent) |parent| parent.recently_updated_child = self; | ||
| 102 | } | ||
| 103 | }; | ||
| 104 | |||
| 105 | /// Create a new progress node. | ||
| 106 | /// Call `Node.end` when done. | ||
| 107 | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this | ||
| 108 | /// API to return Progress rather than accept it as a parameter. | ||
| 109 | pub fn start(self: *Progress, name: []const u8, estimated_total_items: ?usize) !*Node { | ||
| 110 | const stderr = std.io.getStdErr(); | ||
| 111 | self.terminal = null; | ||
| 112 | if (stderr.supportsAnsiEscapeCodes()) { | ||
| 113 | self.terminal = stderr; | ||
| 114 | self.supports_ansi_escape_codes = true; | ||
| 115 | } else if (std.builtin.os.tag == .windows and stderr.isTty()) { | ||
| 116 | self.terminal = stderr; | ||
| 117 | } | ||
| 118 | self.root = Node{ | ||
| 119 | .context = self, | ||
| 120 | .parent = null, | ||
| 121 | .completed_items = 0, | ||
| 122 | .name = name, | ||
| 123 | .estimated_total_items = estimated_total_items, | ||
| 124 | }; | ||
| 125 | self.columns_written = 0; | ||
| 126 | self.prev_refresh_timestamp = 0; | ||
| 127 | self.timer = try std.time.Timer.start(); | ||
| 128 | self.done = false; | ||
| 129 | return &self.root; | ||
| 130 | } | ||
| 131 | |||
| 132 | /// Updates the terminal if enough time has passed since last update. | ||
| 133 | pub fn maybeRefresh(self: *Progress) void { | ||
| 134 | const now = self.timer.read(); | ||
| 135 | if (now < self.initial_delay_ns) return; | ||
| 136 | if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return; | ||
| 137 | self.refresh(); | ||
| 138 | } | ||
| 139 | |||
| 140 | /// Updates the terminal and resets `self.next_refresh_timestamp`. | ||
| 141 | pub fn refresh(self: *Progress) void { | ||
| 142 | const file = self.terminal orelse return; | ||
| 143 | |||
| 144 | const prev_columns_written = self.columns_written; | ||
| 145 | var end: usize = 0; | ||
| 146 | if (self.columns_written > 0) { | ||
| 147 | // restore the cursor position by moving the cursor | ||
| 148 | // `columns_written` cells to the left, then clear the rest of the | ||
| 149 | // line | ||
| 150 | if (self.supports_ansi_escape_codes) { | ||
| 151 | end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{}D", .{self.columns_written}) catch unreachable).len; | ||
| 152 | end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; | ||
| 153 | } else if (std.builtin.os.tag == .windows) winapi: { | ||
| 154 | var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; | ||
| 155 | if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) | ||
| 156 | unreachable; | ||
| 157 | |||
| 158 | var cursor_pos = windows.COORD{ | ||
| 159 | .X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written), | ||
| 160 | .Y = info.dwCursorPosition.Y, | ||
| 161 | }; | ||
| 162 | |||
| 163 | if (cursor_pos.X < 0) | ||
| 164 | cursor_pos.X = 0; | ||
| 165 | |||
| 166 | const fill_chars = @intCast(windows.DWORD, info.dwSize.X - cursor_pos.X); | ||
| 167 | |||
| 168 | var written: windows.DWORD = undefined; | ||
| 169 | if (windows.kernel32.FillConsoleOutputAttribute( | ||
| 170 | file.handle, | ||
| 171 | info.wAttributes, | ||
| 172 | fill_chars, | ||
| 173 | cursor_pos, | ||
| 174 | &written, | ||
| 175 | ) != windows.TRUE) { | ||
| 176 | // Stop trying to write to this file. | ||
| 177 | self.terminal = null; | ||
| 178 | break :winapi; | ||
| 179 | } | ||
| 180 | if (windows.kernel32.FillConsoleOutputCharacterA( | ||
| 181 | file.handle, | ||
| 182 | ' ', | ||
| 183 | fill_chars, | ||
| 184 | cursor_pos, | ||
| 185 | &written, | ||
| 186 | ) != windows.TRUE) unreachable; | ||
| 187 | |||
| 188 | if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE) | ||
| 189 | unreachable; | ||
| 190 | } else unreachable; | ||
| 191 | |||
| 192 | self.columns_written = 0; | ||
| 193 | } | ||
| 194 | |||
| 195 | if (!self.done) { | ||
| 196 | var need_ellipse = false; | ||
| 197 | var maybe_node: ?*Node = &self.root; | ||
| 198 | while (maybe_node) |node| { | ||
| 199 | if (need_ellipse) { | ||
| 200 | self.bufWrite(&end, "... ", .{}); | ||
| 201 | } | ||
| 202 | need_ellipse = false; | ||
| 203 | if (node.name.len != 0 or node.estimated_total_items != null) { | ||
| 204 | if (node.name.len != 0) { | ||
| 205 | self.bufWrite(&end, "{}", .{node.name}); | ||
| 206 | need_ellipse = true; | ||
| 207 | } | ||
| 208 | if (node.estimated_total_items) |total| { | ||
| 209 | if (need_ellipse) self.bufWrite(&end, " ", .{}); | ||
| 210 | self.bufWrite(&end, "[{}/{}] ", .{ node.completed_items + 1, total }); | ||
| 211 | need_ellipse = false; | ||
| 212 | } else if (node.completed_items != 0) { | ||
| 213 | if (need_ellipse) self.bufWrite(&end, " ", .{}); | ||
| 214 | self.bufWrite(&end, "[{}] ", .{node.completed_items + 1}); | ||
| 215 | need_ellipse = false; | ||
| 216 | } | ||
| 217 | } | ||
| 218 | maybe_node = node.recently_updated_child; | ||
| 219 | } | ||
| 220 | if (need_ellipse) { | ||
| 221 | self.bufWrite(&end, "... ", .{}); | ||
| 222 | } | ||
| 223 | } | ||
| 224 | |||
| 225 | _ = file.write(self.output_buffer[0..end]) catch |e| { | ||
| 226 | // Stop trying to write to this file once it errors. | ||
| 227 | self.terminal = null; | ||
| 228 | }; | ||
| 229 | self.prev_refresh_timestamp = self.timer.read(); | ||
| 230 | } | ||
| 231 | |||
| 232 | pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { | ||
| 233 | const file = self.terminal orelse return; | ||
| 234 | self.refresh(); | ||
| 235 | file.outStream().print(format, args) catch { | ||
| 236 | self.terminal = null; | ||
| 237 | return; | ||
| 238 | }; | ||
| 239 | self.columns_written = 0; | ||
| 240 | } | ||
| 241 | |||
| 242 | fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { | ||
| 243 | if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { | ||
| 244 | const amt = written.len; | ||
| 245 | end.* += amt; | ||
| 246 | self.columns_written += amt; | ||
| 247 | } else |err| switch (err) { | ||
| 248 | error.NoSpaceLeft => { | ||
| 249 | self.columns_written += self.output_buffer.len - end.*; | ||
| 250 | end.* = self.output_buffer.len; | ||
| 251 | }, | ||
| 252 | } | ||
| 253 | const bytes_needed_for_esc_codes_at_end = if (std.builtin.os.tag == .windows) 0 else 11; | ||
| 254 | const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end; | ||
| 255 | if (end.* > max_end) { | ||
| 256 | const suffix = "... "; | ||
| 257 | self.columns_written = self.columns_written - (end.* - max_end) + suffix.len; | ||
| 258 | std.mem.copy(u8, self.output_buffer[max_end..], suffix); | ||
| 259 | end.* = max_end + suffix.len; | ||
| 260 | } | ||
| 261 | } | ||
| 262 | }; | ||
| 263 | |||
| 264 | test "basic functionality" { | ||
| 265 | var disable = true; | ||
| 266 | if (disable) { | ||
| 267 | // This test is disabled because it uses time.sleep() and is therefore slow. It also | ||
| 268 | // prints bogus progress data to stderr. | ||
| 269 | return error.SkipZigTest; | ||
| 270 | } | ||
| 271 | var progress = Progress{}; | ||
| 272 | const root_node = try progress.start("", 100); | ||
| 273 | defer root_node.end(); | ||
| 274 | |||
| 275 | const sub_task_names = [_][]const u8{ | ||
| 276 | "reticulating splines", | ||
| 277 | "adjusting shoes", | ||
| 278 | "climbing towers", | ||
| 279 | "pouring juice", | ||
| 280 | }; | ||
| 281 | var next_sub_task: usize = 0; | ||
| 282 | |||
| 283 | var i: usize = 0; | ||
| 284 | while (i < 100) : (i += 1) { | ||
| 285 | var node = root_node.start(sub_task_names[next_sub_task], 5); | ||
| 286 | node.activate(); | ||
| 287 | next_sub_task = (next_sub_task + 1) % sub_task_names.len; | ||
| 288 | |||
| 289 | node.completeOne(); | ||
| 290 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 291 | node.completeOne(); | ||
| 292 | node.completeOne(); | ||
| 293 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 294 | node.completeOne(); | ||
| 295 | node.completeOne(); | ||
| 296 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 297 | |||
| 298 | node.end(); | ||
| 299 | |||
| 300 | std.time.sleep(5 * std.time.ns_per_ms); | ||
| 301 | } | ||
| 302 | { | ||
| 303 | 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); | ||
| 304 | node.activate(); | ||
| 305 | std.time.sleep(10 * std.time.ns_per_ms); | ||
| 306 | progress.refresh(); | ||
| 307 | std.time.sleep(10 * std.time.ns_per_ms); | ||
| 308 | node.end(); | ||
| 309 | } | ||
| 310 | } | ||
src/ThreadPool.zig-1| ... | @@ -77,7 +77,6 @@ pub fn spawn(self: *ThreadPool, comptime func: anytype, args: anytype) !void { | ... | @@ -77,7 +77,6 @@ pub fn spawn(self: *ThreadPool, comptime func: anytype, args: anytype) !void { |
| 77 | }; | 77 | }; |
| 78 | 78 | ||
| 79 | const closure = try self.allocator.create(Closure); | 79 | const closure = try self.allocator.create(Closure); |
| 80 | errdefer self.allocator.destroy(closure); | ||
| 81 | closure.* = .{ | 80 | closure.* = .{ |
| 82 | .arguments = args, | 81 | .arguments = args, |
| 83 | .pool = self, | 82 | .pool = self, |