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 @@
11//! This API is non-allocating, non-fallible, and thread-safe.
2//!
23//! The tradeoff is that users of this API must provide the storage
34//! for each `Progress.Node`.
4//!
5//! Initialize the struct directly, overriding these fields as desired:
6//! * `refresh_rate_ms`
7//! * `initial_delay_ms`
85
96const std = @import("std");
107const builtin = @import("builtin");
......@@ -12,63 +9,64 @@ const windows = std.os.windows;
129const testing = std.testing;
1310const assert = std.debug.assert;
1411const Progress = @This();
12const posix = std.posix;
1513
1614/// `null` if the current node (and its children) should
1715/// not print on update()
18terminal: ?std.fs.File = undefined,
16terminal: ?std.fs.File,
1917
2018/// Is this a windows API terminal (note: this is not the same as being run on windows
2119/// because other terminals exist like MSYS/git-bash)
22is_windows_terminal: bool = false,
20is_windows_terminal: bool,
2321
2422/// Whether the terminal supports ANSI escape codes.
25supports_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.
32dont_print_on_dumb: bool = false,
33
34root: 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`.
38timer: ?std.time.Timer = null,
39
40/// When the previous refresh was written to the terminal.
41/// Used to compare with `refresh_rate_ms`.
42prev_refresh_timestamp: u64 = undefined,
43
44/// This buffer represents the maximum number of bytes written to the terminal
45/// with each refresh.
46output_buffer: [100]u8 = undefined,
47
48/// How many nanoseconds between writing updates to the terminal.
49refresh_rate_ns: u64 = 50 * std.time.ns_per_ms,
50
51/// How many nanoseconds to keep the output hidden
52initial_delay_ns: u64 = 500 * std.time.ns_per_ms,
53
54done: 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.
59update_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.
63columns_written: usize = undefined,
23supports_ansi_escape_codes: bool,
24
25root: Node,
26
27/// Protects all the state shared between the update thread and the public API calls.
28mutex: std.Thread.Mutex,
29update_thread: ?std.Thread,
30
31/// Atomically set by SIGWINCH as well as the root done() function.
32redraw_event: std.Thread.ResetEvent,
33/// Ensure there is only 1 global Progress object.
34initialized: bool,
35/// Indicates a request to shut down and reset global state.
36done: bool,
37
38refresh_rate_ns: u64,
39initial_delay_ns: u64,
40
41rows: u16,
42cols: u16,
43
44/// Accessed only by the update thread.
45draw_buffer: []u8,
46
47pub 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};
6464
6565/// Represents one unit of progress. Each node can have children nodes, or
6666/// one can use integers with `update`.
6767pub const Node = struct {
68 context: *Progress,
6968 parent: ?*Node,
7069 name: []const u8,
71 unit: []const u8 = "",
7270 /// Must be handled atomically to be thread-safe.
7371 recently_updated_child: ?*Node = null,
7472 /// Must be handled atomically to be thread-safe. 0 means null.
......@@ -76,15 +74,15 @@ pub const Node = struct {
7674 /// Must be handled atomically to be thread-safe.
7775 unprotected_completed_items: usize,
7876
77 pub const ListNode = std.DoublyLinkedList(void);
78
7979 /// Create a new child progress node. Thread-safe.
80 ///
8081 /// 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 ///
8483 /// Passing 0 for `estimated_total_items` means unknown.
8584 pub fn start(self: *Node, name: []const u8, estimated_total_items: usize) Node {
86 return Node{
87 .context = self.context,
85 return .{
8886 .parent = self,
8987 .name = name,
9088 .unprotected_estimated_total_items = estimated_total_items,
......@@ -94,66 +92,33 @@ pub const Node = struct {
9492
9593 /// This is the same as calling `start` and then `end` on the returned `Node`. Thread-safe.
9694 pub fn completeOne(self: *Node) void {
97 if (self.parent) |parent| {
98 @atomicStore(?*Node, &parent.recently_updated_child, self, .release);
99 }
10095 _ = @atomicRmw(usize, &self.unprotected_completed_items, .Add, 1, .monotonic);
101 self.context.maybeRefresh();
96 self.activate();
10297 }
10398
10499 /// Finish a started `Node`. Thread-safe.
105100 pub fn end(self: *Node) void {
106 self.context.maybeRefresh();
107101 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 }
113102 parent.completeOne();
114103 } 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();
119111 }
120112 }
121113
122114 /// Tell the parent node that this node is actively being worked on. Thread-safe.
123115 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;
157122 }
158123 }
159124
......@@ -168,280 +133,202 @@ pub const Node = struct {
168133 }
169134};
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///
172157/// 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.
176pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) *Node {
158pub fn start(options: Options) *Node {
159 assert(!global_progress.initialized);
177160 const stderr = std.io.getStdErr();
178 self.terminal = null;
179161 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;
182164 } 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;
185167 } else if (builtin.os.tag != .windows) {
186168 // we are in a "dumb" terminal like in acme or writing to a file
187 self.terminal = stderr;
169 global_progress.terminal = stderr;
188170 }
189 self.root = Node{
190 .context = self,
171 global_progress.root = .{
191172 .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,
194175 .unprotected_completed_items = 0,
195176 };
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 };
202194
203/// Updates the terminal if enough time has passed since last update. Thread-safe.
204pub 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 }
209201 }
202
203 return &global_progress.root;
210204}
211205
212fn 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.
207fn 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);
220215}
221216
222/// Updates the terminal and resets `self.next_refresh_timestamp`. Thread-safe.
223pub fn refresh(self: *Progress) void {
224 if (!self.update_mutex.tryLock()) return;
225 defer self.update_mutex.unlock();
217fn updateThreadRun() void {
218 {
219 const resize_flag = wait(global_progress.initial_delay_ns);
220 maybeUpdateSize(resize_flag);
226221
227 return self.refreshWithHeldLock();
228}
222 const buffer = b: {
223 global_progress.mutex.lock();
224 defer global_progress.mutex.unlock();
229225
230fn 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();
249227
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);
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);
295246 }
296 end_ptr.* = end;
297247}
298248
299fn 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;
249const start_sync = "\x1b[?2026h";
250const clear = "\x1b[J";
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;
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);
307278
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;
314282 }
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;
333287 }
334 maybe_node = @atomicLoad(?*Node, &node.recently_updated_child, .acquire);
335288 }
336 if (need_ellipse) {
337 self.bufWrite(&end, "... ", .{});
338 }
339 }
340289
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;
347291 }
348}
349292
350pub 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);
362294
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.
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}
295 buf[i..][0..suffix.len].* = suffix.*;
296 i += suffix.len;
377297
378pub fn unlock_stderr(p: *Progress) void {
379 std.debug.getStderrMutex().unlock();
380 p.update_mutex.unlock();
298 return buf[0..i];
381299}
382300
383fn 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 }
301fn write(buf: []const u8) void {
302 const tty = global_progress.terminal orelse return;
303 tty.writeAll(buf) catch {
304 global_progress.terminal = null;
305 };
396306}
397307
398test "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();
308fn maybeUpdateSize(resize_flag: bool) void {
309 if (!resize_flag) return;
409310
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,
417316 };
418 var next_sub_task: usize = 0;
419317
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");
446326 }
447327}
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}