summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThanos Apollo <[email protected]>2025-01-03 20:08:49 +0200
committerThanos Apollo <[email protected]>2025-01-03 20:08:49 +0200
commit6d174e37695b2928954eaa4e1a9d73bc0ba24be1 (patch)
tree131d92d544b259ad7d168591c038eb97b33ba52e
parent1ec9e74c7ecdb085b9c92b5c742f27ee800d8a14 (diff)
Rewrite review & editing of themas.
-rw-r--r--gnosis.el642
1 files changed, 392 insertions, 250 deletions
diff --git a/gnosis.el b/gnosis.el
index dc89b86..7fc2c06 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -873,7 +873,7 @@ SUSPEND: whether to suspend not"
(cl-assert (listp images) nil "Images must be a list of string paths")
(cl-assert (listp tags) nil "Tags value must be a list of tags as strings")
(cl-assert (or (= suspend 1) (= suspend 0)) nil "Suspend value must be either 0 or 1")
- (gnosis-add-note-fields deck "mc-cloze" question options answer extra tags (or suspend 0)
+ (gnosis-add-thema-fields deck "mc-cloze" question options answer extra tags (or suspend 0)
(car images) (cdr images)))
(defun gnosis-add-note-mc-cloze (deck)
@@ -1277,45 +1277,31 @@ SUCCESS is a boolean value, t for success, nil for failure."
(setf gnosis-due-notes-total (length (gnosis-review-get-due-notes))))
(defun gnosis-review-mcq (id)
- "Display multiple choice answers for question ID."
- (gnosis-display-question id)
- (gnosis-display-image id)
- (when gnosis-mcq-display-choices
- (gnosis-display-mcq-options id))
- (let* ((choices (gnosis-get 'options 'notes `(= id ,id)))
- (answer (nth (- (gnosis-get 'answer 'notes `(= id ,id)) 1) choices))
+ "Review MCQ thema with ID."
+ (gnosis-display-keimenon (gnosis-get 'keimenon 'notes `(= id ,id)))
+ (let* ((answer (car (gnosis-get 'apocalypse 'notes `(= id ,id))))
(user-choice (gnosis-mcq-answer id))
(success (string= answer user-choice)))
(gnosis-display-correct-answer-mcq answer user-choice)
- (gnosis-display-extra id)
+ (gnosis-display-parathema (gnosis-get 'parathema 'extras '(= id ,id)))
(gnosis-display-next-review id success)
success))
(defun gnosis-review-basic (id)
"Review basic type note for ID."
- (gnosis-display-question id)
- (gnosis-display-image id)
- (gnosis-display-hint (gnosis-get 'options 'notes `(= id ,id)))
- (let* ((answer (gnosis-get 'answer 'notes `(= id ,id)))
- (user-input (read-string "Answer: "))
- (success (gnosis-compare-strings answer user-input)))
- (gnosis-display-basic-answer answer success user-input)
- (gnosis-display-extra id)
- (gnosis-display-next-review id success)
- success))
-
-(defun gnosis-review-y-or-n (id)
- "Review y-or-n type note for ID."
- (gnosis-display-question id)
- (gnosis-display-image id)
- (gnosis-display-hint (gnosis-get 'options 'notes `(= id ,id)))
- (let* ((answer (gnosis-get 'answer 'notes `(= id ,id)))
- (user-input (read-char-choice "[y]es or [n]o: " '(?y ?n)))
- (success (equal answer user-input)))
- (gnosis-display-y-or-n-answer :answer answer :success success)
- (gnosis-display-extra id)
- (gnosis-display-next-review id success)
- success))
+ (let* ((hypothesis (car (gnosis-get 'hypothesis 'notes `(= id ,id))))
+ (parathema (gnosis-get 'parathema 'extras `(= id ,id)))
+ (keimenon (gnosis-get 'keimenon 'notes `(= id ,id)))
+ (apocalypse (car (gnosis-get 'apocalypse 'notes `(= id ,id)))))
+ (gnosis-display-keimenon keimenon)
+ (gnosis-display-hint hypothesis)
+ (let* ((answer apocalypse)
+ (user-input (read-string "Answer: "))
+ (success (gnosis-compare-strings answer user-input)))
+ (gnosis-display-basic-answer answer success user-input)
+ (gnosis-display-parathema parathema)
+ (gnosis-display-next-review id success)
+ success)))
(defun gnosis-review-cloze--input (cloze)
"Prompt for user input during cloze review.
@@ -1326,10 +1312,11 @@ If user-input is equal to CLOZE, return t."
(defun gnosis-review-cloze (id)
"Review cloze type note for ID."
- (let* ((main (gnosis-get 'main 'notes `(= id ,id)))
- (clozes (gnosis-get 'answer 'notes `(= id ,id)))
+ (let* ((main (gnosis-get 'keimenon 'notes `(= id ,id)))
+ (clozes (gnosis-get 'apocalypse 'notes `(= id ,id)))
(num 0) ;; Number of clozes revealed
- (hints (gnosis-get 'options 'notes `(= id ,id)))
+ (hints (gnosis-get 'hypothesis 'notes `(= id ,id)))
+ (parathema (gnosis-get 'parathema 'extras `(= id ,id)))
(success nil))
;; Quick fix for old cloze note versions.
(cond ((and (stringp hints) (string-empty-p hints))
@@ -1356,19 +1343,18 @@ If user-input is equal to CLOZE, return t."
(cl-return)))
;; Update note after all clozes are revealed successfully
finally (setq success t))
- (gnosis-display-extra id)
+ (gnosis-display-parathema parathema)
(gnosis-display-next-review id success)
success))
(defun gnosis-review-mc-cloze (id)
"Review MC-CLOZE note of ID."
- (let ((main (gnosis-get 'main 'notes `(= id ,id)))
+ (let ((main (gnosis-get 'keimenon 'notes `(= id ,id)))
;; Cloze needs to be a list, we take car as the answer
- (cloze (list (gnosis-get 'answer 'notes `(= id ,id))))
+ (cloze (list (gnosis-get 'hypothesis 'notes `(= id ,id))))
(user-choice nil)
(success nil))
(gnosis-display-cloze-string main cloze nil nil nil)
- (gnosis-display-image id)
(setf user-choice (gnosis-mcq-answer id)
success (string= user-choice (car cloze)))
(if success
@@ -1436,7 +1422,7 @@ If NEW? is non-nil, increment new notes log by 1."
;; Fix sync by adding a small delay, `vc-pull' is async.
(sit-for 0.3)
;; Reopen gnosis-db after pull
- (setf gnosis-db (emacsql-sqlite-open (expand-file-name "gnosis.db" dir)))))
+ (setf gnosis-db gnosis-db)))
(defun gnosis-review-commit (note-num)
"Commit review session on git repository.
@@ -1465,8 +1451,8 @@ editing NOTE with it's new contents.
After done editing, call `gnosis-review-actions' with SUCCESS NOTE
NOTE-COUNT."
- (gnosis-edit-save-exit)
- (gnosis-edit-note note)
+ (gnosis-edit-thema note)
+ (setf gnosis-review-editing-p t)
(recursive-edit)
(gnosis-review-actions success note note-count))
@@ -1498,6 +1484,13 @@ be called with new SUCCESS value plus NOTE & NOTE-COUNT."
(gnosis-display-next-review note success)
(gnosis-review-actions success note note-count))
+;; (defun gnosis-review-action--read (id)
+;; "Open link for note at extras."
+;; (let* ((extras (gnosis-get 'extra-notes 'extras `(= id ,id)))
+;; (ids (gnosis-extract-id-links extras))
+;; ())
+;; )
+
(defun gnosis-review-actions (success note note-count)
"Specify action during review of note.
@@ -1508,7 +1501,7 @@ NOTE-COUNT: Total notes reviewed
To customize the keybindings, adjust `gnosis-review-keybindings'."
(let* ((choice
(read-char-choice
- (format "Action: %sext gnosis, %sverride result, %suspend note, %sdit note, %suit review session"
+ (format "Action: %sext gnosis, %sverride result, %suspend note, %sdit note, %suit"
(propertize "n" 'face 'gnosis-face-review-action-next)
(propertize "o" 'face 'gnosis-face-review-action-override)
(propertize "s" 'face 'gnosis-face-review-action-suspend)
@@ -1551,176 +1544,284 @@ NOTE-COUNT: Total notes to be commited for session."
;; Refresh modeline
(setq gnosis-due-notes-total (length (gnosis-review-get-due-notes)))
;; Select review type
- (let ((review-type (gnosis-completing-read "Review: " '("Due notes"
- "Due notes of deck"
- "Due notes of specified tag(s)"
- "Overdue notes"
- "Due notes (Without Overdue)"
- "All notes of deck"
- "All notes of tag(s)"))))
+ (let ((review-type
+ (gnosis-completing-read "Review: "
+ '("Due notes"
+ "Due notes of deck"
+ "Due notes of specified tag(s)"
+ "Overdue notes"
+ "Due notes (Without Overdue)"
+ "All notes of deck"
+ "All notes of tag(s)"))))
(pcase review-type
("Due notes" (gnosis-review-session (gnosis-collect-note-ids :due t) t))
("Due notes of deck" (gnosis-review-session
(gnosis-collect-note-ids :due t :deck (gnosis--get-deck-id))))
- ("Due notes of specified tag(s)" (gnosis-review-session (gnosis-collect-note-ids :due t :tags t)))
+ ("Due notes of specified tag(s)" (gnosis-review-session
+ (gnosis-collect-note-ids :due t :tags t)))
("Overdue notes" (gnosis-review-session (gnosis-review-get-overdue-notes)))
- ("Due notes (Without Overdue)" (gnosis-review-session (gnosis-review-get-due-notes--no-overdue)))
- ("All notes of deck" (gnosis-review-session (gnosis-collect-note-ids :deck (gnosis--get-deck-id))))
+ ("Due notes (Without Overdue)" (gnosis-review-session
+ (gnosis-review-get-due-notes--no-overdue)))
+ ("All notes of deck" (gnosis-review-session
+ (gnosis-collect-note-ids :deck (gnosis--get-deck-id))))
("All notes of tag(s)" (gnosis-review-session (gnosis-collect-note-ids :tags t))))))
+(defun gnosis-add-thema-fields (deck-id type keimenon hypothesis apocalypse parathema tags suspend links)
+ "Insert fields for new note.
+
+DECK-ID: Deck ID for new thema.
+TYPE: Note type e.g \"mcq\"
+KEIMENON: Note's keimenon
+HYPOTHESIS: Thema hypothesis, e.g choices for mcq for OR hints for
+cloze/basic thema
+APOCALYPSE: Correct answer for note, for MCQ is an integer while for
+cloze/basic a string/list of the right answer(s)
+PARATHEMA: Parathema information to display after the apocalypse
+TAGS: Tags to organize notes
+SUSPEND: Integer value of 1 or 0, where 1 suspends the card.
+LINKS: List of id links."
+ (cl-assert (integerp deck-id) nil "Deck ID must be an integer")
+ (cl-assert (stringp type) nil "Type must be a string")
+ (cl-assert (stringp keimenon) nil "Keimenon must be a string")
+ (cl-assert (listp hypothesis) nil "Hypothesis value must be a list")
+ (cl-assert (listp apocalypse) nil "Apocalypse value must be a list")
+ (cl-assert (stringp parathema) nil "Parathema must be a string")
+ (cl-assert (listp tags) nil "Tags must be a list")
+ (cl-assert (listp links) nil "Links must be a list")
+ (let* ((note-id (gnosis-generate-id)))
+ (emacsql-with-transaction gnosis-db
+ ;; Refer to `gnosis-db-schema-SCHEMA' e.g `gnosis-db-schema-review-log'
+ (gnosis--insert-into 'notes `([,note-id ,(downcase type) ,keimenon ,hypothesis ,apocalypse ,tags ,deck-id]))
+ (gnosis--insert-into 'review `([,note-id ,gnosis-algorithm-gnosis-value
+ ,gnosis-algorithm-amnesia-value]))
+ (gnosis--insert-into 'review-log `([,note-id ,(gnosis-algorithm-date)
+ ,(gnosis-algorithm-date) 0 0 0 0 ,suspend 0]))
+ (gnosis--insert-into 'extras `([,note-id ,parathema]))
+ (cl-loop for link in links
+ do (gnosis--insert-into 'links `([,note-id ,link]))))))
+
+(defun gnosis-update-thema (id keimenon hypothesis apocalypse parathema tags links)
+ "Update thema entry for ID."
+ (let ((id (if (stringp id) (string-to-number id) id))) ;; Make sure we provided the id as a number.
+ (emacsql-with-transaction gnosis-db
+ (gnosis-update 'notes `(= keimenon ,keimenon) `(= id ,id))
+ (gnosis-update 'notes `(= hypothesis ',hypothesis) `(= id ,id))
+ (gnosis-update 'notes `(= apocalypse ',apocalypse) `(= id ,id))
+ (gnosis-update 'extras `(= parathema ,parathema) `(= id ,id))
+ (gnosis-update 'notes `(= tags ',tags) `(= id ,id))
+ (cl-loop for link in links
+ do (gnosis-update 'links `(= dest ,link) `(= source ,id))))))
+
+;;;;;;;;;;;;;;;;;;;;;; THEMA HELPERS ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; These functions provide assertions depending on the type of thema.
+;;
+;; Each thema should use a helper function that calls to provide
+;; assertions, such as length of hypothesis and apocalypse, for said
+;; thema.
+;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defun gnosis-add-thema--basic (id deck-id type keimenon hypothesis apocalypse parathema tags suspend links)
+ "Default format for adding a thema.
+
+DECK-ID: Integer value of deck-id.
+TYPE: String representing the type of note.
+KEIMENON: String for the thema text.
+HYPOTHESIS: List of a signle string.
+APOCALYPSE: List of a single string.
+PARATHEMA: String for the parathema text.
+TAGS: List of thema tags.
+SUSPEND: Integer value of 0 for nil and 1 for true (suspended).
+LINKS: List of id links in PARATHEMA."
+ (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+ (cl-assert (stringp type) nil "Type must be an integer.")
+ (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+ (cl-assert (or (null hypothesis)
+ (and (listp hypothesis)
+ (= (length hypothesis) 1)))
+ nil "Hypothesis value must be a list of a single item or nil.")
+ (cl-assert (and (listp apocalypse)
+ (= (length apocalypse) 1))
+ nil "Apocalypse value must be a list of a signle item")
+ (cl-assert (listp tags) nil "Tags must be a list.")
+ (cl-assert (or (= suspend 0)
+ (= suspend 1))
+ nil "Suspend value must either 0 or 1")
+ (cl-assert (listp links) nil "Links must be a list")
+ (if (equal id "NEW")
+ (gnosis-add-thema-fields deck-id type keimenon (or hypothesis (list "")) apocalypse parathema tags suspend links)
+ (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags links)))
+
+(defun gnosis-add-thema--double (id deck-id type keimenon hypothesis apocalypse parathema tags suspend links)
+ "Double thema format.
+
+Changes TYPE to basic & inserts a second basic thema with APOCALYPSE
+and KEIMENON reversed."
+ (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+ (cl-assert (stringp type) nil "Type must be an integer.")
+ (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+ (cl-assert (listp hypothesis) nil "Hypothesis value must be a list.")
+ (cl-assert (and (listp apocalypse) (= (length apocalypse) 1))
+ nil "Apocalypse value must be a list of a signle item")
+ (cl-assert (listp tags) nil "Tags must be a list.")
+ (cl-assert (or (= suspend 0) (= suspend 1)) nil "Suspend value must either 0 or 1")
+ (cl-assert (listp links) nil "Links must be a list")
+ ;; Change type to basic
+ (let ((type "basic")
+ (hypothesis (or hypothesis (list ""))))
+ (if (equal id "NEW")
+ (progn
+ (gnosis-add-thema-fields deck-id type keimenon hypothesis apocalypse parathema tags suspend links)
+ (gnosis-add-thema-fields deck-id type (car apocalypse) hypothesis (list keimenon) parathema tags suspend links))
+ ;; There should not be a double type thema in database to
+ ;; update. This is used for testing purposes.
+ (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags links))))
+
+(defun gnosis-add-thema--mcq (id deck-id type keimenon hypothesis apocalypse parathema tags suspend links)
+ "Default format for adding a thema.
+
+ID: Thema ID, either an integer value or NEW.
+DECK-ID: Integer value of deck-id.
+TYPE: String representing the type of note.
+KEIMENON: String for the thema text.
+HYPOTHESIS: List of a signle string.
+APOCALYPSE: List of a single string.
+PARATHEMA: String for the parathema text.
+TAGS: List of thema tags.
+SUSPEND: Integer value of 0 for nil and 1 for true (suspended).
+LINKS: List of id links in PARATHEMA."
+ (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+ (cl-assert (stringp type) nil "Type must be an integer.")
+ (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+ (cl-assert (and (listp hypothesis)
+ (> (length hypothesis) 1))
+ nil "Hypothesis value must be a list greater than 1 item.")
+ (cl-assert (and (listp apocalypse)
+ (= (length apocalypse) 1)
+ (member (car apocalypse) hypothesis))
+ nil "Apocalypse value must be a single item, member of the Hypothesis")
+ (cl-assert (listp tags) nil "Tags must be a list.")
+ (cl-assert (or (= suspend 0)
+ (= suspend 1))
+ nil "Suspend value must either 0 or 1")
+ (cl-assert (listp links) nil "Links must be a list")
+ (if (equal id "NEW")
+ (gnosis-add-thema-fields deck-id type keimenon (or hypothesis (list "")) apocalypse parathema tags suspend links)
+ (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags links)))
+
+(defun gnosis-add-thema--cloze (id deck-id type keimenon hypothesis apocalypse parathema tags suspend links)
+ "Add cloze type thema."
+ (cl-assert (integerp deck-id) nil "Deck-id value must be an integer.")
+ (cl-assert (stringp type) nil "Type must be an integer.")
+ (cl-assert (stringp keimenon) nil "Keimenon must be an integer.")
+ (cl-assert (or (= (length apocalypse) (length hypothesis))
+ (null hypothesis))
+ nil "Hypothesis value must be a list or nil, equal in length of Apocalypse.")
+ (cl-assert (listp apocalypse) nil "Apocalypse value must be a list.")
+ (cl-assert (listp tags) nil "Tags must be a list.")
+ (cl-assert (or (= suspend 0)
+ (= suspend 1))
+ nil "Suspend value must either 0 or 1")
+ (cl-assert (listp links) nil "Links must be a list")
+ (cl-assert (gnosis-cloze-check keimenon apocalypse) nil "Clozes (apocalypse) values are not part of keimenon")
+ (if (equal id "NEW")
+ (gnosis-add-thema-fields deck-id type keimenon (or hypothesis (list "")) apocalypse parathema tags suspend links)
+ (gnosis-update-thema id keimenon hypothesis apocalypse parathema tags links)))
+
+(defun gnosis-save-thema (thema deck)
+ "Save THEMA for DECK."
+ (let* ((id (nth 0 thema))
+ (type (nth 1 thema))
+ (keimenon (nth 2 thema))
+ (hypothesis (and (nth 3 thema) (mapcar (lambda (item) (string-remove-prefix "- " item))
+ (split-string (nth 3 thema) gnosis-org-separator))))
+ (apocalypse (and (nth 4 thema) (mapcar (lambda (item)
+ "Replace `gnosis-org-separator'."
+ (string-remove-prefix "- " item))
+ (split-string (nth 4 thema) gnosis-org-separator))))
+ (parathema (or (nth 5 thema) ""))
+ (tags (nth 6 thema))
+ (links (gnosis-extract-id-links parathema))
+ (thema-func (cdr (assoc (downcase type)
+ (mapcar (lambda (pair) (cons (downcase (car pair))
+ (cdr pair)))
+ gnosis-thema-types)))))
+ ;; (message "asdfs")
+ (funcall thema-func id deck type keimenon hypothesis apocalypse parathema tags 0 links)))
-;; Editing notes
-(defun gnosis-edit-read-only-values (&rest values)
- "Make the provided VALUES read-only in the whole buffer."
- (goto-char (point-min))
- (dolist (value values)
- (while (search-forward value nil t)
- (put-text-property (match-beginning 0) (match-end 0) 'read-only t)))
- (goto-char (point-min)))
-
-(cl-defun gnosis-edit-note (id)
- "Edit the contents of a note with the given ID.
-
-This function creates an Emacs Lisp buffer named *gnosis-edit* on the
-same window and populates it with the values of the note identified by
-the specified ID using `gnosis-export-note'. The note values are
-inserted as keywords for the `gnosis-edit-update-note' function.
-
-To make changes, edit the values in the buffer, and then evaluate the
-`gnosis-edit-update-note' expression to save the changes.
-
-RECURSIVE-EDIT: If t, exit `recursive-edit' after finishing editing.
-It should only be t when starting a recursive edit, when editing a
-note during a review session.
-
-The buffer automatically indents the expressions for readability.
-After finishing editing, evaluate the entire expression to apply the
-changes."
- (pop-to-buffer-same-window (get-buffer-create "*gnosis-edit*"))
- (gnosis-edit-mode)
- (erase-buffer)
- (insert ";;\n;; You are editing a gnosis note.\n\n")
- (insert "(gnosis-edit-update-note ")
- (gnosis-export-note id)
- (insert ")")
- (insert "\n\n;; After finishing editing, save changes with `<C-c> <C-c>'\n;; Avoid exiting without saving.")
- (indent-region (point-min) (point-max))
- ;; Insert id & fields as read-only values
- (gnosis-edit-read-only-values (format ":id %s" id) ":main" ":options" ":answer"
- ":tags" ":extra-notes" ":image" ":second-image"
- ":gnosis" ":amensia" ":suspend")
- (local-set-key (kbd "C-c C-c") (lambda () (interactive) (gnosis-edit-note-save-exit))))
-
-(defun gnosis-assert-int-or-nil (value description)
- "Assert that VALUE is an integer or nil.
-
-DESCRIPTION is a string that describes the value."
- (unless (or (null value) (integerp value))
- (error "Invalid value: %s, %s" value description)))
-
-(defun gnosis-assert-float-or-nil (value description &optional less-than-1)
- "Assert that VALUE is a float or nil.
-
-DESCRIPTION is a string that describes the value.
-LESS-THAN-1: If t, assert that VALUE is a float less than 1."
- (if less-than-1
- (unless (or (null value) (and (floatp value) (< value 1)))
- (error "Invalid value: %s, %s" value description))
- (unless (or (null value) (floatp value))
- (error "Invalid value: %s, %s" value description))))
-
-(defun gnosis-assert-number-or-nil (value description)
- "Assert that VALUE is a number or nil.
-
-DESCRIPTION is a string that describes the value."
- (unless (or (null value) (numberp value))
- (error "Invalid value: %s, %s" value description)))
-
-(cl-defun gnosis-edit-save-exit ()
- "Save edits and exit using EXIT-FUNC, with ARGS."
+;;;###autoload
+(defun gnosis-add-thema (deck type &optional keimenon hypothesis apocalypse parathema tags example)
+ "Add thema with TYPE in DECK."
+ (interactive (list
+ (gnosis--get-deck-name)
+ (downcase (completing-read "Select type: " gnosis-thema-types))))
+ (pop-to-buffer "*Gnosis NEW*")
+ (with-current-buffer "*Gnosis NEW*"
+ (let ((inhibit-read-only 1))
+ (erase-buffer))
+ (insert "#+DECK: " deck)
+ (gnosis-edit-mode)
+ (gnosis-org--insert-thema "NEW" type keimenon hypothesis apocalypse parathema tags example))
+ (search-backward "keimenon")
+ (forward-line))
+
+(defun gnosis-export-note (id)
+ "Export note with ID."
+ (let ((note-data (append (gnosis-select '[type keimenon hypothesis apocalypse tags] 'notes `(= id ,id) t)
+ (gnosis-select 'parathema 'extras `(= id ,id) t))))
+ (gnosis-org--insert-thema (number-to-string id)
+ (nth 0 note-data)
+ (nth 1 note-data)
+ (concat (string-remove-prefix "\n" gnosis-org-separator)
+ (mapconcat 'identity (nth 2 note-data) gnosis-org-separator))
+ (concat (string-remove-prefix "\n" gnosis-org-separator)
+ (mapconcat 'identity (nth 3 note-data) gnosis-org-separator))
+ (nth 5 note-data)
+ (nth 4 note-data))))
+
+(defun gnosis-edit-thema (id)
+ "Edit note with ID."
+ (with-current-buffer (pop-to-buffer "*Gnosis Edit*")
+ (let ((inhibit-read-only 1)
+ (deck-name (gnosis--get-deck-name
+ (gnosis-get 'deck-id 'notes `(= id ,id)))))
+ (erase-buffer)
+ (insert "#+DECK: " deck-name))
+ (gnosis-edit-mode)
+ (gnosis-export-note id)
+ (search-backward "keimenon")
+ (forward-line)))
+
+(defun gnosis-save ()
+ "Save themas in current buffer."
(interactive)
- (when (get-buffer "*gnosis-edit*")
- (switch-to-buffer "*gnosis-edit*")
- (eval-buffer)
- (quit-window t)
- (gnosis-dashboard-return)))
-
-(cl-defun gnosis-edit-note-save-exit ()
- "Save edits and exit using EXIT-FUNC, with ARGS."
+ (let ((themas (gnosis-org-parse-themas))
+ (deck (gnosis--get-deck-id (gnosis-org-parse--deck-name))))
+ (cl-loop for thema in themas
+ do (gnosis-save-thema thema deck))
+ (gnosis-edit-quit)))
+
+(defun gnosis-edit-quit ()
+ "Quit recrusive edit & kill current buffer."
(interactive)
- (when (get-buffer "*gnosis-edit*")
- (switch-to-buffer "*gnosis-edit*")
- (eval-buffer)
- (quit-window t)
+ (kill-buffer-and-window)
+ (when gnosis-review-editing-p
+ (setf gnosis-review-editing-p nil)
(exit-recursive-edit)))
(defvar-keymap gnosis-edit-mode-map
- :doc "gnosis-edit keymap"
- "C-c C-c" #'gnosis-edit-save-exit)
+ :doc "gnosis org mode map"
+ "C-c C-c" #'gnosis-save
+ "C-c C-k" #'gnosis-edit-quit)
-(define-derived-mode gnosis-edit-mode emacs-lisp-mode "Gnosis EDIT"
- "Gnosis Edit Mode."
+(define-derived-mode gnosis-edit-mode org-mode "Gnosis Org"
+ "Gnosis Org Mode."
:interactive nil
:lighter " Gnosis Edit"
- :keymap gnosis-edit-mode-map)
-
-(cl-defun gnosis-edit-update-note (&key id main options answer tags (extra-notes nil) (image nil)
- (second-image nil) gnosis amnesia suspend)
- "Update note with id value of ID.
-
-ID: Note id
-MAIN: Main part of note, the stem part of MCQ, question for basic, etc.
-OPTIONS: Options for mcq type notes/Hint for basic & cloze type notes
-ANSWER: Answer for MAIN
-TAGS: Tags for note, used to organize & differentiate between notes
-EXTRA-NOTES: Notes to display after user-input
-IMAGE: Image to display before user-input
-SECOND-IMAGE: Image to display after user-input
-GNOSIS: Gnosis score
-AMNESIA: Amnesia value
-SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
- (cl-assert (stringp main) nil "Main must be a string")
- (cl-assert (or (stringp image) (null image)) nil
- "Image must be a string, path to image file from `gnosis-images-dir', or nil")
- (cl-assert (or (stringp second-image) (null second-image)) nil
- "Second-image must be a string, path to image file from `gnosis-images-dir', or nil")
- (cl-assert (or (stringp extra-notes) (null extra-notes)) nil
- "Extra-notes must be a string, or nil")
- (cl-assert (and (listp tags) (cl-every #'stringp tags)) nil "Tags must be a list of strings")
- (cl-assert (and (listp gnosis) (length= gnosis 3) (cl-every #'floatp gnosis))
- nil "gnosis must be a list of 3 floats")
- (cl-assert (or (stringp options) (and (listp options) (cl-every #'(lambda (x) (or (stringp x) (null x)))
- options)))
- nil "Options must be a string or a list of strings")
- (cl-assert (and (numberp suspend) (or (= suspend 0) (= suspend 1))) nil "Suspend must be either 0 or 1")
- (when (and (string= (gnosis-get-type id) "cloze")
- (not (stringp options)))
- (cl-assert (or (listp options) (stringp options)) nil "Options must be a list or a string.")
- (cl-assert (gnosis-cloze-check main answer) nil "Clozes are not part of the question (main).")
- (cl-assert (>= (length answer) (length options)) nil
- "Hints (options) must be equal or less than clozes (answer).")
- (cl-assert (cl-every (lambda (item) (or (null item) (stringp item))) options) nil "Hints (options) must be either nil or a string."))
- ;; Construct the update clause for the emacsql update statement.
- (cl-loop for (field . value) in `((main . ,main)
- (options . ,options)
- (answer . ,answer)
- (tags . ,tags)
- (extra-notes . ,extra-notes)
- (images . ,image)
- (extra-image . ,second-image)
- (gnosis . ',gnosis)
- (amnesia . ,amnesia)
- (suspend . ,suspend))
- when value
- do (cond ((memq field '(extra-notes images extra-image))
- (gnosis-update 'extras `(= ,field ,value) `(= id ,id)))
- ((memq field '(gnosis amnesia))
- (gnosis-update 'review `(= ,field ,value) `(= id ,id)))
- ((eq field 'suspend)
- (gnosis-update 'review-log `(= ,field ,value) `(= id ,id)))
- ((listp value)
- (gnosis-update 'notes `(= ,field ',value) `(= id ,id)))
- (t (gnosis-update 'notes `(= ,field ,value) `(= id ,id))))))
+ :keymap gnosis-edit-mode-map
+ (setq header-line-format (format " Save thema by running %s or %s to quit"
+ (propertize "C-c C-c" 'face 'help-key-binding)
+ (propertize "C-c C-k" 'face 'help-key-binding))))
(defun gnosis-validate-custom-values (new-value)
"Validate the structure and values of NEW-VALUE for gnosis-custom-values."
@@ -2217,12 +2318,14 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
(erase-buffer)
(gnosis-animate-string "Welcome to the Gnosis demo!" 2 nil "Gnosis demo" 'underline)
(sit-for 1)
- (gnosis-animate-string "Gnosis is a tool designed to create a gnostikon" 3 nil "gnostikon" 'bold-italic)
+ (gnosis-animate-string "Gnosis is a tool designed to create a gnosiotheke"
+ 3 nil "gnosiotheke" 'bold-italic)
(sit-for 1.5)
(gnosis-animate-string "--A place to store & test your knowledge--" 4 nil nil 'italic)
(sit-for 1)
- (gnosis-animate-string "The objective of gnosis is to maximize memory retention, through repetition." 6 nil
- "maximize memory retention" 'underline)
+ (gnosis-animate-string
+ "The objective of gnosis is to maximize memory retention, through repetition." 6 nil
+ "maximize memory retention" 'underline)
(sit-for 1)
(gnosis-animate-string "Remember, repetitio est mater memoriae" 8 nil
"repetitio est mater memoriae" 'bold-italic)
@@ -2230,7 +2333,8 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
(gnosis-animate-string "-- repetition is the mother of memory --" 9 nil
"repetition is the mother of memory" 'italic)
(sit-for 1)
- (gnosis-animate-string "Consistency is key; be sure to do your daily reviews!" 11 nil "Consistency is key" 'bold)
+ (gnosis-animate-string "Consistency is key; be sure to do your daily reviews!"
+ 11 nil "Consistency is key" 'bold)
(sit-for 1)
(when (y-or-n-p "Try out demo gnosis review session?")
(gnosis-demo-create-deck)
@@ -2256,7 +2360,7 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
:tags note-tags)
(gnosis-add-note--mcq :deck deck-name
:question "Which one is the capital of Greece?"
- :choices '("Athens" "Sparta" "Rome" "Berlin")
+ :choices '("Athens" "Sparta" "Rome" "Constantinople")
:correct-answer 1
:extra "Athens (Ἀθήνα) is the largest city of Greece & one of the world's oldest cities, with it's recorded history spanning over 3,500 years."
:tags note-tags)
@@ -2318,47 +2422,39 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
(gnosis-dashboard-output-tags)))))
(defun gnosis-dashboard--streak (dates &optional num date)
- "Return current review streak.
+ "Return current review streak number as a string.
-DATES: Dates in the activity log.
+DATES: Dates in the activity log, a list of dates in (YYYY MM DD).
NUM: Streak number.
DATE: Integer, used with `gnosis-algorithm-date' to get previous dates."
(let ((num (or num 0))
(date (or date 0)))
- (if (member (gnosis-algorithm-date date) dates)
- (gnosis-dashboard--streak dates (cl-incf num) (- date 1))
- num)))
+ (cond ((> num 666)
+ "+666") ;; do not go over 666, avoiding `max-lisp-eval-depth'
+ ((member (gnosis-algorithm-date date) dates)
+ (gnosis-dashboard--streak dates (cl-incf num) (- date 1)))
+ (t (number-to-string num)))))
(defun gnosis-dashboard-output-average-rev ()
"Output the average daily notes reviewed for current year.
Skips days where no note was reviewed."
- (let ((reviews (gnosis-select 'reviewed-total 'activity-log '1=1 t)))
+ (let ((reviews (gnosis-select 'reviewed-total 'activity-log '(> reviewed-total 0) t)))
(if (null reviews) 0
(format "%.2f" (/ (apply '+ reviews) (float (length reviews)))))))
-(defun gnosis-dashboard-output-note (id)
- "Output contents for note with ID, formatted for gnosis dashboard."
- (cl-loop for item in (append (gnosis-select
- '[main options answer tags type] 'notes `(= id ,id) t)
- (gnosis-select 'suspend 'review-log `(= id ,id) t))
- if (listp item)
- collect (mapconcat #'identity item ",")
- else
- collect (replace-regexp-in-string "\n" " " (format "%s" item))))
-
-(defun gnosis-dashboard-edit-note (&optional id)
+(defun gnosis-dashboard-edit-note ()
"Edit note with ID."
(interactive)
- (let ((id (or id (string-to-number (tabulated-list-get-id)))))
- (gnosis-edit-note id)))
+ (let ((id (tabulated-list-get-id)))
+ (gnosis-edit-thema id)))
(defun gnosis-dashboard-suspend-note ()
"Suspend note."
(interactive)
(if gnosis-dashboard--selected-ids
(gnosis-dashboard-marked-suspend)
- (gnosis-suspend-note (string-to-number (tabulated-list-get-id)))
+ (gnosis-suspend-note (tabulated-list-get-id))
(gnosis-dashboard-output-notes gnosis-dashboard-note-ids)
(revert-buffer t t t)))
@@ -2367,7 +2463,7 @@ Skips days where no note was reviewed."
(interactive)
(if gnosis-dashboard--selected-ids
(gnosis-dashboard-marked-delete)
- (gnosis-delete-note (string-to-number (tabulated-list-get-id)))
+ (gnosis-delete-note (tabulated-list-get-id))
(gnosis-dashboard-output-notes gnosis-dashboard-note-ids)
(revert-buffer t t t)))
@@ -2381,8 +2477,8 @@ Skips days where no note was reviewed."
:doc "Keymap for notes dashboard."
"e" #'gnosis-dashboard-edit-note
"s" #'gnosis-dashboard-suspend-note
- "C-s" #'gnosis-dashboard-search-note
- "a" #'gnosis-add-note
+ "SPC" #'gnosis-dashboard-search-note
+ "a" #'gnosis-add-thema
"r" #'gnosis-dashboard-return
"g" #'gnosis-dashboard-return
"d" #'gnosis-dashboard-delete
@@ -2393,26 +2489,52 @@ Skips days where no note was reviewed."
"Minor mode for gnosis dashboard notes output."
:keymap gnosis-dashboard-notes-mode-map)
+(defun gnosis-dashboard--output-notes (note-ids)
+ "Output tabulated-list format for NOTE-IDS."
+ (cl-assert (listp note-ids))
+ (let ((entries (emacsql gnosis-db
+ `[:select
+ [notes:id notes:keimenon notes:hypothesis notes:apocalypse
+ notes:tags notes:type review-log:suspend]
+ :from notes
+ :join review-log :on (= notes:id review-log:id)
+ :where (in notes:id ,(vconcat note-ids))])))
+ (cl-loop for sublist in entries
+ collect
+ (list (car sublist)
+ (vconcat
+ (cl-loop for item in (cdr sublist)
+ if (listp item)
+ collect (mapconcat #'identity item ",")
+ else
+ collect (replace-regexp-in-string "\n" " " (format "%s" item))))))))
+
(defun gnosis-dashboard-output-notes (note-ids)
"Return NOTE-IDS contents on gnosis dashboard."
(cl-assert (listp note-ids) t "`note-ids' must be a list of note ids.")
(pop-to-buffer-same-window gnosis-dashboard-buffer-name)
(gnosis-dashboard-enable-mode)
(gnosis-dashboard-notes-mode)
- (setf tabulated-list-format `[("Main" ,(/ (window-width) 4) t)
- ("Options" ,(/ (window-width) 6) t)
- ("Answer" ,(/ (window-width) 6) t)
- ("Tags" ,(/ (window-width) 5) t)
- ("Type" ,(/ (window-width) 10) T)
- ("Suspend" ,(/ (window-width) 6) t)]
- tabulated-list-entries (cl-loop for id in note-ids
- for output = (gnosis-dashboard-output-note id)
- when output
- collect (list (number-to-string id) (vconcat output)))
- gnosis-dashboard-note-ids note-ids)
+ (setf tabulated-list-format `[("Keimenon" ,(/ (window-width) 4) t)
+ ("Hypothesis" ,(/ (window-width) 6) t)
+ ("Apocalypse" ,(/ (window-width) 6) t)
+ ("Tags" ,(/ (window-width) 5) t)
+ ("Type" ,(/ (window-width) 10) t)
+ ("Suspend" ,(/ (window-width) 6) t)]
+ gnosis-dashboard-note-ids note-ids
+ tabulated-list-entries nil)
+ (make-local-variable 'tabulated-list-entries)
(tabulated-list-init-header)
- (tabulated-list-print t)
- (setf gnosis-dashboard--current `(:type notes :ids ,note-ids)))
+ (let ((inhibit-read-only t))
+ (erase-buffer)
+ (insert (format "Loading %s notes..." (length note-ids))))
+ (run-with-timer 0.1 nil
+ (lambda ()
+ (let ((entries (gnosis-dashboard--output-notes note-ids)))
+ (with-current-buffer gnosis-dashboard-buffer-name
+ (setq tabulated-list-entries entries)
+ (tabulated-list-print t)
+ (setf gnosis-dashboard--current `(:type notes :ids ,note-ids)))))))
(defun gnosis-dashboard-deck-note-count (id)
"Return total note count for deck with ID."
@@ -2434,12 +2556,31 @@ Skips days where no note was reviewed."
(defun gnosis-dashboard-rename-tag (&optional tag new-tag )
"Rename TAG to NEW-TAG."
(interactive)
- (let ((new-tag (or new-tag (read-string "News tag name: ")))
+ (let ((new-tag (or new-tag (read-string "New tag name: ")))
(tag (or tag (tabulated-list-get-id))))
(cl-loop for note in (gnosis-get-tag-notes tag)
do (let* ((tags (car (gnosis-select '[tags] 'notes `(= id ,note) t)))
(new-tags (cl-substitute new-tag tag tags :test #'string-equal)))
- (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))))
+ (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))
+ ;; Update tags in database
+ (gnosis-tags--update (gnosis-tags-get-all))
+ ;; Output tags anew
+ (gnosis-dashboard-output-tags)))
+
+(defun gnosis-dashboard-delete-tag (&optional tag)
+ "Rename TAG to NEW-TAG."
+ (interactive)
+ (let ((tag (or tag (tabulated-list-get-id))))
+ (when (y-or-n-p (format "Delete tag %s?" (propertize tag 'face 'font-lock-keyword-face)))
+ (cl-loop for note in (gnosis-get-tag-notes tag)
+ do (let* ((tags (car (gnosis-select '[tags] 'notes `(= id ,note) t)))
+ (new-tags (remove tag tags)))
+ (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))
+ ;; Update tags in database
+ (gnosis-tags--update (gnosis-tags-get-all))
+ ;; Output tags anew
+ (gnosis-dashboard-output-tags))))
+
(defun gnosis-dashboard-rename-deck (&optional deck-id new-name)
"Rename deck where DECK-ID with NEW-NAME."
@@ -2469,6 +2610,7 @@ Skips days where no note was reviewed."
"e" #'gnosis-dashboard-rename-tag
"s" #'gnosis-dashboard-suspend-tag
"r" #'gnosis-dashboard-rename-tag
+ "d" #'gnosis-dashboard-delete-tag
"g" #'gnosis-dashboard-return)
(define-minor-mode gnosis-dashboard-tags-mode
@@ -2477,7 +2619,8 @@ Skips days where no note was reviewed."
(defun gnosis-dashboard-output-tags (&optional tags)
"Format gnosis dashboard with output of TAGS."
- (let ((tags (or tags (gnosis-get-tags--unique))))
+ (gnosis-tags-refresh) ;; Refresh tags
+ (let ((tags (or tags (gnosis-tags-get-all))))
(pop-to-buffer-same-window gnosis-dashboard-buffer-name)
(gnosis-dashboard-enable-mode)
(gnosis-dashboard-tags-mode)
@@ -2564,7 +2707,7 @@ When called with called with a prefix, unsuspend all notes of deck."
"q" #'quit-window
"h" #'gnosis-dashboard-menu
"r" #'gnosis-review
- "a" #'gnosis-add-note
+ "a" #'gnosis-add-thema
"A" #'gnosis-add-deck
"s" #'gnosis-dashboard-suffix-query
"n" #'(lambda () (interactive) (gnosis-dashboard-output-notes (gnosis-collect-note-ids)))
@@ -2630,7 +2773,8 @@ DASHBOARD-TYPE: either Notes or Decks to display the respective dashboard."
(setf gnosis-dashboard--selected-ids
(append gnosis-dashboard--selected-ids (list id)))
(overlay-put ov 'face 'highlight)
- (overlay-put ov 'gnosis-mark t))))
+ (overlay-put ov 'gnosis-mark t)))
+ (forward-line))
(message "No entry at point"))
(message "Not in a tabulated-list-mode"))))
@@ -2647,7 +2791,7 @@ DASHBOARD-TYPE: either Notes or Decks to display the respective dashboard."
(interactive)
(when (y-or-n-p "Delete selected notes?")
(cl-loop for note in gnosis-dashboard--selected-ids
- do (gnosis-delete-note (string-to-number note) t))
+ do (gnosis-delete-note note t))
(gnosis-dashboard-return)))
(defun gnosis-dashboard-marked-suspend ()
@@ -2655,7 +2799,7 @@ DASHBOARD-TYPE: either Notes or Decks to display the respective dashboard."
(interactive)
(when (y-or-n-p "Toggle SUSPEND on selected notes?")
(cl-loop for note in gnosis-dashboard--selected-ids
- do (gnosis-suspend-note (string-to-number note) t))
+ do (gnosis-suspend-note note t))
(gnosis-dashboard-return)))
(transient-define-suffix gnosis-dashboard-suffix-query (query)
@@ -2686,8 +2830,7 @@ DASHBOARD-TYPE: either Notes or Decks to display the respective dashboard."
"Launch gnosis dashboard."
(interactive)
;; Refresh gnosis-db
- (unless gnosis-testing
- (setf gnosis-db (emacsql-sqlite-open (expand-file-name "gnosis.db" gnosis-dir))))
+ (setf gnosis-db gnosis-db)
(let* ((buffer-name gnosis-dashboard-buffer-name)
(due-log (gnosis-review-get--due-notes))
(due-note-ids (mapcar #'car due-log)))
@@ -2730,9 +2873,8 @@ DASHBOARD-TYPE: either Notes or Decks to display the respective dashboard."
(insert (gnosis-center-string
(format "Current streak: %s days"
(propertize
- (number-to-string
- (gnosis-dashboard--streak
- (gnosis-select 'date 'activity-log '1=1 t)))
+ (gnosis-dashboard--streak
+ (gnosis-select 'date 'activity-log '1=1 t))
'face 'success))))
(insert "\n\n"))
(pop-to-buffer-same-window buffer)