authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-31 11:54:44-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-31 19:00:14-04:00
loge09963d8544c19812db20e520f09f6e5d9a57d64
tree8c6281bf5f0b9b04a6dbfb2c368d2a6497015745
parent4918c2ce2d739ba5e1b207a2ef32afcc04bfc435

std.Progress: keep the cursor at the beginning

This changes the terminal display to keep the cursor at the top left of the progress display, so that unlocked stderr writes, perhaps by child processes, don't get eaten by the clear.

1 files changed, 83 insertions(+), 85 deletions(-)

lib/std/Progress.zig+83-85
......@@ -23,17 +23,13 @@ redraw_event: std.Thread.ResetEvent,
2323/// Indicates a request to shut down and reset global state.
2424/// Accessed atomically.
2525done: bool,
26need_clear: bool,
2627
2728refresh_rate_ns: u64,
2829initial_delay_ns: u64,
2930
3031rows: u16,
3132cols: u16,
32/// Tracks the number of newlines that have been actually written to the terminal.
33written_newline_count: u16,
34/// Tracks the number of newlines that will be written to the terminal if the
35/// draw buffer is sent.
36accumulated_newline_count: u16,
3733
3834/// Accessed only by the update thread.
3935draw_buffer: []u8,
......@@ -312,10 +308,9 @@ var global_progress: Progress = .{
312308 .initial_delay_ns = undefined,
313309 .rows = 0,
314310 .cols = 0,
315 .written_newline_count = 0,
316 .accumulated_newline_count = 0,
317311 .draw_buffer = undefined,
318312 .done = false,
313 .need_clear = false,
319314
320315 .node_parents = &node_parents_buffer,
321316 .node_storage = &node_storage_buffer,
......@@ -446,10 +441,11 @@ fn updateThreadRun() void {
446441 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
447442 maybeUpdateSize(resize_flag);
448443
449 const buffer = computeRedraw(&serialized_buffer);
444 const buffer, _ = computeRedraw(&serialized_buffer);
450445 if (stderr_mutex.tryLock()) {
451446 defer stderr_mutex.unlock();
452447 write(buffer) catch return;
448 global_progress.need_clear = true;
453449 }
454450 }
455451
......@@ -464,10 +460,11 @@ fn updateThreadRun() void {
464460
465461 maybeUpdateSize(resize_flag);
466462
467 const buffer = computeRedraw(&serialized_buffer);
463 const buffer, _ = computeRedraw(&serialized_buffer);
468464 if (stderr_mutex.tryLock()) {
469465 defer stderr_mutex.unlock();
470466 write(buffer) catch return;
467 global_progress.need_clear = true;
471468 }
472469 }
473470}
......@@ -488,11 +485,13 @@ fn windowsApiUpdateThreadRun() void {
488485 if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return;
489486 maybeUpdateSize(resize_flag);
490487
491 const buffer = computeRedraw(&serialized_buffer);
488 const buffer, const nl_n = computeRedraw(&serialized_buffer);
492489 if (stderr_mutex.tryLock()) {
493490 defer stderr_mutex.unlock();
494491 windowsApiWriteMarker();
495492 write(buffer) catch return;
493 global_progress.need_clear = true;
494 windowsApiMoveToMarker(nl_n) catch return;
496495 }
497496 }
498497
......@@ -507,12 +506,14 @@ fn windowsApiUpdateThreadRun() void {
507506
508507 maybeUpdateSize(resize_flag);
509508
510 const buffer = computeRedraw(&serialized_buffer);
509 const buffer, const nl_n = computeRedraw(&serialized_buffer);
511510 if (stderr_mutex.tryLock()) {
512511 defer stderr_mutex.unlock();
513512 clearWrittenWindowsApi() catch return;
514513 windowsApiWriteMarker();
515514 write(buffer) catch return;
515 global_progress.need_clear = true;
516 windowsApiMoveToMarker(nl_n) catch return;
516517 }
517518 }
518519}
......@@ -645,40 +646,16 @@ fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize {
645646}
646647
647648fn clearWrittenWithEscapeCodes() anyerror!void {
648 if (global_progress.written_newline_count == 0) return;
649 if (!global_progress.need_clear) return;
649650
650651 var i: usize = 0;
651652 const buf = global_progress.draw_buffer;
652653
653 buf[i..][0..start_sync.len].* = start_sync.*;
654 i += start_sync.len;
655
656 i = computeClear(buf, i);
657
658 buf[i..][0..finish_sync.len].* = finish_sync.*;
659 i += finish_sync.len;
660
661 global_progress.accumulated_newline_count = 0;
662 try write(buf[0..i]);
663}
664
665fn computeClear(buf: []u8, start_i: usize) usize {
666 var i = start_i;
667
668 const prev_nl_n = global_progress.written_newline_count;
669 if (prev_nl_n > 0) {
670 buf[i] = '\r';
671 i += 1;
672 for (0..prev_nl_n) |_| {
673 buf[i..][0..up_one_line.len].* = up_one_line.*;
674 i += up_one_line.len;
675 }
676 }
677
678654 buf[i..][0..clear.len].* = clear.*;
679655 i += clear.len;
680656
681 return i;
657 global_progress.need_clear = false;
658 try write(buf[0..i]);
682659}
683660
684661/// U+25BA or ►
......@@ -704,38 +681,44 @@ fn clearWrittenWindowsApi() error{Unexpected}!void {
704681 // but it must be a valid attribute and it actually needs to apply to the first
705682 // character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem
706683 // like any of the available attributes are invisible/benign.
707 const prev_nl_n = global_progress.written_newline_count;
708 if (prev_nl_n > 0) {
709 const handle = global_progress.terminal.handle;
710 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
684 if (!global_progress.need_clear) return;
685 const handle = global_progress.terminal.handle;
686 const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows;
711687
712 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
713 if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
714 return error.Unexpected;
715 }
716 const cursor_pos = console_info.dwCursorPosition;
717 const expected_y = cursor_pos.Y - @as(i16, @intCast(prev_nl_n));
718 var start_pos = windows.COORD{ .X = 0, .Y = expected_y };
719 while (start_pos.Y >= 0) {
720 var wchar: [1]u16 = undefined;
721 var num_console_chars_read: windows.DWORD = undefined;
722 if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
723 return error.Unexpected;
724 }
688 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
689 if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
690 return error.Unexpected;
691 }
692 var num_chars_written: windows.DWORD = undefined;
693 if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, console_info.dwCursorPosition, &num_chars_written) == 0) {
694 return error.Unexpected;
695 }
696}
725697
726 if (wchar[0] == windows_api_start_marker) break;
727 start_pos.Y -= 1;
728 } else {
729 // If we couldn't find the marker, then just assume that no lines wrapped
730 start_pos = .{ .X = 0, .Y = expected_y };
731 }
732 var num_chars_written: windows.DWORD = undefined;
733 if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, start_pos, &num_chars_written) == 0) {
734 return error.Unexpected;
735 }
736 if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {
698fn windowsApiMoveToMarker(nl_n: usize) error{Unexpected}!void {
699 const handle = global_progress.terminal.handle;
700 var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
701 if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) {
702 return error.Unexpected;
703 }
704 const cursor_pos = console_info.dwCursorPosition;
705 const expected_y = cursor_pos.Y - @as(i16, @intCast(nl_n));
706 var start_pos: windows.COORD = .{ .X = 0, .Y = expected_y };
707 while (start_pos.Y >= 0) {
708 var wchar: [1]u16 = undefined;
709 var num_console_chars_read: windows.DWORD = undefined;
710 if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) {
737711 return error.Unexpected;
738712 }
713
714 if (wchar[0] == windows_api_start_marker) break;
715 start_pos.Y -= 1;
716 } else {
717 // If we couldn't find the marker, then just assume that no lines wrapped
718 start_pos = .{ .X = 0, .Y = expected_y };
719 }
720 if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) {
721 return error.Unexpected;
739722 }
740723}
741724
......@@ -1052,7 +1035,7 @@ fn useSavedIpcData(
10521035 return start_serialized_len + storage.len;
10531036}
10541037
1055fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
1038fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } {
10561039 const serialized = serialize(serialized_buffer);
10571040
10581041 // Now we can analyze our copy of the graph without atomics, reconstructing
......@@ -1078,8 +1061,10 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
10781061 }
10791062 }
10801063
1081 // The strategy is: keep the cursor at the end, and then with every redraw:
1082 // move cursor to beginning of line, move cursor up N lines, erase to end of screen, write
1064 // The strategy is, with every redraw:
1065 // erase to end of screen, write, move cursor to beginning of line, move cursor up N lines
1066 // This keeps the cursor at the beginning so that unlocked stderr writes
1067 // don't get eaten by the clear.
10831068
10841069 var i: usize = 0;
10851070 const buf = global_progress.draw_buffer;
......@@ -1091,20 +1076,31 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
10911076
10921077 switch (global_progress.terminal_mode) {
10931078 .off => unreachable,
1094 .ansi_escape_codes => i = computeClear(buf, i),
1079 .ansi_escape_codes => {
1080 buf[i..][0..clear.len].* = clear.*;
1081 i += clear.len;
1082 },
10951083 .windows_api => if (!is_windows) unreachable,
10961084 }
10971085
1098 global_progress.accumulated_newline_count = 0;
10991086 const root_node_index: Node.Index = @enumFromInt(0);
1100 i = computeNode(buf, i, serialized, children, root_node_index);
1087 i, const nl_n = computeNode(buf, i, 0, serialized, children, root_node_index);
11011088
11021089 if (global_progress.terminal_mode == .ansi_escape_codes) {
1090 if (nl_n > 0) {
1091 buf[i] = '\r';
1092 i += 1;
1093 for (0..nl_n) |_| {
1094 buf[i..][0..up_one_line.len].* = up_one_line.*;
1095 i += up_one_line.len;
1096 }
1097 }
1098
11031099 buf[i..][0..finish_sync.len].* = finish_sync.*;
11041100 i += finish_sync.len;
11051101 }
11061102
1107 return buf[0..i];
1103 return .{ buf[0..i], nl_n };
11081104}
11091105
11101106fn computePrefix(
......@@ -1138,20 +1134,23 @@ fn computePrefix(
11381134}
11391135
11401136const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) +
1141 "[4294967296/4294967296] ".len + Node.max_name_len + finish_sync.len;
1137 "[4294967296/4294967296] ".len + Node.max_name_len + (1 + up_one_line.len) + finish_sync.len;
11421138
11431139fn computeNode(
11441140 buf: []u8,
11451141 start_i: usize,
1142 start_nl_n: usize,
11461143 serialized: Serialized,
11471144 children: []const Children,
11481145 node_index: Node.Index,
1149) usize {
1146) struct { usize, usize } {
11501147 var i = start_i;
1148 var nl_n = start_nl_n;
1149
11511150 i = computePrefix(buf, i, serialized, children, node_index);
11521151
11531152 if (i + line_upper_bound_len > buf.len)
1154 return start_i;
1153 return .{ start_i, start_nl_n };
11551154
11561155 const storage = &serialized.storage[@intFromEnum(node_index)];
11571156 const estimated_total = storage.estimated_total_count;
......@@ -1186,34 +1185,33 @@ fn computeNode(
11861185 i = @min(global_progress.cols + start_i, i);
11871186 buf[i] = '\n';
11881187 i += 1;
1189 global_progress.accumulated_newline_count += 1;
1188 nl_n += 1;
11901189 }
11911190
1192 if (global_progress.withinRowLimit()) {
1191 if (global_progress.withinRowLimit(nl_n)) {
11931192 if (children[@intFromEnum(node_index)].child.unwrap()) |child| {
1194 i = computeNode(buf, i, serialized, children, child);
1193 i, nl_n = computeNode(buf, i, nl_n, serialized, children, child);
11951194 }
11961195 }
11971196
1198 if (global_progress.withinRowLimit()) {
1197 if (global_progress.withinRowLimit(nl_n)) {
11991198 if (children[@intFromEnum(node_index)].sibling.unwrap()) |sibling| {
1200 i = computeNode(buf, i, serialized, children, sibling);
1199 i, nl_n = computeNode(buf, i, nl_n, serialized, children, sibling);
12011200 }
12021201 }
12031202
1204 return i;
1203 return .{ i, nl_n };
12051204}
12061205
1207fn withinRowLimit(p: *Progress) bool {
1206fn withinRowLimit(p: *Progress, nl_n: usize) bool {
12081207 // The +2 here is so that the PS1 is not scrolled off the top of the terminal.
12091208 // one because we keep the cursor on the next line
12101209 // one more to account for the PS1
1211 return p.accumulated_newline_count + 2 < p.rows;
1210 return nl_n + 2 < p.rows;
12121211}
12131212
12141213fn write(buf: []const u8) anyerror!void {
12151214 try global_progress.terminal.writeAll(buf);
1216 global_progress.written_newline_count = global_progress.accumulated_newline_count;
12171215}
12181216
12191217var remaining_write_trash_bytes: usize = 0;