|
|
@@ -1,14 +1,28 @@
|
|
|
namespace Usm.Cli {
|
|
|
|
|
|
/**
|
|
|
- * Single-line, in-place progress bar for a running
|
|
|
- * {@link Usm.Transaction}: the bar redraws on the bottom line while
|
|
|
- * completed packages scroll above it, coloured per
|
|
|
+ * Two-line terminal progress display for a running
|
|
|
+ * {@link Usm.Transaction}. The bottom line is a bar for the ENTIRE
|
|
|
+ * transaction — `(task + task progress) / total tasks` taken from
|
|
|
+ * {@link Usm.Transaction.progress_updated} — and the line above it
|
|
|
+ * carries the current action with its own percentage. When a package
|
|
|
+ * finishes, its action line is erased and replaced by the completion
|
|
|
+ * log, which scrolls up as the next action takes the slot:
|
|
|
+ *
|
|
|
+ * ```
|
|
|
+ * ✓ falpha-1.0.0 installed
|
|
|
+ * Building fgamma-1.0.0 (67%)
|
|
|
+ * [████████████████░░░░░░░░░░░░░░░░░░░] 42%
|
|
|
+ * ```
|
|
|
+ *
|
|
|
+ * The bar's cell count is picked once, at construction, from the
|
|
|
+ * terminal width and never changes for the bar's lifetime (a resize
|
|
|
+ * mid-transaction is not re-fetched). Colours follow the current
|
|
|
* {@link Usm.TransactionTask} (green unpack/install, blue build,
|
|
|
* magenta test, yellow remove/rebuild/clean-up, cyan strategy, red
|
|
|
- * failure). When stderr is not a terminal the bar degrades to plain
|
|
|
- * `[42%] Installing invercargill (2/5)` lines on 10% steps with no
|
|
|
- * ANSI codes.
|
|
|
+ * failure). When stderr is not a terminal the display degrades to
|
|
|
+ * plain `[42%] Installing invercargill (2/5)` lines on 10% steps
|
|
|
+ * with no ANSI codes.
|
|
|
*
|
|
|
* Connect {@link on_transaction_progress} to
|
|
|
* {@link Usm.Transaction.progress_updated}:
|
|
|
@@ -37,10 +51,14 @@ namespace Usm.Cli {
|
|
|
private bool interactive;
|
|
|
private int width;
|
|
|
|
|
|
+ /** Bar cell count, fixed for the bar's lifetime so the bar never grows or shrinks. */
|
|
|
+ private int bar_capacity;
|
|
|
+
|
|
|
private string bar_filled_char;
|
|
|
private string bar_empty_char;
|
|
|
private string success_mark;
|
|
|
private string failure_mark;
|
|
|
+ private bool unicode;
|
|
|
|
|
|
private string? current_package;
|
|
|
private TransactionTask current_task;
|
|
|
@@ -48,11 +66,24 @@ namespace Usm.Cli {
|
|
|
private int current_index;
|
|
|
private int current_total;
|
|
|
|
|
|
- /** A package's completion line fires exactly once, at CLEANING_UP's final 1.0 report. */
|
|
|
+ /** A package's completion log fires exactly once, at CLEANING_UP's final 1.0 report. */
|
|
|
private bool completion_printed;
|
|
|
private bool current_was_removal;
|
|
|
private bool current_was_rebuild;
|
|
|
|
|
|
+ /**
|
|
|
+ * Whole-transaction fraction: the running maximum of
|
|
|
+ * `(task + task progress) / total tasks`. Task weighting is the
|
|
|
+ * only scale the signal offers (removals weigh 3 tasks,
|
|
|
+ * installs 5), and the maximum keeps the bar monotonic through
|
|
|
+ * build-cache clean retries that restart a task's progress.
|
|
|
+ */
|
|
|
+ private float overall = 0.0f;
|
|
|
+
|
|
|
+ /** Managed-region state: the bar row exists once drawn; the action row above it comes and goes. */
|
|
|
+ private bool bar_started;
|
|
|
+ private bool action_live;
|
|
|
+
|
|
|
private string plain_key = "";
|
|
|
private int plain_decile = -1;
|
|
|
|
|
|
@@ -63,12 +94,13 @@ namespace Usm.Cli {
|
|
|
Intl.setlocale(LocaleCategory.ALL, "");
|
|
|
interactive = Posix.isatty(Posix.STDERR_FILENO);
|
|
|
width = detect_terminal_width();
|
|
|
+ bar_capacity = int.max(10, int.min(40, width - 40));
|
|
|
unowned string charset;
|
|
|
- var utf8 = get_charset(out charset);
|
|
|
- bar_filled_char = utf8 ? "█" : "#";
|
|
|
- bar_empty_char = utf8 ? "░" : "-";
|
|
|
- success_mark = utf8 ? "✓" : "+";
|
|
|
- failure_mark = utf8 ? "✗" : "x";
|
|
|
+ unicode = get_charset(out charset);
|
|
|
+ bar_filled_char = unicode ? "█" : "#";
|
|
|
+ bar_empty_char = unicode ? "░" : "-";
|
|
|
+ success_mark = unicode ? "✓" : "+";
|
|
|
+ failure_mark = unicode ? "✗" : "x";
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -80,15 +112,13 @@ namespace Usm.Cli {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Records one progress report and redraws the bar in place. A
|
|
|
- * package whose CLEANING_UP reaches 100% — its last report — gets
|
|
|
- * a completion line printed above the bar first (see
|
|
|
+ * Records one progress report and redraws the two-line display
|
|
|
+ * with the cursor left at the end of the bar row. A package whose
|
|
|
+ * CLEANING_UP reaches 100% — its last report — instead has its
|
|
|
+ * action line erased and replaced by the completion log (see
|
|
|
* {@link complete_line}).
|
|
|
*/
|
|
|
public void update(TransactionTask task, float progress, string? package_name, int index, int total) {
|
|
|
- // A package's CLEANING_UP reaching 100% is its final report;
|
|
|
- // the completion line prints above the bar — in plain mode
|
|
|
- // after its own 100% line — so items scroll as they finish
|
|
|
var completes_now = task == TransactionTask.CLEANING_UP && progress >= 1.0f
|
|
|
&& package_name != null && package_name == current_package && !completion_printed;
|
|
|
var outcome = current_was_removal ? TransactionTask.REMOVING
|
|
|
@@ -111,21 +141,20 @@ namespace Usm.Cli {
|
|
|
current_progress = progress < 0.0f ? 0.0f : progress > 1.0f ? 1.0f : progress;
|
|
|
current_index = index;
|
|
|
current_total = total;
|
|
|
+ var reported = current_total > 0 ? ((float)current_index + current_progress) / (float)current_total : 0.0f;
|
|
|
+ overall = reported > overall ? reported : overall;
|
|
|
|
|
|
if(completes_now) {
|
|
|
if(!interactive) {
|
|
|
print_plain();
|
|
|
}
|
|
|
- print_completion(package_name, outcome, true, false);
|
|
|
+ print_completion(package_name, outcome, true);
|
|
|
completion_printed = true;
|
|
|
- if(interactive) {
|
|
|
- redraw();
|
|
|
- }
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if(interactive) {
|
|
|
- redraw();
|
|
|
+ draw_region();
|
|
|
}
|
|
|
else {
|
|
|
print_plain();
|
|
|
@@ -133,78 +162,131 @@ namespace Usm.Cli {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Prints a completion summary line above the bar —
|
|
|
- * `✓ invercargill-1.1.0 installed` in the task colour, or
|
|
|
- * `✗ invercargill-1.1.0 failed` in red — then redraws the bar.
|
|
|
+ * Replaces the current action line with a completion log —
|
|
|
+ * `✓ falpha-1.0.0 installed` in the task colour, or
|
|
|
+ * `✗ falpha-1.0.0 failed` in red — which scrolls up as history,
|
|
|
+ * then redraws the bar underneath it.
|
|
|
*/
|
|
|
public void complete_line(string package_name, TransactionTask task, bool success) {
|
|
|
- print_completion(package_name, task, success, true);
|
|
|
+ print_completion(package_name, task, success);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Marks the in-flight package failed (a thrown
|
|
|
- * {@link Usm.TransactionError} aborts the run): erases the bar
|
|
|
- * and prints `✗ <package> failed` in red — no redraw, the
|
|
|
- * transaction is over. Harmless when nothing was drawn yet.
|
|
|
+ * {@link Usm.TransactionError} aborts the run): erases the whole
|
|
|
+ * managed region and prints `✗ <package> failed` in red — no
|
|
|
+ * redraw, the transaction is over. Harmless when nothing was
|
|
|
+ * drawn yet.
|
|
|
*/
|
|
|
public void fail() {
|
|
|
- if(interactive) {
|
|
|
- stderr.printf("\r\x1b[K");
|
|
|
- }
|
|
|
- if(current_package == null || completion_printed) {
|
|
|
+ if(!interactive) {
|
|
|
+ if(current_package != null && !completion_printed) {
|
|
|
+ completion_printed = true;
|
|
|
+ stderr.printf("%s %s failed\n", failure_mark, current_package);
|
|
|
+ }
|
|
|
return;
|
|
|
}
|
|
|
- completion_printed = true;
|
|
|
- if(interactive) {
|
|
|
- stderr.printf("%s%s %s failed%s\n", COLOUR_RED, failure_mark, current_package, COLOUR_RESET);
|
|
|
+
|
|
|
+ if(bar_started) {
|
|
|
+ collapse_region();
|
|
|
}
|
|
|
- else {
|
|
|
- stderr.printf("%s %s failed\n", failure_mark, current_package);
|
|
|
+ if(current_package != null && !completion_printed) {
|
|
|
+ completion_printed = true;
|
|
|
+ stderr.printf("%s%s %s failed%s\r\n", COLOUR_RED, failure_mark, current_package, COLOUR_RESET);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Erases the bar line, leaving the cursor at column zero of an
|
|
|
- * empty line — the effect of a closing newline without a stray
|
|
|
- * blank line when completion lines were printed last.
|
|
|
+ * Erases the managed region and prints the closing summary line,
|
|
|
+ * leaving the cursor on a fresh row for the caller's own totals.
|
|
|
*/
|
|
|
public void finish() {
|
|
|
- if(interactive) {
|
|
|
- stderr.printf("\r\x1b[K");
|
|
|
+ if(!interactive || !bar_started) {
|
|
|
+ return;
|
|
|
}
|
|
|
+ collapse_region();
|
|
|
+ stderr.printf("%s%s Transaction completed%s\r\n", COLOUR_GREEN, success_mark, COLOUR_RESET);
|
|
|
}
|
|
|
|
|
|
- private void print_completion(string package_name, TransactionTask task, bool success, bool redraw_bar) {
|
|
|
+ /**
|
|
|
+ * Redraws both managed rows. The cursor starts (and ends) on the
|
|
|
+ * bar row; the action row is one `\033[1A` above it:
|
|
|
+ * up, erase, action; down, erase, bar. When the action row was
|
|
|
+ * consumed by a completion log, `\r\n` first opens a fresh row
|
|
|
+ * below the bar so the bar can move onto it while the action
|
|
|
+ * text takes the old bar row.
|
|
|
+ */
|
|
|
+ private void draw_region() {
|
|
|
+ var action = colourised_action_line();
|
|
|
+ var bar = bar_line();
|
|
|
+ if(!bar_started) {
|
|
|
+ stderr.printf("%s\r\n\033[K%s", action, bar);
|
|
|
+ bar_started = true;
|
|
|
+ action_live = true;
|
|
|
+ }
|
|
|
+ else if(!action_live) {
|
|
|
+ stderr.printf("\r\n\033[1A\033[K%s\r\033[1B\033[K%s", action, bar);
|
|
|
+ action_live = true;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ stderr.printf("\r\033[1A\033[K%s\r\033[1B\033[K%s", action, bar);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void print_completion(string package_name, TransactionTask task, bool success) {
|
|
|
var outcome = success ? past_tense(task) : "failed";
|
|
|
var mark = success ? success_mark : failure_mark;
|
|
|
- if(interactive) {
|
|
|
- stderr.printf("\r\x1b[K%s%s %s %s%s\n", success ? colour_for_task(task) : COLOUR_RED, mark, package_name, outcome, COLOUR_RESET);
|
|
|
- if(redraw_bar) {
|
|
|
- redraw();
|
|
|
- }
|
|
|
+ var colour = success ? colour_for_task(task) : COLOUR_RED;
|
|
|
+ var log = @"$colour$(fit(@"$mark $package_name $outcome"))$COLOUR_RESET";
|
|
|
+
|
|
|
+ if(!interactive) {
|
|
|
+ stderr.printf("%s %s %s\n", mark, package_name, outcome);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // The log takes the row the action line (or, without one, the
|
|
|
+ // bar itself) occupied; the bar redraws below it, so the log
|
|
|
+ // stays above the region as scrolling history
|
|
|
+ if(action_live) {
|
|
|
+ stderr.printf("\r\033[1A\033[K%s\r\n\033[K%s", log, bar_line());
|
|
|
+ action_live = false;
|
|
|
+ }
|
|
|
+ else if(bar_started) {
|
|
|
+ stderr.printf("\r\n\033[1A\033[K%s\r\n\033[K%s", log, bar_line());
|
|
|
}
|
|
|
else {
|
|
|
- stderr.printf("%s %s %s\n", mark, package_name, outcome);
|
|
|
+ stderr.printf("%s\r\n", log);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private void redraw() {
|
|
|
- var label = label_for(current_task, current_package);
|
|
|
+ /** Erases the bar row and, when live, the action row above it, leaving the cursor at the action row's column zero. */
|
|
|
+ private void collapse_region() {
|
|
|
+ stderr.printf("\r\033[K");
|
|
|
+ if(action_live) {
|
|
|
+ stderr.printf("\r\033[1A\033[K");
|
|
|
+ action_live = false;
|
|
|
+ }
|
|
|
+ bar_started = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ private string colourised_action_line() {
|
|
|
+ var colour = colour_for_task(current_task);
|
|
|
var percent = (int)(current_progress * 100);
|
|
|
- var counts = current_total > 0 ? @" ($(current_index + 1)/$current_total)" : "";
|
|
|
- var tail = @" $percent% $label$counts";
|
|
|
- var capacity = int.max(10, int.min(width - 40, width - tail.length - 2));
|
|
|
+ return @"$colour $(fit(@"$(label_for(current_task, current_package)) ($percent%)"))$COLOUR_RESET";
|
|
|
+ }
|
|
|
|
|
|
- var filled = (int)(current_progress * capacity);
|
|
|
- var bar = new StringBuilder();
|
|
|
+ private string bar_line() {
|
|
|
+ var colour = colour_for_task(current_task);
|
|
|
+ var percent = (int)(overall * 100);
|
|
|
+ var filled = (int)(overall * bar_capacity);
|
|
|
+ var cells = new StringBuilder();
|
|
|
for(var i = 0; i < filled; i++) {
|
|
|
- bar.append(bar_filled_char);
|
|
|
+ cells.append(bar_filled_char);
|
|
|
}
|
|
|
- for(var i = filled; i < capacity; i++) {
|
|
|
- bar.append(bar_empty_char);
|
|
|
+ for(var i = filled; i < bar_capacity; i++) {
|
|
|
+ cells.append(bar_empty_char);
|
|
|
}
|
|
|
-
|
|
|
- stderr.printf("\r\x1b[K%s[%s]%s%s", colour_for_task(current_task), bar.str, tail, COLOUR_RESET);
|
|
|
+ return @"$colour[$(cells.str)] $percent%$COLOUR_RESET";
|
|
|
}
|
|
|
|
|
|
private void print_plain() {
|
|
|
@@ -220,6 +302,20 @@ namespace Usm.Cli {
|
|
|
stderr.printf("[%d%%] %s%s\n", (int)(current_progress * 100), label_for(current_task, current_package), counts);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Truncates {@link text} to one terminal row — a wrapped action
|
|
|
+ * or log line would break the two-line geometry. Applied to
|
|
|
+ * plain text only, before colour wrapping, so the budget counts
|
|
|
+ * visible characters and never cuts inside an escape sequence.
|
|
|
+ */
|
|
|
+ private string fit(string text) {
|
|
|
+ var limit = int.max(8, width - 1);
|
|
|
+ if(text.char_count() <= limit) {
|
|
|
+ return text;
|
|
|
+ }
|
|
|
+ return text.substring(0, text.index_of_nth_char(limit - 1)) + (unicode ? "…" : "~");
|
|
|
+ }
|
|
|
+
|
|
|
private string label_for(TransactionTask task, string? package_name) {
|
|
|
if(task == TransactionTask.STRATEGISING) {
|
|
|
return "Preparing transaction";
|