#define _POSIX_C_SOURCE 200809L /* colitur-x -- a raw Xlib front-end for colitur. * * Reads `colitur emit --format csv` on stdin, draws the year as a month * grid coloured by the day's liturgical colour, and shows the selected * day's readings underneath. Arrow keys / hjkl move, n and p change month, * q quits. * * Links libX11 and libXft. No toolkit, no runtime config: edit config.h. * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include "config.h" #define MAXDAYS 400 #define F 16 /* CSV columns, see colitur emit --format csv */ typedef struct { char *f[F]; } Row; static Row rows[MAXDAYS]; static int nrows; static int cur; /* selected row */ static Display *dpy; static Window win; static int scr; static XftFont *fnt, *fnt_big; static XftDraw *xd; static Visual *vis; static Colormap cmap; /* --- tiny CSV reader: no quoting in colitur's output except commas in citations, which are quoted. Handle both. ------------------------------ */ 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; } static void load(void) { char buf[4096]; if (!fgets(buf, sizeof buf, stdin)) return; /* header */ while (nrows < MAXDAYS && fgets(buf, sizeof buf, stdin)) { buf[strcspn(buf, "\n")] = 0; char *dup = strdup(buf); if (!dup) break; if (split(dup, rows[nrows].f) >= 12) nrows++; } } /* --- colour helpers ----------------------------------------------------- */ static void alloc(const char *spec, XftColor *c) { XftColorAllocName(dpy, vis, cmap, spec, c); } static void colours_for(const char *name, XftColor *bg, XftColor *fg) { for (size_t i = 0; i < sizeof palette / sizeof *palette; i++) if (!strcmp(palette[i].name, name)) { alloc(palette[i].bg, bg); alloc(palette[i].fg, fg); return; } alloc("#808080", bg); alloc("#ffffff", fg); } static void text(XftColor *c, XftFont *f, int x, int y, const char *s, int maxw) { XGlyphInfo gi; int len = strlen(s); while (len > 0) { XftTextExtentsUtf8(dpy, f, (FcChar8 *)s, len, &gi); if (gi.xOff <= maxw) break; len--; } XftDrawStringUtf8(xd, c, f, x, y, (FcChar8 *)s, len); } static void fill(const char *spec, int x, int y, int w, int h) { XftColor c; alloc(spec, &c); XRenderColor rc = c.color; XftDrawRect(xd, &c, x, y, w, h); (void)rc; } /* --- drawing ------------------------------------------------------------ */ static int month_of(int i) { return atoi(rows[i].f[0] + 5); } static int dom_of(int i) { return atoi(rows[i].f[0] + 8); } static int dow_of(int i) { /* Sakamoto, on the ISO date in column 0. */ static const int t[] = {0,3,2,5,0,3,5,1,4,6,2,4}; int y = atoi(rows[i].f[0]), m = month_of(i), d = dom_of(i); if (m < 3) y--; return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; } static void redraw(void) { XWindowAttributes wa; XGetWindowAttributes(dpy, win, &wa); fill(col_bg, 0, 0, wa.width, wa.height); XftColor head, detail, rank1, grid; alloc(col_head, &head); alloc(col_detail, &detail); alloc(col_rank1, &rank1); alloc(col_grid, &grid); int mon = month_of(cur); char title[128]; snprintf(title, sizeof title, "%.4s %02d %s", rows[cur].f[0], mon, rows[cur].f[3]); text(&head, fnt_big, PAD, 26, title, wa.width - 2*PAD); static const char *dows[] = {"Dom","Fer II","Fer III","Fer IV","Fer V","Fer VI","Sab"}; for (int c = 0; c < 7; c++) text(&head, fnt, PAD + c*CELL_W + 4, HEAD_H - 6, dows[c], CELL_W - 8); /* first row index of this month, and where it starts in the week */ int first = 0; while (first < nrows && month_of(first) != mon) first++; int slot = dow_of(first); for (int i = first; i < nrows && month_of(i) == mon; i++, slot++) { int r = slot / 7, c = slot % 7; int x = PAD + c*CELL_W, y = HEAD_H + r*CELL_H; XftColor bg, fg; colours_for(rows[i].f[10], &bg, &fg); XftDrawRect(xd, &bg, x, y, CELL_W - 2, CELL_H - 2); if (i == cur) for (int k = 0; k < 3; k++) XftDrawRect(xd, &rank1, x+k, y+k, CELL_W-2-2*k, 1), XftDrawRect(xd, &rank1, x+k, y+CELL_H-3-k, CELL_W-2-2*k, 1), XftDrawRect(xd, &rank1, x+k, y+k, 1, CELL_H-2-2*k), XftDrawRect(xd, &rank1, x+CELL_W-3-k, y+k, 1, CELL_H-2-2*k); char d[8]; snprintf(d, sizeof d, "%d", dom_of(i)); text(&fg, fnt_big, x + 6, y + 20, d, 40); if (!strcmp(rows[i].f[8], "class-1")) text(&rank1, fnt, x + CELL_W - 26, y + 18, "I", 20); /* Feast name over up to three lines, breaking on spaces that fit the cell. Measured with XftTextExtents rather than guessed at a character count, so it is right for any font in config.h. */ const char *nm = rows[i].f[6]; int avail = CELL_W - 12, ty = y + 36; const char *p = nm; for (int ln = 0; ln < 3 && *p; ln++) { size_t take = strlen(p), best = 0; XGlyphInfo gi; for (size_t k = 1; k <= take; k++) { XftTextExtentsUtf8(dpy, fnt, (FcChar8 *)p, k, &gi); if (gi.xOff > avail) break; if (p[k] == ' ' || p[k] == 0) best = k; } if (!best) { /* one long word: hard cut */ for (size_t k = 1; k <= take; k++) { XftTextExtentsUtf8(dpy, fnt, (FcChar8 *)p, k, &gi); if (gi.xOff > avail) { best = k ? k - 1 : 1; break; } best = k; } } XftDrawStringUtf8(xd, &fg, fnt, x + 6, ty, (FcChar8 *)p, best); ty += 13; p += best; while (*p == ' ') p++; } } int dy = wa.height - DETAIL_H; XftDrawRect(xd, &grid, 0, dy - 1, wa.width, 1); char line[512]; snprintf(line, sizeof line, "%s %s", rows[cur].f[0], rows[cur].f[6]); text(&detail, fnt_big, PAD, dy + 22, line, wa.width - 2*PAD); snprintf(line, sizeof line, "%s ยท %s", rows[cur].f[9], rows[cur].f[11]); text(&head, fnt, PAD, dy + 42, line, wa.width - 2*PAD); snprintf(line, sizeof line, "Epistola %s", rows[cur].f[13]); text(&detail, fnt, PAD, dy + 62, line, wa.width - 2*PAD); snprintf(line, sizeof line, "Evangelium %s", rows[cur].f[14]); text(&detail, fnt, PAD, dy + 80, line, wa.width - 2*PAD); } int main(void) { load(); if (!nrows) { fputs("colitur-x: no rows on stdin\n", stderr); return 2; } if (!(dpy = XOpenDisplay(NULL))) { fputs("cannot open display\n", stderr); return 1; } scr = DefaultScreen(dpy); vis = DefaultVisual(dpy, scr); cmap = DefaultColormap(dpy, scr); int w = PAD*2 + 7*CELL_W, h = HEAD_H + 6*CELL_H + DETAIL_H; win = XCreateSimpleWindow(dpy, RootWindow(dpy, scr), 0, 0, w, h, 0, BlackPixel(dpy, scr), BlackPixel(dpy, scr)); XStoreName(dpy, win, "colitur"); { /* Ask the window manager for exactly the grid we drew. Without this a tiling or maximising WM picks its own size and the grid floats in a field of background. */ XSizeHints *sh = XAllocSizeHints(); sh->flags = PSize | PMinSize; sh->width = w; sh->height = h; sh->min_width = w; sh->min_height = h; XSetWMNormalHints(dpy, win, sh); XFree(sh); } XSelectInput(dpy, win, ExposureMask | KeyPressMask | StructureNotifyMask); XMapWindow(dpy, win); fnt = XftFontOpenName(dpy, scr, font); fnt_big = XftFontOpenName(dpy, scr, font_big); xd = XftDrawCreate(dpy, win, vis, cmap); for (XEvent ev;;) { XNextEvent(dpy, &ev); if (ev.type == Expose || ev.type == ConfigureNotify) redraw(); else if (ev.type == KeyPress) { KeySym k = XLookupKeysym(&ev.xkey, 0); int mon = month_of(cur); if (k == XK_q) break; else if (k == XK_Right || k == XK_l) cur = (cur + 1) % nrows; else if (k == XK_Left || k == XK_h) cur = (cur + nrows - 1) % nrows; else if (k == XK_Down || k == XK_j) cur = (cur + 7) % nrows; else if (k == XK_Up || k == XK_k) cur = (cur + nrows - 7) % nrows; else if (k == XK_n) { while (cur < nrows-1 && month_of(cur) == mon) cur++; } else if (k == XK_p) { while (cur > 0 && month_of(cur) == mon) cur--; mon = month_of(cur); while (cur > 0 && month_of(cur-1) == mon) cur--; } redraw(); } } XCloseDisplay(dpy); return 0; }