From 555b3bd0e9dc9f529cac0206d735aa2192afca6b Mon Sep 17 00:00:00 2001 From: "S. Gilles" Date: Sun, 26 Jul 2020 02:19:01 -0400 Subject: [PATCH] initial --- README | 53 ++++++++ bld.proj | 7 ++ sispare.myr | 403 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 463 insertions(+) create mode 100644 README create mode 100644 bld.proj create mode 100644 sispare.myr diff --git a/README b/README new file mode 100644 index 0000000..02445ba --- /dev/null +++ b/README @@ -0,0 +1,53 @@ +This is sispare: SImple SPAced REpetition. I wrote it because I don't +think Anki should be 100MB+ of code. It depends on specific, silly +dependencies like + + - termdraw [ https://git.eigenstate.org/npnth/libtermdraw.git ] + +I use it to learn Japanese vocabulary. You can probably use it for other +things, too. + +To use it, run + + /path/to/sispare [ -d ] + +The argument defaults to ${XDG_DATA_HOME}/sispare or +${HOME}/.sispare. + +Sispare is mostly controlled by the data directory. It expects the +following layout: + + /path/to/sispare/dir/ + + cards/ + | + card_000/ + | | + side_A + | | + side_B + | | + level + | + card_001/ + | | + side_A + | | + side_B + | | + level + | + whatever name you like/ + | | + side_A + | | + side_B + | | + level + + schedule/ + + 2031-08-12 + + 2031-08-13 + + 2031-08-16 + +When sispare starts a session, it reads all the files in schedule/ whose +names correspond to the current date or earlier. Each line in those +files will be something like "card_000" or "whatever name you like". + +All of those lines are collected and shuffled. For each line, the sides +will be displayed in a dumb TUI interface. If the user marks the card as +successfully learned, the value stored in that card's level fille will +be increased, which influences how much time passes before the card is +next reviewed. If the level reaches 8, the card is not scheduled for +another review. If the user marks the card as failed, that card's level +is reset to 1. + +To add things, you can run + + - sispare-add-card [ -d ] [ -a | -A ] [ -b | -B ] diff --git a/bld.proj b/bld.proj new file mode 100644 index 0000000..719bf70 --- /dev/null +++ b/bld.proj @@ -0,0 +1,7 @@ +bin sispare = + sispare.myr +;; + +sispare-add-card = + add_card.myr +;; diff --git a/sispare.myr b/sispare.myr new file mode 100644 index 0000000..22d56ae --- /dev/null +++ b/sispare.myr @@ -0,0 +1,403 @@ +use std + +use bio +use date +use fileutil +use sys + +use t +use termdraw + +const wait_days : int[:] = [ 0, 1, 2, 3, 5, 8, 13, 21, 34, 55 ][:] + +const main = {args + var basedir : byte[:] = [][:] + var dir_checked = false + var cmd : std.optparsed = std.optparse(args, &[ + .minargs = 0, + .maxargs = 0, + .opts = [ + [ .opt = 'd', .arg = "dir", .desc = "base directory" ], + ][:], + ]) + + for opt : cmd.opts + match opt + | ('d', dir): basedir = std.sldup(dir) + | _: std.fatal("impossible {}\n", opt) + ;; + ;; + + /* Try to open the XDG one */ + if basedir.len == 0 + match std.getenv("XDG_DATA_HOME") + | `std.None: + | `std.Some d: basedir = std.fmt("{}/sispare", d) + ;; + + if basedir.len != 0 + match std.diropen(basedir) + | `std.Ok d2: + dir_checked = true + std.dirclose(d2) + | `std.Err _: + std.slfree(basedir) + basedir = [][:] + ;; + ;; + ;; + + /* Try to open the $HOME one */ + if basedir.len == 0 + var d : byte[:] = fileutil.homedir() + if d.len != 0 + basedir = std.fmt("{}/.sispare", d) + ;; + + if basedir.len != 0 + match std.diropen(basedir) + | `std.Ok d2: + dir_checked = true + std.dirclose(d2) + | `std.Err _: + std.slfree(basedir) + basedir = [][:] + ;; + ;; + ;; + + /* Make sure we've got one. */ + if !dir_checked + if basedir.len == 0 + std.fatal("cannot find any directory; make or specify one.\n") + ;; + + match std.diropen(basedir) + | `std.Ok d2: std.dirclose(d2) + | `std.Err e: + std.fatal("std.diropen(\"{}\"): {}\n", basedir, e) + ;; + ;; + + auto (basedir : t.doomed_str) + do_session(basedir) +} + +const do_session = {d : byte[:] + var sched_dir = std.fmt("{}/schedule", d) + var now : date.instant = date.utcnow() + var cards : byte[:][:] = [][:] + var files_to_rm : byte[:][:] = [][:] + + auto (sched_dir : t.doomed_str) + + /* Pick up the cards for today */ + for f : fileutil.bywalk(sched_dir) + var fbase = std.basename(f) + var deal_with = false + match date.parsefmt("%Y-%m-%d", fbase) + | `std.Ok then: deal_with = (now.actual >= then.actual) + | `std.Err e: deal_with = true + ;; + auto (fbase : t.doomed_str) + + if deal_with + match std.slurp(f) + | `std.Err e: + std.fput(std.Err, "std.slurp(\"{}\"): {}\n", f, e) + | `std.Ok lines: + var byline = std.strsplit(lines, "\n") + std.sljoin(&cards, byline) + std.slfree(byline) + std.slpush(&files_to_rm, std.sldup(f)) + ;; + ;; + ;; + + /* sort according to a shuffled (but consistent) order */ + var nowhash = std.hash(now.actual) + std.sort(cards, { a : byte[:], b : byte[:] + var ahash = std.hash(a) ^ nowhash + var bhash = std.hash(b) ^ nowhash + -> std.numcmp(ahash, bhash)}) + + /* uniq (and check validity) */ + for var j = 1; j < cards.len; ++j + if std.eq(cards[j], cards[j - 1]) + std.sldel(&cards, j) + j-- + ;; + ;; + + for var j = 0; j < cards.len; ++j + if cards[j].len == 0 + std.sldel(&cards, j) + j-- + ;; + ;; + + for var j = 0; j < cards.len; ++j + var dirpath = std.fmt("{}/cards/{}", d, cards[j]) + match std.diropen(dirpath) + | `std.Ok d2: + std.dirclose(d2) + | `std.Err _: + std.sldel(&cards, j) + j-- + ;; + std.slfree(dirpath) + ;; + + match show_each_card(d, cards) + | `std.Err e: std.fatal("{}\n", e) + | `std.Ok res: + for (card, passed) : res + /* compute new level for this card */ + var levelpath = std.fmt("{}/cards/{}/level", d, card) + var level = 1 + auto (levelpath : t.doomed_str) + + match std.slurp(levelpath) + | `std.Err e: + | `std.Ok levelstr: + match std.intparse(levelstr) + | `std.Some l: level = l + | `std.None: + ;; + ;; + + if passed + level++ + else + level = 1 + ;; + + /* write new level for this card */ + match std.open(levelpath, std.Owrite | std.Ocreat | std.Otrunc) + | `std.Err e: std.fput(std.Err, "std.open(\"{}\"): {}\n", levelpath, e) + | `std.Ok lfd: + std.fput(lfd, "{}\n", level) + std.close(lfd) + ;; + + /* determine when this card will be next studied */ + if level >= wait_days.len + continue + ;; + + var when = date.addperiod(now, `date.Day wait_days[std.max(0, level)]) + var schedpath = std.fmt("{}/schedule/{f=%Y-%m-%d}", d, when) + auto (schedpath : t.doomed_str) + + match std.open(schedpath, std.Owrite | std.Ocreat | std.Oappend) + | `std.Err e: std.fput(std.Err, "std.open(\"{}\"): {}\n", schedpath, e) + | `std.Ok sfd: + std.fput(sfd, "{}\n", card) + std.close(sfd) + ;; + ;; + ;; + + + /* delete the old lists */ + for f : files_to_rm + std.remove(f) + ;; +} + +const show_each_card : (d : byte[:], cards : byte[:][:] -> std.result((byte[:], bool)[:], byte[:])) = {d : byte[:], cards : byte[:][:] + var ret : (byte[:], bool)[:] = [][:] + var t : termdraw.term# = termdraw.mk(1) + var k : std.size = 0 + var progress : byte[:] = [][:] + + termdraw.raw(t) + + while k < cards.len + var c : byte[:] = cards[k] + std.slfree(progress) + progress = std.fmt("{} / {}", k + 1, cards.len) + var sideapath : byte[:] = std.fmt("{}/cards/{}/side_A", d, c) + var sidebpath : byte[:] = std.fmt("{}/cards/{}/side_B", d, c) + var widtha : int = 0 + var widthb : int = 0 + + var sideafull : byte[:] = [][:] + var sidebfull : byte[:] = [][:] + var sidea : byte[:][:] = [][:] + var sideb : byte[:][:] = [][:] + var dispside : byte[:][:] = [][:] + var dispwidth : int = 0 + var still : bool = true + var seen_side_b : bool = false + + match std.slurp(sideapath) + | `std.Err e: + std.sldel(&cards, k) + goto done_with_this + | `std.Ok b: sideafull = b + ;; + + match std.slurp(sidebpath) + | `std.Err e: + std.sldel(&cards, k) + goto done_with_this + | `std.Ok b: sidebfull = b + ;; + + sidea = std.strsplit(sideafull, "\n") + for var j = 0; j < sidea.len; ++j + widtha = std.max(widtha, (std.strcellwidth(sidea[j]) : int)) + ;; + + sideb = std.strsplit(sidebfull, "\n") + for var j = 0; j < sideb.len; ++j + widthb = std.max(widthb, (std.strcellwidth(sideb[j]) : int)) + ;; + + dispside = sidea + dispwidth = widtha + while still + print_file(t, dispside, dispwidth) + print_instructions(t, seen_side_b, k > 0) + print_progress(t, progress) + termdraw.flush(t) + match termdraw.event(t) + | `termdraw.Ctrl 'c': + termdraw.free(t) + std.fatal("") + | `termdraw.Kc 'a' || `termdraw.Kc 'A': + dispside = sidea + dispwidth = widtha + | `termdraw.Kc 'b' || `termdraw.Kc 'B': + dispside = sideb + dispwidth = widthb + seen_side_b = true + | `termdraw.Kc 'y' || `termdraw.Kc 'Y': + if seen_side_b + std.slpush(&ret, (c, true)) + still = false + k++ + ;; + | `termdraw.Kc 'n' || `termdraw.Kc 'N': + if seen_side_b + std.slpush(&ret, (c, false)) + still = false + k++ + ;; + | `termdraw.Kc '<': + if k > 0 + std.sldel(&ret, ret.len - 1) + k-- + still = false + ;; + | _: + ;; + + ;; + +:done_with_this + std.slfree(sideafull) + std.slfree(sidebfull) + std.slfree(sidea) + std.slfree(sideb) + std.slfree(sideapath) + std.slfree(sidebpath) + ;; + + termdraw.free(t) + -> `std.Ok ret +} + +const print_file = {t : termdraw.term#, lines : byte[:][:], lines_width : int + var start_y : int = 0 + var start_x : int = 0 + termdraw.setbg(t, bg_plain) + termdraw.setfg(t, fg_plain) + termdraw.clear(t, 0, 0, t.width, t.height) + + if (lines_width < t.width) && (lines.len < t.height - 1) + /* We have enough room to center it */ + start_y = (t.height - 1 - lines.len) / 2 + start_x = (t.width - lines_width) / 2 + + for var j = 0; j < lines.len; ++j + termdraw.move(t, start_x, start_y + j) + termdraw.put(t, lines[j]) + ;; + else + /* We don't. Lines/breaks/whatever. */ + var y = start_y + for var j = 0; j < lines.len; ++j + var immediate_dispatch : byte[:] = [][:] + var to_print : byte[:] = lines[j] + while to_print.len > 0 && y < t.height - 1 + (immediate_dispatch, to_print) = split_by_cell_width(to_print, (t.width : std.size)) + termdraw.move(t, 0, y) + termdraw.put(t, immediate_dispatch) + y++ + ;; + + if y >= t.height - 1 + break + ;; + ;; + ;; +} + +const print_instructions = {t : termdraw.term#, seen_side_b : bool, can_go_back : bool + termdraw.setbg(t, bg_alt) + termdraw.clear(t, 0, t.height - 1, t.width, t.height) + termdraw.move(t, 0, t.height - 1) + + show_instruction(t, "a", "side A", true) + show_instruction(t, "b", "side B", true) + show_instruction(t, "y", "pass", seen_side_b) + show_instruction(t, "n", "fail", seen_side_b) + show_instruction(t, "<", "go back", can_go_back) +} + +const show_instruction = {t : termdraw.term#, key : byte[:], desc : byte[:], visible + if visible + termdraw.setfg(t, fg_bluer) + else + termdraw.setfg(t, fg_lighter) + ;; + termdraw.put(t, "[{}]", key) + if visible + termdraw.setfg(t, fg_plain) + else + termdraw.setfg(t, fg_lighter) + ;; + termdraw.put(t, " {} ", desc) +} + +const print_progress = {t : termdraw.term#, s : byte[:] + termdraw.setbg(t, bg_alt) + termdraw.setfg(t, fg_plain) + termdraw.move(t, t.width - s.len - 1, t.height - 1) + termdraw.put(t, s) +} + + +const fg_plain = `termdraw.RGB (0x20, 0x1b, 0x0c) +const fg_lighter = `termdraw.RGB (0x60, 0x5b, 0x5c) +const bg_plain = `termdraw.RGB (0xb2, 0xad, 0x99) +const bg_alt = `termdraw.RGB (0xa4, 0x9d, 0x88) +const fg_bluer = `termdraw.RGB (0x1b, 0x28, 0x70) +const bg_alt2 = `termdraw.RGB (0x4e, 0x4a, 0x42) + + +const split_by_cell_width = {base : byte[:], width : std.size + var position : std.size = 0 + var cur_width : std.size = 0 + for (c, o) : std.bycharoff(base) + cur_width += (std.cellwidth(c) : std.size) + if cur_width > width + -> (base[:position], base[position:]) + ;; + position = o + ;; + + -> (base[:], [][:]) +} -- 2.11.4.GIT