oboeru

a collection of simple, scriptable flashcard programs
git clone anongit@rnpnr.xyz:oboeru.git
Log | Files | Refs | Feed | README | LICENSE

oboeru.c (7120B)


      1 /* See LICENSE for license details. */
      2 #include <fcntl.h>
      3 #include <limits.h>
      4 #include <signal.h>
      5 #include <stdint.h>
      6 #include <stdio.h>
      7 #include <stdlib.h>
      8 #include <string.h>
      9 #include <sys/stat.h>
     10 #include <sys/types.h>
     11 #include <time.h>
     12 #include <unistd.h>
     13 
     14 #include "arg.h"
     15 #include "util.h"
     16 #include "config.h"
     17 
     18 #define BUF_SIZE BUFSIZ
     19 
     20 enum {
     21 	CARD_PASS,
     22 	CARD_FAIL
     23 };
     24 
     25 typedef struct {
     26 	int64_t reviewed, due;
     27 	size_t id, deck;
     28 	char *extra;
     29 	uint8_t leeches, nobump;
     30 } Card;
     31 
     32 typedef struct node {
     33 	Card *card;
     34 	struct node *next;
     35 } Node;
     36 
     37 static const char *scanfmt = "%ld" DELIM "%[^"DELIM"]" DELIM "%[^"DELIM"]" DELIM "%d" DELIM "%[^\n]";
     38 static const char *logfmt = CARDID DELIM "%s" DELIM "%s" DELIM "%d" DELIM "%s\n";
     39 
     40 static Node *head;
     41 static size_t n_reviews, n_reviewed;
     42 
     43 /* option parsing variables */
     44 char *argv0;
     45 static int cflag;
     46 static int dflag;
     47 
     48 static void
     49 usage(void)
     50 {
     51 	die("usage: %s [-cd] pipe deck [deck1 ...]\n", argv0);
     52 }
     53 
     54 static void
     55 sighandler(const int signo)
     56 {
     57 	fprintf(stderr, remfmt, n_reviews - n_reviewed);
     58 }
     59 
     60 static void
     61 freenodes(Node *node)
     62 {
     63 	if (node->next)
     64 		freenodes(node->next);
     65 
     66 	if (node->card) {
     67 		free(node->card->extra);
     68 		free(node->card);
     69 	}
     70 	free(node);
     71 	node = NULL;
     72 }
     73 
     74 static void
     75 cleanup(Node *node, void *decks, void *reviews)
     76 {
     77 	freenodes(node);
     78 	free(decks);
     79 	free(reviews);
     80 }
     81 
     82 /* returns a filled out Card * after parsing */
     83 static Card *
     84 parse_line(const char *line)
     85 {
     86 	struct tm tm;
     87 	char reviewed[BUF_SIZE], due[BUF_SIZE];
     88 	Card *c = xmalloc(sizeof(Card));
     89 	c->extra = xmalloc(BUF_SIZE);
     90 
     91 	sscanf(line, scanfmt, &c->id, reviewed, due, &c->leeches, c->extra);
     92 
     93 	memset(&tm, 0, sizeof(tm));
     94 	strptime(reviewed, timefmt, &tm);
     95 	c->reviewed = timegm(&tm);
     96 
     97 	memset(&tm, 0, sizeof(tm));
     98 	strptime(due, timefmt, &tm);
     99 	c->due = timegm(&tm);
    100 
    101 	c->nobump = 0;
    102 
    103 	return c;
    104 }
    105 
    106 /* returns the latest allocated node with no card */
    107 static Node *
    108 parse_file(const char *file, Node *node, size_t deck_id)
    109 {
    110 	FILE *fp;
    111 	char *line = NULL;
    112 	size_t len = 0;
    113 
    114 	fp = fopen(file, "r");
    115 	if (!fp)
    116 		die("fopen(%s)\n", file);
    117 
    118 	for (; getline(&line, &len, fp) != -1; node = node->next = xmalloc(sizeof(Node))) {
    119 		node->next = NULL;
    120 		node->card = parse_line(line);
    121 		if (node->card)
    122 			node->card->deck = deck_id;
    123 	}
    124 
    125 	free(line);
    126 	fclose(fp);
    127 
    128 	return node;
    129 }
    130 
    131 static int
    132 needs_review(Card *card)
    133 {
    134 	time_t t;
    135 
    136 	if (card->leeches >= MAX_LEECHES)
    137 		return 0;
    138 
    139 	t = time(NULL);
    140 
    141 	if (card->due > t)
    142 		return 0;
    143 
    144 	return 1;
    145 }
    146 
    147 static Card **
    148 add_review(Card *r[], Card *c)
    149 {
    150 	r = xreallocarray(r, ++n_reviews, sizeof(Card **));
    151 	r[n_reviews - 1] = c;
    152 
    153 	return r;
    154 }
    155 
    156 static Card **
    157 mkreviews(Node *node)
    158 {
    159 	Card **r = NULL;
    160 
    161 	for (; node; node = node->next)
    162 		if (node->card && needs_review(node->card))
    163 			r = add_review(r, node->card);
    164 
    165 	return r;
    166 }
    167 
    168 static int8_t
    169 bump_card(Card *card, int8_t status)
    170 {
    171 	int64_t diff;
    172 	time_t t = time(NULL);
    173 
    174 	if (card->nobump && status != CARD_FAIL)
    175 		return 0;
    176 
    177 	diff = card->due - card->reviewed;
    178 	if (diff < 0)
    179 		fprintf(stderr, "card id: %ld: malformed review time\n", card->id);
    180 
    181 	/* only hit this on the first time through */
    182 	if (!card->nobump)
    183 		card->reviewed = t;
    184 
    185 	switch (status) {
    186 	case CARD_PASS:
    187 		/* - 1s to avoid rounding error */
    188 		if (diff < MINIMUM_INCREASE - 1) {
    189 			card->due = t + MINIMUM_INCREASE;
    190 			return 1;
    191 		} else if (diff > LEECH_AGE) {
    192 			card->due = t + diff * GROWTH_RATE + 24 * 3600 * (rand() % 2);
    193 		} else {
    194 			card->due = t + diff * GROWTH_RATE;
    195 		}
    196 		break;
    197 	case CARD_FAIL:
    198 		if (diff > LEECH_AGE && !card->nobump)
    199 			card->leeches++;
    200 
    201 		if (diff * SHRINK_RATE < MINIMUM_INCREASE - 1)
    202 			card->due = t + MINIMUM_INCREASE;
    203 		else
    204 			card->due = t + diff * SHRINK_RATE;
    205 		return 1;
    206 	}
    207 
    208 	return 0;
    209 }
    210 
    211 static void
    212 shuffle_reviews(Card *r[], size_t n)
    213 {
    214 	size_t i, j;
    215 	Card *t;
    216 
    217 	if (n <= 1)
    218 		return;
    219 
    220 	srand(time(NULL));
    221 
    222 	/* this could be improved */
    223 	for (i = 0; i < n; i++) {
    224 		j = i + rand() % (n - i);
    225 
    226 		t = r[j];
    227 		r[j] = r[i];
    228 		r[i] = t;
    229 	}
    230 }
    231 
    232 static Card **
    233 review_loop(Card *r[], const char *decks[], const char *fifo)
    234 {
    235 	char reply[BUF_SIZE];
    236 	int fd;
    237 	size_t i, j, n;
    238 	struct timespec wait = { .tv_sec = 0, .tv_nsec = 50e6 };
    239 	struct { const char *str; int status; } reply_map[] = {
    240 		{ .str = "pass", .status = CARD_PASS },
    241 		{ .str = "fail", .status = CARD_FAIL }
    242 	};
    243 
    244 	for (i = 0; i < n_reviews; i++, n_reviewed++) {
    245 		fprintf(stdout, "%s\t"CARDID"\n", decks[r[i]->deck], r[i]->id);
    246 		/* force a flush before blocking in open() */
    247 		fflush(stdout);
    248 
    249 		reply[0] = 0;
    250 		fd = open(fifo, O_RDONLY);
    251 		if (fd == -1)
    252 			break;
    253 		n = read(fd, reply, sizeof(reply));
    254 		close(fd);
    255 
    256 		/* strip a trailing newline */
    257 		if (reply[n-1] == '\n')
    258 			reply[n-1] = 0;
    259 
    260 		if (!strcmp(reply, "quit"))
    261 			break;
    262 
    263 		for (j = 0; j < LEN(reply_map); j++)
    264 			if (!strcmp(reply, reply_map[j].str))
    265 				r[i]->nobump = bump_card(r[i], reply_map[j].status);
    266 
    267 		/* if the card wasn't bumped it needs an extra review */
    268 		if (r[i]->nobump) {
    269 			r = add_review(r, r[i]);
    270 			/* r[i+1] exists because we have added a review */
    271 			shuffle_reviews(&r[i + 1], n_reviews - i - 1);
    272 		}
    273 
    274 		/* give the writing process time to close its fd */
    275 		nanosleep(&wait, NULL);
    276 	}
    277 	return r;
    278 }
    279 
    280 static void
    281 write_deck(const char *deck, size_t deck_id)
    282 {
    283 	FILE *fp;
    284 	Node *node;
    285 	Card *c;
    286 	char reviewed[BUF_SIZE], due[BUF_SIZE];
    287 	char path[PATH_MAX];
    288 
    289 	if (dflag) {
    290 		snprintf(path, sizeof(path), "%s.debug", deck);
    291 		fp = fopen(path, "w+");
    292 	} else {
    293 		fp = fopen(deck, "w");
    294 	}
    295 
    296 	if (!fp)
    297 		return;
    298 
    299 	for (node = head; node; node = node->next) {
    300 		c = node->card;
    301 		if (!c || (c->deck != deck_id))
    302 			continue;
    303 
    304 		strftime(reviewed, sizeof(reviewed), timefmt,
    305 			gmtime((time_t *)&c->reviewed));
    306 		strftime(due, sizeof(due), timefmt,
    307 			gmtime((time_t *)&c->due));
    308 
    309 		fprintf(fp, logfmt, c->id, reviewed, due,
    310 			c->leeches, c->extra);
    311 	}
    312 	fclose(fp);
    313 }
    314 
    315 
    316 int
    317 main(int argc, char *argv[])
    318 {
    319 	Node *tail;
    320 	Card **reviews;
    321 	size_t i, n_decks = 0;
    322 	const char *fifo = NULL, **decks = NULL;
    323 	struct sigaction sa;
    324 	struct stat sb;
    325 
    326 	ARGBEGIN {
    327 	case 'c':
    328 		cflag = 1;
    329 		break;
    330 	case 'd':
    331 		dflag = 1;
    332 		break;
    333 	default:
    334 		usage();
    335 	} ARGEND
    336 
    337 	if ((cflag && argc < 1) || (!cflag && argc < 2))
    338 		usage();
    339 
    340 	if (!cflag) {
    341 		fifo = *argv++;
    342 		argc--;
    343 
    344 		stat(fifo, &sb);
    345 		if (!S_ISFIFO(sb.st_mode))
    346 			usage();
    347 	}
    348 
    349 	memset(&sa, 0, sizeof(sa));
    350 	sa.sa_flags = SA_RESTART;
    351 	sa.sa_handler = sighandler;
    352 	sigaction(SIGUSR1, &sa, NULL);
    353 
    354 	tail = head = xmalloc(sizeof(Node));
    355 
    356 	/* remaining argv elements are deck files */
    357 	for (i = 0; argc && *argv; argv++, i++, argc--) {
    358 		decks = xreallocarray(decks, ++n_decks, sizeof(char *));
    359 		decks[i] = *argv;
    360 
    361 		tail = parse_file(*argv, tail, i);
    362 	}
    363 
    364 	reviews = mkreviews(head);
    365 
    366 	if (cflag) {
    367 		cleanup(head, decks, reviews);
    368 		printf("Cards Due: %ld\n", n_reviews);
    369 		return 0;
    370 	}
    371 
    372 	if (reviews == NULL) {
    373 		cleanup(head, decks, reviews);
    374 		die("mkreviews()\n");
    375 	}
    376 
    377 	shuffle_reviews(reviews, n_reviews);
    378 	reviews = review_loop(reviews, decks, fifo);
    379 
    380 	/* write updated data into deck files */
    381 	for (i = 0; i < n_decks; i++)
    382 		write_deck(decks[i], i);
    383 
    384 	cleanup(head, decks, reviews);
    385 
    386 	return 0;
    387 }