diff options
author | Thanos Apollo <[email protected]> | 2025-01-03 20:08:49 +0200 |
---|---|---|
committer | Thanos Apollo <[email protected]> | 2025-01-03 20:08:49 +0200 |
commit | 6d174e37695b2928954eaa4e1a9d73bc0ba24be1 (patch) | |
tree | 131d92d544b259ad7d168591c038eb97b33ba52e | |
parent | 1ec9e74c7ecdb085b9c92b5c742f27ee800d8a14 (diff) |
Rewrite review & editing of themas.
-rw-r--r-- | gnosis.el | 642 |
1 files changed, 392 insertions, 250 deletions
@@ -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) |