| 1 | /* @title: Console report composer */ |
| 2 | #pragma once |
| 3 | #include <compiler.h> |
| 4 | #include <console/term.h> |
| 5 | #include <stdarg.h> |
| 6 | #include <stdbool.h> |
| 7 | #include <stddef.h> |
| 8 | #include <stdint.h> |
| 9 | |
| 10 | /* Compose a report as a stream of lines, all statically allocated |
| 11 | * with no locks taken, primarily useful for panicking */ |
| 12 | |
| 13 | /* Bytes for a composed line. Full width rules of box drawing glyphs cost |
| 14 | * three bytes a column, so we use this to bound terminal width */ |
| 15 | #define REPORT_LINE_MAX 1024 |
| 16 | |
| 17 | /* Build a line into a caller supplied buffer, counting display columns |
| 18 | * |
| 19 | * `max_col` bounds layout and `cap` bounds buffer, and partial writes |
| 20 | * are not allowed, and overflow uses ellipsis */ |
| 21 | struct report_line { |
| 22 | char *buf; |
| 23 | size_t cap; |
| 24 | size_t len; /* bytes used */ |
| 25 | size_t col; /* display columns used */ |
| 26 | size_t max_col; /* display column bound/budget */ |
| 27 | size_t glyph_at; /* byte offset of the last visible glyph */ |
| 28 | bool full; /* budget exhausted */ |
| 29 | bool truncated; /* something was cut somewhere on this line */ |
| 30 | }; |
| 31 | |
| 32 | #define REPORT_LINE(name, width) \ |
| 33 | char name##_storage[REPORT_LINE_MAX]; \ |
| 34 | struct report_line name; \ |
| 35 | report_line_init(&name, name##_storage, sizeof(name##_storage), (width)) |
| 36 | |
| 37 | void report_line_init(struct report_line *l, char *buf, size_t cap, |
| 38 | size_t max_col); |
| 39 | void report_line_reset(struct report_line *l); |
| 40 | |
| 41 | void report_line_puts(struct report_line *l, const char *s); |
| 42 | __printf_like(2, 3) void report_line_printf(struct report_line *l, |
| 43 | const char *fmt, ...); |
| 44 | void report_line_vprintf(struct report_line *l, const char *fmt, va_list ap); |
| 45 | |
| 46 | /* Spaces out to a display column */ |
| 47 | void report_line_pad_to(struct report_line *l, size_t col); |
| 48 | void report_line_repeat(struct report_line *l, const char *glyph, size_t n); |
| 49 | |
| 50 | /* take up exactly `width` display columns, and truncate if too long/pad |
| 51 | * if too short */ |
| 52 | void report_line_field(struct report_line *l, const char *s, size_t width); |
| 53 | void report_line_right(struct report_line *l, const char *s); |
| 54 | |
| 55 | const char *report_line_str(const struct report_line *l); |
| 56 | size_t report_line_width(const struct report_line *l); |
| 57 | bool report_line_truncated(const struct report_line *l); |
| 58 | |
| 59 | /* Display columns a string would occupy */ |
| 60 | size_t report_strwidth(const char *s); |
| 61 | |
| 62 | struct report_panes; |
| 63 | struct report_box; |
| 64 | |
| 65 | /* Where a finished line actually goes and how wide it is, either the |
| 66 | * console or the pane of a buffered region |
| 67 | * |
| 68 | * `col` means a display column, a pane is the vertical panel */ |
| 69 | struct report_target { |
| 70 | struct report_panes *panes; /* NULL means the console/a box */ |
| 71 | struct report_box *box; /* non-NULL = lines come out bordered */ |
| 72 | uint16_t width; /* display columns */ |
| 73 | uint8_t indent; |
| 74 | uint8_t pane; /* which pane of `panes` */ |
| 75 | }; |
| 76 | |
| 77 | struct report_target report_console(void); |
| 78 | struct report_target report_console_indent(uint16_t indent); |
| 79 | struct report_target report_pane(struct report_panes *panes, uint32_t pane); |
| 80 | |
| 81 | /* Nesting indentation supported */ |
| 82 | struct report_target report_target_indent(struct report_target tgt, |
| 83 | uint16_t indent); |
| 84 | |
| 85 | static inline uint16_t report_target_width(const struct report_target *tgt) { |
| 86 | return tgt->width; |
| 87 | } |
| 88 | |
| 89 | /* Emit a line, don't pass '\n' */ |
| 90 | void report_puts(struct report_target *tgt, const char *s); |
| 91 | __printf_like(2, 3) void report_printf(struct report_target *tgt, |
| 92 | const char *fmt, ...); |
| 93 | void report_blank(struct report_target *tgt); |
| 94 | |
| 95 | void report_line_emit(struct report_target *tgt, struct report_line *l); |
| 96 | |
| 97 | /* --- TITLE ---- , to the target's width */ |
| 98 | void report_rule(struct report_target *tgt, const char *title); |
| 99 | void report_rule_sev(struct report_target *tgt, enum term_sev sev, |
| 100 | const char *title); |
| 101 | |
| 102 | /* Word wrap to `tgt`'s width, emitting however many lines it takes |
| 103 | * |
| 104 | * Break at spaces, honor newlines, count display columns */ |
| 105 | void report_wrap(struct report_target *tgt, const char *text); |
| 106 | __printf_like(2, 3) void report_wrap_printf(struct report_target *tgt, |
| 107 | const char *fmt, ...); |
| 108 | |
| 109 | /* A box, such as |
| 110 | * |
| 111 | * ┌─ title ─────────────┐ |
| 112 | * │ this is my box data │ |
| 113 | * └─────────────────────┘ |
| 114 | * |
| 115 | */ |
| 116 | struct report_box { |
| 117 | struct report_target target; |
| 118 | uint16_t inner; /* content columns, i.e. |
| 119 | * everything but borders and padding */ |
| 120 | }; |
| 121 | |
| 122 | /* inner == 0 fills target, size with `report_strwidth()`, when the box |
| 123 | * should hug rather than span the terminal, content wraps */ |
| 124 | void report_box_open(struct report_box *b, struct report_target target, |
| 125 | const char *title, uint16_t inner); |
| 126 | |
| 127 | /* This wraps, different from all the other _printf functions here */ |
| 128 | __printf_like(2, 3) void report_box_printf(struct report_box *b, |
| 129 | const char *fmt, ...); |
| 130 | |
| 131 | /* A target whose lines get bordered, so anything that draws |
| 132 | * to a target works in a box, its width is the box's inner width */ |
| 133 | struct report_target report_box_body(struct report_box *b); |
| 134 | void report_box_close(struct report_box *b); |
| 135 | |
| 136 | /* Gives a header to a region we're inside of it, with faults being recorded |
| 137 | * to avoid nested panics breaking the report */ |
| 138 | bool report_section_begin(const char *name); |
| 139 | bool report_section_begin_at(struct report_target *tgt, const char *name); |
| 140 | |
| 141 | /* No heading emitted, for callers that already have drawn one */ |
| 142 | bool report_section_claim(const char *name); |
| 143 | bool report_section_claim_at(struct report_target *tgt, const char *name); |
| 144 | void report_section_end(void); |
| 145 | const char *report_section_current(void); |
| 146 | |
| 147 | #define REPORT_FIELD_GAP 2 |
| 148 | |
| 149 | /* packs kv pairs, as many per row as target is wide, flushing as it fills, |
| 150 | * holding a line writer pointing into its storage */ |
| 151 | struct report_fields { |
| 152 | struct report_target target; |
| 153 | uint8_t per_row; |
| 154 | uint8_t in_row; |
| 155 | uint8_t keyw; |
| 156 | uint8_t valw; |
| 157 | struct report_line line; |
| 158 | char storage[REPORT_LINE_MAX]; |
| 159 | }; |
| 160 | |
| 161 | void report_fields_begin(struct report_fields *g, struct report_target target, |
| 162 | uint32_t keyw, uint32_t valw); |
| 163 | __printf_like(3, 4) void report_field(struct report_fields *g, const char *key, |
| 164 | const char *fmt, ...); |
| 165 | |
| 166 | /* End row and take the full line, for values carrying annotations too long */ |
| 167 | __printf_like(3, 4) void report_field_full(struct report_fields *g, |
| 168 | const char *key, const char *fmt, |
| 169 | ...); |
| 170 | void report_fields_end(struct report_fields *g); |
| 171 | |
| 172 | /* Storage is MAX * LINES * BYTES */ |
| 173 | #define REPORT_PANES_MAX 3 |
| 174 | |
| 175 | /* Max rows for a pane */ |
| 176 | #define REPORT_PANE_ROWS 48 |
| 177 | |
| 178 | /* Bytes stored per line */ |
| 179 | #define REPORT_PANE_LINE_MAX 384 |
| 180 | #define REPORT_PANE_MIN_WIDTH 24 |
| 181 | #define REPORT_PANE_GAP 3 |
| 182 | |
| 183 | /* Vertical panels side by side, only 2D thing here, and the only buffered one |
| 184 | * |
| 185 | * Panes can't stream, and lines are stored per-pane and interleaved at flush |
| 186 | * |
| 187 | * This is statically allocated */ |
| 188 | struct report_panes { |
| 189 | uint8_t n; |
| 190 | uint16_t width[REPORT_PANES_MAX]; |
| 191 | const char *title[REPORT_PANES_MAX]; |
| 192 | uint8_t title_sev[REPORT_PANES_MAX]; |
| 193 | uint16_t nrows[REPORT_PANES_MAX]; |
| 194 | uint32_t dropped[REPORT_PANES_MAX]; |
| 195 | bool stacked; /* too narrow to sit side by side */ |
| 196 | bool undivided; /* no vertical rule */ |
| 197 | char text[REPORT_PANES_MAX][REPORT_PANE_ROWS][REPORT_PANE_LINE_MAX]; |
| 198 | |
| 199 | /* Which rows are rules */ |
| 200 | bool rule[REPORT_PANES_MAX][REPORT_PANE_ROWS]; |
| 201 | }; |
| 202 | |
| 203 | /* `weights` == NULL is even split */ |
| 204 | void report_panes_begin(struct report_panes *panes, uint32_t n, |
| 205 | const uint8_t *weights); |
| 206 | |
| 207 | /* Remove the vertical rule between panes, leaving only the gap */ |
| 208 | void report_panes_undivided(struct report_panes *panes); |
| 209 | |
| 210 | /* Heading for a pane, drawn as a segment of the pane's opening rule */ |
| 211 | void report_panes_title(struct report_panes *panes, uint32_t pane, |
| 212 | enum term_sev sev, const char *title); |
| 213 | |
| 214 | /* Framing rules with dividers: |
| 215 | * |
| 216 | * -- text -----+--- more ------ |
| 217 | * ... |
| 218 | * -- data -----+--------------- |
| 219 | * |
| 220 | * The opening rule is segmented, and closing rule can double as a title |
| 221 | */ |
| 222 | void report_panes_top(struct report_panes *panes); |
| 223 | void report_panes_bottom(struct report_panes *panes, enum term_sev sev, |
| 224 | const char *title); |
| 225 | |
| 226 | /* Hand this region's divider positions to next opening rule, so one |
| 227 | * rule can close the region above and open one below, such as |
| 228 | * |
| 229 | * -----------+------ |
| 230 | * |
| 231 | * -- text ---+------ |
| 232 | * |
| 233 | * Call after flushing and before `begin()` changes widths, consumed by |
| 234 | * the next `report_panes_top()` */ |
| 235 | void report_panes_carry(struct report_panes *panes); |
| 236 | |
| 237 | /* Below REPORT_PANE_MIN_WIDTH, the panes stack vertically */ |
| 238 | void report_panes_flush(struct report_panes *panes); |
| 239 | uint16_t report_pane_rows(const struct report_panes *panes, uint32_t pane); |
| 240 | uint16_t report_panes_rows_max(const struct report_panes *panes); |
| 241 | |
| 242 | /* Reserved for panic paths, so callers don't need to own the storage, regions |
| 243 | * get used one at a time with `begin()` resetting it */ |
| 244 | struct report_panes *report_panes_panic(void); |
| 245 | |
| 246 | /* Enter panic report path, so writes are lock free and the screen |
| 247 | * is given back, used in panic handler and tracks nested panics */ |
| 248 | void report_enter_panic(void); |
| 249 | |