aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile5
-rw-r--r--README.md37
-rw-r--r--colitur-x.c256
-rw-r--r--config.h23
4 files changed, 321 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..c74426a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,5 @@
+CFLAGS = -std=c99 -Os -Wall -Wextra $(shell pkg-config --cflags xft x11)
+LDLIBS = $(shell pkg-config --libs xft x11)
+colitur-x: colitur-x.c config.h
+ $(CC) $(CFLAGS) -o $@ colitur-x.c $(LDLIBS)
+clean:; rm -f colitur-x
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e7b8c6e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# colitur-x
+
+A raw Xlib front-end for [colitur](../colitur): reads `colitur emit --format
+csv` on stdin, draws the year as a month grid coloured by each day's
+liturgical colour, and shows the selected day's readings underneath.
+
+ colitur emit --format csv --from 2027 --to 2027 | colitur-x
+
+Keys: arrows or `hjkl` move, `n`/`p` change month, `q` quits.
+
+## Why it exists
+
+colitur is a Unix tool: it computes and prints. This is a separate program
+that composes with it, so colitur's own dependencies stay frozen
+(`dune alcotest qcheck qcheck-alcotest sexplib ppx_sexp_conv`) and its
+kernel stays pure.
+
+## Build
+
+ make
+
+`-std=c99 -Os -Wall -Wextra`, linking libX11 and libXft. ~17 KB.
+
+Configuration is compile-time, suckless style: edit `config.h` and rebuild.
+There is deliberately no runtime config file — colitur itself owns the
+calendar's configuration (language, sigla, overlays); this program owns only
+how it is painted.
+
+## Limits, stated rather than discovered
+
+- X11 only. Runs under XWayland but is not a Wayland client.
+- Read-only. It renders what colitur emits; it edits nothing.
+- Xft is used for antialiasing and UTF-8. Dropping it for core X11 fonts
+ would reduce the dependency list to libX11 and libc alone, at the cost of
+ correct rendering for `æ`/`œ` and Polish diacritics. Not worth it.
+- A tiling window manager will ignore the size hints and give the grid its
+ own tile; the layout handles that, the empty space is the WM's choice.
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;
+}
diff --git a/config.h b/config.h
new file mode 100644
index 0000000..5b242b4
--- /dev/null
+++ b/config.h
@@ -0,0 +1,23 @@
+/* colitur-x -- compile-time configuration, suckless style.
+ * Edit, `make`, done. There is no runtime config file by design. */
+
+static const char *font = "monospace:size=10";
+static const char *font_big = "monospace:size=14:bold";
+
+/* Liturgical colours, as the engine names them -> what to paint. */
+static const struct { const char *name, *bg, *fg; } palette[] = {
+ { "white", "#f7f5ef", "#1a1a1a" },
+ { "red", "#b03030", "#ffffff" },
+ { "violet", "#5a3a78", "#ffffff" },
+ { "green", "#2f6b3a", "#ffffff" },
+ { "black", "#202020", "#e0e0e0" },
+ { "rose", "#d9a0b0", "#1a1a1a" },
+};
+
+static const char *col_bg = "#141414"; /* window background */
+static const char *col_grid = "#3a3a3a"; /* cell borders */
+static const char *col_head = "#8a8a8a"; /* weekday headings */
+static const char *col_detail = "#e8e8e8"; /* detail pane text */
+static const char *col_rank1 = "#ffcc55"; /* I-class marker */
+
+enum { CELL_W = 122, CELL_H = 74, PAD = 10, HEAD_H = 46, DETAIL_H = 92 };