aboutsummaryrefslogtreecommitdiff
path: root/colitur-x.c
blob: 06f2f1af2d1d38e4b0466a17dc46881f523dba93 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#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;
}

/* The header line is DETECTED, not assumed. Skipping the first line
   unconditionally punishes the obvious way to look at one month --
   `... | grep "^2027-04" | colitur-x` -- by silently eating the 1st,
   which is a filter this program should compose with rather than fight.
   A header is exactly the line beginning "date,"; anything else is a day. */
static int is_header(const char *line)
{
	return strncmp(line, "date,", 5) == 0;
}

static void load(void)
{
	char buf[4096];
	int first = 1;
	while (nrows < MAXDAYS && fgets(buf, sizeof buf, stdin)) {
		buf[strcspn(buf, "\n")] = 0;
		if (first && is_header(buf)) { first = 0; continue; }
		first = 0;
		if (buf[0] == 0) continue;
		char *dup = strdup(buf);
		if (!dup) break;
		if (split(dup, rows[nrows].f) >= 12) nrows++; else free(dup);
	}
}

/* --- 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;
}