authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2024-05-28 03:40:18-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2024-05-28 10:41:07-07:00
log40afac40b8d9f274d63448a11f9f4259a1f68528
treef99b515330cb441183d1873343ef542337952b55
parent65a0e14e4f1dee68906b4a380cdb1d8517fd88ea

std.Progress: Use Windows console API calls when ANSI escape codes are not supported


2 files changed, 188 insertions(+), 18 deletions(-)

lib/std/Progress.zig+179-18
......@@ -16,6 +16,8 @@ terminal: ?std.fs.File,
1616/// Is this a windows API terminal (note: this is not the same as being run on windows
1717/// because other terminals exist like MSYS/git-bash)
1818is_windows_terminal: bool,
19/// The output code page of the console (only set if the console is a Windows API terminal)
20console_code_page: if (builtin.os.tag == .windows) windows.UINT else void,
1921
2022/// Whether the terminal supports ANSI escape codes.
2123supports_ansi_escape_codes: bool,
......@@ -297,6 +299,7 @@ pub const Node = struct {
297299var global_progress: Progress = .{
298300 .terminal = null,
299301 .is_windows_terminal = false,
302 .console_code_page = if (builtin.os.tag == .windows) undefined else {},
300303 .supports_ansi_escape_codes = false,
301304 .update_thread = null,
302305 .redraw_event = .{},
......@@ -378,13 +381,15 @@ pub fn start(options: Options) Node {
378381 global_progress.supports_ansi_escape_codes = true;
379382 } else if (builtin.os.tag == .windows and stderr.isTty()) {
380383 global_progress.is_windows_terminal = true;
384 global_progress.console_code_page = windows.kernel32.GetConsoleOutputCP();
381385 global_progress.terminal = stderr;
382386 } else if (builtin.os.tag != .windows) {
383387 // we are in a "dumb" terminal like in acme or writing to a file
384388 global_progress.terminal = stderr;
385389 }
386390
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) {
388393 return .{ .index = .none };
389394 }
390395
......@@ -515,11 +520,90 @@ const save = "\x1b7";
515520const restore = "\x1b8";
516521const finish_sync = "\x1b[?2026l";
517522
518const tree_tee = "\x1B\x28\x30\x74\x71\x1B\x28\x42 "; // ├─
519const tree_line = "\x1B\x28\x30\x78\x1B\x28\x42 "; // │
520const tree_langle = "\x1B\x28\x30\x6d\x71\x1B\x28\x42 "; // └─
523const 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
578fn 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}
521599
522600fn 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
523607 if (global_progress.written_newline_count == 0) return;
524608
525609 var i: usize = 0;
......@@ -558,6 +642,64 @@ fn computeClear(buf: []u8, start_i: usize) usize {
558642 return i;
559643}
560644
645/// U+25BA or ►
646const windows_api_start_marker = 0x25BA;
647
648fn 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
561703const Children = struct {
562704 child: Node.OptionalIndex,
563705 sibling: Node.OptionalIndex,
......@@ -877,17 +1019,35 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) []u8 {
8771019 var i: usize = 0;
8781020 const buf = global_progress.draw_buffer;
8791021
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;
8821025
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 }
8841042
8851043 global_progress.accumulated_newline_count = 0;
8861044 const root_node_index: Node.Index = @enumFromInt(0);
8871045 i = computeNode(buf, i, serialized, children, root_node_index);
8881046
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 }
8911051
8921052 return buf[0..i];
8931053}
......@@ -915,15 +1075,14 @@ fn computePrefix(
9151075 buf[i..][0..prefix.len].* = prefix.*;
9161076 i += prefix.len;
9171077 } 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;
9191079 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);
9221081 }
9231082 return i;
9241083}
9251084
926const line_upper_bound_len = @max(tree_tee.len, tree_langle.len) + "[4294967296/4294967296] ".len +
1085const line_upper_bound_len = @max(TreeSymbol.tee.maxByteLen(), TreeSymbol.langle.maxByteLen()) + "[4294967296/4294967296] ".len +
9271086 Node.max_name_len + finish_sync.len;
9281087
9291088fn computeNode(
......@@ -950,11 +1109,9 @@ fn computeNode(
9501109 break :p;
9511110 }
9521111 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);
9551113 } else {
956 buf[i..][0..tree_tee.len].* = tree_tee.*;
957 i += tree_tee.len;
1114 i = appendTreeSymbol(.tee, buf, i);
9581115 }
9591116 }
9601117
......@@ -1072,7 +1229,11 @@ fn maybeUpdateSize(resize_flag: bool) void {
10721229 global_progress.cols = 80;
10731230 }
10741231
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);
10761237 global_progress.cols = @intCast(info.dwSize.X);
10771238 } else {
10781239 var winsize: posix.winsize = .{
lib/std/os/windows/kernel32.zig+9
......@@ -175,6 +175,15 @@ pub extern "kernel32" fn FillConsoleOutputCharacterW(hConsoleOutput: HANDLE, cCh
175175pub extern "kernel32" fn FillConsoleOutputAttribute(hConsoleOutput: HANDLE, wAttribute: WORD, nLength: DWORD, dwWriteCoord: COORD, lpNumberOfAttrsWritten: *DWORD) callconv(WINAPI) BOOL;
176176pub extern "kernel32" fn SetConsoleCursorPosition(hConsoleOutput: HANDLE, dwCursorPosition: COORD) callconv(WINAPI) BOOL;
177177
178pub extern "kernel32" fn WriteConsoleW(hConsoleOutput: HANDLE, lpBuffer: [*]const u16, nNumberOfCharsToWrite: DWORD, lpNumberOfCharsWritten: ?*DWORD, lpReserved: ?LPVOID) callconv(WINAPI) BOOL;
179pub extern "kernel32" fn ReadConsoleOutputCharacterW(
180 hConsoleOutput: windows.HANDLE,
181 lpCharacter: [*]u16,
182 nLength: windows.DWORD,
183 dwReadCoord: windows.COORD,
184 lpNumberOfCharsRead: *windows.DWORD,
185) callconv(windows.WINAPI) windows.BOOL;
186
178187pub extern "kernel32" fn GetCurrentDirectoryW(nBufferLength: DWORD, lpBuffer: ?[*]WCHAR) callconv(WINAPI) DWORD;
179188
180189pub extern "kernel32" fn GetCurrentThread() callconv(WINAPI) HANDLE;