Fix 3949c09
John Smith

John Smith commited on 2023-138 05:44:33
Showing 15 changed files, with 0 additions and 1077 deletions.

... ...
@@ -1,23 +0,0 @@
1
---[[
2
-    clears the terminal
3
-    implementation may be platform-specific
4
-]]
5
-function clear()
6
-    -- ANSI
7
-    io.write("\x1b[H\x1b[J")
8
-end
9
-
10
---[[
11
-    check if standard input is a terminal
12
-]]
13
-function isatty()
14
-    local succ = os.execute("tty >/dev/null")
15
-    return succ
16
-end
17
-
18
-local actions = {}
19
-for _, fname in ipairs({"addcards", "correction", "help", "leeches", "query", "reviewtty", "statistics", "toggle", "vocalreview"}) do
20
-    actions[fname:sub(1, 1)] = dofile(SRC_DIR .. fname .. ".lua")
21
-end
22
-
23
-return actions
... ...
@@ -1,44 +0,0 @@
1
---[[
2
-    returns a not-necessarily-meaningful number
3
-    iff `s` has mismathched curly-brackets
4
-]]
5
-local function mismatched(s)
6
-    return ("}" .. s):find("}[^{]+}") or (s .. "{"):find("{[^}]+{")
7
-end
8
-
9
-local function fn(config, deck)
10
-    if isatty() then
11
-        io.write("Reading prompts; press Ctrl-D to stop\n")
12
-    end
13
-    local rt = io.read("a")
14
-    local cardctr = 0
15
-    local prctr = 0
16
-    for pt in rt:gmatch("(..-)\n\n") do
17
-        local nc = {}
18
-        -- these Prompts will just be converted to Rec, so we don't need to exclude answers from prompt-text
19
-        for _re in pt:gmatch("%{[^%}]+%}") do
20
-            table.insert(nc, Prompt:new(pt, "", os.time(), 30, 0, 0, os.time(), config:get("StartingEase") or 2.5))
21
-            prctr = prctr + 1
22
-        end
23
-        if mismatched(pt) then
24
-            io.write(string.format("Prompt %d:1 (\"%s\") contains mismatched braces\n", #deck + 1, pt))
25
-        end
26
-        if #nc >= 1 then
27
-            table.insert(deck, nc)
28
-            cardctr = cardctr + 1
29
-        else
30
-            io.write(string.format("Prompt \"%s\" contains no answers (check braces)\n", pt))
31
-        end
32
-    end
33
-    io.write(string.format(
34
-        "Added %d cards (%d prompts)\n",
35
-        cardctr,
36
-        prctr
37
-    ))
38
-    if cardctr > 0 then
39
-        return deck
40
-    end
41
-end
42
-
43
-return { desc = "(a)dd card (from stdin)", nd = true, fn = fn }
44
-
... ...
@@ -1,25 +0,0 @@
1
-local function fn(config, deck, _, ip)
2
-    local sp = deck[ip[1]][ip[2]]
3
-    local rt = sp:spt()
4
-    local fname = "memoire_cc.tmp"
5
-    local fh = io.open(fname, "w")
6
-    fh:write(rt)
7
-    fh:close()
8
-    os.execute(string.format(
9
-        "%s %q",
10
-        os.getenv("EDITOR") or "vi",
11
-        fname
12
-    ))
13
-    fh = io.open(fname, "r")
14
-    local nt = fh:read("a"):gsub("%s+$", ""):gsub("^%s+", "")
15
-    fh:close()
16
-    os.remove(fname)
17
-    if nt ~= sp:spt() and nt ~= "" then
18
-        local cardrec = Prompt:torec(deck[ip[1]])
19
-        cardrec.PromptText = { nt }
20
-        deck[ip[1]] = Prompt:fromrecs({ cardrec }, config)[1]
21
-        return deck
22
-    end
23
-end
24
-
25
-return { desc = "(c)orrect card (with card:prompt index)", nd = true, ni = true, fn = fn }
... ...
@@ -1,13 +0,0 @@
1
-local function fn()
2
-    io.write(string.format("Usage: %s ACTION\n\nACTION may be one of\n", arg[0]))
3
-    local sal = {}
4
-    for _, v in pairs(ACTIONS) do
5
-        table.insert(sal, v.desc)
6
-    end
7
-    table.sort(sal)
8
-    for _, v in ipairs(sal) do
9
-        io.write(string.format("\t%s\n", v))
10
-    end
11
-end
12
-
13
-return { desc = "(h)elp", nd = false, fn = fn } 
... ...
@@ -1,26 +0,0 @@
1
---[[
2
-    return number indicating how much of a leech the prompt is
3
-]]
4
-local function badness(pr)
5
-    return pr.fail / (pr.fail + pr.succ) * math.log(pr.fail)
6
-end
7
-
8
-local function fn(config, deck)
9
-    local leechqueue = {}
10
-    for i, cg in ipairs(deck) do
11
-        for j, pr in ipairs(cg) do
12
-            if pr.ease < 2 and pr.delay >= 0 then
13
-                table.insert(leechqueue, { i, j })
14
-            end
15
-        end
16
-    end
17
-    table.sort(leechqueue, function(a, b)
18
-        return badness(deck[a[1]][a[2]]) > badness(deck[b[1]][b[2]])
19
-    end)
20
-    io.write(string.format("Detected %d leeches:\n", #leechqueue))
21
-    for _, ij in ipairs(leechqueue) do
22
-        io.write(string.format("%d:%d %s\n", ij[1], ij[2], deck[ij[1]][ij[2]]))
23
-    end
24
-end
25
-
26
-return { desc = "(l)eech search", nd = true, fn = fn } 
... ...
@@ -1,39 +0,0 @@
1
---[[
2
-    deck loading and saving functions (working with Recfiles)
3
-]]
4
-
5
-local Prompt = dofile(SRC_DIR .. "prompt.lua")
6
-local recrw = dofile(SRC_DIR .. "recrw.lua")
7
-
8
---[[
9
-    load a deck (a table of tables of `Prompt`s) from a Recfile
10
-]]
11
-local function loaddeck(fname, conf)
12
-    local fh = io.open(fname)
13
-    if not fh then return nil end
14
-    local rt = fh:read "a"
15
-    if not rt then return nil end
16
-    fh:close()
17
-    local pr = recrw.read(rt)
18
-    if not pr then return nil end
19
-    local ep = Prompt:fromrecs(pr, conf)
20
-    return ep
21
-end
22
-
23
---[[
24
-    save a deck (a table of tables of `Prompt`s) to a Recfile
25
-]]
26
-local function savedeck(fname, deck)
27
-    local rrf = {}
28
-    for i, cg in ipairs(deck) do
29
-        rrf[i] = Prompt:torec(cg)
30
-    end
31
-    local wot = recrw.write(rrf)
32
-    local fh = io.open(fname, "w")
33
-    if not fh then io.write("couldn't open output\n") end
34
-    local wr, err = fh:write(wot)
35
-    if not wr then io.write("couldn't write to output\n") end
36
-    fh:close()
37
-end
38
-
39
-return { load = loaddeck, save = savedeck }
... ...
@@ -1,136 +0,0 @@
1
-#!/usr/bin/env lua
2
---[[
3
-    the main user interface for memoire
4
-]]
5
-
6
-SRC_DIR = arg[0]:match("^.*/") or ""
7
-
8
-Prompt = dofile(SRC_DIR .. "prompt.lua")
9
-review = dofile(SRC_DIR .. "review.lua")
10
-loader = dofile(SRC_DIR .. "loader.lua")
11
-recrw = dofile(SRC_DIR .. "recrw.lua")
12
-ACTIONS = dofile(SRC_DIR .. "actions.lua")
13
-
14
---[[
15
-    check action based on first command-line argument;
16
-    if nothing valid given, default to help
17
-]]
18
-local action = nil
19
-if arg[1] then
20
-    local fc = arg[1]:sub(1, 1)
21
-    action = ACTIONS[fc] or ACTIONS.h
22
-else
23
-    action = ACTIONS.h
24
-end
25
-
26
-local confdir = os.getenv("HOME") .. "/.config/memoire/"
27
-local config = { DeckFile = { "" } }
28
-
29
-do
30
-local fh = io.open(confdir .. "config.rec")
31
-if fh then
32
-    local ft = fh:read("a")
33
-    local ct = recrw.read(ft)
34
-    if ct and ct[1] then config = ct[1]
35
-    else io.write("Erroneous configuration file\n") end
36
-else
37
-    io.write("Could not open configuration file in " .. confdir .. "\n")
38
-end
39
-end
40
-
41
---[[
42
-    gets config option `name`; if multiple values were given,
43
-    provide the `ind`th one (or the first, if `ind` is not given)
44
-    return `nil` if no such option available
45
-]]
46
-function config:get(name, ind)
47
-    return (self[name] or {})[ind or 1]
48
-end
49
-
50
-if not config.DeckFile then
51
-    io.write("No deck files specified; aborting\n")
52
-    os.exit(true, true)
53
-end
54
-
55
-if #config.DeckFile > 1 then
56
-    io.write("Select deck to use (by number):\n")
57
-    for i, n in ipairs(config.DeckFile) do
58
-       io.write(string.format("\t%d. %s\n", i, n))
59
-    end
60
-    local rl = io.read("l")
61
-    local si = tonumber(rl)
62
-    if si and si <= #config.DeckFile and si >= 1 then
63
-        deckfname = config.DeckFile[si]
64
-    elseif si then
65
-        io.write("Selected deck out of range; aborting\n")
66
-        os.exit(true, true)
67
-    else
68
-        io.write("Input not a number; aborting")
69
-        os.exit(true, true)
70
-    end
71
-else
72
-    deckfname = config.DeckFile[1]
73
-end
74
-
75
---[[
76
-    returns four tables:
77
-    question-text, question-lang, answer-text, answer-lang
78
-]]
79
-defaultprspeak = function(pr)
80
-    return {(pr.text:gsub("%{([^{}]+)%}", "%1"):gsub("%%A", "WHAT"))}, {false}, {"ANSWER: " .. pr.ans}, {false}
81
-end
82
-prspeak = defaultprspeak
83
-do
84
-local fh = io.open(confdir .. "prspeak.lua")
85
-if fh then
86
-    fh:close()
87
-    prspeak = dofile(confdir .. "prspeak.lua")
88
-end
89
-end
90
-
91
-deckfname = os.getenv("HOME") .. "/" .. deckfname
92
-
93
-math.randomseed(os.time())
94
-
95
-if action.nd then
96
-    local deck = loader.load(deckfname, config)
97
-    if deck then
98
-        if action.ni then
99
-            local ip
100
-            if arg[2] then
101
-                local a, b = arg[2]:match("(%d+):(%d+)")
102
-                if a and b then
103
-                    -- a and b are valid numbers because they came from a match
104
-                    ip = { tonumber(a), tonumber(b) }
105
-                else
106
-                    ip = tonumber(arg[2])
107
-                    if ip then
108
-                        ip = { ip, 1 }
109
-                    end
110
-                end
111
-            end
112
-            if ip then
113
-                deck = action.fn(config, deck, deckfname, ip)
114
-            else
115
-                io.write("Bad prompt index (must be cardnum:promptnum)\n")
116
-            end
117
-        elseif action.nq then
118
-            local qs
119
-            if arg[2] then
120
-                qs = arg[2]
121
-            end
122
-            deck = action.fn(config, deck, deckfname, qs)
123
-        else
124
-            deck = action.fn(config, deck, deckfname)
125
-        end
126
-        if deck then
127
-            loader.save(deckfname, deck)
128
-        end
129
-    else
130
-        io.write("Failed to load deck\n")
131
-    end
132
-else
133
-    action.fn(config)
134
-end
... ...
@@ -1,112 +0,0 @@
1
---[[
2
-    a prompt consists of prompt text (with an answer-insertion point),
3
-    a correct answer, the review timestamp, the review delay,
4
-    counts of successful and failed reviews,
5
-    a creation timestamp, and an ease measurement
6
-
7
-    timestamps are seconds from the Unix epoch
8
-    review delay is in seconds
9
-]]
10
-
11
-local Prompt = {}
12
-Prompt.__index = Prompt
13
-
14
---[[
15
-    the answer-insertion point is indicated with a "%A" in the prompt text
16
-]]
17
-function Prompt:new(pt, ca, rts, rd, sc, fc, ct, ez)
18
-    local o = {}
19
-    setmetatable(o, self)
20
-    o.text = pt
21
-    o.ans = ca
22
-    o.time = tonumber(rts)
23
-    o.delay = tonumber(rd)
24
-    o.succ = tonumber(sc)
25
-    o.fail = tonumber(fc)
26
-    o.created = tonumber(ct)
27
-    o.ease = tonumber(ez)
28
-    return o
29
-end
30
-
31
-function Prompt:__tostring()
32
-    return string.format(
33
-        "\"%s\" (\"%s\") %d -> %d+%d*%f %d/%d",
34
-        self.text, self.ans, self.created or 0,
35
-        self.time, math.floor(self.delay), self.ease,
36
-        self.succ, self.fail
37
-    )
38
-end
39
-
40
-function Prompt:retention()
41
-    return self.succ / (self.succ + self.fail)
42
-end
43
-
44
---[[
45
-    derive the `PromptText` used to create this `Prompt`
46
-]]
47
-function Prompt:spt()
48
-    return self.text:gsub("%%A", "{" .. self.ans:gsub("%%", "%%%%") .. "}")
49
-end
50
-
51
---[[
52
-    extracts `Prompt`s from a record-based deck
53
-    returns a table of tables of `Prompt`s
54
-]]
55
-function Prompt:fromrecs(recs, conf)
56
-    local ret = {}
57
-    for i, rec in ipairs(recs) do
58
-        -- only bother with records with `PromptText`
59
-        if rec.PromptText then
60
-            ret[i] = {}
61
-            local opt = rec.PromptText[1]
62
-            local m, n = opt:find "%{[^%}]+%}"
63
-            local ctr = 1
64
-            while m do
65
-                local mpt = opt:sub(1, m - 1) .. "%A" .. opt:sub(n + 1)
66
-                table.insert(ret[i], Prompt:new(
67
-                    mpt,
68
-                    opt:sub(m + 1, n - 1),
69
-                    (rec.LastReview or {})[ctr] or (rec.LastReview or {})[1],
70
-                    (rec.LastDelay or {})[ctr] or 15,
71
-                    (rec.Successes or {})[ctr] or 0,
72
-                    (rec.Failures or {})[ctr] or 0,
73
-                    (rec.Created or {})[1],
74
-                    (rec.Ease or {})[ctr] or conf:get("StartingEase") or "2.5"
75
-                ))
76
-                m, n = opt:find("%{[^%}]+%}", n)
77
-                ctr = ctr + 1
78
-            end
79
-        end
80
-    end
81
-    return ret
82
-end
83
-
84
---[[
85
-    serialises an ordered table of `Prompt`s derived from the same `PromptText` to a record
86
-    returns (on success) a table mapping labels to value-lists
87
-    returns (on failure) nil
88
-]]
89
-function Prompt:torec(prompts)
90
-    if prompts[1].deleted then
91
-        return {}
92
-    end
93
-    local fspt = prompts[1]:spt()
94
-    local ret = { PromptText = { fspt }, LastReview = {}, LastDelay = {}, Successes = {}, Failures = {}, Ease = {} }
95
-    if prompts[1].created then
96
-        ret.Created = { prompts[1].created }
97
-    end
98
-    for i, pr in ipairs(prompts) do
99
-        if pr:spt() ~= fspt then
100
-            -- not from the same `PromptText`
101
-            return nil
102
-        end
103
-        ret.LastReview[i] = pr.time
104
-        ret.LastDelay[i] = pr.delay
105
-        ret.Successes[i] = pr.succ
106
-        ret.Failures[i] = pr.fail
107
-        ret.Ease[i] = pr.ease
108
-    end
109
-    return ret
110
-end
111
-
112
-return Prompt
... ...
@@ -1,18 +0,0 @@
1
-local function fn(config, deck, deckfname, qs)
2
-    local mcl = {}
3
-    for i, card in ipairs(deck) do
4
-        if card[1]:spt():find(qs) then
5
-            table.insert(mcl, i)
6
-        end
7
-    end
8
-    if #mcl >= 1 then
9
-        io.write(string.format("Found %d match%s for %q:\n", #mcl, #mcl == 1 and "" or "es", qs))
10
-    else
11
-        io.write(string.format("No matches for %q\n", qs))
12
-    end
13
-    for _, i in ipairs(mcl) do
14
-        io.write(string.format("%d %s\n", i, deck[i][1]:spt()))
15
-    end
16
-end
17
-
18
-return { desc = "(q)uery deck for cards (case-sensitive)", nd = true, nq = true, fn = fn }
... ...
@@ -1,106 +0,0 @@
1
---[[
2
-    a Recfile is (to simplify) a sequence of records
3
-    a record is a sequence of fields
4
-    a field is a label-value pair
5
-    a label is an identifier
6
-    a value is a string
7
-]]
8
-
9
---[[
10
-    converts text from a Recfile into a sequence of records
11
-    returns (on success) a table of tables of pairs of labels and value-lists
12
-    returns (on failure) nil
13
-]]
14
-local function text2recs(str)
15
-    local ret = {}
16
-    local cr = {}
17
-    local cl = nil
18
-    local cv = nil
19
-    -- Rec is line-based
20
-    for line in str:gmatch "([^\n]*)\n" do
21
-        -- continuation lines
22
-        if cv and cv:sub(-1) == "\\" then
23
-            cv = cv:sub(1, -2) .. line
24
-        elseif line:sub(1, 1) == "+" then
25
-            cv = cv .. "\n" .. line:gsub("^%+ ?", "")
26
-        -- blank lines separate records
27
-        elseif line:match "^%s*$" then
28
-            -- add field to record if non-empty
29
-            if cl then
30
-                if cr[cl] then
31
-                    table.insert(cr[cl], cv)
32
-                else
33
-                    cr[cl] = { cv }
34
-                end
35
-            end
36
-            cl = nil
37
-            cv = nil
38
-            -- add record to return value if non-empty
39
-            if next(cr) then
40
-                table.insert(ret, cr)
41
-            end
42
-            cr = {}
43
-        -- lines starting with # are comments
44
-        elseif line:sub(1, 1) == "#" then
45
-            if cl then
46
-                if cr[cl] then
47
-                    table.insert(cr[cl], cv)
48
-                else
49
-                    cr[cl] = { cv }
50
-                end
51
-            end
52
-            cl = nil
53
-            cv = nil
54
-        -- new field
55
-        else
56
-            if cl then
57
-                if cr[cl] then
58
-                    table.insert(cr[cl], cv)
59
-                else
60
-                    cr[cl] = { cv }
61
-                end
62
-            end
63
-            cl, cv = line:match "^([%a%%][%w_]*):%s(.*)"
64
-            -- no match? bad field
65
-            if not cl then return nil end
66
-        end
67
-    end
68
-    if cl then
69
-        if cr[cl] then
70
-            table.insert(cr[cl], cv)
71
-        else
72
-            cr[cl] = { cv }
73
-        end
74
-    end
75
-    if next(cr) then
76
-        table.insert(ret, cr)
77
-    end
78
-    return ret
79
-end
80
-
81
---[[
82
-    converts a sequence of records into text for a Recfile
83
-    returns (on success) a string
84
-    returns (on failure) nil
85
-]]
86
-local function recs2text(recs, si, ei)
87
-    local ret = ""
88
-    si = si or 1
89
-    ei = ei or #recs
90
-    --[[
91
-        use recursive (binary-splitting) string building
92
-        this makes stuff way more efficient
93
-    ]]
94
-    if si == ei then
95
-        for label, vl in pairs(recs[si]) do
96
-            for j, value in ipairs(vl) do
97
-                ret = ret .. string.format("\n%s: %s", label, tostring(value):gsub("\n", "\n+ "))
98
-            end
99
-        end
100
-        return ret .. "\n"
101
-    else
102
-        return recs2text(recs, si, (si + ei) // 2) .. recs2text(recs, (si + ei) // 2 + 1, ei)
103
-    end
104
-end
105
-
106
-return { read = text2recs, write = recs2text }
... ...
@@ -1,120 +0,0 @@
1
---[[
2
-    stuff to help with the review mechanics
3
-]]
4
-
5
---[[
6
-    determines the delay until next review (in seconds)
7
-    when `Prompt` `prompt` is reviewed at timestamp `revtime`
8
-    with review success `revsucc`
9
-]]
10
-local function schedule(prompt, revtime, revsucc, conf)
11
-    if revsucc and revsucc > 0 then
12
-        local ad = revtime - (prompt.time or revtime)
13
-        local adm = ad + math.max(40000 - 0.5 * ad, 0)
14
-        -- SM2, as explained by Bjornstad
15
-        -- https://controlaltbackspace.org/memory/spaced-repetition-from-the-ground-up/
16
-        if revsucc < 0.8 then
17
-            return (conf:get("HardRatio") or 1.2) * revsucc * 2 * adm
18
-        else
19
-            return prompt.ease * (1 + 2 * ((conf:get("EasyRatio") or 1.3) - 1) * (revsucc - 1)) * adm
20
-        end
21
-    else
22
-        return 15
23
-    end
24
-end
25
-
26
---[[
27
-    manipulates `Prompt` `prompt` in-place as it should be
28
-    when reviewed at timestamp `revtime` with review success `revsucc`
29
-]]
30
-local function update(prompt, revtime, revsucc, conf)
31
-    if revsucc and revsucc > 0 then
32
-        prompt.succ = (prompt.succ or 0) + 1
33
-    else
34
-        prompt.fail = (prompt.fail or 0) + 1
35
-    end
36
-    -- allowed random variation, as a fraction of the "optimal" duration
37
-    local ARV = conf:get("AllowedRandomVariation") or 0.1
38
-    prompt.delay = schedule(prompt, revtime, revsucc, conf) * ((1 - ARV) + 2 * ARV * math.random())
39
-    prompt.ease = prompt.ease + ({ -0.2, -0.15, 0, 0.15 })[2 * revsucc + 1]
40
-    prompt.time = revtime
41
-end
42
-
43
---[[
44
-    rates the importance of `Prompt` `prompt` as an additive bias
45
-    based on the rules given in `conf`
46
-]]
47
-local function importance(prompt, conf)
48
-    pt = conf["PriorityText"] or {}
49
-    pb = conf["PriorityBias"] or {}
50
-    s = 0
51
-    for i, t in ipairs(pt) do
52
-        if prompt:spt():find(t) then
53
-            s = s + pb[i]
54
-        end
55
-    end
56
-    return s
57
-end
58
-
59
---[[
60
-    checks if `Prompt` `prompt` should be reviewed now,
61
-    returning `false` if not and a number to indicate urgency if it should
62
-]]
63
-local function should(prompt, conf)
64
-    local ct = os.time()
65
-    if not prompt.delay then return -1 end
66
-    if prompt.delay >= 0 and ct - (prompt.time or 0) >= (prompt.delay or 0) then
67
-        return (ct - (prompt.time or 0)) / prompt.delay / prompt.ease / (prompt.succ + 1) + importance(prompt, conf) + 0.1 * math.random()
68
-    end
69
-    return false
70
-end
71
-
72
-local function alwaystrue(x)
73
-    return true
74
-end
75
-
76
---[[
77
-    returns a table of pairs of integers to index the deck
78
-    to get prompts to be reviewed now;
79
-    prompts are shuffled in order
80
-    if a filter function is provided, filters the list accordingly,
81
-    calling the filter function on each `Prompt`
82
-]]
83
-local function toreviewlist(deck, conf, filter)
84
-    local ctrt = {}
85
-    local ff = filter or alwaystrue
86
-    for i, cg in ipairs(deck) do
87
-        for j, pr in ipairs(cg) do
88
-            local sc = should(pr, conf)
89
-            if sc and ff(pr) then
90
-                table.insert(ctrt, { i, j, sc or 0 })
91
-            end
92
-        end
93
-    end
94
-    table.sort(ctrt, function(a, b)
95
-        return a[3] > b[3]
96
-    end)
97
-    return ctrt
98
-end
99
-
100
---[[
101
-    gets the score (again/hard/good/easy/correction) from a string by first character
102
-    scores are numbers, higher is easier, -1 is correction
103
-]]
104
-local function succfromstr(s)
105
-    local f = s:sub(1, 1):lower()
106
-    if f == "0" or f == "a" then
107
-        return 0
108
-    elseif f == "1" or f == "h" then
109
-        return 0.5
110
-    elseif f == "2" or f == "g" then
111
-        return 1
112
-    elseif f == "3" or f == "e" then
113
-        return 1.5
114
-    elseif f == "-" or f == "c" then
115
-        return -1
116
-    end
117
-    return 1
118
-end
119
-
120
-return { schedule = schedule, update = update, should = should, list = toreviewlist, succfromstr = succfromstr }
... ...
@@ -1,187 +0,0 @@
1
---[[
2
-    show progress thru a review session, conditioned on the user
3
-    configuring that to happen
4
-]]
5
-local function showprogress(k, n, conf)
6
-    if conf:get("ShowProgress") then
7
-        io.write(string.format("(%d/%d) ", k, n))
8
-    end
9
-end
10
-
11
---[[
12
-    display text with images, inserted with `IMG[path]`
13
-    implementation may be platform-specific
14
-    `basepath` points to the base directory of the images
15
-]]
16
-local function imgify(text, basepath, ind, climn, conf)
17
-    local fh = io.popen("tput lines")
18
-    local tl = tonumber(fh:read("a"))
19
-    io.write(("\n"):rep(math.floor(0.4 * tl)))
20
-    showprogress(ind, climn, conf)
21
-    if conf:get("BoldContext") then
22
-        local csi, cei = text:find("^[^:]+:")
23
-        if csi then
24
-            local cpt = text:sub(csi, cei)
25
-            io.write("\x1b[1m")
26
-            io.write(cpt)
27
-            io.write("\x1b[0m")
28
-            text = text:sub(cei + 1)
29
-        end
30
-    end
31
-    -- adapted from https://stackoverflow.com/a/1579673
32
-    local IMG_PATTERN = "IMG%[([^%]]+)%]"
33
-    local PATTERN = "(.-)" .. IMG_PATTERN
34
-    -- Linux/dwm
35
-    local DISPIMG = function(path, st)
36
-        if path and #path > 0 then
37
-            os.execute(string.format(
38
-                "/usr/local/bin/iv -t '%s' %s%s &",
39
-                st:gsub("'", "\""):gsub(IMG_PATTERN, ""), basepath, path
40
-            ))
41
-        end
42
-    end
43
-    local lastend = 1
44
-    local mstart, mend, prevtext, imgpath = text:find(PATTERN)
45
-    io.write(prevtext or "")
46
-    DISPIMG(imgpath, text)
47
-    while mstart do
48
-        lastend = mend + 1
49
-        mstart, mend, prevtext, imgpath = text:find(PATTERN, lastend)
50
-        io.write(prevtext or "")
51
-        DISPIMG(imgpath, text)
52
-    end
53
-    if lastend <= #text then
54
-        io.write(text:sub(lastend))
55
-    end
56
-end
57
-
58
---[[
59
-    rounds `x` to `n` places (default 0) after the radix point
60
-    in base `b` (default 10)
61
-]]
62
-local function round(x, n, b)
63
-    local b = b or 10
64
-    local n = n or 0
65
-    if n == 0 then
66
-        return math.tointeger(math.floor(x * b ^ n) / b ^ n)
67
-    else
68
-        return math.floor(x * b ^ n) / b ^ n
69
-    end
70
-end
71
-
72
---[[
73
-    return human-friendly string to present a duration (input in seconds),
74
-    e.g. "44:37", "4.8h", "11d", "8.3m", "17.4y"
75
-]]
76
-local function disptime(ts)
77
-    local YDAY = 365.24
78
-    local MDAY = YDAY / 12
79
-    local DSEC = 86400
80
-    local HSEC = 3600
81
-    if ts >= 3 * YDAY * DSEC then
82
-        return round(ts / (YDAY * DSEC), 1) .. "y"
83
-    elseif ts >= 4 * MDAY * DSEC then
84
-        return round(ts / (MDAY * DSEC), 1) .. "m"
85
-    elseif ts >= 10 * DSEC then
86
-        return round(ts / DSEC) .. "d"
87
-    elseif ts >= 1.5 * DSEC then
88
-        return round(ts / DSEC, 1) .. "d"
89
-    elseif ts >= 8 * HSEC then
90
-        return round(ts / HSEC) .. "h"
91
-    elseif ts >= HSEC then
92
-        return round(ts / HSEC, 1) .. "h"
93
-    else
94
-        return string.format("%d:%02d", round(ts / 60), round(ts % 60))
95
-    end
96
-end
97
-
98
-local function fn(config, deck, deckfname)
99
-    if not isatty() then
100
-        io.write("Review should be done in the terminal\n")
101
-        return nil
102
-    end
103
-    -- collect list of index-pairs for prompts to be reviewed this session
104
-    local ctrt = review.list(deck, config)
105
-    io.write(string.format(
106
-        "%d prompts are ready for review.\nHow many to review in this session? ", #ctrt
107
-    ))
108
-    io.flush()
109
-    local climt = io.read("l")
110
-    local climn = tonumber(climt)
111
-    if not climn then
112
-        io.write("Input not a number; aborting\n")
113
-        return nil
114
-    end
115
-    climn = math.min(climn, #ctrt)
116
-    local lrc = 0
117
-    local editqueue = {}
118
-    local start = os.time()
119
-    -- review prompts in this session's set
120
-    for ind, ip in ipairs(ctrt) do
121
-        if ind > climn then
122
-            break
123
-        end
124
-        local pr = deck[ip[1]][ip[2]]
125
-        -- remove braces for display
126
-        local pst = pr.text:gsub("%{([^{}]+)%}", "%1")
127
-        local st = os.time()
128
-        imgify(pst:gsub("%%A", "___"), deckfname:match(".*/") or "", ind, climn, config)
129
-        io.write("\n")
130
-        io.read("l")
131
-        clear()
132
-        imgify(
133
-            pst:gsub("%%A", "\x1b[1m" .. (pr.ans:gsub("%%", "%%%%")) .. "\x1b[0m"),
134
-            deckfname:match(".*/") or "",
135
-            ind,
136
-            climn,
137
-            config
138
-        )
139
-        io.write(string.format(
140
-            "\nAgain / Hard (%s) / Good (%s) / Easy (%s) / Correction ",
141
-            disptime(review.schedule(pr, st, 0.5, config)),
142
-            disptime(review.schedule(pr, st, 1, config)),
143
-            disptime(review.schedule(pr, st, 1.5, config))
144
-        ))
145
-        io.flush()
146
-        local resp = io.read("l")
147
-        local sc = review.succfromstr(resp)
148
-        if sc == -1 then
149
-            table.insert(editqueue, ip)
150
-        else
151
-            if sc == 0 then
152
-                lrc = lrc + 1
153
-            end
154
-            review.update(deck[ip[1]][ip[2]], st, sc, config)
155
-        end
156
-        clear()
157
-        if resp:sub(#resp):lower() == "q" then
158
-            climn = ind
159
-            break
160
-        end
161
-    end
162
-    if #ctrt > 0 then
163
-        local endt = os.time()
164
-        io.write(string.format(
165
-            "Reviewed %d prompts in %d seconds\n",
166
-            math.min(#ctrt, climn),
167
-            endt - start
168
-        ))
169
-        local s
170
-        if lrc == 1 then s = "" else s = "s" end
171
-        io.write(string.format(
172
-            "(%d lapse%s, %f seconds per card)\n",
173
-            lrc, s, (endt - start) / math.min(#ctrt, climn)
174
-        ))
175
-    end
176
-    if #editqueue > 0 then
177
-        io.write(string.format("The following cards are marked for correction:\n"))
178
-        for _, ip in ipairs(editqueue) do
179
-            io.write(string.format("%d:%d %s\n", ip[1], ip[2], deck[ip[1]][ip[2]]))
180
-        end
181
-    end
182
-    if climn > 0 then
183
-        return deck
184
-    end
185
-end
186
-
187
-return { desc = "(r)eview sesion", nd = true, fn = fn }
... ...
@@ -1,115 +0,0 @@
1
---[[
2
-    generate and return text-based vertical histogram,
3
-    with bins of given width across interval from max to min,
4
-    transforming values to numbers via func,
5
-    scaling bar-length by scale
6
-]]
7
-local function histogram(list, min, max, width, func, scale)
8
-    local bins = {}
9
-    local oorc = 0
10
-    local prc = 0
11
-    for _, v in ipairs(list) do
12
-        local n = func(v)
13
-        if n == n and n >= min and n < max + width then
14
-            local k = math.floor((n - min) / width) + 1
15
-            bins[k] = (bins[k] or 0) + 1
16
-        else
17
-            oorc = oorc + 1
18
-        end
19
-        prc = prc + 1
20
-    end
21
-    local s = ""
22
-    for n = min, max, width do
23
-        local k = math.floor((n - min) / width) + 1
24
-        s = string.format("%s%.2f\t%s %d\n", s, n, ("\u{2588}"):rep(math.ceil(scale * (bins[k] or 0))), bins[k] or 0)
25
-    end
26
-    return s .. string.format("(%d out of range)\n", oorc)
27
-end
28
-
29
-local function fn(config, deck, deckfname, qs)
30
-    local tcc = 0
31
-    local tsc = 0
32
-    local tfc = 0
33
-    local spc = 0
34
-    local expdeck = {}
35
-    for _, card in ipairs(deck) do
36
-        local rtc
37
-        for _, pr in ipairs(card) do
38
-            if not qs or (pr.text:find(qs) or pr.ans:find(qs)) then
39
-                rtc = true
40
-                tsc = tsc + pr.succ
41
-                tfc = tfc + pr.fail
42
-                if pr.delay < 0 then
43
-                    spc = spc + 1
44
-                else
45
-                    table.insert(expdeck, pr)
46
-                end
47
-            end
48
-        end
49
-        if rtc then
50
-            tcc = tcc + 1
51
-        end
52
-    end
53
-    local qfn
54
-    if qs then
55
-        qfn = string.format(" (filtered with query %q)", qs)
56
-    else
57
-        qfn = ""
58
-    end
59
-    io.write(string.format(
60
-        "%s%s has %d cards and %d total prompts\n",
61
-        deckfname, qfn, tcc, #expdeck + spc
62
-    ))
63
-    io.write(string.format(
64
-        "(average %.2f prompts per card, %d/%.1f%% prompts suspended).\n",
65
-        (#expdeck + spc) / tcc, spc, 100 * spc / (#expdeck + spc)
66
-    ))
67
-    io.write(string.format(
68
-        "The deck's prompts have been reviewed a total of %d times\n",
69
-        tsc + tfc
70
-    ))
71
-    io.write(string.format(
72
-        "(average %.2f reviews per prompt), overall retention %.2f%%.\n",
73
-        (tsc + tfc) / (#expdeck + spc), 100 * tsc / (tsc + tfc)
74
-    ))
75
-    io.write("All further data excludes suspended prompts.\n")
76
-    io.write("\nEase\n" .. histogram(expdeck, 1.0, 3.3, 0.1, function(pr) return pr.ease end, 0.03))
77
-    io.write("\nLog2-delay (seconds)\n" .. histogram(expdeck, 15, 28, 0.5, function(pr) return math.log(pr.delay, 2) end, 0.1))
78
-    io.write("\nDelay-last review ratio\n" .. histogram(expdeck, 0, 2, 0.1, function(pr) return (os.time() - pr.time) / pr.delay end, 0.1))
79
-    io.write("\nRetention (%)\n" .. histogram(expdeck, 30, 100, 10, function(pr) return 100 * pr:retention() end, 0.02))
80
-    io.write("\nReview counts\n" .. histogram(expdeck, 0, 20, 2, function(pr) return pr.succ + pr.fail end, 0.03))
81
-    io.write("\nLog2-age (seconds)\n" .. histogram(expdeck, 18, 28, 0.5, function(pr) return math.log(os.time() - (pr.created or 2 * os.time()), 2) end, 0.05))
82
-    io.write("\nPrompt-count\n" .. histogram(deck, 1, 10, 1, function(card) return #card end, 0.03))
83
-    local cpcs = {}
84
-    for _, card in ipairs(deck) do
85
-        local cp = card[1].text:match("([^:]+):") or ""
86
-        cpcs[cp] = (cpcs[cp] or 0) + 1
87
-    end
88
-    local scl = {}
89
-    for l, c in pairs(cpcs) do
90
-        table.insert(scl, { l = l, c = c })
91
-    end
92
-    table.sort(scl, function(a, b) return a.c > b.c end)
93
-    for k, lc in ipairs(scl) do
94
-        scl[k] = string.format("%s (%d)", lc.l ~= "" and lc.l or "(none)", lc.c)
95
-    end
96
-    io.write("\nMost common contexts: " .. table.concat(scl, ", ", 1, math.min(20, #scl)) .. "\n")
97
-    table.sort(expdeck, function(a, b) return a.time + a.delay < b.time + b.delay end)
98
-    io.write("\nUpcoming due prompts wrt days from now:\n")
99
-    local ppi = 1
100
-    local mpi = 1
101
-    for dc = 1,100 do
102
-        for uc = ppi,#expdeck do
103
-            if expdeck[uc].time + expdeck[uc].delay - os.time() > dc * 86400 then
104
-                mpi = uc
105
-                break
106
-            end
107
-        end
108
-        if dc <= 10 or dc % 10 == 0 then
109
-            io.write(string.format("%d\t%s %d\n", dc, ("\u{2588}"):rep(math.ceil((mpi - ppi) / 10)), mpi - ppi))
110
-        end
111
-        ppi = mpi
112
-    end
113
-end
114
-
115
-return { desc = "(s)tatistics (with optional query)", nd = true, nq = true, fn = fn }
... ...
@@ -1,14 +0,0 @@
1
-local function fn(config, deck, _, ip)
2
-    local sp = deck[ip[1]][ip[2]]
3
-    local dir
4
-    if sp.delay < 0 then
5
-        dir = "Restoring"
6
-    else
7
-        dir = "Suspending"
8
-    end
9
-    io.write(string.format("%s %s\n", dir, sp))
10
-    sp.delay = -sp.delay
11
-    return deck
12
-end
13
-
14
-return { desc = "(t)oggle suspension of prompt (with card:prompt index)", nd = true, ni = true, fn = fn }
... ...
@@ -1,101 +0,0 @@
1
---[[
2
-    presents `text` thru text-to-speech,
3
-    with language/voice code `lang` (`false` or `nil` for default)
4
-    `text` and `lang` may also both be parallel tables
5
-    implementation may be platform-specific
6
-]]
7
-local function speak(text, conf, lang)
8
-    if type(text) == "string" and (type(lang) == "string" or type(lang) == "nil") then
9
-        text = {text}
10
-        lang = {lang}
11
-    end
12
-    for i, t in ipairs(text) do
13
-        os.execute(string.format(
14
-            "espeak %s %s %q",
15
-            conf:get("EspeakFlags") or "",
16
-            lang[i] and ("-v " .. lang[i]) or "",
17
-            t
18
-        ))
19
-    end
20
-end
21
-
22
-local function fn(config, deck, deckfname)
23
-    if not isatty() then
24
-        io.write("Review should be done in the terminal\n")
25
-        return nil
26
-    end
27
-    -- collect list of index-pairs for prompts to be reviewed this session
28
-    local ctrt = review.list(deck, config, function(pr) return not pr:spt():find("IMG%[") end)
29
-    speak(string.format(
30
-        "%d prompts to review; how many in this session? ", #ctrt
31
-    ), config)
32
-    local climt = io.read("l")
33
-    local climn = tonumber(climt)
34
-    if not climn then
35
-        speak("Input not a number; aborting", conf)
36
-        return nil
37
-    end
38
-    climn = math.min(climn, #ctrt)
39
-    local lrc = 0
40
-    local editqueue = {}
41
-    local start = os.time()
42
-    -- review prompts in this session's set
43
-    for ind, ip in ipairs(ctrt) do
44
-        if ind > climn then
45
-            break
46
-        end
47
-        local pr = deck[ip[1]][ip[2]]
48
-        -- remove braces for display
49
-        local pst = pr.text:gsub("%{([^{}]+)%}", "%1")
50
-        local st = os.time()
51
-        io.write(pst .. "\n" .. ind .. "/" .. climn .. "\n")
52
-        local qt, ql, at, al = prspeak(pr)
53
-        local heard = false
54
-        while not heard do
55
-            speak(qt, config, ql)
56
-            if io.read("l") ~= "r" then heard = true end
57
-        end
58
-        speak(at, config, al)
59
-        speak("hard, good, easy?", config)
60
-        local resp = io.read("l")
61
-        local sc = review.succfromstr(resp)
62
-        if sc == -1 then
63
-            table.insert(editqueue, ip)
64
-        else
65
-            if sc == 0 then
66
-                lrc = lrc + 1
67
-            end
68
-            review.update(deck[ip[1]][ip[2]], st, sc, config)
69
-        end
70
-        clear()
71
-        if resp:sub(#resp):lower() == "q" then
72
-            climn = ind
73
-            break
74
-        end
75
-    end
76
-    if #ctrt > 0 then
77
-        local endt = os.time()
78
-        io.write(string.format(
79
-            "Reviewed %d prompts in %d seconds\n",
80
-            math.min(#ctrt, climn),
81
-            endt - start
82
-        ))
83
-        local s
84
-        if lrc == 1 then s = "" else s = "s" end
85
-        io.write(string.format(
86
-            "(%d lapse%s, %f seconds per card)\n",
87
-            lrc, s, (endt - start) / math.min(#ctrt, climn)
88
-        ))
89
-    end
90
-    if #editqueue > 0 then
91
-        io.write(string.format("The following cards are marked for correction:\n"))
92
-        for _, ip in ipairs(editqueue) do
93
-            io.write(string.format("%d:%d %s\n", ip[1], ip[2], deck[ip[1]][ip[2]]))
94
-        end
95
-    end
96
-    if climn > 0 then
97
-        return deck
98
-    end
99
-end
100
-
101
-return { desc = "(v)ocal review session", nd = true, fn = fn }
102 0