| ... | ... | @@ -8,19 +8,13 @@ const assert = std.debug.assert; |
| 8 | 8 | const Progress = @This(); |
| 9 | 9 | const posix = std.posix; |
| 10 | 10 | const is_big_endian = builtin.cpu.arch.endian() == .big; |
| 11 | const is_windows = builtin.os.tag == .windows; |
| 11 | 12 | |
| 12 | 13 | /// `null` if the current node (and its children) should |
| 13 | 14 | /// not print on update() |
| 14 | | terminal: ?std.fs.File, |
| 15 | terminal: std.fs.File, |
| 15 | 16 | |
| 16 | | /// Is this a windows API terminal (note: this is not the same as being run on windows |
| 17 | | /// because other terminals exist like MSYS/git-bash) |
| 18 | | is_windows_terminal: bool, |
| 19 | | /// The output code page of the console (only set if the console is a Windows API terminal) |
| 20 | | console_code_page: if (builtin.os.tag == .windows) windows.UINT else void, |
| 21 | | |
| 22 | | /// Whether the terminal supports ANSI escape codes. |
| 23 | | supports_ansi_escape_codes: bool, |
| 17 | terminal_mode: TerminalMode, |
| 24 | 18 | |
| 25 | 19 | update_thread: ?std.Thread, |
| 26 | 20 | |
| ... | ... | @@ -53,6 +47,19 @@ node_freelist: []Node.OptionalIndex, |
| 53 | 47 | node_freelist_first: Node.OptionalIndex, |
| 54 | 48 | node_end_index: u32, |
| 55 | 49 | |
| 50 | pub const TerminalMode = union(enum) { |
| 51 | off, |
| 52 | ansi_escape_codes, |
| 53 | /// This is not the same as being run on windows because other terminals |
| 54 | /// exist like MSYS/git-bash. |
| 55 | windows_api: if (is_windows) WindowsApi else void, |
| 56 | |
| 57 | pub const WindowsApi = struct { |
| 58 | /// The output code page of the console. |
| 59 | code_page: windows.UINT, |
| 60 | }; |
| 61 | }; |
| 62 | |
| 56 | 63 | pub const Options = struct { |
| 57 | 64 | /// User-provided buffer with static lifetime. |
| 58 | 65 | /// |
| ... | ... | @@ -297,10 +304,8 @@ pub const Node = struct { |
| 297 | 304 | }; |
| 298 | 305 | |
| 299 | 306 | var global_progress: Progress = .{ |
| 300 | | .terminal = null, |
| 301 | | .is_windows_terminal = false, |
| 302 | | .console_code_page = if (builtin.os.tag == .windows) undefined else {}, |
| 303 | | .supports_ansi_escape_codes = false, |
| 307 | .terminal = undefined, |
| 308 | .terminal_mode = .off, |
| 304 | 309 | .update_thread = null, |
| 305 | 310 | .redraw_event = .{}, |
| 306 | 311 | .refresh_rate_ns = undefined, |
| ... | ... | @@ -376,20 +381,16 @@ pub fn start(options: Options) Node { |
| 376 | 381 | return .{ .index = .none }; |
| 377 | 382 | } |
| 378 | 383 | const stderr = std.io.getStdErr(); |
| 384 | global_progress.terminal = stderr; |
| 379 | 385 | if (stderr.supportsAnsiEscapeCodes()) { |
| 380 | | global_progress.terminal = stderr; |
| 381 | | global_progress.supports_ansi_escape_codes = true; |
| 382 | | } else if (builtin.os.tag == .windows and stderr.isTty()) { |
| 383 | | global_progress.is_windows_terminal = true; |
| 384 | | global_progress.console_code_page = windows.kernel32.GetConsoleOutputCP(); |
| 385 | | global_progress.terminal = stderr; |
| 386 | | } else if (builtin.os.tag != .windows) { |
| 387 | | // we are in a "dumb" terminal like in acme or writing to a file |
| 388 | | global_progress.terminal = stderr; |
| 386 | global_progress.terminal_mode = .ansi_escape_codes; |
| 387 | } else if (is_windows and stderr.isTty()) { |
| 388 | global_progress.terminal_mode = TerminalMode{ .windows_api = .{ |
| 389 | .code_page = windows.kernel32.GetConsoleOutputCP(), |
| 390 | } }; |
| 389 | 391 | } |
| 390 | 392 | |
| 391 | | const can_clear_terminal = global_progress.supports_ansi_escape_codes or global_progress.is_windows_terminal; |
| 392 | | if (global_progress.terminal == null or !can_clear_terminal) { |
| 393 | if (global_progress.terminal_mode == .off) { |
| 393 | 394 | return .{ .index = .none }; |
| 394 | 395 | } |
| 395 | 396 | |
| ... | ... | @@ -404,7 +405,11 @@ pub fn start(options: Options) Node { |
| 404 | 405 | }; |
| 405 | 406 | } |
| 406 | 407 | |
| 407 | | if (std.Thread.spawn(.{}, updateThreadRun, .{})) |thread| { |
| 408 | if (switch (global_progress.terminal_mode) { |
| 409 | .off => unreachable, // handled a few lines above |
| 410 | .ansi_escape_codes => std.Thread.spawn(.{}, updateThreadRun, .{}), |
| 411 | .windows_api => if (is_windows) std.Thread.spawn(.{}, windowsApiUpdateThreadRun, .{}) else unreachable, |
| 412 | }) |thread| { |
| 408 | 413 | global_progress.update_thread = thread; |
| 409 | 414 | } else |err| { |
| 410 | 415 | std.log.warn("unable to spawn thread for printing progress to terminal: {s}", .{@errorName(err)}); |
| ... | ... | @@ -438,13 +443,42 @@ fn updateThreadRun() void { |
| 438 | 443 | |
| 439 | 444 | { |
| 440 | 445 | const resize_flag = wait(global_progress.initial_delay_ns); |
| 446 | if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return; |
| 441 | 447 | maybeUpdateSize(resize_flag); |
| 442 | 448 | |
| 449 | const buffer = computeRedraw(&serialized_buffer); |
| 450 | if (stderr_mutex.tryLock()) { |
| 451 | defer stderr_mutex.unlock(); |
| 452 | write(buffer) catch return; |
| 453 | } |
| 454 | } |
| 455 | |
| 456 | while (true) { |
| 457 | const resize_flag = wait(global_progress.refresh_rate_ns); |
| 458 | |
| 443 | 459 | if (@atomicLoad(bool, &global_progress.done, .seq_cst)) { |
| 444 | 460 | stderr_mutex.lock(); |
| 445 | 461 | defer stderr_mutex.unlock(); |
| 446 | | return clearTerminal(); |
| 462 | return clearWrittenWithEscapeCodes() catch {}; |
| 463 | } |
| 464 | |
| 465 | maybeUpdateSize(resize_flag); |
| 466 | |
| 467 | const buffer = computeRedraw(&serialized_buffer); |
| 468 | if (stderr_mutex.tryLock()) { |
| 469 | defer stderr_mutex.unlock(); |
| 470 | write(buffer) catch return; |
| 447 | 471 | } |
| 472 | } |
| 473 | } |
| 474 | |
| 475 | fn windowsApiUpdateThreadRun() void { |
| 476 | var serialized_buffer: Serialized.Buffer = undefined; |
| 477 | |
| 478 | { |
| 479 | const resize_flag = wait(global_progress.initial_delay_ns); |
| 480 | if (@atomicLoad(bool, &global_progress.done, .seq_cst)) return; |
| 481 | maybeUpdateSize(resize_flag); |
| 448 | 482 | |
| 449 | 483 | const buffer = computeRedraw(&serialized_buffer); |
| 450 | 484 | if (stderr_mutex.tryLock()) { |
| ... | ... | @@ -455,17 +489,19 @@ fn updateThreadRun() void { |
| 455 | 489 | |
| 456 | 490 | while (true) { |
| 457 | 491 | const resize_flag = wait(global_progress.refresh_rate_ns); |
| 458 | | maybeUpdateSize(resize_flag); |
| 459 | 492 | |
| 460 | 493 | if (@atomicLoad(bool, &global_progress.done, .seq_cst)) { |
| 461 | 494 | stderr_mutex.lock(); |
| 462 | 495 | defer stderr_mutex.unlock(); |
| 463 | | return clearTerminal(); |
| 496 | return clearWrittenWindowsApi() catch {}; |
| 464 | 497 | } |
| 465 | 498 | |
| 499 | maybeUpdateSize(resize_flag); |
| 500 | |
| 466 | 501 | const buffer = computeRedraw(&serialized_buffer); |
| 467 | 502 | if (stderr_mutex.tryLock()) { |
| 468 | 503 | defer stderr_mutex.unlock(); |
| 504 | clearWrittenWindowsApi() catch return; |
| 469 | 505 | write(buffer) catch return; |
| 470 | 506 | } |
| 471 | 507 | } |
| ... | ... | @@ -476,7 +512,7 @@ fn updateThreadRun() void { |
| 476 | 512 | /// During the lock, any `std.Progress` information is cleared from the terminal. |
| 477 | 513 | pub fn lockStdErr() void { |
| 478 | 514 | stderr_mutex.lock(); |
| 479 | | clearTerminal(); |
| 515 | clearWrittenWithEscapeCodes() catch {}; |
| 480 | 516 | } |
| 481 | 517 | |
| 482 | 518 | pub fn unlockStdErr() void { |
| ... | ... | @@ -504,7 +540,7 @@ fn ipcThreadRun(fd: posix.fd_t) anyerror!void { |
| 504 | 540 | _ = wait(global_progress.refresh_rate_ns); |
| 505 | 541 | |
| 506 | 542 | if (@atomicLoad(bool, &global_progress.done, .seq_cst)) |
| 507 | | return clearTerminal(); |
| 543 | return; |
| 508 | 544 | |
| 509 | 545 | const serialized = serialize(&serialized_buffer); |
| 510 | 546 | writeIpc(fd, serialized) catch |err| switch (err) { |
| ... | ... | @@ -569,41 +605,36 @@ const TreeSymbol = enum { |
| 569 | 605 | var max: usize = 0; |
| 570 | 606 | inline for (@typeInfo(Encoding).Enum.fields) |field| { |
| 571 | 607 | const len = symbol.bytes(@field(Encoding, field.name)).len; |
| 572 | | if (len > max) max = len; |
| 608 | max = @max(max, len); |
| 573 | 609 | } |
| 574 | 610 | return max; |
| 575 | 611 | } |
| 576 | 612 | }; |
| 577 | 613 | |
| 578 | | fn appendTreeSymbol(comptime symbol: TreeSymbol, buf: []u8, start_i: usize) usize { |
| 579 | | if (builtin.os.tag == .windows and global_progress.is_windows_terminal) { |
| 580 | | const bytes = switch (global_progress.console_code_page) { |
| 581 | | // Code page 437 is the default code page and contains the box drawing symbols |
| 582 | | 437 => symbol.bytes(.code_page_437), |
| 583 | | // UTF-8 |
| 584 | | 65001 => symbol.bytes(.utf8), |
| 585 | | // Fall back to ASCII approximation |
| 586 | | else => symbol.bytes(.ascii), |
| 587 | | }; |
| 588 | | @memcpy(buf[start_i..][0..bytes.len], bytes); |
| 589 | | return start_i + bytes.len; |
| 614 | fn appendTreeSymbol(symbol: TreeSymbol, buf: []u8, start_i: usize) usize { |
| 615 | switch (global_progress.terminal_mode) { |
| 616 | .off => unreachable, |
| 617 | .ansi_escape_codes => { |
| 618 | const bytes = symbol.escapeSeq(); |
| 619 | buf[start_i..][0..bytes.len].* = bytes.*; |
| 620 | return start_i + bytes.len; |
| 621 | }, |
| 622 | .windows_api => |windows_api| { |
| 623 | const bytes = if (!is_windows) unreachable else switch (windows_api.code_page) { |
| 624 | // Code page 437 is the default code page and contains the box drawing symbols |
| 625 | 437 => symbol.bytes(.code_page_437), |
| 626 | // UTF-8 |
| 627 | 65001 => symbol.bytes(.utf8), |
| 628 | // Fall back to ASCII approximation |
| 629 | else => symbol.bytes(.ascii), |
| 630 | }; |
| 631 | @memcpy(buf[start_i..][0..bytes.len], bytes); |
| 632 | return start_i + bytes.len; |
| 633 | }, |
| 590 | 634 | } |
| 591 | | |
| 592 | | // Drawing the tree is disabled when ansi escape codes are not supported |
| 593 | | assert(global_progress.supports_ansi_escape_codes); |
| 594 | | |
| 595 | | const bytes = symbol.escapeSeq(); |
| 596 | | buf[start_i..][0..bytes.len].* = bytes.*; |
| 597 | | return start_i + bytes.len; |
| 598 | 635 | } |
| 599 | 636 | |
| 600 | | fn clearTerminal() void { |
| 601 | | if (builtin.os.tag == .windows and global_progress.is_windows_terminal) { |
| 602 | | return clearTerminalWindowsApi() catch { |
| 603 | | global_progress.terminal = null; |
| 604 | | }; |
| 605 | | } |
| 606 | | |
| 637 | fn clearWrittenWithEscapeCodes() anyerror!void { |
| 607 | 638 | if (global_progress.written_newline_count == 0) return; |
| 608 | 639 | |
| 609 | 640 | var i: usize = 0; |
| ... | ... | @@ -618,9 +649,7 @@ fn clearTerminal() void { |
| 618 | 649 | i += finish_sync.len; |
| 619 | 650 | |
| 620 | 651 | global_progress.accumulated_newline_count = 0; |
| 621 | | write(buf[0..i]) catch { |
| 622 | | global_progress.terminal = null; |
| 623 | | }; |
| 652 | try write(buf[0..i]); |
| 624 | 653 | } |
| 625 | 654 | |
| 626 | 655 | fn computeClear(buf: []u8, start_i: usize) usize { |
| ... | ... | @@ -645,7 +674,7 @@ fn computeClear(buf: []u8, start_i: usize) usize { |
| 645 | 674 | /// U+25BA or ► |
| 646 | 675 | const windows_api_start_marker = 0x25BA; |
| 647 | 676 | |
| 648 | | fn clearTerminalWindowsApi() error{Unexpected}!void { |
| 677 | fn clearWrittenWindowsApi() error{Unexpected}!void { |
| 649 | 678 | // This uses a 'marker' strategy. The idea is: |
| 650 | 679 | // - Always write a marker (in this case U+25BA or ►) at the beginning of the progress |
| 651 | 680 | // - Get the current cursor position (at the end of the progress) |
| ... | ... | @@ -667,7 +696,7 @@ fn clearTerminalWindowsApi() error{Unexpected}!void { |
| 667 | 696 | // like any of the available attributes are invisible/benign. |
| 668 | 697 | const prev_nl_n = global_progress.written_newline_count; |
| 669 | 698 | if (prev_nl_n > 0) { |
| 670 | | const handle = (global_progress.terminal orelse return).handle; |
| 699 | const handle = global_progress.terminal.handle; |
| 671 | 700 | const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows; |
| 672 | 701 | |
| 673 | 702 | var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; |
| ... | ... | @@ -777,14 +806,14 @@ const SavedMetadata = struct { |
| 777 | 806 | nodes_len: u8, |
| 778 | 807 | |
| 779 | 808 | fn getIpcFd(metadata: SavedMetadata) posix.fd_t { |
| 780 | | return if (builtin.os.tag == .windows) |
| 809 | return if (is_windows) |
| 781 | 810 | @ptrFromInt(@as(usize, metadata.ipc_fd) << 2) |
| 782 | 811 | else |
| 783 | 812 | metadata.ipc_fd; |
| 784 | 813 | } |
| 785 | 814 | |
| 786 | 815 | fn setIpcFd(fd: posix.fd_t) u16 { |
| 787 | | return @intCast(if (builtin.os.tag == .windows) |
| 816 | return @intCast(if (is_windows) |
| 788 | 817 | @shrExact(@intFromPtr(fd), 2) |
| 789 | 818 | else |
| 790 | 819 | fd); |
| ... | ... | @@ -1019,35 +1048,21 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 { |
| 1019 | 1048 | var i: usize = 0; |
| 1020 | 1049 | const buf = global_progress.draw_buffer; |
| 1021 | 1050 | |
| 1022 | | if (global_progress.supports_ansi_escape_codes) { |
| 1023 | | buf[i..][0..start_sync.len].* = start_sync.*; |
| 1024 | | i += start_sync.len; |
| 1025 | | |
| 1026 | | i = computeClear(buf, i); |
| 1027 | | } else if (builtin.os.tag == .windows and global_progress.is_windows_terminal) { |
| 1028 | | clearTerminalWindowsApi() catch { |
| 1029 | | global_progress.terminal = null; |
| 1030 | | return buf[0..0]; |
| 1031 | | }; |
| 1051 | buf[i..][0..start_sync.len].* = start_sync.*; |
| 1052 | i += start_sync.len; |
| 1032 | 1053 | |
| 1033 | | // Write the marker that we will use to find the beginning of the progress when clearing. |
| 1034 | | // Note: This doesn't have to use WriteConsoleW, but doing so avoids dealing with the code page. |
| 1035 | | var num_chars_written: windows.DWORD = undefined; |
| 1036 | | const handle = (global_progress.terminal orelse return buf[0..0]).handle; |
| 1037 | | if (windows.kernel32.WriteConsoleW(handle, &[_]u16{windows_api_start_marker}, 1, &num_chars_written, null) == 0) { |
| 1038 | | global_progress.terminal = null; |
| 1039 | | return buf[0..0]; |
| 1040 | | } |
| 1054 | switch (global_progress.terminal_mode) { |
| 1055 | .off => unreachable, |
| 1056 | .ansi_escape_codes => i = computeClear(buf, i), |
| 1057 | .windows_api => if (!is_windows) unreachable, |
| 1041 | 1058 | } |
| 1042 | 1059 | |
| 1043 | 1060 | global_progress.accumulated_newline_count = 0; |
| 1044 | 1061 | const root_node_index: Node.Index = @enumFromInt(0); |
| 1045 | 1062 | i = computeNode(buf, i, serialized, children, root_node_index); |
| 1046 | 1063 | |
| 1047 | | if (global_progress.supports_ansi_escape_codes) { |
| 1048 | | buf[i..][0..finish_sync.len].* = finish_sync.*; |
| 1049 | | i += finish_sync.len; |
| 1050 | | } |
| 1064 | buf[i..][0..finish_sync.len].* = finish_sync.*; |
| 1065 | i += finish_sync.len; |
| 1051 | 1066 | |
| 1052 | 1067 | return buf[0..i]; |
| 1053 | 1068 | } |
| ... | ... | @@ -1075,15 +1090,15 @@ fn computePrefix( |
| 1075 | 1090 | buf[i..][0..prefix.len].* = prefix.*; |
| 1076 | 1091 | i += prefix.len; |
| 1077 | 1092 | } else { |
| 1078 | | const upper_bound_len = TreeSymbol.line.maxByteLen() + line_upper_bound_len; |
| 1093 | const upper_bound_len = comptime (TreeSymbol.line.maxByteLen() + line_upper_bound_len); |
| 1079 | 1094 | if (i + upper_bound_len > buf.len) return buf.len; |
| 1080 | 1095 | i = appendTreeSymbol(.line, buf, i); |
| 1081 | 1096 | } |
| 1082 | 1097 | return i; |
| 1083 | 1098 | } |
| 1084 | 1099 | |
| 1085 | | const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) + "[4294967296/4294967296] ".len + |
| 1086 | | Node.max_name_len + finish_sync.len; |
| 1100 | const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) + |
| 1101 | "[4294967296/4294967296] ".len + Node.max_name_len + finish_sync.len; |
| 1087 | 1102 | |
| 1088 | 1103 | fn computeNode( |
| 1089 | 1104 | buf: []u8, |
| ... | ... | @@ -1157,8 +1172,7 @@ fn withinRowLimit(p: *Progress) bool { |
| 1157 | 1172 | } |
| 1158 | 1173 | |
| 1159 | 1174 | fn write(buf: []const u8) anyerror!void { |
| 1160 | | const tty = global_progress.terminal orelse return; |
| 1161 | | try tty.writeAll(buf); |
| 1175 | try global_progress.terminal.writeAll(buf); |
| 1162 | 1176 | global_progress.written_newline_count = global_progress.accumulated_newline_count; |
| 1163 | 1177 | } |
| 1164 | 1178 | |
| ... | ... | @@ -1218,23 +1232,23 @@ fn writeIpc(fd: posix.fd_t, serialized: Serialized) error{BrokenPipe}!void { |
| 1218 | 1232 | fn maybeUpdateSize(resize_flag: bool) void { |
| 1219 | 1233 | if (!resize_flag) return; |
| 1220 | 1234 | |
| 1221 | | const fd = (global_progress.terminal orelse return).handle; |
| 1235 | const fd = global_progress.terminal.handle; |
| 1222 | 1236 | |
| 1223 | | if (builtin.os.tag == .windows) { |
| 1237 | if (is_windows) { |
| 1224 | 1238 | var info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; |
| 1225 | 1239 | |
| 1226 | | if (windows.kernel32.GetConsoleScreenBufferInfo(fd, &info) == windows.FALSE) { |
| 1240 | if (windows.kernel32.GetConsoleScreenBufferInfo(fd, &info) != windows.FALSE) { |
| 1241 | // In the old Windows console, dwSize.Y is the line count of the |
| 1242 | // entire scrollback buffer, so we use this instead so that we |
| 1243 | // always get the size of the screen. |
| 1244 | const screen_height = info.srWindow.Bottom - info.srWindow.Top; |
| 1245 | global_progress.rows = @intCast(screen_height); |
| 1246 | global_progress.cols = @intCast(info.dwSize.X); |
| 1247 | } else { |
| 1227 | 1248 | std.log.debug("failed to determine terminal size; using conservative guess 80x25", .{}); |
| 1228 | 1249 | global_progress.rows = 25; |
| 1229 | 1250 | global_progress.cols = 80; |
| 1230 | 1251 | } |
| 1231 | | |
| 1232 | | // In the old Windows console, dwSize.Y is the line count of the entire |
| 1233 | | // scrollback buffer, so we use this instead so that we always get the |
| 1234 | | // size of the screen. |
| 1235 | | const screen_height = info.srWindow.Bottom - info.srWindow.Top; |
| 1236 | | global_progress.rows = @intCast(screen_height); |
| 1237 | | global_progress.cols = @intCast(info.dwSize.X); |
| 1238 | 1252 | } else { |
| 1239 | 1253 | var winsize: posix.winsize = .{ |
| 1240 | 1254 | .ws_row = 0, |