1/* @title: Console report composer */
2#include <asm.h>
3#include <colors.h>
4#include <compiler/core.h>
5#include <console/printf.h>
6#include <console/report.h>
7#include <console/term.h>
8#include <math/min_max.h>
9#include <sch/irql.h>
10#include <stdarg.h>
11#include <stdbool.h>
12#include <stddef.h>
13#include <stdint.h>
14#include <string.h>
15
16/* Which section is being rendered right now, so a panic arriving mid render
17 * can state which is being faulted and skip on the way through */
18static const char *report_section;
19static const char *report_skip_section;
20
21static struct report_panes panic_panes;
22
23static const char *glyph_hbar(void) {
24 return term_unicode() && !term_plain() ? "─" : "-";
25}
26
27static const char *glyph_vbar(void) {
28 return term_unicode() && !term_plain() ? "│" : "|";
29}
30
31static const char *glyph_ellipsis(void) {
32 return term_unicode() && !term_plain() ? "…" : ">";
33}
34
35static const char *glyph_join(bool left, bool right) {
36 bool uni = term_unicode() && !term_plain();
37
38 if (left && right)
39 return uni ? "┼" : "+";
40 if (left)
41 return uni ? "┤" : "+";
42 if (right)
43 return uni ? "├" : "+";
44
45 return uni ? "│" : "|";
46}
47
48static const char *glyph_tee_up(void) {
49 return term_unicode() && !term_plain() ? "┴" : "+";
50}
51
52/* A divider meets a horizontal rule */
53static const char *glyph_tee(bool above, bool below) {
54 bool uni = term_unicode() && !term_plain();
55
56 if (above && below)
57 return uni ? "┼" : "+";
58 if (above)
59 return uni ? "┴" : "+";
60 if (below)
61 return uni ? "┬" : "+";
62
63 return uni ? "─" : "-";
64}
65
66static const char *glyph_continue(void) {
67 return term_unicode() && !term_plain() ? "↩" : "\\";
68}
69
70void report_line_init(struct report_line *l, char *buf, size_t cap,
71 size_t max_col) {
72 l->buf = buf;
73 l->cap = cap;
74 l->max_col = max_col;
75
76 if (buf && cap)
77 buf[0] = '\0';
78
79 report_line_reset(l);
80}
81
82void report_line_reset(struct report_line *l) {
83 l->len = 0;
84 l->col = 0;
85 l->glyph_at = 0;
86 l->full = false;
87 l->truncated = false;
88
89 if (l->buf && l->cap)
90 l->buf[0] = '\0';
91}
92
93/* Callers hand one escape sequence or a whole glyph at a time,
94 * so refusing partial writes keep byte caps from landing in the middle...
95 *
96 * Half a UTF-8 glyph prints as a [?] and half a CSI eats the next
97 * thing the terminal reads */
98static bool line_raw(struct report_line *l, const char *s, size_t n) {
99 if (!l->buf || l->cap == 0 || l->len + n >= l->cap)
100 return false;
101
102 memcpy(l->buf + l->len, s, n);
103 l->len += n;
104 l->buf[l->len] = '\0';
105 return true;
106}
107
108static void line_mark_full(struct report_line *l) {
109 const char *ell = glyph_ellipsis();
110
111 l->full = true;
112 l->truncated = true;
113
114 if (l->col == 0)
115 return;
116
117 l->len = l->glyph_at;
118 l->buf[l->len] = '\0';
119 l->col--;
120
121 if (line_raw(l, s: ell, n: strlen(str: ell)))
122 l->col++;
123}
124
125/* Past the end of an escape sequence starting at `s` */
126static const char *esc_skip(const char *s, const char *end) {
127 s++;
128
129 if (s < end && *s == '[') {
130 s++;
131
132 while (s < end && (*s < '@' || *s > '~'))
133 s++;
134 }
135
136 if (s < end)
137 s++;
138
139 return s;
140}
141
142static void line_puts_n(struct report_line *l, const char *s, const char *end) {
143 if (l->full)
144 return;
145
146 while (s < end) {
147 if (*s == '\033') {
148 const char *start = s;
149
150 s = esc_skip(s, end);
151
152 /* Plain mode strips here, so color format strings need no changes,
153 * and a styling escape that will not fit is dropped */
154 if (!term_plain())
155 line_raw(l, s: start, n: (size_t) (s - start));
156
157 continue;
158 }
159
160 if ((unsigned char) *s < 0x20 || *s == 0x7f) {
161 s++;
162 continue;
163 }
164
165 if (l->col >= l->max_col) {
166 line_mark_full(l);
167 return;
168 }
169
170 /* Lead byte plus continuations */
171 const char *glyph = s++;
172
173 while (s < end && (*s & 0xC0) == 0x80)
174 s++;
175
176 l->glyph_at = l->len;
177
178 if (!line_raw(l, s: glyph, n: (size_t) (s - glyph))) {
179 line_mark_full(l);
180 return;
181 }
182
183 l->col++;
184 }
185}
186
187void report_line_puts(struct report_line *l, const char *s) {
188 if (!s)
189 return;
190
191 line_puts_n(l, s, end: s + strlen(str: s));
192}
193
194void report_line_vprintf(struct report_line *l, const char *fmt, va_list ap) {
195 char buf[REPORT_LINE_MAX];
196
197 if (l->full)
198 return;
199
200 vsnprintf(buf, (int) sizeof(buf), fmt, ap);
201 report_line_puts(l, s: buf);
202}
203
204void report_line_printf(struct report_line *l, const char *fmt, ...) {
205 va_list ap;
206
207 va_start(ap, fmt);
208 report_line_vprintf(l, fmt, ap);
209 va_end(ap);
210}
211
212void report_line_pad_to(struct report_line *l, size_t col) {
213 if (col > l->max_col)
214 col = l->max_col;
215
216 while (l->col < col) {
217 if (!line_raw(l, s: " ", n: 1))
218 break;
219
220 l->col++;
221 }
222}
223
224void report_line_repeat(struct report_line *l, const char *glyph, size_t n) {
225 for (size_t i = 0; i < n && !l->full; i++)
226 report_line_puts(l, s: glyph);
227}
228
229void report_line_field(struct report_line *l, const char *s, size_t width) {
230 if (l->full)
231 return;
232
233 size_t outer = l->max_col;
234 size_t end = l->col + width;
235
236 if (end > outer)
237 end = outer;
238
239 l->max_col = end;
240 report_line_puts(l, s);
241 l->full = false;
242 report_line_pad_to(l, col: end);
243
244 l->max_col = outer;
245 l->full = l->col >= outer;
246}
247
248void report_line_right(struct report_line *l, const char *s) {
249 size_t w = report_strwidth(s);
250
251 if (l->col + w < l->max_col)
252 report_line_pad_to(l, col: l->max_col - w);
253
254 report_line_puts(l, s);
255}
256
257const char *report_line_str(const struct report_line *l) {
258 return l->buf ? l->buf : "";
259}
260
261size_t report_line_width(const struct report_line *l) {
262 return l->col;
263}
264
265bool report_line_truncated(const struct report_line *l) {
266 return l->truncated;
267}
268
269size_t report_strwidth(const char *s) {
270 size_t w = 0;
271
272 if (!s)
273 return 0;
274
275 while (*s) {
276 if (*s == '\033') {
277 s++;
278
279 if (*s == '[') {
280 s++;
281 while (*s && (*s < '@' || *s > '~'))
282 s++;
283 }
284
285 if (*s)
286 s++;
287
288 continue;
289 }
290
291 if ((unsigned char) *s < 0x20 || *s == 0x7f) {
292 s++;
293 continue;
294 }
295
296 if ((*s & 0xC0) != 0x80)
297 w++;
298
299 s++;
300 }
301
302 return w;
303}
304
305struct report_guard {
306 enum irql irql;
307 bool locked;
308};
309
310static struct report_guard report_lock(void) TSA_NO_ANALYSIS {
311 struct report_guard g = {.irql = 0, .locked = false};
312
313 if (term_in_panic())
314 return g;
315
316 g.irql = printf_lock();
317 g.locked = true;
318 return g;
319}
320
321static void report_unlock(struct report_guard g) TSA_NO_ANALYSIS {
322 if (g.locked)
323 printf_unlock(i: g.irql);
324}
325
326static void console_line(const char *s, uint8_t indent) {
327 struct report_guard g = report_lock();
328
329 printf_unlocked(format: "%*s", (int) indent, "");
330 printf_unlocked(format: "%s", s);
331
332 if (!term_plain())
333 printf_unlocked(format: "%s", ANSI_RESET);
334
335 printf_unlocked(format: "\n");
336 report_unlock(g);
337}
338
339struct report_target report_console(void) {
340 return (struct report_target){
341 .panes = NULL, .width = term_size().cols, .indent = 0, .pane = 0};
342}
343
344struct report_target report_console_indent(uint16_t indent) {
345 uint16_t cols = term_size().cols;
346
347 if (indent > cols / 2)
348 indent = (uint16_t) (cols / 2);
349
350 return (struct report_target){.panes = NULL,
351 .width = (uint16_t) (cols - indent),
352 .indent = (uint8_t) indent,
353 .pane = 0};
354}
355
356static void pane_push(struct report_panes *panes, uint32_t pane, const char *s,
357 bool rule) {
358 if (pane >= panes->n)
359 return;
360
361 if (panes->nrows[pane] >= REPORT_PANE_ROWS) {
362 panes->dropped[pane]++;
363 return;
364 }
365
366 panes->rule[pane][panes->nrows[pane]] = rule;
367
368 char *dst = panes->text[pane][panes->nrows[pane]++];
369 size_t n = 0;
370
371 /* The line writer bounded this by display columns, but store is bounded by
372 * bytes, and escapes plus padding causes problems, so we advance a whole
373 * sequence or a whole glyph at a time */
374 const char *tail = term_plain() ? "" : ANSI_RESET;
375 size_t cap =
376 REPORT_PANE_LINE_MAX - strlen(str: tail) - strlen(str: glyph_ellipsis()) - 1;
377
378 while (*s) {
379 const char *start = s;
380
381 if (*s == '\033') {
382 s++;
383
384 if (*s == '[') {
385 s++;
386 while (*s && (*s < '@' || *s > '~'))
387 s++;
388 }
389
390 if (*s)
391 s++;
392 } else {
393 s++;
394 while ((*s & 0xC0) == 0x80)
395 s++;
396 }
397
398 size_t len = (size_t) (s - start);
399
400 if (n + len > cap)
401 break;
402
403 memcpy(dst + n, start, len);
404 n += len;
405 }
406
407 /* Not counting this as dropped, because the line is here, and
408 * the marker says where it stops */
409 if (*s)
410 n += (size_t) snprintf(dst + n, (int) (REPORT_PANE_LINE_MAX - n),
411 "%s%s", tail, glyph_ellipsis());
412
413 dst[n] = '\0';
414}
415
416struct report_target report_pane(struct report_panes *panes, uint32_t pane) {
417 uint16_t w = (pane < panes->n) ? panes->width[pane] : term_size().cols;
418
419 return (struct report_target){
420 .panes = panes, .width = w, .indent = 0, .pane = (uint8_t) pane};
421}
422
423static void box_emit_line(struct report_box *b, const char *s);
424
425static void target_emit(struct report_target *tgt, struct report_line *l,
426 bool rule) {
427 if (tgt->box) {
428 box_emit_line(b: tgt->box, s: report_line_str(l));
429 report_line_reset(l);
430 return;
431 }
432
433 if (!tgt->panes) {
434 console_line(s: report_line_str(l), indent: tgt->indent);
435 report_line_reset(l);
436 return;
437 }
438
439 if (tgt->indent) {
440 /* Console path indents as it writes: a column stores the text, so
441 * indent must be baked */
442 char buf[REPORT_PANE_LINE_MAX];
443
444 snprintf(buf, (int) sizeof(buf), "%*s%s", (int) tgt->indent, "",
445 report_line_str(l));
446 pane_push(panes: tgt->panes, pane: tgt->pane, s: buf, rule);
447 } else {
448 pane_push(panes: tgt->panes, pane: tgt->pane, s: report_line_str(l), rule);
449 }
450
451 report_line_reset(l);
452}
453
454struct report_target report_target_indent(struct report_target tgt,
455 uint16_t indent) {
456 if (indent > tgt.width / 2)
457 indent = (uint16_t) (tgt.width / 2);
458
459 tgt.indent = (uint8_t) (tgt.indent + indent);
460 tgt.width = (uint16_t) (tgt.width - indent);
461 return tgt;
462}
463
464void report_line_emit(struct report_target *tgt, struct report_line *l) {
465 target_emit(tgt, l, false);
466}
467
468void report_puts(struct report_target *tgt, const char *s) {
469 REPORT_LINE(l, report_target_width(tgt));
470
471 report_line_puts(l: &l, s);
472 report_line_emit(tgt, l: &l);
473}
474
475void report_printf(struct report_target *tgt, const char *fmt, ...) {
476 REPORT_LINE(l, report_target_width(tgt));
477 va_list ap;
478
479 va_start(ap, fmt);
480 report_line_vprintf(l: &l, fmt, ap);
481 va_end(ap);
482
483 report_line_emit(tgt, l: &l);
484}
485
486void report_blank(struct report_target *tgt) {
487 report_puts(tgt, s: "");
488}
489
490void report_rule_sev(struct report_target *tgt, enum term_sev sev,
491 const char *title) {
492 REPORT_LINE(l, report_target_width(tgt));
493 const char *hbar = glyph_hbar();
494
495 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
496 report_line_repeat(l: &l, glyph: hbar, n: 2);
497
498 if (title && *title) {
499 report_line_puts(l: &l, s: " ");
500 report_line_puts(l: &l, s: term_style(sev));
501 report_line_puts(l: &l, s: title);
502 report_line_puts(l: &l, s: term_style_reset());
503 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
504 report_line_puts(l: &l, s: " ");
505 }
506
507 report_line_repeat(l: &l, glyph: hbar, n: l.max_col - l.col);
508
509 /* Flagged so a column flush knows to grow a stub on the divider */
510 target_emit(tgt, l: &l, true);
511}
512
513void report_rule(struct report_target *tgt, const char *title) {
514 report_rule_sev(tgt, sev: TERM_SEV_LABEL, title);
515}
516
517/* Styling active at a line break, replayed at the start of next line so
518 * bold/colored runs survive being wrapped */
519#define REPORT_WRAP_STYLE_MAX 64
520
521#define REPORT_WRAP_MIN_COLS 8
522
523struct report_wrap {
524 struct report_target *tgt;
525 struct report_line line;
526 char style[REPORT_WRAP_STYLE_MAX];
527 size_t style_len;
528 char storage[REPORT_LINE_MAX];
529};
530
531static void wrap_note_style(struct report_wrap *w, const char *esc, size_t n) {
532 bool reset =
533 (n == 3 && esc[2] == 'm') || (n == 4 && esc[2] == '0' && esc[3] == 'm');
534
535 if (reset) {
536 w->style_len = 0;
537 w->style[0] = '\0';
538 return;
539 }
540
541 /* Drop if it can't fit */
542 if (w->style_len + n >= sizeof(w->style))
543 return;
544
545 memcpy(w->style + w->style_len, esc, n);
546 w->style_len += n;
547 w->style[w->style_len] = '\0';
548}
549
550static void wrap_emit(struct report_wrap *w) {
551 report_line_emit(tgt: w->tgt, l: &w->line);
552
553 if (w->style_len)
554 report_line_puts(l: &w->line, s: w->style);
555}
556
557/* End of the run of non-space starting at `s`, with its display width */
558static const char *wrap_scan(const char *s, size_t *width) {
559 size_t n = 0;
560
561 while (*s && *s != ' ' && *s != '\t' && *s != '\n') {
562 if (*s == '\033') {
563 s = esc_skip(s, end: s + strlen(str: s));
564 continue;
565 }
566
567 s++;
568 while ((*s & 0xC0) == 0x80)
569 s++;
570
571 n++;
572 }
573
574 *width = n;
575 return s;
576}
577
578static size_t wrap_width(const char *s, const char *end) {
579 size_t n = 0;
580
581 while (s < end) {
582 if (*s == '\033') {
583 s = esc_skip(s, end);
584 continue;
585 }
586
587 s++;
588 while (s < end && (*s & 0xC0) == 0x80)
589 s++;
590
591 n++;
592 }
593
594 return n;
595}
596
597/* Advance at most `cols`, don't split glyphs */
598static const char *wrap_take(const char *s, const char *end, size_t cols) {
599 while (s < end && cols) {
600 if (*s == '\033') {
601 s = esc_skip(s, end);
602 continue;
603 }
604
605 s++;
606 while (s < end && (*s & 0xC0) == 0x80)
607 s++;
608
609 cols--;
610 }
611
612 return s;
613}
614
615static void wrap_copy(struct report_wrap *w, const char *s, const char *end) {
616 for (const char *p = s; p < end;) {
617 if (*p != '\033') {
618 p++;
619 continue;
620 }
621
622 const char *esc = p;
623
624 p = esc_skip(s: p, end);
625 wrap_note_style(w, esc, n: (size_t) (p - esc));
626 }
627
628 line_puts_n(l: &w->line, s, end);
629}
630
631void report_wrap(struct report_target *tgt, const char *text) {
632 size_t width = report_target_width(tgt);
633 struct report_wrap w;
634
635 /* Too narrow to break */
636 if (!text || !*text || width < REPORT_WRAP_MIN_COLS) {
637 report_puts(tgt, s: text ? text : "");
638 return;
639 }
640
641 w.tgt = tgt;
642 w.style[0] = '\0';
643 w.style_len = 0;
644 report_line_init(l: &w.line, buf: w.storage, cap: sizeof(w.storage), max_col: width);
645
646 while (*text) {
647 /* Honor newlines */
648 if (*text == '\n') {
649 wrap_emit(w: &w);
650 text++;
651 continue;
652 }
653
654 if (*text == ' ' || *text == '\t') {
655 text++;
656 continue;
657 }
658
659 size_t word;
660 const char *end = wrap_scan(s: text, width: &word);
661
662 /* separating space counts against the budget as well */
663 if (w.line.col && w.line.col + 1 + word > width)
664 wrap_emit(w: &w);
665
666 if (w.line.col)
667 report_line_puts(l: &w.line, s: " ");
668
669 if (w.line.col + word <= width) {
670 wrap_copy(w: &w, s: text, end);
671 text = end;
672 continue;
673 }
674
675 /* Longer than a line on its own, breaking and marking breaks to show
676 * when it's a break vs a sentence that just happened to wrap */
677 while (text < end) {
678 size_t room = width - w.line.col;
679
680 if (wrap_width(s: text, end) <= room) {
681 wrap_copy(w: &w, s: text, end);
682 text = end;
683 break;
684 }
685
686 /* after a flush, `room` is the full width */
687 if (room < 4) {
688 wrap_emit(w: &w);
689 continue;
690 }
691
692 const char *stop = wrap_take(s: text, end, cols: room - 1);
693
694 wrap_copy(w: &w, s: text, end: stop);
695 text = stop;
696
697 report_line_puts(l: &w.line, s: term_style(sev: TERM_SEV_DIM));
698 report_line_puts(l: &w.line, s: glyph_continue());
699 wrap_emit(w: &w);
700 }
701 }
702
703 if (w.line.col)
704 report_line_emit(tgt, l: &w.line);
705}
706
707void report_wrap_printf(struct report_target *tgt, const char *fmt, ...) {
708 char buf[REPORT_LINE_MAX];
709 va_list ap;
710
711 va_start(ap, fmt);
712 vsnprintf(buf, (int) sizeof(buf), fmt, ap);
713 va_end(ap);
714
715 report_wrap(tgt, text: buf);
716}
717
718/* Boxes are borders + one space of padding, so it occupies inner + 4 columns
719 * and the closing corner sits at inner + 3 */
720#define REPORT_BOX_CHROME 4
721
722void report_box_open(struct report_box *b, struct report_target target,
723 const char *title, uint16_t inner) {
724 const char *h = glyph_hbar();
725 uint16_t room = target.width > REPORT_BOX_CHROME
726 ? (uint16_t) (target.width - REPORT_BOX_CHROME)
727 : 1;
728
729 if (!inner || inner > room)
730 inner = room;
731
732 b->target = target;
733 b->inner = inner;
734
735 REPORT_LINE(l, target.width);
736
737 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
738 report_line_puts(l: &l, s: term_unicode() && !term_plain() ? "┌" : "+");
739 report_line_puts(l: &l, s: h);
740
741 if (title && *title) {
742 report_line_puts(l: &l, s: " ");
743 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_LABEL));
744 report_line_puts(l: &l, s: title);
745 report_line_puts(l: &l, s: term_style_reset());
746 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
747 report_line_puts(l: &l, s: " ");
748 }
749
750 /* title longer than the box has eaten the top edge */
751 if (l.col < (size_t) inner + 3)
752 report_line_repeat(l: &l, glyph: h, n: (size_t) inner + 3 - l.col);
753
754 report_line_puts(l: &l, s: term_unicode() && !term_plain() ? "┐" : "+");
755 report_line_emit(tgt: &b->target, l: &l);
756}
757
758/* One already composed line, wrapped in the borders and passed on */
759static void box_emit_line(struct report_box *b, const char *s) {
760 const char *v = glyph_vbar();
761
762 REPORT_LINE(l, b->target.width);
763
764 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
765 report_line_puts(l: &l, s: v);
766 report_line_puts(l: &l, s: term_style_reset());
767 report_line_puts(l: &l, s: " ");
768
769 report_line_field(l: &l, s, width: b->inner);
770
771 report_line_puts(l: &l, s: " ");
772 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
773 report_line_puts(l: &l, s: v);
774 report_line_emit(tgt: &b->target, l: &l);
775}
776
777struct report_target report_box_body(struct report_box *b) {
778 return (struct report_target){.box = b, .width = b->inner};
779}
780
781void report_box_printf(struct report_box *b, const char *fmt, ...) {
782 struct report_target body = report_box_body(b);
783 char buf[REPORT_LINE_MAX];
784 va_list ap;
785
786 va_start(ap, fmt);
787 vsnprintf(buf, (int) sizeof(buf), fmt, ap);
788 va_end(ap);
789
790 /* Wrapping, not clipping: the box grows downwards for free */
791 report_wrap(tgt: &body, text: buf);
792}
793
794void report_box_close(struct report_box *b) {
795 REPORT_LINE(l, b->target.width);
796
797 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
798 report_line_puts(l: &l, s: term_unicode() && !term_plain() ? "└" : "+");
799 report_line_repeat(l: &l, glyph: glyph_hbar(), n: (size_t) b->inner + 2);
800 report_line_puts(l: &l, s: term_unicode() && !term_plain() ? "┘" : "+");
801 report_line_emit(tgt: &b->target, l: &l);
802}
803
804static bool section_claim(struct report_target *tgt, const char *name) {
805 if (report_skip_section && report_skip_section == name) {
806 report_printf(tgt, fmt: "%s[%s skipped: faulted while rendering]%s",
807 term_style(sev: TERM_SEV_WARN), name, term_style_reset());
808 return false;
809 }
810
811 report_section = name;
812 return true;
813}
814
815bool report_section_claim_at(struct report_target *tgt, const char *name) {
816 return section_claim(tgt, name);
817}
818
819bool report_section_claim(const char *name) {
820 struct report_target con = report_console();
821
822 return section_claim(tgt: &con, name);
823}
824
825bool report_section_begin_at(struct report_target *tgt, const char *name) {
826 if (!section_claim(tgt, name))
827 return false;
828
829 report_blank(tgt);
830 report_rule_sev(tgt, sev: TERM_SEV_LABEL, title: name);
831 return true;
832}
833
834bool report_section_begin(const char *name) {
835 struct report_target con = report_console();
836
837 return report_section_begin_at(tgt: &con, name);
838}
839
840void report_section_end(void) {
841 report_section = NULL;
842}
843
844const char *report_section_current(void) {
845 return report_section;
846}
847
848static void fields_flush_row(struct report_fields *g) {
849 if (!g->in_row)
850 return;
851
852 report_line_emit(tgt: &g->target, l: &g->line);
853 g->in_row = 0;
854}
855
856void report_fields_begin(struct report_fields *g, struct report_target target,
857 uint32_t keyw, uint32_t valw) {
858 uint32_t cell = keyw + 1 + valw + REPORT_FIELD_GAP;
859 uint32_t per = cell ? (target.width + REPORT_FIELD_GAP) / cell : 1;
860
861 g->target = target;
862 g->keyw = (uint8_t) keyw;
863 g->valw = (uint8_t) valw;
864 g->per_row = per ? (uint8_t) per : 1;
865 g->in_row = 0;
866
867 report_line_init(l: &g->line, buf: g->storage, cap: sizeof(g->storage), max_col: target.width);
868}
869
870void report_field(struct report_fields *g, const char *key, const char *fmt,
871 ...) {
872 char val[REPORT_PANE_LINE_MAX];
873 va_list ap;
874
875 va_start(ap, fmt);
876 vsnprintf(val, (int) sizeof(val), fmt, ap);
877 va_end(ap);
878
879 if (g->in_row >= g->per_row)
880 fields_flush_row(g);
881
882 if (g->in_row)
883 report_line_repeat(l: &g->line, glyph: " ", REPORT_FIELD_GAP);
884
885 report_line_puts(l: &g->line, s: term_style(sev: TERM_SEV_LABEL));
886 report_line_field(l: &g->line, s: key, width: g->keyw);
887 report_line_puts(l: &g->line, s: term_style_reset());
888 report_line_puts(l: &g->line, s: " ");
889 report_line_field(l: &g->line, s: val, width: g->valw);
890
891 g->in_row++;
892}
893
894void report_field_full(struct report_fields *g, const char *key,
895 const char *fmt, ...) {
896 char val[REPORT_LINE_MAX];
897 va_list ap;
898
899 fields_flush_row(g);
900
901 va_start(ap, fmt);
902 vsnprintf(val, (int) sizeof(val), fmt, ap);
903 va_end(ap);
904
905 report_line_puts(l: &g->line, s: term_style(sev: TERM_SEV_LABEL));
906 report_line_field(l: &g->line, s: key, width: g->keyw);
907 report_line_puts(l: &g->line, s: term_style_reset());
908 report_line_puts(l: &g->line, s: " ");
909 report_line_puts(l: &g->line, s: val);
910 report_line_emit(tgt: &g->target, l: &g->line);
911}
912
913void report_fields_end(struct report_fields *g) {
914 fields_flush_row(g);
915}
916
917uint16_t report_pane_rows(const struct report_panes *panes, uint32_t pane) {
918 return pane < panes->n ? panes->nrows[pane] : 0;
919}
920
921uint16_t report_panes_rows_max(const struct report_panes *panes) {
922 uint16_t max = 0;
923
924 for (uint32_t i = 0; i < panes->n; i++) {
925 if (panes->nrows[i] > max)
926 max = panes->nrows[i];
927 }
928
929 return max;
930}
931
932struct report_panes *report_panes_panic(void) {
933 return &panic_panes;
934}
935
936void report_panes_begin(struct report_panes *panes, uint32_t n,
937 const uint8_t *weights) {
938 uint16_t total = term_size().cols;
939 uint32_t sum = 0;
940 uint16_t used = 0;
941
942 n = CLAMP(n, UINT32_C(1), REPORT_PANES_MAX);
943
944 panes->n = (uint8_t) n;
945 panes->stacked = false;
946 panes->undivided = false;
947
948 for (uint32_t i = 0; i < n; i++) {
949 panes->nrows[i] = 0;
950 panes->dropped[i] = 0;
951 panes->title[i] = NULL;
952 sum += weights ? weights[i] : 1;
953 }
954
955 if (!sum)
956 sum = n;
957
958 uint16_t gaps = (uint16_t) (REPORT_PANE_GAP * (n - 1));
959 uint16_t avail = total > gaps ? (uint16_t) (total - gaps) : 0;
960
961 for (uint32_t i = 0; i < n; i++) {
962 uint32_t w = weights ? weights[i] : 1;
963 panes->width[i] = (uint16_t) ((avail * w) / sum);
964 used = (uint16_t) (used + panes->width[i]);
965 }
966
967 /* Rounding to the last column */
968 if (avail > used)
969 panes->width[n - 1] = (uint16_t) (panes->width[n - 1] + (avail - used));
970
971 for (uint32_t i = 0; i < n; i++) {
972 if (panes->width[i] < REPORT_PANE_MIN_WIDTH)
973 panes->stacked = true;
974 }
975
976 /* Stacked panels get full width each */
977 if (panes->stacked) {
978 for (uint32_t i = 0; i < n; i++)
979 panes->width[i] = total;
980 }
981}
982
983void report_panes_undivided(struct report_panes *panes) {
984 panes->undivided = true;
985}
986
987void report_panes_title(struct report_panes *panes, uint32_t pane,
988 enum term_sev sev, const char *title) {
989 if (pane >= panes->n)
990 return;
991
992 panes->title[pane] = title;
993 panes->title_sev[pane] = (uint8_t) sev;
994}
995
996static void panes_flush_stacked(struct report_panes *panes) {
997 struct report_target con = report_console();
998
999 for (uint32_t i = 0; i < panes->n; i++) {
1000 if (panes->title[i])
1001 report_rule_sev(tgt: &con, sev: TERM_SEV_LABEL, title: panes->title[i]);
1002
1003 for (uint32_t r = 0; r < panes->nrows[i]; r++)
1004 report_puts(tgt: &con, s: panes->text[i][r]);
1005
1006 if (panes->dropped[i])
1007 report_printf(tgt: &con, fmt: "%s +%u more line(s) not shown%s",
1008 term_style(sev: TERM_SEV_WARN), panes->dropped[i],
1009 term_style_reset());
1010 }
1011}
1012
1013/* Display column the divider before col `i` occupies */
1014static size_t panes_divider_at(const struct report_panes *panes, uint32_t i) {
1015 size_t pos = 0;
1016
1017 for (uint32_t k = 0; k < i; k++)
1018 pos += (size_t) panes->width[k] + REPORT_PANE_GAP;
1019
1020 return pos - REPORT_PANE_GAP + 1;
1021}
1022
1023static bool pane_row_is_rule(const struct report_panes *panes, uint32_t i,
1024 uint16_t r) {
1025 return r < panes->nrows[i] && panes->rule[i][r];
1026}
1027
1028/* The gap between two columns for a row, carrying
1029 * whichever rules exist here through the divider */
1030static void panes_divider(struct report_line *l,
1031 const struct report_panes *panes, uint32_t i,
1032 uint16_t r) {
1033 if (panes->undivided) {
1034 report_line_repeat(l, glyph: " ", REPORT_PANE_GAP);
1035 return;
1036 }
1037
1038 bool left = pane_row_is_rule(panes, i: i - 1, r);
1039 bool right = pane_row_is_rule(panes, i, r);
1040
1041 report_line_puts(l, s: term_style(sev: TERM_SEV_DIM));
1042 report_line_puts(l, s: left ? glyph_hbar() : " ");
1043 report_line_puts(l, s: glyph_join(left, right));
1044 report_line_puts(l, s: right ? glyph_hbar() : " ");
1045 report_line_puts(l, s: term_style_reset());
1046}
1047
1048static void panes_frame(struct report_panes *panes, enum term_sev sev,
1049 const char *title, const char *tee) {
1050 struct report_target con = report_console();
1051 const char *h = glyph_hbar();
1052
1053 /* Nothing to tie off if the columns were stacked or never divided */
1054 if (!panes->n || panes->stacked || panes->undivided) {
1055 report_rule_sev(tgt: &con, sev, title);
1056 return;
1057 }
1058
1059 REPORT_LINE(l, report_target_width(&con));
1060
1061 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
1062 report_line_repeat(l: &l, glyph: h, n: 2);
1063
1064 if (title && *title) {
1065 report_line_puts(l: &l, s: " ");
1066 report_line_puts(l: &l, s: term_style(sev));
1067 report_line_puts(l: &l, s: title);
1068 report_line_puts(l: &l, s: term_style_reset());
1069 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
1070 report_line_puts(l: &l, s: " ");
1071 }
1072
1073 for (uint32_t i = 1; i < panes->n; i++) {
1074 size_t at = panes_divider_at(panes, i);
1075
1076 /* title long enough to reach the divider has passed it */
1077 if (l.col < at)
1078 report_line_repeat(l: &l, glyph: h, n: at - l.col);
1079
1080 report_line_puts(l: &l, s: tee);
1081 }
1082
1083 report_line_repeat(l: &l, glyph: h, n: l.max_col - l.col);
1084 report_line_emit(tgt: &con, l: &l);
1085}
1086
1087/* A titled segment per column, a column's first heading
1088 * costs no rows, since it's the top edge */
1089
1090/* Divider columns of a region that has finished, waiting for the next opening
1091 * rule to tie them off. Kept out here because report_panes_begin() wipes the
1092 * struct the widths were derived from */
1093static struct {
1094 uint16_t at[REPORT_PANES_MAX];
1095 uint32_t n;
1096} panes_pending;
1097
1098void report_panes_carry(struct report_panes *panes) {
1099 panes_pending.n = 0;
1100
1101 if (panes->stacked || panes->undivided)
1102 return;
1103
1104 for (uint32_t i = 1; i < panes->n; i++)
1105 panes_pending.at[panes_pending.n++] =
1106 (uint16_t) panes_divider_at(panes, i);
1107}
1108
1109static void panes_title_segment(struct report_line *l,
1110 const struct report_panes *panes, uint32_t i) {
1111 report_line_puts(l, s: term_style(sev: TERM_SEV_DIM));
1112 report_line_repeat(l, glyph: glyph_hbar(), n: 2);
1113
1114 if (!panes->title[i] || !*panes->title[i])
1115 return;
1116
1117 report_line_puts(l, s: " ");
1118 report_line_puts(l, s: term_style(sev: (enum term_sev) panes->title_sev[i]));
1119 report_line_puts(l, s: panes->title[i]);
1120 report_line_puts(l, s: term_style_reset());
1121 report_line_puts(l, s: term_style(sev: TERM_SEV_DIM));
1122 report_line_puts(l, s: " ");
1123}
1124
1125void report_panes_top(struct report_panes *panes) {
1126 struct report_target con = report_console();
1127 const char *h = glyph_hbar();
1128 uint16_t open_at[REPORT_PANES_MAX];
1129 uint32_t open_count = 0;
1130 uint32_t oi = 0, ci = 0;
1131
1132 if (!panes->n) {
1133 panes_pending.n = 0;
1134 return;
1135 }
1136
1137 if (panes->stacked) {
1138 /* No divider to hang the later segments off */
1139 report_rule_sev(tgt: &con, sev: panes->title_sev[0], title: panes->title[0]);
1140 panes_pending.n = 0;
1141 return;
1142 }
1143
1144 if (!panes->undivided) {
1145 for (uint32_t i = 1; i < panes->n; i++)
1146 open_at[open_count++] = (uint16_t) panes_divider_at(panes, i);
1147 }
1148
1149 REPORT_LINE(l, report_target_width(&con));
1150
1151 panes_title_segment(l: &l, panes, i: 0);
1152
1153 /* Two ascending lists of divider columns merged so a position
1154 * in both gets a crossing instead of two rules a line apart */
1155 while (oi < open_count || ci < panes_pending.n) {
1156 size_t at;
1157
1158 if (oi >= open_count)
1159 at = panes_pending.at[ci];
1160 else if (ci >= panes_pending.n)
1161 at = open_at[oi];
1162 else
1163 at = open_at[oi] < panes_pending.at[ci] ? open_at[oi]
1164 : panes_pending.at[ci];
1165
1166 bool below = oi < open_count && open_at[oi] == at;
1167 bool above = ci < panes_pending.n && panes_pending.at[ci] == at;
1168 uint32_t pane = oi + 1;
1169
1170 if (below)
1171 oi++;
1172 if (above)
1173 ci++;
1174
1175 /* A title long enough to reach the divider has passed it */
1176 if (l.col < at)
1177 report_line_repeat(l: &l, glyph: h, n: at - l.col);
1178
1179 report_line_puts(l: &l, s: term_style(sev: TERM_SEV_DIM));
1180 report_line_puts(l: &l, s: glyph_tee(above, below));
1181
1182 if (below)
1183 panes_title_segment(l: &l, panes, i: pane);
1184 }
1185
1186 panes_pending.n = 0;
1187
1188 report_line_repeat(l: &l, glyph: h, n: l.max_col - l.col);
1189 report_line_emit(tgt: &con, l: &l);
1190}
1191
1192void report_panes_bottom(struct report_panes *panes, enum term_sev sev,
1193 const char *title) {
1194 panes_pending.n = 0;
1195 panes_frame(panes, sev, title, tee: glyph_tee_up());
1196}
1197
1198void report_panes_flush(struct report_panes *panes) {
1199 struct report_target con = report_console();
1200 uint16_t maxlines = 0;
1201 bool any_dropped = false;
1202
1203 if (!panes->n)
1204 return;
1205
1206 if (panes->stacked) {
1207 panes_flush_stacked(panes);
1208 return;
1209 }
1210
1211 for (uint32_t i = 0; i < panes->n; i++) {
1212 if (panes->nrows[i] > maxlines)
1213 maxlines = panes->nrows[i];
1214 if (panes->dropped[i])
1215 any_dropped = true;
1216 }
1217
1218 for (uint16_t r = 0; r < maxlines; r++) {
1219 REPORT_LINE(l, report_target_width(&con));
1220
1221 for (uint32_t i = 0; i < panes->n; i++) {
1222 if (i)
1223 panes_divider(l: &l, panes, i, r);
1224
1225 report_line_field(l: &l, s: r < panes->nrows[i] ? panes->text[i][r] : "",
1226 width: panes->width[i]);
1227 }
1228
1229 report_line_emit(tgt: &con, l: &l);
1230 }
1231
1232 if (!any_dropped)
1233 return;
1234
1235 for (uint32_t i = 0; i < panes->n; i++) {
1236 if (!panes->dropped[i])
1237 continue;
1238
1239 report_printf(tgt: &con, fmt: "%s[%s: +%u more line(s) not shown]%s",
1240 term_style(sev: TERM_SEV_WARN),
1241 panes->title[i] ? panes->title[i] : "panel",
1242 panes->dropped[i], term_style_reset());
1243 }
1244}
1245
1246void report_enter_panic(void) {
1247 bool nested = term_in_panic();
1248 const char *sec = report_section;
1249
1250 report_section = NULL;
1251 term_enter_panic();
1252
1253 if (!nested)
1254 return;
1255
1256 /* Re-entered with a section open: that section is what killed us */
1257 struct report_target con = report_console();
1258
1259 report_blank(tgt: &con);
1260
1261 if (sec) {
1262 report_skip_section = sec;
1263 report_printf(tgt: &con,
1264 fmt: "%s!! nested panic while rendering [%s] - skipping it%s",
1265 term_style(sev: TERM_SEV_CRIT), sec, term_style_reset());
1266 } else {
1267 report_printf(tgt: &con, fmt: "%s!! nested panic outside any section%s",
1268 term_style(sev: TERM_SEV_CRIT), term_style_reset());
1269 }
1270}
1271