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,...@@ -59,10 +59,6 @@ done: bool = true,
59/// while it was still being accessed by the `refresh` function.59/// while it was still being accessed by the `refresh` function.
60update_lock: std.Thread.Mutex = .{},60update_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
66/// Represents one unit of progress. Each node can have children nodes, or62/// Represents one unit of progress. Each node can have children nodes, or
67/// one can use integers with `update`.63/// one can use integers with `update`.
68pub const Node = struct {64pub const Node = struct {
...@@ -159,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) !*...@@ -159,7 +155,6 @@ pub fn start(self: *Progress, name: []const u8, estimated_total_items: usize) !*
159 .unprotected_estimated_total_items = estimated_total_items,155 .unprotected_estimated_total_items = estimated_total_items,
160 .unprotected_completed_items = 0,156 .unprotected_completed_items = 0,
161 };157 };
162 self.columns_written = 0;
163 self.prev_refresh_timestamp = 0;158 self.prev_refresh_timestamp = 0;
164 self.timer = try std.time.Timer.start();159 self.timer = try std.time.Timer.start();
165 self.done = false;160 self.done = false;
...@@ -187,64 +182,56 @@ pub fn refresh(self: *Progress) void {...@@ -187,64 +182,56 @@ pub fn refresh(self: *Progress) void {
187 return self.refreshWithHeldLock();182 return self.refreshWithHeldLock();
188}183}
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
190fn refreshWithHeldLock(self: *Progress) void {192fn refreshWithHeldLock(self: *Progress) void {
191 const is_dumb = !self.supports_ansi_escape_codes and !(std.builtin.os.tag == .windows);193 const is_dumb = !self.supports_ansi_escape_codes and !(std.builtin.os.tag == .windows);
192 if (is_dumb and self.dont_print_on_dumb) return;194 if (is_dumb and self.dont_print_on_dumb) return;
193 const file = self.terminal orelse return;195 const file = self.terminal orelse return;
194196
195 const prev_columns_written = self.columns_written;
196 var end: usize = 0;197 var end: usize = 0;
197 if (self.columns_written > 0) {198 // Save the cursor position and clear the part of the screen below.
198 // restore the cursor position by moving the cursor199 // Clearing only the line is not enough as the terminal may wrap the line
199 // `columns_written` cells to the left, then clear the rest of the200 // when it becomes too long.
200 // line201 var saved_cursor_pos: windows.COORD = undefined;
201 if (self.supports_ansi_escape_codes) {202 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 const seq_before = DECSC ++ ED;
203 end += (std.fmt.bufPrint(self.output_buffer[end..], "\x1b[0K", .{}) catch unreachable).len;204 std.mem.copy(u8, self.output_buffer[end..], seq_before);
204 } else if (std.builtin.os.tag == .windows) winapi: {205 end += seq_before.len;
205 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;206 } else if (std.builtin.os.tag == .windows) winapi: {
206 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)207 var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
207 unreachable;208 if (windows.kernel32.GetConsoleScreenBufferInfo(file.handle, &info) != windows.TRUE)
208209 unreachable;
209 var cursor_pos = windows.COORD{210
210 .X = info.dwCursorPosition.X - @intCast(windows.SHORT, self.columns_written),211 saved_cursor_pos = info.dwCursorPosition;
211 .Y = info.dwCursorPosition.Y,212 const fill_chars = @intCast(windows.DWORD, info.dwSize.X * (info.dwSize.Y - info.dwCursorPosition.Y) - info.dwCursorPosition.X);
212 };213
213214 var written: windows.DWORD = undefined;
214 if (cursor_pos.X < 0)215 if (windows.kernel32.FillConsoleOutputAttribute(
215 cursor_pos.X = 0;216 file.handle,
216217 info.wAttributes,
217 const fill_chars = @intCast(windows.DWORD, info.dwSize.X - cursor_pos.X);218 fill_chars,
218219 saved_cursor_pos,
219 var written: windows.DWORD = undefined;220 &written,
220 if (windows.kernel32.FillConsoleOutputAttribute(221 ) != windows.TRUE) {
221 file.handle,222 // Stop trying to write to this file.
222 info.wAttributes,223 self.terminal = null;
223 fill_chars,224 break :winapi;
224 cursor_pos,225 }
225 &written,226 if (windows.kernel32.FillConsoleOutputCharacterA(
226 ) != windows.TRUE) {227 file.handle,
227 // Stop trying to write to this file.228 ' ',
228 self.terminal = null;229 fill_chars,
229 break :winapi;230 saved_cursor_pos,
230 }231 &written,
231 if (windows.kernel32.FillConsoleOutputCharacterA(232 ) != windows.TRUE) {
232 file.handle,233 unreachable;
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;
245 }234 }
246
247 self.columns_written = 0;
248 }235 }
249236
250 if (!self.done) {237 if (!self.done) {
...@@ -279,6 +266,20 @@ fn refreshWithHeldLock(self: *Progress) void {...@@ -279,6 +266,20 @@ fn refreshWithHeldLock(self: *Progress) void {
279 }266 }
280 }267 }
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
282 _ = file.write(self.output_buffer[0..end]) catch |e| {283 _ = file.write(self.output_buffer[0..end]) catch |e| {
283 // Stop trying to write to this file once it errors.284 // Stop trying to write to this file once it errors.
284 self.terminal = null;285 self.terminal = null;
...@@ -293,17 +294,14 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {...@@ -293,17 +294,14 @@ pub fn log(self: *Progress, comptime format: []const u8, args: anytype) void {
293 self.terminal = null;294 self.terminal = null;
294 return;295 return;
295 };296 };
296 self.columns_written = 0;
297}297}
298298
299fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void {299fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: anytype) void {
300 if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| {300 if (std.fmt.bufPrint(self.output_buffer[end.*..], format, args)) |written| {
301 const amt = written.len;301 const amt = written.len;
302 end.* += amt;302 end.* += amt;
303 self.columns_written += amt;
304 } else |err| switch (err) {303 } else |err| switch (err) {
305 error.NoSpaceLeft => {304 error.NoSpaceLeft => {
306 self.columns_written += self.output_buffer.len - end.*;
307 end.* = self.output_buffer.len;305 end.* = self.output_buffer.len;
308 },306 },
309 }307 }
...@@ -311,7 +309,6 @@ fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: any...@@ -311,7 +309,6 @@ fn bufWrite(self: *Progress, end: *usize, comptime format: []const u8, args: any
311 const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end;309 const max_end = self.output_buffer.len - bytes_needed_for_esc_codes_at_end;
312 if (end.* > max_end) {310 if (end.* > max_end) {
313 const suffix = "... ";311 const suffix = "... ";
314 self.columns_written = self.columns_written - (end.* - max_end) + suffix.len;
315 std.mem.copy(u8, self.output_buffer[max_end..], suffix);312 std.mem.copy(u8, self.output_buffer[max_end..], suffix);
316 end.* = max_end + suffix.len;313 end.* = max_end + suffix.len;
317 }314 }