skip to content
==> 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}