Radically refactor main interface
John Smith

John Smith commited on 2022-302 00:35:58
Showing 2 changed files, with 352 additions and 325 deletions.


main.lua now only contains a framework to select and execute an action;
all actions are implemented in actions.lua.
... ...
@@ -0,0 +1,341 @@
1
+--[[
2
+    gets the score (again/hard/good/easy/correction) from a string by first character
3
+    scores are numbers, higher is easier, -1 is correction
4
+]]
5
+local function succfromstr(s)
6
+    local f = s:sub(1, 1):lower()
7
+    if f == "0" or f == "a" then
8
+        return 0
9
+    elseif f == "1" or f == "h" then
10
+        return 0.5
11
+    elseif f == "2" or f == "g" then
12
+        return 1
13
+    elseif f == "3" or f == "e" then
14
+        return 1.5
15
+    elseif f == "-" or f == "c" then
16
+        return -1
17
+    end
18
+    return 1
19
+end
20
+
21
+--[[
22
+    clears the terminal
23
+    implementation may be platform-specific
24
+]]
25
+local function clear()
26
+    -- ANSI
27
+    io.write("\x1b[H\x1b[J")
28
+end
29
+
30
+--[[
31
+    display text with images, inserted with `IMG[path]`
32
+    implementation may be platform-specific
33
+    `basepath` points to the base directory of the images
34
+]]
35
+local function imgify(text, basepath, conf)
36
+    if conf:get("BoldContext") then
37
+        local csi, cei = text:find("^[^:]+:")
38
+        local cpt = text:sub(csi, cei)
39
+        io.write("\x1b[1m")
40
+        io.write(cpt)
41
+        io.write("\x1b[0m")
42
+        text = text:sub(cei + 1)
43
+    end
44
+    -- adapted from https://stackoverflow.com/a/1579673
45
+    local IMG_PATTERN = "IMG%[([^%]]+)%]"
46
+    local PATTERN = "(.-)" .. IMG_PATTERN
47
+    -- Linux/dwm
48
+    local DISPIMG = function(path, st)
49
+        if path and #path > 0 then
50
+            os.execute(string.format(
51
+                "/usr/local/bin/iv -t '%s' %s%s &",
52
+                st:gsub("'", "\""):gsub(IMG_PATTERN, ""), basepath, path
53
+            ))
54
+        end
55
+    end
56
+    local lastend = 1
57
+    local mstart, mend, prevtext, imgpath = text:find(PATTERN)
58
+    io.write(prevtext or "")
59
+    DISPIMG(imgpath, text)
60
+    while mstart do
61
+        lastend = mend + 1
62
+        mstart, mend, prevtext, imgpath = text:find(PATTERN, lastend)
63
+        io.write(prevtext or "")
64
+        DISPIMG(imgpath, text)
65
+    end
66
+    if lastend <= #text then
67
+        io.write(text:sub(lastend))
68
+    end
69
+end
70
+
71
+--[[
72
+    rounds `x` to `n` places (default 0) after the radix point
73
+    in base `b` (default 10)
74
+]]
75
+local function round(x, n, b)
76
+    local b = b or 10
77
+    local n = n or 0
78
+    if n == 0 then
79
+        return math.tointeger(math.floor(x * b ^ n) / b ^ n)
80
+    else
81
+        return math.floor(x * b ^ n) / b ^ n
82
+    end
83
+end
84
+
85
+--[[
86
+    return human-friendly string to present a duration (input in seconds),
87
+    e.g. "44:37", "4.8h", "11d", "8.3m", "17.4y"
88
+]]
89
+local function disptime(ts)
90
+    local YDAY = 365.24
91
+    local MDAY = YDAY / 12
92
+    local DSEC = 86400
93
+    local HSEC = 3600
94
+    if ts >= 3 * YDAY * DSEC then
95
+        return round(ts / (YDAY * DSEC), 1) .. "y"
96
+    elseif ts >= 4 * MDAY * DSEC then
97
+        return round(ts / (MDAY * DSEC), 1) .. "m"
98
+    elseif ts >= 10 * DSEC then
99
+        return round(ts / DSEC) .. "d"
100
+    elseif ts >= 1.5 * DSEC then
101
+        return round(ts / DSEC, 1) .. "d"
102
+    elseif ts >= 8 * HSEC then
103
+        return round(ts / HSEC) .. "h"
104
+    elseif ts >= HSEC then
105
+        return round(ts / HSEC, 1) .. "h"
106
+    else
107
+        return string.format("%d:%02d", round(ts / 60), round(ts % 60))
108
+    end
109
+end
110
+
111
+--[[
112
+    check if standard input is a terminal
113
+]]
114
+local function isatty()
115
+    local succ = os.execute("tty >/dev/null")
116
+    return succ
117
+end
118
+
119
+--[[
120
+    return number indicating how much of a leech the prompt is
121
+]]
122
+local function badness(pr)
123
+    return pr.fail / (pr.fail + pr.succ) * math.log(pr.fail)
124
+end
125
+
126
+local actions = {
127
+    a = {
128
+        desc = "(a)dd card",
129
+        nd = true,
130
+        fn = function(config, deck)
131
+            if isatty() then
132
+                io.write("Reading prompts -- press Ctrl-D to stop\n")
133
+            else
134
+                io.write("Reading prompts ...\n")
135
+            end
136
+            local rt = io.read("a")
137
+            local cardctr = 0
138
+            local prctr = 0
139
+            for pt in rt:gmatch("(..-)\n\n") do
140
+                local nc = {}
141
+                -- these Prompts will just be converted to Rec, so we don't need to exclude answers from prompt-text
142
+                for _re in pt:gmatch("%{[^%}]+%}") do
143
+                    table.insert(nc, Prompt:new(pt, "", os.time(), 30, 0, 0, os.time(), config:get("StartingEase") or 2.5))
144
+                    prctr = prctr + 1
145
+                end
146
+                table.insert(deck, nc)
147
+                cardctr = cardctr + 1
148
+            end
149
+            io.write(string.format(
150
+                "Added %d cards (%d prompts)\n",
151
+                cardctr,
152
+                prctr
153
+            ))
154
+            if cardctr > 0 then
155
+                return deck
156
+            end
157
+        end,
158
+    },
159
+
160
+    h = {
161
+        desc = "(h)elp",
162
+        nd = false,
163
+        fn = function()
164
+            io.write(string.format("Usage: %s ACTION\n\nACTION may be one of\n", arg[0]))
165
+            local sal = {}
166
+            for _, v in pairs(ACTIONS) do
167
+                table.insert(sal, v.desc)
168
+            end
169
+            table.sort(sal)
170
+            for _, v in ipairs(sal) do
171
+                io.write(string.format("\t%s\n", v))
172
+            end
173
+        end
174
+    },
175
+
176
+    l = {
177
+        desc = "(l)eech search",
178
+        nd = true,
179
+        fn = function(config, deck)
180
+            local leechqueue = {}
181
+            for i, cg in ipairs(deck) do
182
+                for j, pr in ipairs(cg) do
183
+                    if pr.fail > 4 and pr.ease < 2 and pr.delay >= 0 then
184
+                        table.insert(leechqueue, { i, j })
185
+                    end
186
+                end
187
+            end
188
+            table.sort(leechqueue, function(a, b)
189
+                return badness(deck[a[1]][a[2]]) > badness(deck[b[1]][b[2]])
190
+            end)
191
+            io.write(string.format("Detected %d leeches:\n", #leechqueue))
192
+            for _, ij in ipairs(leechqueue) do
193
+                io.write(string.format("%d:%d %s\n", ij[1], ij[2], deck[ij[1]][ij[2]]))
194
+            end
195
+        end,
196
+    },
197
+
198
+    r = {
199
+        desc = "(r)eview sesion",
200
+        nd = true,
201
+        fn = function(config, deck, deckfname)
202
+            if not isatty() then
203
+                io.write("Review should be done in the terminal\n")
204
+                return nil
205
+            end
206
+            -- collect list of index-pairs for prompts to be reviewed this session
207
+            local ctrt = review.list(deck)
208
+            io.write(string.format(
209
+                "%d prompts are ready for review.\nHow many to review in this session? ", #ctrt
210
+            ))
211
+            io.flush()
212
+            local climt = io.read("l")
213
+            local climn = tonumber(climt)
214
+            if not climn then
215
+                io.write("Input not a number -- aborting\n")
216
+                return nil
217
+            end
218
+            local lrc = 0
219
+            local editqueue = {}
220
+            local start = os.time()
221
+            -- review prompts in this session's set
222
+            for ind, ip in ipairs(ctrt) do
223
+                if ind > climn then
224
+                    break
225
+                end
226
+                local pr = deck[ip[1]][ip[2]]
227
+                -- remove braces for display
228
+                local pst = pr.text:gsub("%{([^{}]+)%}", "%1")
229
+                local st = os.time()
230
+                imgify(pst:gsub("%%A", "___"), deckfname:match(".*/") or "", config)
231
+                io.write("\n")
232
+                io.read("l")
233
+                imgify(pst:gsub("%%A", (pr.ans:gsub("%%", "%%%%"))), deckfname:match(".*/") or "", config)
234
+                io.write(string.format(
235
+                    "\nAgain / Hard (%s) / Good (%s) / Easy (%s) / Correction ",
236
+                    disptime(review.schedule(pr, st, 0.5, config)),
237
+                    disptime(review.schedule(pr, st, 1, config)),
238
+                    disptime(review.schedule(pr, st, 1.5, config))
239
+                ))
240
+                io.flush()
241
+                local resp = io.read("l")
242
+                local sc = succfromstr(resp)
243
+                if sc == -1 then
244
+                    table.insert(editqueue, ip[1])
245
+                else
246
+                    if sc == 0 then
247
+                        lrc = lrc + 1
248
+                    end
249
+                    review.update(deck[ip[1]][ip[2]], st, sc, config)
250
+                end
251
+                clear()
252
+            end
253
+            if #ctrt > 0 then
254
+                local endt = os.time()
255
+                io.write(string.format(
256
+                    "Reviewed %d prompts in %d seconds\n",
257
+                    math.min(#ctrt, climn),
258
+                    endt - start
259
+                ))
260
+                io.write(string.format(
261
+                    "(%d lapses, %f seconds per card)\n",
262
+                    lrc, (endt - start) / math.min(#ctrt, climn)
263
+                ))
264
+            end
265
+            if #editqueue > 0 then
266
+                io.write(string.format(
267
+                    "\n%d cards queued for editing -- press Enter to proceed",
268
+                    #editqueue
269
+                ))
270
+                io.flush()
271
+                io.read("l")
272
+                for ind, ci in ipairs(editqueue) do
273
+                    local pr = deck[ci][1]
274
+                    if pr then
275
+                        io.write(string.format(
276
+                            "Card #%d:\n%s\nCancel correction / Delete / Edit ",
277
+                            ci, pr:spt()
278
+                        ))
279
+                        io.flush()
280
+                        local resp = io.read("l"):sub(1, 1):lower()
281
+                        if resp == "d" then
282
+                            deck[ci][1].deleted = true
283
+                        elseif resp == "e" then
284
+                            io.write("Enter edited version:\n")
285
+                            local ev = io.read("l")
286
+                            local cardrec = Prompt:torec(deck[ci])
287
+                            cardrec.PromptText = { ev }
288
+                            deck[ci] = Prompt:fromrecs({ cardrec }, config)[1]
289
+                        -- assume all other inputs to mean Cancel
290
+                        else
291
+                        end
292
+                    else
293
+                        io.write(string.format("Card #%d is invalid!\n", ci))
294
+                    end
295
+                end
296
+            end
297
+            if climn > 0 then
298
+                return deck
299
+            end
300
+        end,
301
+    },
302
+
303
+    s = {
304
+        desc = "(s)tatistics",
305
+        nd = false,
306
+        fn = function()
307
+            io.write("Not yet implemented ...\n")
308
+        end
309
+    },
310
+
311
+    t = {
312
+        desc = "(t)oggle suspension of prompt",
313
+        nd = true,
314
+        fn = function(config, deck)
315
+            local ip
316
+            if arg[2] then
317
+                local a, b = arg[2]:match("(%d+):(%d+)")
318
+                if a and b then
319
+                    -- a and b are valid numbers because they came from a match
320
+                    ip = { tonumber(a), tonumber(b) }
321
+                end
322
+            end
323
+            if not ip then
324
+                io.write("Bad prompt index (must be cardnum:promptnum)\n")
325
+                return nil
326
+            end
327
+            local sp = deck[ip[1]][ip[2]]
328
+            local dir
329
+            if sp.delay < 0 then
330
+                dir = "Restoring"
331
+            else
332
+                dir = "Suspending"
333
+            end
334
+            io.write(string.format("%s %s\n", dir, sp))
335
+            sp.delay = -sp.delay
336
+            return deck
337
+        end,
338
+    },
339
+}
340
+
341
+return actions
... ...
@@ -5,135 +5,11 @@
5 5
 
6 6
 local SRC_DIR = arg[0]:match("^.*/") or ""
7 7
 
8
-local Prompt = dofile(SRC_DIR .. "prompt.lua")
9
-local review = dofile(SRC_DIR .. "review.lua")
10
-local loader = dofile(SRC_DIR .. "loader.lua")
11
-local recrw = dofile(SRC_DIR .. "recrw.lua")
12
-
13
---[[
14
-    gets the score (again/hard/good/easy/correction) from a string by first character
15
-    scores are numbers, higher is easier, -1 is correction
16
-]]
17
-local function succfromstr(s)
18
-    local f = s:sub(1, 1):lower()
19
-    if f == "0" or f == "a" then
20
-        return 0
21
-    elseif f == "1" or f == "h" then
22
-        return 0.5
23
-    elseif f == "2" or f == "g" then
24
-        return 1
25
-    elseif f == "3" or f == "e" then
26
-        return 1.5
27
-    elseif f == "-" or f == "c" then
28
-        return -1
29
-    end
30
-    return 1
31
-end
32
-
33
---[[
34
-    clears the terminal
35
-    implementation may be platform-specific
36
-]]
37
-local function clear()
38
-    -- ANSI
39
-    io.write("\x1b[H\x1b[J")
40
-end
41
-
42
---[[
43
-    display text with images, inserted with `IMG[path]`
44
-    implementation may be platform-specific
45
-    `basepath` points to the base directory of the images
46
-]]
47
-local function imgify(text, basepath, conf)
48
-    if conf:get("BoldContext") then
49
-        local csi, cei = text:find("^[^:]+:")
50
-        local cpt = text:sub(csi, cei)
51
-        io.write("\x1b[1m")
52
-        io.write(cpt)
53
-        io.write("\x1b[0m")
54
-        text = text:sub(cei + 1)
55
-    end
56
-    -- adapted from https://stackoverflow.com/a/1579673
57
-    local IMG_PATTERN = "IMG%[([^%]]+)%]"
58
-    local PATTERN = "(.-)" .. IMG_PATTERN
59
-    -- Linux/dwm
60
-    local DISPIMG = function(path, st)
61
-        if path and #path > 0 then
62
-            os.execute(string.format(
63
-                "/usr/local/bin/iv -t '%s' %s%s &",
64
-                st:gsub("'", "\""):gsub(IMG_PATTERN, ""), basepath, path
65
-            ))
66
-        end
67
-    end
68
-    local lastend = 1
69
-    local mstart, mend, prevtext, imgpath = text:find(PATTERN)
70
-    io.write(prevtext or "")
71
-    DISPIMG(imgpath, text)
72
-    while mstart do
73
-        lastend = mend + 1
74
-        mstart, mend, prevtext, imgpath = text:find(PATTERN, lastend)
75
-        io.write(prevtext or "")
76
-        DISPIMG(imgpath, text)
77
-    end
78
-    if lastend <= #text then
79
-        io.write(text:sub(lastend))
80
-    end
81
-end
82
-
83
---[[
84
-    rounds `x` to `n` places (default 0) after the radix point
85
-    in base `b` (default 10)
86
-]]
87
-local function round(x, n, b)
88
-    local b = b or 10
89
-    local n = n or 0
90
-    if n == 0 then
91
-        return math.tointeger(math.floor(x * b ^ n) / b ^ n)
92
-    else
93
-        return math.floor(x * b ^ n) / b ^ n
94
-    end
95
-end
96
-
97
---[[
98
-    return human-friendly string to present a duration (input in seconds),
99
-    e.g. "44:37", "4.8h", "11d", "8.3m", "17.4y"
100
-]]
101
-local function disptime(ts)
102
-    local YDAY = 365.24
103
-    local MDAY = YDAY / 12
104
-    local DSEC = 86400
105
-    local HSEC = 3600
106
-    if ts >= 3 * YDAY * DSEC then
107
-        return round(ts / (YDAY * DSEC), 1) .. "y"
108
-    elseif ts >= 4 * MDAY * DSEC then
109
-        return round(ts / (MDAY * DSEC), 1) .. "m"
110
-    elseif ts >= 10 * DSEC then
111
-        return round(ts / DSEC) .. "d"
112
-    elseif ts >= 1.5 * DSEC then
113
-        return round(ts / DSEC, 1) .. "d"
114
-    elseif ts >= 8 * HSEC then
115
-        return round(ts / HSEC) .. "h"
116
-    elseif ts >= HSEC then
117
-        return round(ts / HSEC, 1) .. "h"
118
-    else
119
-        return string.format("%d:%02d", round(ts / 60), round(ts % 60))
120
-    end
121
-end
122
-
123
---[[
124
-    check if standard input is a terminal
125
-]]
126
-local function isatty()
127
-    local succ = os.execute("tty >/dev/null")
128
-    return succ
129
-end
130
-
131
---[[
132
-    return number indicating how much of a leech the prompt is
133
-]]
134
-local function badness(pr)
135
-    return pr.fail / (pr.fail + pr.succ) * math.log(pr.fail)
136
-end
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")
137 13
 
138 14
 --[[
139 15
     check action based on first command-line argument;
... ...
@@ -142,13 +18,9 @@ end
142 18
 local action = nil
143 19
 if arg[1] then
144 20
     local fc = arg[1]:sub(1, 1)
145
-    if fc == "a" or fc == "h" or fc == "l" or fc == "r" or fc == "s" or fc == "t" then
146
-        action = fc
21
+    action = ACTIONS[fc] or ACTIONS.h
147 22
 else
148
-        action = "h"
149
-    end
150
-else
151
-    action = "h"
23
+    action = ACTIONS.h
152 24
 end
153 25
 
154 26
 -- load settings from config Recfile
... ...
@@ -205,208 +77,17 @@ deckfname = os.getenv("HOME") .. "/" .. deckfname
205 77
 
206 78
 math.randomseed(os.time())
207 79
 
208
---[[
209
-    do requested action
210
-]]
211
-if action == "a" then
212
-    local deck = loader.load(deckfname, config)
213
-    if deck then
214
-        if isatty() then
215
-            io.write("Reading prompts -- press Ctrl-D to stop\n")
216
-        else
217
-            io.write("Reading prompts ...\n")
218
-        end
219
-        local rt = io.read("a")
220
-        local cardctr = 0
221
-        local prctr = 0
222
-        for pt in rt:gmatch("(..-)\n\n") do
223
-            local nc = {}
224
-            -- these Prompts will just be converted to Rec, so we don't need to exclude answers from prompt-text
225
-            for _re in pt:gmatch("%{[^%}]+%}") do
226
-                table.insert(nc, Prompt:new(pt, "", os.time(), 30, 0, 0, os.time(), config:get("StartingEase") or 2.5))
227
-                prctr = prctr + 1
228
-            end
229
-            table.insert(deck, nc)
230
-            cardctr = cardctr + 1
231
-        end
232
-        io.write(string.format(
233
-            "Added %d cards (%d prompts)\n",
234
-            cardctr,
235
-            prctr
236
-        ))
237
-        if cardctr > 0 then
238
-            loader.save(deckfname, deck)
239
-        end
240
-    else
241
-        io.write("Failed to load deck\n")
242
-    end
243
-elseif action == "h" then
244
-    io.write(string.format(
245
-        "Usage: %s ACTION\n\nACTION may be one of\n\t(a)dd card\n\t(h)elp\n\t(l)eech search\n\t(r)eview session\n\t(s)tatistics\n\t(t)oggle suspension of prompt\n",
246
-        arg[0]
247
-    ))
248
-elseif action == "l" then
80
+-- do requested action
81
+if action.nd then
249 82
     local deck = loader.load(deckfname, config)
250 83
     if deck then
251
-        local leechqueue = {}
252
-        for i, cg in ipairs(deck) do
253
-            for j, pr in ipairs(cg) do
254
-                if pr.fail > 4 and pr.ease < 2 and pr.delay >= 0 then
255
-                    table.insert(leechqueue, { i, j })
256
-                end
257
-            end
258
-        end
259
-        table.sort(leechqueue, function(a, b)
260
-            return badness(deck[a[1]][a[2]]) > badness(deck[b[1]][b[2]])
261
-        end)
262
-        io.write(string.format("Detected %d leeches:\n", #leechqueue))
263
-        for _, ij in ipairs(leechqueue) do
264
-            io.write(string.format("%d:%d %s\n", ij[1], ij[2], deck[ij[1]][ij[2]]))
265
-        end
266
-    else
267
-        io.write("Failed to load deck\n")
268
-    end
269
-elseif action == "r" then
270
-    local deck
271
-    if isatty() then
272
-        deck = loader.load(deckfname, config)
273
-    else
274
-        io.write("Review should be done in the terminal\n")
275
-    end
84
+        deck = action.fn(config, deck, deckfname)
276 85
         if deck then
277
-        -- collect list of index-pairs for prompts to be reviewed this session
278
-        local ctrt = review.list(deck)
279
-        io.write(string.format(
280
-            "%d prompts are ready for review.\nHow many to review in this session? ", #ctrt
281
-        ))
282
-        io.flush()
283
-        local climt = io.read("l")
284
-        local climn = tonumber(climt)
285
-        if climn then
286
-            local lrc = 0
287
-            local editqueue = {}
288
-            local start = os.time()
289
-            -- review prompts in this session's set
290
-            for ind, ip in ipairs(ctrt) do
291
-                if ind > climn then
292
-                    break
293
-                end
294
-                local pr = deck[ip[1]][ip[2]]
295
-                -- remove braces for display
296
-                local pst = pr.text:gsub("%{([^{}]+)%}", "%1")
297
-                local st = os.time()
298
-                imgify(pst:gsub("%%A", "___"), deckfname:match(".*/") or "", config)
299
-                io.write("\n")
300
-                io.read("l")
301
-                imgify(pst:gsub("%%A", (pr.ans:gsub("%%", "%%%%"))), deckfname:match(".*/") or "", config)
302
-                io.write(string.format(
303
-                    "\nAgain / Hard (%s) / Good (%s) / Easy (%s) / Correction ",
304
-                    disptime(review.schedule(pr, st, 0.5, config)),
305
-                    disptime(review.schedule(pr, st, 1, config)),
306
-                    disptime(review.schedule(pr, st, 1.5, config))
307
-                ))
308
-                io.flush()
309
-                local resp = io.read("l")
310
-                local sc = succfromstr(resp)
311
-                if sc == -1 then
312
-                    table.insert(editqueue, ip[1])
313
-                else
314
-                    if sc == 0 then
315
-                        lrc = lrc + 1
316
-                    end
317
-                    review.update(deck[ip[1]][ip[2]], st, sc, config)
318
-                end
319
-                clear()
320
-            end
321
-            if #ctrt > 0 then
322
-                local endt = os.time()
323
-                io.write(string.format(
324
-                    "Reviewed %d prompts in %d seconds\n",
325
-                    math.min(#ctrt, climn),
326
-                    endt - start
327
-                ))
328
-                io.write(string.format(
329
-                    "(%d lapses, %f seconds per card)\n",
330
-                    lrc, (endt - start) / math.min(#ctrt, climn)
331
-                ))
332
-            end
333
-            if #editqueue > 0 then
334
-                io.write(string.format(
335
-                    "\n%d cards queued for editing -- press Enter to proceed",
336
-                    #editqueue
337
-                ))
338
-                io.flush()
339
-                io.read("l")
340
-                for ind, ci in ipairs(editqueue) do
341
-                    local pr = deck[ci][1]
342
-                    if pr then
343
-                        io.write(string.format(
344
-                            "Card #%d:\n%s\nCancel correction / Delete / Edit ",
345
-                            ci, pr:spt()
346
-                        ))
347
-                        io.flush()
348
-                        local resp = io.read("l"):sub(1, 1):lower()
349
-                        if resp == "d" then
350
-                            deck[ci][1].deleted = true
351
-                        elseif resp == "e" then
352
-                            io.write("Enter edited version:\n")
353
-                            local ev = io.read("l")
354
-                            local cardrec = Prompt:torec(deck[ci])
355
-                            cardrec.PromptText = { ev }
356
-                            deck[ci] = Prompt:fromrecs({ cardrec }, config)[1]
357
-                        -- assume all other inputs to mean Cancel
358
-                        else
359
-                        end
360
-                    else
361
-                        io.write(string.format("Card #%d is invalid!\n", ci))
362
-                    end
363
-                end
364
-            end
365
-            if climn > 0 then
366 86
             loader.save(deckfname, deck)
367 87
         end
368
-        else
369
-            io.write("Input not a number -- aborting\n")
370
-        end
371
-    else
372
-        io.write("Failed to load deck\n")
373
-    end
374
-elseif action == "s" then
375
-    -- TODO
376
-elseif action == "t" then
377
-    local ip
378
-    if arg[2] then
379
-        local a, b = arg[2]:match("(%d+):(%d+)")
380
-        if a and b then
381
-            -- a and b are valid numbers because they came from a match
382
-            ip = { tonumber(a), tonumber(b) }
383
-        end
384
-    end
385
-    if ip then
386
-        deck = loader.load(deckfname, config)
387
-        if deck then
388
-            local sp = deck[ip[1]][ip[2]]
389
-            local dir
390
-            if sp.delay < 0 then
391
-                dir = "Restoring"
392
-            else
393
-                dir = "Suspending"
394
-            end
395
-            io.write(string.format("%s %s\n", dir, sp))
396
-            sp.delay = -sp.delay
397
-            loader.save(deckfname, deck)
398 88
     else
399 89
         io.write("Failed to load deck\n")
400 90
     end
401 91
 else
402
-        io.write("Bad prompt index (must be cardnum:promptnum)\n")
92
+    action.fn(config)
403 93
 end
404
-else
405
-    io.write "unreachable"
406
-end
407
-
408 94