-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathui.cppm
More file actions
723 lines (635 loc) · 26.3 KB
/
Copy pathui.cppm
File metadata and controls
723 lines (635 loc) · 26.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
// mcpp.ui — verb-style colored status output.
//
// All user-visible status lines from CLI / fetcher / build go through
// here. TTY auto-detect; MCPP_NO_COLOR / --no-color disables colors.
module;
#include <cstdio> // fileno, stdout
export module mcpp.ui;
import std;
import mcpp.platform;
export namespace mcpp::ui {
// One-time initialization. Call once at program start.
void init();
// Force-disable color. Useful for --no-color flag handling.
void disable_color();
// Check if color is enabled.
bool is_color_enabled();
// Verb-style status ("Compiling foo v0.1.0" pattern).
// verb verb word, padded right-aligned in 12-char column
// message metadata after the verb
void status(std::string_view verb, std::string_view message);
// Cyan verb (Updating, Downloading, Cleaned).
void info(std::string_view verb, std::string_view message);
// Bold green Finished line.
// `descriptor` annotates the profile's actual effect (e.g. "optimized",
// "unoptimized + debuginfo"). Empty = print the profile name alone; callers
// that never resolved the profile knobs must not invent one.
void finished(std::string_view profile, std::chrono::milliseconds elapsed,
std::string_view descriptor = {});
// "warning:" / "error:" prefix lines (yellow / red).
void warning(std::string_view message);
void error(std::string_view message);
// Multi-line Rust-style diagnostic (M4 #8.1).
// Renders as:
//
// error[E0001]: <title>
// --> path:line
// |
// <line> | <source line>
// | ^^^^ <span message>
// |
// = note: <note>
// = help: <help>
// = help: see `mcpp --explain E0001` for more details
//
// Empty fields are omitted.
struct Diagnostic {
std::string code; // e.g. "E0001" (optional)
std::string title;
std::filesystem::path path;
std::size_t line = 0;
std::size_t column = 0;
std::string sourceLine; // optional snippet
std::string spanMessage; // points at column
std::vector<std::string> notes;
std::vector<std::string> helps;
};
void diagnostic(const Diagnostic& d);
// Plain output (no verb), respecting -q flag.
void plain(std::string_view message);
// Flush stdout. Every stdout-writing function above already calls this, so
// callers only need it when they wrote to stdout directly (std::println) and
// want that line visible now rather than whenever the libc buffer happens to
// fill. main() also sets stdout line-buffered, which covers the POSIX
// platforms; this is what makes the guarantee hold on Windows too, where
// MSVCRT treats _IOLBF as _IOFBF. Progress-driven output must be visible while
// the process is still running: a build that is killed mid-flight is exactly
// when its last lines matter most.
void flush();
// Make stdout line-buffered. Call once, before any output. See main() for why
// this is not left to the libc default: the default block size is a different
// number on every platform (musl 1024, Apple libc st_blksize = 65536 on a pipe,
// MSVCRT 4096), so identical output becomes visible at wildly different times —
// and not at all if the process is killed before its buffer fills.
void set_line_buffered();
// --- progress bar (single-line, \r-rewritten) ---
class ProgressBar {
public:
ProgressBar(std::string_view verb, std::string_view label);
~ProgressBar();
ProgressBar(const ProgressBar&) = delete;
ProgressBar& operator=(const ProgressBar&) = delete;
// Update progress; renders only once per ~50ms to avoid jitter.
void update(std::size_t percent);
// elapsed_sec, when > 0, drives a `~X.Y MB/s` average-rate suffix.
void update_bytes(std::size_t current_bytes, std::size_t total_bytes,
double elapsed_sec = 0.0);
// Connecting / pre-sizing phase: the total size isn't known yet (the
// downloader reports totalBytes==0 during DNS/TLS/redirect before the
// transfer's Content-Length is available, and some servers stream with no
// length at all). Renders a swept (indeterminate) bar plus a ticking
// `connecting… Ns` / `X.Y MB Ns` suffix so the line never freezes.
void update_indeterminate(std::size_t current_bytes,
double elapsed_sec = 0.0);
// Finish: replaces progress with final-state line.
void finish();
void finish_with(std::string_view final_message);
private:
void render_line(std::size_t percent, const std::string& info_text);
void render_line_swept(std::size_t frame, const std::string& info_text);
std::string verb_;
std::string label_;
std::chrono::steady_clock::time_point lastDraw_;
bool finished_ = false;
};
// --- download progress (centralized) ---
//
// One file's download state, decoded from xlings' NDJSON `download_progress`
// `files[]` entries. A neutral struct so this UI module stays free of any
// fetcher / config dependency (those import each other and would cycle).
struct DownloadFile {
std::string name;
std::size_t downloaded = 0;
std::size_t total = 0; // 0 = size not known yet (connecting)
bool started = false;
bool finished = false;
};
// Centralized renderer for a streaming multi-file download. Owns the
// ProgressBar plus the "which file is active / which are done" bookkeeping,
// and decides per frame whether to draw a percentage bar (size known) or a
// swept/indeterminate bar (still connecting). Absorbs two xlings quirks:
// each file's `finished=true` is reported twice, and `files[]` reshuffles
// between events. Feed it each event's `files[]` snapshot + cumulative
// `elapsedSec`; it is the single place mcpp turns download events into UI.
class DownloadProgress {
public:
DownloadProgress() = default;
~DownloadProgress();
DownloadProgress(const DownloadProgress&) = delete;
DownloadProgress& operator=(const DownloadProgress&) = delete;
void update(std::span<const DownloadFile> files, double elapsed_sec);
void finish(); // finish the active bar if any (idempotent)
private:
std::optional<ProgressBar> bar_;
std::string active_;
std::unordered_set<std::string> finished_;
};
// --- quiet flag (suppresses status / info / finished) ---
void set_quiet(bool q);
bool is_quiet();
// --- path display ---
//
// Path shortening for status output. Long absolute paths under the project
// root, MCPP_HOME, or the user's home directory get rewritten to short
// relative forms so the user can see _what_ rather than _where_.
//
// Substitution rules (most specific wins):
// <project_root>/x/y/z → x/y/z (project-relative)
// <mcpp_home>/x/y/z → @mcpp/x/y/z
// <home>/x/y/z → ~/x/y/z
// anything else → absolute path
//
// `project_root` is optional — leave empty when the caller doesn't have a
// project context (e.g. for `mcpp self env`).
struct PathContext {
std::filesystem::path project_root;
std::filesystem::path mcpp_home;
std::filesystem::path home;
};
std::string shorten_path(const std::filesystem::path& p, const PathContext& ctx);
} // namespace mcpp::ui
namespace mcpp::ui {
namespace {
bool g_color = false;
bool g_quiet = false;
bool g_inited = false;
constexpr std::string_view kReset = "\033[0m";
constexpr std::string_view kBold = "\033[1m";
constexpr std::string_view kGreen = "\033[32m";
constexpr std::string_view kBrightGreen= "\033[92m";
constexpr std::string_view kCyan = "\033[36m";
constexpr std::string_view kBrightCyan = "\033[96m";
constexpr std::string_view kYellow = "\033[33m";
constexpr std::string_view kRed = "\033[31m";
constexpr std::string_view kBrightRed = "\033[91m";
bool detect_color() {
if (auto* e = std::getenv("MCPP_NO_COLOR"); e && *e == '1') return false;
if (auto* e = std::getenv("NO_COLOR"); e && *e) return false;
return mcpp::platform::terminal::is_tty();
}
std::string with_color(std::string_view code, std::string_view text) {
if (!g_color) return std::string(text);
std::string out;
out.reserve(code.size() + text.size() + kReset.size());
out.append(code).append(text).append(kReset);
return out;
}
std::string verb_padded(std::string_view verb) {
constexpr std::size_t W = 12;
if (verb.size() >= W) return std::string(verb);
std::string s(W - verb.size(), ' ');
s.append(verb);
return s;
}
} // namespace
void init() {
if (g_inited) return;
g_color = detect_color();
g_inited = true;
}
void disable_color() { g_color = false; }
bool is_color_enabled() { return g_color; }
void set_quiet(bool q) { g_quiet = q; }
bool is_quiet() { return g_quiet; }
void flush() { std::fflush(stdout); }
void set_line_buffered() {
#if defined(_WIN32)
// Not on Windows, and not as a preference. The UCRT documents setvbuf's
// size as `2 <= size <= INT_MAX`, and a zero goes through the
// invalid-parameter handler, whose default action terminates the process:
// 0xC0000409, which git-bash reports as a bare exit 127. The freshly built
// mcpp.exe died on `--version` before printing anything.
//
// Passing a real size instead would buy nothing: MSVCRT has no line
// buffering at all — it accepts _IOLBF and treats it as _IOFBF. Windows
// gets the same guarantee from ui::flush(), which every stdout-writing
// function here calls, so the platform that cannot do this cheaply is also
// the one that does not need it: its 4096-byte block is the smallest of the
// three anyway, and `mcpp test` routes its result lines through ui::plain.
#else
std::setvbuf(stdout, nullptr, _IOLBF, 0);
#endif
}
void status(std::string_view verb, std::string_view message) {
if (g_quiet) return;
init();
auto v = verb_padded(verb);
if (g_color) {
std::println("{}{}{}{} {}",
kBold, kBrightGreen, v, kReset, message);
} else {
std::println("{} {}", v, message);
}
flush();
}
void info(std::string_view verb, std::string_view message) {
if (g_quiet) return;
init();
auto v = verb_padded(verb);
if (g_color) {
std::println("{}{}{}{} {}",
kBold, kBrightCyan, v, kReset, message);
} else {
std::println("{} {}", v, message);
}
flush();
}
void finished(std::string_view profile, std::chrono::milliseconds elapsed,
std::string_view descriptor) {
if (g_quiet) return;
init();
auto v = verb_padded("Finished");
auto secs = static_cast<double>(elapsed.count()) / 1000.0;
// `[optimized]` used to be hardcoded here alongside a hardcoded "release"
// at the only call site, so every build — including `--dev` at -O0 -g —
// announced "Finished release [optimized]". The descriptor is now supplied
// by whoever actually resolved the profile knobs, and omitted by callers
// (the fast path) that never resolved them: no caller has to guess.
auto msg = descriptor.empty()
? std::format("{} in {:.2f}s", profile, secs)
: std::format("{} [{}] in {:.2f}s", profile, descriptor, secs);
if (g_color) {
std::println("{}{}{}{} {}",
kBold, kBrightGreen, v, kReset, msg);
} else {
std::println("{} {}", v, msg);
}
flush();
}
void warning(std::string_view message) {
init();
if (g_color) {
std::println(stderr, "{}{}warning:{} {}", kBold, kYellow, kReset, message);
} else {
std::println(stderr, "warning: {}", message);
}
}
void error(std::string_view message) {
init();
if (g_color) {
std::println(stderr, "{}{}error:{} {}", kBold, kBrightRed, kReset, message);
} else {
std::println(stderr, "error: {}", message);
}
}
void plain(std::string_view message) {
if (g_quiet) return;
std::println("{}", message);
flush();
}
void diagnostic(const Diagnostic& d) {
init();
auto bold_red = [&](std::string_view s) {
return g_color ? std::format("{}{}{}{}", kBold, kBrightRed, s, kReset)
: std::string(s);
};
auto blue = [&](std::string_view s) {
return g_color ? std::format("{}{}{}{}", kBold, kBrightCyan, s, kReset)
: std::string(s);
};
std::string head = "error";
if (!d.code.empty()) head += "[" + d.code + "]";
head += ":";
std::println(stderr, "{} {}", bold_red(head), d.title);
if (!d.path.empty()) {
if (d.line)
std::println(stderr, " {} {}:{}{}",
blue("-->"), d.path.string(), d.line,
d.column ? std::format(":{}", d.column) : "");
else
std::println(stderr, " {} {}", blue("-->"), d.path.string());
}
if (!d.sourceLine.empty()) {
std::println(stderr, " {}", blue("|"));
std::println(stderr, " {} {} {}",
d.line ? std::format("{:>2}", d.line) : " ", blue("|"), d.sourceLine);
if (!d.spanMessage.empty()) {
std::string caret(d.column ? d.column - 1 : 0, ' ');
caret += "^";
std::println(stderr, " {} {} {}", blue("|"), caret, d.spanMessage);
}
}
if (!d.notes.empty() || !d.helps.empty()) {
std::println(stderr, " {}", blue("|"));
}
for (auto& n : d.notes) {
std::println(stderr, " {} {}: {}", blue("="), blue("note"), n);
}
for (auto& h : d.helps) {
std::println(stderr, " {} {}: {}", blue("="), blue("help"), h);
}
if (!d.code.empty()) {
std::println(stderr, "");
std::println(stderr, "For more information on this error: `mcpp --explain {}`",
d.code);
}
}
// --- ProgressBar ---
namespace {
std::string render_bar(std::size_t percent, std::size_t width = 20) {
auto filled = (percent * width) / 100;
if (filled > width) filled = width;
std::string bar = "[";
for (std::size_t i = 0; i < filled; ++i) bar += "=";
if (filled < width) bar += ">";
for (std::size_t i = filled + 1; i < width; ++i) bar += " ";
bar += "]";
return bar;
}
std::string fmt_bytes(std::size_t b) {
if (b < 1024) return std::format("{} B", b);
if (b < 1024 * 1024) return std::format("{} KB", b / 1024);
if (b < 1024UL*1024*1024) return std::format("{:.1f} MB", static_cast<double>(b) / (1024.0*1024.0));
return std::format("{:.2f} GB", static_cast<double>(b) / (1024.0*1024.0*1024.0));
}
// Best-effort terminal width. Tries TIOCGWINSZ first; on failure (e.g.,
// stdout is a pipe) honours $COLUMNS so users can clamp the width
// manually for testing or when running under CI loggers that don't
// propagate winsize. Falls back to 80 cols.
//
// 80 is the right safe default for a "fixed-shape" status line — we'd
// rather collapse the bar than wrap into a second row that `\r\033[2K`
// can't clean up later.
std::size_t terminal_cols() {
return mcpp::platform::terminal::cols();
}
// Truncate a "visible" string (no ANSI codes inside) to `max` chars, replacing
// the last char with `…` when we cut. Used to keep the progress line under
// terminal width without wrapping into a second row.
std::string trunc_visible(std::string s, std::size_t max) {
if (s.size() <= max) return s;
if (max == 0) return std::string{};
if (max == 1) { s.resize(1); return s; }
s.resize(max - 1);
s += "…"; // 3-byte UTF-8 char in a single visible column — harmless
return s;
}
// Indeterminate ("swept") bar: a fixed-width block bounces back and forth so
// the bar animates while the total size is still unknown. `frame` advances with
// elapsed time; the result includes the `[`/`]` brackets so it drops into the
// same layout budget as render_bar().
std::string render_bar_swept(std::size_t frame, std::size_t width = 20) {
if (width == 0) return "[]";
std::size_t block = std::min<std::size_t>(3, width);
std::size_t span = width - block; // cells the block can travel
std::size_t pos = 0;
if (span > 0) {
std::size_t period = span * 2;
std::size_t p = frame % period;
pos = (p <= span) ? p : (period - p); // ping-pong
}
std::string inner(width, ' ');
for (std::size_t i = 0; i < block && pos + i < width; ++i) inner[pos + i] = '=';
return "[" + inner + "]";
}
// Shared terminal-width budgeting for a one-line status: draws
// <verb-padded-12> <label> <bar> <info>
// shrinking the bar first, then truncating the label, so the result is always
// ≤ cols-1 visible chars. `makeBar(innerWidth)` produces the bar string
// (including its `[`/`]` brackets) for the negotiated inner width.
template <class MakeBar>
void draw_status_line(std::string_view verb, const std::string& label,
MakeBar&& makeBar, const std::string& info_text)
{
constexpr std::size_t kVerbWidth = 12;
constexpr std::size_t kBarMax = 20;
constexpr std::size_t kBarMin = 6;
auto cols = terminal_cols();
if (cols < 30) cols = 30; // pathological — give us a chance
auto budget = cols - 1; // leave one cell for cursor
auto fixed = kVerbWidth + 3 + 2 + info_text.size();
if (fixed >= budget) {
// Truly tiny terminal — drop the bar entirely.
auto labelBudget = budget > kVerbWidth + 1 + info_text.size() + 1
? budget - kVerbWidth - 1 - info_text.size() - 1
: 0;
auto lbl = trunc_visible(label, labelBudget);
if (g_color) {
std::print("\r\033[2K{}{}{}{} {} {}",
kBold, kBrightCyan, verb_padded(verb), kReset,
lbl, info_text);
} else {
std::print("\r\033[2K{} {} {}", verb_padded(verb), lbl, info_text);
}
std::fflush(stdout);
return;
}
auto contentBudget = budget - fixed; // barInner + visible-label-cols
std::size_t barW = std::min(kBarMax, contentBudget);
std::size_t labelMax = contentBudget - barW;
if (barW < kBarMin && labelMax > 0) {
auto steal = std::min(kBarMin - barW, labelMax);
barW += steal;
labelMax -= steal;
}
auto bar = makeBar(barW);
auto lbl = trunc_visible(label, labelMax);
if (g_color) {
std::print("\r\033[2K{}{}{}{} {} {} {}",
kBold, kBrightCyan, verb_padded(verb), kReset,
lbl, bar, info_text);
} else {
std::print("\r\033[2K{} {} {} {}",
verb_padded(verb), lbl, bar, info_text);
}
std::fflush(stdout);
}
} // namespace
ProgressBar::ProgressBar(std::string_view verb, std::string_view label)
: verb_(verb), label_(label),
lastDraw_(std::chrono::steady_clock::now() - std::chrono::seconds(1))
{}
ProgressBar::~ProgressBar() {
if (!finished_) finish();
}
// Render a single progress-bar frame. The verb is drawn separately (with
// optional color) so we can keep ANSI escapes out of the truncation budget.
// `cols` is the available terminal width; `info_text` is the trailing
// "%" / "X MB / Y MB / Z MB/s" suffix; `pct` drives the bar fill.
//
// Layout (visible chars only):
// <verb-padded-12> <label> <bar> <info>
//
// The bar shrinks first when we run out of room, then `label` is truncated
// with an ellipsis. Result is always ≤ cols-1 chars so a `\r\033[2K{...}`
// write never wraps into a second row.
// Layout (visible chars only): <verb-padded-12> <label> <bar> <info>.
// The width budgeting lives in draw_status_line(); this just supplies a
// percentage-fill bar for the negotiated inner width.
void ProgressBar::render_line(std::size_t pct, const std::string& info_text)
{
init();
draw_status_line(verb_, label_,
[pct](std::size_t w) { return render_bar(pct, w); },
info_text);
}
// Same layout, but an animated swept/indeterminate bar (used while the total
// download size is still unknown).
void ProgressBar::render_line_swept(std::size_t frame, const std::string& info_text)
{
init();
draw_status_line(verb_, label_,
[frame](std::size_t w) { return render_bar_swept(frame, w); },
info_text);
}
void ProgressBar::update(std::size_t percent) {
if (g_quiet || finished_) return;
auto now = std::chrono::steady_clock::now();
if (now - lastDraw_ < std::chrono::milliseconds(80) && percent < 100) return;
lastDraw_ = now;
render_line(percent, std::format("{}%", percent));
}
void ProgressBar::update_bytes(std::size_t current, std::size_t total,
double elapsed_sec) {
if (g_quiet || finished_) return;
auto now = std::chrono::steady_clock::now();
auto pct = total ? (current * 100 / total) : 0;
if (pct > 100) pct = 100;
// Same throttle as update(): one render per ~80ms unless we hit 100%.
if (now - lastDraw_ < std::chrono::milliseconds(80) && pct < 100) return;
lastDraw_ = now;
auto info = std::format("{} / {}", fmt_bytes(current), fmt_bytes(total));
// Average rate since the download started. xlings only ships the
// cumulative `elapsedSec`, so this is "since-start" rather than
// a sliding-window instantaneous speed — accurate enough for UX.
if (elapsed_sec > 0.5 && current > 0) {
auto rate = static_cast<std::size_t>(
static_cast<double>(current) / elapsed_sec);
info += std::format(" {}/s", fmt_bytes(rate));
}
render_line(pct, info);
}
void ProgressBar::update_indeterminate(std::size_t current_bytes,
double elapsed_sec) {
if (g_quiet || finished_) return;
auto now = std::chrono::steady_clock::now();
// Same ~80ms throttle as update_bytes(); there is no "100%" early-out here
// because there is no known total.
if (now - lastDraw_ < std::chrono::milliseconds(80)) return;
lastDraw_ = now;
// Before any byte arrives we only have "connecting…"; once the body starts
// streaming (no Content-Length) show the running byte count instead. The
// ticking elapsed-seconds suffix proves the line is alive either way.
std::string info = current_bytes > 0 ? fmt_bytes(current_bytes)
: std::string{"connecting…"};
if (elapsed_sec > 0)
info += std::format(" {:.0f}s", elapsed_sec);
auto frame = static_cast<std::size_t>(elapsed_sec * 6.0);
render_line_swept(frame, info);
}
void ProgressBar::finish() {
if (finished_) return;
finished_ = true;
if (g_quiet) return;
// Clear the line and re-emit as a static info line.
std::print("\r\033[2K");
info(verb_, label_);
}
void ProgressBar::finish_with(std::string_view final_message) {
if (finished_) return;
finished_ = true;
if (g_quiet) return;
std::print("\r\033[2K");
info(verb_, final_message);
}
// --- DownloadProgress ---
DownloadProgress::~DownloadProgress() { finish(); }
void DownloadProgress::finish() {
if (bar_) bar_->finish();
bar_.reset();
active_.clear();
}
void DownloadProgress::update(std::span<const DownloadFile> files,
double elapsed_sec) {
if (files.empty()) return;
// 1. Retire newly-finished entries. Each file's finished=true is reported
// twice (xlings quirk); the `finished_` set dedupes that and the case
// where the same file reappears at a different slot in a later event.
for (auto& f : files) {
if (finished_.contains(f.name)) continue;
if (!f.finished) continue;
if (active_ == f.name) {
if (bar_) bar_->finish();
bar_.reset();
active_.clear();
}
finished_.insert(f.name);
}
// 2. Pick what to display: keep showing `active_` if it's still streaming,
// else the first started+unfinished file. This stops the bar from
// flickering between names when files[] reshuffles across events during
// a multi-package install.
const DownloadFile* current = nullptr;
for (auto& f : files) {
if (f.name == active_ && !f.finished && !finished_.contains(f.name)) {
current = &f;
break;
}
}
if (!current) {
for (auto& f : files) {
if (finished_.contains(f.name)) continue;
if (f.started && !f.finished) { current = &f; break; }
}
}
if (!current) return;
if (current->name != active_) {
if (bar_) bar_->finish();
active_ = current->name;
bar_.emplace("Downloading", current->name);
}
if (current->total > 0) {
bar_->update_bytes(current->downloaded, current->total, elapsed_sec);
} else {
// Size not known yet (connecting / no Content-Length): keep the line
// animated instead of frozen.
bar_->update_indeterminate(current->downloaded, elapsed_sec);
}
}
std::string shorten_path(const std::filesystem::path& p, const PathContext& ctx) {
namespace fs = std::filesystem;
// Use a pure string-prefix comparison rather than fs::relative —
// fs::relative internally canonicalises both arguments, which would
// resolve symlinks. We want to display the path the user thinks they
// are working with (e.g. `<MCPP_HOME>/registry/data/xpkgs/<pkg>` even
// when xpkgs/ is symlinked to a system xlings cache), so we keep
// every comparison purely lexical.
auto can = p.lexically_normal().generic_string();
auto rel_to = [&](const fs::path& base) -> std::optional<std::string> {
if (base.empty()) return std::nullopt;
auto bs = base.lexically_normal().generic_string();
// Strip trailing slashes on the base so "/x/y" and "/x/y/" match
// the same set of candidate paths.
while (!bs.empty() && bs.back() == '/') bs.pop_back();
if (bs.empty()) return std::nullopt;
if (can == bs) return std::string{};
if (can.size() > bs.size()
&& can.compare(0, bs.size(), bs) == 0
&& can[bs.size()] == '/') {
return can.substr(bs.size() + 1);
}
return std::nullopt;
};
if (auto r = rel_to(ctx.project_root); r) {
// Project-relative — print bare ("target/release/foo"), no prefix.
return r->empty() ? std::string{"."} : *r;
}
if (auto r = rel_to(ctx.mcpp_home); r) {
return r->empty() ? std::string{"@mcpp"} : "@mcpp/" + *r;
}
if (auto r = rel_to(ctx.home); r) {
return r->empty() ? std::string{"~"} : "~/" + *r;
}
return can;
}
} // namespace mcpp::ui