summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThanos Apollo <[email protected]>2024-09-05 19:27:15 +0300
committerThanos Apollo <[email protected]>2024-09-05 19:27:44 +0300
commit731f0ba4910c872efedf7e460d904f3d9c3be9a7 (patch)
treeea50df01d2ea9dee092572875696afa545d3b419
parentbc626d511c111545387df12219a6412506eaf5a1 (diff)
parentd034335bde30c7873768bfbbae531a704d011245 (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--.elpaignore1
-rw-r--r--Makefile8
-rw-r--r--README9
-rw-r--r--README.md9
-rw-r--r--doc/gnosis.org60
-rw-r--r--gnosis-org.el92
-rw-r--r--gnosis.el114
-rw-r--r--manifest.scm3
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
diff --git a/Makefile b/Makefile
index 4ee28ad..9b1a262 100644
--- a/Makefile
+++ b/Makefile
@@ -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)
diff --git a/README b/README
deleted file mode 100644
index 34e8089..0000000
--- a/README
+++ /dev/null
@@ -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.
diff --git a/gnosis.el b/gnosis.el
index 3c04826..4619fa9 100644
--- a/gnosis.el
+++ b/gnosis.el
@@ -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"))