diff options
author | Thanos Apollo <[email protected]> | 2024-09-05 19:27:15 +0300 |
---|---|---|
committer | Thanos Apollo <[email protected]> | 2024-09-05 19:27:44 +0300 |
commit | 731f0ba4910c872efedf7e460d904f3d9c3be9a7 (patch) | |
tree | ea50df01d2ea9dee092572875696afa545d3b419 | |
parent | bc626d511c111545387df12219a6412506eaf5a1 (diff) | |
parent | d034335bde30c7873768bfbbae531a704d011245 (diff) |
Release version 0.4.2.0.4.2
* Comment out gnosis-org sections that are under development.
* gnosis-org should be ready by next version.
* Fix display issues on non-grapical interface.
* Add variable watchers for custom algorithm values.
* Update assertions for editing notes
This is a minor release with a few bug fixes.
-rw-r--r-- | .elpaignore | 1 | ||||
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | README | 9 | ||||
-rw-r--r-- | README.md | 9 | ||||
-rw-r--r-- | doc/gnosis.org | 60 | ||||
-rw-r--r-- | gnosis-org.el | 92 | ||||
-rw-r--r-- | gnosis.el | 114 | ||||
-rw-r--r-- | manifest.scm | 3 |
8 files changed, 222 insertions, 74 deletions
diff --git a/.elpaignore b/.elpaignore index d8ed22f..9a93dad 100644 --- a/.elpaignore +++ b/.elpaignore @@ -1,3 +1,4 @@ CONTRIBUTING.org LICENSE Makefile +manifest.scm @@ -6,15 +6,21 @@ EMACS = emacs ORG := doc/gnosis.org TEXI := doc/gnosis.texi INFO := doc/gnosis.info - +TEST_FILE := gnosis-test.el all: doc doc: $(ORG) $(EMACS) --batch \ + -Q \ --load org \ --eval "(with-current-buffer (find-file \"$(ORG)\") (org-texinfo-export-to-texinfo) (org-texinfo-export-to-info) (save-buffer))" \ --kill +test: + $(EMACS) --batch \ + --load $(TEST_FILE) \ + --eval "(ert-run-tests-batch-and-exit)" + clean: rm -f $(TEXI) $(INFO) @@ -1,9 +0,0 @@ - -Gnosis (γνῶσις) -==== - -Gnosis (γνῶσις), pronounced "noh-sis", meaning knowledge in Greek, -is a Spaced Repetition System for note taking and self testing. - -- Project's Page: <https://thanosapollo.org/projects/gnosis/> -- User Manual: <https://thanosapollo.org/user-manual/gnosis/> diff --git a/README.md b/README.md new file mode 100644 index 0000000..8ed18df --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Γνῶσις | Gnosis + +## About + +Γνῶσις (gnosis), pronounced "GNU-sis", meaning knowledge in Greek, +is a GNU Emacs Spaced Repetition System for storing knowledge. + +- [Project's Page](https://thanosapollo.org/projects/gnosis/) +- [User Manual](https://elpa.nongnu.org/nongnu/doc/gnosis.html) diff --git a/doc/gnosis.org b/doc/gnosis.org index 15afc52..9d0efb9 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.4.0 -#+macro: release-date 2024-08-7 +#+macro: stable-version 0.4.2 +#+macro: release-date 2024-09-5 #+macro: file @@texinfo:@file{@@$1@@texinfo:}@@ #+macro: space @@texinfo:@: @@ #+macro: kbd @@texinfo:@kbd{@@$1@@texinfo:}@@ @@ -22,15 +22,16 @@ #+texinfo_header: @set MAINTAINERCONTACT @uref{mailto:[email protected],contact the maintainer} -Gnosis is a customizable spaced repetition system designed to enhance +Gnosis (GNU-sis) is a customizable spaced repetition system designed to enhance memory retention through active recall. It allows users to set specific review intervals for note decks & tags, creating an optimal -learning environment tailored to each specific topic. +learning environment tailored to each specific topic/subject. #+texinfo: @noindent This manual is written for Gnosis version {{{stable-version}}}, released on {{{release-date}}}. -+ Official manual: <https://thanosapollo.org/user-manual/gnosis> ++ Official manual: + + <https://elpa.nongnu.org/nongnu/doc/gnosis.html> + Git repositories: + <https://git.thanosapollo.org/gnosis> @@ -260,7 +261,6 @@ name suggests, they rely on =vc= to work properly. Depending on your setup, =vc= might require an external package for the ssh passphrase dialog, such as ~x11-ssh-askpass~. - To automatically push changes after a review session, add this to your configuration: #+begin_src emacs-lisp (setf gnosis-vc-auto-push t) @@ -268,44 +268,30 @@ To automatically push changes after a review session, add this to your configura #+end_src * Configuring Note Types -** Adjust Current Types Entries +** Custom Note Types Each gnosis note type has an /interactive/ function, named -=gnosis-add-note-TYPE=. You can set default values for each entry by -hard coding specific values to their keywords. +=gnosis-add-note-TYPE= and a "hidden" function +named =gnosis-add-note--TYPE=. You can create your own custom interactive +functions to ignore or hard-code specific values by using already +defined hidden functions that handle all the logic. For example: #+begin_src emacs-lisp -(defun gnosis-add-note-basic (deck) - (gnosis-add-note--basic :deck deck - :question (gnosis-read-string-from-buffer "Question: " "") - :answer (read-string "Answer: ") - :hint (gnosis-hint-prompt gnosis-previous-note-hint) - :extra "" - :images nil - :tags (gnosis-prompt-tags--split gnosis-previous-note-tags))) + (defun gnosis-add-note-custombasic (deck) + (gnosis-add-note--basic :deck deck + :question (gnosis-read-string-from-buffer "Question: " "") + :answer (read-string "Answer: ") + :hint (gnosis-hint-prompt gnosis-previous-note-hint) + :extra "" + :images nil + :tags (gnosis-prompt-tags--split gnosis-previous-note-tags))) + ;; Add custom note type to gnosis-note-types + (add-to-list 'gnosis-note-types "custombasic") #+end_src -By evaluating the above code snippet, you won't be prompted to enter -anything for ~extra~ & ~images~. -** 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 an interactive function - -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. You can use one of the =current gnosis-add-note--TYPE= -functions or create one of your own. - -Refer to =gnosis-add-note-basic= & =gnosis-add-note--basic= for a simple -example of how this is done, as well as =gnosis-add-note-double=. +Now ~custombasic~ is available as a note type, for which you won't be prompted to enter +anything for ~extra~ & ~images~. ** Development To make development and customization easier, gnosis comes with diff --git a/gnosis-org.el b/gnosis-org.el new file mode 100644 index 0000000..e977083 --- /dev/null +++ b/gnosis-org.el @@ -0,0 +1,92 @@ +;;; gnosis-org.el --- Org module for Gnosis -*- lexical-binding: t; -*- + +;; Copyright (C) 2023-2024 Thanos Apollo + +;; Author: Thanos Apollo <[email protected]> +;; Keywords: extensions +;; URL: https://git.thanosapollo.org/gnosis +;; Version: 0.0.1 + +;; Package-Requires: ((emacs "27.2") (compat "29.1.4.2")) + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see <https://www.gnu.org/licenses/>. + +;;; Commentary: + +;; Under development. + +;;; Code: + +(require 'cl-lib) +(require 'org) +(require 'org-element) + +(defun gnosis-org--global-props (name &optional buffer) + "Get the plists of global org properties by NAME in BUFFER. + +NAME is a string representing the property name to search for. +BUFFER defaults to the current buffer if not specified." + (cl-assert (stringp name) nil "NAME must be a string.") + (with-current-buffer (or buffer (current-buffer)) + (let ((elements (org-element-map (org-element-parse-buffer) 'keyword + (lambda (el) + (when (string= (org-element-property :key el) name) + el)) + nil t))) + (if elements elements + (message "No properties found for %s" name) + nil)))) + +(defun gnosis-org--heading-props (property &optional buffer) + "Get the values of a custom PROPERTY from all headings in BUFFER. + +PROPERTY is a string representing the property name to search for. +BUFFER defaults to the current buffer if not specified." + (cl-assert (stringp property) nil "PROPERTY must be a string.") + (with-current-buffer (or buffer (current-buffer)) + (let ((results nil)) + (org-element-map (org-element-parse-buffer) 'headline + (lambda (headline) + (let ((prop (org-element-property (intern (concat ":" property)) headline))) + (when prop + (push prop results))))) + (if results (reverse results) + (message "No custom properties found for %s" property) + nil)))) +;; TODO: Add support for tags. +(cl-defun gnosis-org-insert-heading (&key main id answer type) + "Insert an Org heading in current buffer. + +- MAIN as the title. +- ID as GNOSIS_ID. +- ANSWER as the subheading. +- TYPE as the note type. + +If BUFFER is not specified, defaults to the current buffer." + (cl-assert (stringp main) nil "MAIN must be a string representing the heading title.") + (cl-assert (stringp id) nil "ID must be a string representing the GNOSIS_ID.") + (cl-assert (stringp type) nil "TYPE must be a string representing the TYPE property.") + (let ((main (if (string-match-p "\n" main) (replace-regexp-in-string "\n" "\\\\n" main) main)) + (answer (cond ((stringp answer) + answer) + ((numberp answer) + (number-to-string answer)) + (t (mapconcat 'identity answer ", "))))) + (goto-char (point-max)) ;; Ensure we're at the end of the buffer + (insert (format "* %s\n:PROPERTIES:\n:GNOSIS_ID: %s\n:TYPE: %s\n:END:\n** %s\n" + main id type answer)) + (message "Inserted heading: %s with GNOSIS_ID %s and TYPE %s" main id type))) + +(provide 'gnosis-org) +;;; gnosis-org.el ends here. @@ -5,9 +5,9 @@ ;; Author: Thanos Apollo <[email protected]> ;; Keywords: extensions ;; URL: https://thanosapollo.org/projects/gnosis -;; Version: 0.4.1 +;; Version: 0.4.2 -;; Package-Requires: ((emacs "27.2") (emacsql "4.0.0") (compat "29.1.4.2") (transient "0.7.2")) +;; Package-Requires: ((emacs "27.2") (emacsql "4.0.1") (compat "29.1.4.2") (transient "0.7.2")) ;; This program is free software; you can redistribute it and/or modify ;; it under the terms of the GNU General Public License as published by @@ -50,6 +50,8 @@ (require 'gnosis-string-edit) (require 'gnosis-dashboard) +;; (require 'gnosis-org) + (require 'animate) (defgroup gnosis nil @@ -217,7 +219,7 @@ When nil, review new notes last." (defvar gnosis-review-notes nil "Review notes.") -;; TODO: Make this as a defcustom +;; TODO: Make this as a defcustom. (defvar gnosis-custom-values '((:deck "demo" (:proto (0 1 3) :anagnosis 3 :epignosis 0.5 :agnoia 0.3 :amnesia 0.5 :lethe 3)) (:tag "demo" (:proto (1 2) :anagnosis 3 :epignosis 0.5 :agnoia 0.3 :amnesia 0.45 :lethe 3))) @@ -482,19 +484,21 @@ or =extra-image'. Instead of using =extra-image' post review, prefer =gnosis-display-extra' which displays the =extra-image' as well. Refer to =gnosis-db-schema-extras' for informations on images stored." - (let* ((img (gnosis-get image 'extras `(= id ,id))) - (path-to-image (expand-file-name (or img "") (file-name-as-directory gnosis-images-dir))) - (image (create-image path-to-image 'png nil :width gnosis-image-width :height gnosis-image-height)) - (image-width (car (image-size image t))) - (frame-width (window-text-width))) ;; Width of the current window in columns - (cond ((or (not img) (string-empty-p img)) - (insert "\n\n")) - ((and img (file-exists-p path-to-image)) - (let* ((padding-cols (/ (- frame-width (floor (/ image-width (frame-char-width)))) 2)) - (padding (make-string (max 0 padding-cols) ?\s))) - (insert "\n\n" padding) ;; Insert padding before the image - (insert-image image) - (insert "\n\n")))))) + ;; Only display images on graphical env + (when (display-graphic-p) + (let* ((img (gnosis-get image 'extras `(= id ,id))) + (path-to-image (expand-file-name (or img "") (file-name-as-directory gnosis-images-dir))) + (image (create-image path-to-image 'png nil :width gnosis-image-width :height gnosis-image-height)) + (image-width (car (image-size image t))) + (frame-width (window-text-width))) ;; Width of the current window in columns + (cond ((or (not img) (string-empty-p img)) + (insert "\n\n")) + ((and img (file-exists-p path-to-image)) + (let* ((padding-cols (/ (- frame-width (floor (/ image-width (frame-char-width)))) 2)) + (padding (make-string (max 0 padding-cols) ?\s))) + (insert "\n\n" padding) ;; Insert padding before the image + (insert-image image) + (insert "\n\n"))))))) (defun gnosis-display-mcq-options (id) "Display answer options for mcq note ID." @@ -1269,7 +1273,7 @@ Optionally, add cusotm PROMPT." (cl-loop for tags in (gnosis-select 'tags 'notes '1=1 t) nconc tags into all-tags finally return (delete-dups all-tags))) -;; TODO: Rewrite this using `gnosis-get-tag-notes'. +;; TODO: Rewrite this using gnosis-get-tag-notes. (defun gnosis-select-by-tag (input-tags &optional due suspended-p) "Return note ID's for every note with INPUT-TAGS. @@ -1389,8 +1393,8 @@ provided, use it as the default value." ;; Collecting note ids -;; TODO: Rewrite. Tags should be an input of strings, interactive -;; handling should be done by "helper" funcs +;; TODO: Rewrite this! Tags should be an input of strings, +;; interactive handling should be done by "helper" funcs (cl-defun gnosis-collect-note-ids (&key (tags nil) (due nil) (deck nil) (query nil)) "Return list of note ids based on TAGS, DUE, DECKS, QUERY. @@ -1739,7 +1743,7 @@ NOTE-COUNT: The number of notes reviewed in the session to be commited." (error "Git not found, please install git")) (unless (file-exists-p (expand-file-name ".git" gnosis-dir)) (vc-create-repo 'Git)) - ;; TODO: Redo this using vc + ;; TODO: Redo this using vc. (unless gnosis-testing (shell-command (format "%s %s %s" git "add" (shell-quote-argument "gnosis.db"))) (shell-command (format "%s %s %s" git "commit -m" @@ -1834,7 +1838,7 @@ NOTE-COUNT: Total notes to be commited for session." (cl-incf note-count) (gnosis-review-actions success note note-count)) finally - ;; TODO: Add optional arg to repeat for specific deck/tag + ;; TODO: Add optional arg, repeat for specific deck/tag. ;; Repeat until there are no due notes (and due (gnosis-review-session (gnosis-collect-note-ids :due t) t note-count)))) (gnosis-dashboard) @@ -1982,10 +1986,12 @@ SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend" "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 (listp tags) nil "Tags must be a list of strings") - (cl-assert (and (listp gnosis) (length= gnosis 3)) nil "gnosis must be a list of 3 floats") - (cl-assert (or (stringp options) (listp options)) nil "Options must be a string, or a list for MCQ") - (cl-assert (or (= suspend 0) (= suspend 1)) nil "Suspend must be either 0 or 1") + (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 #'stringp 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.") @@ -2015,6 +2021,48 @@ SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend" (gnosis-update 'notes `(= ,field ',value) `(= id ,id))) (t (gnosis-update 'notes `(= ,field ,value) `(= id ,id)))))) +(defun gnosis-validate-custom-values (new-value) + "Validate the structure and values of NEW-VALUE for gnosis-custom-values." + (unless (listp new-value) + (error "GNOSIS-CUSTOM-VALUES should be a list of entries")) + (dolist (entry new-value) + (unless (and (listp entry) (= (length entry) 3) + (memq (nth 0 entry) '(:deck :tag)) + (stringp (nth 1 entry)) + (listp (nth 2 entry))) ; Ensure the third element is a plist + (error "Each entry should a :deck or :tag keyword, a string, and a plist of custom values")) + (let ((proto (plist-get (nth 2 entry) :proto)) + (anagnosis (plist-get (nth 2 entry) :anagnosis)) + (epignosis (plist-get (nth 2 entry) :epignosis)) + (agnoia (plist-get (nth 2 entry) :agnoia)) + (amnesia (plist-get (nth 2 entry) :amnesia)) + (lethe (plist-get (nth 2 entry) :lethe))) + (unless (listp proto) + (error "Proto must be a list of interval integer values")) + (unless (or (null anagnosis) (integerp anagnosis)) + (error "Anagnosis should be an integer")) + (unless (or (null epignosis) (numberp epignosis)) + (error "Epignosis should be a number")) + (unless (or (null agnoia) (numberp agnoia)) + (error "Agnoia should be a number")) + (unless (or (null amnesia) (and (numberp amnesia) (<= amnesia 1) (>= amnesia 0))) + (error "Amnesia should be a number between 0 and 1")) + (unless (or (null lethe) (and (integerp lethe) (> lethe 0))) + (error "Lethe should be an integer greater than 0"))))) + +(defun gnosis-custom-values-watcher (symbol new-value _operation _where) + "Watcher for gnosis custom values. + +SYMBOL to watch changes for. +NEW-VALUE is the new value set to the variable. +OPERATION is the type of operation being performed. +WHERE is the buffer or object where the change happens." + (when (eq symbol 'gnosis-custom-values) + (gnosis-validate-custom-values new-value))) + +(add-variable-watcher 'gnosis-custom-values 'gnosis-custom-values-watcher) + +;; Validate custom values during review process as well. (defun gnosis-get-custom-values--validate (plist valid-keywords) "Verify that PLIST consists of VALID-KEYWORDS." (let ((keys (let (ks) @@ -2251,7 +2299,7 @@ Defaults to current date." (let* ((date (or date (gnosis-algorithm-date))) (reviewed-new (or (car (gnosis-select 'reviewed-new 'activity-log `(= date ',date) t)) 0))) reviewed-new)) -;; TODO: Auto tag overdue tags +;; TODO: Auto tag overdue tags. (defun gnosis-tags--append (id tag) "Append TAG to the list of tags of note ID." (cl-assert (numberp id) nil "ID must be the note id number") @@ -2500,7 +2548,7 @@ If STRING-SECTION is nil, apply FACE to the entire STRING." (gnosis-add-note--cloze :deck deck-name :note "GNU Emacs is an extensible editor created by {{c1::Richard}} {{c1::Stallman}} in {{c2::1984::year}}" :tags note-tags - :extra "Emacs was originally implemented in 1976 on the MIT AI Lab's Incompatible Timesharing System (ITS), as a collection of TECO macros. The name “Emacs” was originally chosen as an abbreviation of “Editor MACroS”. =This version of Emacs=, GNU Emacs, was originally *written in 1984*") + :extra "Emacs was originally implemented in 1976 on the MIT AI Lab's Incompatible Timesharing System (ITS), as a collection of TECO macros. The name “Emacs” was originally chosen as an abbreviation of “Editor MACroS”. This version of Emacs, =GNU= =Emacs=, was originally written in _1984_") (gnosis-add-note--y-or-n :deck deck-name :question "Is GNU Emacs the unparalleled pinnacle of all software creation?" :hint "Duh" @@ -2509,6 +2557,18 @@ If STRING-SECTION is nil, apply FACE to the entire STRING." :tags note-tags)) (error "Demo deck already exists")))) +;; TODO: Add Export funcs +;; (defun gnosis-export-deck (&optional deck) +;; "Export contents of DECK." +;; (interactive (list (gnosis--get-deck-id))) +;; (with-current-buffer (get-buffer-create "*test*") +;; (insert (format "#+GNOSIS_DECK: %s\n\n" (gnosis--get-deck-name deck))) +;; (cl-loop for note in (gnosis-select '[main answer id type] 'notes `(= deck-id ,deck)) +;; do (gnosis-org-insert-heading :main (car note) +;; :answer (cadr note) +;; :id (number-to-string (caddr note)) +;; :type (cadddr note))))) + ;; Gnosis mode ;; ;;;;;;;;;;;;;;;;; diff --git a/manifest.scm b/manifest.scm new file mode 100644 index 0000000..ce98337 --- /dev/null +++ b/manifest.scm @@ -0,0 +1,3 @@ +;; +(specifications->manifest + (list "make" "texinfo" "emacs" "emacs-org")) |