diff options
Diffstat (limited to 'colitur-x.c')
| -rw-r--r-- | colitur-x.c | 256 |
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; +} |
