aboutsummaryrefslogtreecommitdiff
path: root/colitur-x.c
diff options
context:
space:
mode:
Diffstat (limited to 'colitur-x.c')
-rw-r--r--colitur-x.c256
1 files changed, 256 insertions, 0 deletions
diff --git a/colitur-x.c b/colitur-x.c
new file mode 100644
index 0000000..def42fa
--- /dev/null
+++ b/colitur-x.c
@@ -0,0 +1,256 @@
+#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 <X11/Xlib.h>
+#include <X11/Xft/Xft.h>
+#include <X11/keysym.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#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;
+}