diff options
Diffstat (limited to 'colitur-mu.c')
| -rw-r--r-- | colitur-mu.c | 227 |
1 files changed, 227 insertions, 0 deletions
diff --git a/colitur-mu.c b/colitur-mu.c new file mode 100644 index 0000000..d76152d --- /dev/null +++ b/colitur-mu.c @@ -0,0 +1,227 @@ +/* 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 <X11/Xlib.h> +#include <X11/keysym.h> +#include <X11/Xutil.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#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; +} |
