summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.org6
-rw-r--r--doc/gnosis.info221
-rw-r--r--doc/gnosis.org124
-rw-r--r--doc/gnosis.texi151
-rw-r--r--gnosis-algorithm.el186
-rw-r--r--gnosis-test.el2
-rw-r--r--gnosis.el654
7 files changed, 741 insertions, 603 deletions
diff --git a/TODO.org b/TODO.org
index a8f313d..79c2324 100644
--- a/TODO.org
+++ b/TODO.org
@@ -2,6 +2,9 @@
#+author: Thanos Apollo
#+startup: content
+
+* Notes
+** TODO Add export deck
* Dashboard
** DONE Add Dashboard
CLOSED: [2024-02-20 Tue 13:33]
@@ -17,7 +20,8 @@ Search by tags, deck or LIKE question.
** TODO Algorithm: changes for ef increase/decrease values :priorityHigh:
+ Create a =gnosis-algorithm-ef-increase=, which will be used to
increase ef increase value upon X consecutive successful reviews
-* Misc
+* Misc
+** TODO Add export deck :priorityHigh:
** DONE Refactor =completing-read= UI choices
CLOSED: [2024-02-17 Sat 21:59]
/DONE on version 0.1.7/
diff --git a/doc/gnosis.info b/doc/gnosis.info
index 5e5f7c5..885f23d 100644
--- a/doc/gnosis.info
+++ b/doc/gnosis.info
@@ -15,7 +15,7 @@ Gnosis (γνῶσις), pronounced "noh-sis", _meaning knowledge in Greek_, is
a spaced repetition system implementation for note taking and self
testing.
-This manual is written for Gnosis version 0.1.9, released on 2023-02-22.
+This manual is written for Gnosis version 0.2.0, released on 2023-03-08.
• Official manual: <https://thanosapollo.org/user-manual/gnosis>
• Git repositories:
@@ -24,7 +24,6 @@ This manual is written for Gnosis version 0.1.9, released on 2023-02-22.
* Menu:
* Introduction::
-* Installation::
* Adding notes::
* Note Types::
* Customization::
@@ -35,11 +34,6 @@ This manual is written for Gnosis version 0.1.9, released on 2023-02-22.
-- The Detailed Node Listing --
-Installation
-
-* Using straight.el: Using straightel.
-* Installing manually from source::
-
Note Types
* Cloze::
@@ -65,74 +59,26 @@ Extending Gnosis

-File: gnosis.info, Node: Introduction, Next: Installation, Prev: Top, Up: Top
+File: gnosis.info, Node: Introduction, Next: Adding notes, Prev: Top, Up: Top
1 Introduction
**************
-Gnosis is a spaced repetition note taking and self testing system, where
-notes are taken in a Question/Answer/Explanation-like format & reviewed
-in spaced intervals.
-
- Gnosis can help you better understand and retain the material by
-encouraging active engagement. It also provides a clear structure for
-your notes & review sessions, making it easier to study.
-
-
-File: gnosis.info, Node: Installation, Next: Adding notes, Prev: Introduction, Up: Top
-
-2 Installation
-**************
-
-Gnosis is available via MELPA
- • <https://melpa.org/#/gnosis>
-
-* Menu:
-
-* Using straight.el: Using straightel.
-* Installing manually from source::
-
-
-File: gnosis.info, Node: Using straightel, Next: Installing manually from source, Up: Installation
-
-2.1 Using straight.el
-=====================
-
-If you have not installed straight.el, follow the instructions here:
-
- <https://github.com/radian-software/straight.el>
-
- Once you have installed straight.el, you can install gnosis using the
-following emacs lisp snippet:
+Gnosis, is a spaced repetition system for note taking & self testing,
+where notes are taken in a Question/Answer/Explanation format & reviewed
+in spaced intervals, determined by the success or failure to recall a
+given answer.
- (straight-use-package
- '(gnosis :type git
- :host nil
- :repo "https://git.thanosapollo.org/gnosis"))
+ Gnosis implements a highly customizable algorithm, inspired by SM-2.
+Gnosis algorithm does not use user's subjective rating of a note to
+determine the next review interval, but instead uses the user's success
+or failure in recalling the answer of a note. Read more on *note Gnosis
+Algorithm::

-File: gnosis.info, Node: Installing manually from source, Prev: Using straightel, Up: Installation
+File: gnosis.info, Node: Adding notes, Next: Note Types, Prev: Introduction, Up: Top
-2.2 Installing manually from source
-===================================
-
-Gnosis depends on the ‘compat’ & ‘emacsql’ libraries which are available
-from MELPA. Install them using ‘M-x package-install RET <package> RET’
-or you may also install them manually from their repository.
-
- • Clone gnosis repository
-
- $ git clone https://git.thanosapollo.org/gnosis ~/.emacs.d/site-lisp/gnosis
-
- • Add this to your emacs configuration
-
- (add-to-list 'load-path "~/.emacs.d/site-lisp/gnosis")
- (load-file "~/.emacs.d/site-lisp/gnosis/gnosis.el")
-
-
-File: gnosis.info, Node: Adding notes, Next: Note Types, Prev: Installation, Up: Top
-
-3 Adding notes
+2 Adding notes
**************
Creating notes for gnosis can be done interactively with:
@@ -140,13 +86,13 @@ Creating notes for gnosis can be done interactively with:
‘M-x gnosis-add-note’
When it comes to adding images, you can select images that are inside
-‘gnosis-images-dir’. For adjusting image size, refer to *note Image
-Size: Image size.
+‘gnosis-images-dir’. For adjusting image size, refer to *note
+Customization::

File: gnosis.info, Node: Note Types, Next: Customization, Prev: Adding notes, Up: Top
-4 Note Types
+3 Note Types
************
* Menu:
@@ -160,15 +106,11 @@ File: gnosis.info, Node: Note Types, Next: Customization, Prev: Adding notes,

File: gnosis.info, Node: Cloze, Next: MCQ (Multiple Choice Question), Up: Note Types
-4.1 Cloze
+3.1 Cloze
=========
A cloze note type is a format where you create sentences or paragraphs
-with "missing" words. Almost all note types can be written as a cloze
-type in a way. Ideal type for memorizing definitions.
-
- To get the most out of gnosis, you have to become familiar with cloze
-type notes.
+with "missing" words. A fill-in-the-blanks question.
You can create a cloze note type using ‘M-x gnosis-add-note’ and
selecting ‘Cloze’, the question should be formatted like this:
@@ -185,31 +127,28 @@ selecting ‘Cloze’, the question should be formatted like this:
• Each 'cX' tag can have multiple clozes, but each cloze must be a
*UNIQUE* word (or a unique combination of words) in given note.
+ You can remove the _guidance_ string by adjusting
+‘gnosis-cloze-guidance’.
+

File: gnosis.info, Node: MCQ (Multiple Choice Question), Next: Basic Type, Prev: Cloze, Up: Note Types
-4.2 MCQ (Multiple Choice Question)
+3.2 MCQ (Multiple Choice Question)
==================================
A MCQ note type, as the name suggests, is a multiple choice question.
- First you will be prompted to input the question ‘stem’ field.
-
- Afterwards you will be prompted to enter the choices, each ‘-’
-indicates a choice, the choice inside ‘{}’ will be marked as the correct
-one.
-
- Example of options:
- • Option 1
+ The stem field (question) is separated by the options (choices) via
+‘gnosis-mcq-separator’, each option is separated by
+‘gnosis-mcq-option-separator’.
- • Option 2
-
- • {Correct choice}
+ You can remove the _guidance_ string by adjusting
+‘gnosis-mcq-guidance’.

File: gnosis.info, Node: Basic Type, Next: Double, Prev: MCQ (Multiple Choice Question), Up: Note Types
-4.3 Basic Type
+3.3 Basic Type
==============
Basic note type is a simple question/answer note, where the user first
@@ -219,19 +158,18 @@ input the answer.

File: gnosis.info, Node: Double, Next: y-or-n, Prev: Basic Type, Up: Note Types
-4.4 Double
+3.4 Double
==========
Double note type, is essentially a note that generates 2 basic notes.
The second one reverses question/answer.
- Ideal for vocabulary acquisition, creating vocabulary/translation
-notes for a foreign language.
+ Ideal for vocabulary acquisition notes.

File: gnosis.info, Node: y-or-n, Prev: Double, Up: Note Types
-4.5 y-or-n
+3.5 y-or-n
==========
y-or-n (yes or no) note type, user is presented with a question and
@@ -244,7 +182,7 @@ the character values used to represent them.

File: gnosis.info, Node: Customization, Next: Gnosis Algorithm, Prev: Note Types, Up: Top
-5 Customization
+4 Customization
***************
* Menu:
@@ -255,19 +193,19 @@ File: gnosis.info, Node: Customization, Next: Gnosis Algorithm, Prev: Note Ty

File: gnosis.info, Node: Image size, Next: Typos | String Comparison, Up: Customization
-5.1 Image size
+4.1 Image size
==============
Adjust image size using ‘gnosis-image-height’ & ‘gnosis-image-width’
Example:
(setf gnosis-image-height 300
- gnosis-image-width 500)
+ gnosis-image-width 300)

File: gnosis.info, Node: Typos | String Comparison, Prev: Image size, Up: Customization
-5.2 Typos | String Comparison
+4.2 Typos | String Comparison
=============================
You can adjust ‘gnosis-string-difference’, this is a threshold value for
@@ -288,9 +226,26 @@ character."

File: gnosis.info, Node: Gnosis Algorithm, Next: Editing notes, Prev: Customization, Up: Top
-6 Gnosis Algorithm
+5 Gnosis Algorithm
******************
+Each gnosis note has an ef (easiness factor), which is a list of 3
+values. The last value is the total ef for a note, which will be used
+to determine the next interval upon a successful answer recall, the
+second value is the ef-decrease value, this value will be subtracted
+from the the total ef upon failure to recall the answer of a note, the
+first value is the ef increase, will be added to the total ef upon a
+successful recall.
+
+ Each gnosis deck has ‘gnosis-algorithm-ef-threshold’, it's an integer
+value that refers to the consecutive success or failures to recall an
+answer. Upon reaching the threshold, gnosis-algorithm-ef-decrease or
+gnosis-algorithm-ef-increase will be applied to the ef-increase or
+ef-decrease of note.
+
+ You can customize deck specific algorithm values using
+‘gnosis-dashboard’.
+
* Menu:
* Initial Interval::
@@ -300,24 +255,27 @@ File: gnosis.info, Node: Gnosis Algorithm, Next: Editing notes, Prev: Customi

