14 const wait_days : (int,int)[:] = [
27 const fg_plain = `termdraw.RGB (0x20, 0x1b, 0x0c)
28 const fg_lighter = `termdraw.RGB (0x60, 0x5b, 0x5c)
29 const fg_bluer = `termdraw.RGB (0x1b, 0x28, 0x70)
30 const bg_plain = `termdraw.RGB (0xb2, 0xad, 0x99)
31 const bg_alt = `termdraw.RGB (0xa4, 0x9d, 0x88)
32 const bg_alt2 = `termdraw.RGB (0x4e, 0x4a, 0x42)
35 var dir_arg : byte[:] = [][:]
36 var basedir : byte[:] = [][:]
37 var dir_checked = false
38 var cmd : std.optparsed = std.optparse(args, &[
42 [ .opt = 'd', .arg = "dir", .desc = "base directory" ],
48 | ('d', dir): dir_arg = dir
49 | _: std.fatal("impossible {}\n", opt)
53 match get_basedir(dir_arg)
54 | `std.Err e: std.fatal("{}\n", e)
55 | `std.Ok b: basedir = b
58 auto (basedir : t.doomed_str)
62 const do_session = {d : byte[:]
63 var sched_dir = std.fmt("{}/schedule", d)
64 var now : date.instant = date.utcnow()
65 var cards : byte[:][:] = [][:]
66 var files_to_scrub : byte[:][:] = [][:]
70 auto (sched_dir : t.doomed_str)
72 /* Pick up the cards for today */
73 for f : fileutil.bywalk(sched_dir)
74 var fbase = std.basename(f)
76 match date.parsefmt("%Y-%m-%d", fbase)
78 deal_with = (now.actual >= then.actual)
79 std.slpush(&files_to_scrub, std.sldup(f))
80 | `std.Err e: deal_with = true
82 auto (fbase : t.doomed_str)
87 std.fput(std.Err, "std.slurp(\"{}\"): {}\n", f, e)
89 var byline = std.strsplit(lines, "\n")
90 std.sljoin(&cards, byline)
96 /* sort according to a shuffled (but consistent) order */
97 var now_salt = std.fmt("{}", now.actual)
98 auto (now_salt : t.doomed_str)
99 std.sort(cards, { a : byte[:], b : byte[:]
102 var ahash : byte[:] = std.slalloc(impl(crypto.Hashsz, crypto.md5))
103 var bhash : byte[:] = std.slalloc(impl(crypto.Hashsz, crypto.md5))
104 auto (ahash : t.doomed_str)
105 auto (bhash : t.doomed_str)
109 crypto.hadd(&ast, now_salt)
110 crypto.hfin(&ast, ahash)
114 crypto.hadd(&bst, now_salt)
115 crypto.hfin(&bst, bhash)
117 for var j = 0; j < ahash.len; ++j
118 match std.numcmp(ahash[j], bhash[j])
127 /* uniq (and check validity) */
128 for var j = 1; j < cards.len; ++j
129 if std.eq(cards[j], cards[j - 1])
135 for var j = 0; j < cards.len; ++j
142 for var j = 0; j < cards.len; ++j
143 var dirpath = std.fmt("{}/cards/{}", d, cards[j])
144 match std.diropen(dirpath)
155 std.put("All up-to-date; no review necessary.\n")
159 match show_each_card(d, cards)
160 | `std.Err e: std.fatal("{}\n", e)
163 Scrub all the cards we've need to schedule from every
166 for f : files_to_scrub
170 var per_line = std.strsplit(contents, "\n")
172 while j < per_line.len
173 var keep_this : bool = true
175 if std.eq(card, per_line[j])
181 std.sldel(&per_line, j)
190 match std.open(f, std.Owrite | std.Ocreat | std.Otrunc)
191 | `std.Err e: std.fput(std.Err, "std.open(\"{}\"): {}\n", f, e)
195 std.fput(sfd, "{}\n", line)
205 for (card, passed) : res
206 /* compute new level for this card */
207 var levelpath = std.fmt("{}/cards/{}/level", d, card)
209 auto (levelpath : t.doomed_str)
211 match std.slurp(levelpath)
215 while len <= levelstr.len && std.isdigit((levelstr[len] : char))
218 levelstr = levelstr[:len]
220 match std.intparse(levelstr)
221 | `std.Some l: level = l
234 /* write new level for this card */
235 match std.open(levelpath, std.Owrite | std.Ocreat | std.Otrunc)
236 | `std.Err e: std.fput(std.Err, "std.open(\"{}\"): {}\n", levelpath, e)
238 std.fput(lfd, "{}\n", level)
242 /* determine when this card will be next studied */
243 if level >= wait_days.len
247 var wait_pair = wait_days[std.max(0, level)]
248 var when = date.addperiod(now, `date.Day std.rand(wait_pair.0, wait_pair.1 + 1))
249 var sched_path = std.fmt("{}/schedule/{f=%Y-%m-%d}", d, when)
250 auto (sched_path : t.doomed_str)
252 match std.open(sched_path, std.Owrite | std.Ocreat | std.Oappend)
253 | `std.Err e: std.fput(std.Err, "std.open(\"{}\"): {}\n", sched_path, e)
255 std.fput(sfd, "{}\n", card)
261 std.put("{w=3,p= } passed\n", npassed)
262 std.put("{w=3,p= } failed\n", nfailed)
264 for f : files_to_scrub
267 std.slfree(files_to_scrub)
270 const show_each_card : (d : byte[:], cards : byte[:][:] -> std.result((byte[:], bool)[:], byte[:])) = {d : byte[:], cards : byte[:][:]
271 var ret : (byte[:], bool)[:] = [][:]
272 var t : termdraw.term# = termdraw.mk(1)
274 var progress : byte[:] = [][:]
279 var c : byte[:] = cards[k]
281 progress = std.fmt("{} / {}", k + 1, cards.len)
282 var sideapath : byte[:] = std.fmt("{}/cards/{}/side_A", d, c)
283 var sidebpath : byte[:] = std.fmt("{}/cards/{}/side_B", d, c)
287 var sideafull : byte[:] = [][:]
288 var sidebfull : byte[:] = [][:]
289 var sidea : byte[:][:] = [][:]
290 var sideb : byte[:][:] = [][:]
291 var dispside : byte[:][:] = [][:]
292 var dispwidth : int = 0
293 var still : bool = true
294 var seen_side_b : bool = false
296 match std.slurp(sideapath)
300 | `std.Ok b: sideafull = b
303 match std.slurp(sidebpath)
307 | `std.Ok b: sidebfull = b
310 sidea = std.strsplit(sideafull, "\n")
311 for var j = 0; j < sidea.len; ++j
312 widtha = std.max(widtha, (std.strcellwidth(sidea[j]) : int))
315 sideb = std.strsplit(sidebfull, "\n")
316 for var j = 0; j < sideb.len; ++j
317 widthb = std.max(widthb, (std.strcellwidth(sideb[j]) : int))
323 print_file(t, dispside, dispwidth)
324 print_instructions(t, seen_side_b, k > 0)
325 print_progress(t, progress)
327 match termdraw.event(t)
328 | `termdraw.Ctrl 'c':
331 | `termdraw.Kc 'q' || `termdraw.Kc 'Q':
334 | `termdraw.Kc 'a' || `termdraw.Kc 'A':
337 | `termdraw.Kc 'b' || `termdraw.Kc 'B':
341 | `termdraw.Kc 'y' || `termdraw.Kc 'Y':
343 std.slpush(&ret, (c, true))
347 | `termdraw.Kc 'n' || `termdraw.Kc 'N':
349 std.slpush(&ret, (c, false))
355 std.sldel(&ret, ret.len - 1)
365 std.slfree(sideafull)
366 std.slfree(sidebfull)
369 std.slfree(sideapath)
370 std.slfree(sidebpath)
377 const print_file = {t : termdraw.term#, lines : byte[:][:], lines_width : int
378 var start_y : int = 0
379 var start_x : int = 0
380 termdraw.setbg(t, bg_plain)
381 termdraw.setfg(t, fg_plain)
382 termdraw.clear(t, 0, 0, t.width, t.height)
384 if (lines_width < t.width) && (lines.len < t.height - 1)
385 /* We have enough room to center it */
386 start_y = (t.height - 1 - lines.len) / 2
387 start_x = (t.width - lines_width) / 2
389 for var j = 0; j < lines.len; ++j
390 termdraw.move(t, start_x, start_y + j)
391 termdraw.put(t, lines[j])
394 /* We don't. Lines/breaks/whatever. */
396 for var j = 0; j < lines.len; ++j
397 var immediate_dispatch : byte[:] = [][:]
398 var to_print : byte[:] = lines[j]
404 while to_print.len > 0 && y < t.height - 1
405 (immediate_dispatch, to_print) = split_by_cell_width(to_print, (t.width : std.size))
406 termdraw.move(t, 0, y)
407 termdraw.put(t, immediate_dispatch)
418 const print_instructions = {t : termdraw.term#, seen_side_b : bool, can_go_back : bool
419 termdraw.setbg(t, bg_alt)
420 termdraw.clear(t, 0, t.height - 1, t.width, t.height)
421 termdraw.move(t, 0, t.height - 1)
423 show_instruction(t, "q", "quit early", true)
424 show_instruction(t, "a", "side A", true)
425 show_instruction(t, "b", "side B", true)
426 show_instruction(t, "y", "pass", seen_side_b)
427 show_instruction(t, "n", "fail", seen_side_b)
428 show_instruction(t, "<", "go back", can_go_back)
431 const show_instruction = {t : termdraw.term#, key : byte[:], desc : byte[:], visible
433 termdraw.setfg(t, fg_bluer)
435 termdraw.setfg(t, fg_lighter)
437 termdraw.put(t, "[{}]", key)
439 termdraw.setfg(t, fg_plain)
441 termdraw.setfg(t, fg_lighter)
443 termdraw.put(t, " {} ", desc)
446 const print_progress = {t : termdraw.term#, s : byte[:]
447 termdraw.setbg(t, bg_alt)
448 termdraw.setfg(t, fg_plain)
449 termdraw.move(t, t.width - s.len - 1, t.height - 1)
453 const split_by_cell_width = {base : byte[:], width : std.size
454 var position : std.size = 0
455 var cur_width : std.size = 0
456 for (c, o) : std.bycharoff(base)
457 cur_width += (std.cellwidth(c) : std.size)
459 -> (base[:position], base[position:])