-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathui.cppm
More file actions
502 lines (436 loc) · 16.9 KB
/
ui.cppm
File metadata and controls
502 lines (436 loc) · 16.9 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
// 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.
void finished(std::string_view profile, std::chrono::milliseconds elapsed);
// "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);
// --- 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);
// 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);
std::string verb_;
std::string label_;
std::chrono::steady_clock::time_point lastDraw_;
bool finished_ = false;
};
// --- 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 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);
}
}
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);
}
}
void finished(std::string_view profile, std::chrono::milliseconds elapsed) {
if (g_quiet) return;
init();
auto v = verb_padded("Finished");
auto secs = static_cast<double>(elapsed.count()) / 1000.0;
auto msg = std::format("{} [optimized] in {:.2f}s", profile, secs);
if (g_color) {
std::println("{}{}{}{} {}",
kBold, kBrightGreen, v, kReset, msg);
} else {
std::println("{} {}", v, msg);
}
}
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);
}
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;
}
} // 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.
void ProgressBar::render_line(std::size_t pct, const std::string& info_text)
{
init();
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
// Visible layout, accounting for `[…]` bracket chars on the bar:
// <verb-padded-12><space><label><space>[<bar-inner>]<space><info>
//
// Fixed cost = verbWidth + 3 spaces + 2 brackets + info.
// Whatever's left in `contentBudget` is split between bar-inner and label.
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) {
// Steal from label to keep at least a tiny bar.
auto steal = std::min(kBarMin - barW, labelMax);
barW += steal;
labelMax -= steal;
}
auto bar = render_bar(pct, 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);
}
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::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);
}
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