| ... | ... | @@ -1,10 +1,7 @@ |
| 1 | 1 | //! This API is non-allocating, non-fallible, and thread-safe. |
| 2 | //! |
| 2 | 3 | //! The tradeoff is that users of this API must provide the storage |
| 3 | 4 | //! for each `Progress.Node`. |
| 4 | | //! |
| 5 | | //! Initialize the struct directly, overriding these fields as desired: |
| 6 | | //! * `refresh_rate_ms` |
| 7 | | //! * `initial_delay_ms` |
| 8 | 5 | |
| 9 | 6 | const std = @import("std"); |
| 10 | 7 | const builtin = @import("builtin"); |
| ... | ... | @@ -12,63 +9,64 @@ const windows = std.os.windows; |
| 12 | 9 | const testing = std.testing; |
| 13 | 10 | const assert = std.debug.assert; |
| 14 | 11 | const Progress = @This(); |
| 12 | const posix = std.posix; |
| 15 | 13 | |
| 16 | 14 | /// `null` if the current node (and its children) should |
| 17 | 15 | /// not print on update() |
| 18 | | terminal: ?std.fs.File = undefined, |
| 16 | terminal: ?std.fs.File, |
| 19 | 17 | |
| 20 | 18 | /// Is this a windows API terminal (note: this is not the same as being run on windows |
| 21 | 19 | /// because other terminals exist like MSYS/git-bash) |
| 22 | | is_windows_terminal: bool = false, |
| 20 | is_windows_terminal: bool, |
| 23 | 21 | |
| 24 | 22 | /// Whether the terminal supports ANSI escape codes. |
| 25 | | supports_ansi_escape_codes: bool = false, |
| 26 | | |
| 27 | | /// If the terminal is "dumb", don't print output. |
| 28 | | /// This can be useful if you don't want to print all |
| 29 | | /// the stages of code generation if there are a lot. |
| 30 | | /// You should not use it if the user should see output |
| 31 | | /// for example showing the user what tests run. |
| 32 | | dont_print_on_dumb: bool = false, |
| 33 | | |
| 34 | | root: Node = undefined, |
| 35 | | |
| 36 | | /// Keeps track of how much time has passed since the beginning. |
| 37 | | /// Used to compare with `initial_delay_ms` and `refresh_rate_ms`. |
| 38 | | timer: ?std.time.Timer = null, |
| 39 | | |
| 40 | | /// When the previous refresh was written to the terminal. |
| 41 | | /// Used to compare with `refresh_rate_ms`. |
| 42 | | prev_refresh_timestamp: u64 = undefined, |
| 43 | | |
| 44 | | /// This buffer represents the maximum number of bytes written to the terminal |
| 45 | | /// with each refresh. |
| 46 | | output_buffer: [100]u8 = undefined, |
| 47 | | |
| 48 | | /// How many nanoseconds between writing updates to the terminal. |
| 49 | | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, |
| 50 | | |
| 51 | | /// How many nanoseconds to keep the output hidden |
| 52 | | initial_delay_ns: u64 = 500 * std.time.ns_per_ms, |
| 53 | | |
| 54 | | done: bool = true, |
| 55 | | |
| 56 | | /// Protects the `refresh` function, as well as `node.recently_updated_child`. |
| 57 | | /// Without this, callsites would call `Node.end` and then free `Node` memory |
| 58 | | /// while it was still being accessed by the `refresh` function. |
| 59 | | update_mutex: std.Thread.Mutex = .{}, |
| 60 | | |
| 61 | | /// Keeps track of how many columns in the terminal have been output, so that |
| 62 | | /// we can move the cursor back later. |
| 63 | | columns_written: usize = undefined, |
| 23 | supports_ansi_escape_codes: bool, |
| 24 | |
| 25 | root: Node, |
| 26 | |
| 27 | /// Protects all the state shared between the update thread and the public API calls. |
| 28 | mutex: std.Thread.Mutex, |
| 29 | update_thread: ?std.Thread, |
| 30 | |
| 31 | /// Atomically set by SIGWINCH as well as the root done() function. |
| 32 | redraw_event: std.Thread.ResetEvent, |
| 33 | /// Ensure there is only 1 global Progress object. |
| 34 | initialized: bool, |
| 35 | /// Indicates a request to shut down and reset global state. |
| 36 | done: bool, |
| 37 | |
| 38 | refresh_rate_ns: u64, |
| 39 | initial_delay_ns: u64, |
| 40 | |
| 41 | rows: u16, |
| 42 | cols: u16, |
| 43 | |
| 44 | /// Accessed only by the update thread. |
| 45 | draw_buffer: []u8, |
| 46 | |
| 47 | pub const Options = struct { |
| 48 | /// User-provided buffer with static lifetime. |
| 49 | /// |
| 50 | /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it |
| 51 | /// cannot fit into this buffer which will look bad but not cause any malfunctions. |
| 52 | /// |
| 53 | /// Must be at least 100 bytes. |
| 54 | draw_buffer: []u8, |
| 55 | /// How many nanoseconds between writing updates to the terminal. |
| 56 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, |
| 57 | /// How many nanoseconds to keep the output hidden |
| 58 | initial_delay_ns: u64 = 500 * std.time.ns_per_ms, |
| 59 | /// If provided, causes the progress item to have a denominator. |
| 60 | /// 0 means unknown. |
| 61 | estimated_total_items: usize = 0, |
| 62 | root_name: []const u8 = "", |
| 63 | }; |
| 64 | 64 | |
| 65 | 65 | /// Represents one unit of progress. Each node can have children nodes, or |
| 66 | 66 | /// one can use integers with `update`. |
| 67 | 67 | pub const Node = struct { |
| 68 | | context: *Progress, |
| 69 | 68 | parent: ?*Node, |
| 70 | 69 | name: []const u8, |
| 71 | | unit: []const u8 = "", |
| 72 | 70 | /// Must be handled atomically to be thread-safe. |
| 73 | 71 | recently_updated_child: ?*Node = null, |
| 74 | 72 | /// Must be handled atomically to be thread-safe. 0 means null. |
| ... | ... | @@ -76,15 +74,15 @@ pub const Node = struct { |
| 76 | 74 | /// Must be handled atomically to be thread-safe. |
| 77 | 75 | unprotected_completed_items: usize, |
| 78 | 76 | |
| 77 | pub const ListNode = std.DoublyLinkedList(void); |
| 78 | |
| 79 | 79 | /// Create a new child progress node. Thread-safe. |
| 80 | /// |
| 80 | 81 | /// Call `Node.end` when done. |
| 81 | | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this |
| 82 | | /// API to set `self.parent.recently_updated_child` with the return value. |
| 83 | | /// Until that is fixed you probably want to call `activate` on the return value. |
| 82 | /// |
| 84 | 83 | /// Passing 0 for `estimated_total_items` means unknown. |
| 85 | 84 | pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node { |
| 86 | | return Node{ |
| 87 | | .context = self.context, |
| 85 | return .{ |
| 88 | 86 | .parent = self, |
| 89 | 87 | .name = name, |
| 90 | 88 | .unprotected_estimated_total_items = estimated_total_items, |
| ... | ... | @@ -94,66 +92,33 @@ pub const Node = struct { |
| 94 | 92 | |
| 95 | 93 | /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe. |
| 96 | 94 | pub fn completeOne(self: *Node) void { |
| 97 | | if (self.parent) |parent| { |
| 98 | | @atomicStore(?*Node, &parent.recently_updated_child, self, .release); |
| 99 | | } |
| 100 | 95 | _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .monotonic); |
| 101 | | self.context.maybeRefresh(); |
| 96 | self.activate(); |
| 102 | 97 | } |
| 103 | 98 | |
| 104 | 99 | /// Finish a started `Node`. Thread-safe. |
| 105 | 100 | pub fn end(self: *Node) void { |
| 106 | | self.context.maybeRefresh(); |
| 107 | 101 | if (self.parent) |parent| { |
| 108 | | { |
| 109 | | self.context.update_mutex.lock(); |
| 110 | | defer self.context.update_mutex.unlock(); |
| 111 | | _ = @cmpxchgStrong(?*Node, &parent.recently_updated_child, self, null, .monotonic, .monotonic); |
| 112 | | } |
| 113 | 102 | parent.completeOne(); |
| 114 | 103 | } else { |
| 115 | | self.context.update_mutex.lock(); |
| 116 | | defer self.context.update_mutex.unlock(); |
| 117 | | self.context.done = true; |
| 118 | | self.context.refreshWithHeldLock(); |
| 104 | { |
| 105 | global_progress.mutex.lock(); |
| 106 | defer global_progress.mutex.unlock(); |
| 107 | global_progress.done = true; |
| 108 | } |
| 109 | global_progress.redraw_event.set(); |
| 110 | if (global_progress.update_thread) |thread| thread.join(); |
| 119 | 111 | } |
| 120 | 112 | } |
| 121 | 113 | |
| 122 | 114 | /// Tell the parent node that this node is actively being worked on. Thread-safe. |
| 123 | 115 | pub fn activate(self: *Node) void { |
| 124 | | if (self.parent) |parent| { |
| 125 | | @atomicStore(?*Node, &parent.recently_updated_child, self, .release); |
| 126 | | self.context.maybeRefresh(); |
| 127 | | } |
| 128 | | } |
| 129 | | |
| 130 | | /// Thread-safe. |
| 131 | | pub fn setName(self: *Node, name: []const u8) void { |
| 132 | | const progress = self.context; |
| 133 | | progress.update_mutex.lock(); |
| 134 | | defer progress.update_mutex.unlock(); |
| 135 | | self.name = name; |
| 136 | | if (self.parent) |parent| { |
| 137 | | @atomicStore(?*Node, &parent.recently_updated_child, self, .release); |
| 138 | | if (parent.parent) |grand_parent| { |
| 139 | | @atomicStore(?*Node, &grand_parent.recently_updated_child, parent, .release); |
| 140 | | } |
| 141 | | if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); |
| 142 | | } |
| 143 | | } |
| 144 | | |
| 145 | | /// Thread-safe. |
| 146 | | pub fn setUnit(self: *Node, unit: []const u8) void { |
| 147 | | const progress = self.context; |
| 148 | | progress.update_mutex.lock(); |
| 149 | | defer progress.update_mutex.unlock(); |
| 150 | | self.unit = unit; |
| 151 | | if (self.parent) |parent| { |
| 152 | | @atomicStore(?*Node, &parent.recently_updated_child, self, .release); |
| 153 | | if (parent.parent) |grand_parent| { |
| 154 | | @atomicStore(?*Node, &grand_parent.recently_updated_child, parent, .release); |
| 155 | | } |
| 156 | | if (progress.timer) |*timer| progress.maybeRefreshWithHeldLock(timer); |
| 116 | var parent = self.parent; |
| 117 | var child = self; |
| 118 | while (parent) |p| { |
| 119 | @atomicStore(?*Node, &p.recently_updated_child, child, .release); |
| 120 | child = p; |
| 121 | parent = p.parent; |
| 157 | 122 | } |
| 158 | 123 | } |
| 159 | 124 | |
| ... | ... | @@ -168,280 +133,202 @@ pub const Node = struct { |
| 168 | 133 | } |
| 169 | 134 | }; |
| 170 | 135 | |
| 171 | | /// Create a new progress node. |
| 136 | var global_progress: Progress = .{ |
| 137 | .terminal = null, |
| 138 | .is_windows_terminal = false, |
| 139 | .supports_ansi_escape_codes = false, |
| 140 | .root = undefined, |
| 141 | .mutex = .{}, |
| 142 | .update_thread = null, |
| 143 | .redraw_event = .{}, |
| 144 | .initialized = false, |
| 145 | .refresh_rate_ns = undefined, |
| 146 | .initial_delay_ns = undefined, |
| 147 | .rows = 0, |
| 148 | .cols = 0, |
| 149 | .draw_buffer = undefined, |
| 150 | .done = false, |
| 151 | }; |
| 152 | |
| 153 | /// Initializes a global Progress instance. |
| 154 | /// |
| 155 | /// Asserts there is only one global Progress instance. |
| 156 | /// |
| 172 | 157 | /// Call `Node.end` when done. |
| 173 | | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this |
| 174 | | /// API to return Progress rather than accept it as a parameter. |
| 175 | | /// `estimated_total_items` value of 0 means unknown. |
| 176 | | pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node { |
| 158 | pub fn start(options: Options) *Node { |
| 159 | assert(!global_progress.initialized); |
| 177 | 160 | const stderr = std.io.getStdErr(); |
| 178 | | self.terminal = null; |
| 179 | 161 | if (stderr.supportsAnsiEscapeCodes()) { |
| 180 | | self.terminal = stderr; |
| 181 | | self.supports_ansi_escape_codes = true; |
| 162 | global_progress.terminal = stderr; |
| 163 | global_progress.supports_ansi_escape_codes = true; |
| 182 | 164 | } else if (builtin.os.tag == .windows and stderr.isTty()) { |
| 183 | | self.is_windows_terminal = true; |
| 184 | | self.terminal = stderr; |
| 165 | global_progress.is_windows_terminal = true; |
| 166 | global_progress.terminal = stderr; |
| 185 | 167 | } else if (builtin.os.tag != .windows) { |
| 186 | 168 | // we are in a "dumb" terminal like in acme or writing to a file |
| 187 | | self.terminal = stderr; |
| 169 | global_progress.terminal = stderr; |
| 188 | 170 | } |
| 189 | | self.root = Node{ |
| 190 | | .context = self, |
| 171 | global_progress.root = .{ |
| 191 | 172 | .parent = null, |
| 192 | | .name = name, |
| 193 | | .unprotected_estimated_total_items = estimated_total_items, |
| 173 | .name = options.root_name, |
| 174 | .unprotected_estimated_total_items = options.estimated_total_items, |
| 194 | 175 | .unprotected_completed_items = 0, |
| 195 | 176 | }; |
| 196 | | self.columns_written = 0; |
| 197 | | self.prev_refresh_timestamp = 0; |
| 198 | | self.timer = std.time.Timer.start() catch null; |
| 199 | | self.done = false; |
| 200 | | return &self.root; |
| 201 | | } |
| 177 | global_progress.done = false; |
| 178 | global_progress.initialized = true; |
| 179 | |
| 180 | assert(options.draw_buffer.len >= 100); |
| 181 | global_progress.draw_buffer = options.draw_buffer; |
| 182 | global_progress.refresh_rate_ns = options.refresh_rate_ns; |
| 183 | global_progress.initial_delay_ns = options.initial_delay_ns; |
| 184 | |
| 185 | var act: posix.Sigaction = .{ |
| 186 | .handler = .{ .sigaction = handleSigWinch }, |
| 187 | .mask = posix.empty_sigset, |
| 188 | .flags = (posix.SA.SIGINFO | posix.SA.RESTART), |
| 189 | }; |
| 190 | posix.sigaction(posix.SIG.WINCH, &act, null) catch { |
| 191 | global_progress.terminal = null; |
| 192 | return &global_progress.root; |
| 193 | }; |
| 202 | 194 | |
| 203 | | /// Updates the terminal if enough time has passed since last update. Thread-safe. |
| 204 | | pub fn maybeRefresh(self: *Progress) void { |
| 205 | | if (self.timer) |*timer| { |
| 206 | | if (!self.update_mutex.tryLock()) return; |
| 207 | | defer self.update_mutex.unlock(); |
| 208 | | maybeRefreshWithHeldLock(self, timer); |
| 195 | if (global_progress.terminal != null) { |
| 196 | if (std.Thread.spawn(.{}, updateThreadRun, .{})) |thread| { |
| 197 | global_progress.update_thread = thread; |
| 198 | } else |_| { |
| 199 | global_progress.terminal = null; |
| 200 | } |
| 209 | 201 | } |
| 202 | |
| 203 | return &global_progress.root; |
| 210 | 204 | } |
| 211 | 205 | |
| 212 | | fn maybeRefreshWithHeldLock(self: *Progress, timer: *std.time.Timer) void { |
| 213 | | const now = timer.read(); |
| 214 | | if (now < self.initial_delay_ns) return; |
| 215 | | // TODO I have observed this to happen sometimes. I think we need to follow Rust's |
| 216 | | // lead and guarantee monotonically increasing times in the std lib itself. |
| 217 | | if (now < self.prev_refresh_timestamp) return; |
| 218 | | if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return; |
| 219 | | return self.refreshWithHeldLock(); |
| 206 | /// Returns whether a resize is needed to learn the terminal size. |
| 207 | fn wait(timeout_ns: u64) bool { |
| 208 | const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_| |
| 209 | true |
| 210 | else |err| switch (err) { |
| 211 | error.Timeout => false, |
| 212 | }; |
| 213 | global_progress.redraw_event.reset(); |
| 214 | return resize_flag or (global_progress.cols == 0); |
| 220 | 215 | } |
| 221 | 216 | |
| 222 | | /// Updates the terminal and resets `self.next_refresh_timestamp`. Thread-safe. |
| 223 | | pub fn refresh(self: *Progress) void { |
| 224 | | if (!self.update_mutex.tryLock()) return; |
| 225 | | defer self.update_mutex.unlock(); |
| 217 | fn updateThreadRun() void { |
| 218 | { |
| 219 | const resize_flag = wait(global_progress.initial_delay_ns); |
| 220 | maybeUpdateSize(resize_flag); |
| 226 | 221 | |
| 227 | | return self.refreshWithHeldLock(); |
| 228 | | } |
| 222 | const buffer = b: { |
| 223 | global_progress.mutex.lock(); |
| 224 | defer global_progress.mutex.unlock(); |
| 229 | 225 | |
| 230 | | fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { |
| 231 | | const file = p.terminal orelse return; |
| 232 | | var end = end_ptr.*; |
| 233 | | if (p.columns_written > 0) { |
| 234 | | // restore the cursor position by moving the cursor |
| 235 | | // `columns_written` cells to the left, then clear the rest of the |
| 236 | | // line |
| 237 | | if (p.supports_ansi_escape_codes) { |
| 238 | | end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[{d}D", .{p.columns_written}) catch unreachable).len; |
| 239 | | end += (std.fmt.bufPrint(p.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len; |
| 240 | | } else if (builtin.os.tag == .windows) winapi: { |
| 241 | | std.debug.assert(p.is_windows_terminal); |
| 242 | | |
| 243 | | var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; |
| 244 | | if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE) { |
| 245 | | // stop trying to write to this file |
| 246 | | p.terminal = null; |
| 247 | | break :winapi; |
| 248 | | } |
| 226 | if (global_progress.done) return clearTerminal(); |
| 249 | 227 | |
| 250 | | var cursor_pos = windows.COORD{ |
| 251 | | .X = info.dwCursorPosition.X - @as(windows.SHORT, @intCast(p.columns_written)), |
| 252 | | .Y = info.dwCursorPosition.Y, |
| 253 | | }; |
| 254 | | |
| 255 | | if (cursor_pos.X < 0) |
| 256 | | cursor_pos.X = 0; |
| 257 | | |
| 258 | | const fill_chars = @as(windows.DWORD, @intCast(info.dwSize.X - cursor_pos.X)); |
| 259 | | |
| 260 | | var written: windows.DWORD = undefined; |
| 261 | | if (windows.kernel32.FillConsoleOutputAttribute( |
| 262 | | file.handle, |
| 263 | | info.wAttributes, |
| 264 | | fill_chars, |
| 265 | | cursor_pos, |
| 266 | | &written, |
| 267 | | ) != windows.TRUE) { |
| 268 | | // stop trying to write to this file |
| 269 | | p.terminal = null; |
| 270 | | break :winapi; |
| 271 | | } |
| 272 | | if (windows.kernel32.FillConsoleOutputCharacterW( |
| 273 | | file.handle, |
| 274 | | ' ', |
| 275 | | fill_chars, |
| 276 | | cursor_pos, |
| 277 | | &written, |
| 278 | | ) != windows.TRUE) { |
| 279 | | // stop trying to write to this file |
| 280 | | p.terminal = null; |
| 281 | | break :winapi; |
| 282 | | } |
| 283 | | if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE) { |
| 284 | | // stop trying to write to this file |
| 285 | | p.terminal = null; |
| 286 | | break :winapi; |
| 287 | | } |
| 288 | | } else { |
| 289 | | // we are in a "dumb" terminal like in acme or writing to a file |
| 290 | | p.output_buffer[end] = '\n'; |
| 291 | | end += 1; |
| 292 | | } |
| 228 | break :b computeRedraw(); |
| 229 | }; |
| 230 | write(buffer); |
| 231 | } |
| 232 | |
| 233 | while (true) { |
| 234 | const resize_flag = wait(global_progress.refresh_rate_ns); |
| 235 | maybeUpdateSize(resize_flag); |
| 293 | 236 | |
| 294 | | p.columns_written = 0; |
| 237 | const buffer = b: { |
| 238 | global_progress.mutex.lock(); |
| 239 | defer global_progress.mutex.unlock(); |
| 240 | |
| 241 | if (global_progress.done) return clearTerminal(); |
| 242 | |
| 243 | break :b computeRedraw(); |
| 244 | }; |
| 245 | write(buffer); |
| 295 | 246 | } |
| 296 | | end_ptr.* = end; |
| 297 | 247 | } |
| 298 | 248 | |
| 299 | | fn refreshWithHeldLock(self: *Progress) void { |
| 300 | | const is_dumb = !self.supports_ansi_escape_codes and !self.is_windows_terminal; |
| 301 | | if (is_dumb and self.dont_print_on_dumb) return; |
| 249 | const start_sync = "\x1b[?2026h"; |
| 250 | const clear = "\x1b[J"; |
| 251 | const save = "\x1b7"; |
| 252 | const restore = "\x1b8"; |
| 253 | const finish_sync = "\x1b[?2026l"; |
| 254 | |
| 255 | fn clearTerminal() void { |
| 256 | write(clear); |
| 257 | } |
| 258 | |
| 259 | fn computeRedraw() []u8 { |
| 260 | // The strategy is: keep the cursor at the beginning, and then with every redraw: |
| 261 | // erase, save, write, restore |
| 262 | |
| 263 | var i: usize = 0; |
| 264 | const buf = global_progress.draw_buffer; |
| 265 | |
| 266 | const prefix = start_sync ++ clear ++ save; |
| 267 | const suffix = restore ++ finish_sync; |
| 268 | |
| 269 | buf[0..prefix.len].* = prefix.*; |
| 270 | i = prefix.len; |
| 302 | 271 | |
| 303 | | const file = self.terminal orelse return; |
| 272 | // Walk the tree and write the progress output to the buffer. |
| 304 | 273 | |
| 305 | | var end: usize = 0; |
| 306 | | clearWithHeldLock(self, &end); |
| 274 | var node: *Node = &global_progress.root; |
| 275 | while (true) { |
| 276 | const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); |
| 277 | const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .monotonic); |
| 307 | 278 | |
| 308 | | if (!self.done) { |
| 309 | | var need_ellipse = false; |
| 310 | | var maybe_node: ?*Node = &self.root; |
| 311 | | while (maybe_node) |node| { |
| 312 | | if (need_ellipse) { |
| 313 | | self.bufWrite(&end, "... ", .{}); |
| 279 | if (node.name.len != 0 or eti > 0) { |
| 280 | if (node.name.len != 0) { |
| 281 | i += (std.fmt.bufPrint(buf[i..], "{s}", .{node.name}) catch @panic("TODO")).len; |
| 314 | 282 | } |
| 315 | | need_ellipse = false; |
| 316 | | const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); |
| 317 | | const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .monotonic); |
| 318 | | const current_item = completed_items + 1; |
| 319 | | if (node.name.len != 0 or eti > 0) { |
| 320 | | if (node.name.len != 0) { |
| 321 | | self.bufWrite(&end, "{s}", .{node.name}); |
| 322 | | need_ellipse = true; |
| 323 | | } |
| 324 | | if (eti > 0) { |
| 325 | | if (need_ellipse) self.bufWrite(&end, " ", .{}); |
| 326 | | self.bufWrite(&end, "[{d}/{d}{s}] ", .{ current_item, eti, node.unit }); |
| 327 | | need_ellipse = false; |
| 328 | | } else if (completed_items != 0) { |
| 329 | | if (need_ellipse) self.bufWrite(&end, " ", .{}); |
| 330 | | self.bufWrite(&end, "[{d}{s}] ", .{ current_item, node.unit }); |
| 331 | | need_ellipse = false; |
| 332 | | } |
| 283 | if (eti > 0) { |
| 284 | i += (std.fmt.bufPrint(buf[i..], "[{d}/{d}] ", .{ completed_items, eti }) catch @panic("TODO")).len; |
| 285 | } else if (completed_items != 0) { |
| 286 | i += (std.fmt.bufPrint(buf[i..], "[{d}] ", .{completed_items}) catch @panic("TODO")).len; |
| 333 | 287 | } |
| 334 | | maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .acquire); |
| 335 | 288 | } |
| 336 | | if (need_ellipse) { |
| 337 | | self.bufWrite(&end, "... ", .{}); |
| 338 | | } |
| 339 | | } |
| 340 | 289 | |
| 341 | | _ = file.write(self.output_buffer[0..end]) catch { |
| 342 | | // stop trying to write to this file |
| 343 | | self.terminal = null; |
| 344 | | }; |
| 345 | | if (self.timer) |*timer| { |
| 346 | | self.prev_refresh_timestamp = timer.read(); |
| 290 | node = @atomicLoad(?*Node, &node.recently_updated_child, .acquire) orelse break; |
| 347 | 291 | } |
| 348 | | } |
| 349 | 292 | |
| 350 | | pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { |
| 351 | | const file = self.terminal orelse { |
| 352 | | std.debug.print(format, args); |
| 353 | | return; |
| 354 | | }; |
| 355 | | self.refresh(); |
| 356 | | file.writer().print(format, args) catch { |
| 357 | | self.terminal = null; |
| 358 | | return; |
| 359 | | }; |
| 360 | | self.columns_written = 0; |
| 361 | | } |
| 293 | i = @min(global_progress.cols + prefix.len, i); |
| 362 | 294 | |
| 363 | | /// Allows the caller to freely write to stderr until unlock_stderr() is called. |
| 364 | | /// During the lock, the progress information is cleared from the terminal. |
| 365 | | pub fn lock_stderr(p: *Progress) void { |
| 366 | | p.update_mutex.lock(); |
| 367 | | if (p.terminal) |file| { |
| 368 | | var end: usize = 0; |
| 369 | | clearWithHeldLock(p, &end); |
| 370 | | _ = file.write(p.output_buffer[0..end]) catch { |
| 371 | | // stop trying to write to this file |
| 372 | | p.terminal = null; |
| 373 | | }; |
| 374 | | } |
| 375 | | std.debug.getStderrMutex().lock(); |
| 376 | | } |
| 295 | buf[i..][0..suffix.len].* = suffix.*; |
| 296 | i += suffix.len; |
| 377 | 297 | |
| 378 | | pub fn unlock_stderr(p: *Progress) void { |
| 379 | | std.debug.getStderrMutex().unlock(); |
| 380 | | p.update_mutex.unlock(); |
| 298 | return buf[0..i]; |
| 381 | 299 | } |
| 382 | 300 | |
| 383 | | fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { |
| 384 | | if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { |
| 385 | | const amt = written.len; |
| 386 | | end.* += amt; |
| 387 | | self.columns_written += amt; |
| 388 | | } else |err| switch (err) { |
| 389 | | error.NoSpaceLeft => { |
| 390 | | self.columns_written += self.output_buffer.len - end.*; |
| 391 | | end.* = self.output_buffer.len; |
| 392 | | const suffix = "... "; |
| 393 | | @memcpy(self.output_buffer[self.output_buffer.len - suffix.len ..], suffix); |
| 394 | | }, |
| 395 | | } |
| 301 | fn write(buf: []const u8) void { |
| 302 | const tty = global_progress.terminal orelse return; |
| 303 | tty.writeAll(buf) catch { |
| 304 | global_progress.terminal = null; |
| 305 | }; |
| 396 | 306 | } |
| 397 | 307 | |
| 398 | | test "basic functionality" { |
| 399 | | var disable = true; |
| 400 | | _ = &disable; |
| 401 | | if (disable) { |
| 402 | | // This test is disabled because it uses time.sleep() and is therefore slow. It also |
| 403 | | // prints bogus progress data to stderr. |
| 404 | | return error.SkipZigTest; |
| 405 | | } |
| 406 | | var progress = Progress{}; |
| 407 | | const root_node = progress.start("", 100); |
| 408 | | defer root_node.end(); |
| 308 | fn maybeUpdateSize(resize_flag: bool) void { |
| 309 | if (!resize_flag) return; |
| 409 | 310 | |
| 410 | | const speed_factor = std.time.ns_per_ms; |
| 411 | | |
| 412 | | const sub_task_names = [_][]const u8{ |
| 413 | | "reticulating splines", |
| 414 | | "adjusting shoes", |
| 415 | | "climbing towers", |
| 416 | | "pouring juice", |
| 311 | var winsize: posix.winsize = .{ |
| 312 | .ws_row = 0, |
| 313 | .ws_col = 0, |
| 314 | .ws_xpixel = 0, |
| 315 | .ws_ypixel = 0, |
| 417 | 316 | }; |
| 418 | | var next_sub_task: usize = 0; |
| 419 | 317 | |
| 420 | | var i: usize = 0; |
| 421 | | while (i < 100) : (i += 1) { |
| 422 | | var node = root_node.start(sub_task_names[next_sub_task], 5); |
| 423 | | node.activate(); |
| 424 | | next_sub_task = (next_sub_task + 1) % sub_task_names.len; |
| 425 | | |
| 426 | | node.completeOne(); |
| 427 | | std.time.sleep(5 * speed_factor); |
| 428 | | node.completeOne(); |
| 429 | | node.completeOne(); |
| 430 | | std.time.sleep(5 * speed_factor); |
| 431 | | node.completeOne(); |
| 432 | | node.completeOne(); |
| 433 | | std.time.sleep(5 * speed_factor); |
| 434 | | |
| 435 | | node.end(); |
| 436 | | |
| 437 | | std.time.sleep(5 * speed_factor); |
| 438 | | } |
| 439 | | { |
| 440 | | 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); |
| 441 | | node.activate(); |
| 442 | | std.time.sleep(10 * speed_factor); |
| 443 | | progress.refresh(); |
| 444 | | std.time.sleep(10 * speed_factor); |
| 445 | | node.end(); |
| 318 | const fd = (global_progress.terminal orelse return).handle; |
| 319 | |
| 320 | const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); |
| 321 | if (posix.errno(err) == .SUCCESS) { |
| 322 | global_progress.rows = winsize.ws_row; |
| 323 | global_progress.cols = winsize.ws_col; |
| 324 | } else { |
| 325 | @panic("TODO: handle this failure"); |
| 446 | 326 | } |
| 447 | 327 | } |
| 328 | |
| 329 | fn handleSigWinch(sig: i32, info: *const posix.siginfo_t, ctx_ptr: ?*anyopaque) callconv(.C) void { |
| 330 | _ = info; |
| 331 | _ = ctx_ptr; |
| 332 | assert(sig == posix.SIG.WINCH); |
| 333 | global_progress.redraw_event.set(); |
| 334 | } |