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:
A | Makefile | | | 29 | +++++++++++++++++++++++++++++ |
A | arg.h | | | 50 | ++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | config.def.h | | | 21 | +++++++++++++++++++++ |
A | config.mk | | | 4 | ++++ |
A | oboeru.c | | | 349 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | util.c | | | 40 | ++++++++++++++++++++++++++++++++++++++++ |
A | util.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);