aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukasz@arcofasiagroup.com>2026-08-21 09:58:05 +0200
committerLukasz Kasprzak <lukasz@arcofasiagroup.com>2026-08-21 09:58:05 +0200
commit5312537c9fc12eed4d14d89d9cc1a226f2df94c3 (patch)
treec05b34d3cc1fb35b399aa9f2ecdabc4bb7138178
parent147932ae2da02b5eceb6be6e1d704e1199bcb72e (diff)
downloadcolitur-x-5312537c9fc12eed4d14d89d9cc1a226f2df94c3.tar.gz
colitur-x-5312537c9fc12eed4d14d89d9cc1a226f2df94c3.zip
fix(colitur-x): make the selection and the I-class marker legible
Both markers were drawn in gold, which works on a dark cell and very nearly disappears on a white one -- and most days are white. The selection is now ringed twice, dark outside and bright inside, so it reads against a white cell and a red one without knowing which it is. The I-class marker is a badge rather than coloured text: filled with the cell's own foreground and lettered in its background, so it inverts against whatever the day's liturgical colour is. It also opens on TODAY when the input covers it, which is what a viewer should do; colitur's kernel still never reads a clock. Drawing now goes to an off-screen pixmap and CX_DUMP writes one frame out, so the result can be checked without screenshotting a window whose contents X does not retain while it is obscured.
-rw-r--r--README.md7
-rwxr-xr-xcolitur-xbin17968 -> 27024 bytes
-rw-r--r--colitur-x.c106
-rw-r--r--config.h14
4 files changed, 115 insertions, 12 deletions
diff --git a/README.md b/README.md
index 9df7b8e..6530ec9 100644
--- a/README.md
+++ b/README.md
@@ -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 b/colitur-x
index b5db32a..a49ed72 100755
--- a/colitur-x
+++ b/colitur-x
Binary files differ
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);
diff --git a/config.h b/config.h
index 5b242b4..6fe5857 100644
--- a/config.h
+++ b/config.h
@@ -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 };