FazBrowse GitHub Viewer
|
Trending
|
URL:
|
Home
Tools:
[Download Repo ZIP]
[View Raw Code]
[Original HTTPS Page]
mcpp/src/ui.cppm at main · mcpp-community/mcpp · GitHub
Uh oh!
There was an error while loading.
Please reload this page
.
mcpp-community
/
mcpp
Public
Notifications
You must be signed in to change notification settings
Fork
12
Star
109
Code
Issues
37
Pull requests
8
Actions
Projects
Security and quality
0
Insights
Additional navigation options
Code
Issues
Pull requests
Actions
Projects
Security and quality
Insights
Expand file tree
Breadcrumbs
mcpp
/
src
/
ui.cppm
Copy path
More file actions
More file actions
Latest commit
History
History
History
723 lines (635 loc) · 26.3 KB
Breadcrumbs
mcpp
/
src
/
ui.cppm
Copy path
File metadata and controls
723 lines (635 loc) · 26.3 KB
Raw
Copy raw file
Download raw file
Open symbols panel
Edit and raw actions
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
Back
|
FazBrowse Home
|
New Git URL