authorgravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2021-03-07 15:23:20+01:00
committergravatar for thatlemon@gmail.comLemonBoy <thatlemon@gmail.com> 2021-03-07 15:23:20+01:00
log4fc2e92876d8aafd087a5f0bdb6ea7a54f195704
tree7eb601c9e37655605989f757d30291d0b8c58755
parent0447a2c041a4be843251396e668e074186aa49a2

std: Better handling of line-wrapping in Progress

In order to update the printed progress string the code tried to move the cursor N cells to the left, where N is the number of written bytes, and then clear the remaining part of the line. This strategy has two main issues: - Is only valid if the number of characters is equal to the number of written bytes, - Is only valid if the line doesn't get too long. The second point is the main motivation for this change, when the line becomes too long the terminal wraps it to a new physical line. This means that moving the cursor to the left won't be enough anymore as once the left border is reached it cannot move anymore. The wrapped line is still stored by the terminal as a single line, despite now taking more than a single one when displayed. If you try to resize the terminal you'll notice how the contents are reflowed and are essentially illegible. Querying the cursor position on non-Windows systems (plot twist, Microsoft suggests using VT escape sequences on newer systems) is extremely cumbersome so let's do something different. Before printing anything let's save the cursor position and clear the screen below the cursor, this way we ensure there's absolutely no trace of stale data on screen, and after the message is printed we simply restore it.

1 files changed, 57 insertions(+), 60 deletions(-)

