summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThanos Apollo <[email protected]>2024-08-07 15:04:00 +0300
committerThanos Apollo <[email protected]>2024-08-07 15:04:00 +0300
commitbc0d2a3ec6195d79903ad7badec10a26538763c2 (patch)
tree62051e3309b420242e4809b02990bd7531308f1b
parent4cd3ce6993a9b97b698537c428addf7387b13a3f (diff)
parentcaae059a0988cdb08ef01f1adc695972300fe09c (diff)
Release version 0.4.00.4.0
* Major rewrite on gnosis algorithm. * Add gnosis-score * Add epignosis and agnoia * Add lethe and anagnosis events * Anagnosis events adjust gnosis-score depending on review performance, using epignosis and agnoia * Lethe resets next interval to 0 * Refactor calculations of next interval and gnosis-score * Add custom variables for tags and decks, configured using emacs lisp. * Rewrite gnosis database. * Remove deck specific values. * Use new algorithm variables.
-rw-r--r--TODO.org43
-rw-r--r--doc/gnosis.info318
-rw-r--r--doc/gnosis.org217
-rw-r--r--doc/gnosis.texi272
-rw-r--r--gnosis-algorithm.el197
-rw-r--r--gnosis-dashboard.el576
-rw-r--r--gnosis-test.el460
-rw-r--r--gnosis.el1137
8 files changed, 2092 insertions, 1128 deletions
diff --git a/TODO.org b/TODO.org
deleted file mode 100644
index 951f5f0..0000000
--- a/TODO.org
+++ /dev/null
@@ -1,43 +0,0 @@
-#+title: TODO's for Gnosis
-#+author: Thanos Apollo
-#+startup: content
-
-
-* Notes
-** TODO Add export deck
-** TODO Add support for org-mode
-+ Create gnosis notes using =org-mode=
-* Dashboard
-** DONE Add Dashboard
-CLOSED: [2024-02-20 Tue 13:33]
-+ Create a dashboard to view all notes created, user can edit &
- suspend notes. Use tabulated-list-mode, preferably.
-** TODO Dashboard: Improve Performance
-+ emacsql is quite fast, but the current tabulated-list implementation
- can be quite slow when having >30K notes. Consider alternatives to tabulated-list
-** DONE Dashboard: Add filtering/search
-CLOSED: [2024-04-20 Sat 12:54]
-- [x] Search using tags
-- [x] Search/Filter for main/answer
-* Misc
-** DONE Refactor =completing-read= UI choices
-CLOSED: [2024-02-17 Sat 21:59]
-/DONE on version 0.1.7/
-
-=completing-read= is not an ideal solution as a UI. If user has not
-enabled a completion system, such as vertico, this would make gnosis
-unusable.
-
-One possible solution is to create defcustom =gnosis-completing-read-function=
-that has ido-completing-read by default if vertico/ivy/helm is not
-enabled
-*** Notes
-Implemented =gnosis-completing-read-function=
-
-** TODO Use vc instead of shell commands
-- [x] Push & Pull commands /DONE on version 0.1.7/
-- [] stage & commit
-
-
-
-
diff --git a/doc/gnosis.info b/doc/gnosis.info
index 6bab4f5..69f63bc 100644
--- a/doc/gnosis.info
+++ b/doc/gnosis.info
@@ -12,19 +12,12 @@ File: gnosis.info, Node: Top, Next: Introduction, Up: (dir)
Gnosis User Manual
******************
-Gnosis (γνῶσις), pronounced "noh-sis", _meaning knowledge in Greek_, is
-a spaced repetition system implementation for note taking and self
-testing. Notes are organized in a Question/Answer/Explanation format
-and reviewed at spaced intervals, determined by the success or failure
-to recall the answer.
+Gnosis 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.
- The goal of Gnosis is to enhance memory retention through active
-recall. To achieve optimal results, users review Gnosis notes by
-writing out the answers.
-
- Above all, Gnosis aspires to be a versatile instrument of learning.
-
-This manual is written for Gnosis version 0.3.1, released on 2024-07-15.
+This manual is written for Gnosis version 0.4.0, released on 2024-08-7.
• Official manual: <https://thanosapollo.org/user-manual/gnosis>
• Git repositories:
@@ -39,15 +32,15 @@ This manual is written for Gnosis version 0.3.1, released on 2024-07-15.
* Gnosis Algorithm::
* Editing notes::
* Sync between devices::
-* Extending Gnosis::
+* Configuring Note Types::
-- The Detailed Node Listing --
Note Types
* Cloze::
-* MC-Cloze::
-* MCQ (Multiple Choice Question)::
+* MC-Cloze (Under development)::
+* MCQ::
* Basic Type::
* Double::
* y-or-n::
@@ -59,13 +52,14 @@ Customization
Gnosis Algorithm
-* Initial Interval::
-* Easiness Factor::
-* Forgetting Factor::
+* Anagnosis Event::
+* Proto::
-Extending Gnosis
+Configuring Note Types
+* Adjust Current Types Entries::
* Creating Custom Note Types::
+* Development::

@@ -74,19 +68,23 @@ File: gnosis.info, Node: Introduction, Next: Adding notes, Prev: Top, Up: To
1 Introduction
**************
-Before reading this manual, it's recommended you first try out
-‘gnosis-demo’
+Gnosis (γνῶσις) is a spaced repetition system that enhances memory
+retention through active recall. It employs a Q&A format, where each
+note consists of a question, answer, and explanation. Notes are
+reviewed at optimally spaced intervals based on the user's success or
+failure to recall the answer. Key benefits arise from writing out
+answers when reviewing notes, fostering deeper understanding and
+improved memory retention.
- 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 algorithm is highly adjustable, allowing users to set specific
+values not just for note decks but for tags as well. Gnosis'
+adjustability allows users to fine-tune settings not only for entire
+note collections but also for specific tagged topics, thereby creating a
+personalized learning environment for each topic. Read more on *note
+Gnosis Algorithm::
- 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::
+ Before continuing reading this manual, it's recommended you try out
+‘gnosis-demo’.

File: gnosis.info, Node: Adding notes, Next: Note Types, Prev: Introduction, Up: Top
@@ -98,6 +96,8 @@ Creating notes for gnosis can be done interactively with:
‘M-x gnosis-add-note’
+ Or from within ‘gnosis-dashboard’
+
When it comes to adding images, you can select images that are inside
‘gnosis-images-dir’. For adjusting image size, refer to *note
Customization::
@@ -111,14 +111,14 @@ File: gnosis.info, Node: Note Types, Next: Customization, Prev: Adding notes,
* Menu:
* Cloze::
-* MC-Cloze::
-* MCQ (Multiple Choice Question)::
+* MC-Cloze (Under development)::
+* MCQ::
* Basic Type::
* Double::
* y-or-n::

-File: gnosis.info, Node: Cloze, Next: MC-Cloze, Up: Note Types
+File: gnosis.info, Node: Cloze, Next: MC-Cloze (Under development), Up: Note Types
3.1 Cloze
=========
@@ -139,7 +139,11 @@ selecting ‘Cloze’, the question should be formatted like this:
example creates 2 cloze type notes.
• Each cX tag can have multiple clozes, but each cloze must be a
- *UNIQUE* word (or a unique combination of words) in given note.
+ *UNIQUE* word, or a unique combination of words, in given note.
+
+ • If a cloze is repeated, such as in phrases with "acetyl" &
+ acetylcholine, include whitespace in the cloze to denote a
+ single word.
• You can use the keyword ‘::’ to indicate a hint.
@@ -147,12 +151,16 @@ selecting ‘Cloze’, the question should be formatted like this:
‘gnosis-cloze-guidance’.

