| ... | @@ -16,6 +16,8 @@ terminal: ?std.fs.File, | ... | @@ -16,6 +16,8 @@ terminal: ?std.fs.File, |
| 16 | /// Is this a windows API terminal (note: this is not the same as being run on windows | 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) | 17 | /// because other terminals exist like MSYS/git-bash) |
| 18 | is_windows_terminal: bool, | 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 | /// Whether the terminal supports ANSI escape codes. | 22 | /// Whether the terminal supports ANSI escape codes. |
| 21 | supports_ansi_escape_codes: bool, | 23 | supports_ansi_escape_codes: bool, |
| ... | @@ -297,6 +299,7 @@ pub const Node = struct { | ... | @@ -297,6 +299,7 @@ pub const Node = struct { |
| 297 | var global_progress: Progress = .{ | 299 | var global_progress: Progress = .{ |
| 298 | .terminal = null, | 300 | .terminal = null, |
| 299 | .is_windows_terminal = false, | 301 | .is_windows_terminal = false, |
| | 302 | .console_code_page = if (builtin.os.tag == .windows) undefined else {}, |
| 300 | .supports_ansi_escape_codes = false, | 303 | .supports_ansi_escape_codes = false, |
| 301 | .update_thread = null, | 304 | .update_thread = null, |
| 302 | .redraw_event = .{}, | 305 | .redraw_event = .{}, |
| ... | @@ -378,13 +381,15 @@ pub fn start(options: Options) Node { | ... | @@ -378,13 +381,15 @@ pub fn start(options: Options) Node { |
| 378 | global_progress.supports_ansi_escape_codes = true; | 381 | global_progress.supports_ansi_escape_codes = true; |
| 379 | } else if (builtin.os.tag == .windows and stderr.isTty()) { | 382 | } else if (builtin.os.tag == .windows and stderr.isTty()) { |
| 380 | global_progress.is_windows_terminal = true; | 383 | global_progress.is_windows_terminal = true; |
| | 384 | global_progress.console_code_page = windows.kernel32.GetConsoleOutputCP(); |
| 381 | global_progress.terminal = stderr; | 385 | global_progress.terminal = stderr; |
| 382 | } else if (builtin.os.tag != .windows) { | 386 | } else if (builtin.os.tag != .windows) { |
| 383 | // we are in a "dumb" terminal like in acme or writing to a file | 387 | // we are in a "dumb" terminal like in acme or writing to a file |
| 384 | global_progress.terminal = stderr; | 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 | return .{ .index = .none }; | 393 | return .{ .index = .none }; |
| 389 | } | 394 | } |
| 390 | | 395 | |
| ... | @@ -515,11 +520,90 @@ const save = "\x1b7"; | ... | @@ -515,11 +520,90 @@ const save = "\x1b7"; |
| 515 | const restore = "\x1b8"; | 520 | const restore = "\x1b8"; |
| 516 | const finish_sync = "\x1b[?2026l"; | 521 | const finish_sync = "\x1b[?2026l"; |
| 517 | | 522 | |
| 518 | const tree_tee = "\x1B\x28\x30\x74\x71\x1B\x28\x42 "; // ├─ | 523 | const TreeSymbol = enum { |
| 519 | const tree_line = "\x1B\x28\x30\x78\x1B\x28\x42 "; // │ | 524 | /// ├─ |
| 520 | const tree_langle = "\x1B\x28\x30\x6d\x71\x1B\x28\x42 "; // └─ | 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 | fn clearTerminal() void { | 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 | if (global_progress.written_newline_count == 0) return; | 607 | if (global_progress.written_newline_count == 0) return; |
| 524 | | 608 | |
| 525 | var i: usize = 0; | 609 | var i: usize = 0; |
| ... | @@ -558,6 +642,64 @@ fn computeClear(buf: []u8, start_i: usize) usize { | ... | @@ -558,6 +642,64 @@ fn computeClear(buf: []u8, start_i: usize) usize { |
| 558 | return i; | 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 | const Children = struct { | 703 | const Children = struct { |
| 562 | child: Node.OptionalIndex, | 704 | child: Node.OptionalIndex, |
| 563 | sibling: Node.OptionalIndex, | 705 | sibling: Node.OptionalIndex, |
| ... | @@ -877,17 +1019,35 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 { | ... | @@ -877,17 +1019,35 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 { |
| 877 | var i: usize = 0; | 1019 | var i: usize = 0; |
| 878 | const buf = global_progress.draw_buffer; | 1020 | const buf = global_progress.draw_buffer; |
| 879 | | 1021 | |
| 880 | buf[i..][0..start_sync.len].* = start_sync.*; | 1022 | if (global_progress.supports_ansi_escape_codes) { |
| 881 | i += start_sync.len; | 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 | global_progress.accumulated_newline_count = 0; | 1043 | global_progress.accumulated_newline_count = 0; |
| 886 | const root_node_index: Node.Index = @enumFromInt(0); | 1044 | const root_node_index: Node.Index = @enumFromInt(0); |
| 887 | i = computeNode(buf, i, serialized, children, root_node_index); | 1045 | i = computeNode(buf, i, serialized, children, root_node_index); |
| 888 | | 1046 | |
| 889 | buf[i..][0..finish_sync.len].* = finish_sync.*; | 1047 | if (global_progress.supports_ansi_escape_codes) { |
| 890 | i += finish_sync.len; | 1048 | buf[i..][0..finish_sync.len].* = finish_sync.*; |
| | 1049 | i += finish_sync.len; |
| | 1050 | } |
| 891 | | 1051 | |
| 892 | return buf[0..i]; | 1052 | return buf[0..i]; |
| 893 | } | 1053 | } |
| ... | @@ -915,15 +1075,14 @@ fn computePrefix( | ... | @@ -915,15 +1075,14 @@ fn computePrefix( |
| 915 | buf[i..][0..prefix.len].* = prefix.*; | 1075 | buf[i..][0..prefix.len].* = prefix.*; |
| 916 | i += prefix.len; | 1076 | i += prefix.len; |
| 917 | } else { | 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 | if (i + upper_bound_len > buf.len) return buf.len; | 1079 | if (i + upper_bound_len > buf.len) return buf.len; |
| 920 | buf[i..][0..tree_line.len].* = tree_line.*; | 1080 | i = appendTreeSymbol(.line, buf, i); |
| 921 | i += tree_line.len; | | |
| 922 | } | 1081 | } |
| 923 | return i; | 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 | Node.max_name_len + finish_sync.len; | 1086 | Node.max_name_len + finish_sync.len; |
| 928 | | 1087 | |
| 929 | fn computeNode( | 1088 | fn computeNode( |
| ... | @@ -950,11 +1109,9 @@ fn computeNode( | ... | @@ -950,11 +1109,9 @@ fn computeNode( |
| 950 | break :p; | 1109 | break :p; |
| 951 | } | 1110 | } |
| 952 | if (children[@intFromEnum(node_index)].sibling == .none) { | 1111 | if (children[@intFromEnum(node_index)].sibling == .none) { |
| 953 | buf[i..][0..tree_langle.len].* = tree_langle.*; | 1112 | i = appendTreeSymbol(.langle, buf, i); |
| 954 | i += tree_langle.len; | | |
| 955 | } else { | 1113 | } else { |
| 956 | buf[i..][0..tree_tee.len].* = tree_tee.*; | 1114 | i = appendTreeSymbol(.tee, buf, i); |
| 957 | i += tree_tee.len; | | |
| 958 | } | 1115 | } |
| 959 | } | 1116 | } |
| 960 | | 1117 | |
| ... | @@ -1072,7 +1229,11 @@ fn maybeUpdateSize(resize_flag: bool) void { | ... | @@ -1072,7 +1229,11 @@ fn maybeUpdateSize(resize_flag: bool) void { |
| 1072 | global_progress.cols = 80; | 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 | global_progress.cols = @intCast(info.dwSize.X); | 1237 | global_progress.cols = @intCast(info.dwSize.X); |
| 1077 | } else { | 1238 | } else { |
| 1078 | var winsize: posix.winsize = .{ | 1239 | var winsize: posix.winsize = .{ |