==> src/ui.c <==1/* ui.rs's frame: draw, the tab bar, the status bar and the prompt, ported. */
2#include "mesh/top_ui.h"
3
4#include "mesh/top_ui_doom.h"
5#include "mesh/top_ui_fleet.h"
6#include "mesh/top_ui_http.h"
7#include "mesh/top_ui_infer.h"
8#include "mesh/top_ui_load.h"
9#include "mesh/top_ui_mesh.h"
10#include "mesh/top_ui_net.h"
11#include "mesh/top_ui_programs.h"
12#include "mesh/top_ui_render.h"
13#include "mesh/top_ui_train.h"
14#include "tui/unicode_width.h"
15
16#include <string.h>
17
18static tui_line tabs_line(mt_frame *f, const mt_app *app, bool full, bool brand) {
19 tui_line l = tui_line_empty();
20 if (brand) {
21 tui_line_push(f->a, &l, mt_styled(" mesh-top ", mt_fg_bg_bold(MT_BG, MT_ACCENT)));
22 tui_line_push(f->a, &l, tui_span_raw(" "));
23 }
24 for (int i = 0; i < MT_TABS; i++) {
25 tui_style st = i == app->tab ? mt_fg_bg_bold(MT_BG, MT_WARN) : tui_fg(MT_DIM);
26 const char *text = full || i == app->tab ? MT_FMT(f, " %c %s ", MT_TAB_KEYS[i], MT_TAB_NAMES[i]) : MT_FMT(f, " %c ", MT_TAB_KEYS[i]);
27 tui_line_push(f->a, &l, mt_styled(text, st));
28 }
29 return l;
30}
31
32static void draw_tabs(mt_frame *f, tui_rect area, const mt_app *app) {
33 /* Full names, else the current tab's name and the others' keys, else the keys alone. */
34 size_t w = area.width;
35 tui_line cands[3] = {tabs_line(f, app, true, true), tabs_line(f, app, false, true), tabs_line(f, app, false, false)};
36 tui_line left = cands[2];
37 for (int i = 0; i < 3; i++)
38 if (tui_line_width(&cands[i]) <= w) {
39 left = cands[i];
40 break;
41 }
42 size_t used = tui_line_width(&left);
43 mt_par_line(f, left, area);
44 size_t online = 0;
45 for (size_t i = 0; i < app->nmachines; i++) online += app->machines[i].online;
46 tui_span focus[3] = {mt_label("focus "), mt_fgb(MT_FMT(f, "[ %s ]", mt_name(f, mt_app_focused(app))), MT_MACHINE[app->focus % 8]),
47 tui_span_raw(" ")};
48 /* The tabs keep their room: the online count goes first, then the focus. */
49 tui_line right = MT_LINE(f, mt_label(MT_FMT(f, "%zu/%zu online ", online, app->nmachines)), focus[0], focus[1], focus[2]);
50 if (used + tui_line_width(&right) + 1 > area.width) right = tui_line_of(f->a, 3, focus);
51 if (used + tui_line_width(&right) < area.width) {
52 tui_paragraph p = tui_paragraph_new(tui_text_line(f->a, right));
53 p.alignment = TUI_ALIGN_RIGHT;
54 tui_paragraph_render(f->a, &p, area, f->buf);
55 }
56}
57
58typedef struct {
59 const char *k, *d;
60} keyhelp;
61
62static void draw_status(mt_frame *f, tui_rect area, const mt_app *app) {
63 static const keyhelp load[] = {{"type/↑↓", "program dir"}, {"Enter", "load"}, {"Esc", "cancel"}};
64 static const keyhelp unload[] = {{"y", "unload"}, {"any key", "cancel"}};
65 static const keyhelp doom[] = {{"WASD/←↑↓→", "move/turn"}, {"Q/E", "strafe"}, {"p", "place local/pin k/split"},
66 {"v", "split/single"}, {"[ ]", "machine"}, {"Esc", "back"}};
67 static const keyhelp confirm[] = {{"y", "confirm"}, {"any key", "cancel"}};
68 static const keyhelp http[] = {{"type", "path"}, {"Enter", "GET"}, {"↑↓ PgUp/Dn", "scroll"}, {"Tab", "tab"}, {"Esc", "back"}};
69 static const keyhelp fleet[] = {{"↑↓←→", "program/member"}, {"c", "cordon"}, {"p/u", "pin/unpin"},
70 {"z", "sched pause"}, {"g", "reform"}, {"q", "quit"}};
71 static const keyhelp jobs[] = {{"↑↓", "job"}, {"x", "cancel job"}, {"0/-", "jobs/pipes"}, {"q", "quit"}};
72 static const keyhelp pipes[] = {{"[ ]", "machine"}, {"9/0", "fleet/jobs"}, {"q", "quit"}};
73 static const keyhelp rfleet[] = {{"m", "mark tiles by machine"}, {"t", "swap the job's tone map"}, {"[ ]", "assembling machine"}, {"q", "quit"}};
74 static const keyhelp render[] = {{"m", "mark tiles by renderer"}, {"t", "swap tone map"}, {"r", "restart"}, {"[ ]", "machine"}, {"q", "quit"}};
75 static const keyhelp programs[] = {{"↑↓", "select"}, {"s", "start/stop"}, {"u", "upgrade"}, {"b", "rollback"}, {"x", "unload"},
76 {"L", "load"}, {"r", "reset"}, {"[ ]", "machine"}, {"q", "quit"}};
77 static const keyhelp other[] = {{"1-9 0 - =", "tab"}, {"[ ]", "machine"}, {"d/t/i", "doom/train/infer"}, {"X", "reset train"}, {"q", "quit"}};
78#define PICK(x) (keys = (x), nkeys = sizeof(x) / sizeof *(x))
79 const keyhelp *keys;
80 size_t nkeys;
81 int pk = app->prompt.kind;
82 if (pk == MT_PROMPT_LOAD) PICK(load);
83 else if (pk == MT_PROMPT_UNLOAD) PICK(unload);
84 else if (app->tab == MT_TAB_DOOM) PICK(doom);
85 else if (pk == MT_PROMPT_CONFIRM) PICK(confirm);
86 else if (app->tab == MT_TAB_HTTP) PICK(http);
87 else if (app->tab == MT_TAB_FLEET) PICK(fleet);
88 else if (app->tab == MT_TAB_JOBS) PICK(jobs);
89 else if (app->tab == MT_TAB_PIPES) PICK(pipes);
90 else if (app->tab == MT_TAB_RENDER && mt_app_focused(app)->has_fleet) PICK(rfleet);
91 else if (app->tab == MT_TAB_RENDER) PICK(render);
92 else if (app->tab == MT_TAB_PROGRAMS) PICK(programs);
93 else PICK(other);
94#undef PICK
95 tui_line l = tui_line_empty();
96 for (size_t i = 0; i < nkeys; i++) {
97 tui_line_push(f->a, &l, mt_fgb(MT_FMT(f, " %s", keys[i].k), MT_ACCENT));
98 tui_line_push(f->a, &l, mt_fgs(MT_FMT(f, " %s", keys[i].d), MT_DIM));
99 }
100 tui_line_push(f->a, &l, mt_fgs(" │ ", MT_FAINT));
101 const char *reply = app->last_reply ? app->last_reply : "";
102 tui_line_push(f->a, &l, mt_fgs(reply, strstr(reply, "-> err") ? MT_BAD : MT_GOOD));
103 tui_paragraph p = tui_paragraph_new(tui_text_line(f->a, l));
104 p.style = tui_style_bg(tui_style_new(), TUI_RGB(22, 25, 34));
105 tui_paragraph_render(f->a, &p, area, f->buf);
106}
107
108static void draw_prompt(mt_frame *f, tui_rect body, const mt_app *app, const mt_prompt *p) {
109 uint16_t w = tui_min16(body.width, p->kind == MT_PROMPT_CONFIRM ? 84 : 64);
110 const char *title;
111 mt_lines lines = {0};
112 if (p->kind == MT_PROMPT_UNLOAD) {
113 title = " unload ";
114 mt_lines_push(f, &lines, MT_LINE(f, mt_label("unload "), mt_val(p->name), mt_label(" on "), mt_val(mt_name(f, &app->machines[p->machine])),
115 mt_label("?")));
116 mt_lines_push(f, &lines, MT_LINE(f, mt_fgb("y", MT_ACCENT), mt_label(" unload any other key cancels")));
117 } else if (p->kind == MT_PROMPT_CONFIRM) {
118 title = " confirm ";
119 mt_lines_push(f, &lines, MT_LINE(f, mt_val(p->what), mt_label("?")));
120 mt_lines_push(f, &lines, MT_LINE(f, mt_label("sends "), mt_fgs(MT_FMT(f, "`%s`", p->line), MT_ACCENT), mt_label(" to "),
121 mt_val(mt_name(f, &app->machines[p->machine]))));
122 mt_lines_push(f, &lines, MT_LINE(f, mt_fgb("y", MT_ACCENT), mt_label(" confirm any other key cancels")));
123 } else {
124 title = " load a program ";
125 const mt_machine *fm = mt_app_focused(app);
126 mt_lines_push(f, &lines, MT_LINE(f, mt_label("load "), mt_fgb(tui_arena_strdup(f->a, p->input), MT_FG), mt_fgs("█", MT_ACCENT)));
127 mt_lines_push(f, &lines, tui_line_span(f->a, mt_label(MT_FMT(f, "catalog (%zu), ↑↓ to pick:", fm->ncatalog))));
128 if (fm->ncatalog == 0) mt_lines_push(f, &lines, tui_line_span(f->a, mt_label(" (none yet: waiting for `catalog`)")));
129 for (size_t i = 0; i < fm->ncatalog; i++) {
130 const char *c = fm->catalog[i];
131 const char *slash = strrchr(c, '/');
132 bool loaded = mt_machine_program(fm, slash ? slash + 1 : c) != NULL;
133 tui_style st = p->has_pick && p->pick == i ? mt_fg_bg_bold(MT_BG, MT_ACCENT) : tui_fg(MT_FG);
134 tui_line l = tui_line_span(f->a, mt_styled(MT_FMT(f, " %s ", c), st));
135 if (loaded) tui_line_push(f->a, &l, mt_label(" loaded"));
136 mt_lines_push(f, &lines, l);
137 }
138 }
139 uint16_t h = tui_min16((uint16_t)(lines.n + 2), body.height);
140 tui_rect r = mt_rect((uint16_t)(body.x + (body.width - w) / 2), (uint16_t)(body.y + (body.height - h) / 2), w, h);
141 tui_buffer_clear(f->buf, r);
142 tui_block b = mt_panel(f, tui_line_span(f->a, mt_label(title)), MT_WARN);
143 tui_paragraph pp = tui_paragraph_new(mt_text(f, &lines));
144 pp.wrap = TUI_WRAP_TRIM;
145 pp.block = &b;
146 tui_paragraph_render(f->a, &pp, r, f->buf);
147}
148
149void mt_ui_draw(tui_arena *a, tui_buffer *buf, const mt_app *app, mt_instant now) {
150 mt_frame fr = {a, buf};
151 mt_frame *f = &fr;
152 tui_rect area = buf->area;
153 tui_buffer_set_style(buf, area, tui_fg_bg(MT_FG, MT_BG));
154 tui_rect r[3];
155 tui_vsplit(area, 3, (tui_constraint[]){TUI_LENGTH(1), TUI_FILL(1), TUI_LENGTH(1)}, r);
156 draw_tabs(f, r[0], app);
157 tui_rect body = r[1];
158 switch (app->tab) {
159 case MT_TAB_MESH: mt_draw_mesh(f, body, app, now); break;
160 case MT_TAB_PROGRAMS: mt_draw_programs(f, body, app, now); break;
161 case MT_TAB_DOOM: mt_draw_doom(f, body, app); break;
162 case MT_TAB_TRAIN: mt_draw_train(f, body, app); break;
163 case MT_TAB_INFER: mt_draw_infer(f, body, app); break;
164 case MT_TAB_HTTP: mt_draw_http(f, body, app); break;
165 case MT_TAB_LOAD: mt_draw_load(f, body, app); break;
166 case MT_TAB_FLEET: mt_draw_fleet(f, body, app, now); break;
167 case MT_TAB_JOBS: mt_draw_jobs(f, body, app, now); break;
168 case MT_TAB_PIPES: mt_draw_pipes(f, body, app, now); break;
169 case MT_TAB_RENDER: mt_draw_render(f, body, app); break;
170 default: mt_draw_net(f, body, app); break;
171 }
172 draw_status(f, r[2], app);
173 if (app->prompt.kind != MT_PROMPT_NONE) draw_prompt(f, body, app, &app->prompt);
174}
175
176static bool cell_eq(const tui_cell *a, const tui_cell *b) {
177 return strcmp(tui_cell_symbol(a), tui_cell_symbol(b)) == 0 && tui_color_eq(a->fg, b->fg) && tui_color_eq(a->bg, b->bg) && a->mod == b->mod;
178}
179
180/* tui-render's BufferDiff (ratatui 0.30) against a blank screen, applied to out. */
181void mt_ui_flush(const tui_buffer *frame, tui_buffer *out) {
182 size_t len = (size_t)frame->area.width * frame->area.height;
183 for (size_t i = 0; i < len;) {
184 const tui_cell *cur = &frame->cells[i];
185 const char *sym = tui_cell_symbol(cur);
186 size_t width = tui_symbol_width(sym, strlen(sym));
187 if (cell_eq(cur, &TUI_CELL_EMPTY)) {
188 i += width > 1 ? width : 1;
189 continue;
190 }
191 out->cells[i] = *cur;
192 if (width > 1 && strstr(sym, "\xEF\xB8\x8F")) {
193 /* The cells under it are written when their symbols changed. */
194 size_t end = i + width < len ? i + width : len;
195 for (size_t j = i + 1; j < end;) {
196 const char *js = tui_cell_symbol(&frame->cells[j]);
197 size_t jw = tui_symbol_width(js, strlen(js));
198 if (strcmp(js, " ") != 0) out->cells[j] = frame->cells[j];
199 j += jw ? jw : 1;
200 if (j > end) end = j < len ? j : len;
201 }
202 i = end;
203 } else {
204 i += width > 1 ? width : 1;
205 }
206 }
207}