File: gnosis.info, Node: Initial Interval, Next: Easiness Factor, Up: Gnosis Algorithm
-6.1 Initial Interval
+5.1 Initial Interval
====================
-‘gnosis-algorithm-interval’ is a list of 2 numbers, representing the
+The default initial interval is defined at ‘gnosis-algorithm-interval’,
+you can define a custom initial interval for each deck as well.
+
+ ‘gnosis-algorithm-interval’ is a list of 2 numbers, representing the
first two initial intervals for successful reviews.
Example:
- (setq gnosis-algorithm-interval '(1 3))
+ (setq gnosis-algorithm-interval '(0 1))
Using the above example, after first successfully reviewing a note,
-you will see it again tomorrow, if you successfully review said note
-again, the next review will be after 3 days.
+you will see it again in the next review session, if you successfully
+review said note again, the next review will be tomorrow.

File: gnosis.info, Node: Easiness Factor, Next: Forgetting Factor, Prev: Initial Interval, Up: Gnosis Algorithm
-6.2 Easiness Factor
+5.2 Easiness Factor
===================
The ‘gnosis-algorithm-ef’ is a list that consists of three items:
@@ -347,7 +305,7 @@ updated by adding the increase value 2.0 + <increase-value>.

File: gnosis.info, Node: Forgetting Factor, Prev: Easiness Factor, Up: Gnosis Algorithm
-6.3 Forgetting Factor
+5.3 Forgetting Factor
=====================
‘gnosis-algorithm-ff’ is a floating number below 1.
@@ -362,10 +320,12 @@ interval was 6 days, the next interval will be 6 * 0.5 = 3 days.
(setq gnosis-algorithm-ff 0.5)
+ You can set a custom ‘gnosis-algorithm-ff’ for each deck as well.
+

File: gnosis.info, Node: Editing notes, Next: Sync between devices, Prev: Gnosis Algorithm, Up: Top
-7 Editing notes
+6 Editing notes
***************
• Currently there are 2 ways for editing notes:
@@ -377,7 +337,7 @@ File: gnosis.info, Node: Editing notes, Next: Sync between devices, Prev: Gno

File: gnosis.info, Node: Sync between devices, Next: Extending Gnosis, Prev: Editing notes, Up: Top
-8 Sync between devices
+7 Sync between devices
**********************
Gnosis uses git to maintain data integrity and facilitate
@@ -406,7 +366,7 @@ your configuration:

File: gnosis.info, Node: Extending Gnosis, Prev: Sync between devices, Up: Top
-9 Extending Gnosis
+8 Extending Gnosis
******************
To make development and customization easier, gnosis comes with
@@ -423,14 +383,14 @@ then enter ‘n’ (no) at the prompt "Start development env?"

File: gnosis.info, Node: Creating Custom Note Types, Up: Extending Gnosis
-9.1 Creating Custom Note Types
+8.1 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’
- (add-to-list 'gnosis-note-types 'new-type)
+ (add-to-list 'gnosis-note-types "new-note-type")
• Create 2 functions; ‘gnosis-add-note-TYPE’ &
‘gnosis-add-note--TYPE’
@@ -456,28 +416,25 @@ should be done.

Tag Table:
Node: Top244
-Node: Introduction1316
-Node: Installation1796
-Node: Using straightel2073
-Node: Installing manually from source2585
-Node: Adding notes3274
-Node: Note Types3651
-Node: Cloze3863
-Node: MCQ (Multiple Choice Question)4856
-Node: Basic Type5429
-Node: Double5732
-Node: y-or-n6054
-Node: Customization6456
-Node: Image size6641
-Node: Typos | String Comparison6927
-Node: Gnosis Algorithm7702
-Node: Initial Interval7914
-Node: Easiness Factor8421
-Node: Forgetting Factor9369
-Node: Editing notes9903
-Node: Sync between devices10295
-Node: Extending Gnosis11323
-Node: Creating Custom Note Types11778
+Node: Introduction1209
+Node: Adding notes1851
+Node: Note Types2220
+Node: Cloze2432
+Node: MCQ (Multiple Choice Question)3350
+Node: Basic Type3846
+Node: Double4149
+Node: y-or-n4415
+Node: Customization4817
+Node: Image size5002
+Node: Typos | String Comparison5288
+Node: Gnosis Algorithm6063
+Node: Initial Interval7099
+Node: Easiness Factor7764
+Node: Forgetting Factor8712
+Node: Editing notes9320
+Node: Sync between devices9712
+Node: Extending Gnosis10740
+Node: Creating Custom Note Types11195

End Tag Table
diff --git a/doc/gnosis.org b/doc/gnosis.org
index 0571593..9ff58e8 100644
--- a/doc/gnosis.org
+++ b/doc/gnosis.org
@@ -4,8 +4,8 @@
#+language: en
#+options: ':t toc:nil author:t email:t num:t
#+startup: content
-#+macro: stable-version 0.1.9
-#+macro: release-date 2023-02-22
+#+macro: stable-version 0.2.0
+#+macro: release-date 2023-03-08
#+macro: file @@texinfo:@file{@@$1@@texinfo:}@@
#+macro: space @@texinfo:@: @@
#+macro: kbd @@texinfo:@kbd{@@$1@@texinfo:}@@
@@ -36,51 +36,16 @@ This manual is written for Gnosis version {{{stable-version}}}, released on {{{r
#+texinfo: @insertcopying
* Introduction
-Gnosis is a spaced repetition note taking and self testing system,
-where notes are taken in a Question/Answer/Explanation-like format &
-reviewed in spaced intervals.
+Gnosis, is a spaced repetition system for note taking & self
+testing, where notes are taken in a Question/Answer/Explanation
+format & reviewed in spaced intervals, determined by the success or
+failure to recall a given answer.
-Gnosis can help you better understand and retain the material by
-encouraging active engagement. It also provides a clear structure for
-your notes & review sessions, making it easier to study.
-
-* Installation
-
-Gnosis is available via MELPA
-+ <https://melpa.org/#/gnosis>
-
-** Using straight.el
-If you have not installed straight.el, follow the instructions here:
-
- <https://github.com/radian-software/straight.el>
-
-Once you have installed straight.el, you can install gnosis using the
-following emacs lisp snippet:
-
-#+begin_src emacs-lisp
- (straight-use-package
- '(gnosis :type git
- :host nil
- :repo "https://git.thanosapollo.org/gnosis"))
-#+end_src
-
-** Installing manually from source
-Gnosis depends on the ~compat~ & ~emacsql~ libraries which are available
-from MELPA. Install them using ~M-x package-install RET <package> RET~
-or you may also install them manually from their repository.
-
-+ Clone gnosis repository
-
- #+begin_src shell
- $ git clone https://git.thanosapollo.org/gnosis ~/.emacs.d/site-lisp/gnosis
- #+end_src
-
-+ Add this to your emacs configuration
-
- #+begin_src emacs-lisp
- (add-to-list 'load-path "~/.emacs.d/site-lisp/gnosis")
- (load-file "~/.emacs.d/site-lisp/gnosis/gnosis.el")
- #+end_src
+Gnosis implements a highly customizable algorithm, inspired by SM-2.
+Gnosis algorithm does not use user's subjective rating of a note to
+determine the next review interval, but instead uses the user's
+success or failure in recalling the answer of a note. Read more on
+[[Gnosis Algorithm]]
* Adding notes
Creating notes for gnosis can be done interactively with:
@@ -88,16 +53,13 @@ Creating notes for gnosis can be done interactively with:
=M-x gnosis-add-note=
When it comes to adding images, you can select images that are inside
-=gnosis-images-dir=. For adjusting image size, refer to [[Image size][Image Size]]
+=gnosis-images-dir=. For adjusting image size, refer to [[Customization]]
* Note Types
** Cloze
A cloze note type is a format where you create sentences or paragraphs
-with "missing" words. Almost all note types can be written as a cloze
-type in a way. Ideal type for memorizing definitions.
-
-To get the most out of gnosis, you have to become familiar with cloze type notes.
+with "missing" words. A fill-in-the-blanks question.
You can create a cloze note type using =M-x gnosis-add-note= and
selecting ~Cloze~, the question should be formatted like this:
@@ -114,24 +76,18 @@ You can also format clozes like Anki if you prefer; e.g ~{{c1::Cyproheptadine}}~
+ Each `cX` tag can have multiple clozes, but each cloze must be a
*UNIQUE* word (or a unique combination of words) in given note.
+You can remove the /guidance/ string by adjusting
+=gnosis-cloze-guidance=.
+
** MCQ (Multiple Choice Question)
A MCQ note type, as the name suggests, is a multiple choice question.
-First you will be prompted to input the question =stem= field.
+The stem field (question) is separated by the options (choices) via
+=gnosis-mcq-separator=, each option is separated by =gnosis-mcq-option-separator=.
-Afterwards you will be prompted to enter the choices, each =-=
-indicates a choice, the choice inside ={}= will be marked as the
-correct one.
-
-Example of options:
-#+BEGIN_QUOTE
- - Option 1
-
- - Option 2
-
- - {Correct choice}
-#+END_QUOTE
+You can remove the /guidance/ string by adjusting
+=gnosis-mcq-guidance=.
** Basic Type
@@ -144,8 +100,7 @@ input the answer.
Double note type, is essentially a note that generates 2 basic notes.
The second one reverses question/answer.
-Ideal for vocabulary acquisition, creating vocabulary/translation
-notes for a foreign language.
+Ideal for vocabulary acquisition notes.
** y-or-n
y-or-n (yes or no) note type, user is presented with a question and
@@ -162,7 +117,7 @@ Adjust image size using =gnosis-image-height= & =gnosis-image-width=
Example:
#+begin_src emacs-lisp
(setf gnosis-image-height 300
- gnosis-image-width 500)
+ gnosis-image-width 300)
#+end_src
** Typos | String Comparison
You can adjust =gnosis-string-difference=, this is a threshold value
@@ -184,20 +139,42 @@ similar, considering that the latter involves just one additional
character."
* Gnosis Algorithm
+
+Each gnosis note has an ef (easiness factor), which is a list of 3
+values. The last value is the total ef for a note, which will be
+used to determine the next interval upon a successful answer recall,
+the second value is the ef-decrease value, this value will be
+subtracted from the the total ef upon failure to recall the answer of
+a note, the first value is the ef increase, will be added to the
+total ef upon a successful recall.
+
+Each gnosis deck has =gnosis-algorithm-ef-threshold=, it's an
+integer value that refers to the consecutive success or failures to
+recall an answer. Upon reaching the threshold, gnosis-algorithm-ef-decrease
+or gnosis-algorithm-ef-increase will be applied to the ef-increase or
+ef-decrease of note.
+
+You can customize deck specific algorithm values using =gnosis-dashboard=.
+
** Initial Interval
-=gnosis-algorithm-interval= is a list of 2 numbers, representing the
-first two initial intervals for successful reviews.
+The default initial interval is defined at
+=gnosis-algorithm-interval=, you can define a custom initial interval
+for each deck as well.
+
+=gnosis-algorithm-interval= is a list of 2
+numbers, representing the first two initial intervals for successful
+reviews.
Example:
#+begin_src emacs-lisp
- (setq gnosis-algorithm-interval '(1 3))
+ (setq gnosis-algorithm-interval '(0 1))
#+end_src
Using the above example, after first successfully reviewing a note,
-you will see it again tomorrow, if you successfully review said note
-again, the next review will be after 3 days.
+you will see it again in the next review session, if you successfully
+review said note again, the next review will be tomorrow.
** Easiness Factor
@@ -245,6 +222,7 @@ Example configuration:
(setq gnosis-algorithm-ff 0.5)
#+end_src
+You can set a custom =gnosis-algorithm-ff= for each deck as well.
* Editing notes
+ Currently there are 2 ways for editing notes:
@@ -294,7 +272,7 @@ 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)
+ (add-to-list 'gnosis-note-types "new-note-type")
#+end_src
+ Create 2 functions; =gnosis-add-note-TYPE= & =gnosis-add-note--TYPE=
diff --git a/doc/gnosis.texi b/doc/gnosis.texi
index a0e4af9..7f98492 100644
--- a/doc/gnosis.texi
+++ b/doc/gnosis.texi
@@ -30,7 +30,7 @@ a spaced repetition system implementation for note taking and self
testing.
@noindent
-This manual is written for Gnosis version 0.1.9, released on 2023-02-22.
+This manual is written for Gnosis version 0.2.0, released on 2023-03-08.
@itemize
@item
@@ -48,7 +48,6 @@ Git repositories:
@menu
* Introduction::
-* Installation::
* Adding notes::
* Note Types::
* Customization::
@@ -60,11 +59,6 @@ Git repositories:
@detailmenu
--- The Detailed Node Listing ---
-Installation
-
-* Using straight.el: Using straightel.
-* Installing manually from source::
-
Note Types
* Cloze::
@@ -94,68 +88,16 @@ Extending Gnosis
@node Introduction
@chapter Introduction
-Gnosis is a spaced repetition note taking and self testing system,
-where notes are taken in a Question/Answer/Explanation-like format &
-reviewed in spaced intervals.
-
-Gnosis can help you better understand and retain the material by
-encouraging active engagement. It also provides a clear structure for
-your notes & review sessions, making it easier to study.
-
-@node Installation
-@chapter Installation
-
-Gnosis is available via MELPA
-@itemize
-@item
-@uref{https://melpa.org/#/gnosis}
-@end itemize
-
-@menu
-* Using straight.el: Using straightel.
-* Installing manually from source::
-@end menu
-
-@node Using straightel
-@section Using straight.el
-
-If you have not installed straight.el, follow the instructions here:
+Gnosis, is a spaced repetition system for note taking & self
+testing, where notes are taken in a Question/Answer/Explanation
+format & reviewed in spaced intervals, determined by the success or
+failure to recall a given answer.
-@uref{https://github.com/radian-software/straight.el}
-
-Once you have installed straight.el, you can install gnosis using the
-following emacs lisp snippet:
-
-@lisp
-(straight-use-package
- '(gnosis :type git
- :host nil
- :repo "https://git.thanosapollo.org/gnosis"))
-@end lisp
-
-@node Installing manually from source
-@section Installing manually from source
-
-Gnosis depends on the @code{compat} & @code{emacsql} libraries which are available
-from MELPA@. Install them using @code{M-x package-install RET <package> RET}
-or you may also install them manually from their repository.
-
-@itemize
-@item
-Clone gnosis repository
-
-@example
-$ git clone https://git.thanosapollo.org/gnosis ~/.emacs.d/site-lisp/gnosis
-@end example
-
-@item
-Add this to your emacs configuration
-
-@lisp
-(add-to-list 'load-path "~/.emacs.d/site-lisp/gnosis")
-(load-file "~/.emacs.d/site-lisp/gnosis/gnosis.el")
-@end lisp
-@end itemize
+Gnosis implements a highly customizable algorithm, inspired by SM-2.
+Gnosis algorithm does not use user's subjective rating of a note to
+determine the next review interval, but instead uses the user's
+success or failure in recalling the answer of a note. Read more on
+@ref{Gnosis Algorithm}
@node Adding notes
@chapter Adding notes
@@ -165,7 +107,7 @@ Creating notes for gnosis can be done interactively with:
@samp{M-x gnosis-add-note}
When it comes to adding images, you can select images that are inside
-@samp{gnosis-images-dir}. For adjusting image size, refer to @ref{Image size, , Image Size}
+@samp{gnosis-images-dir}. For adjusting image size, refer to @ref{Customization}
@node Note Types
@chapter Note Types
@@ -182,10 +124,7 @@ When it comes to adding images, you can select images that are inside
@section Cloze
A cloze note type is a format where you create sentences or paragraphs
-with ``missing'' words. Almost all note types can be written as a cloze
-type in a way. Ideal type for memorizing definitions.
-
-To get the most out of gnosis, you have to become familiar with cloze type notes.
+with ``missing'' words. A fill-in-the-blanks question.
You can create a cloze note type using @samp{M-x gnosis-add-note} and
selecting @code{Cloze}, the question should be formatted like this:
@@ -207,31 +146,19 @@ Each `cX` tag can have multiple clozes, but each cloze must be a
@strong{UNIQUE} word (or a unique combination of words) in given note.
@end itemize
+You can remove the @emph{guidance} string by adjusting
+@samp{gnosis-cloze-guidance}.
+
@node MCQ (Multiple Choice Question)
@section MCQ (Multiple Choice Question)
A MCQ note type, as the name suggests, is a multiple choice question.
-First you will be prompted to input the question @samp{stem} field.
-
-Afterwards you will be prompted to enter the choices, each @samp{-}
-indicates a choice, the choice inside @samp{@{@}} will be marked as the
-correct one.
-
-Example of options:
-@quotation
-@itemize
-@item
-Option 1
-
-@item
-Option 2
-
-@item
-@{Correct choice@}
-@end itemize
+The stem field (question) is separated by the options (choices) via
+@samp{gnosis-mcq-separator}, each option is separated by @samp{gnosis-mcq-option-separator}.
-@end quotation
+You can remove the @emph{guidance} string by adjusting
+@samp{gnosis-mcq-guidance}.
@node Basic Type
@section Basic Type
@@ -246,8 +173,7 @@ input the answer.
Double note type, is essentially a note that generates 2 basic notes.
The second one reverses question/answer.
-Ideal for vocabulary acquisition, creating vocabulary/translation
-notes for a foreign language.
+Ideal for vocabulary acquisition notes.
@node y-or-n
@section y-or-n
@@ -275,7 +201,7 @@ Adjust image size using @samp{gnosis-image-height} & @samp{gnosis-image-width}
Example:
@lisp
(setf gnosis-image-height 300
- gnosis-image-width 500)
+ gnosis-image-width 300)
@end lisp
@node Typos | String Comparison
@@ -302,6 +228,22 @@ character.``
@node Gnosis Algorithm
@chapter Gnosis Algorithm
+Each gnosis note has an ef (easiness factor), which is a list of 3
+values. The last value is the total ef for a note, which will be
+used to determine the next interval upon a successful answer recall,
+the second value is the ef-decrease value, this value will be
+subtracted from the the total ef upon failure to recall the answer of
+a note, the first value is the ef increase, will be added to the
+total ef upon a successful recall.
+
+Each gnosis deck has @samp{gnosis-algorithm-ef-threshold}, it's an
+integer value that refers to the consecutive success or failures to
+recall an answer. Upon reaching the threshold, gnosis-algorithm-ef-decrease
+or gnosis-algorithm-ef-increase will be applied to the ef-increase or
+ef-decrease of note.
+
+You can customize deck specific algorithm values using @samp{gnosis-dashboard}.
+
@menu
* Initial Interval::
* Easiness Factor::
@@ -311,18 +253,23 @@ character.``
@node Initial Interval
@section Initial Interval
-@samp{gnosis-algorithm-interval} is a list of 2 numbers, representing the
-first two initial intervals for successful reviews.
+The default initial interval is defined at
+@samp{gnosis-algorithm-interval}, you can define a custom initial interval
+for each deck as well.
+
+@samp{gnosis-algorithm-interval} is a list of 2
+numbers, representing the first two initial intervals for successful
+reviews.
Example:
@lisp
-(setq gnosis-algorithm-interval '(1 3))
+(setq gnosis-algorithm-interval '(0 1))
@end lisp
Using the above example, after first successfully reviewing a note,
-you will see it again tomorrow, if you successfully review said note
-again, the next review will be after 3 days.
+you will see it again in the next review session, if you successfully
+review said note again, the next review will be tomorrow.
@node Easiness Factor
@section Easiness Factor
@@ -377,6 +324,8 @@ Example configuration:
(setq gnosis-algorithm-ff 0.5)
@end lisp
+You can set a custom @samp{gnosis-algorithm-ff} for each deck as well.
+
@node Editing notes
@chapter Editing notes
@@ -447,7 +396,7 @@ Creating custom note types for gnosis is a fairly simple thing to do
First add your NEW-TYPE to @samp{gnosis-note-types}
@lisp
-(add-to-list 'gnosis-note-types 'new-type)
+(add-to-list 'gnosis-note-types "new-note-type")
@end lisp
@item
diff --git a/gnosis-algorithm.el b/gnosis-algorithm.el
index e850c9f..cee1d13 100644
--- a/gnosis-algorithm.el
+++ b/gnosis-algorithm.el
@@ -1,12 +1,14 @@
;;; gnosis-algorithm.el --- Spaced Repetition Algorithm for Gnosis -*- lexical-binding: t; -*-
-;; Copyright (C) 2023 Thanos Apollo
+;; 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 "29.1"))
+
;; 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
@@ -22,7 +24,26 @@
;;; Commentary:
-;; Work in progress
+;; Handles date calculation as well as ef & interval calculations.
+
+;; Gnosis implements a highly customizable algorithm, inspired by SM-2.
+;; Gnosis algorithm does not use user's subjective rating of a note to
+;; determine the next review interval, but instead uses the user's
+;; success or failure in recalling the answer of a note.
+
+;; Each gnosis note has an ef (easiness factor), which is a list of 3
+;; values. The last value is the total ef for a note, which will be
+;; used to determine the next interval upon a successful answer recall,
+;; the second value is the ef-decrease value, this value will be
+;; subtracted from the the total ef upon failure to recall the answer of
+;; a note, the first value is the ef increase, will be added to the
+;; total ef upon a successful recall.
+
+;; Each gnosis deck has a gnosis-algorithm-ef-threshold, it's an
+;; integer value that refers to the consecutive success or failures to
+;; recall an answer. Upon reaching the threshold, gnosis-algorithm-ef-decrease
+;; or gnosis-algorithm-ef-increase will be applied to the ef-increase or
+;; ef-decrease of note.
;;; Code:
@@ -30,7 +51,7 @@
(require 'calendar)
(defcustom gnosis-algorithm-interval '(1 3)
- "Gnosis initial interval for successful reviews.
+ "Gnosis initial interval for initial successful reviews.
First item: First interval,
Second item: Second interval."
@@ -40,11 +61,9 @@ Second item: Second interval."
(defcustom gnosis-algorithm-ef '(0.35 0.30 1.3)
"Gnosis easiness factor.
-First item : Increase factor
-Second item: Decrease factor
-Third item : Starting total ef
-
-Note: Starting total ef should not be above 3.0"
+First item : Increase value
+Second item: Decrease value
+Third item : Total ef"
:group 'gnosis
:type '(list float))
@@ -53,10 +72,43 @@ Note: Starting total ef should not be above 3.0"
Used to calcuate new interval for failed questions.
-NOTE: Do not change this value above 1"
+NOTE: This value should be less than 1.0."
+ :group 'gnosis
+ :type 'float)
+
+(defcustom gnosis-algorithm-ef-increase 0.1
+ "Value to increase ef increase value with.
+
+Increase ef-increase value by this amount for every
+`gnosis-algorithm-ef-threshold' number of successful reviews."
:group 'gnosis
:type 'float)
+(defcustom gnosis-algorithm-ef-decrease 0.2
+ "Value to decrease ef decrease value with.
+
+Decrease ef decrease value by this amount for every
+`gnosis-algorithm-ef-threshold' number of failed reviews."
+ :group 'gnosis
+ :type 'float)
+
+(defcustom gnosis-algorithm-ef-threshold 3
+ "Threshold for updating ef increase/decrease values.
+
+Refers to the number of consecutive successful or failed reviews."
+ :group 'gnosis
+ :type 'integer)
+
+(defun gnosis-algorithm-replace-at-index (index new-item list)
+ "Replace item at INDEX with NEW-ITEM in LIST."
+ (cl-loop for item in list
+ for i from 0
+ collect (if (= i index) new-item item)))
+
+(defun gnosis-algorithm-round-items (list)
+ "Round all items in LIST to 2 decimal places."
+ (cl-loop for item in list
+ collect (/ (round (* item 100)) 100.0)))
(defun gnosis-algorithm-date (&optional offset)
"Return the current date in a list (year month day).
@@ -74,76 +126,68 @@ Optional integer OFFSET is a number of days from the current date."
(defun gnosis-algorithm-date-diff (date)
"Find the difference between the current date and the given DATE.
-DATE format must be given as (yyyy mm dd)
-The structure of the given date is (YEAR MONTH DAY)."
+DATE format must be given as (year month day)."
(let ((given-date (encode-time 0 0 0 (caddr date) (cadr date) (car date))))
(- (time-to-days (current-time))
(time-to-days given-date))))
+(cl-defun gnosis-algorithm-next-ef (&key ef success increase decrease threshold
+ c-successes c-failures)
+ "Return the new EF, (increase-value decrease-value total-value)
+
+Calculate the new e-factor given existing EF and SUCCESS, either t or nil.
-(defun gnosis-algorithm-e-factor (ef success)
- "Calculate the new e-factor given existing EF and SUCCESS, either t or nil."
- (pcase success
- (`t (+ ef (car gnosis-algorithm-ef)))
- (`nil (max 1.3 (- ef (cadr gnosis-algorithm-ef))))))
+Next EF is calculated as follows:
+Upon a successful review, increase total ef value (nth 2) by
+ef-increase value (nth 0).
-(cl-defun gnosis-algorithm-next-interval (&key last-interval review-num ef success failure-factor successful-reviews successful-reviews-c fails-c fails-t initial-interval)
+Upon a failed review, decrease total ef by ef-decrease value (nth 1).
+
+For every THRESHOLD of C-SUCCESSES (consecutive successful reviews)
+reviews, increase ef-increase by INCREASE.
+
+For every THRESHOLD of C-FAILURES reviews, decrease ef-decrease value
+by DECREASE."
+ (cl-assert (listp ef) nil "Assertion failed: ef must be a list")
+ (cl-assert (booleanp success) nil "Assertion failed: success must be a boolean value")
+ (cl-assert (numberp increase) nil "Assertion failed: increase must be a number")
+ (cl-assert (numberp decrease) nil "Assertion failed: decrease must be a number")
+ (cl-assert (numberp threshold) nil "Assertion failed: threshold must be a number")
+ (let ((threshold-p (= (% (max 1 (if success c-successes c-failures)) threshold) 0))
+ (new-ef (if success (gnosis-algorithm-replace-at-index 2 (+ (nth 2 ef) (nth 0 ef)) ef)
+ (gnosis-algorithm-replace-at-index 2 (max 1.3 (- (nth 2 ef) (nth 1 ef))) ef))))
+ (cond ((and success threshold-p)
+ (setf new-ef (gnosis-algorithm-replace-at-index 0 (+ (nth 0 ef) increase) new-ef)))
+ ((and (not success) threshold-p
+ (setf new-ef (gnosis-algorithm-replace-at-index 1 (+ (nth 1 ef) decrease) new-ef)))))
+ (gnosis-algorithm-round-items new-ef)))
+
+(cl-defun gnosis-algorithm-next-interval (&key last-interval ef success successful-reviews
+ failure-factor initial-interval)
"Calculate next interval.
-- LAST-INTERVAL : The number of days since the item was last reviewed.
--review-num: Number of times the item has been reviewed.
-- EF : Easiness Factor.
-- SUCCESS : Success of the recall, ranges from 0 (unsuccessful) to 1
- (successful).
-- FF: Failure factor
-- SUCCESSFUL-REVIEWS : Number of successful reviews.
-- SUCCESSFULL-REVIEWS-C: Successful reviews in a row.
-- FAILS-C: Failed reviews in a row.
-- FAILS-T: Total failed reviews.
-- INITIAL-INTERVAL: Initial intervals for successful reviews.
-
-Returns a list of: (INTERVAL N EF) where,
-- Next review date in (yyyy mm dd) format.
-- REVIEW-NUM: Incremented by 1.
-- EF : Modified based on the recall success for the item."
- ;; Check if gnosis-algorithm-ff is lower than 1 & is total-ef above 1.3
- (cond ((>= gnosis-algorithm-ff 1)
- (error "Value of `gnosis-algorithm-ff' must be lower than 1"))
- ((< (nth 2 gnosis-algorithm-ef) 1.3)
- (error "Value of total-ef from `gnosis-algorithm-ef' must be above 1.3")))
- ;; Calculate the next easiness factor.
- (let* ((next-ef (gnosis-algorithm-e-factor ef success))
- (interval
- (cond
- ;; TODO: Rewrite this!
- ;; First successful review -> first interval
- ((and (= successful-reviews 0) success
- (car initial-interval)))
- ;; Second successful review -> second interval
- ((and (= successful-reviews 1) success)
- (cadr initial-interval))
- ;; When successful-reviews-c is above 3, use 150% or 180%
- ;; of ef depending on the value of successful-reviews
- ((and success
- (>= successful-reviews-c 3)
- (>= review-num 5)
- (> last-interval 1))
- (* (* ef (if (>= successful-reviews 10) 1.8 1.5)) last-interval))
- ((and (equal success nil)
- (> fails-c 3)
- (>= review-num 5)
- (> last-interval 1))
- ;; When fails-c is above 3, use 150% or 180% of
- ;; failure-factor depending on the value of total failed
- ;; reviews.
- (* (max (min 0.8 (* failure-factor (if (>= fails-t 10) 1.8 1.5)))
- failure-factor)
- last-interval))
- ;; For everything else
- (t (if success
- (* ef last-interval)
- (* failure-factor last-interval))))))
- (list (gnosis-algorithm-date (round interval)) next-ef)))
+
+LAST-INTERVAL: number of days since last review
+EF: Easiness factor
+SUCCESS: t if review was successful, nil otherwise
+SUCCESSFUL-REVIEWS: number of successful reviews
+FAILURE-FACTOR: factor to multiply last interval by if review was unsuccessful
+INITIAL-INTERVAL: list of initial intervals for initial successful
+reviews. Will be used to determine the next interval for the first 2
+successful reviews."
+ (cl-assert (< gnosis-algorithm-ff 1) "Value of `gnosis-algorithm-ff' must be lower than 1")
+ ;; This should only occur in testing env or when the user has made breaking changes.
+ (cl-assert (> (nth 2 ef) 1) "Total ef value must be above 1")
+ (let* ((ef (nth 2 gnosis-algorithm-ef))
+ (interval (cond ((and (= successful-reviews 0) success)
+ (car initial-interval))
+ ((and (= successful-reviews 1) success)
+ (cadr initial-interval))
+ (t (if success
+ (* ef last-interval)
+ (* failure-factor last-interval))))))
+ (gnosis-algorithm-date (round interval))))
+
(provide 'gnosis-algorithm)
;;; gnosis-algorithm.el ends here
diff --git a/gnosis-test.el b/gnosis-test.el
index 153c82b..045506a 100644
--- a/gnosis-test.el
+++ b/gnosis-test.el
@@ -118,7 +118,7 @@ by the thoracodorsal nerve."
If ask nil, leave testing env"
(interactive)
(let ((ask (y-or-n-p "Start development env (n for exit)?"))
- (testing-dir (expand-file-name gnosis-dir "testing")))
+ (testing-dir (expand-file-name "testing" gnosis-dir)))
(if ask
(progn
(unless (file-exists-p testing-dir)
diff --git a/gnosis.el b/gnosis.el
index 09d083e..7b6b507 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.1.9
+;; Version: 0.2.0
-;; Package-Requires: ((emacs "27.2") (compat "29.1.4.2") (emacsql "20240124"))
+;; Package-Requires: ((emacs "29.1") (emacsql "20240124"))
;; 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
@@ -24,14 +24,15 @@
;;; Commentary:
-;; Gnosis, pronounced "noh-sis", is a spaced repetition system for
-;; note taking & self testing, where notes are taken in a
-;; Question/Answer/Explanation format & reviewed in spaced
-;; intervals.
-;;
-;; Gnosis can help you better understand and retain the material by
-;; encouraging active engagement. It also provides a clear structure for
-;; your notes & review sessions, making it easier to study.
+;; Gnosis, is a spaced repetition system for note taking & self
+;; testing, where notes are taken in a Question/Answer/Explanation
+;; format & reviewed in spaced intervals, determined by the success or
+;; failure to recall a given answer for question.
+
+;; Gnosis implements a highly customizable algorithm, inspired by SM-2.
+;; Gnosis algorithm does not use user's subjective rating of a note to
+;; determine the next review interval, but instead uses the user's
+;; success or failure in recalling the answer of a note.
;;; Code:
@@ -109,6 +110,9 @@ When nil, the image will be displayed at its original size."
(make-directory gnosis-dir)
(make-directory gnosis-images-dir))
+(defvar gnosis-db-file (expand-file-name "gnosis.db" gnosis-dir)
+ "Gnosis database file.")
+
(defconst gnosis-db
(emacsql-sqlite-open (expand-file-name "gnosis.db" gnosis-dir))
"Gnosis database file.")
@@ -116,7 +120,7 @@ When nil, the image will be displayed at its original size."
(defvar gnosis-testing nil
"When t, warn user he is in a testing environment.")
-(defconst gnosis-db-version 1
+(defconst gnosis-db-version 2
"Gnosis database version.")
(defvar gnosis-note-types '("MCQ" "Cloze" "Basic" "Double" "y-or-n")
@@ -129,12 +133,32 @@ When nil, the image will be displayed at its original size."
"Hint input from previously added note.")
(defvar gnosis-cloze-guidance
- "Cloze questions are formatted like this:\n
+ '("Cloze questions are formatted like this:\n
{c1:Cyproheptadine} is a(n) {c2:5-HT2} receptor antagonist used to treat {c2:serotonin syndrome}
- For each `cX`-tag there will be created a cloze type note, the above
- example creates 2 cloze type notes."
- "Guidance for cloze note type.")
+ example creates 2 cloze type notes.)" . "")
+ "Guidance for cloze note type.
+
+car value is the prompt, cdr is the prewritten string.")
+
+(defvar gnosis-mcq-guidance
+ '("Write question options after the `--'. Each `-' corresponds to an option\n-Example Option 1\n-{Correct Option}\nCorrect Option must be inside {}" . "Question\n--\n- Option\n- {Correct Option}")
+ "Guidance for MCQ note type.
+
+car value is the prompt, cdr is the prewritten string.")
+
+(defcustom gnosis-mcq-separator "\n--\n"
+ "Separator for stem field and options in mcq note type.
+
+Seperate the question/stem from options."
+ :type 'string
+ :group 'gnosis)
+
+(defcustom gnosis-mcq-option-separator "-"
+ "Separator for options in mcq note type."
+ :type 'string
+ :group 'gnosis)
;;; Faces
@@ -155,9 +179,9 @@ When nil, the image will be displayed at its original size."
"Face for the main section from note."
:group 'gnosis-face-faces)
-(defface gnosis-face-seperator
+(defface gnosis-face-separator
'((t :inherit warning))
- "Face for section seperator."
+ "Face for section separator."
:group 'gnosis-face)
(defface gnosis-face-directions
@@ -231,6 +255,19 @@ Example:
"From TABLE use where to delete VALUE."
(emacsql gnosis-db `[:delete :from ,table :where ,value]))
+;; (defun gnosis-delete-note (id)
+;; "Delete note with ID."
+;; (when (y-or-n-p "Delete note?")
+;; (emacsql-with-transaction gnosis-db (gnosis--delete 'notes `(= id ,id)))))
+
+;; (defun gnosis-delete-deck (id)
+;; "Delete deck with ID."
+;; (interactive (list (gnosis--get-deck-id)))
+;; (let ((deck-name (gnosis--get-deck-name id)))
+;; (when (y-or-n-p (format "Delete deck `%s'? " deck-name))
+;; (gnosis--delete 'decks `(= id ,id))
+;; (message "Deleted deck `%s'" deck-name))))
+
(defun gnosis-replace-item-at-index (index new-item list)
"Replace item at INDEX in LIST with NEW-ITEM."
(cl-loop for i from 0 for item in list
@@ -293,7 +330,7 @@ SUCCESS is t when user-input is correct, else nil"
(let ((hint (or hint "")))
(goto-char (point-max))
(insert
- (propertize "\n\n-----\n" 'face 'gnosis-face-seperator)
+ (propertize "\n\n-----\n" 'face 'gnosis-face-separator)
(propertize hint 'face 'gnosis-face-hint))))
(cl-defun gnosis-display-cloze-reveal (&key (cloze-char gnosis-cloze-string) replace (success t) (face nil))
@@ -352,7 +389,7 @@ Refer to `gnosis-db-schema-extras' for more."
"Display extra information & extra-image for note ID."
(let ((extras (or (gnosis-get 'extra-notes 'extras `(= id ,id)) "")))
(goto-char (point-max))
- (insert (propertize "\n\n-----\n" 'face 'gnosis-face-seperator))
+ (insert (propertize "\n\n-----\n" 'face 'gnosis-face-separator))
(gnosis-display-image id 'extra-image)
(fill-paragraph (insert "\n" (propertize extras 'face 'gnosis-face-extra)))))
@@ -391,46 +428,45 @@ Set SPLIT to t to split all input given."
(error "Aborted")))
(if (gnosis-get 'name 'decks `(= name ,name))
(error "Deck `%s' already exists" name)
- (gnosis--insert-into 'decks `([nil ,name]))
+ (gnosis--insert-into 'decks `([nil ,name nil nil nil nil nil]))
(message "Created deck '%s'" name)))
-(defun gnosis--get-deck-name ()
- "Return name from table DECKS."
+(defun gnosis--get-deck-name (&optional id)
+ "Get deck name for ID, or prompt for deck name when ID is nil."
(when (equal (gnosis-select 'name 'decks) nil)
(error "No decks found"))
- (funcall gnosis-completing-read-function "Deck: " (gnosis-select 'name 'decks)))
+ (if id
+ (gnosis-get 'name 'decks `(= id ,id))
+ (funcall gnosis-completing-read-function "Deck: " (gnosis-select 'name 'decks))))
(cl-defun gnosis--get-deck-id (&optional (deck (gnosis--get-deck-name)))
"Return id for DECK name."
(gnosis-get 'id 'decks `(= name ,deck)))
-;;;###autoload
-(defun gnosis-delete-deck (deck)
- "Delete DECK."
- (interactive (list (gnosis--get-deck-name)))
- (gnosis--delete 'decks `(= name ,deck))
- (message "Deleted deck %s" deck))
-
-;; TODO: Redo this as a single function
-(cl-defun gnosis-suspend-note (id &optional (suspend 1))
- "Suspend note with ID.
-SUSPEND: 1 to suspend, 0 to unsuspend."
- (gnosis-update 'review-log `(= suspend ,suspend) `(= id ,id)))
+(cl-defun gnosis-suspend-note (id)
+ "Suspend note with ID."
+ (let ((suspended (= (gnosis-get 'suspend 'review-log `(= id ,id)) 1)))
+ (when (y-or-n-p (if suspended "Unsuspend note? " "Suspend note? "))
+ (if suspended
+ (gnosis-update 'review-log '(= suspend 0) `(= id ,id))
+ (gnosis-update 'review-log '(= suspend 1) `(= id ,id))))))
(cl-defun gnosis-suspend-deck (&optional (deck (gnosis--get-deck-id)))
"Suspend all note(s) with DECK id.
When called with a prefix, unsuspends all notes in deck."
- (let ((notes (gnosis-select 'id 'notes `(= deck-id ,deck)))
- (suspend (if current-prefix-arg 0 1))
- (note-count 0))
- (cl-loop for note in notes
- do (gnosis-update 'review-log `(= suspend ,suspend) `(= id ,(car note)))
+ (let* ((notes (gnosis-select 'id 'notes `(= deck-id ,deck) t))
+ (suspend (if current-prefix-arg 0 1))
+ (note-count 0)
+ (confirm (y-or-n-p (if (= suspend 0) "Unsuspend all notes for deck? " "Suspend all notes for deck? "))))
+ (when confirm
+ (cl-loop for note in notes
+ do (gnosis-update 'review-log `(= suspend ,suspend) `(= id ,note))
(setq note-count (1+ note-count))
finally (if (equal suspend 0)
(message "Unsuspended %s notes" note-count)
- (message "Suspended %s notes" note-count)))))
+ (message "Suspended %s notes" note-count))))))
(defun gnosis-suspend-tag ()
"Suspend all note(s) with tag.
@@ -469,15 +505,14 @@ SECOND-IMAGE: Image to display after user-input.
NOTE: If a gnosis--insert-into fails, the whole transaction will be
(or at least it should). Else there will be an error for foreign key
constraint."
- (condition-case nil
- (progn
- ;; Refer to `gnosis-db-schema-SCHEMA' e.g `gnosis-db-schema-review-log'
- (gnosis--insert-into 'notes `([nil ,type ,main ,options ,answer ,tags ,(gnosis--get-deck-id deck)]))
- (gnosis--insert-into 'review `([nil ,gnosis-algorithm-ef ,gnosis-algorithm-ff ,gnosis-algorithm-interval]))
- (gnosis--insert-into 'review-log `([nil ,(gnosis-algorithm-date) ,(gnosis-algorithm-date) 0 0 0 0 ,suspend 0]))
- (gnosis--insert-into 'extras `([nil ,extra ,image ,second-image])))
- (error (message "An error occurred during insertion"))))
-
+ (let* ((deck-id (gnosis--get-deck-id deck))
+ (initial-interval (gnosis-get-deck-initial-interval deck-id)))
+ (emacsql-with-transaction gnosis-db
+ ;; Refer to `gnosis-db-schema-SCHEMA' e.g `gnosis-db-schema-review-log'
+ (gnosis--insert-into 'notes `([nil ,type ,main ,options ,answer ,tags ,deck-id]))
+ (gnosis--insert-into 'review `([nil ,gnosis-algorithm-ef ,gnosis-algorithm-ff ,initial-interval]))
+ (gnosis--insert-into 'review-log `([nil ,(gnosis-algorithm-date) ,(gnosis-algorithm-date) 0 0 0 0 ,suspend 0]))
+ (gnosis--insert-into 'extras `([nil ,extra ,image ,second-image])))))
;; Adding note(s) consists firstly of a hidden 'gnosis-add-note--TYPE'
;; function that does the computation & error checking to generate a
@@ -508,22 +543,19 @@ is the image to display post review
(defun gnosis-add-note-mcq ()
"Add note(s) of type `MCQ' interactively to selected deck.
-Create a note type MCQ for specified deck, that consists of:
-QUESTION: The question or problem statement
-OPTIONS: Options for the user to select
-ANSWER: Answer is the index NUMBER of the correct answer from OPTIONS.
-EXTRA: Information to display after user-input
-IMAGES: Cons cell, where car is the image to display before user-input
- and cdr is the image to display post review.
-TAGS: Used to organize notes
+Prompt user for input to create a note of type `MCQ'.
-Refer to `gnosis-add-note--mcq' for more."
+Stem field is seperated from options by `gnosis-mcq-separator', and
+each option is seperated by `gnosis-mcq-option-separator'. The correct
+answer is surrounded by curly braces, e.g {Correct Answer}.
+
+Refer to `gnosis-add-note--mcq' & `gnosis-prompt-mcq-input' for more."
(let ((deck (gnosis--get-deck-name)))
(while (y-or-n-p (format "Add note of type `MCQ' to `%s' deck? " deck))
- (let* ((stem (read-string-from-buffer "Question: " ""))
- (input-choices (gnosis-prompt-mcq-choices))
- (choices (car input-choices))
- (correct-choice (cadr input-choices)))
+ (let* ((input (gnosis-prompt-mcq-input))
+ (stem (caar input))
+ (choices (cdr (car input)))
+ (correct-choice (cadr input)))
(gnosis-add-note--mcq :deck deck
:question stem
:choices choices
@@ -709,7 +741,8 @@ See `gnosis-add-note--cloze' for more reference."
(let ((deck (gnosis--get-deck-name)))
(while (y-or-n-p (format "Add note of type `cloze' to `%s' deck? " deck))
(gnosis-add-note--cloze :deck deck
- :note (read-string-from-buffer gnosis-cloze-guidance "")
+ :note (read-string-from-buffer (or (car gnosis-cloze-guidance) "")
+ (or (cdr gnosis-cloze-guidance) ""))
:hint (gnosis-hint-prompt gnosis-previous-note-hint)
:extra (read-string-from-buffer "Extra" "")
:images (gnosis-select-images)
@@ -745,12 +778,12 @@ Works both with {} and {{}} to make easier to import anki notes."
"In STRING replace only the first occurrence of each word in WORDS with NEW."
(cl-assert (listp words))
(cl-loop for word in words
- do (if (string-match (concat "\\<" word "\\>") string)
- (setq string (replace-match new t t string))
- ;; This error will be produced when user has edited a
- ;; note to an invalid cloze.
- (error "`%s' is an invalid cloze for question: `%s'."
- word string )))
+ do (if (string-match (regexp-quote word) string)
+ (setq string (replace-match new t t string))
+ ;; This error will be produced when user has edited a
+ ;; note to an invalid cloze.
+ (error "`%s' is an invalid cloze for question: `%s'"
+ word string)))
string)
(defun gnosis-cloze-extract-answers (str)
@@ -779,9 +812,13 @@ Valid cloze formats include:
"Compare STR1 and STR2.
Compare 2 strings, ignoring case and whitespace."
- (<= (string-distance (downcase (replace-regexp-in-string "\\s-" "" str1))
- (downcase (replace-regexp-in-string "\\s-" "" str2)))
- gnosis-string-difference))
+ (let ((string-compare-func (if (or (> (length str1) gnosis-string-difference)
+ (> (length str2) gnosis-string-difference))
+ #'(lambda (str1 str2) (<= (string-distance str1 str2) gnosis-string-difference))
+ #'string=)))
+ (funcall string-compare-func
+ (downcase (replace-regexp-in-string "\\s-" "" str1))
+ (downcase (replace-regexp-in-string "\\s-" "" str2)))))
(defun gnosis-directory-files (&optional dir regex)
@@ -832,7 +869,7 @@ Optionally, add cusotm PROMPT."
"Return note ID's for every note with INPUT-TAGS."
(unless (listp input-tags)
(error "`input-tags' need to be a list"))
- (cl-loop for (id tags) in (emacsql gnosis-db [:select [id tags] :from notes])
+ (cl-loop for (id tags) in (gnosis-select '[id tags] 'notes)
when (and (cl-every (lambda (tag) (member tag tags)) input-tags)
(not (gnosis-suspended-p id)))
collect id))
@@ -887,20 +924,34 @@ Returns a list of unique tags."
(reverse tags)))
(defun gnosis-hint-prompt (previous-hint &optional prompt)
+ "Prompt user for hint.
+
+PROMPT: Prompt string value
+PREVIOUS-HINT: Previous hint value, if any. If nil, use PROMPT as
+default value."
(let* ((prompt (or prompt "Hint: "))
(hint (read-string prompt previous-hint)))
(setf gnosis-previous-note-hint hint)
hint))
-(defun gnosis-prompt-mcq-choices ()
- "Prompt user for mcq choices."
- (let* ((input (split-string
- (read-string-from-buffer "Options\nEach '-' corresponds to an option\n-Example Option 1\n-Example Option 2\nYou can add as many options as you want\nCorrect Option must be inside {}" "-\n-")
- "-" t "[\s\n]"))
- (correct-choice-index (or (cl-position-if (lambda (string) (string-match "{.*}" string)) input)
- (error "Correct choice not found. Use {} to indicate the correct opiton")))
- (choices (mapcar (lambda (string) (replace-regexp-in-string "{\\|}" "" string)) input)))
- (list choices (+ correct-choice-index 1))))
+(defun gnosis-prompt-mcq-input ()
+ "Prompt for MCQ content.
+
+Return a list of the form ((QUESTION CHOICES) CORRECT-CHOICE-INDEX)."
+ (let ((user-input (read-string-from-buffer (or (car gnosis-mcq-guidance) "")
+ (or (cdr gnosis-mcq-guidance) ""))))
+ (unless (string-match-p gnosis-mcq-separator user-input)
+ (error "Separator %s not found" gnosis-mcq-separator))
+ (let* ((input-seperated (split-string user-input gnosis-mcq-separator t "[\s\n]"))
+ (stem (car input-seperated))
+ (input (split-string
+ (mapconcat 'identity (cdr input-seperated) "\n")
+ gnosis-mcq-option-separator t "[\s\n]"))
+ (correct-choice-index
+ (or (cl-position-if (lambda (string) (string-match "{.*}" string)) input)
+ (error "Correct choice not found. Use {} to indicate the correct option")))
+ (choices (mapcar (lambda (string) (replace-regexp-in-string "{\\|}" "" string)) input)))
+ (list (cons stem choices) (+ correct-choice-index 1)))))
(defun gnosis-prompt-tags--split (&optional previous-note-tags)
"Prompt user for tags, split string by space.
@@ -946,57 +997,45 @@ well."
due-notes)
:test #'equal)))
-(defun gnosis-review--algorithm (id success)
+(defun gnosis-review-algorithm (id success)
"Return next review date & ef for note with value of id ID.
SUCCESS is a boolean value, t for success, nil for failure.
-Returns a list of the form ((yyyy mm dd) ef)."
+Returns a list of the form ((yyyy mm dd) (ef-increase ef-decrease ef-total))."
(let ((ff gnosis-algorithm-ff)
- (ef (nth 2 (gnosis-get 'ef 'review `(= id ,id))))
- (t-success (gnosis-get 't-success 'review-log `(= id ,id)))
- (c-success (gnosis-get 'c-success 'review-log `(= id ,id)))
- (c-fails (gnosis-get 'c-fails 'review-log `(= id ,id)))
- (t-fails (gnosis-get 't-fails 'review-log `(= id ,id)))
- (initial-interval (gnosis-get 'interval 'review `(= id ,id))))
- (gnosis-algorithm-next-interval :last-interval (max (gnosis-review--get-offset id) 1) ;; last-interv always >=1
- :review-num (gnosis-get 'n 'review-log `(= id ,id))
- :ef ef
+ (ef (gnosis-get 'ef 'review `(= id ,id)))
+ (t-success (gnosis-get 't-success 'review-log `(= id ,id))) ;; total successful reviews
+ (c-success (gnosis-get 'c-success 'review-log `(= id ,id))) ;; consecutive successful reviews
+ (c-fails (gnosis-get 'c-fails 'review-log `(= id ,id))) ;; consecutive failed reviews
+ ;; (t-fails (gnosis-get 't-fails 'review-log `(= id ,id))) ;; total failed reviews
+ ;; (review-num (gnosis-get 'n 'review-log `(= id ,id))) ;; total reviews
+ (last-interval (max (gnosis-review--get-offset id) 1))) ;; last interval
+ (list (gnosis-algorithm-next-interval :last-interval last-interval
+ :ef ef
+ :success success
+ :successful-reviews t-success
+ :failure-factor ff
+ :initial-interval (gnosis-get-note-initial-interval id))
+ (gnosis-algorithm-next-ef :ef ef
:success success
- :failure-factor ff
- :successful-reviews t-success
- :successful-reviews-c c-success
- :fails-c c-fails
- :fails-t t-fails
- :initial-interval initial-interval)))
+ :increase (gnosis-get-ef-increase id)
+ :decrease (gnosis-get-ef-decrease id)
+ :threshold (gnosis-get-ef-threshold id)
+ :c-successes c-success
+ :c-failures c-fails))))
(defun gnosis-review--get-offset (id)
"Return offset for note with value of id ID."
(let ((last-rev (gnosis-get 'last-rev 'review-log `(= id ,id))))
(gnosis-algorithm-date-diff last-rev)))
-(defun gnosis-review-round (num)
- "Round NUM to 2 decimals.
-
-This function is used to round floating point numbers to 2 decimals,
-such as the easiness factor (ef)."
- (/ (round (* num 100.00)) 100.00))
-
-(defun gnosis-review-new-ef (id success)
- "Return new ef for note with value of id ID.
-
-Returns a list of the form (ef-increase ef-decrease ef).
-SUCCESS is a boolean value, t for success, nil for failure."
- (let ((ef (nth 1 (gnosis-review--algorithm id success)))
- (old-ef (gnosis-get 'ef 'review `(= id ,id))))
- (cl-substitute (gnosis-review-round ef) (nth 2 old-ef) old-ef)))
-
(defun gnosis-review--update (id success)
"Update review-log for note with value of id ID.
SUCCESS is a boolean value, t for success, nil for failure."
- (let ((ef (gnosis-review-new-ef id success))
- (next-rev (car (gnosis-review--algorithm id success))))
+ (let ((ef (cadr (gnosis-review-algorithm id success)))
+ (next-rev (car (gnosis-review-algorithm id success))))
;; Update review-log
(gnosis-update 'review-log `(= last-rev ',(gnosis-algorithm-date)) `(= id ,id))
(gnosis-update 'review-log `(= next-rev ',next-rev) `(= id ,id))
@@ -1129,7 +1168,9 @@ Used to reveal all clozes left with `gnosis-face-cloze-unanswered' face."
"Run `vc-pull' in DIR."
(interactive)
(let ((default-directory dir))
- (vc-pull)))
+ (vc-pull)
+ ;; Reopen gnosis-db after pull
+ (setf gnosis-db (emacsql-sqlite-open (expand-file-name "gnosis.db" dir)))))
(defun gnosis-review-commit (note-num)
"Commit review session on git repository.
@@ -1174,7 +1215,7 @@ NOTES: List of note ids"
(?q "quit"))))
(?n nil)
(?s (gnosis-suspend-note note))
- (?e (gnosis-edit-note note)
+ (?e (gnosis-edit-note note t)
(recursive-edit))
(?q (gnosis-review-commit note-count)
(cl-return)))
@@ -1183,14 +1224,14 @@ NOTES: List of note ids"
;; Editing notes
(defun gnosis-edit-read-only-values (&rest values)
- "Makes the provided values read-only in the whole buffer."
+ "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)))
-(defun gnosis-edit-note (id)
+(cl-defun gnosis-edit-note (id &optional (recursive-edit nil))
"Edit the contents of a note with the given ID.
This function creates an Emacs Lisp buffer named *gnosis-edit* on the
@@ -1226,17 +1267,106 @@ changes."
;; 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"
- ":ef" ":ff" ":suspend"))
-
-(defun gnosis-edit-save-exit ()
- "Save edits and exit."
+ ":ef" ":ff" ":suspend")
+ (local-unset-key (kbd "C-c C-c"))
+ (local-set-key (kbd "C-c C-c") (lambda () (interactive) (if recursive-edit
+ (gnosis-edit-save-exit 'exit-recursive-edit)
+ (gnosis-edit-save-exit 'gnosis-dashboard "Notes")))))
+
+(defun gnosis-edit-deck--export (id)
+ "Export deck with ID.
+
+WARNING: This export is only for editing said deck!
+
+Insert deck values:
+ `ef-increase', `ef-decrease', `ef-threshold', `failure-factor'"
+ (let ((name (gnosis-get 'name 'decks `(= id ,id)))
+ (ef-increase (gnosis-get 'ef-increase 'decks `(= id ,id)))
+ (ef-decrease (gnosis-get 'ef-decrease 'decks `(= id ,id)))
+ (ef-threshold (gnosis-get 'ef-threshold 'decks `(= id ,id)))
+ (failure-factor (gnosis-get 'failure-factor 'decks `(= id ,id)))
+ (initial-interval (gnosis-get 'initial-interval 'decks `(= id ,id))))
+ (insert
+ (format "\n:id %s\n:name \"%s\"\n:ef-increase %s\n:ef-decrease %s\n:ef-threshold %s\n:failure-factor %s\n :initial-interval '%s"
+ id name ef-increase ef-decrease ef-threshold failure-factor initial-interval))))
+
+(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-update-deck (&key id name ef-increase ef-decrease ef-threshold failure-factor initial-interval)
+ "Update deck with id value of ID.
+
+NAME: Name of deck
+EF-INCREASE: Easiness factor increase value
+EF-DECREASE: Easiness factor decrease value
+EF-THRESHOLD: Easiness factor threshold value
+FAILURE-FACTOR: Failure factor value
+INITIAL-INTERVAL: Initial interval for notes of deck"
+ (gnosis-assert-float-or-nil failure-factor "failure-factor must be a float less than 1" t)
+ (gnosis-assert-int-or-nil ef-threshold "ef-threshold must be an integer")
+ (gnosis-assert-number-or-nil ef-increase "ef-increase must be a number")
+ (cl-assert (or (and (listp initial-interval)
+ (and (cl-every #'integerp initial-interval)
+ (length= initial-interval 2)))
+ (null initial-interval))
+ nil "Initial-interval must be a list of 2 integers")
+ (cl-loop for (field . value) in
+ `((ef-increase . ,ef-increase)
+ (ef-decrease . ,ef-decrease)
+ (ef-threshold . ,ef-threshold)
+ (failure-factor . ,failure-factor)
+ (initial-interval . ',initial-interval)
+ (name . ,name))
+ when value
+ do (gnosis-update 'decks `(= ,field ,value) `(= id ,id))))
+
+(defun gnosis-edit-deck (&optional id)
+ "Edit the contents of a deck with the given ID."
+ (interactive "P")
+ (let ((id (or id (gnosis--get-deck-id))))
+ (pop-to-buffer-same-window (get-buffer-create "*gnosis-edit*"))
+ (gnosis-edit-mode)
+ (erase-buffer)
+ (insert ";;\n;; You are editing a gnosis deck.\n\n")
+ (insert "(gnosis-edit-update-deck ")
+ (gnosis-edit-deck--export 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))
+ (gnosis-edit-read-only-values (format ":id %s" id) ":name" ":ef-increase"
+ ":ef-decrease" ":ef-threshold" ":failure-factor")
+ (local-unset-key (kbd "C-c C-c"))
+ (local-set-key (kbd "C-c C-c") (lambda () (interactive) (gnosis-edit-save-exit 'gnosis-dashboard "Decks")))))
+
+(cl-defun gnosis-edit-save-exit (&optional exit-func &rest args)
+ "Save edits and exit using EXIT-FUNC, with ARGS."
(interactive)
(eval-buffer)
(quit-window t)
- ;; exit recursive edit if we are in one
- (if (>= (recursion-depth) 1)
- (exit-recursive-edit)
- (gnosis-dashboard)))
+ (when exit-func
+ (apply exit-func args)))
(defvar-keymap gnosis-edit-mode-map
:doc "gnosis-edit keymap"
@@ -1244,7 +1374,7 @@ changes."
(define-derived-mode gnosis-edit-mode emacs-lisp-mode "Gnosis EDIT"
"Gnosis Edit Mode."
- :interactive t
+ :interactive nil
:lighter " Gnosis Edit"
:keymap gnosis-edit-mode-map)
@@ -1255,12 +1385,24 @@ changes."
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, user is asked for input, if equal user-input
-= answer review is marked as successfull
+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"
+SECOND-IMAGE: Image to display after user-input
+EF: Easiness factor value
+FF: Failure factor 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 (listp tags) nil "Tags must be a list of strings")
+ (cl-assert (and (listp ef) (length= ef 3)) nil "ef 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")
;; Construct the update clause for the emacsql update statement.
(cl-loop for (field . value) in
`((main . ,main)
@@ -1288,10 +1430,36 @@ SECOND-IMAGE: Image to display after user-input"
"Return a list of ID vlaues for each note with value of deck-id DECK."
(gnosis-select 'id 'notes `(= deck-id ,deck) t))
+(defun gnosis-get-ef-increase (id)
+ "Return ef-increase for note with value of id ID."
+ (let ((ef-increase (gnosis-get 'ef-increase 'decks `(= id ,(gnosis-get 'deck-id 'notes `(= id ,id))))))
+ (or ef-increase gnosis-algorithm-ef-increase)))
+
+(defun gnosis-get-ef-decrease (id)
+ "Return ef-decrease for note with value of id ID."
+ (let ((ef-decrease (gnosis-get 'ef-decrease 'decks `(= id ,(gnosis-get 'deck-id 'notes `(= id ,id))))))
+ (or ef-decrease gnosis-algorithm-ef-decrease)))
+
+(defun gnosis-get-ef-threshold (id)
+ "Return ef-threshold for note with value of id ID."
+ (let ((ef-threshold (gnosis-get 'ef-threshold 'decks `(= id ,(gnosis-get 'deck-id 'notes `(= id ,id))))))
+ (or ef-threshold gnosis-algorithm-ef-threshold)))
+
+(defun gnosis-get-deck-initial-interval (id)
+ "Return initial-interval for notes of deck ID."
+ (let ((initial-interval (gnosis-get 'initial-interval 'decks `(= id ,id))))
+ (or initial-interval gnosis-algorithm-interval)))
+
+(defun gnosis-get-note-initial-interval (id)
+ "Return initial-interval for note with ID."
+ (let ((deck-id (gnosis-get 'deck-id 'notes `(= id ,id))))
+ (gnosis-get-deck-initial-interval deck-id)))
+
(cl-defun gnosis-export-note (id &optional (export-for-deck nil))
"Export fields for note with value of id ID.
ID: Identifier of the note to export.
+EXPORT-FOR-DECK: If t, add type field and remove review fields
This function retrieves the fields of a note with the given ID and
inserts them into the current buffer. Each field is represented as a
@@ -1329,61 +1497,6 @@ to improve readability."
(format "\n%s '%s" (symbol-name field) (prin1-to-string value)))
(t (format "\n%s %s" (symbol-name field) (prin1-to-string value))))))))
-;; TODO: Fix export of deck!
-(defun gnosis-export-deck (deck export-deck-name filename)
- "Export notes for deck in FILENAME.
-
-WARNING: This function is not yet implemented.
-
-FILENAME: The name of the file to save the exported deck.
-
-This function prompts the user to provide a deck name and allows the
-user to specify a filename for exporting notes belonging to that deck.
-It then retrieves all the notes associated with the deck and exports
-them.
-
-The exported notes are formatted as an Emacs Lisp code block that can
-be evaluated to recreate the deck with its associated notes. The
-resulting code is saved to a file with the provided FILENAME and a
-'.el' extension is added automatically.
-
-Each note is exported using the `gnosis-export-note` function. The
-generated code includes a call to `gnosis-define-deck` with the deck
-name and all notes formatted as nested lists"
- ;; (interactive (list (gnosis-get-notes-for-deck)
- ;; (read-string "Export deck as (name): ")
- ;; (read-string "Filename: ")))
- (with-temp-file (concat filename ".el")
- (insert "(gnosis-define-deck " "'" export-deck-name " '(")
- (cl-loop for note in deck
- do (insert "(") (gnosis-export-note note t) (insert ")" "\n")
- finally (insert "))"))))
-
-;; TODO: Add defcustom to have suspended as 0 or 1 depending on
-;; gnosis-add-decks-suspended t or nil
-(cl-defun gnosis-define-deck (deck notes &optional (suspended 0))
- "Define DECK consisting of NOTES, optionally add them as SUSPENDED."
- (gnosis-add-deck (symbol-name deck))
- (sit-for 0.1)
- (cl-loop for note in notes
- do (let ((type (plist-get note :type))
- (main (plist-get note :main))
- (options (plist-get note :options))
- (answer (plist-get note :answer))
- (extra-notes (plist-get note :extra-notes))
- (tags (plist-get note :tags))
- (suspend (plist-get note :suspend))
- (image (plist-get note :image))
- (second-image (plist-get note :second-image)))
- (gnosis-add-note-fields deck type main options answer extra-notes tags suspend image second-image))
- collect note))
-
-;; Rewrite this similarly to gnosis
-(cl-defun gnosis-define-deck--note (&keys deck type main options answer extra-notes tags image second-image)
- "Define a note for DECK."
- (gnosis-add-note-fields deck type main options answer extra-notes tags 0 image second-image))
-
-
;;;###autoload
(defun gnosis-review ()
"Start gnosis review session."
@@ -1401,7 +1514,12 @@ name and all notes formatted as nested lists"
;;; Database Schemas
(defvar gnosis-db-schema-decks '([(id integer :primary-key :autoincrement)
- (name text :not-null)]))
+ (name text :not-null)
+ (failure-factor float)
+ (ef-increase float)
+ (ef-decrease float)
+ (ef-threshold integer)
+ (initial-interval listp)]))
(defvar gnosis-db-schema-notes '([(id integer :primary-key :autoincrement)
(type text :not-null)
@@ -1452,7 +1570,7 @@ name and all notes formatted as nested lists"
;; Dashboard
(defun gnosis-dashboard-output-note (id)
- "Output note contents formatted for gnosis dashboard."
+ "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)
@@ -1463,8 +1581,82 @@ name and all notes formatted as nested lists"
(defun gnosis-dashboard-output-notes ()
"Return note contents for gnosis dashboard."
(let ((max-id (apply 'max (gnosis-select 'id 'notes '1=1 t))))
- (cl-loop for id from 1 to max-id collect
- (list (number-to-string id) (vconcat (gnosis-dashboard-output-note id))))))
+ (setq tabulated-list-format [("Main" 30 t)
+ ("Options" 20 t)
+ ("Answer" 25 t)
+ ("Tags" 25 t)
+ ("Type" 10 t)
+ ("Suspend" 2 t)])
+ (tabulated-list-init-header)
+ (setf tabulated-list-entries
+ (cl-loop for id from 1 to max-id
+ for output = (gnosis-dashboard-output-note id)
+ when output
+ collect (list (number-to-string id) (vconcat output))))
+ ;; Keybindings, for editing, suspending, deleting notes.
+ ;; We use `local-set-key' to bind keys to the buffer to avoid
+ ;; conflicts when using the dashboard for displaying either notes
+ ;; or decks.
+ (local-set-key (kbd "e") #'gnosis-dashboard-edit-note)
+ (local-set-key (kbd "s") #'(lambda () (interactive)
+ (gnosis-suspend-note
+ (string-to-number (tabulated-list-get-id)))
+ (gnosis-dashboard-output-notes)
+ (revert-buffer t t t)))
+ ;; (local-set-key (kbd "d") #'(lambda () (interactive)
+ ;; (gnosis-delete-note
+ ;; (string-to-number (tabulated-list-get-id)))
+ ;; (gnosis-dashboard-output-notes)
+ ;; (revert-buffer t t t)))
+ (local-set-key (kbd "a") #'gnosis-add-note)))
+
+(defun gnosis-dashboard-deck-note-count (id)
+ "Return total note count for deck with ID."
+ (let ((note-count (caar (emacsql gnosis-db (format "SELECT COUNT(*) FROM notes WHERE deck_id=%s" id)))))
+ (when (gnosis-select 'id 'decks `(= id ,id))
+ (list (number-to-string note-count)))))
+
+(defun gnosis-dashboard-output-deck (id)
+ "Output contents from deck with ID, formatted for gnosis dashboard."
+ (cl-loop for item in (append (gnosis-select
+ '[name failure-factor ef-increase ef-decrease ef-threshold initial-interval]
+ 'decks `(= id ,id) t)
+ (mapcar 'string-to-number (gnosis-dashboard-deck-note-count id)))
+ when (listp item)
+ do (cl-remove-if (lambda (x) (and (vectorp x) (zerop (length x)))) item)
+ collect (prin1-to-string item)))
+
+(defun gnosis-dashboard-output-decks ()
+ "Return deck contents for gnosis dashboard."
+ (setq tabulated-list-format [("Name" 15 t)
+ ("failure-factor" 15 t)
+ ("ef-increase" 15 t)
+ ("ef-decrease" 15 t)
+ ("ef-threshold" 15 t)
+ ("Initial Interval" 20 t)
+ ("Total Notes" 10 t)])
+ (tabulated-list-init-header)
+ (let ((max-id (apply 'max (gnosis-select 'id 'decks '1=1 t))))
+ (setq tabulated-list-entries
+ (cl-loop for id from 1 to max-id
+ for output = (gnosis-dashboard-output-deck id)
+ when output
+ collect (list (number-to-string id) (vconcat output)))))
+ (local-set-key (kbd "e") #'gnosis-dashboard-edit-deck)
+ (local-set-key (kbd "a") #'(lambda () (interactive)
+ (gnosis-add-deck (read-string "Deck name: "))
+ (gnosis-dashboard-output-decks)
+ (revert-buffer t t t)))
+ (local-set-key (kbd "s") #'(lambda () (interactive)
+ (gnosis-suspend-deck
+ (string-to-number (tabulated-list-get-id)))
+ (gnosis-dashboard-output-decks)
+ (revert-buffer t t t))))
+ ;; (local-set-key (kbd "d") #'(lambda () (interactive)
+ ;; (gnosis-delete-deck
+ ;; (string-to-number (tabulated-list-get-id)))
+ ;; (gnosis-dashboard-output-decks)
+ ;; (revert-buffer t t t))))
(defun gnosis-dashboard-edit-note ()
"Get note id from tabulated list and edit it."
@@ -1473,53 +1665,67 @@ name and all notes formatted as nested lists"
(gnosis-edit-note (string-to-number id))
(message "Editing note with id: %s" id)))
+(defun gnosis-dashboard-edit-deck ()
+ "Get deck id from tabulated list and edit it."
+ (interactive)
+ (let ((id (tabulated-list-get-id)))
+ (gnosis-edit-deck (string-to-number id))))
+
(defvar-keymap gnosis-dashboard-mode-map
:doc "gnosis-dashboard keymap"
- "e" #'gnosis-dashboard-edit-note
"q" #'quit-window)
(define-derived-mode gnosis-dashboard-mode tabulated-list-mode "Gnosis Dashboard"
"Major mode for displaying Gnosis dashboard."
:keymap gnosis-dashboard-mode-map
- (interactive)
(display-line-numbers-mode 0)
- (setq tabulated-list-format [("Main" 30 t)
- ("Options" 20 t)
- ("Answer" 25 t)
- ("Tags" 25 t)
- ("Type" 10 t)
- ("Suspend" 2 t)])
(setq tabulated-list-padding 2
- tabulated-list-sort-key nil)
- (tabulated-list-init-header))
+ tabulated-list-sort-key nil))
;;;###autoload
-(defun gnosis-dashboard ()
- "Display gnosis dashboard."
+(cl-defun gnosis-dashboard (&optional dashboard-type)
+ "Display gnosis dashboard.
+
+DASHBOARD-TYPE: either 'Notes' or 'Decks' to display the respective dashboard."
(interactive)
- (pop-to-buffer "*gnosis-dashboard*" nil)
- (gnosis-dashboard-mode)
- (setq tabulated-list-entries
- (gnosis-dashboard-output-notes))
- (tabulated-list-print t))
+ (let ((type (or dashboard-type
+ (cadr (read-multiple-choice
+ "Display dashboard for:"
+ '((?N "Notes")
+ (?D "Decks")))))))
+ (pop-to-buffer "*gnosis-dashboard*")
+ (gnosis-dashboard-mode)
+ (pcase type
+ ("Notes" (gnosis-dashboard-output-notes))
+ ("Decks" (gnosis-dashboard-output-decks)))
+ (tabulated-list-print t)))
(defun gnosis-db-init ()
"Create gnosis essential directories & database."
- (unless (length= (emacsql gnosis-db [:select name :from sqlite-master :where (= type table)]) 6)
- ;; Enable foreign keys
- (emacsql gnosis-db "PRAGMA foreign_keys = ON")
- ;; Gnosis version
- (emacsql gnosis-db (format "PRAGMA user_version = %s" gnosis-db-version))
- ;; Create decks table
- (gnosis--create-table 'decks gnosis-db-schema-decks)
- ;; Create notes table
- (gnosis--create-table 'notes gnosis-db-schema-notes)
- ;; Create review table
- (gnosis--create-table 'review gnosis-db-schema-review)
- ;; Create review-log table
- (gnosis--create-table 'review-log gnosis-db-schema-review-log)
- ;; Create extras table
- (gnosis--create-table 'extras gnosis-db-schema-extras)))
+ (let ((gnosis-curr-version (caar (emacsql gnosis-db (format "PRAGMA user_version")))))
+ (unless (length= (emacsql gnosis-db [:select name :from sqlite-master :where (= type table)]) 6)
+ ;; Enable foreign keys
+ (emacsql gnosis-db "PRAGMA foreign_keys = ON")
+ ;; Gnosis version
+ (emacsql gnosis-db (format "PRAGMA user_version = %s" gnosis-db-version))
+ ;; Create decks table
+ (gnosis--create-table 'decks gnosis-db-schema-decks)
+ ;; Create notes table
+ (gnosis--create-table 'notes gnosis-db-schema-notes)
+ ;; Create review table
+ (gnosis--create-table 'review gnosis-db-schema-review)
+ ;; Create review-log table
+ (gnosis--create-table 'review-log gnosis-db-schema-review-log)
+ ;; Create extras table
+ (gnosis--create-table 'extras gnosis-db-schema-extras))
+ ;; Update database schema for version
+ (cond ((= gnosis-curr-version 1) ;; Update to version 2
+ (emacsql gnosis-db [:alter-table decks :add failure-factor])
+ (emacsql gnosis-db [:alter-table decks :add ef-increase])
+ (emacsql gnosis-db [:alter-table decks :add ef-decrease])
+ (emacsql gnosis-db [:alter-table decks :add ef-threshold])
+ (emacsql gnosis-db [:alter-table decks :add initial-interval])
+ (emacsql gnosis-db (format "PRAGMA user_version = %s" gnosis-db-version))))))
(gnosis-db-init)