| ... | @@ -1,10 +1,7 @@ | ... | @@ -1,10 +1,7 @@ |
| 1 | //! This API is non-allocating, non-fallible, and thread-safe. | 1 | //! This API is non-allocating, non-fallible, and thread-safe. |
| | 2 | //! |
| 2 | //! The tradeoff is that users of this API must provide the storage | 3 | //! The tradeoff is that users of this API must provide the storage |
| 3 | //! for each `Progress.Node`. | 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 | const std = @import("std"); | 6 | const std = @import("std"); |
| 10 | const builtin = @import("builtin"); | 7 | const builtin = @import("builtin"); |
| ... | @@ -12,63 +9,64 @@ const windows = std.os.windows; | ... | @@ -12,63 +9,64 @@ const windows = std.os.windows; |
| 12 | const testing = std.testing; | 9 | const testing = std.testing; |
| 13 | const assert = std.debug.assert; | 10 | const assert = std.debug.assert; |
| 14 | const Progress = @This(); | 11 | const Progress = @This(); |
| | 12 | const posix = std.posix; |
| 15 | | 13 | |
| 16 | /// `null` if the current node (and its children) should | 14 | /// `null` if the current node (and its children) should |
| 17 | /// not print on update() | 15 | /// not print on update() |
| 18 | terminal: ?std.fs.File = undefined, | 16 | terminal: ?std.fs.File, |
| 19 | | 17 | |
| 20 | /// Is this a windows API terminal (note: this is not the same as being run on windows | 18 | /// Is this a windows API terminal (note: this is not the same as being run on windows |
| 21 | /// because other terminals exist like MSYS/git-bash) | 19 | /// because other terminals exist like MSYS/git-bash) |
| 22 | is_windows_terminal: bool = false, | 20 | is_windows_terminal: bool, |
| 23 | | 21 | |
| 24 | /// Whether the terminal supports ANSI escape codes. | 22 | /// Whether the terminal supports ANSI escape codes. |
| 25 | supports_ansi_escape_codes: bool = false, | 23 | supports_ansi_escape_codes: bool, |
| 26 | | 24 | |
| 27 | /// If the terminal is "dumb", don't print output. | 25 | root: Node, |
| 28 | /// This can be useful if you don't want to print all | 26 | |
| 29 | /// the stages of code generation if there are a lot. | 27 | /// Protects all the state shared between the update thread and the public API calls. |
| 30 | /// You should not use it if the user should see output | 28 | mutex: std.Thread.Mutex, |
| 31 | /// for example showing the user what tests run. | 29 | update_thread: ?std.Thread, |
| 32 | dont_print_on_dumb: bool = false, | 30 | |
| 33 | | 31 | /// Atomically set by SIGWINCH as well as the root done() function. |
| 34 | root: Node = undefined, | 32 | redraw_event: std.Thread.ResetEvent, |
| 35 | | 33 | /// Ensure there is only 1 global Progress object. |
| 36 | /// Keeps track of how much time has passed since the beginning. | 34 | initialized: bool, |
| 37 | /// Used to compare with `initial_delay_ms` and `refresh_rate_ms`. | 35 | /// Indicates a request to shut down and reset global state. |
| 38 | timer: ?std.time.Timer = null, | 36 | done: bool, |
| 39 | | 37 | |
| 40 | /// When the previous refresh was written to the terminal. | 38 | refresh_rate_ns: u64, |
| 41 | /// Used to compare with `refresh_rate_ms`. | 39 | initial_delay_ns: u64, |
| 42 | prev_refresh_timestamp: u64 = undefined, | 40 | |
| 43 | | 41 | rows: u16, |
| 44 | /// This buffer represents the maximum number of bytes written to the terminal | 42 | cols: u16, |
| 45 | /// with each refresh. | 43 | |
| 46 | output_buffer: [100]u8 = undefined, | 44 | /// Accessed only by the update thread. |
| 47 | | 45 | draw_buffer: []u8, |
| 48 | /// How many nanoseconds between writing updates to the terminal. | 46 | |
| 49 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, | 47 | pub const Options = struct { |
| 50 | | 48 | /// User-provided buffer with static lifetime. |
| 51 | /// How many nanoseconds to keep the output hidden | 49 | /// |
| 52 | initial_delay_ns: u64 = 500 * std.time.ns_per_ms, | 50 | /// Used to store the entire write buffer sent to the terminal. Progress output will be truncated if it |
| 53 | | 51 | /// cannot fit into this buffer which will look bad but not cause any malfunctions. |
| 54 | done: bool = true, | 52 | /// |
| 55 | | 53 | /// Must be at least 100 bytes. |
| 56 | /// Protects the `refresh` function, as well as `node.recently_updated_child`. | 54 | draw_buffer: []u8, |
| 57 | /// Without this, callsites would call `Node.end` and then free `Node` memory | 55 | /// How many nanoseconds between writing updates to the terminal. |
| 58 | /// while it was still being accessed by the `refresh` function. | 56 | refresh_rate_ns: u64 = 50 * std.time.ns_per_ms, |
| 59 | update_mutex: std.Thread.Mutex = .{}, | 57 | /// How many nanoseconds to keep the output hidden |
| 60 | | 58 | initial_delay_ns: u64 = 500 * std.time.ns_per_ms, |
| 61 | /// Keeps track of how many columns in the terminal have been output, so that | 59 | /// If provided, causes the progress item to have a denominator. |
| 62 | /// we can move the cursor back later. | 60 | /// 0 means unknown. |
| 63 | columns_written: usize = undefined, | 61 | estimated_total_items: usize = 0, |
| | 62 | root_name: []const u8 = "", |
| | 63 | }; |
| 64 | | 64 | |
| 65 | /// Represents one unit of progress. Each node can have children nodes, or | 65 | /// Represents one unit of progress. Each node can have children nodes, or |
| 66 | /// one can use integers with `update`. | 66 | /// one can use integers with `update`. |
| 67 | pub const Node = struct { | 67 | pub const Node = struct { |
| 68 | context: *Progress, | | |
| 69 | parent: ?*Node, | 68 | parent: ?*Node, |
| 70 | name: []const u8, | 69 | name: []const u8, |
| 71 | unit: []const u8 = "", | | |
| 72 | /// Must be handled atomically to be thread-safe. | 70 | /// Must be handled atomically to be thread-safe. |
| 73 | recently_updated_child: ?*Node = null, | 71 | recently_updated_child: ?*Node = null, |
| 74 | /// Must be handled atomically to be thread-safe. 0 means null. | 72 | /// Must be handled atomically to be thread-safe. 0 means null. |
| ... | @@ -76,15 +74,15 @@ pub const Node = struct { | ... | @@ -76,15 +74,15 @@ pub const Node = struct { |
| 76 | /// Must be handled atomically to be thread-safe. | 74 | /// Must be handled atomically to be thread-safe. |
| 77 | unprotected_completed_items: usize, | 75 | unprotected_completed_items: usize, |
| 78 | | 76 | |
| | 77 | pub const ListNode = std.DoublyLinkedList(void); |
| | 78 | |
| 79 | /// Create a new child progress node. Thread-safe. | 79 | /// Create a new child progress node. Thread-safe. |
| | 80 | /// |
| 80 | /// Call `Node.end` when done. | 81 | /// Call `Node.end` when done. |
| 81 | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this | 82 | /// |
| 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. | | |
| 84 | /// Passing 0 for `estimated_total_items` means unknown. | 83 | /// Passing 0 for `estimated_total_items` means unknown. |
| 85 | pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node { | 84 | pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node { |
| 86 | return Node{ | 85 | return .{ |
| 87 | .context = self.context, | | |
| 88 | .parent = self, | 86 | .parent = self, |
| 89 | .name = name, | 87 | .name = name, |
| 90 | .unprotected_estimated_total_items = estimated_total_items, | 88 | .unprotected_estimated_total_items = estimated_total_items, |
| ... | @@ -94,66 +92,33 @@ pub const Node = struct { | ... | @@ -94,66 +92,33 @@ pub const Node = struct { |
| 94 | | 92 | |
| 95 | /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe. | 93 | /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe. |
| 96 | pub fn completeOne(self: *Node) void { | 94 | pub fn completeOne(self: *Node) void { |
| 97 | if (self.parent) |parent| { | | |
| 98 | @atomicStore(?*Node, &parent.recently_updated_child, self, .release); | | |
| 99 | } | | |
| 100 | _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .monotonic); | 95 | _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .monotonic); |
| 101 | self.context.maybeRefresh(); | 96 | self.activate(); |
| 102 | } | 97 | } |
| 103 | | 98 | |
| 104 | /// Finish a started `Node`. Thread-safe. | 99 | /// Finish a started `Node`. Thread-safe. |
| 105 | pub fn end(self: *Node) void { | 100 | pub fn end(self: *Node) void { |
| 106 | self.context.maybeRefresh(); | | |
| 107 | if (self.parent) |parent| { | 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 | parent.completeOne(); | 102 | parent.completeOne(); |
| 114 | } else { | 103 | } else { |
| 115 | self.context.update_mutex.lock(); | 104 | { |
| 116 | defer self.context.update_mutex.unlock(); | 105 | global_progress.mutex.lock(); |
| 117 | self.context.done = true; | 106 | defer global_progress.mutex.unlock(); |
| 118 | self.context.refreshWithHeldLock(); | 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 | /// Tell the parent node that this node is actively being worked on. Thread-safe. | 114 | /// Tell the parent node that this node is actively being worked on. Thread-safe. |
| 123 | pub fn activate(self: *Node) void { | 115 | pub fn activate(self: *Node) void { |
| 124 | if (self.parent) |parent| { | 116 | var parent = self.parent; |
| 125 | @atomicStore(?*Node, &parent.recently_updated_child, self, .release); | 117 | var child = self; |
| 126 | self.context.maybeRefresh(); | 118 | while (parent) |p| { |
| 127 | } | 119 | @atomicStore(?*Node, &p.recently_updated_child, child, .release); |
| 128 | } | 120 | child = p; |
| 129 | | 121 | parent = p.parent; |
| 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); | | |
| 157 | } | 122 | } |
| 158 | } | 123 | } |
| 159 | | 124 | |
| ... | @@ -168,280 +133,202 @@ pub const Node = struct { | ... | @@ -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 | /// Call `Node.end` when done. | 157 | /// Call `Node.end` when done. |
| 173 | /// TODO solve https://github.com/ziglang/zig/issues/2765 and then change this | 158 | pub fn start(options: Options) *Node { |
| 174 | /// API to return Progress rather than accept it as a parameter. | 159 | assert(!global_progress.initialized); |
| 175 | /// `estimated_total_items` value of 0 means unknown. | | |
| 176 | pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node { | | |
| 177 | const stderr = std.io.getStdErr(); | 160 | const stderr = std.io.getStdErr(); |
| 178 | self.terminal = null; | | |
| 179 | if (stderr.supportsAnsiEscapeCodes()) { | 161 | if (stderr.supportsAnsiEscapeCodes()) { |
| 180 | self.terminal = stderr; | 162 | global_progress.terminal = stderr; |
| 181 | self.supports_ansi_escape_codes = true; | 163 | global_progress.supports_ansi_escape_codes = true; |
| 182 | } else if (builtin.os.tag == .windows and stderr.isTty()) { | 164 | } else if (builtin.os.tag == .windows and stderr.isTty()) { |
| 183 | self.is_windows_terminal = true; | 165 | global_progress.is_windows_terminal = true; |
| 184 | self.terminal = stderr; | 166 | global_progress.terminal = stderr; |
| 185 | } else if (builtin.os.tag != .windows) { | 167 | } else if (builtin.os.tag != .windows) { |
| 186 | // we are in a "dumb" terminal like in acme or writing to a file | 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{ | 171 | global_progress.root = .{ |
| 190 | .context = self, | | |
| 191 | .parent = null, | 172 | .parent = null, |
| 192 | .name = name, | 173 | .name = options.root_name, |
| 193 | .unprotected_estimated_total_items = estimated_total_items, | 174 | .unprotected_estimated_total_items = options.estimated_total_items, |
| 194 | .unprotected_completed_items = 0, | 175 | .unprotected_completed_items = 0, |
| 195 | }; | 176 | }; |
| 196 | self.columns_written = 0; | 177 | global_progress.done = false; |
| 197 | self.prev_refresh_timestamp = 0; | 178 | global_progress.initialized = true; |
| 198 | self.timer = std.time.Timer.start() catch null; | 179 | |
| 199 | self.done = false; | 180 | assert(options.draw_buffer.len >= 100); |
| 200 | return &self.root; | 181 | global_progress.draw_buffer = options.draw_buffer; |
| 201 | } | 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. | 195 | if (global_progress.terminal != null) { |
| 204 | pub fn maybeRefresh(self: *Progress) void { | 196 | if (std.Thread.spawn(.{}, updateThreadRun, .{})) |thread| { |
| 205 | if (self.timer) |*timer| { | 197 | global_progress.update_thread = thread; |
| 206 | if (!self.update_mutex.tryLock()) return; | 198 | } else |_| { |
| 207 | defer self.update_mutex.unlock(); | 199 | global_progress.terminal = null; |
| 208 | maybeRefreshWithHeldLock(self, timer); | 200 | } |
| 209 | } | 201 | } |
| | 202 | |
| | 203 | return &global_progress.root; |
| 210 | } | 204 | } |
| 211 | | 205 | |
| 212 | fn maybeRefreshWithHeldLock(self: *Progress, timer: *std.time.Timer) void { | 206 | /// Returns whether a resize is needed to learn the terminal size. |
| 213 | const now = timer.read(); | 207 | fn wait(timeout_ns: u64) bool { |
| 214 | if (now < self.initial_delay_ns) return; | 208 | const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_| |
| 215 | // TODO I have observed this to happen sometimes. I think we need to follow Rust's | 209 | true |
| 216 | // lead and guarantee monotonically increasing times in the std lib itself. | 210 | else |err| switch (err) { |
| 217 | if (now < self.prev_refresh_timestamp) return; | 211 | error.Timeout => false, |
| 218 | if (now - self.prev_refresh_timestamp < self.refresh_rate_ns) return; | 212 | }; |
| 219 | return self.refreshWithHeldLock(); | 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. | 217 | fn updateThreadRun() void { |
| 223 | pub fn refresh(self: *Progress) void { | 218 | { |
| 224 | if (!self.update_mutex.tryLock()) return; | 219 | const resize_flag = wait(global_progress.initial_delay_ns); |
| 225 | defer self.update_mutex.unlock(); | 220 | maybeUpdateSize(resize_flag); |
| 226 | | 221 | |
| 227 | return self.refreshWithHeldLock(); | 222 | const buffer = b: { |
| 228 | } | 223 | global_progress.mutex.lock(); |
| | 224 | defer global_progress.mutex.unlock(); |
| 229 | | 225 | |
| 230 | fn clearWithHeldLock(p: *Progress, end_ptr: *usize) void { | 226 | if (global_progress.done) return clearTerminal(); |
| 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 | } | | |
| 249 | | 227 | |
| 250 | var cursor_pos = windows.COORD{ | 228 | break :b computeRedraw(); |
| 251 | .X = info.dwCursorPosition.X - @as(windows.SHORT, @intCast(p.columns_written)), | 229 | }; |
| 252 | .Y = info.dwCursorPosition.Y, | 230 | write(buffer); |
| 253 | }; | 231 | } |
| 254 | | 232 | |
| 255 | if (cursor_pos.X < 0) | 233 | while (true) { |
| 256 | cursor_pos.X = 0; | 234 | const resize_flag = wait(global_progress.refresh_rate_ns); |
| 257 | | 235 | maybeUpdateSize(resize_flag); |
| 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 | } | | |
| 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 { | 249 | const start_sync = "\x1b[?2026h"; |
| 300 | const is_dumb = !self.supports_ansi_escape_codes and !self.is_windows_terminal; | 250 | const clear = "\x1b[J"; |
| 301 | if (is_dumb and self.dont_print_on_dumb) return; | 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; | 274 | var node: *Node = &global_progress.root; |
| 306 | clearWithHeldLock(self, &end); | 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) { | 279 | if (node.name.len != 0 or eti > 0) { |
| 309 | var need_ellipse = false; | 280 | if (node.name.len != 0) { |
| 310 | var maybe_node: ?*Node = &self.root; | 281 | i += (std.fmt.bufPrint(buf[i..], "{s}", .{node.name}) catch @panic("TODO")).len; |
| 311 | while (maybe_node) |node| { | | |
| 312 | if (need_ellipse) { | | |
| 313 | self.bufWrite(&end, "... ", .{}); | | |
| 314 | } | 282 | } |
| 315 | need_ellipse = false; | 283 | if (eti > 0) { |
| 316 | const eti = @atomicLoad(usize, &node.unprotected_estimated_total_items, .monotonic); | 284 | i += (std.fmt.bufPrint(buf[i..], "[{d}/{d}] ", .{ completed_items, eti }) catch @panic("TODO")).len; |
| 317 | const completed_items = @atomicLoad(usize, &node.unprotected_completed_items, .monotonic); | 285 | } else if (completed_items != 0) { |
| 318 | const current_item = completed_items + 1; | 286 | i += (std.fmt.bufPrint(buf[i..], "[{d}] ", .{completed_items}) catch @panic("TODO")).len; |
| 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 | } | | |
| 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 { | 290 | node = @atomicLoad(?*Node, &node.recently_updated_child, .acquire) orelse break; |
| 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(); | | |
| 347 | } | 291 | } |
| 348 | } | | |
| 349 | | 292 | |
| 350 | pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void { | 293 | i = @min(global_progress.cols + prefix.len, i); |
| 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 | } | | |
| 362 | | 294 | |
| 363 | /// Allows the caller to freely write to stderr until unlock_stderr() is called. | 295 | buf[i..][0..suffix.len].* = suffix.*; |
| 364 | /// During the lock, the progress information is cleared from the terminal. | 296 | i += suffix.len; |
| 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 | } | | |
| 377 | | 297 | |
| 378 | pub fn unlock_stderr(p: *Progress) void { | 298 | return buf[0..i]; |
| 379 | std.debug.getStderrMutex().unlock(); | | |
| 380 | p.update_mutex.unlock(); | | |
| 381 | } | 299 | } |
| 382 | | 300 | |
| 383 | fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void { | 301 | fn write(buf: []const u8) void { |
| 384 | if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| { | 302 | const tty = global_progress.terminal orelse return; |
| 385 | const amt = written.len; | 303 | tty.writeAll(buf) catch { |
| 386 | end.* += amt; | 304 | global_progress.terminal = null; |
| 387 | self.columns_written += amt; | 305 | }; |
| 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 | } | | |
| 396 | } | 306 | } |
| 397 | | 307 | |
| 398 | test "basic functionality" { | 308 | fn maybeUpdateSize(resize_flag: bool) void { |
| 399 | var disable = true; | 309 | if (!resize_flag) return; |
| 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(); | | |
| 409 | | 310 | |
| 410 | const speed_factor = std.time.ns_per_ms; | 311 | var winsize: posix.winsize = .{ |
| 411 | | 312 | .ws_row = 0, |
| 412 | const sub_task_names = [_][]const u8{ | 313 | .ws_col = 0, |
| 413 | "reticulating splines", | 314 | .ws_xpixel = 0, |
| 414 | "adjusting shoes", | 315 | .ws_ypixel = 0, |
| 415 | "climbing towers", | | |
| 416 | "pouring juice", | | |
| 417 | }; | 316 | }; |
| 418 | var next_sub_task: usize = 0; | | |
| 419 | | 317 | |
| 420 | var i: usize = 0; | 318 | const fd = (global_progress.terminal orelse return).handle; |
| 421 | while (i < 100) : (i += 1) { | 319 | |
| 422 | var node = root_node.start(sub_task_names[next_sub_task], 5); | 320 | const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); |
| 423 | node.activate(); | 321 | if (posix.errno(err) == .SUCCESS) { |
| 424 | next_sub_task = (next_sub_task + 1) % sub_task_names.len; | 322 | global_progress.rows = winsize.ws_row; |
| 425 | | 323 | global_progress.cols = winsize.ws_col; |
| 426 | node.completeOne(); | 324 | } else { |
| 427 | std.time.sleep(5 * speed_factor); | 325 | @panic("TODO: handle this failure"); |
| 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(); | | |
| 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 | } |