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