lib/std/Progress.zig+57-60
......@@ -59,10 +59,6 @@ done: bool = true,
5959/// while it was still being accessed by the `refresh` function.
6060update_lock: std.Thread.Mutex = .{},
6161
62/// Keeps track of how many columns in the terminal have been output, so that
63/// we can move the cursor back later.
64columns_written: usize = undefined,
65
6662/// Represents one unit of progress. Each node can have children nodes, or
6763/// one can use integers with `update`.
6864pub const Node = struct {
......@@ -159,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) !*
159155 .unprotected_estimated_total_items = estimated_total_items,
160156 .unprotected_completed_items = 0,
161157 };
162 self.columns_written = 0;
163158 self.prev_refresh_timestamp = 0;
164159 self.timer = try std.time.Timer.start();
165160 self.done = false;
......@@ -187,64 +182,56 @@ pub fn refresh(self: *Progress) void {
187182 return self.refreshWithHeldLock();
188183}
189184
185// ED -- Clear screen
186const ED = "\x1b[J";
187// DECSC -- Save cursor position
188const DECSC = "\x1b[s";
189// DECRC -- Restore cursor position
190const DECRC = "\x1b[u";
191
190192fn refreshWithHeldLock(self: *Progress) void {
191193 const is_dumb = !self.supports_ansi_escape_codes and !(std.builtin.os.tag == .windows);
192194 if (is_dumb and self.dont_print_on_dumb) return;
193195 const file = self.terminal orelse return;
194196
195 const prev_columns_written = self.columns_written;
196197 var end: usize = 0;
197 if (self.columns_written > 0) {
198 // restore the cursor position by moving the cursor
199 // `columns_written` cells to the left, then clear the rest of the
200 // line
201 if (self.supports_ansi_escape_codes) {
202 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[{d}D", .{self.columns_written}) catch unreachable).len;
203 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;
204 } else if (std.builtin.os.tag == .windows) winapi: {
205 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
206 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
207 unreachable;
208
209 var cursor_pos = windows.COORD{
210 .X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written),
211 .Y = info.dwCursorPosition.Y,
212 };
213
214 if (cursor_pos.X < 0)
215 cursor_pos.X = 0;
216
217 const fill_chars = @intCast(windows.DWORD, info.dwSize.X - cursor_pos.X);
218
219 var written: windows.DWORD = undefined;
220 if (windows.kernel32.FillConsoleOutputAttribute(
221 file.handle,
222 info.wAttributes,
223 fill_chars,
224 cursor_pos,
225 &written,
226 ) != windows.TRUE) {
227 // Stop trying to write to this file.
228 self.terminal = null;
229 break :winapi;
230 }
231 if (windows.kernel32.FillConsoleOutputCharacterA(
232 file.handle,
233 ' ',
234 fill_chars,
235 cursor_pos,
236 &written,
237 ) != windows.TRUE) unreachable;
238
239 if (windows.kernel32.SetConsoleCursorPosition(file.handle, cursor_pos) != windows.TRUE)
240 unreachable;
241 } else {
242 // we are in a "dumb" terminal like in acme or writing to a file
243 self.output_buffer[end] = '\n';
244 end += 1;
198 // Save the cursor position and clear the part of the screen below.
199 // Clearing only the line is not enough as the terminal may wrap the line
200 // when it becomes too long.
201 var saved_cursor_pos: windows.COORD = undefined;
202 if (self.supports_ansi_escape_codes) {
203 const seq_before = DECSC ++ ED;
204 std.mem.copy(u8, self.output_buffer[end..], seq_before);
205 end += seq_before.len;
206 } else if (std.builtin.os.tag == .windows) winapi: {
207 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
208 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
209 unreachable;
210
211 saved_cursor_pos = info.dwCursorPosition;
212 const fill_chars = @intCast(windows.DWORD, info.dwSize.X * (info.dwSize.Y - info.dwCursorPosition.Y) - info.dwCursorPosition.X);
213
214 var written: windows.DWORD = undefined;
215 if (windows.kernel32.FillConsoleOutputAttribute(
216 file.handle,
217 info.wAttributes,
218 fill_chars,
219 saved_cursor_pos,
220 &written,
221 ) != windows.TRUE) {
222 // Stop trying to write to this file.
223 self.terminal = null;
224 break :winapi;
225 }
226 if (windows.kernel32.FillConsoleOutputCharacterA(
227 file.handle,
228 ' ',
229 fill_chars,
230 saved_cursor_pos,
231 &written,
232 ) != windows.TRUE) {
233 unreachable;
245234 }
246
247 self.columns_written = 0;
248235 }
249236
250237 if (!self.done) {
......@@ -279,6 +266,20 @@ fn refreshWithHeldLock(self: *Progress) void {
279266 }
280267 }
281268
269 // We're done printing the updated message, restore the cursor position.
270 if (self.supports_ansi_escape_codes) {
271 const seq_after = DECRC;
272 std.mem.copy(u8, self.output_buffer[end..], seq_after);
273 end += seq_after.len;
274 } else if (std.builtin.os.tag == .windows) {
275 if (windows.kernel32.SetConsoleCursorPosition(file.handle, saved_cursor_pos) != windows.TRUE) {
276 unreachable;
277 }
278 } else {
279 self.output_buffer[end] = '\n';
280 end += 1;
281 }
282
282283 _ = file.write(self.output_buffer[0..end]) catch |e| {
283284 // Stop trying to write to this file once it errors.
284285 self.terminal = null;
......@@ -293,17 +294,14 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {
293294 self.terminal = null;
294295 return;
295296 };
296 self.columns_written = 0;
297297}
298298
299299fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void {
300300 if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| {
301301 const amt = written.len;
302302 end.* += amt;
303 self.columns_written += amt;
304303 } else |err| switch (err) {
305304 error.NoSpaceLeft => {
306 self.columns_written += self.output_buffer.len - end.*;
307305 end.* = self.output_buffer.len;
308306 },
309307 }
......@@ -311,7 +309,6 @@ fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: any
311309 const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end;
312310 if (end.* > max_end) {
313311 const suffix = "... ";
314 self.columns_written = self.columns_written - (end.* - max_end) + suffix.len;
315312 std.mem.copy(u8, self.output_buffer[max_end..], suffix);
316313 end.* = max_end + suffix.len;
317314 }