| ... | ... | @@ -16,6 +16,8 @@ terminal: ?std.fs.File, |
| 16 | 16 | /// Is this a windows API terminal (note: this is not the same as being run on windows |
| 17 | 17 | /// because other terminals exist like MSYS/git-bash) |
| 18 | 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, |
| 19 | 21 | |
| 20 | 22 | /// Whether the terminal supports ANSI escape codes. |
| 21 | 23 | supports_ansi_escape_codes: bool, |
| ... | ... | @@ -297,6 +299,7 @@ pub const Node = struct { |
| 297 | 299 | var global_progress: Progress = .{ |
| 298 | 300 | .terminal = null, |
| 299 | 301 | .is_windows_terminal = false, |
| 302 | .console_code_page = if (builtin.os.tag == .windows) undefined else {}, |
| 300 | 303 | .supports_ansi_escape_codes = false, |
| 301 | 304 | .update_thread = null, |
| 302 | 305 | .redraw_event = .{}, |
| ... | ... | @@ -378,13 +381,15 @@ pub fn start(options: Options) Node { |
| 378 | 381 | global_progress.supports_ansi_escape_codes = true; |
| 379 | 382 | } else if (builtin.os.tag == .windows and stderr.isTty()) { |
| 380 | 383 | global_progress.is_windows_terminal = true; |
| 384 | global_progress.console_code_page = windows.kernel32.GetConsoleOutputCP(); |
| 381 | 385 | global_progress.terminal = stderr; |
| 382 | 386 | } else if (builtin.os.tag != .windows) { |
| 383 | 387 | // we are in a "dumb" terminal like in acme or writing to a file |
| 384 | 388 | global_progress.terminal = stderr; |
| 385 | 389 | } |
| 386 | 390 | |
| 387 | | if (global_progress.terminal == null or !global_progress.supports_ansi_escape_codes) { |
| 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) { |
| 388 | 393 | return .{ .index = .none }; |
| 389 | 394 | } |
| 390 | 395 | |
| ... | ... | @@ -515,11 +520,90 @@ const save = "\x1b7"; |
| 515 | 520 | const restore = "\x1b8"; |
| 516 | 521 | const finish_sync = "\x1b[?2026l"; |
| 517 | 522 | |
| 518 | | const tree_tee = "\x1B\x28\x30\x74\x71\x1B\x28\x42 "; // ├─ |
| 519 | | const tree_line = "\x1B\x28\x30\x78\x1B\x28\x42 "; // │ |
| 520 | | const tree_langle = "\x1B\x28\x30\x6d\x71\x1B\x28\x42 "; // └─ |
| 523 | const TreeSymbol = enum { |
| 524 | /// ├─ |
| 525 | tee, |
| 526 | /// │ |
| 527 | line, |
| 528 | /// └─ |
| 529 | langle, |
| 530 | |
| 531 | const Encoding = enum { |
| 532 | ansi_escapes, |
| 533 | code_page_437, |
| 534 | utf8, |
| 535 | ascii, |
| 536 | }; |
| 537 | |
| 538 | /// The escape sequence representation as a string literal |
| 539 | fn escapeSeq(symbol: TreeSymbol) *const [9:0]u8 { |
| 540 | return switch (symbol) { |
| 541 | .tee => "\x1B\x28\x30\x74\x71\x1B\x28\x42 ", |
| 542 | .line => "\x1B\x28\x30\x78\x1B\x28\x42 ", |
| 543 | .langle => "\x1B\x28\x30\x6d\x71\x1B\x28\x42 ", |
| 544 | }; |
| 545 | } |
| 546 | |
| 547 | fn bytes(symbol: TreeSymbol, encoding: Encoding) []const u8 { |
| 548 | return switch (encoding) { |
| 549 | .ansi_escapes => escapeSeq(symbol), |
| 550 | .code_page_437 => switch (symbol) { |
| 551 | .tee => "\xC3\xC4 ", |
| 552 | .line => "\xB3 ", |
| 553 | .langle => "\xC0\xC4 ", |
| 554 | }, |
| 555 | .utf8 => switch (symbol) { |
| 556 | .tee => "├─ ", |
| 557 | .line => "│ ", |
| 558 | .langle => "└─ ", |
| 559 | }, |
| 560 | .ascii => switch (symbol) { |
| 561 | .tee => "|- ", |
| 562 | .line => "| ", |
| 563 | .langle => "+- ", |
| 564 | }, |
| 565 | }; |
| 566 | } |
| 567 | |
| 568 | fn maxByteLen(symbol: TreeSymbol) usize { |
| 569 | var max: usize = 0; |
| 570 | inline for (@typeInfo(Encoding).Enum.fields) |field| { |
| 571 | const len = symbol.bytes(@field(Encoding, field.name)).len; |
| 572 | if (len > max) max = len; |
| 573 | } |
| 574 | return max; |
| 575 | } |
| 576 | }; |
| 577 | |
| 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; |
| 590 | } |
| 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 | } |
| 521 | 599 | |
| 522 | 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 | |
| 523 | 607 | if (global_progress.written_newline_count == 0) return; |
| 524 | 608 | |
| 525 | 609 | var i: usize = 0; |
| ... | ... | @@ -558,6 +642,64 @@ fn computeClear(buf: []u8, start_i: usize) usize { |
| 558 | 642 | return i; |
| 559 | 643 | } |
| 560 | 644 | |
| 645 | /// U+25BA or ► |
| 646 | const windows_api_start_marker = 0x25BA; |
| 647 | |
| 648 | fn clearTerminalWindowsApi() error{Unexpected}!void { |
| 649 | // This uses a 'marker' strategy. The idea is: |
| 650 | // - Always write a marker (in this case U+25BA or ►) at the beginning of the progress |
| 651 | // - Get the current cursor position (at the end of the progress) |
| 652 | // - Subtract the number of lines written to get the expected start of the progress |
| 653 | // - Check to see if the first character at the start of the progress is the marker |
| 654 | // - If it's not the marker, keep checking the line before until we find it |
| 655 | // - Clear the screen from that position down, and set the cursor position to the start |
| 656 | // |
| 657 | // This strategy works even if there is line wrapping, and can handle the window |
| 658 | // being resized/scrolled arbitrarily. |
| 659 | // |
| 660 | // Notes: |
| 661 | // - Ideally, the marker would be a zero-width character, but the Windows console |
| 662 | // doesn't seem to support rendering zero-width characters (they show up as a space) |
| 663 | // - This same marker idea could technically be done with an attribute instead |
| 664 | // (https://learn.microsoft.com/en-us/windows/console/console-screen-buffers#character-attributes) |
| 665 | // but it must be a valid attribute and it actually needs to apply to the first |
| 666 | // character in order to be readable via ReadConsoleOutputAttribute. It doesn't seem |
| 667 | // like any of the available attributes are invisible/benign. |
| 668 | const prev_nl_n = global_progress.written_newline_count; |
| 669 | if (prev_nl_n > 0) { |
| 670 | const handle = (global_progress.terminal orelse return).handle; |
| 671 | const screen_area = @as(windows.DWORD, global_progress.cols) * global_progress.rows; |
| 672 | |
| 673 | var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; |
| 674 | if (windows.kernel32.GetConsoleScreenBufferInfo(handle, &console_info) == 0) { |
| 675 | return error.Unexpected; |
| 676 | } |
| 677 | const cursor_pos = console_info.dwCursorPosition; |
| 678 | const expected_y = cursor_pos.Y - @as(i16, @intCast(prev_nl_n)); |
| 679 | var start_pos = windows.COORD{ .X = 0, .Y = expected_y }; |
| 680 | while (start_pos.Y >= 0) { |
| 681 | var wchar: [1]u16 = undefined; |
| 682 | var num_console_chars_read: windows.DWORD = undefined; |
| 683 | if (windows.kernel32.ReadConsoleOutputCharacterW(handle, &wchar, wchar.len, start_pos, &num_console_chars_read) == 0) { |
| 684 | return error.Unexpected; |
| 685 | } |
| 686 | |
| 687 | if (wchar[0] == windows_api_start_marker) break; |
| 688 | start_pos.Y -= 1; |
| 689 | } else { |
| 690 | // If we couldn't find the marker, then just assume that no lines wrapped |
| 691 | start_pos = .{ .X = 0, .Y = expected_y }; |
| 692 | } |
| 693 | var num_chars_written: windows.DWORD = undefined; |
| 694 | if (windows.kernel32.FillConsoleOutputCharacterW(handle, ' ', screen_area, start_pos, &num_chars_written) == 0) { |
| 695 | return error.Unexpected; |
| 696 | } |
| 697 | if (windows.kernel32.SetConsoleCursorPosition(handle, start_pos) == 0) { |
| 698 | return error.Unexpected; |
| 699 | } |
| 700 | } |
| 701 | } |
| 702 | |
| 561 | 703 | const Children = struct { |
| 562 | 704 | child: Node.OptionalIndex, |
| 563 | 705 | sibling: Node.OptionalIndex, |
| ... | ... | @@ -877,17 +1019,35 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 { |
| 877 | 1019 | var i: usize = 0; |
| 878 | 1020 | const buf = global_progress.draw_buffer; |
| 879 | 1021 | |
| 880 | | buf[i..][0..start_sync.len].* = start_sync.*; |
| 881 | | i += start_sync.len; |
| 1022 | if (global_progress.supports_ansi_escape_codes) { |
| 1023 | buf[i..][0..start_sync.len].* = start_sync.*; |
| 1024 | i += start_sync.len; |
| 882 | 1025 | |
| 883 | | i = computeClear(buf, i); |
| 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 | }; |
| 1032 | |
| 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 | } |
| 1041 | } |
| 884 | 1042 | |
| 885 | 1043 | global_progress.accumulated_newline_count = 0; |
| 886 | 1044 | const root_node_index: Node.Index = @enumFromInt(0); |
| 887 | 1045 | i = computeNode(buf, i, serialized, children, root_node_index); |
| 888 | 1046 | |
| 889 | | buf[i..][0..finish_sync.len].* = finish_sync.*; |
| 890 | | i += finish_sync.len; |
| 1047 | if (global_progress.supports_ansi_escape_codes) { |
| 1048 | buf[i..][0..finish_sync.len].* = finish_sync.*; |
| 1049 | i += finish_sync.len; |
| 1050 | } |
| 891 | 1051 | |
| 892 | 1052 | return buf[0..i]; |
| 893 | 1053 | } |
| ... | ... | @@ -915,15 +1075,14 @@ fn computePrefix( |
| 915 | 1075 | buf[i..][0..prefix.len].* = prefix.*; |
| 916 | 1076 | i += prefix.len; |
| 917 | 1077 | } else { |
| 918 | | const upper_bound_len = tree_line.len + line_upper_bound_len; |
| 1078 | const upper_bound_len = TreeSymbol.line.maxByteLen() + line_upper_bound_len; |
| 919 | 1079 | if (i + upper_bound_len > buf.len) return buf.len; |
| 920 | | buf[i..][0..tree_line.len].* = tree_line.*; |
| 921 | | i += tree_line.len; |
| 1080 | i = appendTreeSymbol(.line, buf, i); |
| 922 | 1081 | } |
| 923 | 1082 | return i; |
| 924 | 1083 | } |
| 925 | 1084 | |
| 926 | | const line_upper_bound_len = @max(tree_tee.len, tree_langle.len) + "[4294967296/4294967296] ".len + |
| 1085 | const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) + "[4294967296/4294967296] ".len + |
| 927 | 1086 | Node.max_name_len + finish_sync.len; |
| 928 | 1087 | |
| 929 | 1088 | fn computeNode( |
| ... | ... | @@ -950,11 +1109,9 @@ fn computeNode( |
| 950 | 1109 | break :p; |
| 951 | 1110 | } |
| 952 | 1111 | if (children[@intFromEnum(node_index)].sibling == .none) { |
| 953 | | buf[i..][0..tree_langle.len].* = tree_langle.*; |
| 954 | | i += tree_langle.len; |
| 1112 | i = appendTreeSymbol(.langle, buf, i); |
| 955 | 1113 | } else { |
| 956 | | buf[i..][0..tree_tee.len].* = tree_tee.*; |
| 957 | | i += tree_tee.len; |
| 1114 | i = appendTreeSymbol(.tee, buf, i); |
| 958 | 1115 | } |
| 959 | 1116 | } |
| 960 | 1117 | |
| ... | ... | @@ -1072,7 +1229,11 @@ fn maybeUpdateSize(resize_flag: bool) void { |
| 1072 | 1229 | global_progress.cols = 80; |
| 1073 | 1230 | } |
| 1074 | 1231 | |
| 1075 | | global_progress.rows = @intCast(info.dwSize.Y); |
| 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); |
| 1076 | 1237 | global_progress.cols = @intCast(info.dwSize.X); |
| 1077 | 1238 | } else { |
| 1078 | 1239 | var winsize: posix.winsize = .{ |