1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
|
;;; org-drill-config.el --- Org Drill Settings -*- lexical-binding: t; coding: utf-8; -*-
;; author: Craig Jennings <c@cjennings.net>
;;; Commentary:
;;
;; Layer: 4 (Optional).
;; Category: O/D/P.
;; Load shape: eager.
;; Eager reason: none; optional flashcard workflow, a command-loaded deferral
;; candidate for Phase 4.
;; Top-level side effects: defines a drill keymap, registers it under cj/custom-keymap.
;; Runtime requires: user-constants, keybindings, system-lib.
;; Direct test load: yes (requires keybindings explicitly).
;;
;; Notes: Org-Drill
;; `C-; D s' picks a flashcard file from `drill-dir' and starts a session;
;; `C-u C-; D s' lets you pick the directory first. `C-; D f' drills
;; whatever Org file is current, so any drill file anywhere works.
;; Capture templates:
;; - "d" for web/EPUB drill captures (uses %i for selected text, %:link for source)
;; - "f" for PDF drill captures (uses special PDF region extraction)
;; The javascript bookmarklet for capturing from web to org-drill:
;; javascript:location.href='org-protocol://capture?template=d&url=%27+encodeURIComponent(location.href)+%27&title=%27+encodeURIComponent(document.title)+%27&body=%27+encodeURIComponent(window.getSelection());void(0);
;; Create a bookmark, add "Drill Entry" to the name field and the above snippet to the URL field.
;;; Code:
(require 'user-constants) ;; `drill-dir'
(require 'keybindings) ;; provides `cj/custom-keymap'
(require 'system-lib) ;; completion table + file annotator
(declare-function org-drill "org-drill" (&optional scope drill-match resume-p))
(declare-function org-drill-resume "org-drill" ())
(declare-function org-capture "org-capture" (&optional goto keys))
(declare-function org-refile "org-refile" (&optional arg1 default-buffer rfloc msg))
(defvar org-refile-targets) ;; org-refile.el
;; ------------------------------ Drill Commands -------------------------------
(defun cj/--drill-files-in (dir)
"Return the drill Org file names directly inside DIR (no leading dots)."
(directory-files dir nil "^[^.].*\\.org$"))
(defun cj/--drill-files-or-error (dir)
"Return the drill Org files in DIR, or signal a clear `user-error'.
Errors when DIR is missing, unreadable, or has no drill files, so callers
fail with an actionable message instead of a low-level `directory-files'
error or an empty `completing-read'. This is the single entry point the
drill commands and the drill capture templates share."
(unless (and (file-directory-p dir) (file-readable-p dir))
(user-error "Drill directory missing or unreadable: %s" dir))
(let ((files (cj/--drill-files-in dir)))
(unless files
(user-error "No drill files (.org) found in %s" dir))
files))
(defun cj/--drill-pick-file (dir)
"Prompt for one of the drill Org files in DIR; return its absolute path."
(expand-file-name
(completing-read "Choose flashcard file: "
(cj/completion-table-annotated
'cj-drill-file
(cj/completion-file-annotator
(lambda (c) (expand-file-name c dir)))
(cj/--drill-files-or-error dir))
nil t)
dir))
(defun cj/--drill-pick-dir (other-dir)
"Return the directory to pick drill files from.
With OTHER-DIR non-nil, prompt for one; otherwise use `drill-dir'."
(if other-dir (read-directory-name "Drill files in: ") drill-dir))
(defun cj/drill-start (&optional other-dir)
"Pick a drill Org file and start an `org-drill' session.
With a prefix arg OTHER-DIR, prompt for the directory to choose from
instead of the default `drill-dir'."
(interactive "P")
(find-file (cj/--drill-pick-file (cj/--drill-pick-dir other-dir)))
(org-drill))
(defun cj/drill-this-file ()
"Start an `org-drill' session on the current Org buffer.
Use this to drill any drill file you have open, wherever it lives."
(interactive)
(unless (derived-mode-p 'org-mode)
(user-error "Not an Org buffer -- visit a `.org' file first"))
(org-drill))
(defun cj/drill-edit (&optional other-dir)
"Pick a drill Org file and open it for editing.
With a prefix arg OTHER-DIR, prompt for the directory instead of `drill-dir'."
(interactive "P")
(find-file (cj/--drill-pick-file (cj/--drill-pick-dir other-dir))))
(defun cj/drill-capture ()
"Quickly capture a drill question."
(interactive)
(org-capture nil "d"))
(defun cj/drill-refile ()
"Refile to a drill file."
(interactive)
(let ((org-refile-targets
`((nil :maxlevel . 1)
(,(mapcar (lambda (f) (expand-file-name f drill-dir))
(cj/--drill-files-or-error drill-dir))
:maxlevel . 1))))
(call-interactively 'org-refile)))
;; ------------------------------- Drill Keymap --------------------------------
(defvar-keymap cj/drill-map
:doc "Keymap for org-drill"
"s" #'cj/drill-start
"f" #'cj/drill-this-file
"e" #'cj/drill-edit
"c" #'cj/drill-capture
"r" #'cj/drill-refile
"R" #'org-drill-resume)
(cj/register-prefix-map "D" cj/drill-map)
(with-eval-after-load 'which-key
(which-key-add-key-based-replacements
"C-; D" "org-drill menu"
"C-; D s" "start drill (C-u: pick dir)"
"C-; D f" "drill current file"
"C-; D e" "edit drill file (C-u: pick dir)"
"C-; D c" "capture question"
"C-; D r" "refile to drill"
"C-; D R" "resume drill"))
;; --------------------------------- Org Drill ---------------------------------
(defconst cj/org-drill-dev-checkout (expand-file-name "org-drill" "~/code/")
"Local org-drill development checkout, preferred when it exists.")
(defun cj/--org-drill-source-keywords (&optional checkout)
"Return the use-package source keywords for org-drill.
With CHECKOUT (default `cj/org-drill-dev-checkout') an existing directory,
load from it via :load-path. Otherwise install from upstream via :vc, so
drill still loads on a machine without the dev checkout (bare :load-path +
:demand t would fail to load there)."
(let ((dir (or checkout cj/org-drill-dev-checkout)))
(if (file-directory-p dir)
(list :load-path dir)
(list :vc '(:url "git@cjennings.net:org-drill.git"
:branch "main"
:rev :newest)))))
;; `use-package' keywords must be literals at macro-expansion, so the
;; source keyword is spliced in through `eval' at load time (same idiom as
;; the computed flycheck checker path elsewhere in the config).
(eval
`(use-package org-drill
,@(cj/--org-drill-source-keywords)
:after (org org-capture)
:demand t
:commands (org-drill org-drill-resume)
:custom
(org-drill-leech-failure-threshold 50 "leech cards = 50 wrong answers")
(org-drill-leech-method 'warn "leech cards show warnings")
(org-drill-use-visible-cloze-face-p t "cloze text shows up in a different font")
(org-drill-hide-item-headings-p t "don't show heading text")
(org-drill-maximum-items-per-session 100 "drill sessions end after 100 cards")
(org-drill-maximum-duration 30 "each drill session can last up to 30 mins")
(org-drill-add-random-noise-to-intervals-p t "vary the days to repetition slightly")
(org-drill-text-size-during-session 24 "24-point font for comfortable reading")
(org-drill-use-variable-pitch t "variable-pitch font for readability")
(org-drill-hide-modeline-during-session t "hide the modeline for a cleaner display")))
(provide 'org-drill-config)
;;; org-drill-config.el ends here.
|