/* colitur-mu -- an editable microui front-end for colitur. * * Unlike colitur-x, which only renders, this one lets you CHANGE the * settings colitur was invoked with -- language, book-name form, numbering * tradition, year -- and re-runs it. It never reimplements any calendar * logic: every value on screen came out of `colitur emit --format csv`. * * SPDX-License-Identifier: GPL-3.0-or-later */ #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include "vendor/microui.h" #include "mu_x11.h" #include "config.h" #define MAXDAYS 400 #define F 16 typedef struct { char *raw; char *f[F]; } Row; static Row rows[MAXDAYS]; static int nrows, sel; static char status[256] = "ready"; /* --- settings the UI edits ---------------------------------------------- */ static int year = 2027; static const char *langs[] = { "la", "en" }; static const char *books[] = { "abbr", "full" }; static const char *traditions[] = { "vulgate", "modern" }; static int lang_i, book_i, trad_i; static int split(char *line, char **out) { int n = 0; char *p = line; while (n < F) { if (*p == '"') { out[n++] = ++p; while (*p && *p != '"') p++; if (*p) *p++ = 0; if (*p == ',') p++; } else { out[n++] = p; while (*p && *p != ',') p++; if (*p) *p++ = 0; else break; } } return n; } /* Shell out to colitur with the current settings. This is the whole point: the front-end owns no calendar logic, only the invocation. */ static void reload(void) { for (int i = 0; i < nrows; i++) free(rows[i].raw); nrows = 0; sel = 0; char cmd[512]; snprintf(cmd, sizeof cmd, "colitur emit --format csv --from %d --to %d " "--lang %s --sigla-book %s --sigla-tradition %s 2>&1", year, year, langs[lang_i], books[book_i], traditions[trad_i]); FILE *p = popen(cmd, "r"); if (!p) { snprintf(status, sizeof status, "cannot run colitur"); return; } char buf[4096]; if (!fgets(buf, sizeof buf, p)) { pclose(p); snprintf(status, sizeof status, "no output"); return; } if (strncmp(buf, "date,", 5)) { /* colitur printed an error */ buf[strcspn(buf, "\n")] = 0; snprintf(status, sizeof status, "%.200s", buf); pclose(p); return; } while (nrows < MAXDAYS && fgets(buf, sizeof buf, p)) { buf[strcspn(buf, "\n")] = 0; char *d = strdup(buf); if (!d) break; rows[nrows].raw = d; if (split(d, rows[nrows].f) >= 12) nrows++; else free(d); } pclose(p); snprintf(status, sizeof status, "%d days ยท %s", nrows, cmd + 8); } /* --- one frame ---------------------------------------------------------- */ /* One frame. NOTE the explicit row heights: mu_layout_row's height argument is stored verbatim as the row size, so passing 0 lays every control out zero pixels tall -- they are "drawn", invisibly, and the result looks like a layout bug rather than a height bug. */ static void frame(mu_Context *ctx, int W, int H) { enum { RH = 24 }; if (!mu_begin_window_ex(ctx, "colitur", mu_rect(0, 0, W, H), MU_OPT_NOTITLE | MU_OPT_NORESIZE | MU_OPT_NOCLOSE)) return; int wy[] = { 120, 40, 40, 90, -1 }; mu_layout_row(ctx, 5, wy, RH); mu_label(ctx, "Year"); if (mu_button(ctx, "-")) { year--; reload(); } if (mu_button(ctx, "+")) { year++; reload(); } char y[16]; snprintf(y, sizeof y, "%d", year); mu_label(ctx, y); mu_label(ctx, ""); int w4[] = { 120, 90, 90, -1 }; struct { const char *cap; const char **opts; int *idx; } sets[] = { { "Language", langs, &lang_i }, { "Book names", books, &book_i }, { "Numbering", traditions, &trad_i }, }; for (size_t r = 0; r < sizeof sets / sizeof *sets; r++) { mu_layout_row(ctx, 4, w4, RH); mu_label(ctx, sets[r].cap); for (int i = 0; i < 2; i++) { char lbl[48]; snprintf(lbl, sizeof lbl, "%s%s", *sets[r].idx == i ? "\xe2\x97\x8f " : " ", sets[r].opts[i]); if (mu_button_ex(ctx, lbl, 0, MU_OPT_ALIGNCENTER)) { *sets[r].idx = i; reload(); } } mu_label(ctx, ""); } int w1[] = { -1 }; mu_layout_row(ctx, 1, w1, RH); mu_label(ctx, status); int listh = H - 7 * RH - 90; if (listh < 100) listh = 100; mu_layout_row(ctx, 1, w1, listh); mu_begin_panel(ctx, "days"); int wrow[] = { 110, 100, 330, -1 }; for (int i = 0; i < nrows; i++) { mu_layout_row(ctx, 4, wrow, 22); if (mu_button(ctx, rows[i].f[0])) sel = i; mu_label(ctx, rows[i].f[9]); mu_label(ctx, rows[i].f[6]); char cit[160]; snprintf(cit, sizeof cit, "%s %s", rows[i].f[13], rows[i].f[14]); mu_label(ctx, cit); } mu_end_panel(ctx); if (nrows) { char d[600]; mu_layout_row(ctx, 1, w1, RH); snprintf(d, sizeof d, "%s %s", rows[sel].f[0], rows[sel].f[6]); mu_label(ctx, d); mu_layout_row(ctx, 1, w1, RH); snprintf(d, sizeof d, "%s %s %s Epistola %s Evangelium %s", rows[sel].f[3], rows[sel].f[9], rows[sel].f[11], rows[sel].f[13], rows[sel].f[14]); mu_label(ctx, d); } mu_end_window(ctx); } int main(void) { Display *dpy = XOpenDisplay(NULL); if (!dpy) { fputs("cannot open display\n", stderr); return 1; } int scr = DefaultScreen(dpy); int W = 1000, H = 700; Window win = XCreateSimpleWindow(dpy, RootWindow(dpy, scr), 0, 0, W, H, 0, BlackPixel(dpy, scr), BlackPixel(dpy, scr)); XStoreName(dpy, win, "colitur-mu"); XSelectInput(dpy, win, ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask | ButtonReleaseMask | PointerMotionMask | StructureNotifyMask); XMapWindow(dpy, win); mux_init(dpy, win, font); mu_Context *ctx = malloc(sizeof *ctx); mu_init(ctx); ctx->text_width = mux_text_width; ctx->text_height = mux_text_height; reload(); /* An immediate-mode UI is a RENDER LOOP, not an event handler. Drawing only in response to an X event is wrong twice over: microui's first frame merely creates its containers and the second is what lays them out, and a container's scroll state settles a frame late. Drain the queue, then always draw. */ for (int running = 1; running;) { while (XPending(dpy)) { XEvent ev; XNextEvent(dpy, &ev); switch (ev.type) { case ConfigureNotify: W = ev.xconfigure.width; H = ev.xconfigure.height; break; case MotionNotify: mu_input_mousemove(ctx, ev.xmotion.x, ev.xmotion.y); break; case ButtonPress: if (ev.xbutton.button == 4) mu_input_scroll(ctx, 0, -30); else if (ev.xbutton.button == 5) mu_input_scroll(ctx, 0, 30); else mu_input_mousedown(ctx, ev.xbutton.x, ev.xbutton.y, MU_MOUSE_LEFT); break; case ButtonRelease: if (ev.xbutton.button < 4) mu_input_mouseup(ctx, ev.xbutton.x, ev.xbutton.y, MU_MOUSE_LEFT); break; case KeyPress: { KeySym k = XLookupKeysym(&ev.xkey, 0); if (k == XK_q) running = 0; if (k == XK_Down && sel < nrows - 1) sel++; if (k == XK_Up && sel > 0) sel--; break; } } } mu_begin(ctx); frame(ctx, W, H); mu_end(ctx); mux_present(ctx, W, H, mu_color(20, 20, 20, 255)); XFlush(dpy); nanosleep(&(struct timespec){ 0, 16000000 }, NULL); /* ~60 Hz */ } XCloseDisplay(dpy); return 0; }