oboeru

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

Commit: 82ca55b78881ee9d6f0e9baad75e14af10e556f0
Parent: b15d78daa446db7e12e7f4e7d8835ee23342a4ce
Author: Randy Palamar
Date:   Wed, 11 Aug 2021 08:36:39 -0600

import oboeru

with a bit of creativity this should be usable without any further
programs. however, there will be more and example scripts soon.

Diffstat:
AMakefile | 29+++++++++++++++++++++++++++++
Aarg.h | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Aconfig.def.h | 21+++++++++++++++++++++
Aconfig.mk | 4++++
Aoboeru.c | 349+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Autil.c | 40++++++++++++++++++++++++++++++++++++++++
Autil.h | 6++++++
7 files changed, 499 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,29 @@ +# See LICENSE for license details. +include config.mk + +OBOERU_SRC = oboeru.c util.c +OBOERU_OBJ = $(OBOERU_SRC:.c=.o) + +default: oboeru + +config.h: + cp config.def.h $@ + +.c.o: + $(CC) $(CFLAGS) -o $@ -c $< + +$(OBOERU_OBJ): config.h + +oboeru: $(OBOERU_OBJ) + $(CC) -o $@ $(OBOERU_OBJ) $(LDFLAGS) + +install: oboeru + mkdir -p $(PREFIX)/bin + cp oboeru $(PREFIX)/bin + chmod 755 $(PREFIX)/bin/oboeru + +uninstall: + rm $(PREFIX)/bin/oboeru + +clean: + rm *.o oboeru diff --git a/arg.h b/arg.h @@ -0,0 +1,50 @@ +/* + * Copy me if you can. + * by 20h + */ + +#ifndef ARG_H__ +#define ARG_H__ + +extern char *argv0; + +/* use main(int argc, char *argv[]) */ +#define ARGBEGIN for (argv0 = *argv, argv++, argc--;\ + argv[0] && argv[0][0] == '-'\ + && argv[0][1];\ + argc--, argv++) {\ + char argc_;\ + char **argv_;\ + int brk_;\ + if (argv[0][1] == '-' && argv[0][2] == '\0') {\ + argv++;\ + argc--;\ + break;\ + }\ + int i_;\ + for (i_ = 1, brk_ = 0, argv_ = argv;\ + argv[0][i_] && !brk_;\ + i_++) {\ + if (argv_ != argv)\ + break;\ + argc_ = argv[0][i_];\ + switch (argc_) + +#define ARGEND }\ + } + +#define ARGC() argc_ + +#define EARGF(x) ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + ((x), abort(), (char *)0) :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#define ARGF() ((argv[0][i_+1] == '\0' && argv[1] == NULL)?\ + (char *)0 :\ + (brk_ = 1, (argv[0][i_+1] != '\0')?\ + (&argv[0][i_+1]) :\ + (argc--, argv++, argv[0]))) + +#endif diff --git a/config.def.h b/config.def.h @@ -0,0 +1,21 @@ +/* number of times a mature card can be failed */ +/* before it is excluded from reviews */ +#define MAX_LEECHES 4 + +/* the minimum age in seconds for card to be considerd a leech */ +/* 3 days for example is 3 * 24 * 3600 (3d * 24h/d * 3600s/h) */ +#define LEECH_AGE (5 * 24 * 3600) + +/* minimum interval in seconds for grow by */ +#define MINIMUM_INCREASE (24 * 3600) + +/* amount to mutliply the cards interval by when it passes */ +/* should be > 1. this leads to exponential growth */ +#define GROWTH_RATE (1.5) + +/* amount to mutliply the cards interval by when it fails */ +/* should be < 1. this leads to exponential decay */ +#define SHRINK_RATE (0.66) + +/* format for times in the output deck file */ +const char *timefmt = "%Y年%m月%d日%H時%M分"; diff --git a/config.mk b/config.mk @@ -0,0 +1,4 @@ +PREFIX = /usr/local + +CPPFLAGS = -D_BSD_SOURCE +CFLAGS = -O2 -std=c99 -Wall -pedantic $(CPPFLAGS) $(INCS) diff --git a/oboeru.c b/oboeru.c @@ -0,0 +1,349 @@ +/* See LICENSE for license details. */ +#include <fcntl.h> +#include <limits.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <time.h> +#include <unistd.h> + +#include "arg.h" +#include "util.h" +#include "config.h" + +#define BUF_SIZE BUFSIZ +#define DELIM "\t" /* fixed unless a better parser gets implemented */ + +enum { + CARD_PASS, + CARD_FAIL +}; + +typedef struct { + size_t id, deck; + int leeches; + /* seconds since epoch */ + int64_t created, due; + char *extra; + int8_t nobump; +} Card; + +typedef struct node { + Card *card; + struct node *next; +} Node; + +static const char *scanfmt = "%ld" DELIM "%s" DELIM "%s" DELIM "%d" DELIM "%[^\n]"; +static const char *logfmt = "%05ld" DELIM "%s" DELIM "%s" DELIM "%d" DELIM "%s\n"; + +static Node *head; +static Card **reviews; +static size_t n_reviews; +static const char **decks; +static size_t n_decks; + +/* option parsing variables */ +char *argv0; +static int cflag; +static int dflag; + +static void +usage(void) +{ + die("usage: %s [-cd] pipe deck [deck1 ...]\n", argv0); +} + +static void +freenodes(Node *node) +{ + if (node->next) + freenodes(node->next); + + if (node->card) { + free(node->card->extra); + free(node->card); + } + free(node); + node = NULL; +} + +static void +cleanup(void) +{ + freenodes(head); + free(reviews); + reviews = NULL; +} + +/* returns a filled out Card * after parsing */ +static Card * +parse_line(const char *line) +{ + struct tm tm; + char created[BUF_SIZE], due[BUF_SIZE]; + Card *c = xmalloc(sizeof(Card)); + c->extra = xmalloc(BUF_SIZE); + + sscanf(line, scanfmt, &c->id, created, due, &c->leeches, c->extra); + + memset(&tm, 0, sizeof(tm)); + strptime(created, timefmt, &tm); + c->created = timegm(&tm); + + memset(&tm, 0, sizeof(tm)); + strptime(due, timefmt, &tm); + c->due = timegm(&tm); + + c->nobump = 0; + + return c; +} + +/* returns the latest allocated node with no card */ +static Node * +parse_file(const char *file, Node *node, size_t deck_id) +{ + FILE *fp; + char *line = NULL; + size_t len = 0; + + fp = fopen(file, "r"); + if (!fp) + die("fopen(%s)\n", file); + + for (; getline(&line, &len, fp) != -1; node = node->next = xmalloc(sizeof(Node))) { + node->next = NULL; + node->card = parse_line(line); + if (node->card) + node->card->deck = deck_id; + } + + free(line); + fclose(fp); + + return node; +} + +static int +needs_review(Card *card) +{ + time_t t; + + if (card->leeches >= MAX_LEECHES) + return 0; + + t = time(NULL); + + if (card->due > t) + return 0; + + return 1; +} + +static void +add_review(Card *c) +{ + Card **r = reviews; + r = xreallocarray(r, ++n_reviews, sizeof(Card **)); + r[n_reviews - 1] = c; + reviews = r; +} + +static void +mkreviews(Node *node) +{ + for (; node; node = node->next) + if (node->card && needs_review(node->card)) + add_review(node->card); +} + +static void +bump_card(Card *card, int status) +{ + int64_t diff; + + if (card->nobump) + return; + + diff = card->due - card->created; + if (diff < 0) + fprintf(stderr, "card id: %ld: malformed review time\n", card->id); + + switch (status) { + case CARD_PASS: + if (diff < MINIMUM_INCREASE) { + card->due += MINIMUM_INCREASE; + add_review(card); + card->nobump = 1; + } else + card->due += diff * GROWTH_RATE; + break; + case CARD_FAIL: + if (diff > LEECH_AGE) + card->leeches++; + card->due += diff * SHRINK_RATE; + add_review(card); + card->nobump = 1; + } +} + +static void +shuffle_reviews(void) +{ + size_t i, j; + Card *t, **r = reviews; + + if (n_reviews <= 1) + return; + + srand(time(NULL)); + + /* this could be improved */ + for (i = 0; i < n_reviews; i++) { + j = i + rand() % (n_reviews - i); + + t = r[j]; + r[j] = r[i]; + r[i] = t; + } +} + +static void +review_loop(const char *fifo) +{ + Card **r = reviews; + char reply[BUF_SIZE]; + int fd; + size_t i, j, n; + struct timespec wait = { .tv_sec = 0, .tv_nsec = 50 * 10e6 }; + struct { const char *str; int status; } reply_map[] = { + { .str = "pass", .status = CARD_PASS }, + { .str = "fail", .status = CARD_FAIL } + }; + + for (i = 0; i < n_reviews; i++) { + fprintf(stdout, "%s\t%ld\n", decks[r[i]->deck], r[i]->id); + + reply[0] = 0; + fd = open(fifo, O_RDONLY); + if (fd == -1) + return; + n = read(fd, reply, sizeof(reply)); + close(fd); + + /* strip a trailing newline */ + if (reply[n-1] == '\n') + reply[n-1] = 0; + + if (!strcmp(reply, "quit")) + return; + + for (j = 0; j < LEN(reply_map); j++) + if (!strcmp(reply, reply_map[j].str)) + bump_card(r[i], reply_map[j].status); + + /* give the writing process time to close its fd */ + nanosleep(&wait, NULL); + + /* reviews can change in bump card */ + r = reviews; + } +} + +static void +write_deck(size_t deck_id) +{ + FILE *fp; + Node *node; + Card *c; + char created[BUF_SIZE], due[BUF_SIZE]; + char path[PATH_MAX]; + + if (dflag) { + snprintf(path, sizeof(path), "%s.debug", decks[deck_id]); + fp = fopen(path, "w+"); + } else { + fp = fopen(decks[deck_id], "w"); + } + + if (!fp) + return; + + for (node = head; node; node = node->next) { + c = node->card; + if (!c || (c->deck != deck_id)) + continue; + + strftime(created, sizeof(created), timefmt, + gmtime((time_t *)&c->created)); + strftime(due, sizeof(due), timefmt, + gmtime((time_t *)&c->due)); + + fprintf(fp, logfmt, c->id, created, due, + c->leeches, c->extra); + } + fclose(fp); +} + + +int +main(int argc, char *argv[]) +{ + Node *tail; + size_t deck_id; + const char *fifo = NULL; + struct stat sb; + + ARGBEGIN { + case 'c': + cflag = 2; + break; + case 'd': + dflag = 1; + break; + default: + usage(); + } ARGEND + + if ((cflag && argc < 1) || (!cflag && argc < 2)) + usage(); + + if (!cflag) { + fifo = *argv++; + argc--; + + stat(fifo, &sb); + if (!S_ISFIFO(sb.st_mode)) + usage(); + } + + tail = head = xmalloc(sizeof(Node)); + + /* remaining argv elements are deck jsons */ + for (deck_id = 0; argc && *argv; argv++, deck_id++, argc--) { + decks = xreallocarray(decks, ++n_decks, sizeof(char *)); + decks[deck_id] = *argv; + + tail = parse_file(*argv, tail, deck_id); + } + + mkreviews(head); + if (cflag) { + cleanup(); + die("Cards Due: %ld\n", n_reviews); + } + + shuffle_reviews(); + review_loop(fifo); + + /* write updated data into deck files */ + for (deck_id = 0; deck_id < n_decks; deck_id++) + write_deck(deck_id); + + cleanup(); + + return 0; +} diff --git a/util.c b/util.c @@ -0,0 +1,40 @@ +/* See LICENSE for license details. */ +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "util.h" + +void +die(const char *errstr, ...) +{ + va_list ap; + + va_start(ap, errstr); + vfprintf(stderr, errstr, ap); + va_end(ap); + + exit(1); +} + +void * +xmalloc(size_t s) +{ + void *p; + + if (!(p = malloc(s))) + die("malloc()\n"); + + return p; +} + +void * +xreallocarray(void *o, size_t n, size_t s) +{ + void *new; + + if (!(new = reallocarray(o, n, s))) + die("reallocarray()\n"); + + return new; +} diff --git a/util.h b/util.h @@ -0,0 +1,6 @@ +/* See LICENSE for license details. */ +#define LEN(a) (sizeof(a) / sizeof(*a)) + +void die(const char *, ...); +void *xmalloc(size_t); +void *xreallocarray(void *, size_t, size_t);