authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-07-25 21:49:25-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-07-25 21:49:25-07:00
logfc4b7c968afa6fa0780a011f3c8cfeaea38b7b98
tree8db85e95c48b70a9bd7ba57a360f4e95c6137967
parent66e49d93b7b3815a5213b39d19b3c5f39c21ffc3
parentb22b9ebfe055f1a358447311605be8afa823287a
signaturebadge-check Signed by PGP key B5690EEEBB952194

Merge pull request #24564 from ziglang/terminal-progress-bar

std.Progress: support progress bar escape codes

2 files changed, 75 insertions(+), 3 deletions(-)

lib/compiler/build_runner.zig+6-2
...@@ -696,8 +696,11 @@ fn runStepNames(...@@ -696,8 +696,11 @@ fn runStepNames(
696 .failures, .none => true,696 .failures, .none => true,
697 else => false,697 else => false,
698 };698 };
699 if (failure_count == 0 and failures_only) {699 if (failure_count == 0) {
700 return run.cleanExit();700 std.Progress.setStatus(.success);
701 if (failures_only) return run.cleanExit();
702 } else {
703 std.Progress.setStatus(.failure);
701 }704 }
702705
703 const ttyconf = run.ttyconf;706 const ttyconf = run.ttyconf;
...@@ -1149,6 +1152,7 @@ fn workerMakeOneStep(...@@ -1149,6 +1152,7 @@ fn workerMakeOneStep(
1149 } else |err| switch (err) {1152 } else |err| switch (err) {
1150 error.MakeFailed => {1153 error.MakeFailed => {
1151 @atomicStore(Step.State, &s.state, .failure, .seq_cst);1154 @atomicStore(Step.State, &s.state, .failure, .seq_cst);
1155 std.Progress.setStatus(.failure_working);
1152 break :handle_result;1156 break :handle_result;
1153 },1157 },
1154 error.MakeSkipped => @atomicStore(Step.State, &s.state, .skipped, .seq_cst),1158 error.MakeSkipped => @atomicStore(Step.State, &s.state, .skipped, .seq_cst),
lib/std/Progress.zig+69-1
...@@ -25,6 +25,7 @@ redraw_event: std.Thread.ResetEvent,...@@ -25,6 +25,7 @@ redraw_event: std.Thread.ResetEvent,
25/// Accessed atomically.25/// Accessed atomically.
26done: bool,26done: bool,
27need_clear: bool,27need_clear: bool,
28status: Status,
2829
29refresh_rate_ns: u64,30refresh_rate_ns: u64,
30initial_delay_ns: u64,31initial_delay_ns: u64,
...@@ -47,6 +48,22 @@ node_freelist: Freelist,...@@ -47,6 +48,22 @@ node_freelist: Freelist,
47/// value may at times temporarily exceed the node count.48/// value may at times temporarily exceed the node count.
48node_end_index: u32,49node_end_index: u32,
4950
51pub const Status = enum {
52 /// Indicates the application is progressing towards completion of a task.
53 /// Unless the application is interactive, this is the only status the
54 /// program will ever have!
55 working,
56 /// The application has completed an operation, and is now waiting for user
57 /// input rather than calling exit(0).
58 success,
59 /// The application encountered an error, and is now waiting for user input
60 /// rather than calling exit(1).
61 failure,
62 /// The application encountered at least one error, but is still working on
63 /// more tasks.
64 failure_working,
65};
66
50const Freelist = packed struct(u32) {67const Freelist = packed struct(u32) {
51 head: Node.OptionalIndex,68 head: Node.OptionalIndex,
52 /// Whenever `node_freelist` is added to, this generation is incremented69 /// Whenever `node_freelist` is added to, this generation is incremented
...@@ -383,6 +400,7 @@ var global_progress: Progress = .{...@@ -383,6 +400,7 @@ var global_progress: Progress = .{
383 .draw_buffer = undefined,400 .draw_buffer = undefined,
384 .done = false,401 .done = false,
385 .need_clear = false,402 .need_clear = false,
403 .status = .working,
386404
387 .node_parents = &node_parents_buffer,405 .node_parents = &node_parents_buffer,
388 .node_storage = &node_storage_buffer,406 .node_storage = &node_storage_buffer,
...@@ -498,6 +516,11 @@ pub fn start(options: Options) Node {...@@ -498,6 +516,11 @@ pub fn start(options: Options) Node {
498 return root_node;516 return root_node;
499}517}
500518
519pub fn setStatus(new_status: Status) void {
520 if (noop_impl) return;
521 @atomicStore(Status, &global_progress.status, new_status, .monotonic);
522}
523
501/// Returns whether a resize is needed to learn the terminal size.524/// Returns whether a resize is needed to learn the terminal size.
502fn wait(timeout_ns: u64) bool {525fn wait(timeout_ns: u64) bool {
503 const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_|526 const resize_flag = if (global_progress.redraw_event.timedWait(timeout_ns)) |_|
...@@ -678,6 +701,14 @@ const save = "\x1b7";...@@ -678,6 +701,14 @@ const save = "\x1b7";
678const restore = "\x1b8";701const restore = "\x1b8";
679const finish_sync = "\x1b[?2026l";702const finish_sync = "\x1b[?2026l";
680703
704const progress_remove = "\x1b]9;4;0\x07";
705const @"progress_normal {d}" = "\x1b]9;4;1;{d}\x07";
706const @"progress_error {d}" = "\x1b]9;4;2;{d}\x07";
707const progress_pulsing = "\x1b]9;4;3\x07";
708const progress_pulsing_error = "\x1b]9;4;2\x07";
709const progress_normal_100 = "\x1b]9;4;1;100\x07";
710const progress_error_100 = "\x1b]9;4;2;100\x07";
711
681const TreeSymbol = enum {712const TreeSymbol = enum {
682 /// ├─713 /// ├─
683 tee,714 tee,
...@@ -760,7 +791,7 @@ fn clearWrittenWithEscapeCodes() anyerror!void {...@@ -760,7 +791,7 @@ fn clearWrittenWithEscapeCodes() anyerror!void {
760 if (noop_impl or !global_progress.need_clear) return;791 if (noop_impl or !global_progress.need_clear) return;
761792
762 global_progress.need_clear = false;793 global_progress.need_clear = false;
763 try write(clear);794 try write(clear ++ progress_remove);
764}795}
765796
766/// U+25BA or ►797/// U+25BA or ►
...@@ -1203,6 +1234,43 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } {...@@ -1203,6 +1234,43 @@ fn computeRedraw(serialized_buffer: *Serialized.Buffer) struct { []u8, usize } {
1203 i, const nl_n = computeNode(buf, i, 0, serialized, children, root_node_index);1234 i, const nl_n = computeNode(buf, i, 0, serialized, children, root_node_index);
12041235
1205 if (global_progress.terminal_mode == .ansi_escape_codes) {1236 if (global_progress.terminal_mode == .ansi_escape_codes) {
1237 {
1238 // Set progress state https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
1239 const root_storage = &serialized.storage[0];
1240 const storage = if (root_storage.name[0] != 0 or children[0].child == .none) root_storage else &serialized.storage[@intFromEnum(children[0].child)];
1241 const estimated_total = storage.estimated_total_count;
1242 const completed_items = storage.completed_count;
1243 const status = @atomicLoad(Status, &global_progress.status, .monotonic);
1244 switch (status) {
1245 .working => {
1246 if (estimated_total == 0) {
1247 buf[i..][0..progress_pulsing.len].* = progress_pulsing.*;
1248 i += progress_pulsing.len;
1249 } else {
1250 const percent = completed_items * 100 / estimated_total;
1251 i += (std.fmt.bufPrint(buf[i..], @"progress_normal {d}", .{percent}) catch &.{}).len;
1252 }
1253 },
1254 .success => {
1255 buf[i..][0..progress_remove.len].* = progress_remove.*;
1256 i += progress_remove.len;
1257 },
1258 .failure => {
1259 buf[i..][0..progress_error_100.len].* = progress_error_100.*;
1260 i += progress_error_100.len;
1261 },
1262 .failure_working => {
1263 if (estimated_total == 0) {
1264 buf[i..][0..progress_pulsing_error.len].* = progress_pulsing_error.*;
1265 i += progress_pulsing_error.len;
1266 } else {
1267 const percent = completed_items * 100 / estimated_total;
1268 i += (std.fmt.bufPrint(buf[i..], @"progress_error {d}", .{percent}) catch &.{}).len;
1269 }
1270 },
1271 }
1272 }
1273
1206 if (nl_n > 0) {1274 if (nl_n > 0) {
1207 buf[i] = '\r';1275 buf[i] = '\r';
1208 i += 1;1276 i += 1;