diff options
| -rw-r--r-- | README.md | 7 | ||||
| -rwxr-xr-x | colitur-x | bin | 17968 -> 27024 bytes | |||
| -rw-r--r-- | colitur-x.c | 106 | ||||
| -rw-r--r-- | config.h | 14 |
4 files changed, 115 insertions, 12 deletions
@@ -9,6 +9,13 @@ liturgical colour, and shows the selected day's readings underneath. Keys: arrows or `hjkl` move, `n`/`p` change month, `q` quits. +It opens on **today** when the input covers it. Reading the clock is a +presentation choice and belongs in a viewer: colitur's own kernel is pure +and deliberately never asks what day it is, which is what lets it be tested +to the year 9999. + + CX_DUMP=/tmp/f.ppm colitur-x < day.csv # write one frame, then exit + It reads CSV on **stdin** and nothing else, so the shell decides what it shows. One month: diff --git a/colitur-x.c b/colitur-x.c index 06f2f1a..03e2793 100644 --- a/colitur-x.c +++ b/colitur-x.c @@ -15,6 +15,7 @@ #include <stdio.h> #include <stdlib.h> #include <string.h> +#include <time.h> #include "config.h" @@ -29,6 +30,9 @@ static int cur; /* selected row */ static Display *dpy; static Window win; +static Pixmap buf; /* draw off-screen, then blit */ +static GC gc; +static int bufw, bufh; static int scr; static XftFont *fnt, *fnt_big; static XftDraw *xd; @@ -80,6 +84,21 @@ static void load(void) } } +/* Start on TODAY if the input covers it. Reading the clock is a + presentation choice and belongs here: colitur's own kernel is pure and + deliberately never asks what day it is, which is what makes it testable + to the year 9999. A viewer has no such duty. */ +static void select_today(void) +{ + time_t t = time(NULL); + struct tm tm; + if (!localtime_r(&t, &tm)) return; + char iso[11]; + strftime(iso, sizeof iso, "%Y-%m-%d", &tm); + for (int i = 0; i < nrows; i++) + if (!strcmp(rows[i].f[0], iso)) { cur = i; return; } +} + /* --- colour helpers ----------------------------------------------------- */ static void alloc(const char *spec, XftColor *c) { @@ -109,6 +128,17 @@ static void text(XftColor *c, XftFont *f, int x, int y, const char *s, int maxw) XftDrawStringUtf8(xd, c, f, x, y, (FcChar8 *)s, len); } +/* An n-pixel ring just inside (x,y,w,h). */ +static void ring(XftColor *c, int x, int y, int w, int h, int n) +{ + for (int k = 0; k < n; k++) { + XftDrawRect(xd, c, x + k, y + k, w - 2*k, 1); + XftDrawRect(xd, c, x + k, y + h - 1 - k, w - 2*k, 1); + XftDrawRect(xd, c, x + k, y + k, 1, h - 2*k); + XftDrawRect(xd, c, x + w - 1 - k, y + k, 1, h - 2*k); + } +} + static void fill(const char *spec, int x, int y, int w, int h) { XftColor c; alloc(spec, &c); @@ -130,15 +160,48 @@ static int dow_of(int i) return (y + y/4 - y/100 + y/400 + t[m-1] + d) % 7; } +/* CX_DUMP=path writes one frame as a PPM and exits. It reads back the + off-screen PIXMAP, not the window: a window's contents are not retained + while it is obscured or on an unfocused tag, so screenshotting one can + return solid black while the program draws perfectly. */ +static void dump_and_exit(const char *path, int w, int h) +{ + XImage *im = XGetImage(dpy, buf, 0, 0, w, h, AllPlanes, ZPixmap); + FILE *f = im ? fopen(path, "wb") : NULL; + if (!f) { fprintf(stderr, "CX_DUMP: cannot write %s\n", path); exit(1); } + fprintf(f, "P6\n%d %d\n255\n", w, h); + for (int y = 0; y < h; y++) + for (int x = 0; x < w; x++) { + unsigned long px = XGetPixel(im, x, y); + unsigned char rgb[3] = { (unsigned char)((px >> 16) & 0xff), + (unsigned char)((px >> 8) & 0xff), + (unsigned char)(px & 0xff) }; + fwrite(rgb, 1, 3, f); + } + fclose(f); + XDestroyImage(im); + fprintf(stderr, "CX_DUMP: wrote %s (%dx%d)\n", path, w, h); + exit(0); +} + static void redraw(void) { XWindowAttributes wa; XGetWindowAttributes(dpy, win, &wa); + if (!buf || wa.width != bufw || wa.height != bufh) { + if (xd) { XftDrawDestroy(xd); xd = NULL; } + if (buf) { XFreePixmap(dpy, buf); } + buf = XCreatePixmap(dpy, win, wa.width, wa.height, + DefaultDepth(dpy, DefaultScreen(dpy))); + xd = XftDrawCreate(dpy, buf, vis, cmap); + bufw = wa.width; bufh = wa.height; + } fill(col_bg, 0, 0, wa.width, wa.height); - XftColor head, detail, rank1, grid; + XftColor head, detail, sel_dark, sel_light, grid; alloc(col_head, &head); alloc(col_detail, &detail); - alloc(col_rank1, &rank1); alloc(col_grid, &grid); + alloc(col_sel_dark, &sel_dark); alloc(col_sel_light, &sel_light); + alloc(col_grid, &grid); int mon = month_of(cur); char title[128]; @@ -162,17 +225,30 @@ static void redraw(void) 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); + if (i == cur) { + /* dark ring outside, bright ring inside: readable on a white + cell and on a red one without knowing which it is */ + ring(&sel_dark, x, y, CELL_W - 2, CELL_H - 2, SEL_RING); + ring(&sel_light, x + SEL_RING, y + SEL_RING, + CELL_W - 2 - 2*SEL_RING, CELL_H - 2 - 2*SEL_RING, SEL_RING); + } 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); + if (!strcmp(rows[i].f[8], "class-1")) { + /* badge: filled with the cell's FOREGROUND, glyph in its + BACKGROUND, so it inverts against any liturgical colour */ + int gw = 0, bw2, bx, by = y + 5, bh2 = 17; + XGlyphInfo gi; + XftTextExtentsUtf8(dpy, fnt, (FcChar8 *)rank1_glyph, + strlen(rank1_glyph), &gi); + gw = gi.xOff; + bw2 = gw + 10; + bx = x + CELL_W - 4 - bw2; + XftDrawRect(xd, &fg, bx, by, bw2, bh2); + XftDrawStringUtf8(xd, &bg, fnt, bx + 5, by + 13, + (FcChar8 *)rank1_glyph, strlen(rank1_glyph)); + } /* Feast name over up to three lines, breaking on spaces that fit the cell. Measured with XftTextExtents rather than guessed at a @@ -213,12 +289,19 @@ static void redraw(void) 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); + + XCopyArea(dpy, buf, win, gc, 0, 0, wa.width, wa.height, 0, 0); + { + const char *p = getenv("CX_DUMP"); + if (p) dump_and_exit(p, wa.width, wa.height); + } } int main(void) { load(); if (!nrows) { fputs("colitur-x: no rows on stdin\n", stderr); return 2; } + select_today(); if (!(dpy = XOpenDisplay(NULL))) { fputs("cannot open display\n", stderr); return 1; } scr = DefaultScreen(dpy); @@ -244,7 +327,8 @@ int main(void) fnt = XftFontOpenName(dpy, scr, font); fnt_big = XftFontOpenName(dpy, scr, font_big); - xd = XftDrawCreate(dpy, win, vis, cmap); + gc = XCreateGC(dpy, win, 0, NULL); + /* xd is created against the off-screen pixmap in redraw() */ for (XEvent ev;;) { XNextEvent(dpy, &ev); @@ -18,6 +18,18 @@ 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 */ +/* The selected day is ringed TWICE: a dark ring outside a bright one, so + the marker survives on a white cell and on a red one alike. A single + bright ring vanishes against white; a single dark one vanishes against + black. */ +static const char *col_sel_dark = "#1a1a1a"; +static const char *col_sel_light = "#ffd24a"; +enum { SEL_RING = 3 }; /* px per ring */ + +/* A I-class day is marked with a BADGE, not coloured text: the badge is + filled with the cell's own foreground and the glyph drawn in its + background, so it inverts against whatever the day's colour is. Gold on + white -- the first attempt -- was very nearly invisible. */ +static const char *rank1_glyph = "I"; enum { CELL_W = 122, CELL_H = 74, PAD = 10, HEAD_H = 46, DETAIL_H = 92 }; |
