├── .gitignore ├── targets ├── compile.el └── install-deps.el ├── elpa.el ├── Makefile ├── pam-test.el ├── doc └── sets │ ├── capitals │ └── capitals.org │ └── clojure │ └── clojure.org ├── README.org └── pamparam.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.pam -------------------------------------------------------------------------------- /targets/compile.el: -------------------------------------------------------------------------------- 1 | (setq files '("pamparam.el")) 2 | (setq byte-compile--use-old-handlers nil) 3 | (mapc #'byte-compile-file files) 4 | -------------------------------------------------------------------------------- /elpa.el: -------------------------------------------------------------------------------- 1 | (setq package-user-dir 2 | (expand-file-name 3 | (format "~/.elpa/%s/elpa" 4 | (concat emacs-version (when (getenv "MELPA_STABLE") "-stable"))))) 5 | (package-initialize) 6 | (add-to-list 'load-path default-directory) 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | emacs ?= emacs 2 | BEMACS = $(emacs) -batch -l elpa.el 3 | 4 | all: test 5 | 6 | test: 7 | $(BEMACS) -batch -l targets/compile.el -l pamparam.el -l pam-test.el -f ert-run-tests-batch-and-exit 8 | 9 | compile: 10 | $(BEMACS) -batch -l targets/compile.el 11 | 12 | plain: 13 | $(emacs) --version 14 | $(emacs) -Q -l elpa.el -l pamparam.el doc/sets/capitals/capitals.org 15 | 16 | update: 17 | $(emacs) -batch -l targets/install-deps.el 18 | 19 | clean: 20 | rm -f *.elc 21 | 22 | .PHONY: all compile clean test 23 | -------------------------------------------------------------------------------- /targets/install-deps.el: -------------------------------------------------------------------------------- 1 | (setq melpa-stable (getenv "MELPA_STABLE")) 2 | (setq package-user-dir 3 | (expand-file-name 4 | (format "~/.elpa/%s/elpa" 5 | (concat emacs-version (when melpa-stable "-stable"))))) 6 | (message "installing in %s ...\n" package-user-dir) 7 | (package-initialize) 8 | (setq package-archives 9 | (list (if melpa-stable 10 | '("melpa-stable" . "https://stable.melpa.org/packages/") 11 | '("melpa" . "https://melpa.org/packages/")) 12 | ;; '("gnu" . "http://elpa.gnu.org/packages/") 13 | )) 14 | (package-refresh-contents) 15 | 16 | (defconst pamparam-dev-packages 17 | '(lispy 18 | worf)) 19 | 20 | (dolist (package pamparam-dev-packages) 21 | (unless (package-installed-p package) 22 | (ignore-errors 23 | (package-install package)))) 24 | 25 | (save-window-excursion 26 | (package-list-packages t) 27 | (condition-case nil 28 | (progn 29 | (package-menu-mark-upgrades) 30 | (package-menu-execute t)) 31 | (error 32 | (message "All packages up to date")))) 33 | -------------------------------------------------------------------------------- /pam-test.el: -------------------------------------------------------------------------------- 1 | (require 'pamparam) 2 | (require 'ert) 3 | 4 | (ert-deftest pamparam-sm2 () 5 | ;; first rep 6 | (should (equal (pamparam-sm2 '(2.5 1) 5) 7 | '(2.6 6 1))) 8 | (should (equal (pamparam-sm2 '(2.5 1) 4) 9 | '(2.5 6 1))) 10 | (should (equal (pamparam-sm2 '(2.5 1) 3) 11 | '(2.3600000000000003 6 1))) 12 | (should (equal (pamparam-sm2 '(2.5 1) 2) 13 | '(2.18 1 1))) 14 | (should (equal (pamparam-sm2 '(2.5 1) 1) 15 | '(1.96 1 1))) 16 | (should (equal (pamparam-sm2 '(2.5 1) 0) 17 | '(1.7000000000000002 1 1))) 18 | ;; second rep 19 | (should (equal (pamparam-sm2 (pamparam-sm2 '(2.5 1) 5) 5) 20 | '(2.7 16 6 1))) 21 | ;; third rep 22 | (should (equal (pamparam-sm2 (pamparam-sm2 (pamparam-sm2 '(2.5 1) 5) 5) 5) 23 | '(2.8000000000000003 45 16 6 1))) 24 | ;; fourth rep 25 | (should (equal (pamparam-sm2 (pamparam-sm2 (pamparam-sm2 (pamparam-sm2 '(2.5 1) 5) 5) 5) 5) 26 | '(2.9000000000000004 131 45 16 6 1)))) 27 | 28 | (ert-deftest pamparam-equal () 29 | (should (pamparam-equal "het plezier" 30 | "het genoegen\nhet plezier"))) 31 | -------------------------------------------------------------------------------- /doc/sets/capitals/capitals.org: -------------------------------------------------------------------------------- 1 | * European countries/capitals by population :cards: 2 | https://en.wikipedia.org/wiki/List_of_European_countries_by_population 3 | ** Germany 4 | Berlin 5 | ** Berlin is the capital of {Germany}. 6 | ** France 7 | Paris 8 | ** United Kingdom 9 | London 10 | ** Italy 11 | Rome 12 | ** Spain 13 | Madrid 14 | ** Ukraine 15 | Kiev 16 | ** Poland 17 | Warsaw 18 | ** Romania 19 | Bucharest 20 | ** Netherlands 21 | Amsterdam 22 | ** Belgium 23 | Brussels 24 | ** Greece 25 | Athens 26 | ** Czech Republic 27 | Prague 28 | ** Portugal 29 | Lisbon 30 | ** Sweden 31 | Stockholm 32 | ** Hungary 33 | Budapest 34 | ** Belarus 35 | Minsk 36 | ** Austria 37 | Vienna 38 | ** Switzerland 39 | Bern 40 | ** Bulgaria 41 | Sofia 42 | ** Serbia 43 | Belgrade 44 | ** Denmark 45 | Copenhagen 46 | ** Finland 47 | Helsinki 48 | ** Slovakia 49 | Bratislava 50 | ** Norway 51 | Oslo 52 | ** Ireland 53 | Dublin 54 | ** Croatia 55 | Zagreb 56 | ** Bosnia and Herzegovina 57 | Sarajevo 58 | ** Moldova 59 | Kishinev 60 | ** Lithuania 61 | Vilnius 62 | ** Albania 63 | Tirana 64 | ** Macedonia 65 | Skopje 66 | ** Slovenia 67 | Ljubljana 68 | ** Latvia 69 | Riga 70 | ** Kosovo 71 | Pristina 72 | ** Estonia 73 | Tallinn 74 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Intro 2 | Pamparam is a new spaced repetition (SR) memory cards implementation 3 | for Emacs. 4 | 5 | Spaced repetition is an algorithm for learning and repeating 6 | cards. Cards for which you get high scores get scheduled for 7 | repetition much further into the future than those with low 8 | scores. This means you spend less time repeating things that are easy 9 | for you to remember and more time on things which are hard for you to 10 | remember. 11 | 12 | * Quickstart 13 | #+begin_src sh 14 | git clone https://github.com/abo-abo/pamparam 15 | #+end_src 16 | 17 | 1. Open =pamparam/doc/sets/capitals/capitals.org= and call =pamparam-sync=. 18 | 19 | You get a message like =35 new cards, 0 updated, 35 total= and the 20 | file =pampile.org= is opened. 21 | 22 | 2. Call =pamparam-drill=. 23 | 24 | You get two buffers. One buffer, named e.g. =pam-2017-04-23.org= is 25 | your schedule file. It holds 10 cards that you should do today. The 26 | other buffer is a card asking you the capital of e.g. =Estonia=. 27 | 28 | 3. Enter =Tallinn.= (the dot at the end starts the card validation) 29 | 30 | Since the answer is correct, you get a score of 5 (best one). 31 | Alternatively, if you reveal the answer with ~S-TAB~ and enter 32 | =Tallinn.=, you will get only a score of 3. 33 | Finally, if you enter a wrong answer, you get a score of 0. 34 | 35 | 4. Theoretically, you can stop here with 1/10 cards done. Resume at 36 | any later stage with =pamparam-drill=. Or press ~n~ (calls =pamparam-drill=) to 37 | select the next card. Any cards scheduled today but not finished 38 | will carry over to tomorrow. 39 | 40 | Assuming you finished all 10 cards, what's next? 41 | 42 | - (optional) call =pamparam-commit= to commit your progress. 43 | 44 | It's not really required, but it's neat to track everything, and 45 | it's a nice restore point if you ever mess up your repo. 46 | 47 | - if you feel like doing more cards, call =pamparam-pull=. 48 | 49 | I don't recommend doing more than 50 cards in a day, because you'll 50 | be spending a lot of time on repetition in the future. 51 | 52 | - come back tomorrow for a new set of cards to repeat and learn. 53 | 54 | * How this works 55 | ** Terminology 56 | After =pamparam-sync= and =pamparam-drill=, you get the following tree: 57 | #+BEGIN_EXAMPLE 58 | . 59 | ├── capitals.org 60 | └── capitals.pam 61 | ├── cards 62 | │   ├── 00 63 | │   │   └── 00247297c394dd443dc97067830c35f4-a606fe014370d8c520a07f30df46ef10.org 64 | │   ├── 01 65 | │   │   └── 01a151debf2bfee8906f43f4342eb10b-654cd76590cebe0ba37e8d4cce8a96ee.org 66 | │ ... (more cards) ... 67 | ├── pam-2017-04-23.org 68 | ├── pam-2017-04-24.org 69 | └── pampile.org 70 | #+END_EXAMPLE 71 | 72 | With this example, let's get the terminology out of the way: 73 | 74 | - Master file :: e.g. =capitals.org=, 75 | 76 | This is an Org file that has a heading tagged with =:cards:= that 77 | contains the definition of all your cards. 78 | 79 | - Card file :: e.g. =capitals.pam/cards/00/00247297c394dd443dc97067830c35f4-a606fe014370d8c520a07f30df46ef10.org= 80 | 81 | This is an Org file that encapsulates a single card. Besides the 82 | front (the question) and the body (the answer), it also contains 83 | the metadata. The metadata contains: dates and scores of previous 84 | learning sessions with this card and the card's accumulated ease 85 | factor. 86 | 87 | - Schedule file :: e.g. =capitals.pam/pam-2017-04-23.org= 88 | 89 | This is an Org file that stores the schedule for a particular day 90 | as =TODO= items with links to card files. Each item's state starts 91 | at =TODO= and ends at =DONE= which you finish the corresponding 92 | card. An intermediate state =REVIEW= happens if you get a score 93 | lower than 5 for the first time. When doing the card for the 94 | second time, =REVIEW= becomes =DONE= with a score of 3 or more. 95 | 96 | - Pile file :: =capitals.pam/pampile.org= 97 | 98 | This is your cards pile. Very similar to a schedule file, it 99 | holds all cards that are not yet scheduled. There are two ways 100 | to interact with your pile: add to it by calling =pamparam-sync= from 101 | the master file or remove from it by calling =pamparam-pull=. 102 | 103 | - Repo :: =capitals.pam= 104 | 105 | A Git repository to store all your cards files, schedules, and 106 | the pile. Note that the master file isn't here. This is 107 | intentional, since any of your existing Org can become a master 108 | file simply by adding a =:cards:= tag to one of the outlines. See 109 | =pamparam-alist= for a way to connect a master file to a repo that's not 110 | in the same directory. 111 | 112 | ** Master file example 113 | A master file is an Org-mode file with things you want to learn. 114 | 115 | One simple example is provided in [[file:doc/sets/capitals/capitals.org][capitals.org]]. 116 | 117 | Here's another, slightly more elaborate, example, which I use for 118 | learning Dutch: 119 | #+begin_src org 120 | ,* Cards :cards: 121 | ,** comic story 122 | het stripverhal 123 | ,*** The Adventures of Tintin is a world famous Belgian comic strip. 124 | De Avonturen van Kuifje is een wereldbekend Belgisch stripverhaal. 125 | 126 | ,** singer 127 | de zanger 128 | de zangeres 129 | ,*** The singer is only known in Belgium. 130 | De zangeres is alleen in België bekend. 131 | #+end_src 132 | 133 | The format of the master file is fairly straightforward: 134 | 135 | 1. There needs to be one or more card sources - first level outlines 136 | marked with the =:cards:= tag. A separate card file will be created 137 | for every second or third level child of each card source. In the 138 | example above, four cards will be created. 139 | 140 | 2. For each second or third level outline, the heading name is the 141 | question and the heading body is the answer. I usually put e.g. a 142 | noun or a verb into the second level, and a more elaborate example 143 | of using that noun or a verb into the child third level. I also 144 | like to organize the words by generation rules and thematically, so 145 | that e.g. =honest= will be close to =modest= and very close to =honesty=. 146 | 147 | The only hard and enforced requirement is that all heading names 148 | must be unique. 149 | 3. If a word has many correct possibilities (like =de zanger= and =de 150 | zangeres= both mean =singer=), I put each on its own line. This allows 151 | to enter either synonym during validation. 152 | 153 | The master file is a great summary of the info that you have 154 | available. It's easy to search and organize. 155 | 156 | One more option is to put all cards as level one headings. In that case, instead of 157 | tagging them with =:cards:=, you can add to the top of the file: 158 | 159 | #+begin_example 160 | #+PROPERTY: pamparam t 161 | #+end_example 162 | 163 | ** Card file example 164 | A card file looks like this: 165 | #+begin_src org 166 | ,* m 167 | ,#+STARTUP: content 168 | ,** scores 169 | | <2017-04-23> | 3 | | 170 | ,** stats 171 | (setq ease-factor 2.360000) 172 | (setq intervals '(1)) 173 | ,* Slovenia 174 | Ljubljana 175 | #+end_src 176 | 177 | The first heading holds all the metadata, like: 178 | 179 | - all times and scores when you did a card 180 | - your wrong answers, if any 181 | - an estimate of the card's ease 182 | 183 | The second heading's name is the card's front, the question. The 184 | second heading's body is the anwer, it starts out hidden. 185 | 186 | ** Card scoring 187 | | score | meaning | 188 | |-------+---------------------------------------------------------------------------| 189 | | 5 | perfect answer, body hidden | 190 | | 4 | wrong answer, pamparam-card-redo was called, followed by a perfect answer | 191 | | 3 | perfect answer, body revealed | 192 | | 0 | wrong answer | 193 | 194 | On the first try, you can get either a 5 or a 3 or a 0. Unless you get 195 | a 5, you have to =REVIEW= the card today. 196 | 197 | You can use =pamparam-card-redo= if you make a mechanical typo and get a 0, 198 | even though you knew the card. If you manage to correct the typo, you 199 | get a 4. You can make use of ~C-y~ to yank your previous answer. 200 | 201 | In the =REVIEW= stage, entering the answer with body revealed is 202 | acceptable to move it to =DONE=. Still, you might want to try to keep 203 | the body hidden. 204 | 205 | ** Commands and key bindings 206 | Certain commands are applicable only in certain types of files. There 207 | are 3 types of files, all of which use =org-mode=: master, card and 208 | schedule. 209 | 210 | *** Card file 211 | Global bindings: 212 | | . | pamparam-card-validate-maybe | 213 | 214 | Local bindings (only active if your point is at a heading start), in 215 | order of importance: 216 | | n | pamparam-drill | 217 | | q | bury-buffer | 218 | | R | pamparam-card-redo | 219 | | D | pamparam-card-delete | 220 | 221 | *** Master file 222 | | pamparam-sync | 223 | 224 | *** Anywhere in the repo 225 | | pamparam-drill | 226 | | pamparam-pull | 227 | | pamparam-commit | 228 | 229 | * Customization 230 | While it's possible to have multiple repos, currently I have only a 231 | single one. In my case, it's not convenient to keep the repo =dutch.pam= 232 | in the same directory as the master file =dutch.org=. So I use this 233 | setting: 234 | 235 | #+begin_src elisp 236 | (setq pamparam-alist 237 | '(("/home/oleh/Dropbox/org/wiki/dutch.org" 238 | . "/home/oleh/Dropbox/source/site-lisp/git/dutch.pam"))) 239 | #+end_src 240 | 241 | =pamparam-drill= doesn't know where your repos are located. It can only 242 | determine if the current buffer's file belongs to a repo or not. In 243 | case it does, the current repo is used. Otherwise, the default repo is 244 | used which is pointed to by =pamparam-path=. 245 | 246 | By default, =pamparam-path= points to the repo of the provided example master 247 | file. Here's my custom setting: 248 | #+begin_src elisp 249 | (setq pamparam-path "/home/oleh/Dropbox/source/site-lisp/git/dutch.pam") 250 | #+end_src 251 | 252 | Finally, you can have all key bindings in one place with a hydra: 253 | #+begin_src elisp 254 | (global-set-key (kbd "C-c m") 'hydra-pam/body) 255 | #+end_src 256 | -------------------------------------------------------------------------------- /doc/sets/clojure/clojure.org: -------------------------------------------------------------------------------- 1 | * Tasks 2 | * Clojure functions :cards: 3 | https://clojuredocs.org/clojure.core/ 4 | ** Number of items in the collection 5 | #+begin_src clojure 6 | (count nil) 7 | ;; => 0 8 | 9 | (count []) 10 | ;; => 0 11 | 12 | (count [1 2 3]) 13 | ;; => 3 14 | 15 | (count {:one 1 :two 2}) 16 | ;; => 2 17 | 18 | (count [1 \a "string" [1 2] {:foo :bar}]) 19 | ;; => 5 20 | 21 | (count "string") 22 | ;; => 6 23 | #+end_src 24 | *** a 25 | count 26 | ** Create a new vector from collection 27 | #+begin_src clojure 28 | (vec '(1 2 3)) 29 | ;; => [1 2 3] 30 | 31 | (vec [1 2 3]) 32 | ;; => [1 2 3] 33 | 34 | (vec #{1 2 3}) 35 | ;; => [1 3 2] 36 | 37 | (vec {:a 1 :b 2 :c 3}) 38 | ;; => [[:a 1] [:b 2] [:c 3]] 39 | 40 | (vec '()) 41 | ;; => [] 42 | #+end_src 43 | *** a 44 | vec 45 | ** Repeat an object several times 46 | #+begin_src clojure 47 | (repeat 5 \A) 48 | ;; => (\A \A \A \A \A) 49 | #+end_src 50 | *** a 51 | repeat 52 | ** Return a new set of the same type without keys 53 | #+begin_src clojure 54 | (disj #{1 2 3} 2) 55 | ;; => #{1 3} 56 | 57 | (disj #{1 2 3} 1 3) 58 | ;; => #{2} 59 | #+end_src 60 | *** a 61 | disj 62 | ** Return the value in a nested map, using a sequence of keys 63 | #+begin_src clojure 64 | ;;* Reach into nested maps 65 | (def m {:username "sally" 66 | :profile {:name "Sally Clojurian" 67 | :address {:city "Austin" :state "TX"}}}) 68 | 69 | ;;** : 70 | (get-in m [:profile :name]) 71 | ;; => "Sally Clojurian" 72 | 73 | ;;** : 74 | (get-in m [:profile :address :city]) 75 | ;; => "Austin" 76 | 77 | ;;** : 78 | (get-in m [:profile :address :zip-code]) 79 | ;; => nil 80 | 81 | ;;** : 82 | (get-in m [:profile :address :zip-code] "no zip code!") 83 | ;; => "no zip code!" 84 | 85 | ;;* Vectors are also associative 86 | (def v [[1 2 3] 87 | [4 5 6] 88 | [7 8 9]]) 89 | 90 | ;;** : 91 | (get-in v [0 2]) 92 | ;; => 3 93 | 94 | ;;** : 95 | (get-in v [2 1]) 96 | ;; => 8 97 | 98 | ;;* Mix associative types: 99 | (def mv {:username "jimmy" 100 | :pets [{:name "Rex" 101 | :type :dog} 102 | {:name "Sniffles" 103 | :type :hamster}]}) 104 | (get-in mv [:pets 1 :type]) 105 | ;; => :hamster 106 | #+end_src 107 | *** a 108 | get-in 109 | ** String to symbol 110 | #+begin_src clojure 111 | (eval (list (symbol "+") 1 2)) 112 | ;; => 3 113 | #+end_src 114 | *** a 115 | symbol 116 | ** Apply map then concat 117 | #+begin_src clojure 118 | ;;* : 119 | (defn f1 [n] 120 | [(- n 1) n (+ n 1)]) 121 | (f1 1) 122 | ;; => 123 | ;; [0 1 2] 124 | 125 | ;;* : 126 | (apply concat (map f1 [1 4 8])) 127 | ;; => 128 | ;; (0 1 2 3 4 5 7 8 9) 129 | 130 | ;;* : 131 | (mapcat f1 [1 4 8]) 132 | ;; => 133 | ;; (0 1 2 3 4 5 7 8 9) 134 | #+end_src 135 | *** a 136 | mapcat 137 | ** cddr 138 | #+begin_src clojure 139 | (nnext '(1 2 3)) 140 | ;; => (3) 141 | #+end_src 142 | *** a 143 | nnext 144 | ** make a forward declaration 145 | *** a 146 | declare 147 | ** avoid symbol resolution in a macro 148 | #+begin_src clojure 149 | ;;* : 150 | (defmacro awhen1 [expr & body] 151 | `(let [~'it ~expr] 152 | (if ~'it 153 | (do ~@body)))) 154 | 155 | (macroexpand-1 '(awhen1 [1 2 3] (it 2))) 156 | ;; => (clojure.core/let [it [1 2 3]] (if it (do (it 2)))) 157 | 158 | ;;* : 159 | (defmacro awhen2 [expr & body] 160 | `(let [it ~expr] 161 | (if it 162 | (do ~@body)))) 163 | 164 | (macroexpand-1 '(awhen2 [1 2 3] (it 2))) 165 | ;; => (clojure.core/let [user/it [1 2 3]] (if user/it (do (it 2)))) 166 | #+end_src 167 | *** a 168 | ~' 169 | ** Take fn1,...,fnN => #(vector (apply fn1 %&) ... (apply fnN %&)) 170 | #+begin_src clojure 171 | ;;* : 172 | (let [coll {:a 1 :b 2 :c 3}] 173 | (= [(:a coll) (:b coll)] 174 | ((juxt :a :b) coll) 175 | (#(vector (apply :a %&) (apply :b %&)) coll) 176 | [1 2])) 177 | ;; => true 178 | 179 | ;;* : 180 | (= ((juxt take drop) 3 (range 9)) 181 | (->> [take drop] 182 | (map #(% 3 (range 9))))) 183 | ;; => true 184 | #+end_src 185 | 186 | *** a 187 | juxt 188 | ** Return a lazy sequence of initial items matching a predicate in a collection 189 | #+begin_src clojure 190 | ;;* : 191 | (take-while neg? [-1 -2 -3 4 5 6 -7 -8 9]) 192 | ;; => (-1 -2 -3) 193 | 194 | ;;* : 195 | (filter neg? [-1 -2 -3 4 5 6 -7 -8 9]) 196 | ;; => (-1 -2 -3 -7 -8) 197 | #+end_src 198 | *** a 199 | take-while 200 | ** Return a lazy sequence of the first =N= items in coll 201 | #+begin_src clojure 202 | ;;* Example 203 | 204 | ;;** : 205 | (take 3 '(1 2 3 4 5 6)) 206 | ;; => (1 2 3) 207 | 208 | ;;** : 209 | (take 3 [1 2]) 210 | ;; => (1 2) 211 | 212 | ;;** : 213 | (take 0 [1]) 214 | ;; => () 215 | #+end_src 216 | *** a 217 | take 218 | ** Return a lazy sequence removing all initial items that don't match a predicate 219 | #+begin_src clojure 220 | ;;* : 221 | (drop-while neg? [-1 -2 -3 4 5 6 -7 -8 9]) 222 | ;; => (4 5 6 -7 -8 9) 223 | 224 | ;;* : 225 | (filter (comp not neg?) [-1 -2 -3 4 5 6 -7 -8 9]) 226 | ;; => (4 5 6 9) 227 | #+end_src 228 | *** a 229 | drop-while 230 | ** Set =*ns*= to the namespace named by the symbol, creating it if needed 231 | #+begin_src clojure 232 | ;;* Examples 233 | ;;** create and switch to a namespace: 234 | (in-ns 'first-namespace) 235 | ;; => first-namespace 236 | 237 | ;;** create a var in the current namespace: 238 | (def my-var "value") 239 | ;; => #'first-namespace/my-var 240 | 241 | ;;** : 242 | (in-ns 'second-namespace) 243 | ;; => second-namespace 244 | 245 | ;;** refer to a symbol in another namespace: 246 | first-namespace/my-var 247 | ;; => "value" 248 | 249 | ;;** clojure.core is not imported: 250 | (lispy-clojure/ok 251 | (list)) 252 | ;; => "java.lang.RuntimeException: Unable to resolve symbol: list in this context, compiling:(/tmp/form-init3619942662088095204.clj:2:2)" 253 | 254 | ;;** back to default ns: 255 | (in-ns 'user) 256 | ;; => user 257 | #+end_src 258 | *** a 259 | in-ns 260 | ** Import all public vars from a namespace into the current namespace, subject to filters 261 | #+begin_src clojure 262 | ;;* Example 1: import everything (lame) 263 | ;;** : 264 | (ns one) 265 | ;; => one 266 | 267 | ;;** : 268 | (def t1 "t1") 269 | ;; => #'one/t1 270 | 271 | ;;** : 272 | (ns two (:refer one)) 273 | ;; => two 274 | 275 | ;;** : 276 | t1 277 | ;; => "t1" 278 | 279 | ;;* Example 2: import specific functions 280 | ;;** : 281 | (in-ns 'temp) 282 | ;; => temp 283 | 284 | ;;** : 285 | (clojure.core/refer 'clojure.string :only '[capitalize trim]) 286 | ;; => nil 287 | 288 | ;;** : 289 | (capitalize (trim " hOnduRAS ")) 290 | ;; => "Honduras" 291 | 292 | ;;* Example 3: import specific functions via aliases 293 | 294 | ;;** : 295 | (clojure.core/refer 'clojure.string :rename '{capitalize cap, trim trm}) 296 | ;; => nil 297 | 298 | ;;** : 299 | (cap (trm " hOnduRAS ")) 300 | ;; => "Honduras" 301 | 302 | ;;** : 303 | (ns user) 304 | ;; => user 305 | #+end_src 306 | *** a 307 | refer 308 | ** Same as (refer 'clojure.core ) 309 | =clojure.core= is included in full by =ns=, i.e.: 310 | #+begin_src python 311 | from clojure.core import * 312 | #+end_src 313 | 314 | Exclude =defstruct= from being imported: 315 | #+begin_src clojure 316 | (ns joy.ns-ex 317 | (:refer-clojure :exclude [defstruct])) 318 | ;; Equivalent 319 | (ns joy.ns-ex 320 | (:refer clojure.core :exclude [defstruct])) 321 | #+end_src 322 | 323 | Python: 324 | #+begin_src python 325 | from clojure.core import * 326 | del defstruct 327 | #+end_src 328 | *** a 329 | refer-clojure 330 | ** Load libs, skipping any that are already loaded 331 | Load =clojure.zip= and alias it to =z=: 332 | #+begin_src clojure 333 | (require '(clojure zip [set :as s])) 334 | #+end_src 335 | Python: 336 | #+begin_src python 337 | from sys import path as p 338 | #+end_src 339 | 340 | Prefix Lists: 341 | It's common for Clojure code to depend on several libs whose names 342 | have the same prefix. When specifying libs, prefix lists can be used 343 | to reduce repetition. A prefix list contains the shared prefix 344 | followed by libspecs with the shared prefix removed from the lib 345 | names. After removing the prefix, the names that remain must not 346 | contain any periods. 347 | 348 | Recognized options: 349 | - =:as= takes a symbol as its argument and makes that symbol an alias to 350 | the lib's namespace in the current namespace. 351 | - =:refer= takes a list of symbols to refer from the namespace or the 352 | =:all= keyword to bring in all public vars. 353 | 354 | Flags: 355 | - =:reload= forces loading of all the identified libs even if they are 356 | already loaded 357 | - =:reload-all= implies =:reload= and also forces loading of all libs that 358 | the identified libs directly or indirectly load via require or use 359 | - =:verbose= triggers printing information about each load, alias, and 360 | refer 361 | 362 | *** a 363 | require 364 | ** Like =require=, but also refer to each lib's namespace using =clojure.core/refer= 365 | Use everyting in =clojure.set= and =clojure.xml= without namespace 366 | qualification. This is never good style to do. 367 | #+begin_src clojure 368 | (ns joy.ns-ex 369 | (:use [clojure set xml])) 370 | #+end_src 371 | 372 | Python: 373 | #+begin_src python 374 | from clojure.set import * 375 | from clojure.xml import * 376 | #+end_src 377 | 378 | Using =:only=: 379 | Use =clojure.test.are= and =clojure.test.is= without ns qualifier: 380 | #+begin_src clojure 381 | (ns joy.ns-ex 382 | (:use [clojure.test :only (are is)])) 383 | #+end_src 384 | Python: 385 | #+begin_src python 386 | from clojure.test import are, is 387 | #+end_src 388 | *** a 389 | use 390 | ** Import a Java class 391 | Import =java.util.Date= and =java.io.File=: 392 | #+begin_src clojure 393 | ;;* Example 394 | ;;** : 395 | (import java.util.Date) 396 | ;; => java.util.Date 397 | 398 | ;;** : 399 | (str (Date.)) 400 | ;; => "Thu Dec 21 14:21:42 CET 2017" 401 | #+end_src 402 | 403 | Jython: 404 | #+begin_src python 405 | from java.util import Date 406 | from java.io import File 407 | #+end_src 408 | *** a 409 | import 410 | ** Load Clojure code from resources in classpath 411 | *** a 412 | load 413 | ** When compiling, generate compiled bytecode for a class 414 | *** a 415 | genclass 416 | ** Return a random permutation of a collection 417 | *** a 418 | shuffle 419 | ** Return the first logical true value of (pred x) in coll 420 | #+begin_src clojure 421 | ;;* Example 422 | 423 | ;;** : 424 | (some even? '(1 2 3 4)) 425 | ;; => true 426 | 427 | ;;** : 428 | (some even? '(1 3 5 7)) 429 | ;; => nil 430 | #+end_src 431 | *** a 432 | some 433 | ** Take a function with no args and return an infinite lazy sequence of calls to it 434 | #+begin_src clojure 435 | ;;* Example 436 | 437 | ;;** : 438 | (take 5 (repeatedly #(rand-int 11))) 439 | ;; => (10 3 6 5 7) 440 | 441 | ;;** : 442 | (repeatedly 5 #(rand-int 11)) 443 | ;; => (4 0 10 3 5) 444 | #+end_src 445 | *** a 446 | repeatedly 447 | ** Set in-transaction value of a ref 448 | #+begin_src clojure 449 | ;;* Example 450 | 451 | ;;** : 452 | (ns user) 453 | ;; => user 454 | 455 | ;;** : 456 | (def names (ref [])) 457 | 458 | (defn add-name [name] 459 | (dosync 460 | (alter names conj name))) 461 | 462 | (add-name "zack") 463 | ;; => ["zack"] 464 | 465 | ;;** : 466 | (add-name "shelley") 467 | ;; => ["zack" "shelley"] 468 | 469 | ;;** : 470 | names 471 | ;; => #ref[{:status :ready, :val ["zack" "shelley"]} 0x581e2fad] 472 | 473 | ;;** : 474 | @names 475 | ;; => ["zack" "shelley"] 476 | 477 | ;;** : 478 | (deref names) 479 | ;; => ["zack" "shelley"] 480 | #+end_src 481 | *** a 482 | alter 483 | ** Run expressions in a transaction 484 | Starts a transaction if none is already running on this thread. 485 | 486 | Any uncaught exception will abort the transaction. 487 | 488 | The exprs may be run more than once, but any effects on refs will be 489 | atomic. 490 | 491 | #+begin_src clojure 492 | ;;* Example 493 | 494 | ;;** create two bank accounts: 495 | (def acc1 (ref 100)) 496 | (def acc2 (ref 200)) 497 | [@acc1 @acc2] 498 | ;; => [100 200] 499 | 500 | ;;** either both accounts will be changed or none: 501 | (defn transfer-money [a1 a2 amount] 502 | (dosync 503 | (alter a1 - amount) 504 | (alter a2 + amount) 505 | amount)) 506 | (transfer-money acc1 acc2 20) 507 | ;; => 20 508 | 509 | ;;** : 510 | [@acc1 @acc2] 511 | ;; => [80 220] 512 | #+end_src 513 | 514 | *** a 515 | dosync 516 | ** Set in-transaction-value of ref to (apply fun in-transaction-value-of-ref args). Commutative 517 | #+begin_src clojure 518 | ;;* Example 1 519 | ;; `commute' will ALWAYS run the update function TWICE 520 | 521 | ;;** : 522 | (ns user) 523 | ;; => user 524 | 525 | ;;** : 526 | (defn sleep-print-update [sleep-time thread-name update-fn] 527 | (fn [state] 528 | (Thread/sleep sleep-time) 529 | (println (str thread-name ": " state)) 530 | (update-fn state))) 531 | (def counter (ref 0)) 532 | 533 | ;;** : 534 | (dosync (ref-set counter 0)) 535 | (dosync (commute counter (sleep-print-update 100 "Commute Thread A" inc))) 536 | (dosync (commute counter (sleep-print-update 150 "Commute Thread B" inc))) 537 | ;; => Commute Thread A: 0 538 | ;; Commute Thread A: 0 539 | ;; Commute Thread B: 1 540 | ;; Commute Thread B: 1 541 | ;; 542 | ;; 2 543 | 544 | ;;* Example 2 545 | 546 | ;;** : 547 | (def counter (ref 0)) 548 | 549 | (defn commute-inc! [counter] 550 | (dosync 551 | (Thread/sleep 100) 552 | (commute counter inc))) 553 | 554 | (defn alter-inc! [counter] 555 | (dosync 556 | (Thread/sleep 100) 557 | (alter counter inc))) 558 | 559 | (defn bombard-counter! [n f counter] 560 | (apply pcalls (repeat n #(f counter)))) 561 | 562 | (dosync (ref-set counter 0)) 563 | ;; => 0 564 | 565 | ;;** : 566 | (time 567 | (doall (bombard-counter! 20 alter-inc! counter))) 568 | ;; => 569 | ;; "Elapsed time: 2009.621166 msecs" 570 | ;; 571 | ;; (1 2 3 11 7 6 4 8 10 9 13 15 14 5 19 17 12 20 18 16) 572 | 573 | ;;** : 574 | @counter 575 | ;; => 20 576 | 577 | ;;** : 578 | (dosync (ref-set counter 0)) 579 | (time 580 | (doall (bombard-counter! 20 commute-inc! counter))) 581 | ;; => 582 | ;; "Elapsed time: 202.306055 msecs" 583 | ;; 584 | ;; (1 1 8 1 1 1 1 10 9 1 1 12 12 12 12 12 12 12 19 19) 585 | 586 | ;;** : 587 | @counter 588 | ;; => 20 589 | #+end_src 590 | *** a 591 | commute 592 | ** Execute exprs while holding the monitor of x. Will always release the monitor of x 593 | #+begin_src clojure 594 | ;;* Example 595 | 596 | ;;** : 597 | (ns user) 598 | ;; => user 599 | 600 | ;;** : 601 | (def o (Object.)) 602 | (future (locking o 603 | (Thread/sleep 5000) 604 | (println "done1"))) 605 | ;; => #future[{:status :pending, :val nil} 0x95509a] 606 | 607 | ;;** : 608 | ;; Run this before 5 seconds are up. The second instance waits for the 609 | ;; first instance to print "done1" and release the lock. 610 | (Thread/sleep 1000) 611 | (locking o 612 | (Thread/sleep 1000) 613 | (println "done2")) 614 | ;; => done2 615 | ;; 616 | ;; nil 617 | #+end_src 618 | *** a 619 | locking 620 | ** Do dynamic binding 621 | #+begin_src clojure 622 | ;;* Example 623 | 624 | ;;** : 625 | (def ^:dynamic x 2) 626 | 627 | (defn addx [] 628 | (+ x 10)) 629 | 630 | (let [x 1000] 631 | (addx)) 632 | ;; => 12 633 | 634 | ;;** : 635 | (binding [x 1000] 636 | (addx)) 637 | ;; => 1010 638 | #+end_src 639 | *** a 640 | binding 641 | ** Return a lazy sequence of the values, which are evaluated in parallel 642 | #+begin_src clojure 643 | ;;* Example 644 | (defn pvs [] 645 | (pvalues 646 | (do 647 | (Thread/sleep 200) 648 | :1st) 649 | (do 650 | (Thread/sleep 300) 651 | :2nd) 652 | (keyword "3rd"))) 653 | 654 | ;;** : 655 | (time (first (pvs))) 656 | ;; => "Elapsed time: 200.664918 msecs" 657 | ;; 658 | ;; :1st 659 | 660 | ;;** : 661 | (time (last (pvs))) 662 | ;; => "Elapsed time: 300.375477 msecs" 663 | ;; 664 | ;; :3rd 665 | 666 | ;;** : 667 | (time (vec (pvs))) 668 | ;; => "Elapsed time: 300.517488 msecs" 669 | ;; 670 | ;; [:1st :2nd :3rd] 671 | #+end_src 672 | *** a 673 | pvalues 674 | ** Parallel version of map 675 | *** a 676 | pmap 677 | ** Return a lazy seq of the intermediate values of the reduction of coll by f 678 | #+begin_src clojure 679 | ;;* Example 680 | 681 | ;;** : 682 | (map #(reduce + %) [[1] [1 2] [1 2 3]]) 683 | ;; => (1 3 6) 684 | 685 | ;;** : 686 | (reductions + [1 2 3]) 687 | ;; => (1 3 6) 688 | 689 | ;;** : 690 | ;; with init arg 691 | (reductions conj [] '(1 2 3)) 692 | ;; => ([] [1] [1 2] [1 2 3]) 693 | #+end_src 694 | *** a 695 | reductions 696 | ** Return a map that consists of the rest of the maps conj-ed onto the first 697 | If a key occurs in more than one map, the mapping from the latter 698 | (left-to-right) will be mapping in the result. 699 | #+begin_src clojure 700 | ;;* Example 701 | 702 | ;;** : 703 | (merge {:a 1 :b 2 :c 3} {:b 9 :d 4}) 704 | ;; => {:a 1, :b 9, :c 3, :d 4} 705 | #+end_src 706 | *** a 707 | merge 708 | ** Return a lazy seq of the first item in each coll, then the second etc. 709 | #+begin_src clojure 710 | ;;* Example 711 | 712 | ;;** : 713 | (interleave [:a :b :c] [1 2 3] [4 5 6]) 714 | ;; => (:a 1 4 :b 2 5 :c 3 6) 715 | #+end_src 716 | *** a 717 | interleave 718 | ** Temporarily redefine vars when executing the body, e.g. when mocking for testing 719 | #+begin_src clojure 720 | ;;* Example 721 | 722 | ;;** : 723 | (type []) 724 | ;; => clojure.lang.PersistentVector 725 | 726 | ;;** : 727 | (with-redefs [type (constantly java.lang.String)] 728 | (type [])) 729 | ;; => java.lang.String 730 | #+end_src 731 | *** a 732 | with-redefs 733 | -------------------------------------------------------------------------------- /pamparam.el: -------------------------------------------------------------------------------- 1 | ;;; pamparam.el --- Simple and fast flashcards. -*- lexical-binding: t -*- 2 | 3 | ;; Copyright (C) 2016-2020 Oleh Krehel 4 | 5 | ;; Author: Oleh Krehel 6 | ;; URL: https://github.com/abo-abo/pamparam 7 | ;; Version: 0.1.0 8 | ;; Package-Requires: ((emacs "26.1") (lispy "0.27.0") (worf "0.1.0") (ivy-posframe "0.5.5")) 9 | ;; Keywords: outlines, hypermedia, flashcards, memory 10 | 11 | ;; This file is not part of GNU Emacs 12 | 13 | ;; This file is free software; you can redistribute it and/or modify 14 | ;; it under the terms of the GNU General Public License as published by 15 | ;; the Free Software Foundation; either version 3, or (at your option) 16 | ;; any later version. 17 | 18 | ;; This program is distributed in the hope that it will be useful, 19 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | ;; GNU General Public License for more details. 22 | 23 | ;; For a full copy of the GNU General Public License 24 | ;; see . 25 | 26 | ;;; Commentary: 27 | ;; 28 | ;; An example master file is given in doc/sets/capitals/capitals.org. 29 | ;; Use `hydra-pamparam/body' as the entry point. 30 | ;; See README.org for more info. 31 | 32 | ;;; Code: 33 | 34 | ;;* Requires 35 | (require 'worf) 36 | (require 'lispy) 37 | (require 'hydra) 38 | (require 'ivy) 39 | 40 | (defgroup pamparam nil 41 | "Simple and fast flashcards." 42 | :group 'flashcards) 43 | 44 | ;;* Pure 45 | (defun pamparam-sm2 (card-stats q) 46 | "Determine the next iteration of CARD-STATS based on Q. 47 | 48 | CARD-STATS is (EASE-FACTOR . INTERVALS), the result has the 49 | same shape, with updated values. 50 | 51 | EASE-FACTOR - the previous ease factor of the card. All cards are 52 | initialized with EASE-FACTOR of 2.5. It will decrease for 53 | difficult cards, but not below 1.3. 54 | 55 | INTERVALS - list of integer day intervals between repetitions. 56 | 57 | Q - the quality of the answer: 58 | 5 - perfect response 59 | 4 - correct response after a hesitation 60 | 3 - correct response recalled with serious difficulty 61 | 2 - incorrect response; where the correct one seemed easy to recall 62 | 1 - incorrect response; the correct one remembered 63 | 0 - complete blackout" 64 | (let ((EF (car card-stats)) 65 | (intervals (cdr card-stats))) 66 | (setq EF (max 1.3 (+ EF 0.1 (* (- q 5) (+ 0.08 (* (- 5 q) 0.02)))))) 67 | (if (< q 3) 68 | (cons EF (cons 1 intervals)) 69 | (cons EF 70 | (cons 71 | (cond ((null intervals) 72 | 1) 73 | ((= (car intervals) 1) 74 | 6) 75 | (t 76 | (round (* EF (car intervals))))) 77 | intervals))))) 78 | 79 | ;;* Card files 80 | (defun pamparam-card-insert-score (score actual-answer) 81 | "Insert SCORE into the current card file." 82 | (goto-char (point-min)) 83 | (outline-show-all) 84 | (if (re-search-forward "^\\*\\* scores" nil t) 85 | (outline-end-of-subtree) 86 | (forward-line 2) 87 | (insert "** scores\n") 88 | (backward-char)) 89 | (when actual-answer 90 | (kill-new actual-answer)) 91 | (insert (format-time-string "\n| <%Y-%m-%d> ") 92 | (format "| %d |" score) 93 | (format " %s |" 94 | (or actual-answer ""))) 95 | (org-table-align)) 96 | 97 | (defun pamparam-wdiff (actual-answer) 98 | (let ((expected-answer 99 | (save-excursion 100 | (goto-char (point-max)) 101 | (skip-chars-backward "\n") 102 | (buffer-substring-no-properties 103 | (line-beginning-position) 104 | (line-end-position))))) 105 | (when (and actual-answer 106 | (not (pamparam-equal actual-answer expected-answer)) 107 | (executable-find "wdiff")) 108 | (message 109 | (string-trim 110 | (shell-command-to-string 111 | (format 112 | "wdiff -i <(echo \"%s\") <(echo \"%s\")" 113 | actual-answer 114 | (string-trim-right expected-answer "[.?!]")))))))) 115 | 116 | (defun pamparam-card-read-stats () 117 | (goto-char (point-min)) 118 | (if (re-search-forward "^\\*\\* stats\n" nil t) 119 | (let ((beg (point)) 120 | (exp1 (read (current-buffer))) 121 | (exp2 (read (current-buffer))) 122 | ease-factor intervals) 123 | (if (and (eq (nth 0 exp1) 'setq) 124 | (eq (nth 1 exp1) 'ease-factor) 125 | (numberp (nth 2 exp1))) 126 | (setq ease-factor (nth 2 exp1)) 127 | (error "Bad sexp %S" exp1)) 128 | (if (and (eq (nth 0 exp2) 'setq) 129 | (eq (nth 1 exp2) 'intervals)) 130 | (setq intervals (cadr (nth 2 exp2))) 131 | (error "Bad sexp %S" exp2)) 132 | (delete-region beg (point)) 133 | (cons ease-factor intervals)) 134 | (if (re-search-forward "^\\*\\* scores\n" nil t) 135 | (progn 136 | (outline-end-of-subtree) 137 | (insert "\n** stats\n") 138 | (list 2.5)) 139 | (error "** scores not found")))) 140 | 141 | (defun pamparam-card-insert-stats (stats) 142 | (insert (format "(setq ease-factor %f)\n" (car stats))) 143 | (insert (format "(setq intervals '%S)" (cdr stats)))) 144 | 145 | (defun pamparam-delete-region (beg end) 146 | (let ((str (buffer-substring-no-properties beg end))) 147 | (delete-region beg end) 148 | str)) 149 | 150 | (defun pamparam-save-buffer () 151 | (let ((inhibit-message t)) 152 | (write-file (buffer-file-name))) 153 | (pamparam-card-abbreviate)) 154 | 155 | (defun pamparam-card-abbreviate () 156 | (let ((fname (file-name-nondirectory (buffer-file-name)))) 157 | (when (> (length fname) 60) 158 | (rename-buffer 159 | (concat "card-" (substring fname 0 6) ".org"))))) 160 | 161 | (defun pamparam-card-score (score &optional actual-answer) 162 | (let* ((card-file (file-name-nondirectory (buffer-file-name))) 163 | (todo-file (pamparam-todo-file)) 164 | (state (with-current-buffer todo-file 165 | (goto-char (point-min)) 166 | (search-forward card-file) 167 | (goto-char (+ 2 (line-beginning-position))) 168 | (buffer-substring-no-properties 169 | (point) 170 | (progn 171 | (forward-word) 172 | (point))))) 173 | (save-silently t) 174 | (inhibit-read-only t)) 175 | (cond ((string= state "REVIEW") 176 | (with-current-buffer todo-file 177 | (goto-char (point-min)) 178 | (search-forward card-file) 179 | (if (or (= score 5) 180 | (= score 4) 181 | (= score 3)) 182 | (let ((org-log-done nil) 183 | (inhibit-message t)) 184 | (org-todo 'done)) 185 | (let ((item (pamparam-delete-region 186 | (line-beginning-position) 187 | (1+ (line-end-position))))) 188 | (goto-char (point-max)) 189 | (insert item))) 190 | (pamparam-save-buffer)) 191 | (pamparam-save-buffer)) 192 | ((string= state "DONE") 193 | (if (y-or-n-p "Card already done today. Re-rate? ") 194 | (pamparam--card-score score t actual-answer) 195 | (user-error "This card is already done today"))) 196 | ((string= state "TODO") 197 | (pamparam--card-score score nil actual-answer)) 198 | (t 199 | (user-error "Unexpected state: %s" state))) 200 | (with-current-buffer todo-file 201 | (pamparam--recalculate-progress)) 202 | (outline-show-all))) 203 | 204 | (defun pamparam-card-manual-score () 205 | "Score the card 0-5 manually." 206 | (interactive) 207 | (undo) 208 | (let ((score (completing-read "score: " '("0" "1" "2" "3" "4" "5") nil t))) 209 | (pamparam-card-score (string-to-number score)))) 210 | 211 | (defun pamparam--todo-from-file (card-file) 212 | (if (string-match "\\`\\([^-]+\\)-" card-file) 213 | (format 214 | "* TODO [[file:cards/%s/%s][%s]]\n" 215 | (substring card-file 0 2) 216 | card-file 217 | (match-string 1 card-file)) 218 | (error "Unexpected file name"))) 219 | 220 | (defun pamparam--card-score (score &optional already-done actual-answer) 221 | (let ((card-file (file-name-nondirectory (buffer-file-name))) 222 | stats 223 | new-interval) 224 | (save-excursion 225 | (pamparam-card-insert-score score actual-answer) 226 | (setq stats (pamparam-card-read-stats)) 227 | (setq stats (pamparam-sm2 stats score)) 228 | (pamparam-card-insert-stats stats) 229 | (setq new-interval (nth 1 stats)) 230 | (unless already-done 231 | (let* ((todo-entry (pamparam--todo-from-file card-file)) 232 | str) 233 | (with-current-buffer (pamparam-todo-file) 234 | (goto-char (point-min)) 235 | (when (search-forward card-file) 236 | (if (memq score '(4 5)) 237 | (progn 238 | (beginning-of-line) 239 | (if (looking-at "\\* \\(TODO\\|REVIEW\\)") 240 | (replace-match "DONE" nil nil nil 1) 241 | (error "Unexpected"))) 242 | (setq str (buffer-substring-no-properties 243 | (+ 7 (line-beginning-position)) 244 | (1+ (line-end-position)))) 245 | (delete-region 246 | (line-beginning-position) 247 | (1+ (line-end-position))) 248 | (goto-char (point-max)) 249 | (insert "* REVIEW " str)) 250 | (pamparam-save-buffer))) 251 | (with-current-buffer (pamparam-todo-file new-interval) 252 | (goto-char (point-min)) 253 | (unless (search-forward todo-entry nil t) 254 | (goto-char (point-max)) 255 | (insert todo-entry) 256 | (pamparam-save-buffer)) 257 | (kill-buffer)))) 258 | (pamparam-save-buffer) 259 | (pamparam-wdiff actual-answer)))) 260 | 261 | (defvar-local pamparam-card-answer-validate-p nil) 262 | 263 | (defcustom pamparam-card-answer-function #'pamparam-card-answer-at-point 264 | "Select how to answer the card." 265 | :type '(choice 266 | (const :tag "Answer at point" pamparam-card-answer-at-point) 267 | (const :tag "Answer in a child frame" pamparam-card-answer-posframe))) 268 | 269 | (defun pamparam-card-answer-at-point () 270 | "Answer the current card. 271 | Enter the answer at point, then press \".\" to validate." 272 | (goto-char (point-min)) 273 | (when (re-search-forward "^\\* m$" nil t) 274 | (delete-region (point-min) (match-beginning 0))) 275 | (goto-char (point-min)) 276 | (insert "* \n") 277 | (goto-char 3) 278 | (setq pamparam-card-answer-validate-p t) 279 | (outline-hide-body)) 280 | 281 | (defvar pamparam-posframe-keymap 282 | (let ((map (make-sparse-keymap))) 283 | (define-key map (kbd "C-v") #'pamparam-card-reveal) 284 | (define-key map (kbd ".") #'ivy-done) 285 | map) 286 | "The keymap for `pamparam-card-answer-posframe'") 287 | 288 | (defun pamparam-card-reveal () 289 | (interactive) 290 | (with-current-buffer (ivy-state-buffer ivy-last) 291 | (pamparam-shifttab))) 292 | 293 | (defun pamparam--ivy-read-posframe (prompt) 294 | (let ((ivy-posframe-state (bound-and-true-p ivy-posframe-mode))) 295 | (unless ivy-posframe-state 296 | (ivy-posframe-mode 1)) 297 | (unwind-protect 298 | (let ((ivy-add-newline-after-prompt t)) 299 | (ivy-read prompt nil 300 | :keymap pamparam-posframe-keymap)) 301 | (unless ivy-posframe-state 302 | (ivy-posframe-mode -1))))) 303 | 304 | (defun pamparam-card-answer-posframe () 305 | (outline-hide-body) 306 | (read-only-mode 1) 307 | (let* ((card-front 308 | (save-excursion 309 | (goto-char (point-min)) 310 | (zo-down 1) 311 | (substring-no-properties (org-get-heading)))) 312 | (answer (pamparam--ivy-read-posframe 313 | (concat card-front ": ")))) 314 | (unless (string= answer "") 315 | (pamparam-card-validate answer (pamparam--card-true-answer))) 316 | (remove-overlays (point-min) (point-max) 'invisible 'outline) 317 | (read-only-mode 1))) 318 | 319 | (defun pamparam-card-answer () 320 | "Answer the current card." 321 | (funcall pamparam-card-answer-function)) 322 | 323 | (defvar pamparam-is-redo nil) 324 | 325 | (defun pamparam--card-true-answer () 326 | (save-excursion 327 | (goto-char (point-max)) 328 | (re-search-backward "^\\*") 329 | (beginning-of-line 2) 330 | (buffer-substring-no-properties 331 | (point) 332 | (1- (point-max))))) 333 | 334 | (defun pamparam-card-validate-maybe (&optional arg) 335 | "Validate the given answer and score the current card. 336 | 337 | The given answer is the text between the card's first heading and 338 | point." 339 | (interactive "p") 340 | (if pamparam-card-answer-validate-p 341 | (let ((tans (pamparam--card-true-answer)) 342 | (actual-answer (buffer-substring-no-properties 343 | (+ (line-beginning-position) 2) 344 | (line-end-position)))) 345 | (delete-region (point-min) 346 | (1+ (line-end-position))) 347 | (setq pamparam-card-answer-validate-p nil) 348 | (pamparam-card-validate actual-answer tans)) 349 | (self-insert-command arg))) 350 | 351 | (defun pamparam-card-validate (actual-answer correct-answer) 352 | "Give a card score, comparing ACTUAL-ANSWER to CORRECT-ANSWER." 353 | (if (pamparam-equal actual-answer correct-answer) 354 | (if (save-excursion 355 | (goto-char (point-max)) 356 | (re-search-backward "^\\* ") 357 | (overlays-in (point) (point-max))) 358 | (if pamparam-is-redo 359 | (pamparam-card-score 4) 360 | (pamparam-card-score 5)) 361 | (pamparam-card-score 3)) 362 | (pamparam-card-score 0 actual-answer))) 363 | 364 | ;;* Equivalence testing 365 | (defvar pamparam-equiv-hash (make-hash-table :test 'equal)) 366 | 367 | (defvar pamparam-equiv-classes '(("we" "wij") 368 | ("je" "jij") 369 | ("ze" "zij") 370 | ("u" "jij") 371 | ("dichtbij" "vlakbij") 372 | ("test" "toets"))) 373 | 374 | (defun pamparam-make-equivalent (a b) 375 | (puthash a b pamparam-equiv-hash) 376 | (puthash b b pamparam-equiv-hash)) 377 | 378 | (dolist (c pamparam-equiv-classes) 379 | (pamparam-make-equivalent (car c) (cadr c))) 380 | 381 | (defun pamparam-equal (sa sb) 382 | "Check if the answer SA matches the question SB. 383 | When SB has multiple lines, SA may match one of them." 384 | (if (string-match-p "\n" sb) 385 | (let ((sbl (split-string sb "\n" t)) 386 | res) 387 | (while (and (null res) (setq sb (pop sbl))) 388 | (setq res (pamparam-equal-single sa sb))) 389 | res) 390 | (pamparam-equal-single sa sb))) 391 | 392 | (defun pamparam-equal-single (sa sb) 393 | "Check if SA matches SB." 394 | (let ((lista (pamparam-sloppy sa)) 395 | (listb (pamparam-sloppy sb)) 396 | (res t) 397 | a b 398 | ah) 399 | (while (and res lista) 400 | (setq a (pop lista)) 401 | (setq b (pop listb)) 402 | (unless (or (string= a b) 403 | (and (setq ah (gethash a pamparam-equiv-hash)) 404 | (equal ah 405 | (gethash b pamparam-equiv-hash)))) 406 | (setq res nil))) 407 | (and res (null listb)))) 408 | 409 | (defun pamparam-sloppy (str) 410 | (mapcar #'downcase 411 | (split-string str "[.,?!: ]" t))) 412 | 413 | (defvar pamparam-load-file-name (or load-file-name 414 | (buffer-file-name))) 415 | 416 | (defvar pamparam-path (expand-file-name 417 | "doc/sets/capitals/capitals.pam" 418 | (file-name-directory pamparam-load-file-name)) 419 | "Point to a default repository. In case you call `pamparam-drill' 420 | while not in any repo, this repo will be selected.") 421 | 422 | (defvar pamparam-alist 423 | (list (cons (expand-file-name "capitals.org" 424 | (file-name-directory pamparam-path)) 425 | pamparam-path)) 426 | "Map a master file to the corresponding repository. 427 | Otherwise, the repository will be in the same directory as the master file.") 428 | 429 | ;;* Schedule files 430 | (defun pamparam-repo-directory (file) 431 | "Return the Git repository that corresponds to FILE." 432 | (or (cdr (assoc file pamparam-alist)) 433 | (if file 434 | (expand-file-name 435 | (concat 436 | (file-name-sans-extension 437 | (file-name-nondirectory 438 | file)) 439 | ".pam/")) 440 | (locate-dominating-file default-directory ".git")))) 441 | 442 | (defun pamparam-repo-init (repo-dir) 443 | "Initialize REPO-DIR Git repository." 444 | (if (file-exists-p repo-dir) 445 | (unless (file-directory-p repo-dir) 446 | (error "%s must be a directory" repo-dir)) 447 | (make-directory repo-dir) 448 | (let ((default-directory repo-dir)) 449 | (shell-command "git init") 450 | (make-directory "cards/")))) 451 | 452 | (defvar pamparam-new-cards-per-day 75) 453 | 454 | (defun pamparam-card-delete (file) 455 | "Delete the card in FILE. 456 | When called interactively, delete the card in the current buffer." 457 | (interactive (list (buffer-file-name))) 458 | (when (and (file-exists-p file) 459 | (y-or-n-p 460 | (format "Really delete %s? " 461 | (file-name-nondirectory file)))) 462 | (delete-file file) 463 | (when (string= (buffer-file-name) file) 464 | (kill-buffer)) 465 | (pamparam--update-card 466 | (file-name-nondirectory file) 467 | nil))) 468 | 469 | (defun pamparam--update-card (prev-file new-entry) 470 | (let ((prev-scheduled (pamparam-cmd-to-list (format "git grep %s" (shell-quote-argument prev-file)))) 471 | (save-silently t)) 472 | (dolist (prev prev-scheduled) 473 | (unless (string-match "\\`\\([^:]+\\):.*\\[\\[file:cards/\\(.*\\)\\]\\[.*\\]\\'" prev) 474 | (user-error "Bad scheduled item: %s" prev)) 475 | (let ((schedule-file 476 | (expand-file-name 477 | (match-string 1 prev))) 478 | (entry (match-string 2 prev))) 479 | (with-temp-buffer 480 | (insert-file-contents schedule-file) 481 | (when (re-search-forward entry nil t) 482 | (if new-entry 483 | (replace-match new-entry) 484 | (delete-region 485 | (line-beginning-position) 486 | (1+ (line-end-position))))) 487 | (write-file schedule-file)))))) 488 | 489 | (defvar pamparam-hash-card-name->file nil) 490 | (defvar pamparam-hash-card-body->file nil) 491 | 492 | (defun pamparam-cmd-to-list (cmd &optional directory) 493 | (let ((default-directory (or directory default-directory))) 494 | (split-string 495 | (shell-command-to-string cmd) 496 | "\n" t))) 497 | 498 | (defun pamparam-cards (repo-dir) 499 | (pamparam-cmd-to-list 500 | "git ls-files cards/" 501 | repo-dir)) 502 | 503 | (defun pamparam-visited-cards (repo-dir) 504 | (pamparam-cmd-to-list 505 | "git grep --files-with-matches '^\\*\\* scores'" 506 | repo-dir)) 507 | 508 | (defun pamparam-unvisited-cards (repo-dir) 509 | (pamparam-cmd-to-list 510 | "git grep --files-without-match '^\\*\\* scores' | grep cards/" 511 | repo-dir)) 512 | 513 | (defun pamparam-pile (repo-dir) 514 | "Pile up all unvisited cards into a single file." 515 | (let ((unvisited-cards (pamparam-unvisited-cards repo-dir)) 516 | (schedule-files (pamparam-cmd-to-list "git ls-files --full-name pamparam-*-[0-9][0-9].org")) 517 | (save-silently t)) 518 | (dolist (sf schedule-files) 519 | (with-current-buffer (find-file (expand-file-name sf repo-dir)) 520 | (dolist (card unvisited-cards) 521 | (goto-char (point-min)) 522 | (while (search-forward card nil t) 523 | (delete-region (line-beginning-position) (1+ (line-end-position))))) 524 | (pamparam-save-buffer) 525 | (kill-buffer))) 526 | (with-current-buffer (find-file (expand-file-name "pampile.org" repo-dir)) 527 | (delete-region (point-min) (point-max)) 528 | (dolist (card unvisited-cards) 529 | (insert (pamparam--todo-from-file (file-name-nondirectory card)))) 530 | (pamparam-save-buffer) 531 | (kill-buffer)))) 532 | 533 | (defun pamparam-pull (arg &optional buffer) 534 | "Pull ARG cards into BUFFER. 535 | When called interactively, use today's schedule file." 536 | (interactive 537 | (list (read-number "how many cards: ") 538 | (pamparam-todo-file))) 539 | (let ((save-silently t) 540 | cards) 541 | (setq arg (min 100 arg)) 542 | (switch-to-buffer buffer) 543 | (with-current-buffer (find-file-noselect 544 | (expand-file-name "pampile.org")) 545 | (goto-char (point-min)) 546 | (end-of-line arg) 547 | (setq cards (pamparam-delete-region (point-min) 548 | (min (1+ (point)) 549 | (point-max)))) 550 | (pamparam-save-buffer) 551 | (kill-buffer)) 552 | (pamparam-goto-schedule-part) 553 | (insert cards) 554 | (pamparam-save-buffer))) 555 | 556 | (defun pamparam-goto-schedule-part () 557 | (goto-char (point-min)) 558 | (if (re-search-forward "^\\*" nil t) 559 | (goto-char (match-beginning 0)) 560 | (goto-char (point-max)))) 561 | 562 | (defun pamparam--recompute-git-cards (repo-dir) 563 | (setq pamparam-hash-card-name->file (make-hash-table :test 'equal)) 564 | (setq pamparam-hash-card-body->file (make-hash-table :test 'equal)) 565 | (let ((git-files (pamparam-cards repo-dir))) 566 | (dolist (gf git-files) 567 | (if (string-match "\\`cards/[0-9a-f]\\{2\\}/\\([^-]+\\)-\\([^.]+\\)\\.org\\'" gf) 568 | (progn 569 | (puthash (match-string 1 gf) gf pamparam-hash-card-name->file) 570 | (puthash (match-string 2 gf) gf pamparam-hash-card-body->file)) 571 | (error "Unexpected file name %s" gf))))) 572 | 573 | (defun pamparam--replace-card (_card-front _card-body repo-dir card-file prev-file) 574 | (let* ((full-name (expand-file-name prev-file repo-dir)) 575 | (old-metadata 576 | (with-temp-buffer 577 | (insert-file-contents full-name) 578 | (goto-char (point-min)) 579 | (when (looking-at "\\* m$") 580 | (outline-end-of-subtree) 581 | (buffer-substring-no-properties 582 | (point-min) 583 | (1+ (point))))))) 584 | (pamparam-kill-buffer-of-file full-name) 585 | (delete-file full-name) 586 | (let ((default-directory repo-dir) 587 | (fnn (file-name-nondirectory card-file))) 588 | (pamparam--update-card prev-file (concat (substring fnn 0 2) "/" fnn))) 589 | old-metadata)) 590 | 591 | (eval-and-compile 592 | (if (eq system-type 'windows-nt) 593 | (defun pamparam-spit (str file) 594 | (with-current-buffer (find-file-noselect file) 595 | (erase-buffer) 596 | (insert str) 597 | (save-buffer) 598 | (kill-buffer (current-buffer)))) 599 | (defun pamparam-spit (str file) 600 | (let ((cmd (format "echo '%s' > %s" 601 | (replace-regexp-in-string "'" "'\\''" str t t) 602 | (shell-quote-argument file)))) 603 | (unless (= 0 (call-process-shell-command cmd)) 604 | (error "Command failed: %s" cmd)))))) 605 | 606 | (defun pamparam-slurp (f) 607 | (with-temp-buffer 608 | (insert-file-contents f) 609 | (buffer-string))) 610 | 611 | (defun pamparam-update-card (card-front card-body repo-dir) 612 | (let* ((card-front-id (md5 card-front)) 613 | (card-body-id (md5 card-body)) 614 | (prev-file 615 | (or 616 | (gethash card-front-id pamparam-hash-card-name->file) 617 | (gethash card-body-id pamparam-hash-card-body->file))) 618 | (subdir (substring card-front-id 0 2)) 619 | (card-file 620 | (concat 621 | "cards/" subdir "/" card-front-id "-" card-body-id ".org")) 622 | (full-card-file (expand-file-name card-file repo-dir)) 623 | (metadata nil)) 624 | (cond ((null prev-file)) 625 | ((string= card-file prev-file)) 626 | (t 627 | (when (file-exists-p (expand-file-name prev-file repo-dir)) 628 | (setq metadata (pamparam--replace-card 629 | card-front card-body repo-dir card-file prev-file))))) 630 | (unless (file-exists-p (expand-file-name card-file repo-dir)) 631 | (let* ((txt 632 | (concat 633 | (or metadata "* m\n#+STARTUP: content\n") 634 | (format "* %s\n%s" card-front card-body)))) 635 | (make-directory (file-name-directory full-card-file) t) 636 | (pamparam-spit txt full-card-file) 637 | (cons (if metadata 638 | 'update 639 | 'new) 640 | card-file))))) 641 | 642 | (defconst pamparam-card-source-regexp "^\\*+ .*:cards:") 643 | 644 | (defun pamparam-sync () 645 | "Synchronize the current `org-mode' master file to the cards repository. 646 | 647 | Create the cards repository if it doesn't exist. 648 | 649 | Each card is uniquely identifiable by either its front or its 650 | back. So if you want to modify both the front and the back, first 651 | modify the front, call `pamparam-sync', then modify the back and call 652 | `pamparam-sync' again. Otherwise, there's no way to \"connect\" the 653 | new card to the old one, and the old card will remain in the 654 | repository, while the new card will start with empty metadata." 655 | (interactive) 656 | (unless (eq major-mode 'org-mode) 657 | (error "Must be in `org-mode' file")) 658 | (when (pamparam--cards-available-p) 659 | (let ((repo-dir (pamparam-repo-directory (buffer-file-name))) 660 | (make-backup-files nil)) 661 | (pamparam-repo-init repo-dir) 662 | (pamparam--recompute-git-cards repo-dir) 663 | (pamparam--sync repo-dir)))) 664 | 665 | (defun pamparam-kill-buffer-of-file (fname) 666 | (dolist (buf (buffer-list)) 667 | (when (equal fname (buffer-file-name buf)) 668 | (kill-buffer buf)))) 669 | 670 | (defvar org-keyword-properties) 671 | 672 | (defun pamapram--cards-at-level-one-p () 673 | (let ((alist (if (boundp 'org-file-properties) 674 | org-file-properties 675 | org-keyword-properties))) 676 | (assoc-string "pamparam" alist t))) 677 | 678 | (defun pamparam--cards-available-p () 679 | (or (pamapram--cards-at-level-one-p) 680 | (save-excursion 681 | (goto-char (point-min)) 682 | (if (re-search-forward pamparam-card-source-regexp nil t) 683 | t 684 | (error "No outlines with the :cards: tag found"))))) 685 | 686 | (defun pamparam--sync (repo-dir) 687 | (let ((old-point (point)) 688 | (processed-headings nil) 689 | (new-cards nil) 690 | (updated-cards nil)) 691 | (goto-char (point-min)) 692 | (let* ((cards-at-level-one-p (pamapram--cards-at-level-one-p)) 693 | (regex (if cards-at-level-one-p 694 | "\\*+ .*$" 695 | pamparam-card-source-regexp))) 696 | (while (re-search-forward regex nil t) 697 | (when cards-at-level-one-p 698 | (beginning-of-line)) 699 | (lispy-destructuring-setq (processed-headings new-cards updated-cards) 700 | (pamparam-sync-current-outline 701 | processed-headings new-cards updated-cards repo-dir)))) 702 | (goto-char old-point) 703 | (when (or new-cards updated-cards) 704 | (let ((pile-fname (expand-file-name "pampile.org" repo-dir))) 705 | (pamparam-kill-buffer-of-file pile-fname) 706 | (pamparam-schedule-today 707 | (mapcar #'pamparam--todo-from-file new-cards) 708 | (find-file-noselect pile-fname))) 709 | (shell-command-to-string 710 | (format 711 | "cd %s && git add . && git commit -m %s" 712 | (shell-quote-argument repo-dir) 713 | (shell-quote-argument 714 | (cond ((null updated-cards) 715 | (format "Add %d new card(s)" (length new-cards))) 716 | ((null new-cards) 717 | (format "Update %d card(s)" (length updated-cards))) 718 | (t 719 | (format "Add %d new card(s), update %d cards" 720 | (length new-cards) 721 | (length updated-cards)))))))) 722 | (message "%d new cards, %d updated, %d total" 723 | (length new-cards) 724 | (length updated-cards) 725 | (length processed-headings)))) 726 | 727 | (defun pamparam--card-info () 728 | (let* ((bnd (worf--bounds-subtree)) 729 | (str (lispy--string-dwim bnd)) 730 | front back) 731 | (cond ((string-match "^\\*+ a\n\\(.*\\)" str) 732 | (setq front (substring str 0 (match-beginning 0))) 733 | (setq back (concat "* a\n" (match-string 1 str))) 734 | (setq front (string-trim-left front)) 735 | (goto-char (cdr bnd))) 736 | ((string-match "\\`\\*+ \\(.*\\)\n\\([^*]+\\)\\(?:\n\\*\\)?" str) 737 | (setq front (match-string 1 str)) 738 | (setq back (match-string 2 str)) 739 | (goto-char (+ (car bnd) (match-end 2))) 740 | (setq back (string-trim-right back))) 741 | ((string-match "\\`\\(\\*+ \\).*{\\([^}]+\\)}.*\\'" str) 742 | (setq front 743 | (concat (substring str (match-end 1) (1- (match-beginning 2))) 744 | "[...]" 745 | (substring str (1+ (match-end 2))))) 746 | (setq back (match-string 2 str))) 747 | (t 748 | (error "unexpected"))) 749 | (cons front back))) 750 | 751 | (defun pamparam-sync-current-outline (processed-headings new-cards updated-cards repo-dir) 752 | (let ((end (save-excursion 753 | (outline-end-of-subtree) 754 | (point)))) 755 | (while (re-search-forward "^\\*+ \\(.*\\)$" end t) 756 | (let* ((card-info (pamparam--card-info)) 757 | (card-front (car card-info)) 758 | (card-body (cdr card-info)) 759 | card-file) 760 | (if (member card-front processed-headings) 761 | (error "Duplicate heading encountered: %s" card-front) 762 | (push card-front processed-headings)) 763 | (when (setq card-info (pamparam-update-card card-front card-body repo-dir)) 764 | (setq card-file (file-name-nondirectory (cdr card-info))) 765 | (cond ((eq (car card-info) 'new) 766 | (push card-file new-cards)) 767 | ((eq (car card-info) 'update) 768 | (push card-file updated-cards)))))) 769 | (list processed-headings new-cards updated-cards))) 770 | 771 | (defun pamparam-default-directory () 772 | (if (string-match "^\\(.*\\.pam/\\)" default-directory) 773 | (expand-file-name (match-string 1 default-directory)) 774 | pamparam-path)) 775 | 776 | (defun pamparam-kill-buffers () 777 | (let* ((pdir (pamparam-default-directory)) 778 | (cards-dir (expand-file-name "cards/" pdir))) 779 | (dolist (b (buffer-list)) 780 | (when (buffer-file-name b) 781 | (let ((dir (file-name-directory (buffer-file-name b)))) 782 | (when (or (equal dir cards-dir) 783 | (and (equal dir pdir) 784 | (not (equal (file-name-nondirectory 785 | (buffer-file-name b)) 786 | (pamparam-schedule-file (current-time)))))) 787 | (kill-buffer b))))))) 788 | 789 | (defun pamparam-schedule-file (time) 790 | (let ((year (format-time-string "%Y" time)) 791 | (current-year (format-time-string "%Y" (current-time))) 792 | (base (format-time-string "pam-%Y-%m-%d.org" time))) 793 | (if (string= year current-year) 794 | base 795 | (let ((dir (expand-file-name 796 | year (expand-file-name "years" (pamparam-default-directory))))) 797 | (unless (file-exists-p dir) 798 | (make-directory dir t)) 799 | (expand-file-name base dir))))) 800 | 801 | (defun pamparam-todo-file (&optional offset) 802 | (setq offset (or offset 0)) 803 | (let* ((default-directory (pamparam-default-directory)) 804 | (todo-file (expand-file-name 805 | (pamparam-schedule-file 806 | (time-add 807 | (current-time) 808 | (days-to-time offset))))) 809 | (save-silently t)) 810 | (unless (file-exists-p todo-file) 811 | (save-current-buffer 812 | (find-file todo-file) 813 | (insert "#+SEQ_TODO: TODO REVIEW | DONE\n") 814 | (when (eq offset 0) 815 | (pamparam-pull 10 (current-buffer)) 816 | (message "Schedule was empty, used `pamparam-pull' for 10 cards")) 817 | (pamparam-save-buffer))) 818 | (find-file-noselect todo-file))) 819 | 820 | (defvar pamparam-last-rechedule nil) 821 | 822 | (defun pamparam-schedule-today (cards &optional buffer) 823 | (with-current-buffer (or buffer (pamparam-todo-file)) 824 | (pamparam-goto-schedule-part) 825 | (dolist (card cards) 826 | (insert card)) 827 | (let ((save-silently t)) 828 | (pamparam-save-buffer)))) 829 | 830 | (defvar-local pamparam--progress nil 831 | "Cache the current progress.") 832 | 833 | (defun pamparam-current-progress () 834 | (with-current-buffer (pamparam-todo-file) 835 | (or pamparam--progress 836 | (pamparam--recalculate-progress)))) 837 | 838 | (defun pamparam--recalculate-progress () 839 | (setq pamparam--progress 840 | (let ((n-done 0) 841 | (n-todo 0) 842 | (n-review 0)) 843 | (save-excursion 844 | (goto-char (point-min)) 845 | (while (re-search-forward "^\\* \\(TODO\\|DONE\\|REVIEW\\)" nil t) 846 | (let ((ms (match-string 1))) 847 | (cond ((string= ms "TODO") 848 | (cl-incf n-todo)) 849 | ((string= ms "DONE") 850 | (cl-incf n-done)) 851 | ((string= ms "REVIEW") 852 | (cl-incf n-review))))) 853 | (list n-done n-todo n-review))))) 854 | 855 | (defun pamparam-mode-line () 856 | (cl-destructuring-bind (n-done n-todo n-review) 857 | (pamparam-current-progress) 858 | (format "(pam: %d/%d+%d)" n-done n-todo n-review))) 859 | 860 | (defvar pamparam-day-limit 50 861 | "Limit for today's repetitions. 862 | All cards above this number that would be scheduled for today 863 | will instead be moved to tomorrow.") 864 | 865 | (defun pamparam-merge-schedules (from to) 866 | "Copy items FROM -> TO. Delete FROM." 867 | (let ((from-lines 868 | (cl-remove-if-not 869 | (lambda (s) (string-match-p "^\\*" s)) 870 | (split-string (pamparam-slurp from) "\n" t))) 871 | (to-lines (split-string (pamparam-slurp to) "\n" t))) 872 | (pamparam-spit 873 | (mapconcat #'identity 874 | (append to-lines from-lines) 875 | "\n") 876 | to) 877 | (delete-file from))) 878 | 879 | (defun pamparam-carryover-year-maybe () 880 | "Move e.g. years/2018/*.org to . if the current year is 2018." 881 | (let* ((today (calendar-current-date)) 882 | (year (nth 2 today)) 883 | (default-directory (pamparam-default-directory)) 884 | (year-directory (format "years/%d" year))) 885 | (when (file-exists-p year-directory) 886 | (let ((year-files (directory-files year-directory nil "org$"))) 887 | (dolist (file year-files) 888 | (let ((file-from (expand-file-name file year-directory)) 889 | (file-to (expand-file-name file))) 890 | (if (file-exists-p file-to) 891 | (pamparam-merge-schedules file-from file-to) 892 | (rename-file file-from file-to))))) 893 | (delete-directory year-directory)))) 894 | 895 | (defun pamparam-check () 896 | "Check the repo for inconsistencies and fix them. 897 | 898 | Check that all existing cards are scheduled, and only once. 899 | Check that there are no scheduled unexisting cards." 900 | (interactive) 901 | (let* ((default-directory (pamparam-default-directory)) 902 | (all-cards (pamparam-cards default-directory)) 903 | (all-schedules (delq nil 904 | (mapcar 905 | (lambda (s) 906 | (when (string-match "file:\\([^]]+\\)" s) 907 | (match-string 1 s))) 908 | (pamparam-cmd-to-list 909 | "git grep '^\\* TODO'")))) 910 | (unscheduled-cards (cl-set-difference 911 | all-cards 912 | all-schedules 913 | :test #'equal)) 914 | (unexisting-cards (cl-set-difference 915 | all-schedules 916 | all-cards 917 | :test #'equal)) 918 | (all-schedules-nodups (delete-dups (copy-sequence all-schedules))) 919 | (duplicate-cards (cl-set-difference all-schedules all-schedules-nodups))) 920 | (with-current-buffer (find-file-noselect "pampile.org") 921 | (goto-char (point-min)) 922 | (dolist (card unscheduled-cards) 923 | (insert (format "* TODO [[file:%s][%s]]\n" 924 | card (nth 1 (split-string card "[-.]"))))) 925 | (save-buffer)) 926 | (dolist (card (append duplicate-cards unexisting-cards)) 927 | (let ((occurences (pamparam-cmd-to-list (format "git grep %s" card)))) 928 | (dolist (occ (if (= (length occurences) 1) 929 | occurences 930 | (cdr occurences))) 931 | (with-current-buffer (find-file-noselect (car (split-string occ ":"))) 932 | (goto-char (point-min)) 933 | (re-search-forward card) 934 | (delete-region (line-beginning-position) 935 | (1+ (line-end-position))) 936 | (save-buffer))))))) 937 | 938 | (defun pamparam-reschedule-maybe () 939 | (pamparam-carryover-year-maybe) 940 | (let ((today (calendar-current-date))) 941 | (unless (and pamparam-last-rechedule 942 | (< 943 | (calendar-absolute-from-gregorian today) 944 | (calendar-absolute-from-gregorian pamparam-last-rechedule))) 945 | (setq pamparam-last-rechedule today) 946 | (let* ((today-file (pamparam-todo-file)) 947 | (today-file-name (file-name-nondirectory 948 | (buffer-file-name today-file))) 949 | (pdir (file-name-directory 950 | (buffer-file-name today-file))) 951 | (all-files (directory-files pdir nil "org$")) 952 | (idx (cl-position today-file-name all-files 953 | :test 'equal)) 954 | (old-files (reverse (cl-subseq all-files 0 idx)))) 955 | (dolist (old-file old-files) 956 | (setq old-file (expand-file-name old-file pdir)) 957 | (let (cards) 958 | (with-current-buffer (find-file-noselect old-file) 959 | (goto-char (point-min)) 960 | (while (re-search-forward "^\\* \\(TODO\\|REVIEW\\) " nil t) 961 | (push (buffer-substring-no-properties 962 | (point) (1+ (line-end-position))) 963 | cards))) 964 | (pamparam-schedule-today (mapcar (lambda (s) (concat "* TODO " s)) 965 | (nreverse cards))) 966 | (delete-file old-file))) 967 | (with-current-buffer today-file 968 | (goto-char (point-min)) 969 | (when (re-search-forward "^\\* TODO" nil t pamparam-day-limit) 970 | (beginning-of-line 2) 971 | (let ((rescheduled (buffer-substring-no-properties 972 | (point) (point-max)))) 973 | (delete-region (point) (point-max)) 974 | (save-buffer) 975 | (with-current-buffer (pamparam-todo-file 1) 976 | (goto-char (point-max)) 977 | (insert rescheduled) 978 | (save-buffer))))))))) 979 | 980 | ;;;###autoload 981 | (defun pamparam-drill () 982 | "Start a learning session. 983 | 984 | When `default-directory' is in a *.pam repository, use that repository. 985 | Otherwise, use the repository that `pamparam-path' points to. 986 | 987 | See `pamparam-sync' for creating and updating a *.pam repository. 988 | 989 | If you have no more cards scheduled for today, use `pamparam-pull'." 990 | (interactive) 991 | (pamparam-reschedule-maybe) 992 | (let (card-link card-file) 993 | (when (bound-and-true-p pamparam-card-mode) 994 | (when (buffer-modified-p) 995 | (pamparam-save-buffer)) 996 | (kill-buffer)) 997 | (delete-other-windows) 998 | (split-window-vertically) 999 | (pamparam-kill-buffers) 1000 | (switch-to-buffer (pamparam-todo-file)) 1001 | (goto-char (point-min)) 1002 | (when (re-search-forward "^* \\(TODO\\|REVIEW\\) " nil t) 1003 | (recenter 5) 1004 | (setq card-link (buffer-substring-no-properties 1005 | (point) (line-end-position))) 1006 | (beginning-of-line) 1007 | (set-window-point (selected-window) (point))) 1008 | (other-window 1) 1009 | (if (null card-link) 1010 | (message "%d cards learned/reviewed today. Well done!" 1011 | (cl-count-if 1012 | (lambda (x) (string-match "^\\* DONE" x)) 1013 | (split-string (with-current-buffer (pamparam-todo-file) 1014 | (buffer-string)) "\n"))) 1015 | (unless (string-match "\\`\\[\\[file:\\([^]]+\\)\\]\\[.*\\]\\]\\'" card-link) 1016 | (error "Bad entry in %s: %s" (pamparam-todo-file) card-link)) 1017 | (setq card-file (match-string 1 card-link)) 1018 | (switch-to-buffer 1019 | (find-file-noselect 1020 | (expand-file-name card-file (pamparam-default-directory)))) 1021 | (pamparam-card-mode)))) 1022 | 1023 | (defun pamparam-commit () 1024 | "Commit the current progress using Git." 1025 | (interactive) 1026 | (let* ((repo-dir (pamparam-repo-directory (buffer-file-name))) 1027 | (default-directory (if (file-exists-p repo-dir) 1028 | repo-dir 1029 | (pamparam-default-directory))) 1030 | (status (pamparam-cmd-to-list "git status")) 1031 | (card-count 1032 | (cl-count-if 1033 | (lambda (s) 1034 | (or (string-match "modified.*cards/" s) 1035 | (string-match "new file.*cards/" s))) 1036 | status)) 1037 | (card-str (if (= card-count 1) 1038 | "card" 1039 | "cards"))) 1040 | (message 1041 | (replace-regexp-in-string 1042 | "%" "%%" 1043 | (shell-command-to-string 1044 | (format 1045 | "git add . && git commit -m 'Do %s %s'" 1046 | card-count card-str)))))) 1047 | 1048 | (defun pamparam-unschedule-card (card-file) 1049 | "Unschedule CARD-FILE everywhere and schedule it for today." 1050 | (let* ((repo-dir (locate-dominating-file card-file ".git")) 1051 | (s-files (pamparam-cmd-to-list (format "git add . && git grep --files-with-matches %s" (shell-quote-argument card-file)) 1052 | repo-dir))) 1053 | (dolist (file s-files) 1054 | (with-current-buffer (find-file-noselect (expand-file-name file repo-dir)) 1055 | (save-excursion 1056 | (goto-char (point-min)) 1057 | (while (re-search-forward card-file nil t) 1058 | (delete-region (line-beginning-position) 1059 | (1+ (line-end-position)))) 1060 | (let ((save-silently t)) 1061 | (pamparam-save-buffer))) 1062 | (unless (equal (current-buffer) (pamparam-todo-file)) 1063 | (kill-buffer)))) 1064 | (with-current-buffer (pamparam-todo-file) 1065 | (pamparam-goto-schedule-part) 1066 | (if (re-search-forward "^\\* \\(TODO\\|REVIEW\\)" nil t) 1067 | (goto-char (match-beginning 0)) 1068 | (goto-char (point-max))) 1069 | (insert (pamparam--todo-from-file card-file))))) 1070 | 1071 | (defun pamparam-card-redo () 1072 | "Redo the current card without penalty." 1073 | (interactive) 1074 | (if (string-match-p "cards/.*org\\'" (buffer-file-name)) 1075 | (let ((fname (buffer-file-name))) 1076 | (pamparam-save-buffer) 1077 | (pamparam-cmd-to-list (format "git checkout -- %s" (shell-quote-argument fname))) 1078 | (revert-buffer nil t nil) 1079 | (pamparam-unschedule-card (file-name-nondirectory fname)) 1080 | (setq-local pamparam-is-redo t) 1081 | (pamparam-card-mode)) 1082 | (user-error "Applies only to card files"))) 1083 | 1084 | (defun pamparam-shifttab () 1085 | "Hide/show everything." 1086 | (interactive) 1087 | (let ((inhibit-message t)) 1088 | (when (eq org-cycle-global-status 'overview) 1089 | (setq org-cycle-global-status 'contents)) 1090 | (setq this-command last-command) 1091 | (org-cycle-internal-global))) 1092 | 1093 | ;;* `pamparam-card-mode' 1094 | (defvar pamparam-card-mode-map 1095 | (let ((map (make-sparse-keymap))) 1096 | (worf-define-key map (kbd "q") 'bury-buffer) 1097 | (worf-define-key map (kbd "R") 'pamparam-card-redo 1098 | :break t) 1099 | (worf-define-key map (kbd "n") 'pamparam-drill 1100 | :break t) 1101 | (worf-define-key map (kbd "D") 'pamparam-card-delete) 1102 | (define-key map (kbd ".") 'pamparam-card-validate-maybe) 1103 | (define-key map (kbd "M-m") 'pamparam-card-manual-score) 1104 | (define-key map (kbd "") 'pamparam-shifttab) 1105 | map)) 1106 | 1107 | (define-minor-mode pamparam-card-mode 1108 | "Minor mode for Pam cards. 1109 | 1110 | \\{pamparam-card-mode-map}" 1111 | :lighter " p" 1112 | (when pamparam-card-mode 1113 | (if (eq major-mode 'org-mode) 1114 | (progn 1115 | (pamparam-card-abbreviate) 1116 | (setq-local mode-line-format 1117 | `((pamparam-card-mode 1118 | (:eval (pamparam-mode-line))) 1119 | ,@(assq-delete-all 1120 | 'pamparam-card-mode 1121 | (default-value 'mode-line-format)))) 1122 | (force-mode-line-update t) 1123 | (setq org-cycle-global-status 'contents) 1124 | (goto-char (point-min)) 1125 | (pamparam-card-answer)) 1126 | (pamparam-card-mode -1)))) 1127 | 1128 | (lispy-raise-minor-mode 'pamparam-card-mode) 1129 | 1130 | ;;* `hydra-pamparam' 1131 | (defhydra hydra-pamparam (:exit t) 1132 | "pam" 1133 | ("d" pamparam-drill "drill") 1134 | ("s" pamparam-sync "sync") 1135 | ("p" pamparam-pull "pull") 1136 | ("c" pamparam-commit "commit") 1137 | ("q" nil "quit")) 1138 | (hydra-set-property 'hydra-pamparam :verbosity 1) 1139 | 1140 | (provide 'pamparam) 1141 | 1142 | ;;; pamparam.el ends here 1143 | --------------------------------------------------------------------------------