Skip to content

Commit e09963d

Browse files
committed
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 parent 4918c2c commit e09963d

File tree

1 file changed

+83
-85
lines changed

1 file changed

+83
-85
lines changed

lib/std/Progress.zig

Lines changed: 83 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,13 @@ redraw_event: std.Thread.ResetEvent,
2323
/// Indicates a request to shut down and reset global state.
2424
/// Accessed atomically.
2525
done: bool,
26+
need_clear: bool,
2627

2728
refresh_rate_ns: u64,
2829
initial_delay_ns: u64,
2930

3031
rows: u16,
3132
cols: u16,
32-
/// Tracks the number of newlines that have been actually written to the terminal.
33-
written_newline_count: u16,
34-
/// Tracks the number of newlines that will be written to the terminal if the
35-
/// draw buffer is sent.
36-
accumulated_newline_count: u16,
3733

3834
/// Accessed only by the update thread.
3935
draw_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

647648
fn 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-
665-
fn 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) {
698+
fn 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

1055-
fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
1038+
fn 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

11101106
fn computePrefix(
@@ -1138,20 +1134,23 @@ fn computePrefix(
11381134
}
11391135

11401136
const 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

11431139
fn 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

1207-
fn withinRowLimit(p: *Progress) bool {
1206+
fn 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

12141213
fn 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

12191217
var remaining_write_trash_bytes: usize = 0;

0 commit comments

Comments
 (0)