/* clectio -- tiny, fast, suckless daily readings. * * Computes nothing at runtime: the liturgical calendar, its reading citations * and the scripture text are all compiled in (see gen/ and the Makefile). For a * date it does a table lookup and prints the day and its readings. Configuration * is compile-time (config.h), suckless-style: edit and recompile. * * Public domain. The compiled scripture corpus is NOT part of this program's * licence (see README): the default is the public-domain Latin Vulgate. */ #define _POSIX_C_SOURCE 200809L /* localtime_r, isatty under -std=c99 */ #define _DEFAULT_SOURCE /* struct winsize / TIOCGWINSZ under -std=c99 */ #include "config.h" #include LITURGY /* gen/liturgy_
.h: names,parts,cites_*,vpool,readings,rpool,days,cal,EPOCH,NDAYS */ #include "text.lz.h" /* embedded DEFLATE text blob: text_lz[], text_lz_len */ #include "puff.h" #include #include #include #include #include #include static char *textbuf; /* inflated verse text */ static const char **verse; /* verse[i] -> the text of the i-th key, nul-terminated */ static int usecolor; static const char *const colname[] = { "green", "white", "red", "violet", "rose", "black" }; static const char *const colansi[] = { "\033[32m", "\033[37m", "\033[31m", "\033[35m", "\033[95m", "\033[90m" }; /* Local calendar overrides: replace one fixed date's name + colour. The colour * names below match colname[]/colansi[] above (index order). The user table * lives in calendar.h (copied from calendar.def.h); see it for the limits. */ enum { GREEN, WHITE, RED, VIOLET, ROSE, BLACK }; typedef struct { int mon, day, colour; const char *name; } Override; #include "calendar.h" /* static const Override overrides[]; ends with a {0,...} row */ static const Override *find_override(int mon, int day) { const Override *o; for (o = overrides; o->mon; o++) if (o->mon == mon && o->day == day) return o; return NULL; } /* Days since the civil epoch 1970-01-01 (Howard Hinnant's algorithm). */ static long days_from_civil(long y, unsigned m, unsigned d) { y -= m <= 2; long era = (y >= 0 ? y : y - 399) / 400; unsigned yoe = (unsigned)(y - era * 400); unsigned doy = (153u * (m + (m > 2 ? -3u : 9u)) + 2u) / 5u + d - 1; unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; return era * 146097 + (long)doe - 719468; } static void load_text(void) { unsigned long rawlen = text_lz[0] | text_lz[1] << 8 | text_lz[2] << 16 | (unsigned long)text_lz[3] << 24; unsigned long dstlen = rawlen, srclen = text_lz_len - 4; size_t cap = 8192, n = 0; char *p; textbuf = malloc(rawlen + 1); puff((unsigned char *)textbuf, &dstlen, text_lz + 4, &srclen); textbuf[dstlen] = 0; verse = malloc(cap * sizeof(*verse)); verse[n++] = textbuf; for (p = textbuf; *p; p++) if (*p == '\n') { *p = 0; if (p[1]) { if (n == cap) { cap *= 2; verse = realloc(verse, cap * sizeof(*verse)); } verse[n++] = p + 1; } } } static const char *citation(int r) { #ifdef SIGLA_LATIN return cites_la[readings_cite_la[r]]; #else return cites_en[readings[r].cite]; #endif } /* Column width of the terminal on stdout, or 72 if it cannot be determined. */ static int termwidth(void) { struct winsize ws; if (ioctl(1, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return ws.ws_col; return 72; } /* Print s wrapped at w columns, breaking on spaces (greedy). w <= 0 prints s * unwrapped. UTF-8 aware: continuation bytes (0x80-0xBF) do not count as a * column, so multibyte letters measure as one. */ static void putwrapped(const char *s, int w) { if (w <= 0) { printf("%s\n", s); return; } while (*s) { const char *p = s, *brk = NULL; int col = 0; while (*p && col < w) { if (*p == ' ') brk = p; if ((*p & 0xC0) != 0x80) col++; p++; } if (!*p) { /* the rest fits on one line */ printf("%s\n", s); return; } if (!brk) /* a single word longer than w: hard-break at the column */ brk = p; fwrite(s, 1, (size_t)(brk - s), stdout); putchar('\n'); for (s = brk; *s == ' '; s++) /* skip the space(s) we broke on */ ; } } /* One line per day of month `mon` in year `y`: weekday, day, colour, celebration. * Needs no scripture text, so it never touches the text blob. Returns an exit * code (1 if the month falls outside the compiled range). */ static int print_month(int y, int mon) { static const char *const wd[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; long base = days_from_civil(EPOCH_Y, EPOCH_M, EPOCH_D); long civ1 = days_from_civil(y, (unsigned)mon, 1); long nextfirst = days_from_civil(mon == 12 ? y + 1 : y, (unsigned)(mon == 12 ? 1 : mon + 1), 1); int dim = (int)(nextfirst - civ1); /* days in the month (leap-correct) */ long off1 = civ1 - base; int day, n; if (off1 < 0 || off1 + dim - 1 >= NDAYS) { fprintf(stderr, "clectio: %04d-%02d is outside the compiled range %d..%d\n", y, mon, EPOCH_Y, EPOCH_Y + (NDAYS / 366)); return 1; } n = printf("%04d-%02d %s\n", y, mon, FORMNAME) - 1; /* underline that line */ while (n-- > 0) putchar('='); putchar('\n'); for (day = 1; day <= dim; day++) { long civ = civ1 + (day - 1); const Day *d = &days[cal[off1 + (day - 1)]]; const Override *ov = find_override(mon, day); const char *dname = ov ? ov->name : names[d->name]; int dcol = ov ? ov->colour : d->colour; int w = (int)((civ + 4) % 7); /* 1970-01-01 was Thursday; 0 = Sunday */ printf("%s %02d %-6s ", wd[w], day, colname[dcol]); if (usecolor) printf("%s%s\033[0m\n", colansi[dcol], dname); else printf("%s\n", dname); } return 0; } /* Parse a --month spec ("YYYY-MM", or "N" for month N of the current year, or * NULL for the current month) and print it. Returns an exit code. */ static int run_month(const char *spec) { struct tm tm; time_t t = time(NULL); int y, mon; localtime_r(&t, &tm); y = tm.tm_year + 1900; mon = tm.tm_mon + 1; if (spec) { int ok = strchr(spec, '-') ? sscanf(spec, "%d-%d", &y, &mon) == 2 : sscanf(spec, "%d", &mon) == 1; if (!ok) { fprintf(stderr, "clectio: bad month %s (want YYYY-MM or 1-12)\n", spec); return 2; } } if (mon < 1 || mon > 12) { fprintf(stderr, "clectio: bad month %s (want YYYY-MM or 1-12)\n", spec ? spec : ""); return 2; } return print_month(y, mon); } static void print_day(long off, const char *datestr, int mon, int day, int gospel, int wrap) { const Day *d = &days[cal[off]]; const Override *ov = find_override(mon, day); const char *dname = ov ? ov->name : names[d->name]; int dcol = ov ? ov->colour : d->colour; unsigned ri; int n; if (usecolor) printf("%s%s\033[0m\n", colansi[dcol], dname); else printf("%s \xc2\xb7 %s\n", dname, colname[dcol]); n = printf("%s for %s\n", FORMNAME, datestr) - 1; /* underline that line */ while (n-- > 0) putchar('='); putchar('\n'); for (ri = 0; ri < d->rlen; ri++) { const Reading *r = &readings[rpool[d->roff + ri]]; unsigned v; if (gospel && strcmp(parts[r->part], "Gospel") != 0) continue; printf("\n%s (%s)\n\n", parts[r->part], citation(rpool[d->roff + ri])); for (v = 0; v < r->vlen; v++) putwrapped(verse[vpool[r->voff + v]], wrap); } } int main(int argc, char **argv) { struct tm tm; long off; char datestr[16]; int y, m, dd, i; int gospel = GOSPEL_ONLY; /* compiled default, overridable per run */ int istty = isatty(1); int wrap, monthmode = 0; const char *datearg = NULL; usecolor = COLOR && istty; for (i = 1; i < argc; i++) { if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) { fprintf(stderr, "usage: clectio [-a|--all | -g|--gospel] [YYYY-MM-DD]\n" " clectio --month [YYYY-MM | N]\n" " -a, --all print all readings\n" " -g, --gospel print the Gospel only\n" " --month list a whole month (default: current month)\n" " (default: today, %s)\n" " compiled: %s, %s, wrap %s, range %d-%d\n", READMODE, FORMNAME, SIGLANAME, WRAPNAME, EPOCH_Y, EPOCH_Y + (NDAYS / 366)); return 0; } else if (!strcmp(argv[i], "-a") || !strcmp(argv[i], "--all")) { gospel = 0; } else if (!strcmp(argv[i], "-g") || !strcmp(argv[i], "--gospel")) { gospel = 1; } else if (!strcmp(argv[i], "--month")) { monthmode = 1; } else if (argv[i][0] == '-' && argv[i][1]) { fprintf(stderr, "clectio: unknown option %s (try -h)\n", argv[i]); return 2; } else if (!datearg) { datearg = argv[i]; } else { fprintf(stderr, "clectio: unexpected argument %s (try -h)\n", argv[i]); return 2; } } if (monthmode) return run_month(datearg); /* month overview: no readings, no text loaded */ if (datearg) { if (sscanf(datearg, "%d-%d-%d", &y, &m, &dd) != 3) { fprintf(stderr, "clectio: bad date %s (want YYYY-MM-DD)\n", datearg); return 2; } snprintf(datestr, sizeof datestr, "%04d-%02d-%02d", y, m, dd); } else { time_t t = time(NULL); localtime_r(&t, &tm); y = tm.tm_year + 1900; m = tm.tm_mon + 1; dd = tm.tm_mday; strftime(datestr, sizeof datestr, "%Y-%m-%d", &tm); } off = days_from_civil(y, (unsigned)m, (unsigned)dd) - days_from_civil(EPOCH_Y, EPOCH_M, EPOCH_D); if (off < 0 || off >= NDAYS) { fprintf(stderr, "clectio: %s is outside the compiled range %d..%d\n", datestr, EPOCH_Y, EPOCH_Y + (NDAYS / 366)); return 1; } /* WRAP>0: always wrap to that width. WRAP==0: fit the terminal, but leave * piped output as one line per verse (0 -> putwrapped prints unwrapped). */ if (WRAP > 0) wrap = WRAP; else wrap = istty ? termwidth() : 0; load_text(); print_day(off, datestr, m, dd, gospel, wrap); return 0; }