Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/cli/install.zig
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,9 @@ fn executeWithOpts(
// Set up multi-progress: assign line indices and create coordinator
const download_index = assignDownloadLineIndices(all_jobs.items);
var multi = progress_mod.MultiProgress.init(download_index);
// Anchor the DECSET pair to scope exit so an early return between
// here and the worker join doesn't leave the terminal in DECRESET.
defer multi.finish();

// Main-thread bar allocation — draw an initial frame on every line
// before workers spawn, and keep pointers stable for them.
Expand Down Expand Up @@ -576,7 +579,6 @@ fn executeWithOpts(
}

for (threads.items) |t| t.join();
multi.finish();
}

// Emit from the main thread so ndjson order is deterministic
Expand Down
25 changes: 19 additions & 6 deletions src/main.zig
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,27 @@

const std = @import("std");
const color_mod = @import("ui/color.zig");
const progress_mod = @import("ui/progress.zig");
const AppCtx = @import("app_ctx.zig").AppCtx;

// Release uses simple_panic so debug.Dwarf stays unreachable (~30 KB smaller).
pub const panic = if (@import("builtin").mode == .Debug)
std.debug.FullPanic(std.debug.defaultPanic)
else
std.debug.simple_panic;
// Wrap the panic path so the cursor + autowrap state owned by
// MultiProgress / Spinner is restored before abort. Defers don't run
// on panic, so this is the only way to leave the terminal usable when
// install/migrate trips a safety check.
pub const panic = std.debug.FullPanic(maltPanic);

fn maltPanic(msg: []const u8, first_trace_addr: ?usize) noreturn {
progress_mod.restoreTerminal();
if (@import("builtin").mode == .Debug) {
std.debug.defaultPanic(msg, first_trace_addr);
} else {
// Release mirror of std.debug.simple_panic.call: emit the
// message and trap, keeping debug.Dwarf out of the binary.
const stderr_writer = &std.debug.lockStderr(&.{}).file_writer.interface;
stderr_writer.writeAll(msg) catch {};
@trap();
}
}

// Gate `.debug` on the runtime --debug flag so release builds still
// surface std.log.debug diagnostics in bug reports.
Expand Down Expand Up @@ -343,7 +357,6 @@ pub fn main(init: std.process.Init.Minimal) !void {
// Seed the ui package state once so output/progress/color stop
// pulling io/environ/stdio from module-level globals.
const output_mod = @import("ui/output.zig");
const progress_mod = @import("ui/progress.zig");
output_mod.setRuntime(ctx.io, ctx.environ, ctx.stdout, ctx.stderr);
progress_mod.setRuntime(ctx.io, ctx.stderr);
color_mod.setRuntime(ctx.io, ctx.environ);
Expand Down
19 changes: 17 additions & 2 deletions src/ui/progress.zig
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,14 @@ fn sleepNs(ns: u64) void {
/// Braille-based spinner frames, shared by ProgressBar and Spinner.
const spinner_chars = [_][]const u8{ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" };

/// Best-effort restore of terminal state mutated by `MultiProgress.init`
/// or `Spinner.start`: re-enable autowrap, show the cursor, return to
/// column 0. Safe to call from a panic / signal handler — the bytes are
/// idempotent, and writes silently drop when stderr is unconfigured.
pub fn restoreTerminal() void {
writeStderrAll("\x1b[?7h\x1b[?25h\r");
}

/// Coordinates multiple progress bars on separate terminal lines.
/// Reserves N lines upfront, then uses ANSI cursor movement so each
/// bar updates its own line without interfering with others.
Expand Down Expand Up @@ -84,8 +92,7 @@ pub const MultiProgress = struct {
/// Must be called after all download threads have joined.
pub fn finish(self: *MultiProgress) void {
if (self.is_tty and !output.isQuiet()) {
// Re-enable autowrap, show cursor, return to col 0.
writeStderrAll("\x1b[?7h\x1b[?25h\r");
restoreTerminal();
}
}
};
Expand Down Expand Up @@ -657,3 +664,11 @@ test "ProgressBar render survives label larger than the draw buffer" {
indet.is_tty = true;
indet.update(0);
}

test "restoreTerminal is callable without an active MultiProgress" {
// Panic / signal handlers may emit the restore sequence without ever
// having paired it to an init: the call must be allocation-free,
// re-entrant, and idempotent.
restoreTerminal();
restoreTerminal();
}
Loading