-File: gnosis.info, Node: MC-Cloze, Next: MCQ (Multiple Choice Question), Prev: Cloze, Up: Note Types
+File: gnosis.info, Node: MC-Cloze (Under development), Next: MCQ, Prev: Cloze, Up: Note Types
+
+3.2 MC-Cloze (Under development)
+================================
+
+MC-Cloze is disabled by default, to enable it add to your configuration:
-3.2 MC-Cloze
-============
+ ‘(add-to-list 'gnosis-note-types "MC-Cloze")’
-A MC-Cloze (_Multiple Choice Cloze_) is a fill-in-the-blank note, but
+ A MC-Cloze (_Multiple Choice Cloze_) is a fill-in-the-blank note, but
unlike *note cloze note type: Cloze. the user is prompted to select an
option instead of typing an answer.
@@ -169,10 +177,10 @@ note will be generated from each cloze.
values that would mess up with regex functions.

-File: gnosis.info, Node: MCQ (Multiple Choice Question), Next: Basic Type, Prev: MC-Cloze, Up: Note Types
+File: gnosis.info, Node: MCQ, Next: Basic Type, Prev: MC-Cloze (Under development), Up: Note Types
-3.3 MCQ (Multiple Choice Question)
-==================================
+3.3 MCQ
+=======
A MCQ note type, as the name suggests, is a multiple choice question.
@@ -184,7 +192,7 @@ A MCQ note type, as the name suggests, is a multiple choice question.
‘gnosis-mcq-guidance’.

-File: gnosis.info, Node: Basic Type, Next: Double, Prev: MCQ (Multiple Choice Question), Up: Note Types
+File: gnosis.info, Node: Basic Type, Next: Double, Prev: MCQ, Up: Note Types
3.4 Basic Type
==============
@@ -267,101 +275,65 @@ File: gnosis.info, Node: Gnosis Algorithm, Next: Editing notes, Prev: Customi
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 note has a gnosis score, which is a list of 3 values,
+(gnosis-plus gnosis-minus gnosis-synolon/total). Gnosis-synolon is what
+is used to determine the next interval upon a successful recall,
+gnosis-plus is added to gnosis-synolon upon a successful recall as well,
+gnosis-minus is subtracted from gnosis-synolon upon failing to recall a
+note's answer.
- 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’.
+ Gnosis has 2 special events, one is ‘anagnosis’ _ανάγνωση_ and
+‘lethe’ _λήθη_.
* Menu:
-* Initial Interval::
-* Easiness Factor::
-* Forgetting Factor::
-
-
-File: gnosis.info, Node: Initial Interval, Next: Easiness Factor, Up: Gnosis Algorithm
-
-5.1 Initial Interval
-====================
-
-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 '(0 1))
-
- Using the above example, after first successfully reviewing a note,
-you will see it again in the next review session, if you successfully
-review said note again, the next review will be tomorrow.
-
- Upon failing to review a note without completing 2 successful
-reviews, you will have to review it again on the same day.
+* Anagnosis Event::
+* Proto::

-File: gnosis.info, Node: Easiness Factor, Next: Forgetting Factor, Prev: Initial Interval, Up: Gnosis Algorithm
+File: gnosis.info, Node: Anagnosis Event, Next: Proto, Up: Gnosis Algorithm
-5.2 Easiness Factor
+5.1 Anagnosis Event
===================
-The ‘gnosis-algorithm-ef’ is a list that consists of three items:
-
- 1. Easiness factor increase value: Added to the easiness factor upon a
- successful review.
+‘Anagnosis’, which means comprehension & recognition of knowledge, is
+triggered when the consecutive successful or failed recalls are equal or
+greater to anagnosis value.
- 2. Easiness factor decrease value: Subtracted from the total easiness
- factor upon a failed review.
+ When ‘anagnosis’ is triggered by consecutive *successful* recalls,
+‘epignosis’ value is added to gnosis-plus. _Epignosis means accuracy of
+knowledge_.
- 3. Total Easiness factor: Used to calculate the next interval.
+ When ‘anagnosis’ is triggered by consecutive *failed* recalls,
+‘agnoia’ value is added to gnosis-minus. _Agnoia means lack of
+knowledge_
- How this is used:
-
- Multiplies the last interval by the easiness factor after a
-successful review.
-
- For example, upon a successful review, if the last review was 6 days
-ago with an easiness factor of 2.0, the next interval would be
-calculated as 6 * 2.0, and the next total easiness factor would be
-updated by adding the increase value 2.0 + <increase-value>.
-
- Configuration example:
-
- (setq gnosis-algorithm-ef '(0.30 0.25 1.3))
+ You can set specific values for each deck and tag of the variables
+mentioned above by adjusting ‘gnosis-custom-values’.

-File: gnosis.info, Node: Forgetting Factor, Prev: Easiness Factor, Up: Gnosis Algorithm
+File: gnosis.info, Node: Proto, Prev: Anagnosis Event, Up: Gnosis Algorithm
-5.3 Forgetting Factor
-=====================
+5.2 Proto
+=========
-‘gnosis-algorithm-ff’ is a floating number below 1.
+The default initial interval is defined at ‘gnosis-algorithm-proto’, you
+can define a custom initial interval for each deck as well.
- Used to determine the next interval after an unsuccessful review.
+ ‘gnosis-algorithm-interval’ is a list of numbers, representing the
+first initial intervals for successful reviews. There is no limit on
+the length of the list.
- Multiplied with the last interval to calculate the next interval.
-For example, if ‘gnosis-algorithm-ff’ is set to 0.5 and the last
-interval was 6 days, the next interval will be 6 * 0.5 = 3 days.
+ Example:
- Example configuration:
+ (setq gnosis-algorithm-interval '(0 1 2 30))
- (setq gnosis-algorithm-ff 0.5)
+ Upon each successful note review, the algorithm will increment to the
+next interval value: 0 days (0), 1 day later (1), 2 days later (2), and
+30 days later.
- You can set a custom ‘gnosis-algorithm-ff’ for each deck as well.
+ Upon failing to review a note without completing it's proto
+successful reviews, it's next review date will be on the same date.

File: gnosis.info, Node: Editing notes, Next: Sync between devices, Prev: Gnosis Algorithm, Up: Top
@@ -376,7 +348,7 @@ File: gnosis.info, Node: Editing notes, Next: Sync between devices, Prev: Gno
note you want to edit and press ‘e’

-File: gnosis.info, Node: Sync between devices, Next: Extending Gnosis, Prev: Editing notes, Up: Top
+File: gnosis.info, Node: Sync between devices, Next: Configuring Note Types, Prev: Editing notes, Up: Top
7 Sync between devices
**********************
@@ -405,78 +377,100 @@ your configuration:
(gnosis-vc-pull) ;; Run vc-pull for gnosis on startup

-File: gnosis.info, Node: Extending Gnosis, Prev: Sync between devices, Up: Top
+File: gnosis.info, Node: Configuring Note Types, Prev: Sync between devices, Up: Top
-8 Extending Gnosis
-******************
-
-To make development and customization easier, gnosis comes with
-‘gnosis-test’ module, that should be used to create a custom database
-for testing.
-
- To exit the testing environment, rerun ‘M-x gnosis-test-start’ and
-then enter ‘n’ (no) at the prompt "Start development env?"
+8 Configuring Note Types
+************************
* Menu:
+* Adjust Current Types Entries::
* Creating Custom Note Types::
+* Development::

-File: gnosis.info, Node: Creating Custom Note Types, Up: Extending Gnosis
+File: gnosis.info, Node: Adjust Current Types Entries, Next: Creating Custom Note Types, Up: Configuring Note Types
+
+8.1 Adjust Current Types Entries
+================================
+
+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.
+
+ For example:
+
+ (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)))
-8.1 Creating Custom Note Types
+ By evaluating the above code snippet, you won't be prompted to enter
+anything for ‘extra’ & ‘images’.
+
+
+File: gnosis.info, Node: Creating Custom Note Types, Next: Development, Prev: Adjust Current Types Entries, Up: Configuring Note Types
+
+8.2 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-note-type")
-
- • Create 2 functions; ‘gnosis-add-note-TYPE’ &
- ‘gnosis-add-note--TYPE’
+ (add-to-list 'gnosis-note-types "NEW-TYPE")
+ • 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.
+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.
+simple example of how this is done, as well as ‘gnosis-add-note-double’.
- • Create ‘gnosis-review-TYPE’
+
+File: gnosis.info, Node: Development, Prev: Creating Custom Note Types, Up: Configuring Note Types
- This function should handle the review process, displaying it's
-contents and updating the database depending on the result of the review
-(fail/pass). Refer to ‘gnosis-review-basic’ for an example of how this
-should be done.
+8.3 Development
+===============
- • Optionally, you might want to create your own custom
- ‘gnosis-display’ functions
+To make development and customization easier, gnosis comes with
+‘gnosis-test’ module, that should be used to create a custom database
+for testing.
+
+ To exit the testing environment, rerun ‘M-x gnosis-test-start’ and
+then enter ‘n’ (no) at the prompt "Start development env?"

Tag Table:
Node: Top250
-Node: Introduction1614
-Node: Adding notes2341
-Node: Note Types2710
-Node: Cloze2935
-Node: MC-Cloze3913
-Node: MCQ (Multiple Choice Question)4737
-Node: Basic Type5236
-Node: Double5539
-Node: y-or-n5805
-Node: Customization6207
-Node: Image size6392
-Node: Typos | String Comparison6678
-Node: Gnosis Algorithm7453
-Node: Initial Interval8489
-Node: Easiness Factor9279
-Node: Forgetting Factor10227
-Node: Editing notes10835
-Node: Sync between devices11227
-Node: Extending Gnosis12248
-Node: Creating Custom Note Types12703
+Node: Introduction1350
+Node: Adding notes2363
+Node: Note Types2774
+Node: Cloze2992
+Node: MC-Cloze (Under development)4154
+Node: MCQ5142
+Node: Basic Type5580
+Node: Double5856
+Node: y-or-n6122
+Node: Customization6524
+Node: Image size6709
+Node: Typos | String Comparison6995
+Node: Gnosis Algorithm7770
+Node: Anagnosis Event8417
+Node: Proto9158
+Node: Editing notes9925
+Node: Sync between devices10317
+Node: Configuring Note Types11344
+Node: Adjust Current Types Entries11576
+Node: Creating Custom Note Types12576
+Node: Development13412

End Tag Table
diff --git a/doc/gnosis.org b/doc/gnosis.org
index dc2e1f5..15afc52 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.3.1
-#+macro: release-date 2024-07-15
+#+macro: stable-version 0.4.0
+#+macro: release-date 2024-08-7
#+macro: file @@texinfo:@file{@@$1@@texinfo:}@@
#+macro: space @@texinfo:@: @@
#+macro: kbd @@texinfo:@kbd{@@$1@@texinfo:}@@
@@ -22,18 +22,10 @@
#+texinfo_header: @set MAINTAINERCONTACT @uref{mailto:[email protected],contact the maintainer}
-
-Gnosis (γνῶσις), pronounced "noh-sis", /meaning knowledge in Greek/,
-is a spaced repetition system implementation for note taking and self
-testing. Notes are organized in a Question/Answer/Explanation format
-and reviewed at spaced intervals, determined by the success or failure
-to recall the answer.
-
-The goal of Gnosis is to enhance memory retention through active
-recall. To achieve optimal results, users review Gnosis notes by
-writing out the answers.
-
-Above all, Gnosis aspires to be a versatile instrument of learning.
+Gnosis 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.
#+texinfo: @noindent
This manual is written for Gnosis version {{{stable-version}}}, released on {{{release-date}}}.
@@ -46,24 +38,31 @@ This manual is written for Gnosis version {{{stable-version}}}, released on {{{r
* Introduction
-Before reading this manual, it's recommended you first try out =gnosis-demo=
-
-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 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 (γνῶσις) is a spaced repetition system that enhances memory
+retention through active recall. It employs a Q&A format, where each
+note consists of a question, answer, and explanation. Notes are
+reviewed at optimally spaced intervals based on the user's success or
+failure to recall the answer. Key benefits arise from writing out
+answers when reviewing notes, fostering deeper understanding
+and improved memory retention.
+
+Gnosis algorithm is highly adjustable, allowing users to set specific
+values not just for note decks but for tags as well. Gnosis'
+adjustability allows users to fine-tune settings not only for entire
+note collections but also for specific tagged topics, thereby creating
+a personalized learning environment for each topic. Read more on
[[Gnosis Algorithm]]
+Before continuing reading this manual, it's recommended you try out
+=gnosis-demo=.
+
* Adding notes
Creating notes for gnosis can be done interactively with:
=M-x gnosis-add-note=
+Or from within =gnosis-dashboard=
+
When it comes to adding images, you can select images that are inside
=gnosis-images-dir=. For adjusting image size, refer to [[#Customization][Customization]]
@@ -88,14 +87,23 @@ You can also format clozes like Anki if you so prefer; e.g ~{{c1::Cyproheptadine
example creates 2 cloze type notes.
+ Each cX tag can have multiple clozes, but each cloze must be a
- *UNIQUE* word (or a unique combination of words) in given note.
+ *UNIQUE* word, or a unique combination of words, in given note.
+
+ + If a cloze is repeated, such as in phrases with "acetyl" &
+ acetylcholine, include whitespace in the cloze to denote a single
+ word.
+ You can use the keyword =::= to indicate a hint.
You can remove the /guidance/ string by adjusting
=gnosis-cloze-guidance=.
-** MC-Cloze
+** MC-Cloze (Under development)
+
+MC-Cloze is disabled by default, to enable it add to your configuration:
+
+ =(add-to-list 'gnosis-note-types "MC-Cloze")=
+
A MC-Cloze (/Multiple Choice Cloze/) is a fill-in-the-blank note,
but unlike [[#Cloze][cloze note type]] the user is prompted to select an option
instead of typing an answer.
@@ -113,7 +121,7 @@ Example:
When customizing =gnosis-mc-cloze=separator= pay attention to not use
values that would mess up with regex functions.
-** MCQ (Multiple Choice Question)
+** MCQ
A MCQ note type, as the name suggests, is a multiple choice question.
@@ -179,92 +187,51 @@ 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 note has a gnosis score, which is a list of 3 values,
+(gnosis-plus gnosis-minus gnosis-synolon/total). Gnosis-synolon is
+what is used to determine the next interval upon a successful recall,
+gnosis-plus is added to gnosis-synolon upon a successful recall as
+well, gnosis-minus is subtracted from gnosis-synolon upon failing to
+recall a note's answer.
+
+Gnosis has 2 special events, one is ~anagnosis~ /ανάγνωση/ and ~lethe~ /λήθη/.
+** Anagnosis Event
+~Anagnosis~, which means comprehension & recognition of knowledge, is
+triggered when the consecutive successful or failed recalls are equal
+or greater to anagnosis value.
-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.
+When ~anagnosis~ is triggered by consecutive *successful* recalls,
+~epignosis~ value is added to gnosis-plus. /Epignosis means accuracy of knowledge/.
-You can customize deck specific algorithm values using =gnosis-dashboard=.
+When ~anagnosis~ is triggered by consecutive *failed* recalls,
+~agnoia~ value is added to gnosis-minus. /Agnoia means lack of knowledge/
-** Initial Interval
+You can set specific values for each deck and tag of the variables
+mentioned above by adjusting =gnosis-custom-values=.
+
+** Proto
The default initial interval is defined at
-=gnosis-algorithm-interval=, you can define a custom initial interval
+=gnosis-algorithm-proto=, 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.
+=gnosis-algorithm-interval= is a list of numbers, representing the
+first initial intervals for successful reviews. There is no limit on
+the length of the list.
Example:
#+begin_src emacs-lisp
- (setq gnosis-algorithm-interval '(0 1))
-#+end_src
-
-Using the above example, after first successfully reviewing a note,
-you will see it again in the next review session, if you successfully
-review said note again, the next review will be tomorrow.
-
-Upon failing to review a note without completing 2 successful reviews,
-you will have to review it again on the same day.
-
-** Easiness Factor
-
-The =gnosis-algorithm-ef= is a list that consists of three items:
-
-1. Easiness factor increase value: Added to the easiness factor upon a
- successful review.
-
-2. Easiness factor decrease value: Subtracted from the total easiness
- factor upon a failed review.
-
-3. Total Easiness factor: Used to calculate the next interval.
-
-
-How this is used:
-
-Multiplies the last interval by the easiness factor after a successful
-review.
-
-For example, upon a successful review, if the last review was 6 days
-ago with an easiness factor of 2.0, the next interval would be
-calculated as 6 * 2.0, and the next total easiness factor would be
-updated by adding the increase value 2.0 + <increase-value>.
-
-Configuration example:
-
-#+begin_src emacs-lisp
- (setq gnosis-algorithm-ef '(0.30 0.25 1.3))
+ (setq gnosis-algorithm-interval '(0 1 2 30))
#+end_src
-** Forgetting Factor
-
-=gnosis-algorithm-ff= is a floating number below 1.
-
-Used to determine the next interval after an unsuccessful review.
-
-Multiplied with the last interval to calculate the next interval. For
-example, if =gnosis-algorithm-ff= is set to 0.5 and the last interval
-was 6 days, the next interval will be 6 * 0.5 = 3 days.
-
+Upon each successful note review, the algorithm will increment to the
+next interval value: 0 days (0), 1 day later (1), 2 days later
+(2), and 30 days later.
-Example configuration:
+Upon failing to review a note without completing it's proto successful reviews,
+it's next review date will be on the same date.
-#+begin_src emacs-lisp
- (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:
@@ -300,14 +267,27 @@ To automatically push changes after a review session, add this to your configura
(gnosis-vc-pull) ;; Run vc-pull for gnosis on startup
#+end_src
-* Extending Gnosis
-To make development and customization easier, gnosis comes with
-=gnosis-test= module, that should be used to create a custom database for
-testing.
+* Configuring Note Types
+** Adjust Current Types Entries
+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.
-To exit the testing environment, rerun =M-x gnosis-test-start= and
-then enter =n= (no) at the prompt "Start development env?"
+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)))
+#+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
@@ -315,23 +295,22 @@ 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-note-type")
+ (add-to-list 'gnosis-note-types "NEW-TYPE")
#+end_src
++ Create an interactive function
-+ Create 2 functions; =gnosis-add-note-TYPE= & =gnosis-add-note--TYPE=
-
-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.
+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.
+example of how this is done, as well as =gnosis-add-note-double=.
-+ Create =gnosis-review-TYPE=
-
-This function should handle the review process, displaying it's
-contents and updating the database depending on the result of the
-review (fail/pass). Refer to =gnosis-review-basic= for an example of how
-this should be done.
+** Development
+To make development and customization easier, gnosis comes with
+=gnosis-test= module, that should be used to create a custom database for
+testing.
-+ Optionally, you might want to create your own custom =gnosis-display= functions
+To exit the testing environment, rerun =M-x gnosis-test-start= and
+then enter =n= (no) at the prompt "Start development env?"
diff --git a/doc/gnosis.texi b/doc/gnosis.texi
index b48163b..779ea47 100644
--- a/doc/gnosis.texi
+++ b/doc/gnosis.texi
@@ -25,20 +25,13 @@
@node Top
@top Gnosis User Manual
-Gnosis (γνῶσις), pronounced ``noh-sis'', @emph{meaning knowledge in Greek},
-is a spaced repetition system implementation for note taking and self
-testing. Notes are organized in a Question/Answer/Explanation format
-and reviewed at spaced intervals, determined by the success or failure
-to recall the answer.
-
-The goal of Gnosis is to enhance memory retention through active
-recall. To achieve optimal results, users review Gnosis notes by
-writing out the answers.
-
-Above all, Gnosis aspires to be a versatile instrument of learning.
+Gnosis 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.
@noindent
-This manual is written for Gnosis version 0.3.1, released on 2024-07-15.
+This manual is written for Gnosis version 0.4.0, released on 2024-08-7.
@itemize
@item
@@ -62,7 +55,7 @@ Git repositories:
* Gnosis Algorithm::
* Editing notes::
* Sync between devices::
-* Extending Gnosis::
+* Configuring Note Types::
@detailmenu
--- The Detailed Node Listing ---
@@ -70,8 +63,8 @@ Git repositories:
Note Types
* Cloze::
-* MC-Cloze::
-* MCQ (Multiple Choice Question)::
+* MC-Cloze (Under development)::
+* MCQ::
* Basic Type::
* Double::
* y-or-n::
@@ -83,13 +76,14 @@ Customization
Gnosis Algorithm
-* Initial Interval::
-* Easiness Factor::
-* Forgetting Factor::
+* Anagnosis Event::
+* Proto::
-Extending Gnosis
+Configuring Note Types
+* Adjust Current Types Entries::
* Creating Custom Note Types::
+* Development::
@end detailmenu
@end menu
@@ -97,19 +91,24 @@ Extending Gnosis
@node Introduction
@chapter Introduction
-Before reading this manual, it's recommended you first try out @samp{gnosis-demo}
-
-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 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 (γνῶσις) is a spaced repetition system that enhances memory
+retention through active recall. It employs a Q&A format, where each
+note consists of a question, answer, and explanation. Notes are
+reviewed at optimally spaced intervals based on the user's success or
+failure to recall the answer. Key benefits arise from writing out
+answers when reviewing notes, fostering deeper understanding
+and improved memory retention.
+
+Gnosis algorithm is highly adjustable, allowing users to set specific
+values not just for note decks but for tags as well. Gnosis'
+adjustability allows users to fine-tune settings not only for entire
+note collections but also for specific tagged topics, thereby creating
+a personalized learning environment for each topic. Read more on
@ref{Gnosis Algorithm}
+Before continuing reading this manual, it's recommended you try out
+@samp{gnosis-demo}.
+
@node Adding notes
@chapter Adding notes
@@ -117,6 +116,8 @@ Creating notes for gnosis can be done interactively with:
@samp{M-x gnosis-add-note}
+Or from within @samp{gnosis-dashboard}
+
When it comes to adding images, you can select images that are inside
@samp{gnosis-images-dir}. For adjusting image size, refer to @ref{Customization}
@@ -125,8 +126,8 @@ When it comes to adding images, you can select images that are inside
@menu
* Cloze::
-* MC-Cloze::
-* MCQ (Multiple Choice Question)::
+* MC-Cloze (Under development)::
+* MCQ::
* Basic Type::
* Double::
* y-or-n::
@@ -155,7 +156,14 @@ example creates 2 cloze type notes.
@item
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.
+@strong{UNIQUE} word, or a unique combination of words, in given note.
+
+@itemize
+@item
+If a cloze is repeated, such as in phrases with ``acetyl'' &
+acetylcholine, include whitespace in the cloze to denote a single
+word.
+@end itemize
@item
You can use the keyword @samp{::} to indicate a hint.
@@ -164,8 +172,12 @@ You can use the keyword @samp{::} to indicate a hint.
You can remove the @emph{guidance} string by adjusting
@samp{gnosis-cloze-guidance}.
-@node MC-Cloze
-@section MC-Cloze
+@node MC-Cloze (Under development)
+@section MC-Cloze (Under development)
+
+MC-Cloze is disabled by default, to enable it add to your configuration:
+
+@samp{(add-to-list 'gnosis-note-types "MC-Cloze")}
A MC-Cloze (@emph{Multiple Choice Cloze}) is a fill-in-the-blank note,
but unlike @ref{Cloze, , cloze note type} the user is prompted to select an option
@@ -186,8 +198,8 @@ The greatest text editor is Emacs&&Vim&&Helix
When customizing @samp{gnosis-mc-cloze=separator} pay attention to not use
values that would mess up with regex functions.
-@node MCQ (Multiple Choice Question)
-@section MCQ (Multiple Choice Question)
+@node MCQ
+@section MCQ
A MCQ note type, as the name suggests, is a multiple choice question.
@@ -265,106 +277,59 @@ 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.
+Each gnosis note has a gnosis score, which is a list of 3 values,
+(gnosis-plus gnosis-minus gnosis-synolon/total). Gnosis-synolon is
+what is used to determine the next interval upon a successful recall,
+gnosis-plus is added to gnosis-synolon upon a successful recall as
+well, gnosis-minus is subtracted from gnosis-synolon upon failing to
+recall a note's answer.
-You can customize deck specific algorithm values using @samp{gnosis-dashboard}.
+Gnosis has 2 special events, one is @code{anagnosis} @emph{ανάγνωση} and @code{lethe} @emph{λήθη}.
@menu
-* Initial Interval::
-* Easiness Factor::
-* Forgetting Factor::
+* Anagnosis Event::
+* Proto::
@end menu
-@node Initial Interval
-@section Initial Interval
+@node Anagnosis Event
+@section Anagnosis Event
-The default initial interval is defined at
-@samp{gnosis-algorithm-interval}, you can define a custom initial interval
-for each deck as well.
+@code{Anagnosis}, which means comprehension & recognition of knowledge, is
+triggered when the consecutive successful or failed recalls are equal
+or greater to anagnosis value.
-@samp{gnosis-algorithm-interval} is a list of 2
-numbers, representing the first two initial intervals for successful
-reviews.
+When @code{anagnosis} is triggered by consecutive @strong{successful} recalls,
+@code{epignosis} value is added to gnosis-plus. @emph{Epignosis means accuracy of knowledge}.
-Example:
-
-@lisp
-(setq gnosis-algorithm-interval '(0 1))
-@end lisp
-
-Using the above example, after first successfully reviewing a note,
-you will see it again in the next review session, if you successfully
-review said note again, the next review will be tomorrow.
-
-Upon failing to review a note without completing 2 successful reviews,
-you will have to review it again on the same day.
-
-@node Easiness Factor
-@section Easiness Factor
-
-The @samp{gnosis-algorithm-ef} is a list that consists of three items:
-
-@enumerate
-@item
-Easiness factor increase value: Added to the easiness factor upon a
-successful review.
-
-@item
-Easiness factor decrease value: Subtracted from the total easiness
-factor upon a failed review.
-
-@item
-Total Easiness factor: Used to calculate the next interval.
-@end enumerate
+When @code{anagnosis} is triggered by consecutive @strong{failed} recalls,
+@code{agnoia} value is added to gnosis-minus. @emph{Agnoia means lack of knowledge}
+You can set specific values for each deck and tag of the variables
+mentioned above by adjusting @samp{gnosis-custom-values}.
-How this is used:
+@node Proto
+@section Proto
-Multiplies the last interval by the easiness factor after a successful
-review.
+The default initial interval is defined at
+@samp{gnosis-algorithm-proto}, you can define a custom initial interval
+for each deck as well.
-For example, upon a successful review, if the last review was 6 days
-ago with an easiness factor of 2.0, the next interval would be
-calculated as 6 * 2.0, and the next total easiness factor would be
-updated by adding the increase value 2.0 + <increase-value>.
+@samp{gnosis-algorithm-interval} is a list of numbers, representing the
+first initial intervals for successful reviews. There is no limit on
+the length of the list.
-Configuration example:
+Example:
@lisp
-(setq gnosis-algorithm-ef '(0.30 0.25 1.3))
+(setq gnosis-algorithm-interval '(0 1 2 30))
@end lisp
-@node Forgetting Factor
-@section Forgetting Factor
-
-@samp{gnosis-algorithm-ff} is a floating number below 1.
+Upon each successful note review, the algorithm will increment to the
+next interval value: 0 days (0), 1 day later (1), 2 days later
+(2), and 30 days later.
-Used to determine the next interval after an unsuccessful review.
-
-Multiplied with the last interval to calculate the next interval. For
-example, if @samp{gnosis-algorithm-ff} is set to 0.5 and the last interval
-was 6 days, the next interval will be 6 * 0.5 = 3 days.
-
-
-Example configuration:
-
-@lisp
-(setq gnosis-algorithm-ff 0.5)
-@end lisp
-
-You can set a custom @samp{gnosis-algorithm-ff} for each deck as well.
+Upon failing to review a note without completing it's proto successful reviews,
+it's next review date will be on the same date.
@node Editing notes
@chapter Editing notes
@@ -412,20 +377,38 @@ To automatically push changes after a review session, add this to your configura
(gnosis-vc-pull) ;; Run vc-pull for gnosis on startup
@end lisp
-@node Extending Gnosis
-@chapter Extending Gnosis
-
-To make development and customization easier, gnosis comes with
-@samp{gnosis-test} module, that should be used to create a custom database for
-testing.
-
-To exit the testing environment, rerun @samp{M-x gnosis-test-start} and
-then enter @samp{n} (no) at the prompt ``Start development env?''
+@node Configuring Note Types
+@chapter Configuring Note Types
@menu
+* Adjust Current Types Entries::
* Creating Custom Note Types::
+* Development::
@end menu
+@node Adjust Current Types Entries
+@section Adjust Current Types Entries
+
+Each gnosis note type has an @emph{interactive} function, named
+@samp{gnosis-add-note-TYPE}. You can set default values for each entry by
+hard coding specific values to their keywords.
+
+For example:
+
+@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)))
+@end lisp
+
+By evaluating the above code snippet, you won't be prompted to enter
+anything for @code{extra} & @code{images}.
+
@node Creating Custom Note Types
@section Creating Custom Note Types
@@ -436,33 +419,28 @@ 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-note-type")
+(add-to-list 'gnosis-note-types "NEW-TYPE")
@end lisp
-
@item
-Create 2 functions; @samp{gnosis-add-note-TYPE} & @samp{gnosis-add-note--TYPE}
+Create an interactive function
@end itemize
-Each note type has a @samp{gnosis-add-note-TYPE} that is used
-interactively & a ``hidden function'' @samp{gnosis-add-note--TYPE} that handles
-all the logic.
+Each note type has a @samp{gnosis-add-note-TYPE} that is used interactively
+& a ``hidden function'' @samp{gnosis-add-note--TYPE} that handles all the
+logic. You can use one of the @samp{current gnosis-add-note--TYPE}
+functions or create one of your own.
Refer to @samp{gnosis-add-note-basic} & @samp{gnosis-add-note--basic} for a simple
-example of how this is done.
+example of how this is done, as well as @samp{gnosis-add-note-double}.
-@itemize
-@item
-Create @samp{gnosis-review-TYPE}
-@end itemize
+@node Development
+@section Development
-This function should handle the review process, displaying it's
-contents and updating the database depending on the result of the
-review (fail/pass). Refer to @samp{gnosis-review-basic} for an example of how
-this should be done.
+To make development and customization easier, gnosis comes with
+@samp{gnosis-test} module, that should be used to create a custom database for
+testing.
-@itemize
-@item
-Optionally, you might want to create your own custom @samp{gnosis-display} functions
-@end itemize
+To exit the testing environment, rerun @samp{M-x gnosis-test-start} and
+then enter @samp{n} (no) at the prompt ``Start development env?''
@bye
diff --git a/gnosis-algorithm.el b/gnosis-algorithm.el
index 33e3232..a8930a3 100644
--- a/gnosis-algorithm.el
+++ b/gnosis-algorithm.el
@@ -24,78 +24,70 @@
;;; Commentary:
-;; 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.
+;; Module that handles date and interval calculation as well as
+;; gnosis-score for notes.
;;; Code:
(require 'cl-lib)
(require 'calendar)
-(defcustom gnosis-algorithm-interval '(1 3)
- "Gnosis initial interval for initial successful reviews.
+(defcustom gnosis-algorithm-proto '(0 1 2)
+ "Gnosis proto interval for the first successful reviews.
-First item: First interval,
-Second item: Second interval."
+Values for the first proto successful intervals. There is no
+restriction for list length."
:group 'gnosis
:type '(list integer))
-(defcustom gnosis-algorithm-ef '(0.35 0.30 1.3)
- "Gnosis easiness factor.
+(defcustom gnosis-algorithm-gnosis-value '(0.35 0.30 1.3)
+ "Starting gnosis score.
-First item : Increase value
-Second item: Decrease value
-Third item : Total ef"
+First item : Increase value (gnosis-plus)
+Second item: Decrease value (gnosis-minus)
+Third item : Total gnosis (gnosis-synolon/totalis) -> Total gnosis score"
:group 'gnosis
:type '(list float))
-(defcustom gnosis-algorithm-ff 0.5
- "Gnosis forgetting factor.
+(defcustom gnosis-algorithm-amnesia-value 0.5
+ "Gnosis amnesia value.
-Used to calcuate new interval for failed questions.
+Used to calcuate new interval upon a failed recall i.e the memmory loss.
-NOTE: This value should be less than 1.0."
+The closer this value is to 1, the more the memory loss."
:group 'gnosis
:type 'float)
-(defcustom gnosis-algorithm-ef-increase 0.1
- "Value to increase ef increase value with.
+(defcustom gnosis-algorithm-epignosis-value 0.1
+ "Value to increase gnosis-plus upon anagnosis.
-Increase ef-increase value by this amount for every
-`gnosis-algorithm-ef-threshold' number of successful reviews."
+Epignosis means knowledge accuracy."
:group 'gnosis
:type 'float)
-(defcustom gnosis-algorithm-ef-decrease 0.2
- "Value to decrease ef decrease value with.
+(defcustom gnosis-algorithm-agnoia-value 0.2
+ "Value to increase gnosis-minus upon anagnosis.
-Decrease ef decrease value by this amount for every
-`gnosis-algorithm-ef-threshold' number of failed reviews."
+Agnoia refers to the lack of knowledge."
:group 'gnosis
:type 'float)
-(defcustom gnosis-algorithm-ef-threshold 3
- "Threshold for updating ef increase/decrease values.
+(defcustom gnosis-algorithm-anagnosis-value 3
+ "Threshold value for anagnosis event.
-Refers to the number of consecutive successful or failed reviews."
+Anagosis is the process recognition & understanding of a context/gnosis.
+
+Anagnosis events update gnosis-plus & gnosis-minus values, depending
+on the success or failure of recall."
+ :group 'gnosis
+ :type 'integer)
+
+(defcustom gnosis-algorithm-lethe-value 2
+ "Threshold value for hitting a lethe event.
+
+Lethe is the process of being unable to recall a memory/gnosis.
+
+On lethe events the next interval is set to 0."
:group 'gnosis
:type 'integer)
@@ -113,6 +105,7 @@ Refers to the number of consecutive successful or failed reviews."
(defun gnosis-algorithm-date (&optional offset)
"Return the current date in a list (year month day).
Optional integer OFFSET is a number of days from the current date."
+ (cl-assert (or (numberp offset) (null offset)) nil "Date offset must be an integer or nil")
(let* ((now (decode-time))
(now (list (decoded-time-month now)
(decoded-time-day now)
@@ -136,66 +129,88 @@ DATE format must be given as (year month day)."
(time-to-days given-date))))
(if (>= diff 0) diff (error "`DATE2' must be higher than `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)
+(cl-defun gnosis-algorithm-next-gnosis (&key gnosis success epignosis agnoia anagnosis
+ c-successes c-failures)
+ "Return the neo GNOSIS value. (gnosis-plus gnosis-minus gnsois-synolon)
+
+Calculate the new e-factor given existing GNOSIS and SUCCESS, either t or nil.
-Calculate the new e-factor given existing EF and SUCCESS, either t or nil.
+Next GNOSIS is calculated as follows:
-Next EF is calculated as follows:
+Upon a successful review, increase gnosis-synolon value (nth 2 gnosis) by
+gnosis-plus value (nth 0 gnosis).
-Upon a successful review, increase total ef value (nth 2) by
-ef-increase value (nth 0).
+Upon a failed review, decrease gnosis-synolon by gnosis-minus value
+ (nth 1 gnosis).
-Upon a failed review, decrease total ef by ef-decrease value (nth 1).
+ANAGNOSIS is an event threshold, updating either the gnosis-plus or
+gnosis-minus values.
-For every THRESHOLD of C-SUCCESSES (consecutive successful reviews)
-reviews, increase ef-increase by INCREASE.
+When C-SUCCESSES (consecutive successes) reach ANAGNOSIS,
+increase gnosis-plus by EPIGNOSIS.
-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")
+When C-FAILURES reach ANAGOSNIS, increase gnosis-minus by AGNOIA."
+ (cl-assert (listp gnosis) nil "Assertion failed: gnosis must be a list of floats.")
(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)
+ (cl-assert (and (floatp epignosis) (< epignosis 1)) nil "Assertion failed: epignosis must be a float < 1")
+ (cl-assert (and (floatp agnoia) (< agnoia 1)) nil "Assertion failed: agnoia must be a float < 1")
+ (cl-assert (integerp anagnosis) nil "Assertion failed: anagosis must be an integer.")
+ (let ((anagnosis-p (= (% (max 1 (if success c-successes c-failures)) anagnosis) 0))
+ (neo-gnosis
+ (if success
+ (gnosis-algorithm-replace-at-index 2 (+ (nth 2 gnosis) (nth 0 gnosis)) gnosis)
+ (gnosis-algorithm-replace-at-index 2 (max 1.3 (- (nth 2 gnosis) (nth 1 gnosis))) gnosis))))
+ ;; TODO: Change amnesia & epignosis value upon reaching a lethe or anagnosis event.
+ (cond ((and success anagnosis-p)
+ (setf neo-gnosis (gnosis-algorithm-replace-at-index 0 (+ (nth 0 gnosis) epignosis) neo-gnosis)))
+ ((and (not success) anagnosis-p
+ (setf neo-gnosis
+ (gnosis-algorithm-replace-at-index 1 (+ (nth 1 gnosis) agnoia) neo-gnosis)))))
+ (gnosis-algorithm-round-items neo-gnosis)))
+
+(cl-defun gnosis-algorithm-next-interval (&key last-interval gnosis-synolon success successful-reviews
+ amnesia proto c-fails lethe)
"Calculate next interval.
-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")
+LAST-INTERVAL: Number of days since last review
+
+C-FAILS: Total consecutive failed reviews.
+
+GNOSIS-SYNOLON: Current gnosis-synolon (gnosis totalis).
+
+SUCCESS: non-nil when review was successful.
+
+SUCCESSFUL-REVIEWS: Number of successful reviews.
+
+AMNESIA: 'Forget value', used to calculate next interval upon failed
+review.
+
+PROTO: List of proto intervals, for successful reviews.
+Until successfully completing proto reviews, for every failed attempt
+the next interval will be set to 0.
+
+LETHE: Upon having C-FAILS >= lethe, set next interval to 0."
+ (cl-assert (booleanp success) nil "Success value must be a boolean")
+ (cl-assert (integerp successful-reviews) nil "Successful-reviews must be an integer")
+ (cl-assert (and (floatp amnesia) (<= amnesia 1)) nil "Amnesia must be a float <=1")
+ (cl-assert (and (<= amnesia 1) (> amnesia 0)) nil "Value of amnesia must be a float <= 1")
+ (cl-assert (and (integerp lethe) (>= lethe 1)) nil "Value of lethe must be an integer >= 1")
;; This should only occur in testing env or when the user has made breaking changes.
(let* ((last-interval (if (<= last-interval 0) 1 last-interval)) ;; If last-interval is 0, use 1 instead.
- (interval (cond ((and (= successful-reviews 0) success)
- (car initial-interval))
- ((and (= successful-reviews 1) success)
- (cadr initial-interval))
- ;; If it's still on initial stage, review the
- ;; same day
- ((and (< successful-reviews 2) (not success)) 0)
- (t (let* ((success-interval (* ef last-interval))
- (failure-interval (* last-interval failure-factor)))
+ (amnesia (- 1 amnesia)) ;; inverse amnesia
+ (interval (cond ((and (< successful-reviews (length proto))
+ success)
+ (nth successful-reviews proto))
+ ;; Lethe event, reset interval.
+ ((and (>= c-fails lethe)
+ (not success))
+ 0)
+ (t (let* ((success-interval (* gnosis-synolon last-interval))
+ (failure-interval (* amnesia last-interval)))
(if success success-interval
;; Make sure failure interval is never
- ;; higher than success
- (min success-interval failure-interval)))))))
+ ;; higher than success and at least 0
+ (max (min success-interval failure-interval) 0)))))))
(gnosis-algorithm-date (round interval))))
diff --git a/gnosis-dashboard.el b/gnosis-dashboard.el
new file mode 100644
index 0000000..1dcf9fb
--- /dev/null
+++ b/gnosis-dashboard.el
@@ -0,0 +1,576 @@
+;;; gnosis-dashboard.el --- Spaced Repetition Algorithm for Gnosis -*- lexical-binding: t; -*-
+
+;; Copyright (C) 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") (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
+;; 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:
+
+;; This an extension of gnosis.el
+
+;;; Code:
+(require 'cl-lib)
+(require 'calendar)
+(require 'transient)
+
+(declare-function gnosis-select "gnosis.el")
+(declare-function gnosis-delete-note "gnosis.el")
+(declare-function gnosis-suspend-note "gnosis.el")
+(declare-function gnosis-collect-note-ids "gnosis.el")
+(declare-function gnosis-edit-deck "gnosis.el")
+(declare-function gnosis-edit-note "gnosis.el")
+(declare-function gnosis-delete-deck "gnosis.el")
+(declare-function gnosis-suspend-deck "gnosis.el")
+(declare-function gnosis-add-deck "gnosis.el")
+(declare-function gnosis-add-note "gnosis.el")
+(declare-function gnosis-insert-separator "gnosis.el")
+(declare-function gnosis-get-date-total-notes "gnosis.el")
+(declare-function gnosis-center-string "gnosis.el")
+(declare-function gnosis-get-date-new-notes "gnosis.el")
+(declare-function gnosis-review-get-due-notes "gnosis.el")
+(declare-function gnosis-algorithm-date "gnosis-algorithm.el")
+(declare-function gnosis-get-tags--unique "gnosis.el")
+(declare-function gnosis-get-tag-notes "gnosis.el")
+(declare-function gnosis-edit-update "gnosis.el")
+(declare-function gnosis-update "gnosis.el")
+
+(defcustom gnosis-dashboard-months 2
+ "Number of additional months to display on dashboard."
+ :type 'integer
+ :group 'gnosis)
+
+(defvar gnosis-dashboard-note-ids nil
+ "Store note ids for dashboard.")
+
+(defvar gnosis-dashboard-search-value nil
+ "Store search value.")
+
+(defvar gnosis-dashboard--current
+ '(:type nil :ids nil)
+ "Current values to return after edits.")
+
+(defface gnosis-dashboard-header-face
+ '((t :foreground "#ff0a6a" :weight bold))
+ "My custom face for both light and dark backgrounds.")
+
+(defvar gnosis-dashboard--selected-ids nil
+ "Selected ids from the tabulated list.")
+
+(defun gnosis-dashboard-return (&optional current-values)
+ "Return to dashboard for CURRENT-VALUES."
+ (interactive)
+ (let* ((current-values (or current-values gnosis-dashboard--current))
+ (type (plist-get current-values :type))
+ (ids (plist-get current-values :ids)))
+ (cond ((eq type 'notes)
+ (gnosis-dashboard-output-notes ids))
+ ((eq type 'decks )
+ (gnosis-dashboard-output-decks))
+ ((eq type 'tags )
+ (gnosis-dashboard-output-tags)))))
+
+(defun gnosis-dashboard-generate-dates (&optional year)
+ "Return a list of all dates (year month day) for YEAR."
+ (let* ((current-year (or (decoded-time-year (decode-time)) year))
+ (result '()))
+ (dotimes (month 12)
+ (let ((days-in-month (calendar-last-day-of-month (+ month 1) current-year)))
+ (dotimes (day days-in-month)
+ (push (list current-year (+ month 1) (+ day 1)) result))))
+ (nreverse result)))
+
+(defun gnosis-dashboard-year-stats (&optional year)
+ "Return YEAR review stats."
+ (let ((notes nil))
+ (cl-loop for date in (gnosis-dashboard-generate-dates (and year))
+ do (setq notes (append notes (list (gnosis-get-date-total-notes date)))))
+ notes))
+
+(defun gnosis-dashboard--streak (dates &optional num date)
+ "Return current review streak.
+
+DATES: Dates in the activity log.
+NUM: Streak number.
+DATE: Integer, used with `gnosis-algorithm-date' to get previous dates."
+ (let ((num (or num 0))
+ (date (or date 0)))
+ (if (member (gnosis-algorithm-date date) dates)
+ (gnosis-dashboard--streak dates (cl-incf num) (- date 1))
+ num)))
+
+(defun gnosis-dashboard-month-reviews (month)
+ "Return reviewes for MONTH in current year."
+ (cl-assert (and (integerp month)
+ (< month 12))
+ nil "Month must be an integer, lower than 12.")
+ (let* ((month-dates (cl-loop for date in (gnosis-dashboard-generate-dates)
+ if (and (= (nth 1 date) month)
+ (= (nth 0 date) (decoded-time-year (decode-time))))
+ collect date))
+ (month-reviews (cl-loop for date in month-dates
+ collect (gnosis-get-date-total-notes date))))
+ month-reviews))
+
+;; TODO: Optionally, add dates where no review was made.
+(defun gnosis-dashboard-output-average-rev ()
+ "Output the average daily notes reviewed for current year.
+
+Skips days where no note was reviewed."
+ (let ((total 0)
+ (entries (gnosis-dashboard-year-stats)))
+ (cl-loop for entry in entries
+ when (not (= entry 0))
+ do (setq total (+ total entry)))
+ (/ total (max (length (remove 0 entries)) 1))))
+
+;; TODO: Add more conds & faces
+(defun gnosis-dashboard--graph-propertize (string num)
+ "Propertize STRING depending on the NUM of reviews."
+ (cond ((= num 0)
+ (propertize string 'face 'shadow))
+ ((> num 0)
+ (propertize string 'face 'font-lock-constant-face))))
+
+(defun gnosis-dashboard--add-padding (str-length)
+ "Add padding for STR-LENGTH."
+ (let ((padding (/ (- (window-width) str-length) 2)))
+ (make-string padding ?\s)))
+
+(defun gnosis-dashboard-reviews-graph (dates &optional )
+ "Insert graph for month DATES.
+
+Optionally, use when using multiple months."
+ (let ((count 0)
+ (row 0)
+ (start-column (current-column))
+ (end-column nil))
+ (cl-loop for day in dates
+ when (= count 0)
+ do (let ((current-column (current-column)))
+ (and (< (move-to-column start-column) start-column)
+ ;; Add spaces to reach start-column.
+ (insert (make-string (- start-column current-column) ?\s))))
+ (insert " ")
+ do (end-of-line)
+ (insert (gnosis-dashboard--graph-propertize (format "[%s] " (if (= day 0) "-" "x")) day))
+ (cl-incf count)
+ when (= count 7)
+ do
+ (setq end-column (current-column))
+ (setq count 0)
+ (insert " ")
+ (cl-incf row)
+ (end-of-line)
+ (when (and (/= (forward-line 1) 0) (eobp))
+ (insert "\n")
+ (forward-line 0)))
+ (insert (make-string (- end-column (current-column)) ?\s))
+ (insert " ")))
+;; TODO: Refactor this!
+(defun gnosis-dashboard-month-overview (&optional num)
+ "Insert review graph for MONTHS."
+ (gnosis-insert-separator)
+ (let* ((point (point))
+ (month (car (calendar-current-date))))
+ (insert (gnosis-dashboard--add-padding (min (* (max num 1) 50) (window-width))))
+ (while (<= month (+ (car (calendar-current-date)) num))
+ ;; (insert (format "%d" month))
+ (gnosis-dashboard-reviews-graph (gnosis-dashboard-month-reviews month))
+ (goto-char point)
+ (end-of-line)
+ (cl-incf month))))
+
+(defun gnosis-dashboard-output-note (id)
+ "Output contents for note with ID, formatted for gnosis dashboard."
+ (cl-loop for item in (append (gnosis-select '[main options answer tags type] 'notes `(= id ,id) t)
+ (gnosis-select 'suspend 'review-log `(= id ,id) t))
+ if (listp item)
+ collect (mapconcat #'identity item ",")
+ else
+ collect (replace-regexp-in-string "\n" " " (format "%s" item))))
+
+(defun gnosis-dashboard-edit-note (&optional id)
+ "Edit note with ID."
+ (interactive)
+ (let ((id (or id (string-to-number (tabulated-list-get-id)))))
+ (gnosis-edit-note id)))
+
+(defun gnosis-dashboard-suspend-note ()
+ "Suspend note."
+ (interactive)
+ (if gnosis-dashboard--selected-ids
+ (gnosis-dashboard-marked-suspend)
+ (gnosis-suspend-note (string-to-number (tabulated-list-get-id)))
+ (gnosis-dashboard-output-notes gnosis-dashboard-note-ids)
+ (revert-buffer t t t)))
+
+(defun gnosis-dashboard-delete ()
+ "Delete note."
+ (interactive)
+ (if gnosis-dashboard--selected-ids
+ (gnosis-dashboard-marked-delete)
+ (gnosis-delete-note (string-to-number (tabulated-list-get-id)))
+ (gnosis-dashboard-output-notes gnosis-dashboard-note-ids)
+ (revert-buffer t t t)))
+
+(defvar-keymap gnosis-dashboard-notes-mode-map
+ :doc "Keymap for notes dashboard."
+ "e" #'gnosis-dashboard-edit-note
+ "s" #'gnosis-dashboard-suspend-note
+ "a" #'gnosis-add-note
+ "r" #'gnosis-dashboard-return
+ "g" #'gnosis-dashboard-return
+ "d" #'gnosis-dashboard-delete
+ "m" #'gnosis-dashboard-mark-toggle
+ "u" #'gnosis-dashboard-mark-toggle)
+
+(define-minor-mode gnosis-dashboard-notes-mode
+ "Minor mode for gnosis dashboard notes output."
+ :keymap gnosis-dashboard-notes-mode-map)
+
+(defun gnosis-dashboard-output-notes (note-ids)
+ "Return NOTE-IDS contents on gnosis dashboard."
+ (cl-assert (listp note-ids) t "`note-ids' must be a list of note ids.")
+ (pop-to-buffer-same-window "*gnosis-dashboard*")
+ (gnosis-dashboard-mode)
+ (gnosis-dashboard-notes-mode)
+ (setf tabulated-list-format `[("Main" ,(/ (window-width) 4) t)
+ ("Options" ,(/ (window-width) 6) t)
+ ("Answer" ,(/ (window-width) 6) t)
+ ("Tags" ,(/ (window-width) 5) t)
+ ("Type" ,(/ (window-width) 10) T)
+ ("Suspend" ,(/ (window-width) 6) t)]
+ tabulated-list-entries (cl-loop for id in note-ids
+ for output = (gnosis-dashboard-output-note id)
+ when output
+ collect (list (number-to-string id) (vconcat output)))
+ gnosis-dashboard-note-ids note-ids)
+ (tabulated-list-init-header)
+ (tabulated-list-print t)
+ (setf gnosis-dashboard--current `(:type notes :ids ,note-ids)))
+
+(defun gnosis-dashboard-deck-note-count (id)
+ "Return total note count for deck with ID."
+ (let ((note-count (length (gnosis-select 'id 'notes `(= deck-id ,id) t))))
+ (when (gnosis-select 'id 'decks `(= id ,id))
+ (list (number-to-string note-count)))))
+
+(defun gnosis-dashboard-output-tag (tag)
+ "Output TAG name and total notes."
+ (let ((notes (gnosis-get-tag-notes tag)))
+ `(,tag ,(number-to-string (length notes)))))
+
+(defun gnosis-dashboard-sort-total-notes (entry1 entry2)
+ "Sort function for the total notes column, for ENTRY1 and ENTRY2."
+ (let ((total1 (string-to-number (elt (cadr entry1) 1)))
+ (total2 (string-to-number (elt (cadr entry2) 1))))
+ (< total1 total2)))
+
+(defun gnosis-dashboard-rename-tag (&optional tag new-tag )
+ "Rename TAG to NEW-TAG."
+ (interactive)
+ (let ((new-tag (or new-tag (read-string "News tag name: ")))
+ (tag (or tag (tabulated-list-get-id))))
+ (cl-loop for note in (gnosis-get-tag-notes tag)
+ do (let* ((tags (car (gnosis-select '[tags] 'notes `(= id ,note) t)))
+ (new-tags (cl-substitute new-tag tag tags :test #'string-equal)))
+ (gnosis-update 'notes `(= tags ',new-tags) `(= id ,note))))))
+
+(defun gnosis-dashboard-suspend-tag (&optional tag)
+ "Suspend notes of TAG."
+ (interactive)
+ (let* ((tag (or tag (tabulated-list-get-id)))
+ (notes (gnosis-get-tag-notes tag)))
+ (when (y-or-n-p "Toggle SUSPEND for tagged notes?")
+ (cl-loop for note in notes
+ do (gnosis-suspend-note note t)))))
+
+(defun gnosis-dashboard-tag-view-notes (&optional tag)
+ "View notes for TAG."
+ (interactive)
+ (let ((tag (or tag (tabulated-list-get-id))))
+ (gnosis-dashboard-output-notes (gnosis-get-tag-notes tag))))
+
+(defvar-keymap gnosis-dashboard-tags-mode-map
+ "RET" #'gnosis-dashboard-tag-view-notes
+ "e" #'gnosis-dashboard-rename-tag
+ "s" #'gnosis-dashboard-suspend-tag
+ "r" #'gnosis-dashboard-rename-tag
+ "g" #'gnosis-dashboard-return)
+
+(define-minor-mode gnosis-dashboard-tags-mode
+ "Mode for dashboard output of tags."
+ :keymap gnosis-dashboard-tags-mode-map)
+
+(defun gnosis-dashboard-output-tags (&optional tags)
+ "Format gnosis dashboard with output of TAGS."
+ (let ((tags (or tags (gnosis-get-tags--unique))))
+ (pop-to-buffer-same-window "*gnosis-dashboard*")
+ (gnosis-dashboard-mode)
+ (gnosis-dashboard-tags-mode)
+ (setf gnosis-dashboard--current '(:type 'tags))
+ (setq tabulated-list-format [("Name" 35 t)
+ ("Total Notes" 10 gnosis-dashboard-sort-total-notes)])
+ (tabulated-list-init-header)
+ (setq tabulated-list-entries
+ (cl-loop for tag in tags
+ collect (list (car (gnosis-dashboard-output-tag tag))
+ (vconcat (gnosis-dashboard-output-tag tag)))))
+ (tabulated-list-print t)))
+
+(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
+ '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 (format "%s" item)))
+
+(defvar-keymap gnosis-dashboard-decks-mode-map
+ "e" #'gnosis-dashboard-edit-deck
+ "a" #'gnosis-dashboard-decks-add
+ "s" #'gnosis-dashboard-decks-suspend-deck
+ "d" #'gnosis-dashboard-decks-delete
+ "RET" #'gnosis-dashboard-decks-view-deck)
+
+(define-minor-mode gnosis-dashboard-decks-mode
+ "Minor mode for deck output."
+ :keymap gnosis-dashboard-decks-mode-map)
+
+(defun gnosis-dashboard-output-decks ()
+ "Return deck contents for gnosis dashboard."
+ (pop-to-buffer-same-window "*gnosis-dashboard*")
+ (gnosis-dashboard-mode)
+ (gnosis-dashboard-decks-mode)
+ (setq tabulated-list-format [("Name" 15 t)
+ ("Total Notes" 10 gnosis-dashboard-sort-total-notes)])
+ (tabulated-list-init-header)
+ (setq tabulated-list-entries
+ (cl-loop for id in (gnosis-select 'id 'decks '1=1 t)
+ for output = (gnosis-dashboard-output-deck id)
+ when output
+ collect (list (number-to-string id) (vconcat output))))
+ (tabulated-list-print t)
+ (setf gnosis-dashboard--current `(:type decks :ids ,(gnosis-select 'id 'decks '1=1 t))))
+
+(defun gnosis-dashboard-decks-add ()
+ "Add deck & refresh."
+ (interactive)
+ (gnosis-add-deck (read-string "Deck name: "))
+ (gnosis-dashboard-output-decks)
+ (revert-buffer t t t))
+
+(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))))
+
+(defun gnosis-dashboard-decks-suspend-deck (&optional deck-id)
+ "Suspend notes for DECK-ID.
+
+When called with called with a prefix, unsuspend all notes of deck."
+ (interactive)
+ (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+ (gnosis-suspend-deck deck-id)
+ (gnosis-dashboard-output-decks)
+ (revert-buffer t t t)))
+
+(defun gnosis-dashboard-decks-delete (&optional deck-id)
+ "Delete DECK-ID."
+ (interactive)
+ (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+ (gnosis-delete-deck deck-id)
+ (gnosis-dashboard-output-decks)
+ (revert-buffer t t t)))
+
+(defun gnosis-dashboard-decks-view-deck (&optional deck-id)
+ "View notes of DECK-ID."
+ (interactive)
+ (let ((deck-id (or deck-id (string-to-number (tabulated-list-get-id)))))
+ (gnosis-dashboard-output-notes (gnosis-collect-note-ids :deck deck-id))))
+
+(defvar-keymap gnosis-dashboard-mode-map
+ :doc "gnosis-dashboard keymap"
+ "q" #'quit-window
+ "h" #'gnosis-dashboard-menu)
+
+(define-derived-mode gnosis-dashboard-mode tabulated-list-mode "Gnosis Dashboard"
+ "Major mode for displaying Gnosis dashboard."
+ :keymap gnosis-dashboard-mode-map
+ (setq tabulated-list-padding 2
+ tabulated-list-sort-key nil
+ gnosis-dashboard--selected-ids nil)
+ (display-line-numbers-mode 0))
+
+(cl-defun gnosis-dashboard--search (&optional dashboard-type (note-ids nil))
+ "Display gnosis dashboard.
+
+NOTE-IDS: List of note ids to display on dashboard. When nil, prompt
+for dashboard type.
+
+DASHBOARD-TYPE: either 'Notes' or 'Decks' to display the respective dashboard."
+ (interactive)
+ (let ((dashboard-type (or dashboard-type
+ (cadr (read-multiple-choice
+ "Display dashboard for:"
+ '((?n "notes")
+ (?d "decks")
+ (?t "tags")
+ (?s "search")))))))
+ (if note-ids (gnosis-dashboard-output-notes note-ids)
+ (pcase dashboard-type
+ ("notes" (gnosis-dashboard-output-notes (gnosis-collect-note-ids)))
+ ("decks" (gnosis-dashboard-output-decks))
+ ("tags" (gnosis-dashboard-output-notes (gnosis-collect-note-ids :tags t)))
+ ("search" (gnosis-dashboard-output-notes
+ (gnosis-collect-note-ids :query (read-string "Search for note: "))))))
+ (tabulated-list-print t)))
+
+(defun gnosis-dashboard-mark-toggle ()
+ "Toggle mark on the current item in the tabulated-list."
+ (interactive)
+ (let ((inhibit-read-only t)
+ (entry (tabulated-list-get-entry))
+ (id (tabulated-list-get-id)))
+ (if (derived-mode-p 'tabulated-list-mode)
+ (if entry
+ (let ((beg (line-beginning-position))
+ (end (line-end-position))
+ (overlays (overlays-in (line-beginning-position) (line-end-position))))
+ (if (cl-some (lambda (ov) (overlay-get ov 'gnosis-mark)) overlays)
+ (progn
+ (remove-overlays beg end 'gnosis-mark t)
+ (setq gnosis-dashboard--selected-ids (remove id gnosis-dashboard--selected-ids))
+ ;; (message "Unmarked: %s" (aref entry 0))
+ )
+ (let ((ov (make-overlay beg end)))
+ (setf gnosis-dashboard--selected-ids
+ (append gnosis-dashboard--selected-ids (list id)))
+ (overlay-put ov 'face 'highlight)
+ (overlay-put ov 'gnosis-mark t)
+ ;; (message "Marked: %s" (aref entry 0))
+ )))
+ (message "No entry at point"))
+ (message "Not in a tabulated-list-mode"))))
+
+(defun gnosis-dashboard-unmark-all ()
+ "Unmark all items in the tabulated-list."
+ (interactive)
+ (let ((inhibit-read-only t))
+ (setq gnosis-dashboard--selected-ids nil)
+ (remove-overlays nil nil 'gnosis-mark t)
+ (message "All items unmarked")))
+
+(defun gnosis-dashboard-marked-delete ()
+ "Delete marked note entries."
+ (interactive)
+ (when (y-or-n-p "Delete selected notes?")
+ (cl-loop for note in gnosis-dashboard--selected-ids
+ do (gnosis-delete-note (string-to-number note) t))
+ (gnosis-dashboard-return)))
+
+(defun gnosis-dashboard-marked-suspend ()
+ "Suspend marked note entries."
+ (interactive)
+ (when (y-or-n-p "Toggle SUSPEND on selected notes?")
+ (cl-loop for note in gnosis-dashboard--selected-ids
+ do (gnosis-suspend-note (string-to-number note) t))
+ (gnosis-dashboard-return)))
+
+(transient-define-suffix gnosis-dashboard-suffix-query (query)
+ "Search for note content for QUERY."
+ (interactive "sSearch for note content: ")
+ (gnosis-dashboard-output-notes (gnosis-collect-note-ids :query query)))
+
+(transient-define-suffix gnosis-dashboard-suffix-decks ()
+ (interactive)
+ (gnosis-dashboard-output-decks))
+
+(transient-define-prefix gnosis-dashboard-menu ()
+ "Transient buffer for gnosis dashboard interactions."
+ [["Actions"
+ ("r" "Review" gnosis-review)
+ ("a" "Add note" gnosis-add-note)
+ ("q" "Quit" quit-window)]
+ ["Notes"
+ ("s" "Search" gnosis-dashboard-suffix-query)
+ ("n" "Notes" (lambda () (interactive) (gnosis-dashboard-output-notes (gnosis-collect-note-ids))))
+ ("d" "Decks" gnosis-dashboard-suffix-decks)
+ ("t" "Tags" (lambda () (interactive) (gnosis-dashboard-output-tags)))]])
+
+;; TODO: Create a dashboard utilizing widgets
+;;;###autoload
+(defun gnosis-dashboard ()
+ "Test function to create an editable field and a search button."
+ (interactive)
+ (delete-other-windows)
+ (let ((buffer-name "*Gnosis Dashboard*"))
+ (when (get-buffer buffer-name)
+ (kill-buffer buffer-name)) ;; Kill the existing buffer if it exists
+ (let ((buffer (get-buffer-create buffer-name)))
+ (with-current-buffer buffer
+ (widget-insert "\n"
+ (gnosis-center-string
+ (format "%s" (propertize "Gnosis Dashboard" 'face 'gnosis-dashboard-header-face))))
+ (gnosis-insert-separator)
+ ;; (widget-insert (gnosis-center-string (propertize "Stats:" 'face 'underline)) "\n\n")
+ (widget-insert (gnosis-center-string
+ (format "Reviewed today: %s | New: %s"
+ (propertize
+ (number-to-string (gnosis-get-date-total-notes))
+ 'face
+ 'font-lock-variable-use-face)
+ (propertize
+ (number-to-string (gnosis-get-date-new-notes))
+ 'face
+ 'font-lock-keyword-face))))
+ (insert "\n")
+ (widget-insert (gnosis-center-string
+ (format "Daily Average: %s"
+ (propertize (number-to-string (gnosis-dashboard-output-average-rev))
+ 'face 'font-lock-type-face))))
+ (insert "\n")
+ (widget-insert (gnosis-center-string
+ (format "Due notes: %s"
+ (propertize
+ (number-to-string (length (gnosis-review-get-due-notes)))
+ 'face 'error))))
+ (insert "\n\n")
+ (widget-insert (gnosis-center-string
+ (format "Current streak: %s days"
+ (propertize
+ (number-to-string
+ (gnosis-dashboard--streak
+ (gnosis-select 'date 'activity-log '1=1 t)))
+ 'face 'success))))
+ (insert "\n\n")
+ ;; (gnosis-dashboard-month-overview (or gnosis-dashboard-months 0))
+ (use-local-map widget-keymap)
+ (widget-setup))
+ (pop-to-buffer-same-window buffer)
+ (goto-char (point-min))
+ (gnosis-dashboard-mode)
+ (gnosis-dashboard-menu))))
+
+(provide 'gnosis-dashboard)
+;;; gnosis-dashboard.el ends here
diff --git a/gnosis-test.el b/gnosis-test.el
index 3104789..1504e02 100644
--- a/gnosis-test.el
+++ b/gnosis-test.el
@@ -28,7 +28,10 @@
;; easier by creating a testing environment with random inputs.
;;; Code:
+(add-to-list 'load-path ".")
+
+(require 'ert)
(require 'gnosis)
(defvar gnosis-test-tags '("anatomy" "thoracic" "serratus-anterior"
@@ -60,74 +63,74 @@ DECK: Deck to add the inputs to."
(unless (gnosis-get 'name 'decks `(= name ,testing-deck))
(gnosis-add-deck testing-deck))
(when (y-or-n-p "Add MCQ type?")
- (dotimes (_ num)
- (gnosis-add-note--mcq :deck testing-deck
- :question "A *37-year-old* man is admitted to the
+ (emacsql-with-transaction gnosis-db
+ (dotimes (_ num)
+ (gnosis-add-note--mcq :deck testing-deck
+ :question "A *37-year-old* man is admitted to the
emergency department after a severe car crash. /After/ examining the
patient the emergency medicine physician concludes *that* the serratus
anterior muscle is damaged. ~Which~ of the following nerves innervates
the =serratus anterior muscle=?"
- :choices '("Long thoracic" "Axillary" "Spinal accessory" "Dorsal scapular" "Thoracodorsal")
- :correct-answer 1
- :extra "The long thoracic is the only nerve that
+ :choices '("Long thoracic" "Axillary" "Spinal accessory" "Dorsal scapular" "Thoracodorsal")
+ :correct-answer 1
+ :extra "The long thoracic is the only nerve that
innervates the serratus anterior. The axillary nerve innervates the
deltoid, the spinal accessory nerve innervates the sternocleidomastoid
and trapezius, the dorsal scapular nerve supplies the rhomboid muscles
and levator scapulae, and the latissimus dorsi is the muscle supplied
by the thoracodorsal nerve."
- :images (cons gnosis-test-image gnosis-test-image)
- :tags (gnosis-test-random-items gnosis-test-tags 2))))
- (when (y-or-n-p "Add Basic type questions?")
- (dotimes (_ num)
- (gnosis-add-note--basic :deck testing-deck
- :question "A question"
- :hint "hint"
- :answer "answer"
- :extra "extra"
:images (cons gnosis-test-image gnosis-test-image)
:tags (gnosis-test-random-items gnosis-test-tags 2))))
- (when (y-or-n-p "Add single Cloze type?")
- (dotimes (_ num)
- ;; TODO: Update tests for include hints.
- (gnosis-add-note--cloze :deck testing-deck
- :note "this is a {c1:note}"
- :tags (gnosis-test-random-items gnosis-test-tags 2)
- :images (cons gnosis-test-image gnosis-test-image)
- :extra "extra")))
- (when (y-or-n-p "Add multimple Clozes note?")
- (dotimes (_ num)
- (gnosis-add-note--cloze :deck testing-deck
- :note "this is a {c1:note}, a note with multiple {c1:clozes::what}"
- :tags (gnosis-test-random-items gnosis-test-tags 2)
- :images (cons gnosis-test-image gnosis-test-image)
- :extra "extra")))
- (when (y-or-n-p "Add y-or-n note type?")
- (dotimes (_ num)
- (gnosis-add-note--y-or-n :deck testing-deck
- :question "Is Codeine recommended in breastfeeding mothers?"
- :hint "hint"
- :answer 110
- :extra "extra"
- :images (cons gnosis-test-image gnosis-test-image)
- :tags (gnosis-test-random-items gnosis-test-tags 2))))))
+ (when (y-or-n-p "Add Basic type questions?")
+ (dotimes (_ num)
+ (gnosis-add-note--basic :deck testing-deck
+ :question "A question"
+ :hint "hint"
+ :answer "answer"
+ :extra "extra"
+ :images (cons gnosis-test-image gnosis-test-image)
+ :tags (gnosis-test-random-items gnosis-test-tags 2))))
+ (when (y-or-n-p "Add single Cloze type?")
+ (dotimes (_ num)
+ ;; TODO: Update tests for include hints.
+ (gnosis-add-note--cloze :deck testing-deck
+ :note "this is a {c1:note}"
+ :tags (gnosis-test-random-items gnosis-test-tags 2)
+ :images (cons gnosis-test-image gnosis-test-image)
+ :extra "extra")))
+ (when (y-or-n-p "Add multimple Clozes note?")
+ (dotimes (_ num)
+ (gnosis-add-note--cloze :deck testing-deck
+ :note "this is a {c1:note}, a note with multiple {c1:clozes::what}"
+ :tags (gnosis-test-random-items gnosis-test-tags 2)
+ :images (cons gnosis-test-image gnosis-test-image)
+ :extra "extra")))
+ (when (y-or-n-p "Add y-or-n note type?")
+ (dotimes (_ num)
+ (gnosis-add-note--y-or-n :deck testing-deck
+ :question "Is Codeine recommended in breastfeeding mothers?"
+ :hint "hint"
+ :answer 110
+ :extra "extra"
+ :images (cons gnosis-test-image gnosis-test-image)
+ :tags (gnosis-test-random-items gnosis-test-tags 2)))))))
(defun gnosis-test-start (&optional note-num)
"Begin/End testing env.
If ask nil, leave testing env"
(interactive)
- (let ((ask (y-or-n-p "Start development env (n for exit)?"))
- (testing-dir (expand-file-name "testing" gnosis-dir)))
+ (let* ((ask (y-or-n-p "Start development env (n for exit)?"))
+ (testing-dir (expand-file-name "testing" gnosis-dir))
+ (testing-db (expand-file-name "testing.db" testing-dir)))
(if ask
(progn
(unless (file-exists-p testing-dir)
(make-directory testing-dir))
- (setf gnosis-db (emacsql-sqlite-open (expand-file-name "testing.db" testing-dir)))
+ (when (file-exists-p testing-db)
+ (delete-file testing-db))
+ (setf gnosis-db (emacsql-sqlite-open testing-db))
(setf gnosis-testing t)
- (dolist (table '(notes decks review review-log extras))
- (condition-case nil
- (gnosis--drop-table table)
- (error (message "No %s table to drop." table))))
(gnosis-db-init)
(gnosis-test-add-fields note-num)
(message "Adding testing values...")
@@ -136,6 +139,371 @@ If ask nil, leave testing env"
(setf gnosis-testing nil)
(message "Exited development env."))))
+(ert-deftest gnosis-test-algorithm-next-interval-proto ()
+ "Test next interval for proto values."
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 1.3
+ :success t
+ :successful-reviews 0
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 0
+ :lethe 3)
+ (gnosis-algorithm-date 1)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 1.3
+ :success t
+ :successful-reviews 1
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 0
+ :lethe 3)
+ (gnosis-algorithm-date 2)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 1.3
+ :success t
+ :successful-reviews 2
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 0
+ :lethe 3)
+ (gnosis-algorithm-date 3)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 1.3
+ :success t
+ :successful-reviews 3
+ :amnesia 0.5
+ :proto '(1 2 3 70)
+ :c-fails 0
+ :lethe 3)
+ (gnosis-algorithm-date 70)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 2.0
+ :success t
+ :successful-reviews 4
+ :amnesia 0.5
+ :proto '(1 2 3 70)
+ :c-fails 0
+ :lethe 3)
+ (gnosis-algorithm-date 2)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 3.0
+ :success t
+ :successful-reviews 5
+ :amnesia 0.5
+ :proto '(1 2 3 70)
+ :c-fails 0
+ :lethe 3)
+ (gnosis-algorithm-date 3))))
+
+(ert-deftest gnosis-test-algorithm-next-interval-lethe ()
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 0
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 3)
+ (gnosis-algorithm-date)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 0
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 0
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 20.0
+ :success nil
+ :successful-reviews 2
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date 5)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 20.0
+ :success nil
+ :successful-reviews 2
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 5
+ :lethe 4)
+ (gnosis-algorithm-date))))
+
+(ert-deftest gnosis-test-algorithm-next-interval-success ()
+ "Test next interval for successful non-proto recalls."
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 2.0
+ :success t
+ :successful-reviews 5
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 500
+ :lethe 4)
+ (gnosis-algorithm-date 20)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 3
+ :gnosis-synolon 1.3
+ :success t
+ :successful-reviews 5
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 300
+ :lethe 4)
+ (gnosis-algorithm-date 4))))
+
+(ert-deftest gnosis-test-algorithm-next-interval-amnesia ()
+ "Test next interval for failed non-proto recalls."
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 3
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date 5)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 3
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 3
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date 2)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 2
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 3
+ :amnesia 0.5
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date 1)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 3
+ :amnesia 0.7
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date 3)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 3
+ :amnesia 0.8
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date 2)))
+ (should (equal (gnosis-algorithm-next-interval :last-interval 10
+ :gnosis-synolon 1.3
+ :success nil
+ :successful-reviews 3
+ :amnesia 1.0
+ :proto '(1 2 3)
+ :c-fails 3
+ :lethe 4)
+ (gnosis-algorithm-date))))
+
+(ert-deftest gnosis-test-algorithm-next-gnosis-synolon ()
+ "Test algorithm for gnosis synolon (totalis)."
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.35 0.30 1.30)
+ :success t
+ :epignosis 0.3
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 1
+ :c-failures 0)
+ '(0.35 0.30 1.65)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 1.30)
+ :success t
+ :epignosis 0.3
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 1
+ :c-failures 0)
+ '(0.45 0.30 1.75)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 2.0)
+ :success nil
+ :epignosis 0.3
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 1
+ :c-failures 0)
+ '(0.45 0.30 1.70)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success nil
+ :epignosis 0.3
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 1
+ :c-failures 0)
+ '(0.45 0.30 3.2))))
+
+(ert-deftest gnosis-test-algorithm-test-epignosis ()
+ "Test epignosis during anagnosis events."
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success t
+ :epignosis 0.3
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 3
+ :c-failures 0)
+ '(0.75 0.30 3.95)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success t
+ :epignosis 0.2
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 3
+ :c-failures 0)
+ '(0.65 0.30 3.95)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success t
+ :epignosis 0.2
+ :agnoia 0.2
+ :anagnosis 4
+ :c-successes 3
+ :c-failures 0)
+ '(0.45 0.30 3.95))))
+
+(ert-deftest gnosis-test-algorithm-test-agnoia ()
+ "Test epignosis during anagnosis events."
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success nil
+ :epignosis 0.3
+ :agnoia 0.2
+ :anagnosis 3
+ :c-successes 0
+ :c-failures 3)
+ '(0.45 0.5 3.2)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success nil
+ :epignosis 0.3
+ :agnoia 0.3
+ :anagnosis 3
+ :c-successes 0
+ :c-failures 3)
+ '(0.45 0.6 3.2)))
+ (should (equal (gnosis-algorithm-next-gnosis
+ :gnosis '(0.45 0.30 3.5)
+ :success nil
+ :epignosis 0.3
+ :agnoia 0.3
+ :anagnosis 4
+ :c-successes 0
+ :c-failures 3)
+ '(0.45 0.3 3.2))))
+
+(ert-deftest gnosis-test-get-note-deck-value ()
+ "Test recovery of deck amnesia values."
+ (let ((test-values '((:deck "demo"
+ (:proto (0 1 3) :anagnosis 3 :epignosis 0.5 :agnoia 0.3 :amnesia 0.5 :lethe 3))
+ (:deck "demo2"
+ (:proto (0 1 3)))
+ (:tag "demo"
+ (:proto (1 2) :anagnosis 2 :epignosis 0.2 :agnoia 0.3 :amnesia 0.4 :lethe 4)))))
+ (should (= (gnosis-get-custom-deck-value "demo" :amnesia test-values) 0.5))
+ (should (= (gnosis-get-note-deck-epignosis nil "demo" test-values) 0.5))
+ (should (= (gnosis-get-note-deck-amnesia nil "demo" test-values) 0.5))
+ (should (= (gnosis-get-note-deck-agnoia nil "demo" test-values) 0.3))
+ (should (= (gnosis-get-note-deck-anagnosis nil "demo" test-values) 3))
+ (should (= (gnosis-get-note-deck-lethe nil "demo" test-values) 3))
+ (should (= (gnosis-get-note-deck-lethe nil "demo" test-values) 3))
+ (should (= (gnosis-get-note-deck-lethe nil "demo2" test-values) gnosis-algorithm-lethe-value))
+ (should (= (gnosis-get-note-deck-anagnosis nil "demo2" test-values) gnosis-algorithm-anagnosis-value))
+ (should (= (gnosis-get-note-deck-epignosis nil "demo2" test-values) gnosis-algorithm-epignosis-value))
+ (should (= (gnosis-get-note-deck-agnoia nil "demo2" test-values) gnosis-algorithm-agnoia-value))
+ (should (= (gnosis-get-note-deck-amnesia nil "demo2" test-values) gnosis-algorithm-amnesia-value))))
+
+(ert-deftest gnosis-test-get-custom-tag-amnesia ()
+ "Test recovery of tag amnesia values."
+ (let ((test-values '((:deck "tag1" (:proto (99 99 99) :epignosis 0.5 :agnoia 0.3 :amnesia 0.5 :lethe 3))
+ (:tag "tag1" (:proto (0 1 3) :epignosis 0.5 :agnoia 0.3 :amnesia 0.3 :lethe 3))
+ (:tag "tag2" (:proto (1 2) :epignosis 0.5 :agnoia 0.3 :amnesia 0.5 :lethe 4))
+ (:tag "tag3" (:proto (2 4 10) :epignosis 0.5 :agnoia 0.5 :amnesia 0.9 :lethe 2)))))
+ (should (equal (gnosis-get-custom-tag-values nil :amnesia '("tag1") test-values) (list 0.3)))
+ (should (equal (gnosis-get-note-tag-amnesia nil '("tag1") test-values) 0.3))
+ (should (equal (gnosis-get-note-tag-amnesia nil '("tag1" "tag2") test-values) 0.5))
+ (should (equal (gnosis-get-note-tag-amnesia nil '("tag1" "tag2" "tag3") test-values) 0.9))
+ (should (equal (gnosis-get-note-tag-amnesia nil '("tag2" "tag1") test-values) 0.5))))
+
+(ert-deftest gnosis-test-get-proto ()
+ (let ((test-values '((:deck "deck1" (:proto (0 1 3) :epignosis 0.5 :agnoia 0.3 :amnesia 0.3 :lethe 3))
+ (:tag "tag1" (:epignosis 0.5))
+ (:tag "tag2" (:proto (2 2 2) :epignosis 0.5))
+ (:tag "tag3" (:proto (1 1 1 1) :epignosis 0.5)))))
+ (should (equal (gnosis-get-note-proto nil '("tag1") "deck1" test-values) '(0 1 3)))
+ (should (equal (gnosis-get-note-proto nil '("tag1" "tag2") "deck1" test-values) '(2 2 2)))
+ (should (equal (gnosis-get-note-proto nil '("tag1" "tag2" "tag3") "deck1" test-values) '(2 2 2 1)))))
+
+(ert-deftest gnosis-test-get-note-amnesia ()
+ (let ((test-values '((:deck "deck1" (:proto (0 1 3) :epignosis 0.5 :agnoia 0.3 :amnesia 0.3 :lethe 3))
+ (:tag "tag1" (:proto (10 1) :epignosis 0.5))
+ (:tag "tag2" (:proto (2 2 2) :epignosis 0.5 :amnesia 0.2))
+ (:tag "tag3" (:proto (1 1 1 1) :epignosis 0.5 :amnesia 0.6)))))
+ (should (equal (gnosis-get-note-amnesia nil "deck1" '("tag1") test-values) 0.3))
+ (should (equal (gnosis-get-note-amnesia nil "deck1" '("tag1" "tag2") test-values) 0.2))
+ (should (equal (gnosis-get-note-amnesia nil "deck1" '("tag1" "tag3") test-values) 0.6))
+ (should (equal (gnosis-get-note-amnesia nil "deck1" '("tag2" "tag3") test-values) 0.6))))
+
+(ert-deftest gnosis-test-get-note-epginosis ()
+ (let ((test-values'((:deck "deck1" (:proto (0 1 3) :epignosis 0.5 :agnoia 0.3 :amnesia 0.3 :lethe 3))
+ (:tag "tag1" (:proto (10 1) :amnesia 0.5))
+ (:tag "tag2" (:proto (2 2 2) :epignosis 0.6 :amnesia 0.2))
+ (:tag "tag3" (:proto (1 1 1 1) :epignosis 0.7 :amnesia 0.4)))))
+ (should (equal (gnosis-get-note-epignosis nil "deck1" '("tag1") test-values) 0.5))
+ (should (equal (gnosis-get-note-epignosis nil "deck1" '("tag1" "tag2") test-values) 0.6))
+ (should (equal (gnosis-get-note-epignosis nil "deck1" '("tag2" "tag3") test-values) 0.7))))
+
+(ert-deftest gnosis-test-get-note-agnoia ()
+ (let ((test-values'((:deck "deck1" (:proto (0 1 3) :epignosis 0.5 :agnoia 0.3 :amnesia 0.3 :lethe 3))
+ (:tag "tag1" (:proto (10 1) :epignosis 0.4 :amnesia 0.5))
+ (:tag "tag2" (:proto (2 2 2) :epignosis 0.6 :amnesia 0.2 :agnoia 0.4))
+ (:tag "tag3" (:proto (1 1 1 1) :epignosis 0.7 :amnesia 0.4 :agnoia 0.5)))))
+ (should (equal (gnosis-get-note-agnoia nil "deck1" '("tag1") test-values) 0.3))
+ (should (equal (gnosis-get-note-agnoia nil "deck1" '("tag1" "tag2") test-values) 0.4))
+ (should (equal (gnosis-get-note-agnoia nil "deck1" '("tag1" "tag2" "tag3") test-values) 0.5))))
+
+(ert-deftest gnosis-test-get-note-anagnosis ()
+ (let ((test-values '((:deck "deck1" (:proto (0 1 3) :anagnosis 3 :amnesia 0.3 :lethe 3))
+ (:deck "deck2" (:anagnosis 1 :amnesia 0.3 :lethe 3))
+ (:tag "tag1" (:proto (10 1)))
+ (:tag "tag2" (:proto (2 2 2) :amnesia 0.2 :agnoia 0.4 :anagnosis 2))
+ (:tag "tag3" (:proto (1 1 1 1) :amnesia 0.3)))))
+ (should (equal (gnosis-get-note-anagnosis nil "deck1" '("tag1") test-values) 3))
+ (should (equal (gnosis-get-note-anagnosis nil "deck1" '("tag1" "tag2") test-values) 2))
+ (should (equal (gnosis-get-note-anagnosis nil "deck2" '("tag1" "tag2") test-values) 2))))
+
+(ert-deftest gnosis-test-get-note-lethe ()
+ (let ((test-values '((:deck "deck1" (:proto (0 1 3) :anagnosis 3 :amnesia 0.3 :lethe 3))
+ (:deck "deck2" (:anagnosis 1 :lethe 9))
+ (:tag "tag1" (:proto (10 1) :lethe nil))
+ (:tag "tag2" (:proto (2 2 2) :lethe 2))
+ (:tag "tag3" (:proto (1 1 1 1) :amnesia 0.3 :lethe 1)))))
+ (should (equal (gnosis-get-note-lethe nil "deck1" '("tag1") test-values) 3))
+ (should (equal (gnosis-get-note-lethe nil "deck1" '("tag2") test-values) 2))
+ (should (equal (gnosis-get-note-lethe nil "deck2" '("tag3" "tag2") test-values) 1))
+ (should (equal (gnosis-get-note-lethe nil "deck2" '("tag1" "tag2") test-values) 2))))
+
+
+(ert-run-tests-batch-and-exit)
(provide 'gnosis-test)
;;; gnosis-test.el ends here
diff --git a/gnosis.el b/gnosis.el
index 8291a1b..8790d5e 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.3.2
+;; Version: 0.4.0
-;; Package-Requires: ((emacs "27.2") (emacsql "20240124") (compat "29.1.4.2"))
+;; Package-Requires: ((emacs "27.2") (emacsql "20240124") (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
@@ -24,16 +24,19 @@
;;; Commentary:
-;; Gnosis is a spaced repetition system for note taking and
-;; self-testing. Notes are organized in a Question/Answer/Explanation
-;; format and reviewed at spaced intervals. Interval durations are
-;; based on the success or failure of recalling the answer to each
-;; question.
+;; Gnosis (γνῶσις) is a spaced repetition system that enhances memory
+;; retention through active recall. It employs a Q&A format, where each
+;; note consists of a question, answer, and explanation. Notes are
+;; reviewed at optimally spaced intervals based on the user's success or
+;; failure to recall the answer. Key benefits arise from writing out
+;; answers when reviewing notes, fostering deeper understanding
+;; and improved memory retention.
-;; Gnosis uses a highly customizable algorithm. Unlike traditional
-;; methods, it doesn't depend on subjective user ratings to determine
-;; the next review interval. Instead, it evaluates the user's success
-;; or failure in recalling an answer by typing it.
+;; Gnosis algorithm is highly adjustable, allowing users to set specific
+;; values not just for note decks but for tags as well. Gnosis'
+;; adjustability allows users to fine-tune settings not only for entire
+;; note collections but also for specific tagged topics, thereby creating
+;; a personalized learning environment for each topic.
;;; Code:
@@ -45,6 +48,7 @@
(require 'gnosis-algorithm)
(require 'gnosis-string-edit)
+(require 'gnosis-dashboard)
(require 'animate)
@@ -136,10 +140,7 @@ a string describing the action."
(defvar gnosis-testing nil
"When t, warn user he is in a testing environment.")
-(defvar gnosis-dashboard-note-ids nil
- "Store note ids for dashboard.")
-
-(defconst gnosis-db-version 2
+(defconst gnosis-db-version 3
"Gnosis database version.")
(defvar gnosis-note-types '("MCQ" "Cloze" "Basic" "Double" "y-or-n")
@@ -190,16 +191,41 @@ Seperate the question/stem from options."
(defcustom gnosis-center-content-p t
"Non-nil means center content."
:type 'boolean
- :group 'gosis)
+ :group 'gnosis)
(defcustom gnosis-apply-highlighting-p t
"Non-nil means apply syntax highlighting."
:type 'boolean
- :group 'gosis)
+ :group 'gnosis)
+
+(defcustom gnosis-new-notes-limit nil
+ "Total new notes limit."
+ :type '(choice (const :tag "None" nil)
+ (integer :tag "Number"))
+ :group 'gnosis)
+
+(defcustom gnosis-review-new-first t
+ "Review new notes first.
+
+When nil, review new notes last."
+ :type 'bolean
+ :group 'gnosis)
(defvar gnosis-due-notes-total nil
"Total due notes.")
+(defvar gnosis-review-notes nil
+ "Review notes.")
+
+;; 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)))
+ "Custom review values for adjusting gnosis algorithm.")
+
+(defvar gnosis-custom--valid-values
+ '(:proto :anagnosis :epignosis :agnoia :amnesia :lethe))
+
;;; Faces
(defgroup gnosis-faces nil
@@ -278,13 +304,21 @@ Optional argument FLATTEN, when non-nil, flattens the result."
"Select VALUE from TABLE for note ID."
(gnosis-select value table `(= id ,id) t))
+(defun gnosis-table-exists-p (table)
+ "Check if TABLE exists."
+ (let ((tables (mapcar (lambda (str) (replace-regexp-in-string "_" "-" (symbol-name str)))
+ (cdr (gnosis-select 'name 'sqlite-master '(= type table) t)))))
+ (member (symbol-name table) tables)))
+
(cl-defun gnosis--create-table (table &optional values)
"Create TABLE for VALUES."
- (emacsql gnosis-db `[:create-table ,table ,values]))
+ (unless (gnosis-table-exists-p table)
+ (emacsql gnosis-db `[:create-table ,table ,values])))
(cl-defun gnosis--drop-table (table)
"Drop TABLE from `gnosis-db'."
- (emacsql gnosis-db `[:drop-table ,table]))
+ (when (gnosis-table-exists-p table)
+ (emacsql gnosis-db `[:drop-table ,table])))
(cl-defun gnosis--insert-into (table values)
"Insert VALUES to TABLE."
@@ -309,9 +343,11 @@ 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?")
+(defun gnosis-delete-note (id &optional verification)
+ "Delete note with ID.
+
+When VERIFICATION is non-nil, skip `y-or-n-p' prompt."
+ (when (or verification (y-or-n-p "Delete note?"))
(emacsql-with-transaction gnosis-db (gnosis--delete 'notes `(= id ,id)))))
(defun gnosis-delete-deck (&optional id)
@@ -374,22 +410,22 @@ Acts only when CENTER? is t."
(let ((window-width (window-width))
(center? (or center? gnosis-center-content-p)))
(if center?
- (mapconcat
- (lambda (line)
- (let* ((text (string-trim line))
- (wrapped (with-temp-buffer
- (insert text)
- (fill-region (point-min) (point-max))
- (buffer-string)))
- (lines (split-string wrapped "\n")))
- (mapconcat
- (lambda (line)
- (let ((padding (max (/ (- window-width (length line)) 2) 0)))
- (concat (make-string padding ? ) line)))
- lines
- "\n")))
- (split-string input-string "\n")
- "\n")
+ (mapconcat
+ (lambda (line)
+ (let* ((text (string-trim line))
+ (wrapped (with-temp-buffer
+ (insert text)
+ (fill-region (point-min) (point-max))
+ (buffer-string)))
+ (lines (split-string wrapped "\n")))
+ (mapconcat
+ (lambda (line)
+ (let ((padding (max (/ (- window-width (length line)) 2) 0)))
+ (concat (make-string padding ? ) line)))
+ lines
+ "\n")))
+ (split-string input-string "\n")
+ "\n")
input-string)))
(defun gnosis-apply-center-buffer-overlay (&optional point)
@@ -542,7 +578,7 @@ When SUCCESS nil, display USER-INPUT as well"
;; Insert user wrong answer
(when (not success)
(insert "\n"
- (propertize "Your answer:" 'face 'gnosis-face-directions)
+ (propertize "Your answer:" 'face 'gnosis-face-directions)
" "
(propertize user-input 'face 'gnosis-face-false))
(gnosis-center-current-line)))
@@ -617,12 +653,12 @@ inserted in the buffer.
Also see `gnosis-string-edit'."
(gnosis-string-edit prompt string
- (lambda (edited)
- (setq string (substring-no-properties edited))
- (exit-recursive-edit))
- :abort-callback (lambda ()
- (exit-recursive-edit)
- (error "Aborted edit")))
+ (lambda (edited)
+ (setq string (substring-no-properties edited))
+ (exit-recursive-edit))
+ :abort-callback (lambda ()
+ (exit-recursive-edit)
+ (error "Aborted edit")))
(recursive-edit)
string)
@@ -663,14 +699,14 @@ Set SPLIT to t to split all input given."
(defun gnosis-add-deck (name)
"Create deck with NAME."
(interactive (list (read-string "Deck Name: ")))
- (when gnosis-testing
- (unless (y-or-n-p "You are using a testing environment! Continue?")
- (error "Aborted")))
- (if (gnosis-get 'name 'decks `(= name ,name))
- (error "Deck `%s' already exists" name)
- (let ((deck-id (gnosis-generate-id 5 t)))
- (gnosis--insert-into 'decks `([,deck-id ,name nil nil nil nil nil]))
- (message "Created deck '%s'" name))))
+ (when gnosis-testing
+ (unless (y-or-n-p "You are using a testing environment! Continue?")
+ (error "Aborted")))
+ (if (gnosis-get 'name 'decks `(= name ,name))
+ (error "Deck `%s' already exists" name)
+ (let ((deck-id (gnosis-generate-id 5 t)))
+ (gnosis--insert-into 'decks `([,deck-id ,name]))
+ (message "Created deck '%s'" name))))
(defun gnosis--get-deck-name (&optional id)
"Get deck name for ID, or prompt for deck name when ID is nil."
@@ -684,6 +720,11 @@ Set SPLIT to t to split all input given."
"Return id for DECK name."
(gnosis-get 'id 'decks `(= name ,deck)))
+(defun gnosis-get-note-deck-name (id)
+ "Return deck name of note ID."
+ (let ((deck (gnosis-get 'deck-id 'notes `(= id ,id))))
+ (and deck (gnosis--get-deck-name deck))))
+
(defun gnosis-get-deck--note (id &optional name)
"Get deck id for note ID.
@@ -692,24 +733,13 @@ If NAME is t, return name of deck."
(deck (gnosis-get 'deck-id 'notes id-clause)))
(if name (gnosis--get-deck-name deck) deck)))
-(defun gnosis-get-deck-ff (id)
- "Return failure factor for deck of ID."
- (let* ((id-clause `(= id ,id))
- (deck-ff (gnosis-get 'failure-factor 'decks id-clause)))
- deck-ff))
-
-(defun gnosis-get-note-ff (id)
- "Return failure factor for note ID."
- (let ((deck-ff (gnosis-get-deck-ff (gnosis-get-deck--note id)))
- (note-ff (gnosis-get 'ff 'review `(= id ,id))))
- (if (and deck-ff (> deck-ff note-ff))
- deck-ff
- note-ff)))
-
-(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? "))
+(cl-defun gnosis-suspend-note (id &optional verification)
+ "Suspend note with ID.
+
+When VERIFICATION is non-nil, skips `y-or-n-p' prompt."
+ (let* ((suspended (= (gnosis-get 'suspend 'review-log `(= id ,id)) 1))
+ (verification (or verification (y-or-n-p (if suspended "Unsuspend note? " "Suspend note? ")))))
+ (when verification
(if suspended
(gnosis-update 'review-log '(= suspend 0) `(= id ,id))
(gnosis-update 'review-log '(= suspend 1) `(= id ,id))))))
@@ -780,16 +810,14 @@ SUSPEND: Integer value of 1 or 0, where 1 suspends the card
IMAGE: Image to display during review.
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."
+If a gnosis--insert-into fails, the whole transaction will be."
(let* ((deck-id (gnosis--get-deck-id deck))
- (initial-interval (gnosis-get-deck-initial-interval deck-id))
(note-id (gnosis-generate-id)))
(emacsql-with-transaction gnosis-db
;; Refer to `gnosis-db-schema-SCHEMA' e.g `gnosis-db-schema-review-log'
(gnosis--insert-into 'notes `([,note-id ,type ,main ,options ,answer ,tags ,deck-id]))
- (gnosis--insert-into 'review `([,note-id ,gnosis-algorithm-ef ,gnosis-algorithm-ff ,initial-interval]))
+ (gnosis--insert-into 'review `([,note-id ,gnosis-algorithm-gnosis-value
+ ,gnosis-algorithm-amnesia-value]))
(gnosis--insert-into 'review-log `([,note-id ,(gnosis-algorithm-date)
,(gnosis-algorithm-date) 0 0 0 0 ,suspend 0]))
(gnosis--insert-into 'extras `([,note-id ,extra ,image ,second-image])))))
@@ -1119,11 +1147,11 @@ TYPE: Type of gnosis note, must be one of `gnosis-note-types'"
(defun gnosis-cloze-check (sentence clozes)
"Check if CLOZES are found in SENTENCE."
- (catch 'not-found
- (dolist (cloze clozes)
- (unless (string-match-p cloze sentence)
- (throw 'not-found nil)))
- t))
+ (catch 'not-found
+ (dolist (cloze clozes)
+ (unless (string-match-p cloze sentence)
+ (throw 'not-found nil)))
+ t))
(defun gnosis-cloze-remove-tags (string)
"Replace cloze tags and hints in STRING.
@@ -1155,7 +1183,7 @@ Valid cloze formats include:
(nreverse result-alist))))
(defun gnosis-cloze-extract-answers (nested-lst)
- "Extract cloze answers for string clozes inside the NESTED-LST.
+ "Extract cloze answers for string clozes inside the NESTED-LST.
This function should be used in combination with `gnosis-cloze-extract-answers'."
(mapcar (lambda (lst)
@@ -1224,11 +1252,11 @@ Optionally, add cusotm PROMPT."
(let* ((prompt (or prompt "Select image: "))
(image (if (y-or-n-p "Add review image?")
(gnosis-completing-read prompt
- (cons nil (gnosis-directory-files gnosis-images-dir)))
+ (cons nil (gnosis-directory-files gnosis-images-dir)))
nil))
(extra-image (if (y-or-n-p "Add post review image?")
(gnosis-completing-read prompt
- (cons nil (gnosis-directory-files gnosis-images-dir))))))
+ (cons nil (gnosis-directory-files gnosis-images-dir))))))
(cons image extra-image))
nil))
@@ -1237,19 +1265,25 @@ 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)))
-
-(defun gnosis-select-by-tag (input-tags &optional due)
+;; 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.
-If DUE, return only due notes."
+If DUE, return only due notes.
+If SUSPENDED-P, return suspended notes as well."
(cl-assert (listp input-tags) t "Input tags must be a list")
(cl-assert (booleanp due) "Due value must be a boolean")
(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))
+ (or (not suspended-p) (not (gnosis-suspended-p id)))
(if due (gnosis-review-is-due-p id) t))
collect id))
+(defun gnosis-get-tag-notes (tag)
+ "Return note ids for TAG."
+ (let ((notes (gnosis-select 'id 'notes `(like tags ',(format "%%\"%s\"%%" tag)) t)))
+ notes))
+
(defun gnosis-suspended-p (id)
"Return t if note with ID is suspended."
(= (gnosis-get 'suspend 'review-log `(= id ,id)) 1))
@@ -1349,6 +1383,40 @@ provided, use it as the default value."
(setf gnosis-previous-note-tags tags)
(if (equal tags '("")) '("untagged") tags)))
+;; Collecting note ids
+
+;; TODO: Rewrite. 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.
+
+TAGS: boolean value, t to specify tags.
+DUE: boolean value, t to specify due notes.
+DECK: Integer, specify deck id.
+QUERY: String value,"
+ (cl-assert (and (booleanp due) (booleanp tags) (or (numberp deck) (null deck)) (or (stringp query) (null query)))
+ nil "Incorrect value passed to `gnosis-collect-note-ids'")
+ (cond ((and (null tags) (null due) (null deck) (null query))
+ (gnosis-select 'id 'notes '1=1 t))
+ ;; All due notes
+ ((and (null tags) due (null deck))
+ (gnosis-review-get-due-notes))
+ ;; All notes for tags
+ ((and tags (null due) (null deck))
+ (gnosis-select-by-tag (gnosis-tag-prompt)))
+ ;; All due notes for tags
+ ((and tags due (null deck))
+ (gnosis-select-by-tag (gnosis-tag-prompt) t))
+ ;; All notes for deck
+ ((and (null tags) (null due) deck)
+ (gnosis-get-deck-notes deck nil))
+ ;; All due notes for deck
+ ((and (null tags) deck due)
+ (gnosis-get-deck-notes deck t))
+ ;; Query
+ ((and (null tags) (null due) (null deck) query)
+ (gnosis-search-note query))))
+
;; Review
;;;;;;;;;;
(defun gnosis-review-is-due-p (note-id)
@@ -1369,10 +1437,19 @@ well."
(defun gnosis-review-get-due-notes ()
"Return a list due notes id for current date."
- (let ((notes (gnosis-select 'id 'notes '1=1 t)))
- (cl-loop for note in notes
- when (gnosis-review-is-due-p note)
- collect note)))
+ (let* ((old-notes (cl-loop for note in (gnosis-select 'id 'review-log '(and (> n 0)
+ (= suspend 0))
+ t)
+ when (gnosis-review-is-due-p note)
+ collect note))
+ (new-notes (cl-loop for note in (gnosis-select 'id 'review-log '(and (= n 0)
+ (= suspend 0))
+ t)
+ when (gnosis-review-is-due-today-p note)
+ collect note)))
+ (if gnosis-review-new-first
+ (append (cl-subseq new-notes 0 gnosis-new-notes-limit) old-notes)
+ (append old-notes (cl-subseq new-notes 0 gnosis-new-notes-limit)))))
(defun gnosis-review-get-due-tags ()
"Return a list of due note tags."
@@ -1396,13 +1473,13 @@ well."
(max (gnosis-algorithm-date-diff last-rev rev-date) 1)))
(defun gnosis-review-algorithm (id success)
- "Return next review date & ef for note with value of id ID.
+ "Return next review date & gnosis 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-increase ef-decrease ef-total))."
- (let ((ff (gnosis-get-note-ff id))
- (ef (gnosis-get 'ef 'review `(= id ,id)))
+ (let ((amnesia (gnosis-get-note-amnesia id))
+ (gnosis (gnosis-get 'gnosis '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
@@ -1410,40 +1487,51 @@ Returns a list of the form ((yyyy mm dd) (ef-increase ef-decrease ef-total))."
;; (review-num (gnosis-get 'n 'review-log `(= id ,id))) ;; total reviews
;; (last-interval (max (gnosis-review--get-offset id) 1))
(last-interval (gnosis-review-last-interval id))) ;; last interval
- (list (gnosis-algorithm-next-interval :last-interval last-interval
- :ef (nth 2 ef) ;; total ef is used for next interval
- :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
- :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))))
+ (list
+ (gnosis-algorithm-next-interval
+ :last-interval last-interval
+ :gnosis-synolon (nth 2 gnosis)
+ :success success
+ :successful-reviews t-success
+ :c-fails c-fails
+ :lethe (gnosis-get-note-lethe id)
+ :amnesia amnesia
+ :proto (gnosis-get-note-proto id))
+ (gnosis-algorithm-next-gnosis
+ :gnosis gnosis
+ :success success
+ :epignosis (gnosis-get-note-epignosis id)
+ :agnoia (gnosis-get-note-agnoia id)
+ :anagnosis (gnosis-get-note-anagnosis id)
+ :c-successes c-success
+ :c-failures c-fails))))
(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 (cadr (gnosis-review-algorithm id success)))
+ (let ((gnosis (cadr (gnosis-review-algorithm id success)))
(next-rev (car (gnosis-review-algorithm id success))))
+ ;; Update activity-log
+ (gnosis-review-increment-activity-log (gnosis-review-is-note-new-p id))
;; Update review-log
(gnosis-update 'review-log `(= last-rev ',(gnosis-algorithm-date)) `(= id ,id))
(gnosis-update 'review-log `(= next-rev ',next-rev) `(= id ,id))
(gnosis-update 'review-log `(= n (+ 1 ,(gnosis-get 'n 'review-log `(= id ,id)))) `(= id ,id))
;; Update review
- (gnosis-update 'review `(= ef ',ef) `(= id ,id))
+ (gnosis-update 'review `(= gnosis ',gnosis) `(= id ,id))
(if success
(progn (gnosis-update 'review-log
- `(= c-success ,(1+ (gnosis-get 'c-success 'review-log `(= id ,id)))) `(= id ,id))
- (gnosis-update 'review-log `(= t-success ,(1+ (gnosis-get 't-success 'review-log `(= id ,id))))
+ `(= c-success ,(1+ (gnosis-get 'c-success 'review-log `(= id ,id))))
+ `(= id ,id))
+ (gnosis-update 'review-log
+ `(= t-success ,(1+ (gnosis-get 't-success 'review-log `(= id ,id))))
`(= id ,id))
(gnosis-update 'review-log `(= c-fails 0) `(= id ,id)))
- (gnosis-update 'review-log `(= c-fails ,(1+ (gnosis-get 'c-fails 'review-log `(= id ,id)))) `(= id ,id))
- (gnosis-update 'review-log `(= t-fails ,(1+ (gnosis-get 't-fails 'review-log `(= id ,id)))) `(= id ,id))
+ (gnosis-update 'review-log
+ `(= c-fails ,(1+ (gnosis-get 'c-fails 'review-log `(= id ,id)))) `(= id ,id))
+ (gnosis-update 'review-log
+ `(= t-fails ,(1+ (gnosis-get 't-fails 'review-log `(= id ,id)))) `(= id ,id))
(gnosis-update 'review-log `(= c-success 0) `(= id ,id)))))
(defun gnosis-review-result (id success)
@@ -1519,11 +1607,13 @@ If user-input is equal to CLOZE, return t."
;; Correct answer -> reveal the current cloze
(progn (cl-incf num)
(gnosis-display-cloze-string main (nthcdr num clozes)
- (nthcdr num hints)
- (cl-subseq clozes 0 num)
- nil))
+ (nthcdr num hints)
+ (cl-subseq clozes 0 num)
+ nil))
;; Incorrect answer
- (gnosis-display-cloze-string main nil nil (cl-subseq clozes 0 num) (member cloze clozes))
+ (gnosis-display-cloze-string main nil nil
+ (cl-subseq clozes 0 num)
+ (member cloze clozes))
(gnosis-display-cloze-user-answer (cdr input))
(setq success nil)
(cl-return)))
@@ -1554,8 +1644,27 @@ If user-input is equal to CLOZE, return t."
(gnosis-display-next-review id success)
success))
+(defun gnosis-review-is-note-new-p (id)
+ "Return t if note with ID is new."
+ (let ((reviews (car (gnosis-select-id 'n 'review-log id))))
+ (not (> reviews 0))))
+
+(defun gnosis-review-increment-activity-log (new? &optional date)
+ "Increament activity log for DATE by one.
+
+If NEW? is non-nil, increment new notes log by 1."
+ (let* ((current-total-value (gnosis-get-date-total-notes))
+ (inc-total (cl-incf current-total-value))
+ (current-new-value (gnosis-get-date-new-notes))
+ (inc-new (cl-incf current-new-value))
+ (date (or date (gnosis-algorithm-date))))
+ (gnosis-update 'activity-log `(= reviewed-total ,inc-total) `(= date ',date))
+ (and new? (gnosis-update 'activity-log `(= reviewed-new ,inc-new) `(= date ',date)))))
+
(defun gnosis-review-note (id)
- "Start review for note with value of id ID, if note is unsuspended."
+ "Start review for note with value of id ID, if note is unsuspended.
+
+DATE: Date to log the note review on the activity-log."
(when (gnosis-suspended-p id)
(message "Suspended note with id: %s" id)
(sit-for 0.3)) ;; this should only occur in testing/dev cases
@@ -1587,14 +1696,14 @@ If user-input is equal to CLOZE, return t."
;; Reopen gnosis-db after pull
(setf gnosis-db (emacsql-sqlite-open (expand-file-name "gnosis.db" dir)))))
-(defun gnosis-review-commit (note-num)
+(defun gnosis-review-commit (note-count)
"Commit review session on git repository.
This function initializes the `gnosis-dir' as a Git repository if it is not
already one. It then adds the gnosis.db file to the repository and commits
the changes with a message containing the reviewed number of notes.
-NOTE-NUM: The number of notes reviewed in the session."
+NOTE-COUNT: The number of notes reviewed in the session to be commited."
(let ((git (executable-find "git"))
(default-directory gnosis-dir))
(unless git
@@ -1605,10 +1714,11 @@ NOTE-NUM: The number of notes reviewed in the session."
(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"
- (shell-quote-argument (format "Total notes for session: %d" note-num)))))
+ (shell-quote-argument
+ (format "Reviewed %d notes." note-count)))))
(when (and gnosis-vc-auto-push (not gnosis-testing))
(gnosis-vc-push))
- (message "Review session finished. %d notes reviewed." note-num)))
+ (message "Review session finished.")))
(defun gnosis-review-action--edit (success note note-count)
"Edit NOTE during review.
@@ -1619,11 +1729,11 @@ editing NOTE with it's new contents.
After done editing, call `gnosis-review-actions' with SUCCESS NOTE
NOTE-COUNT."
(gnosis-edit-save-exit)
- (gnosis-edit-note note t)
+ (gnosis-edit-note note)
(recursive-edit)
(gnosis-review-actions success note note-count))
-(defun gnosis-review-action--quit (success note note-count)
+(defun gnosis-review-action--quit (success note)
"Quit review session.
Update result for NOTE review with SUCCESS and commit session for NOTE-COUNT.
@@ -1631,9 +1741,8 @@ Update result for NOTE review with SUCCESS and commit session for NOTE-COUNT.
This function should be used with `gnosis-review-actions', to finish
the review session."
(gnosis-review-result note success)
- (gnosis-review-commit note-count)
- ;; Break the loop of `gnosis-review-session'
- (throw 'stop-loop t))
+ ;; Break the review loop of `gnosis-review-session'
+ (throw 'review-loop t))
(defun gnosis-review-action--suspend (success note note-count)
"Suspend/Unsuspend NOTE.
@@ -1678,22 +1787,29 @@ To customize the keybindings, adjust `gnosis-review-keybindings'."
("override" (gnosis-review-action--override success note note-count))
("suspend" (gnosis-review-action--suspend success note note-count))
("edit" (gnosis-review-action--edit success note note-count))
- ("quit" (gnosis-review-action--quit success note note-count)))))
+ ("quit" (gnosis-review-action--quit success note)))))
-(defun gnosis-review-session (notes)
+(defun gnosis-review-session (notes &optional due note-count)
"Start review session for NOTES.
-NOTES: List of note ids"
- (let ((note-count 0))
+NOTES: List of note ids
+DUE: If due is non-nil, session will loop for due notes.
+NOTE-COUNT: Total notes to be commited for session."
+ (let ((note-count (or note-count 0)))
(if (null notes)
(message "No notes for review.")
- (when (y-or-n-p (format "You have %s total notes for review, start session?" (length notes)))
- (catch 'stop-loop
- (cl-loop for note in notes
- do (let ((success (gnosis-review-note note)))
- (cl-incf note-count)
- (gnosis-review-actions success note note-count))
- finally (gnosis-review-commit note-count)))))))
+ (setf gnosis-review-notes notes)
+ (catch 'review-loop
+ (cl-loop for note in notes
+ do (let ((success (gnosis-review-note note)))
+ (cl-incf note-count)
+ (gnosis-review-actions success note note-count))
+ finally
+ ;; TODO: Add optional arg to 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)
+ (gnosis-review-commit note-count))))
;;;###autoload
(defun gnosis-review ()
@@ -1701,14 +1817,17 @@ NOTES: List of note ids"
(interactive)
;; Refresh modeline
(setq gnosis-due-notes-total (length (gnosis-review-get-due-notes)))
+ ;; Select review type
(let ((review-type (gnosis-completing-read "Review: " '("Due notes"
"Due notes of deck"
"Due notes of specified tag(s)"
"All notes of tag(s)"))))
(pcase review-type
- ("Due notes" (gnosis-review-session (gnosis-collect-note-ids :due t)))
- ("Due notes of deck" (gnosis-review-session (gnosis-collect-note-ids :due t :deck (gnosis--get-deck-id))))
+ ("Due notes" (gnosis-review-session (gnosis-collect-note-ids :due t) t))
+ ("Due notes of deck" (gnosis-review-session
+ (gnosis-collect-note-ids :due t :deck (gnosis--get-deck-id))))
("Due notes of specified tag(s)" (gnosis-review-session (gnosis-collect-note-ids :due t :tags t)))
+ ("All notes of deck" (gnosis-review-session (gnosis-collect-note-ids :deck (gnosis--get-deck-id))))
("All notes of tag(s)" (gnosis-review-session (gnosis-collect-note-ids :tags t))))))
@@ -1721,7 +1840,7 @@ NOTES: List of note ids"
(put-text-property (match-beginning 0) (match-end 0) 'read-only t)))
(goto-char (point-min)))
-(cl-defun gnosis-edit-note (id &optional (recursive-edit nil) (dashboard "notes"))
+(cl-defun gnosis-edit-note (id)
"Edit the contents of a note with the given ID.
This function creates an Emacs Lisp buffer named *gnosis-edit* on the
@@ -1736,9 +1855,6 @@ RECURSIVE-EDIT: If t, exit `recursive-edit' after finishing editing.
It should only be t when starting a recursive edit, when editing a
note during a review session.
-DASHBOARD: Dashboard to return after editing. Default value is
-Notes.
-
The buffer automatically indents the expressions for readability.
After finishing editing, evaluate the entire expression to apply the
changes."
@@ -1754,29 +1870,8 @@ 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")
- (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 dashboard
- gnosis-dashboard-note-ids)))))
-
-(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))))
+ ":gnosis" ":amensia" ":suspend")
+ (local-set-key (kbd "C-c C-c") (lambda () (interactive) (gnosis-edit-note-save-exit))))
(defun gnosis-assert-int-or-nil (value description)
"Assert that VALUE is an integer or nil.
@@ -1803,60 +1898,23 @@ 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)
+(cl-defun gnosis-edit-save-exit ()
"Save edits and exit using EXIT-FUNC, with ARGS."
(interactive)
(when (get-buffer "*gnosis-edit*")
(switch-to-buffer "*gnosis-edit*")
(eval-buffer)
(quit-window t)
- (when exit-func
- (apply exit-func args))))
+ (gnosis-dashboard-return)))
+
+(cl-defun gnosis-edit-note-save-exit ()
+ "Save edits and exit using EXIT-FUNC, with ARGS."
+ (interactive)
+ (when (get-buffer "*gnosis-edit*")
+ (switch-to-buffer "*gnosis-edit*")
+ (eval-buffer)
+ (quit-window t)
+ (exit-recursive-edit)))
(defvar-keymap gnosis-edit-mode-map
:doc "gnosis-edit keymap"
@@ -1868,8 +1926,8 @@ INITIAL-INTERVAL: Initial interval for notes of deck"
:lighter " Gnosis Edit"
:keymap gnosis-edit-mode-map)
-(cl-defun gnosis-edit-update-note (&key id main options answer tags (extra-notes nil) (image nil) (second-image nil)
- ef ff suspend)
+(cl-defun gnosis-edit-update-note (&key id main options answer tags (extra-notes nil) (image nil)
+ (second-image nil) gnosis amnesia suspend)
"Update note with id value of ID.
ID: Note id
@@ -1880,8 +1938,8 @@ 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
-EF: Easiness factor value
-FF: Failure factor value
+GNOSIS: Gnosis score
+AMNESIA: Amnesia value
SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
(cl-assert (stringp main) nil "Main must be a string")
(cl-assert (or (stringp image) (null image)) nil
@@ -1891,14 +1949,15 @@ SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
(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 (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")
(when (and (string= (gnosis-get-type id) "cloze")
(not (stringp options)))
(cl-assert (or (listp options) (stringp options)) nil "Options must be a list or a string.")
(cl-assert (gnosis-cloze-check main answer) nil "Clozes are not part of the question (main).")
- (cl-assert (>= (length answer) (length options)) nil "Hints (options) must be equal or less than clozes (answer).")
+ (cl-assert (>= (length answer) (length options)) nil
+ "Hints (options) must be equal or less than clozes (answer).")
(cl-assert (cl-every (lambda (item) (or (null item) (stringp item))) options) nil "Hints (options) must be either nil or a string."))
;; Construct the update clause for the emacsql update statement.
(cl-loop for (field . value) in `((main . ,main)
@@ -1908,13 +1967,13 @@ SUSPEND: Suspend note, 0 for unsuspend, 1 for suspend"
(extra-notes . ,extra-notes)
(images . ,image)
(extra-image . ,second-image)
- (ef . ',ef)
- (ff . ,ff)
+ (gnosis . ',gnosis)
+ (amnesia . ,amnesia)
(suspend . ,suspend))
when value
do (cond ((memq field '(extra-notes images extra-image))
(gnosis-update 'extras `(= ,field ,value) `(= id ,id)))
- ((memq field '(ef ff))
+ ((memq field '(gnosis amnesia))
(gnosis-update 'review `(= ,field ,value) `(= id ,id)))
((eq field 'suspend)
(gnosis-update 'review-log `(= ,field ,value) `(= id ,id)))
@@ -1922,30 +1981,224 @@ 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-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-custom-values--validate (plist valid-keywords)
+ "Verify that PLIST consists of VALID-KEYWORDS."
+ (let ((keys (let (ks)
+ (while plist
+ (setq ks (cons (car plist) ks))
+ (setq plist (cddr plist)))
+ ks)))
+ (let ((invalid-key (cl-find-if (lambda (key) (not (member key valid-keywords))) keys)))
+ (if invalid-key
+ (error "Invalid custom keyword found in: %s" invalid-key)
+ t))))
+
+(defun gnosis-get-custom-values (key search-value &optional values)
+ "Return SEARCH-VALUE for KEY from VALUES.
+
+VALUES: Defaults to `gnosis-custom-values'."
+ (cl-assert (or (eq key :deck) (eq key :tag)) nil "Key value must be either :tag or :deck")
+ (cl-assert (stringp search-value) nil "Search-value must be the name of tag or deck as a string.")
+ (let ((results)
+ (values (or values gnosis-custom-values)))
+ (dolist (rule values)
+ (when (and (plist-get rule key)
+ (equal (plist-get rule key) search-value))
+ (setq results (append results (nth 2 rule)))))
+ (gnosis-get-custom-values--validate results gnosis-custom--valid-values)
+ results))
+
+(defun gnosis-get-custom-deck-value (deck value &optional values)
+ "Return custom VALUE for note DECK."
+ (plist-get (gnosis-get-custom-values :deck deck values) value))
+
+(defun gnosis-get-custom-tag-values (id keyword &optional custom-tags custom-values)
+ "Return KEYWORD values for note ID."
+ (cl-assert (keywordp keyword) nil "keyword must be a keyword!")
+ (let ((tags (if id (gnosis-get 'tags 'notes `(= id ,id)) custom-tags)))
+ (cl-loop for tag in tags
+ ;; Only collect non-nil values
+ when (plist-get (gnosis-get-custom-values :tag tag custom-values) keyword)
+ collect (plist-get (gnosis-get-custom-values :tag tag custom-values) keyword))))
+
+(defun gnosis-get-note-tag-amnesia (id &optional custom-tags custom-values)
+ "Return tag MINIMUM amnesia for note ID.
+
+The closer the amnesia value is to 0, the closer it is to total
+amnesia i.e next interval to be 0.
+
+CUSTOM-TAGS: Specify tags for note id.
+CUSTOM-VALUES: Specify values for tags."
+ (let ((amnesia-values (gnosis-get-custom-tag-values id :amnesia custom-tags custom-values)))
+ (and amnesia-values (apply #'max amnesia-values))))
+
+(defun gnosis-get-note-deck-amnesia (id &optional custom-deck custom-values)
+ "Return tag amnesia for note ID.
+
+Optionally, use CUSTOM-DECK and CUSTOM-VALUES."
+ (let ((deck (or (gnosis-get-note-deck-name id) custom-deck )))
+ (or (gnosis-get-custom-deck-value deck :amnesia custom-values)
+ gnosis-algorithm-amnesia-value)))
+
+(defun gnosis-get-note-amnesia (id &optional custom-deck custom-tags custom-values )
+ "Return amnesia value for note ID.
+
+Note amnesia should be hte MINIMUM value of deck's & tags' amnesia.
+
+CUSTOM-TAGS: Specify tags for note id.
+CUSTOM-VALUES: Specify values for tags."
+ (let* ((deck-amnesia (gnosis-get-note-deck-amnesia id custom-deck custom-values))
+ (tags-amnesia (gnosis-get-note-tag-amnesia id custom-tags custom-values))
+ (note-amnesia (or tags-amnesia deck-amnesia)))
+ (if (>= note-amnesia 1)
+ (error "Amnesia value must be lower than 1")
+ note-amnesia)))
+
+(defun gnosis-get-note-tag-epignosis (id &optional custom-tags custom-values)
+ "Return tag epignosis for note ID.
+
+CUSTOM-TAGS: Specify tags for note id.
+CUSTOM-VALUES: Specify values for tags."
+ (let* ((epignosis-values (gnosis-get-custom-tag-values id :epignosis custom-tags custom-values)))
+ (and epignosis-values (apply #'max epignosis-values))))
+
+(defun gnosis-get-note-deck-epignosis (id &optional custom-deck custom-values)
+ "Return deck epignosis for note ID."
+ (let ((deck (or (gnosis-get-note-deck-name id) custom-deck)))
+ (or (gnosis-get-custom-deck-value deck :epignosis custom-values)
+ gnosis-algorithm-epignosis-value)))
+
+(defun gnosis-get-note-epignosis (id &optional custom-deck custom-tags custom-values)
+ "Return epignosis value for note ID."
+ (let* ((deck-epignosis (gnosis-get-note-deck-epignosis id custom-deck custom-values))
+ (tag-epignosis (gnosis-get-note-tag-epignosis id custom-tags custom-values))
+ (note-epignosis (or tag-epignosis deck-epignosis)))
+ (if (>= note-epignosis 1)
+ (error "Epignosis value must be lower than 1")
+ note-epignosis)))
+
+(defun gnosis-get-note-tag-agnoia (id &optional custom-tags custom-values)
+ "Return agnoia value for note ID."
+ (let ((agnoia-values (gnosis-get-custom-tag-values id :agnoia custom-tags custom-values)))
+ (and agnoia-values (apply #'max agnoia-values))))
+
+(defun gnosis-get-note-deck-agnoia (id &optional custom-deck custom-values)
+ "Return agnoia value for note ID."
+ (let ((deck (or (gnosis-get-note-deck-name id) custom-deck)))
+ (or (gnosis-get-custom-deck-value deck :agnoia custom-values)
+ gnosis-algorithm-agnoia-value)))
+
+(defun gnosis-get-note-agnoia (id &optional custom-deck custom-tags custom-values)
+ "Return agnoia value for note ID."
+ (let* ((deck-agnoia (gnosis-get-note-deck-agnoia id custom-deck custom-values))
+ (tag-agnoia (gnosis-get-note-tag-agnoia id custom-tags custom-values))
+ (note-agnoia (or tag-agnoia deck-agnoia)))
+ (if (>= note-agnoia 1)
+ (error "Agnoia value must be lower than 1")
+ note-agnoia)))
+
+(defun gnosis-proto-max-values (proto-values)
+ "Return max values from PROTO-VALUES."
+ (if (not (and (listp proto-values) (cl-every #'listp proto-values)))
+ proto-values
+ (let* ((max-len (apply #'max (mapcar #'length proto-values)))
+ (padded-lists (mapcar (lambda (lst)
+ (append lst (make-list (- max-len (length lst)) 0)))
+ proto-values)))
+ (apply #'cl-mapcar #'max padded-lists))))
+
+(defun gnosis-get-note-proto (id &optional custom-tags custom-deck custom-values)
+ "Return tag proto values for note ID.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-TAGS: Custom tags to be used instead.
+CUSTOM-DECK: Custom deck to be used instead."
+ (let* ((deck (or custom-deck (gnosis-get-note-deck-name id)))
+ (tags-proto (gnosis-get-custom-tag-values id :proto custom-tags custom-values))
+ (decks-proto (gnosis-get-custom-deck-value deck :proto custom-values)))
+ (if tags-proto (gnosis-proto-max-values tags-proto) (gnosis-proto-max-values (or decks-proto gnosis-algorithm-proto)))))
+
+(defun gnosis-get-note-tag-anagnosis (id &optional custom-tags custom-values)
+ "Return the minimum anagnosis tag value for note ID.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-TAGS: Custom tags to be used instead."
+ (let ((anagnosis-values (gnosis-get-custom-tag-values id :anagnosis custom-tags custom-values)))
+ (and anagnosis-values (apply #'min anagnosis-values))))
+
+(defun gnosis-get-note-deck-anagnosis (id &optional custom-deck custom-values)
+ "Return anagnosis deck value for note ID.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-DECK: Custom deck to be used instead."
+ (let ((deck (or (gnosis-get-note-deck-name id) custom-deck)))
+ (or (gnosis-get-custom-deck-value deck :anagnosis custom-values)
+ gnosis-algorithm-anagnosis-value)))
+
+(defun gnosis-get-note-anagnosis (id &optional custom-deck custom-tags custom-values)
+ "Return minimum anagnosis value for note ID.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-TAGS: Custom tags to be used instead.
+CUSTOM-DECK: Custom deck to be used instead."
+ (let* ((deck-anagnosis (gnosis-get-note-deck-anagnosis id custom-deck custom-values))
+ (tag-anagnosis (gnosis-get-note-tag-anagnosis id custom-tags custom-values))
+ (note-anagnosis (or tag-anagnosis deck-anagnosis)))
+ note-anagnosis))
+
+(defun gnosis-get-note-deck-lethe (id &optional custom-deck custom-values)
+ "Return lethe deck value for note ID.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-DECK: Custom deck to be used instead."
+ (let ((deck (or (gnosis-get-note-deck-name id) custom-deck)))
+ (or (gnosis-get-custom-deck-value deck :lethe custom-values)
+ gnosis-algorithm-lethe-value)))
+
+(defun gnosis-get-note-tag-lethe (id &optional custom-tags custom-values)
+ "Return note ID tag lethe values.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-TAGS: Custom tags to be used instead."
+ (let ((lethe-values (gnosis-get-custom-tag-values id :lethe custom-tags custom-values)))
+ (and lethe-values (apply #'min lethe-values))))
+
+(defun gnosis-get-note-lethe (id &optional custom-deck custom-tags custom-values)
+ "Return note ID lethe value.
+
+CUSTOM-VALUES: Custom values to be used instead.
+CUSTOM-TAGS: Custom tags to be used instead.
+CUSTOM-DECK: Custom deck to be used instead."
+ (let* ((deck-lethe (gnosis-get-note-deck-lethe id custom-deck custom-values))
+ (tag-lethe (gnosis-get-note-tag-lethe id custom-tags custom-values))
+ (note-lethe (or tag-lethe deck-lethe)))
+ note-lethe))
+
+(defun gnosis-get-date-total-notes (&optional date)
+ "Return total notes reviewed for DATE.
+
+If entry for DATE does not exist, it will be created.
+
+Defaults to current date."
+ (cl-assert (listp date) nil "Date must be a list.")
+ (let* ((date (or date (gnosis-algorithm-date)))
+ (reviewed-total (car (gnosis-select 'reviewed-total 'activity-log `(= date ',date) t)))
+ (reviewed-new (or (car (gnosis-select 'reviewed-new 'activity-log `(= date ',date) t)) 0)))
+ (or reviewed-total
+ (progn
+ ;; Using reviewed-new instead of hardcoding 0 just to not mess up tests.
+ (and (equal date (gnosis-algorithm-date))
+ (gnosis--insert-into 'activity-log `([,date 0 ,reviewed-new])))
+ 0))))
-(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-date-new-notes (&optional date)
+ "Return total notes reviewed for DATE.
-(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)))
+Defaults to current date."
+ (cl-assert (listp date) nil "Date must be a list.")
+ (let* ((date (or date (gnosis-algorithm-date)))
+ (reviewed-new (or (car (gnosis-select 'reviewed-new 'activity-log `(= date ',date) t)) 0)))
+ reviewed-new))
(cl-defun gnosis-export-note (id &optional (export-for-deck nil))
"Export fields for note with value of id ID.
@@ -1975,9 +2228,10 @@ The final exported note is indented using the `indent-region' function
to improve readability."
(let ((values (append (gnosis-select '[id main options answer tags] 'notes `(= id ,id) t)
(gnosis-select '[extra-notes images extra-image] 'extras `(= id ,id) t)
- (gnosis-select '[ef ff] 'review `(= id ,id) t)
+ (gnosis-select '[gnosis amnesia] 'review `(= id ,id) t)
(gnosis-select 'suspend 'review-log `(= id ,id) t)))
- (fields '(:id :main :options :answer :tags :extra-notes :image :second-image :ef :ff :suspend)))
+ (fields (list :id :main :options :answer :tags
+ :extra-notes :image :second-image :gnosis :amnesia :suspend)))
(when export-for-deck
(setf values (append (gnosis-select 'type 'notes `(= id ,id) t)
(butlast (cdr values) 3)))
@@ -1991,12 +2245,7 @@ to improve readability."
;;; Database Schemas
(defvar gnosis-db-schema-decks '([(id integer :primary-key :autoincrement)
- (name text :not-null)
- (failure-factor float)
- (ef-increase float)
- (ef-decrease float)
- (ef-threshold integer)
- (initial-interval listp)]))
+ (name text :not-null)]))
(defvar gnosis-db-schema-notes '([(id integer :primary-key :autoincrement)
(type text :not-null)
@@ -2009,33 +2258,29 @@ to improve readability."
:on-delete :cascade)))
(defvar gnosis-db-schema-review '([(id integer :primary-key :not-null) ;; note-id
- (ef integer :not-null) ;; Easiness factor
- (ff integer :not-null) ;; Forgetting factor
- (interval integer :not-null)] ;; Initial Interval
+ (gnosis integer :not-null)
+ (amnesia integer :not-null)]
(:foreign-key [id] :references notes [id]
:on-delete :cascade)))
(defvar gnosis-db-schema-review-log '([(id integer :primary-key :not-null) ;; note-id
(last-rev integer :not-null) ;; Last review date
(next-rev integer :not-null) ;; Next review date
- (c-success integer :not-null) ;; number of consecutive successful reviews
- (t-success integer :not-null) ;; Number of total successful reviews
- (c-fails integer :not-null) ;; Number of consecutive failed reviewss
- (t-fails integer :not-null) ;; Number of total failed reviews
+ (c-success integer :not-null) ;; Consecutive successful reviews
+ (t-success integer :not-null) ;; Total successful reviews
+ (c-fails integer :not-null) ;; Consecutive failed reviewss
+ (t-fails integer :not-null) ;; Total failed reviews
(suspend integer :not-null) ;; Binary value, 1=suspended
(n integer :not-null)] ;; Number of reviews
(:foreign-key [id] :references notes [id]
:on-delete :cascade)))
+(defvar gnosis-db-schema-activity-log '([(date text :not-null)
+ (reviewed-total integer :not-null)
+ (reviewed-new integer :not-null)]))
+
(defvar gnosis-db-schema-extras '([(id integer :primary-key :not-null)
(extra-notes string)
- ;; Despite the name 'images', this
- ;; is a single string value. At
- ;; first it was designed to hold a
- ;; list of strings for image paths,
- ;; but it was changed to just a
- ;; string to hold a single image
- ;; path.
(images string)
;; Extra image path to show after review
(extra-image string)]
@@ -2052,217 +2297,69 @@ Return note ids for notes that match QUERY."
(cl-assert (or (stringp query) (eq query nil)))
(let* ((query (or query (read-string "Search for note: ")))
(words (split-string query))
- (clause `(and ,@(mapcar (lambda (word)
- `(like main ,(format "%%%s%%" word)))
- words))))
- (gnosis-select 'id 'notes clause t)))
-
-;; Dashboard
-
-(defun gnosis-dashboard-output-note (id)
- "Output contents for note with ID, formatted for gnosis dashboard."
- (cl-loop for item in (append (gnosis-select '[main options answer tags type] 'notes `(= id ,id) t)
- (gnosis-select 'suspend 'review-log `(= id ,id) t))
- if (listp item)
- collect (mapconcat #'identity item ",")
- else
- collect (replace-regexp-in-string "\n" " " (format "%s" item))))
-
-;; TODO: Rewrite. 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.
-
-TAGS: boolean value, t to specify tags.
-DUE: boolean value, t to specify due notes.
-DECK: Integer, specify deck id.
-QUERY: String value,"
- (cl-assert (and (booleanp due) (booleanp tags) (or (numberp deck) (null deck)) (or (stringp query) (null query)))
- nil "Incorrect value passed to `gnosis-collect-note-ids'")
- (cond ((and (null tags) (null due) (null deck) (null query))
- (gnosis-select 'id 'notes '1=1 t))
- ;; All due notes
- ((and (null tags) due (null deck))
- (gnosis-review-get-due-notes))
- ;; All notes for tags
- ((and tags (null due) (null deck))
- (gnosis-select-by-tag (gnosis-tag-prompt)))
- ;; All due notes for tags
- ((and tags due (null deck))
- (gnosis-select-by-tag (gnosis-tag-prompt) t))
- ;; All notes for deck
- ((and (null tags) (null due) deck)
- (gnosis-get-deck-notes deck nil))
- ;; All due notes for deck
- ((and (null tags) deck due)
- (gnosis-get-deck-notes deck t))
- ;; Query
- ((and (null tags) (null due) (null deck) query)
- (gnosis-search-note query))))
-
-(defun gnosis-dashboard-output-notes (note-ids)
- "Return NOTE-IDS contents on gnosis dashboard."
- (cl-assert (listp note-ids) t "`note-ids' must be a list of note ids.")
- (pop-to-buffer "*gnosis-dashboard*")
- (gnosis-dashboard-mode)
- (setf tabulated-list-format `[("Main" ,(/ (window-width) 4) t)
- ("Options" ,(/ (window-width) 6) t)
- ("Answer" ,(/ (window-width) 6) t)
- ("Tags" ,(/ (window-width) 5) t)
- ("Type" ,(/ (window-width) 10) T)
- ("Suspend" ,(/ (window-width) 6) t)]
- tabulated-list-entries (cl-loop for id in note-ids
- for output = (gnosis-dashboard-output-note id)
- when output
- collect (list (number-to-string id) (vconcat output)))
- gnosis-dashboard-note-ids note-ids)
- (tabulated-list-init-header)
- ;; 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 gnosis-dashboard-note-ids)
- (revert-buffer t t t)))
- (local-set-key (kbd "a") #'gnosis-add-note)
- (local-set-key (kbd "r") #'gnosis-dashboard)
- (local-set-key (kbd "d") #'(lambda () (interactive)
- (gnosis-delete-note (string-to-number (tabulated-list-get-id)))
- (gnosis-dashboard-output-notes gnosis-dashboard-note-ids)
- (revert-buffer t t t)))
- (local-unset-key (kbd "RET")))
-
-(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 (format "%s" item)))
-
-(defun gnosis-dashboard-output-decks ()
- "Return deck contents for gnosis dashboard."
- (pop-to-buffer "*gnosis-dashboard*")
- (gnosis-dashboard-mode)
- (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)
- (setq tabulated-list-entries
- (cl-loop for id in (gnosis-select 'id 'decks '1=1 t)
- 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 () "Add deck & refresh" (interactive)
- (gnosis-add-deck (read-string "Deck name: "))
- (gnosis-dashboard-output-decks)
- (revert-buffer t t t)))
- (local-set-key (kbd "s") #'(lambda () "Suspend notes" (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 () "Delete deck" (interactive)
- (gnosis-delete-deck (string-to-number (tabulated-list-get-id)))
- (gnosis-dashboard-output-decks)
- (revert-buffer t t t)))
- (local-set-key (kbd "RET") #'(lambda () "View notes of deck" (interactive)
- (gnosis-dashboard "notes"
- (gnosis-collect-note-ids
- :deck (string-to-number (tabulated-list-get-id)))))))
-
-(defun gnosis-dashboard-edit-note (&optional dashboard)
- "Get note id from tabulated list and edit it.
-
-DASHBOARD: Dashboard to return to after editing."
- (interactive)
- (let ((id (tabulated-list-get-id))
- (dashboard (or dashboard "notes")))
- (gnosis-edit-note (string-to-number id) nil dashboard)
- (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"
- "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
- (setq tabulated-list-padding 2
- tabulated-list-sort-key nil))
-
-;;;###autoload
-(cl-defun gnosis-dashboard (&optional dashboard-type (note-ids nil))
- "Display gnosis dashboard.
-
-NOTE-IDS: List of note ids to display on dashboard. When nil, prompt
-for dashboard type.
-
-DASHBOARD-TYPE: either 'Notes' or 'Decks' to display the respective dashboard."
- (interactive)
- (let ((dashboard-type (or dashboard-type
- (cadr (read-multiple-choice
- "Display dashboard for:"
- '((?n "notes")
- (?d "decks")
- (?t "tags")
- (?s "search")))))))
- (if note-ids (gnosis-dashboard-output-notes note-ids)
- (pcase dashboard-type
- ("notes" (gnosis-dashboard-output-notes (gnosis-collect-note-ids)))
- ("decks" (gnosis-dashboard-output-decks))
- ("tags" (gnosis-dashboard-output-notes (gnosis-collect-note-ids :tags t)))
- ("search" (gnosis-dashboard-output-notes
- (gnosis-collect-note-ids :query (read-string "Search for note: "))))))
- (tabulated-list-print t)))
+ (clause-main `(and ,@(mapcar (lambda (word)
+ `(like main ,(format "%%%s%%" word)))
+ words)))
+ (clause-answer `(and ,@(mapcar (lambda (word)
+ `(like answer ,(format "%%%s%%" word)))
+ words))))
+ (append (gnosis-select 'id 'notes clause-main t)
+ (gnosis-select 'id 'notes clause-answer t))))
+
+(defun gnosis-db-update-v2 ()
+ "Update to first gnosis-db version."
+ (emacsql-with-transaction gnosis-db
+ (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 = 2"))
+ (gnosis--create-table 'activity-log gnosis-db-schema-activity-log)
+ ;; Update to most recent gnosis db version.
+ (gnosis-db-update-v3)))
+
+(defun gnosis-db-update-v3 ()
+ "Upgrade database to version 3."
+ (emacsql-with-transaction gnosis-db
+ (emacsql gnosis-db [:alter-table decks :drop-column failure-factor])
+ (emacsql gnosis-db [:alter-table decks :drop-column ef-increase])
+ (emacsql gnosis-db [:alter-table decks :drop-column ef-threshold])
+ (emacsql gnosis-db [:alter-table decks :drop-column ef-decrease])
+ (emacsql gnosis-db [:alter-table decks :drop-column initial-interval])
+ ;; Review changes
+ (emacsql gnosis-db [:alter-table review :rename ef :to gnosis])
+ (emacsql gnosis-db [:alter-table review :rename ff :to amnesia])
+ (emacsql gnosis-db [:alter-table review :drop-column interval])
+ ;; Add activity log
+ (gnosis--create-table 'activity-log gnosis-db-schema-activity-log)
+ ;; Update version
+ (emacsql gnosis-db (format "PRAGMA user_version = %s" gnosis-db-version))))
(defun gnosis-db-init ()
- "Create gnosis essential directories & database."
+ "Create essential directories & database."
(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))
+ (unless (length> (emacsql gnosis-db [:select name :from sqlite-master :where (= type table)]) 3)
+ (emacsql-with-transaction gnosis-db
+ ;; 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)
+ ;; Create activity-log table
+ (gnosis--create-table 'activity-log gnosis-db-schema-activity-log)))
;; 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))))))
+ (cond ((<= gnosis-curr-version 2)
+ (gnosis-db-update-v3)))))
(gnosis-db-init)
@@ -2280,12 +2377,12 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
(goto-char (point-min))
(animate-string string vpos hpos)
(and face
- (if string-section
- (progn
- (goto-char (point-min))
- (while (search-forward string-section nil t)
- (add-text-properties (match-beginning 0) (match-end 0) `(face ,face))))
- (add-text-properties (line-beginning-position) (line-end-position) `(face ,face))))))
+ (if string-section
+ (progn
+ (goto-char (point-min))
+ (while (search-forward string-section nil t)
+ (add-text-properties (match-beginning 0) (match-end 0) `(face ,face))))
+ (add-text-properties (line-beginning-position) (line-end-position) `(face ,face))))))
;;;###autoload
(defun gnosis-demo ()
@@ -2323,33 +2420,33 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
(if (not (cl-some #'(lambda (x) (member "demo" x)) (gnosis-select 'name 'decks)))
(progn (gnosis-add-deck deck-name)
(gnosis-add-note--basic :deck deck-name
- :question "Repetitio est mater memoriae"
- :hint "Translate this Latin phrase to English."
- :answer "Repetition is the mother of memory"
- :extra "/Regular review/ at increasing intervals *reinforces* *memory* *retention*. Strengthening neural connections & making it easier to recall information long-term"
+ :question "Repetitio est mater memoriae"
+ :hint "Translate this Latin phrase to English."
+ :answer "Repetition is the mother of memory"
+ :extra "/Regular review/ at increasing intervals *reinforces* *memory* *retention*. Strengthening neural connections & making it easier to recall information long-term"
+ :tags note-tags)
+ (gnosis-add-note--mc-cloze :deck deck-name
+ :question "Consistency is _key_ to using gnosis effectively."
+ :options '("Consistency" "Procrastination" "Incosistency")
+ :answer "Consistency"
+ :extra "Avoid monotony, try to engage with the material actively, and stay _consistent_!"
+ :tags note-tags)
+ (gnosis-add-note--mcq :deck deck-name
+ :question "Which one is the capital of Greece?"
+ :choices '("Athens" "Sparta" "Rome" "Berlin")
+ :correct-answer 1
+ :extra "Athens (Αθήνα) is the largest city of Greece & one of the world's oldest cities, with it's recorded history spanning over 3,500 years."
:tags note-tags)
- (gnosis-add-note--mc-cloze :deck deck-name
- :question "Consistency is _key_ to using gnosis effectively."
- :options '("Consistency" "Procrastination" "Incosistency")
- :answer "Consistency"
- :extra "Avoid monotony, try to engage with the material actively, and stay _consistent_!"
- :tags note-tags)
- (gnosis-add-note--mcq :deck deck-name
- :question "Which one is the capital of Greece?"
- :choices '("Athens" "Sparta" "Rome" "Berlin")
- :correct-answer 1
- :extra "Athens (Αθήνα) is the largest city of Greece & one of the world's oldest cities, with it's recorded history spanning over 3,500 years."
- :tags note-tags)
- (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*")
- (gnosis-add-note--y-or-n :deck deck-name
- :question "Is GNU Emacs the greatest program ever written?"
- :hint "Duh"
- :answer 121
- :extra ""
- :tags note-tags))
+ (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*")
+ (gnosis-add-note--y-or-n :deck deck-name
+ :question "Is GNU Emacs the greatest program ever written?"
+ :hint "Duh"
+ :answer 121
+ :extra ""
+ :tags note-tags))
(error "Demo deck already exists"))))
;; Gnosis mode ;;
@@ -2361,18 +2458,18 @@ If STRING-SECTION is nil, apply FACE to the entire STRING."
:global t
:group 'gnosis
:lighter nil
- (setq gnosis-due-notes-total (length (gnosis-review-get-due-notes)))
- (if gnosis-modeline-mode
- (progn
- (add-to-list 'global-mode-string '(:eval
- (format " G:%d" gnosis-due-notes-total)))
- (force-mode-line-update))
- (setq global-mode-string
- (seq-remove (lambda (item)
- (and (listp item) (eq (car item) :eval)
- (string-prefix-p " G:" (format "%s" (eval (cadr item))))))
- global-mode-string))
- (force-mode-line-update)))
+ (setq gnosis-due-notes-total (length (gnosis-review-get-due-notes)))
+ (if gnosis-modeline-mode
+ (progn
+ (add-to-list 'global-mode-string '(:eval
+ (format " G:%d" gnosis-due-notes-total)))
+ (force-mode-line-update))
+ (setq global-mode-string
+ (seq-remove (lambda (item)
+ (and (listp item) (eq (car item) :eval)
+ (string-prefix-p " G:" (format "%s" (eval (cadr item))))))
+ global-mode-string))
+ (force-mode-line-update)))
(define-derived-mode gnosis-mode special-mode "Gnosis"
"Gnosis Mode."