#!/usr/bin/env lua --[[ the main user interface for memoire ]] local SRC_DIR = arg[0]:match("^.*/") or "" local Prompt = dofile(SRC_DIR .. "prompt.lua") local review = dofile(SRC_DIR .. "review.lua") local loader = dofile(SRC_DIR .. "loader.lua") --[[ gets the score (again/hard/good/easy/correction) from a string by first character scores are numbers, higher is easier, -1 is correction ]] local function succfromstr(s) local f = s:sub(1, 1):lower() if f == "0" or f == "a" then return 0 elseif f == "1" or f == "h" then return 0.5 elseif f == "2" or f == "g" then return 1 elseif f == "3" or f == "e" then return 1.5 elseif f == "-" or f == "c" then return -1 end return 1 end --[[ clears the terminal implementation may be platform-specific ]] local function clear() -- XTerm io.write("\x1bc") end --[[ display text with images, inserted with `IMG[path]` implementation may be platform-specific `basepath` points to the base directory of the images ]] local function imgify(text, basepath) -- adapted from https://stackoverflow.com/a/1579673 local PATTERN = "(.-)IMG%[([^%]]+)%]" -- Cygwin/Mintty local DISPIMG = function(path) if path and #path > 0 then io.write("\n") os.execute(string.format("imgcat %s%s", basepath, path)) end end local le = 1 local s, e, pt, ip = text:find(PATTERN) while s do io.write(pt) DISPIMG(ip) le = e + 1 s, e, pt, ip = text:find(PATTERN, le) io.write(pt or "") DISPIMG(ip) end if le <= #text then io.write(text:sub(le)) end end --[[ rounds `x` to `n` places (default 0) after the radix point in base `b` (default 10) ]] local function round(x, n, b) local b = b or 10 local n = n or 0 if n == 0 then return math.tointeger(math.floor(x * b ^ n) / b ^ n) else return math.floor(x * b ^ n) / b ^ n end end --[[ return human-friendly string to present a duration (input in seconds), e.g. "44:37", "4.8h", "11d", "8.3m", "17.4y" ]] local function disptime(ts) local YDAY = 365.24 local MDAY = YDAY / 12 local DSEC = 86400 local HSEC = 3600 if ts >= 3 * YDAY * DSEC then return round(ts / (YDAY * DSEC), 1) .. "y" elseif ts >= 4 * MDAY * DSEC then return round(ts / (MDAY * DSEC), 1) .. "m" elseif ts >= 10 * DSEC then return round(ts / DSEC) .. "d" elseif ts >= 1.5 * DSEC then return round(ts / DSEC, 1) .. "d" elseif ts >= 8 * HSEC then return round(ts / HSEC) .. "h" elseif ts >= HSEC then return round(ts / HSEC, 1) .. "h" else return string.format("%d:%02d", round(ts / 60), round(ts % 60)) end end --[[ check action based on first command-line argument; if nothing valid given, default to help ]] local action = nil if arg[1] then local fc = arg[1]:sub(1, 1) if fc == "a" or fc == "h" or fc == "r" or fc == "s" then action = fc else action = "h" end else action = "h" end local deckfname = "example.rec" if arg[2] then deckfname = arg[2] end math.randomseed(os.time()) --[[ do requested action ]] -- Add card(s) if action == "a" then local deck = loader.load(deckfname) if deck then io.write("Reading prompts -- press Ctrl-D to stop\n") local rt = io.read("a") local cardctr = 0 local prctr = 0 for pt in rt:gmatch("(..-)\n\n") do local nc = {} -- these Prompts will just be converted to Rec, so we don't need to exclude answers from prompt-text for _re in pt:gmatch("%{[^%}]+%}") do table.insert(nc, Prompt:new(pt, "", os.time(), 30, 0, 0, os.time())) prctr = prctr + 1 end table.insert(deck, nc) cardctr = cardctr + 1 end io.write(string.format( "Added %d cards (%d prompts)\n", cardctr, prctr )) if cardctr > 0 then loader.save(deckfname, deck) end else io.write("Failed to load deck\n") end -- Help elseif action == "h" then io.write(string.format( "Usage: %s ACTION [REPO]\n\nACTION may be one of\n (a)dd card\n (h)elp\n (r)eview session\n (s)tatistics\n", arg[0] )) -- Review session elseif action == "r" then local deck = loader.load(deckfname) if deck then -- collect list of index-pairs for prompts to be reviewed this session local ctrt = review.list(deck, function(pr) -- exclude prompts that use images or Unicode return (not pr.text:find("IMG%[")) and (not pr.text:find("[\u{81}-\u{FFFFF}]")) end) io.write(string.format( "%d prompts are ready for review.\nHow many to review in this session? ", #ctrt )) local climt = io.read("l") local climn = tonumber(climt) if climn then local editqueue = {} local start = os.time() -- review prompts in this session's set for ind, ip in ipairs(ctrt) do if ind > climn then break end local pr = deck[ip[1]][ip[2]] -- remove braces for display local pst = pr.text:gsub("%{([^{}]+)%}", "%1") local st = os.time() imgify(pst:gsub("%%A", "___"), deckfname:match(".*/") or "") io.write("\n") io.read("l") imgify(pst:gsub("%%A", (pr.ans:gsub("%%", "%%%%"))), deckfname:match(".*/") or "") io.write(string.format( "\nAgain / Hard (%s) / Good (%s) / Easy (%s) / Correction ", disptime(review.schedule(pr, st, 0.5)), disptime(review.schedule(pr, st, 1)), disptime(review.schedule(pr, st, 1.5)) )) local resp = io.read("l") local sc = succfromstr(resp) if sc == -1 then table.insert(editqueue, ip[1]) else review.update(deck[ip[1]][ip[2]], st, sc) end clear() end if #ctrt > 0 then local endt = os.time() io.write(string.format( "Reviewed %d prompts in %d seconds\n", math.min(#ctrt, climn), endt - start )) io.write(string.format( "(%f seconds per card)\n", (endt - start) / math.min(#ctrt, climn) )) end if #editqueue > 0 then io.write(string.format( "\n%d cards queued for editing -- press Enter to proceed", #editqueue )) io.read("l") for ind, ci in ipairs(editqueue) do local pr = deck[ci][1] if pr then io.write(string.format( "Card #%d:\n%s\nCancel correction / Delete / Edit ", ci, pr:spt() )) local resp = io.read("l"):sub(1, 1):lower() if resp == "d" then deck[ci][1].deleted = true elseif resp == "e" then io.write("Enter edited version:\n") local ev = io.read("l") local cardrec = Prompt:torec(deck[ci]) cardrec.PromptText = { ev } deck[ci] = Prompt:fromrecs({ cardrec })[1] -- assume all other inputs to mean Cancel else end else io.write(string.format("Card #%d is invalid!\n", ci)) end end end if climn > 0 then loader.save(deckfname, deck) end else io.write("Input not a number -- aborting\n") end else io.write("Failed to load deck\n") end -- Statistics elseif action == "s" then -- TODO else io.write "unreachable" end