authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-02 18:27:53-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-27 20:56:48-07:00
logd6e8ba3f97b778676bdb3c79b37afc8003b883ea
treeb71996f10a7e4e708e7479ebd4c3cef739d37c53
parent759c2211c2eba44cccf0608267bf1a05934ad8a1

start reworking std.Progress

New design ideas: * One global instance, don't try to play nicely with other instances except via IPC. * One process owns the terminal and the other processes communicate via IPC. * Clear the whole terminal and use multiple lines. What's implemented so far: * Query the terminal for size. * Register a SIGWINCH handler. * Use a thread for redraws. To be done: * IPC * Handling single threaded targets * Porting to Windows * More intelligent display of the progress tree rather than only using one line.

1 files changed, 220 insertions(+), 333 deletions(-)

lib/std/Progress.zig+220-333
...@@ -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 storage3//! 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`
85
9const std = @import("std");6const std = @import("std");
10const builtin = @import("builtin");7const builtin = @import("builtin");
...@@ -12,63 +9,64 @@ const windows = std.os.windows;...@@ -12,63 +9,64 @@ const windows = std.os.windows;
12const testing = std.testing;9const testing = std.testing;
13const assert = std.debug.assert;10const assert = std.debug.assert;
14const Progress = @This();11const Progress = @This();
12const posix = std.posix;
1513
16/// `null` if the current node (and its children) should14/// `null` if the current node (and its children) should
17/// not print on update()15/// not print on update()
18terminal: ?std.fs.File = undefined,16terminal: ?std.fs.File,
1917
20/// Is this a windows API terminal (note: this is not the same as being run on windows18/// 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)
22is_windows_terminal: bool = false,20is_windows_terminal: bool,
2321
24/// Whether the terminal supports ANSI escape codes.22/// Whether the terminal supports ANSI escape codes.
25supports_ansi_escape_codes: bool = false,23supports_ansi_escape_codes: bool,
2624
27/// If the terminal is "dumb", don't print output.25root: Node,
28/// This can be useful if you don't want to print all26
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 output28mutex: std.Thread.Mutex,
31/// for example showing the user what tests run.29update_thread: ?std.Thread,
32dont_print_on_dumb: bool = false,30
3331/// Atomically set by SIGWINCH as well as the root done() function.
34root: Node = undefined,32redraw_event: std.Thread.ResetEvent,
3533/// Ensure there is only 1 global Progress object.
36/// Keeps track of how much time has passed since the beginning.34initialized: bool,
37/// Used to compare with `initial_delay_ms` and `refresh_rate_ms`.35/// Indicates a request to shut down and reset global state.
38timer: ?std.time.Timer = null,36done: bool,
3937
40/// When the previous refresh was written to the terminal.38refresh_rate_ns: u64,
41/// Used to compare with `refresh_rate_ms`.39initial_delay_ns: u64,
42prev_refresh_timestamp: u64 = undefined,40
4341rows: u16,
44/// This buffer represents the maximum number of bytes written to the terminal42cols: u16,
45/// with each refresh.43
46output_buffer: [100]u8 = undefined,44/// Accessed only by the update thread.
4745draw_buffer: []u8,
48/// How many nanoseconds between writing updates to the terminal.46
49refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,47pub const Options = struct {
5048 /// User-provided buffer with static lifetime.
51/// How many nanoseconds to keep the output hidden49 ///
52initial_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
5351 /// cannot fit into this buffer which will look bad but not cause any malfunctions.
54done: bool = true,52 ///
5553 /// 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` memory55 /// 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,
59update_mutex: std.Thread.Mutex = .{},57 /// How many nanoseconds to keep the output hidden
6058 initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
61/// Keeps track of how many columns in the terminal have been output, so that59 /// If provided, causes the progress item to have a denominator.
62/// we can move the cursor back later.60 /// 0 means unknown.
63columns_written: usize = undefined,61 estimated_total_items: usize = 0,
62 root_name: []const u8 = "",
63};
6464
65/// Represents one unit of progress. Each node can have children nodes, or65/// 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`.
67pub const Node = struct {67pub 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,
7876
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 this82 ///
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 {
9492
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 }
10398
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 }
121113
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;
129121 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 }
159124
...@@ -168,280 +133,202 @@ pub const Node = struct {...@@ -168,280 +133,202 @@ pub const Node = struct {
168 }133 }
169};134};
170135
171/// Create a new progress node.136var 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 this158pub 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.
176pub 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 file168 // 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 };
202194
203/// Updates the terminal if enough time has passed since last update. Thread-safe.195 if (global_progress.terminal != null) {
204pub 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}
211205
212fn 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();207fn 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's209 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}
221216
222/// Updates the terminal and resets `self.next_refresh_timestamp`. Thread-safe.217fn updateThreadRun() void {
223pub 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);
226221
227 return self.refreshWithHeldLock();222 const buffer = b: {
228}223 global_progress.mutex.lock();
224 defer global_progress.mutex.unlock();
229225
230fn 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 }
249227
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 }
254232
255 if (cursor_pos.X < 0)233 while (true) {
256 cursor_pos.X = 0;234 const resize_flag = wait(global_progress.refresh_rate_ns);
257235 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 }
293236
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}
298248
299fn refreshWithHeldLock(self: *Progress) void {249const start_sync = "\x1b[?2026h";
300 const is_dumb = !self.supports_ansi_escape_codes and !self.is_windows_terminal;250const clear = "\x1b[J";
301 if (is_dumb and self.dont_print_on_dumb) return;251const save = "\x1b7";
252const restore = "\x1b8";
253const finish_sync = "\x1b[?2026l";
254
255fn clearTerminal() void {
256 write(clear);
257}
258
259fn 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;
302271
303 const file = self.terminal orelse return;272 // Walk the tree and write the progress output to the buffer.
304273
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);
307278
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 }
340289
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}
349292
350pub 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}
362294
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;
365pub 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}
377297
378pub fn unlock_stderr(p: *Progress) void {298 return buf[0..i];
379 std.debug.getStderrMutex().unlock();
380 p.update_mutex.unlock();
381}299}
382300
383fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void {301fn 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}
397307
398test "basic functionality" {308fn 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();
409310
410 const speed_factor = std.time.ns_per_ms;311 var winsize: posix.winsize = .{
411312 .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;
419317
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;
425323 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
329fn 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}