diff options
-rw-r--r-- | doc/gnosis.org | 54 | ||||
-rw-r--r-- | gnosis-dev.el | 7 | ||||
-rw-r--r-- | gnosis.el | 123 |
3 files changed, 114 insertions, 70 deletions
diff --git a/doc/gnosis.org b/doc/gnosis.org index 8d23a30..28b9cf9 100644 --- a/doc/gnosis.org +++ b/doc/gnosis.org @@ -4,8 +4,8 @@ #+language: en #+options: ':t toc:nil author:t email:t num:t #+startup: content -#+macro: stable-version 0.1.3 -#+macro: release-date 2023-01-18 +#+macro: stable-version 0.1.4 +#+macro: release-date 2023-01-19 #+macro: development-version 0.1.4-dev #+macro: file @@texinfo:@file{@@$1@@texinfo:}@@ #+macro: space @@texinfo:@: @@ @@ -120,7 +120,7 @@ Every note type has these values in common: + ~second-image~ Image displayed /after/ user input The following sections will cover the important differences you have -to know when creating notes. +to know when creating new notes. * Note Types ** Cloze @@ -175,8 +175,48 @@ When using the hidden function =gnosis-add-note--y-or-n=, note that the ANSWER must be either 121 (~y~) or 110 (~n~), as those correspond to the character values used to represent them. -* Customization -** Gnosis Algorithm Initial Interval +* Customization & Development + +To make development and customization easier, gnosis comes with +=gnosis-dev= module, that should be used to create a custom database for +testing. + +To use =gnosis-dev=, first you have to =(require 'gnosis-dev)= & run =M-x +gnosis-dev-test=. This will create a new directory 'testing' with a new +database. + +To exit the testing environment, rerun =M-x gnosis-dev-test= and then +enter =n= (no) at the prompt "Start development env?" + +** Creating Custom Note Types +Creating custom note types for gnosis is a fairly simple thing to do + ++ First add your NEW-TYPE to =gnosis-note-types= + + #+begin_src emacs-lisp + (add-to-list 'gnosis-note-types 'new-type) + #+end_src + ++ Create 2 functions; =gnosis-add-note-TYPE= & =gnosis-add-note--TYPE= + +Each note type has a =gnosis-add-note-TYPE= that is used +interactively & a "hidden function" =gnosis-add-note--TYPE= that handles +all the logic. + +Refer to =gnosis-add-note-basic= & =gnosis-add-note--basic= for a simple +example of how this is done. + ++ Create =gnosis-review-TYPE= + +This function should handle the review process, displaying it's +contents and updating the database depending on the result of the +review (fail/pass). Refer to =gnosis-review-basic= for an example of how +this should be done. + ++ Optionally, you might want to create your own custom =gnosis-display= functions + +** Customizing Gnosis Algorithm +*** Gnosis Algorithm Initial Interval =gnosis-algorithm-interval= is a list of 2 numbers, representing the first two initial intervals for successful reviews. @@ -191,7 +231,7 @@ Using the above example, after first successfully reviewing a note, you will see it again tomorrow, if you successfully review said note again, the next review will be after 3 days. -** Gnosis Algorithm Easiness Factor +*** Gnosis Algorithm Easiness Factor =gnosis-algorithm-ef= is a list that consists of 3 items. @@ -216,7 +256,7 @@ Example: (setq gnosis-algorithm-ef '(0.3 0.3 1.3)) #+end_src -** Gnosis Algorithm Forgetting Factor +*** Gnosis Algorithm Forgetting Factor =gnosis-algorithm-ff= is a floating number below 1. diff --git a/gnosis-dev.el b/gnosis-dev.el index 4387ce9..6801290 100644 --- a/gnosis-dev.el +++ b/gnosis-dev.el @@ -22,11 +22,10 @@ ;; You should have received a copy of the GNU General Public License ;; along with this program. If not, see <https://www.gnu.org/licenses/>. -;; Random functions to make development/testing of gnosis.el easier - ;;; Commentary: -;; Development tools used for gnosis +;; Development module for gnosis, to make development of gnosis.el +;; easier by creating a testing environment with random inputs. ;;; Code: @@ -110,7 +109,7 @@ by the thoracodorsal nerve." If ask nil, leave development env" (interactive) - (let ((ask (y-or-n-p "Start development env?")) + (let ((ask (y-or-n-p "Start development env (n for exit)?")) (testing-dir (concat gnosis-dir "/testing"))) (if ask (progn @@ -1,4 +1,4 @@ -;;; gnosis.el --- Spaced Repetition System -*- lexical-binding: t; -*- +;;; gnosis.el --- Spaced Repetition System For Note Taking & Self Testing -*- lexical-binding: t; -*- ;; Copyright (C) 2023 Thanos Apollo @@ -24,9 +24,9 @@ ;;; Commentary: -;; Gnosis (γνῶσις), pronounced "noh-sis", meaning knowledge in Greek, is -;; a Spaced Repetition System <https://en.wikipedia.org/wiki/Spaced_repetition> -;; for GNU Emacs. +;; Gnosis is a spaced repetition system for note taking & self testing, +;; where notes are taken in a Question/Answer/Explanation-like format +;; & reviewed in spaced intervals. ;; ;; Gnosis does not implement flashcard type review sessions where the ;; user rates his own answer on an arbitrary scale. Instead implements @@ -34,9 +34,14 @@ ;; the MCQ, multiple choice question, even allow for simulating ;; real-life exams. ;; -;; Unlike other SRS implementations for GNU Emacs, gnosis not rely on -;; org-mode. Instead utilizes an sqlite database & git, enabling -;; efficient data management, manipulation and data integrity. +;; Gnosis can help you better understand and retain the material by +;; encouraging active engagement. It also provides a clear structure for +;; your notes & review sessions, making it easier to study. +;; +;; Unlike other SRS implementations for GNU Emacs, gnosis does not +;; rely on org-mode. Instead utilizes an sqlite database & git, +;; enabling efficient data management, manipulation and data +;; integrity. ;;; Code: @@ -45,13 +50,14 @@ (require 'cl-lib) (require 'gnosis-algorithm) +(require 'vc) (defgroup gnosis nil - "Spaced repetition learning tool." + "Spaced Repetition System For Note Taking & Self Testing." :group 'external :prefix "gnosis-") -(defcustom gnosis-dir (concat user-emacs-directory "gnosis") +(defcustom gnosis-dir (locate-user-emacs-file "gnosis") "Gnosis directory." :type 'directory :group 'gnosis) @@ -62,13 +68,13 @@ :group 'gnosis) -(defvar gnosis-images-dir (concat (file-name-as-directory gnosis-dir) "images") +(defvar gnosis-images-dir (expand-file-name "images" gnosis-dir) "Gnosis images directory.") (defconst gnosis-db (if (not (file-directory-p gnosis-dir)) (gnosis-db-init) - (emacsql-sqlite (concat (file-name-as-directory gnosis-dir) "gnosis.db"))) + (emacsql-sqlite (expand-file-name "gnosis.db" gnosis-dir))) "Gnosis database file.") (defvar gnosis-testing nil @@ -77,6 +83,9 @@ (defconst gnosis-db-version 1 "Gnosis database version.") +(defvar gnosis-note-types '(MCQ Cloze Basic Double y-or-n) + "Gnosis available note types.") + ;;; Faces (defgroup gnosis-faces nil @@ -136,8 +145,6 @@ "Face for next review." :group 'gnosis-face) - - (cl-defun gnosis-select (value table &optional (restrictions '1=1)) "Select VALUE from TABLE, optionally with RESTRICTIONS. @@ -647,17 +654,14 @@ See `gnosis-add-note--cloze' for more reference." ;;;###autoload (defun gnosis-add-note (type) "Create note(s) as TYPE interactively." - (interactive (list (completing-read "Type: " '(MCQ Cloze Basic Double y-or-n) nil t))) + (interactive (list (completing-read "Type: " gnosis-note-types nil t))) (when gnosis-testing (unless (y-or-n-p "You are using a testing environment! Continue?") (error "Aborted"))) - (pcase type - ("MCQ" (gnosis-add-note-mcq)) - ("Cloze" (gnosis-add-note-cloze)) - ("Basic" (gnosis-add-note-basic)) - ("Double" (gnosis-add-note-double)) - ("y-or-n" (gnosis-add-note-y-or-n)) - (_ (message "No such type.")))) + (let ((func-name (intern (format "gnosis-add-note-%s" (downcase type))))) + (if (fboundp func-name) + (funcall func-name) + (message "No such type.")))) (defun gnosis-mcq-answer (id) "Choose the correct answer, from mcq choices for question ID." @@ -758,9 +762,7 @@ Optionally, add cusotm PROMPT." (defun gnosis-suspended-p (id) "Return t if note with ID is suspended." - (if (= (gnosis-get 'suspend 'review-log `(= id ,id)) 1) - t - nil)) + (= (gnosis-get 'suspend 'review-log `(= id ,id)) 1)) (defun gnosis-get-deck-due-notes (&optional deck-id) "Return due notes for deck, with value of DECK-ID. @@ -788,7 +790,7 @@ DATE is a list of the form (year month day)." (cl-mapcan (lambda (note-id) (gnosis-get-note-tags note-id)) due-notes) - :test 'equal))) + :test #'equal))) (cl-defun gnosis-tag-prompt (&key (prompt "Selected tags") (match nil) (due nil)) @@ -816,10 +818,8 @@ Returns a list of unique tags." "Check if note with value of NOTE-ID for id is due for review. Check if it's suspended, and if it's due today." - (if (and (not (gnosis-suspended-p note-id)) - (gnosis-review-is-due-today-p note-id)) - t - nil)) + (and (not (gnosis-suspended-p note-id)) + (gnosis-review-is-due-today-p note-id))) (defun gnosis-review-is-due-today-p (id) "Return t if note with ID is due today. @@ -867,7 +867,7 @@ Returns a list of the form ((yyyy mm dd) ef)." (cl-mapcan (lambda (note-id) (gnosis-get-note-tags note-id)) due-notes) - :test 'equal))) + :test #'equal))) (defun gnosis-review--get-offset (id) "Return offset for note with value of id ID." @@ -875,9 +875,9 @@ Returns a list of the form ((yyyy mm dd) ef)." (gnosis-algorithm-date-diff last-rev))) (defun gnosis-review-round (num) - "Round NUM to 1 decimal. + "Round NUM to 2 decimals. -This function is used to round floating point numbers to 1 decimal, +This function is used to round floating point numbers to 2 decimals, such as the easiness factor (ef)." (/ (round (* num 100.00)) 100.00)) @@ -917,8 +917,9 @@ SUCCESS is a binary value, 1 is for successful review." (answer (nth (- (gnosis-get 'answer 'notes `(= id ,id)) 1) choices)) (user-choice (gnosis-mcq-answer id))) (if (string= answer user-choice) - (progn (gnosis-review--update id 1) - (message "Correct!")) + (progn + (gnosis-review--update id 1) + (message "Correct!")) (gnosis-review--update id 0) (message "False")) (gnosis-display-correct-answer-mcq answer user-choice) @@ -997,18 +998,17 @@ Used to reveal all clozes left with `gnosis-face-cloze-unanswered' face." (defun gnosis-review-note (id) "Start review for note with value of id ID, if note is unsuspended." - (cond ((gnosis-suspended-p id) - (message "Note is suspended.")) - (t - (with-current-buffer (switch-to-buffer (get-buffer-create "*gnosis*")) - (let ((type (gnosis-get 'type 'notes `(= id ,id)))) - (gnosis-mode) - (pcase type - ("mcq" (gnosis-review-mcq id)) - ("basic" (gnosis-review-basic id)) - ("cloze" (gnosis-review-cloze id)) - ("y-or-n" (gnosis-review-y-or-n id)) - (_ (error "Malformed note type")))))))) + (when (gnosis-suspended-p id) + (message "Suspended note with id: %s" id) + (sit-for 0.3)) ;; this should only occur in testing/dev cases + (let* ((type (gnosis-get 'type 'notes `(= id ,id))) + (func-name (intern (format "gnosis-review-%s" (downcase type))))) + (if (fboundp func-name) + (progn + (with-current-buffer (switch-to-buffer (get-buffer-create "*gnosis*")) + (gnosis-mode) + (funcall func-name id))) + (error "Malformed note type: '%s'" type)))) (defun gnosis-review-commit (note-num) "Commit review session on git repository. @@ -1022,9 +1022,9 @@ NOTE-NUM: The number of notes reviewed in the session." (default-directory gnosis-dir)) (unless git (error "Git not found, please install git")) - (unless (file-exists-p (concat (file-name-as-directory gnosis-dir) ".git")) - (shell-command "git init")) - (sit-for 0.2) ;; wait for shell command to finish + (unless (file-exists-p (expand-file-name ".git" gnosis-dir)) + (vc-create-repo 'Git)) + ;; TODO: Redo this using vc (shell-command (concat git " add " (shell-quote-argument "gnosis.db"))) (shell-command (concat git " commit -m " (shell-quote-argument (concat (format "Total notes for session: %d " note-num))))) @@ -1039,13 +1039,18 @@ NOTE-NUM: The number of notes reviewed in the session." (cl-loop for note in notes do (gnosis-review-note note) (setf note-count (1+ note-count)) - (pcase (read-char-choice "Note Action: [n]ext, [s]uspend, [e]dit, [q]uit: " '(?n ?s ?e ?q)) + (pcase (car (read-multiple-choice + "Note actions" + '((?n "next") + (?s "suspend") + (?e "edit") + (?q "quit")))) (?n nil) (?s (gnosis-suspend-note note)) - (?e (progn (gnosis-edit-note note) - (recursive-edit))) - (?q (progn (gnosis-review-commit note-count) - (cl-return)))) + (?e (gnosis-edit-note note) + (recursive-edit)) + (?q (gnosis-review-commit note-count) + (cl-return))) finally (gnosis-review-commit note-count)))))) @@ -1119,7 +1124,7 @@ changes." (extra-notes ,extra-notes) (image ,image) (second-image ,second-image)) - do (cond ((equal field 'id) + do (cond ((eq field 'id) (insert (format (concat ":%s " (propertize "%s" 'read-only t) "\n") field value))) ((numberp value) (insert (format ":%s %s\n" field value))) @@ -1127,7 +1132,7 @@ changes." (not (equal value nil))) (insert (format ":%s '%s\n" field (format "%s" (cl-loop for item in value collect (format "\"%s\"" item)))))) - ((equal value nil) + ((null value) (insert (format ":%s %s\n" field 'nil))) (t (insert (format ":%s \"%s\"\n" field value))))) (delete-char -1) ;; delete extra line @@ -1149,7 +1154,7 @@ changes." (define-derived-mode gnosis-edit-mode emacs-lisp-mode "Gnosis EDIT" "Gnosis Edit Mode." :interactive t - :lighter " gnosis-edit-mode" + :lighter " Gnosis Edit" :keymap gnosis-edit-mode-map) @@ -1175,7 +1180,7 @@ SECOND-IMAGE: Image to display after user-input" (image . ,image) (second-image . ,second-image)) when value - do (cond ((member field '(extra-notes image second-image)) + do (cond ((memq field '(extra-notes image second-image)) (gnosis-update 'extras `(= ,field ,value) `(= id ,id))) ((listp value) (gnosis-update 'notes `(= ,field ',value) `(= id ,id))) @@ -1377,7 +1382,7 @@ review." ;; Make sure gnosis-db is initialized (setf gnosis-db (emacsql-sqlite (concat (file-name-as-directory gnosis-dir) "gnosis.db")))) ;; Create database tables - (unless (= (length (emacsql gnosis-db [:select name :from sqlite-master :where (= type table)])) 6) + (unless (length= (emacsql gnosis-db [:select name :from sqlite-master :where (= type table)]) 6) ;; Enable foreign keys (emacsql gnosis-db "PRAGMA foreign_keys = ON") ;; Gnosis version |