From b345aa1f7edfe1b6f6315dbd41f23a84d72bb4aa Mon Sep 17 00:00:00 2001 From: Leon Rische Date: Sun, 7 Mar 2021 15:28:35 +0100 Subject: Extract review functions --- org-fc-review.el | 511 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ org-fc.el | 513 +------------------------------------------------------ 2 files changed, 513 insertions(+), 511 deletions(-) create mode 100644 org-fc-review.el diff --git a/org-fc-review.el b/org-fc-review.el new file mode 100644 index 0000000..3937ef2 --- /dev/null +++ b/org-fc-review.el @@ -0,0 +1,511 @@ +;;; org-fc-review.el --- Review mode for org-fc -*- lexical-binding: t; -*- + +;; Copyright (C) 2020 Leon Rische + +;; Author: Leon Rische +;; Url: https://www.leonrische.me/pages/org_flashcards.html +;; Package-requires: ((emacs "26.3") (org "9.3")) +;; Version: 0.0.1 + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (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 . + +;;; Commentary: +;; +;; During review, due cards are presented one after another and the +;; user is asked to rate each card. +;; +;; Cards are reviewed by +;; 1. opening the file they are in +;; 2. calling the setup function for the card type +;; 3. switch to review-flip-mode +;; 4. calling the flip function for the card type +;; 5. switch to review-rate-mode +;; 6. updating the review data based on the rating +;; +;;; Code: + +;;;###autoload +(defun org-fc-review (context) + "Start a review session for all cards in CONTEXT. +Called interactively, prompt for the context. +Valid contexts: +- 'all, all cards in `org-fc-directories' +- 'buffer, all cards in the current buffer +- a list of paths" + (interactive (list (org-fc-select-context))) + (if org-fc-review--session + (message "Flashcards are already being reviewed") + (let* ((index (org-fc-index context)) + (cards (org-fc-index-filter-due index))) + (if org-fc-shuffle-positions + (setq cards (org-fc-index-shuffled-positions cards)) + (setq cards (org-fc-index-positions cards))) + (if (null cards) + (message "No cards due right now") + (progn + (setq org-fc-review--session + (org-fc-make-review-session cards)) + (run-hooks 'org-fc-before-review-hook) + (org-fc-review-next-card)))))) + +(defun org-fc-review-resume () + "Resume review session, if it was paused." + (interactive) + (if org-fc-review--session + (progn + (org-fc-review-edit-mode -1) + (org-fc-review-next-card 'resuming)) + (message "No session to resume to"))) + +;;;###autoload +(defun org-fc-review-buffer () + "Review due cards in the current buffer." + (interactive) + (org-fc-review org-fc-context-buffer)) + +;;;###autoload +(defun org-fc-review-all () + "Review all due cards." + (interactive) + (org-fc-review org-fc-context-all)) + +(defun org-fc-review-next-card (&optional resuming) + "Review the next card of the current session. +If RESUMING is non-nil, some parts of the buffer setup are skipped." + (if (not (null (oref org-fc-review--session cards))) + (condition-case err + (let* ((card (pop (oref org-fc-review--session cards))) + (path (plist-get card :path)) + (id (plist-get card :id)) + (type (plist-get card :type)) + (position (plist-get card :position))) + (setf (oref session current-item) card) + (let ((buffer (find-buffer-visiting path))) + (with-current-buffer (find-file path) + (unless resuming + ;; If buffer was already open, don't kill it after rating the card + (if buffer + (setq-local org-fc-reviewing-existing-buffer t) + (setq-local org-fc-reviewing-existing-buffer nil)) + (org-fc-set-header-line)) + + (goto-char (point-min)) + (org-fc-id-goto id path) + + (org-fc-indent) + ;; Make sure the headline the card is in is expanded + (org-reveal) + (org-fc-narrow) + (org-fc-hide-keyword-times) + (org-fc-hide-drawers) + (org-fc-show-latex) + (org-display-inline-images) + (run-hooks 'org-fc-before-setup-hook) + + (setq org-fc-timestamp (time-to-seconds (current-time))) + (let ((step (funcall (org-fc-type-setup-fn type) position))) + (run-hooks 'org-fc-after-setup-hook) + + ;; If the card has a no-noop flip function, + ;; skip to rate-mode + (let ((flip-fn (org-fc-type-flip-fn type))) + (if (or + (eq step 'rate) + (null flip-fn) + (eq flip-fn #'org-fc-noop)) + (org-fc-review-rate-mode 1) + (org-fc-review-flip-mode 1))))))) + (error + (org-fc-review-quit) + (signal (car err) (cdr err)))) + (message "Review Done") + (org-fc-review-quit))) + +(defmacro org-fc-review-with-current-item (var &rest body) + "Evaluate BODY with the current card bound to VAR. +Before evaluating BODY, check if the heading at point has the +same ID as the current card in the session." + (declare (indent defun)) + `(if org-fc-review--session + (if-let ((,var (oref org-fc-review--session current-item))) + (if (string= (plist-get ,var :id) (org-id-get)) + (progn ,@body) + (message "Flashcard ID mismatch")) + (message "No flashcard review is in progress")))) + +(defun org-fc-review-flip () + "Flip the current flashcard." + (interactive) + (condition-case err + (org-fc-review-with-current-item card + (let ((type (plist-get card :type))) + (funcall (org-fc-type-flip-fn type)) + (run-hooks 'org-fc-after-flip-hook) + (org-fc-review-rate-mode))) + (error + (org-fc-review-quit) + (signal (car err) (cdr err))))) + +(defun org-fc-review-rate (rating) + "Rate the card at point with RATING." + (interactive) + (condition-case err + (org-fc-review-with-current-item card + (let* ((path (plist-get card :path)) + (id (plist-get card :id)) + (position (plist-get card :position)) + (now (time-to-seconds (current-time))) + (delta (- now org-fc-timestamp))) + (org-fc-review-add-rating org-fc-review--session rating) + (org-fc-review-update-data path id position rating delta) + (org-fc-review-reset) + + (if (and (eq rating 'again) org-fc-append-failed-cards) + (with-slots (cards) org-fc-review--session + (setf cards (append cards (list card))))) + + (save-buffer) + (if org-fc-reviewing-existing-buffer + (org-fc-review-reset) + (kill-buffer)) + (org-fc-review-next-card))) + (error + (org-fc-review-quit) + (signal (car err) (cdr err))))) + +(defun org-fc-review-rate-again () + "Rate the card at point with 'again'." + (interactive) + (org-fc-review-rate 'again)) + +(defun org-fc-review-rate-hard () + "Rate the card at point with 'hard'." + (interactive) + (org-fc-review-rate 'hard)) + +(defun org-fc-review-rate-good () + "Rate the card at point with 'good'." + (interactive) + (org-fc-review-rate 'good)) + +(defun org-fc-review-rate-easy () + "Rate the card at point with 'easy'." + (interactive) + (org-fc-review-rate 'easy)) + +(defun org-fc-review-skip-card () + "Skip card and proceed to next." + (interactive) + (org-fc-review-reset) + (org-fc-review-next-card)) + +(defun org-fc-review-suspend-card () + "Suspend card and proceed to next." + (interactive) + (org-fc-suspend-card) + ;; Remove all other positions from review session + (with-slots (current-item cards) org-fc-review--session + (let ((id (plist-get current-item :id))) + (setf cards + (cl-remove-if + (lambda (card) + (string= id (plist-get card :id))) cards)))) + (org-fc-review-reset) + (org-fc-review-next-card)) + +(defun org-fc-review-update-data (path id position rating delta) + "Update the review data of the card. +Also add a new entry in the review history file. PATH, ID, +POSITION identify the position that was reviewed, RATING is a +review rating and DELTA the time in seconds between showing and +rating the card." + (org-fc-with-point-at-entry + ;; If the card is marked as a demo card, don't log its reviews and + ;; don't update its review data + (unless (member org-fc-demo-tag (org-get-tags)) + (let* ((data (org-fc-review-data-get)) + (current (assoc position data #'string=))) + (unless current + (error "No review data found for this position")) + (let ((ease (string-to-number (cl-second current))) + (box (string-to-number (cl-third current))) + (interval (string-to-number (cl-fourth current)))) + (org-fc-review-history-add + (list + (org-fc-timestamp-now) + path + id + position + (format "%.2f" ease) + (format "%d" box) + (format "%.2f" interval) + (symbol-name rating) + (format "%.2f" delta) + (symbol-name org-fc-algorithm))) + (cl-destructuring-bind (next-ease next-box next-interval) + (org-fc-sm2-next-parameters ease box interval rating) + (setcdr + current + (list (format "%.2f" next-ease) + (number-to-string next-box) + (format "%.2f" next-interval) + (org-fc-timestamp-in next-interval))) + (org-fc-review-data-set data))))))) + +(defun org-fc-review-reset () + "Reset the buffer to its state before the review." + (org-fc-review-rate-mode -1) + (org-fc-review-flip-mode -1) + (org-fc-review-edit-mode -1) + (org-fc-reset-header-line) + (org-fc-remove-overlays) + (widen)) + +;;;###autoload +(defun org-fc-review-quit () + "Quit the review, remove all overlays from the buffer." + (interactive) + (org-fc-review-reset) + (run-hooks 'org-fc-after-review-hook) + (org-fc-review-history-save) + (setq org-fc-review--session nil)) + +;;;###autoload +(defun org-fc-review-edit () + "Edit current flashcard. +Pauses the review, unnarrows the buffer and activates +`org-fc-edit-mode'." + (interactive) + (widen) + (org-fc-remove-overlays) + ;; Queue the current flashcard so it's reviewed a second time + (push + (oref org-fc-review--session current-item) + (oref org-fc-review--session cards)) + (setf (oref org-fc-review--session paused) t) + (setf (oref org-fc-review--session current-item) nil) + (org-fc-review-edit-mode 1)) + +;;; Managing Review Data + +;; Based on `org-log-beginning' +(defun org-fc-review-data-position (&optional create) + "Return (BEGINNING . END) points of the review data drawer. +When optional argument CREATE is non-nil, the function creates a +drawer, if necessary. Returned position ignores narrowing. + +BEGINNING is the start of the first line inside the drawer, +END is the start of the line with :END: on it." + (org-with-wide-buffer + (org-end-of-meta-data) + (let ((regexp (concat "^[ \t]*:" (regexp-quote org-fc-review-data-drawer) ":[ \t]*$")) + (end (if (org-at-heading-p) (point) + (save-excursion (outline-next-heading) (point)))) + (case-fold-search t)) + (catch 'exit + ;; Try to find existing drawer. + (while (re-search-forward regexp end t) + (let ((element (org-element-at-point))) + (when (eq (org-element-type element) 'drawer) + (throw 'exit + (cons (org-element-property :contents-begin element) + (org-element-property :contents-end element)))))) + ;; No drawer found. Create one, if permitted. + (when create + (unless (bolp) (insert "\n")) + (let ((beg (point))) + (insert ":" org-fc-review-data-drawer ":\n:END:\n") + (org-indent-region beg (point))) + (cons + (line-beginning-position 0) + (line-beginning-position 0))))))) + +(defun org-fc-review-data-get () + "Get a cards review data as a Lisp object." + (if-let ((position (org-fc-review-data-position))) + (org-with-point-at (car position) + (cddr (org-table-to-lisp))))) + +(defun org-fc-review-data-set (data) + "Set the cards review data to DATA." + (save-excursion + (let ((position (org-fc-review-data-position 'create))) + (kill-region (car position) (cdr position)) + (goto-char (car position)) + (insert "| position | ease | box | interval | due |\n") + (insert "|-|-|-|-|-|\n") + (dolist (datum data) + (insert + "| " + (mapconcat (lambda (x) (format "%s" x)) datum " | ") + " |\n")) + (org-table-align)))) + +(defun org-fc-review-data-default (position) + "Default review data for position POSITION." + (cl-case org-fc-algorithm + ('sm2-v1 (org-fc-algo-sm2-initial-review-data position)) + ('sm2-v2 (org-fc-algo-sm2-initial-review-data position)))) + +(defun org-fc-review-data-update (positions) + "Update review data to POSITIONS. +If a doesn't exist already, it is initialized with default +values. Entries in the table not contained in POSITIONS are +removed." + (let ((old-data (org-fc-review-data-get))) + (org-fc-review-data-set + (mapcar + (lambda (pos) + (or + (assoc pos old-data #'string=) + (org-fc-review-data-default pos))) + positions)))) + +;;; Sessions + +(defclass org-fc-review-session () + ((current-item :initform nil) + (paused :initform nil :initarg :paused) + (history :initform nil) + (ratings :initform nil :initarg :ratings) + (cards :initform nil :initarg :cards))) + +(defun org-fc-make-review-session (cards) + "Create a new review session with CARDS." + (make-instance + 'org-fc-review-session + :ratings + (if-let ((stats (org-fc-awk-stats-reviews))) + (plist-get stats :day) + '(:total 0 :again 0 :hard 0 :good 0 :easy 0)) + :cards cards)) + +(defun org-fc-review-history-add (elements) + "Add ELEMENTS to review history." + (push + elements + (slot-value org-fc-review--session 'history))) + +(defun org-fc-review-history-save () + "Save all history entries in the current session." + (when (and org-fc-review--session (oref org-fc-review--session history)) + (append-to-file + (concat + (mapconcat + (lambda (elements) (mapconcat #'identity elements "\t")) + (reverse (oref org-fc-review--session history)) + "\n") + "\n") + nil + org-fc-review-history-file) + (setf (oref org-fc-review--session history) nil))) + +;; Make sure the history is saved even if Emacs is killed +(add-hook 'kill-emacs-hook #'org-fc-review-history-save) + +(defun org-fc-review-add-rating (session rating) + "Store RATING in the review history of SESSION." + (with-slots (ratings) session + (cl-case rating + ('again (cl-incf (cl-getf ratings :again) 1)) + ('hard (cl-incf (cl-getf ratings :hard) 1)) + ('good (cl-incf (cl-getf ratings :good) 1)) + ('easy (cl-incf (cl-getf ratings :easy) 1))) + (cl-incf (cl-getf ratings :total 1)))) + +(defvar org-fc-review--session nil + "Current review session.") + +;;; Modes + +(defvar org-fc-review-flip-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "RET") 'org-fc-review-flip) + (define-key map (kbd "q") 'org-fc-review-quit) + (define-key map (kbd "p") 'org-fc-review-edit) + (define-key map (kbd "s") 'org-fc-review-suspend-card) + map) + "Keymap for `org-fc-flip-mode'.") + +(define-minor-mode org-fc-review-flip-mode + "Minor mode for flipping flashcards. + +\\{org-fc-review-flip-mode-map}" + :init-value nil + :lighter " fc-flip" + :keymap org-fc-review-flip-mode-map + :group 'org-fc + (when org-fc-review-flip-mode + ;; Make sure only one of the modes is active at a time + (org-fc-review-rate-mode -1) + ;; Make sure we're in org mode and there is an active review session + (unless (and (derived-mode-p 'org-mode) org-fc-review--session) + (org-fc-review-flip-mode -1)))) + +;;;;; Rate Mode + +(defvar org-fc-review-rate-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "a") 'org-fc-review-rate-again) + (define-key map (kbd "h") 'org-fc-review-rate-hard) + (define-key map (kbd "g") 'org-fc-review-rate-good) + (define-key map (kbd "e") 'org-fc-review-rate-easy) + (define-key map (kbd "s") 'org-fc-review-suspend-card) + (define-key map (kbd "p") 'org-fc-review-edit) + (define-key map (kbd "q") 'org-fc-review-quit) + map) + "Keymap for `org-fc-rate-mode'.") + +(define-minor-mode org-fc-review-rate-mode + "Minor mode for rating flashcards. + +\\{org-fc-review-rate-mode-map}" + :init-value nil + :lighter " fc-rate" + :keymap org-fc-review-rate-mode-map + :group 'org-fc + (when org-fc-review-rate-mode + ;; Make sure only one of the modes is active at a time + (org-fc-review-flip-mode -1) + ;; Make sure we're in org mode and there is an active review session + (unless (and (derived-mode-p 'org-mode) org-fc-review--session) + (org-fc-review-rate-mode -1)))) + +(defvar org-fc-review-edit-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "C-c C-c") 'org-fc-review-resume) + (define-key map (kbd "C-c C-k") 'org-fc-review-quit) + map) + "Keymap for `org-fc-edit-mode'.") + +(define-minor-mode org-fc-review-edit-mode + "Minor mode for editing flashcards. + +\\{org-fc-review-edit-mode-map}" + :init-value nil + :lighter " fc-edit" + :keymap org-fc-review-edit-mode-map + :group 'org-fc + (when org-fc-review-edit-mode + (org-fc-review-flip-mode -1) + (org-fc-review-rate-mode -1) + ;; Make sure we're in org mode and there is an active review session + (unless (and (derived-mode-p 'org-mode) org-fc-review--session) + (org-fc-review-edit-mode -1)))) + +;;; Footer + +(provide 'org-fc-review) + +;;; org-fc-review.el ends here diff --git a/org-fc.el b/org-fc.el index 49f123c..7c99049 100644 --- a/org-fc.el +++ b/org-fc.el @@ -743,170 +743,7 @@ Positions are shuffled in a way that preserves the order of the (with-current-buffer (find-file path) (org-fc-review-buffer)))) -;;;; Session Management - -(defclass org-fc-review-session () - ((current-item :initform nil) - (paused :initform nil :initarg :paused) - (history :initform nil) - (ratings :initform nil :initarg :ratings) - (cards :initform nil :initarg :cards))) - -(defun org-fc-make-review-session (cards) - "Create a new review session with CARDS." - (make-instance - 'org-fc-review-session - :ratings - (if-let ((stats (org-fc-awk-stats-reviews))) - (plist-get stats :day) - '(:total 0 :again 0 :hard 0 :good 0 :easy 0)) - :cards cards)) - -(defun org-fc-review-history-add (elements) - "Add ELEMENTS to review history." - (push - elements - (slot-value org-fc--session 'history))) - -(defun org-fc-review-history-save () - "Save all history entries in the current session." - (when (and org-fc--session (oref org-fc--session history)) - (append-to-file - (concat - (mapconcat - (lambda (elements) (mapconcat #'identity elements "\t")) - (reverse (oref org-fc--session history)) - "\n") - "\n") - nil - org-fc-review-history-file) - (setf (oref org-fc--session history) nil))) - -;; Make sure the history is saved even if Emacs is killed -(add-hook 'kill-emacs-hook #'org-fc-review-history-save) - -(defun org-fc-session-cards-pending-p (session) - "Check if there are any cards in SESSION." - (not (null (oref session cards)))) - -(defun org-fc-session-pop-next-card (session) - "Remove and return one card from SESSION." - (let ((card (pop (oref session cards)))) - (setf (oref session current-item) card) - card)) - -(defun org-fc-session-append-card (session card) - "Append CARD to the cards of SESSION." - (with-slots (cards) session - (setf cards (append cards (list card))))) - -(defun org-fc-session-prepend-card (session card) - "Prepend CARD to the cards of SESSION." - (with-slots (cards) session - (setf cards (cons card cards)))) - -(defun org-fc-session-add-rating (session rating) - "Store RATING in the review history of SESSION." - (with-slots (ratings) session - (cl-case rating - ('again (cl-incf (cl-getf ratings :again) 1)) - ('hard (cl-incf (cl-getf ratings :hard) 1)) - ('good (cl-incf (cl-getf ratings :good) 1)) - ('easy (cl-incf (cl-getf ratings :easy) 1))) - (cl-incf (cl-getf ratings :total 1)))) - -(defun org-fc-session-stats-string (session) - "Generate a string with review stats for SESSION." - (with-slots (ratings) session - (let ((total (plist-get ratings :total))) - (if (cl-plusp total) - (format "%.2f again, %.2f hard, %.2f good, %.2f easy" - (/ (* 100.0 (plist-get ratings :again)) total) - (/ (* 100.0 (plist-get ratings :hard)) total) - (/ (* 100.0 (plist-get ratings :good)) total) - (/ (* 100.0 (plist-get ratings :easy)) total)) - "No ratings yet")))) - -(defvar org-fc--session nil - "Current review session.") - -;;;; Reading / Writing Review Data - -;; Based on `org-log-beginning' -(defun org-fc-review-data-position (&optional create) - "Return (BEGINNING . END) points of the review data drawer. -When optional argument CREATE is non-nil, the function creates a -drawer, if necessary. Returned position ignores narrowing. - -BEGINNING is the start of the first line inside the drawer, -END is the start of the line with :END: on it." - (org-with-wide-buffer - (org-end-of-meta-data) - (let ((regexp (concat "^[ \t]*:" (regexp-quote org-fc-review-data-drawer) ":[ \t]*$")) - (end (if (org-at-heading-p) (point) - (save-excursion (outline-next-heading) (point)))) - (case-fold-search t)) - (catch 'exit - ;; Try to find existing drawer. - (while (re-search-forward regexp end t) - (let ((element (org-element-at-point))) - (when (eq (org-element-type element) 'drawer) - (throw 'exit - (cons (org-element-property :contents-begin element) - (org-element-property :contents-end element)))))) - ;; No drawer found. Create one, if permitted. - (when create - (unless (bolp) (insert "\n")) - (let ((beg (point))) - (insert ":" org-fc-review-data-drawer ":\n:END:\n") - (org-indent-region beg (point))) - (cons - (line-beginning-position 0) - (line-beginning-position 0))))))) - -(defun org-fc-get-review-data () - "Get a cards review data as a Lisp object." - (if-let ((position (org-fc-review-data-position))) - (org-with-point-at (car position) - (cddr (org-table-to-lisp))))) - -(defun org-fc-set-review-data (data) - "Set the cards review data to DATA." - (save-excursion - (let ((position (org-fc-review-data-position 'create))) - (kill-region (car position) (cdr position)) - (goto-char (car position)) - (insert "| position | ease | box | interval | due |\n") - (insert "|-|-|-|-|-|\n") - (dolist (datum data) - (insert - "| " - (mapconcat (lambda (x) (format "%s" x)) datum " | ") - " |\n")) - (org-table-align)))) - -(defun org-fc-review-data-default (position) - "Default review data for position POSITION." - (cl-case org-fc-algorithm - ('sm2-v1 (org-fc-algo-sm2-initial-review-data position)) - ('sm2-v2 (org-fc-algo-sm2-initial-review-data position)))) - -(defun org-fc-review-data-update (positions) - "Update review data to POSITIONS. -If a doesn't exist already, it is initialized with default -values. Entries in the table not contained in POSITIONS are -removed." - (let ((old-data (org-fc-get-review-data))) - (org-fc-set-review-data - (mapcar - (lambda (pos) - (or - (assoc pos old-data #'string=) - (org-fc-review-data-default pos))) - positions)))) - -;;;; Review Modes -;;;;; Header Line +;;; Header Line (defun org-fc-set-header-line () "Set the header-line for review." @@ -928,93 +765,7 @@ removed." "Reset the header-line to its original value." (setq-local header-line-format org-fc-original-header-line-format)) -;;;;; Flip Mode - -(defvar org-fc-review-flip-mode-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "RET") 'org-fc-review-flip) - (define-key map (kbd "q") 'org-fc-review-quit) - (define-key map (kbd "p") 'org-fc-review-edit) - (define-key map (kbd "s") 'org-fc-review-suspend-card) - map) - "Keymap for `org-fc-flip-mode'.") - -(define-minor-mode org-fc-review-flip-mode - "Minor mode for flipping flashcards. - -\\{org-fc-review-flip-mode-map}" - :init-value nil - :lighter " fc-flip" - :keymap org-fc-review-flip-mode-map - :group 'org-fc - (when org-fc-review-flip-mode - ;; Make sure only one of the modes is active at a time - (org-fc-review-rate-mode -1) - ;; Make sure we're in org mode and there is an active review session - (unless (and (derived-mode-p 'org-mode) org-fc--session) - (org-fc-review-flip-mode -1)))) - -;;;;; Rate Mode - -(defvar org-fc-review-rate-mode-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "a") 'org-fc-review-rate-again) - (define-key map (kbd "h") 'org-fc-review-rate-hard) - (define-key map (kbd "g") 'org-fc-review-rate-good) - (define-key map (kbd "e") 'org-fc-review-rate-easy) - (define-key map (kbd "s") 'org-fc-review-suspend-card) - (define-key map (kbd "p") 'org-fc-review-edit) - (define-key map (kbd "q") 'org-fc-review-quit) - map) - "Keymap for `org-fc-rate-mode'.") - -(define-minor-mode org-fc-review-rate-mode - "Minor mode for rating flashcards. - -\\{org-fc-review-rate-mode-map}" - :init-value nil - :lighter " fc-rate" - :keymap org-fc-review-rate-mode-map - :group 'org-fc - (when org-fc-review-rate-mode - ;; Make sure only one of the modes is active at a time - (org-fc-review-flip-mode -1) - ;; Make sure we're in org mode and there is an active review session - (unless (and (derived-mode-p 'org-mode) org-fc--session) - (org-fc-review-rate-mode -1)))) - -(defvar org-fc-review-edit-mode-map - (let ((map (make-sparse-keymap))) - (define-key map (kbd "C-c C-c") 'org-fc-review-resume) - (define-key map (kbd "C-c C-k") 'org-fc-review-quit) - map) - "Keymap for `org-fc-edit-mode'.") - -(define-minor-mode org-fc-review-edit-mode - "Minor mode for editing flashcards. - -\\{org-fc-review-edit-mode-map}" - :init-value nil - :lighter " fc-edit" - :keymap org-fc-review-edit-mode-map - :group 'org-fc - (when org-fc-review-edit-mode - (org-fc-review-flip-mode -1) - (org-fc-review-rate-mode -1) - ;; Make sure we're in org mode and there is an active review session - (unless (and (derived-mode-p 'org-mode) org-fc--session) - (org-fc-review-edit-mode -1)))) - -;;;; Main Loop -;; -;; Cards are reviewed by -;; 1. opening the file they are in -;; 2. calling the setup function for the card type -;; 3. switch to review-flip-mode -;; 4. calling the flip function for the card type -;; 5. switch to review-rate-mode -;; 6. updating the review data based on the rating -;; +;;; Contexts (defvar org-fc-custom-contexts '() "User-defined review contexts.") @@ -1041,266 +792,6 @@ removed." (unless (string= context "") (alist-get (intern context) (org-fc-contexts))))) -;;;###autoload -(defun org-fc-review (context) - "Start a review session for all cards in CONTEXT. -Called interactively, prompt for the context. -Valid contexts: -- 'all, all cards in `org-fc-directories' -- 'buffer, all cards in the current buffer -- a list of paths" - (interactive (list (org-fc-select-context))) - (if org-fc--session - (message "Flashcards are already being reviewed") - (let* ((index (org-fc-index context)) - (cards (org-fc-index-filter-due index))) - (if org-fc-shuffle-positions - (setq cards (org-fc-index-shuffled-positions cards)) - (setq cards (org-fc-index-positions cards))) - (if (null cards) - (message "No cards due right now") - (progn - (setq org-fc--session - (org-fc-make-review-session cards)) - (run-hooks 'org-fc-before-review-hook) - (org-fc-review-next-card)))))) - -(defun org-fc-review-resume () - "Resume review session, if it was paused." - (interactive) - (if org-fc--session - (progn - (org-fc-review-edit-mode -1) - (org-fc-review-next-card 'resuming)) - (message "No session to resume to"))) - -;;;###autoload -(defun org-fc-review-buffer () - "Review due cards in the current buffer." - (interactive) - (org-fc-review org-fc-context-buffer)) - -;;;###autoload -(defun org-fc-review-all () - "Review all due cards." - (interactive) - (org-fc-review org-fc-context-all)) - -(defun org-fc-review-next-card (&optional resuming) - "Review the next card of the current session. -If RESUMING is non-nil, some parts of the buffer setup are skipped." - (if (org-fc-session-cards-pending-p org-fc--session) - (condition-case err - (let* ((card (org-fc-session-pop-next-card org-fc--session)) - (path (plist-get card :path)) - (id (plist-get card :id)) - (type (plist-get card :type)) - (position (plist-get card :position))) - (let ((buffer (find-buffer-visiting path))) - (with-current-buffer (find-file path) - (unless resuming - ;; If buffer was already open, don't kill it after rating the card - (if buffer - (setq-local org-fc-reviewing-existing-buffer t) - (setq-local org-fc-reviewing-existing-buffer nil)) - (org-fc-set-header-line)) - - (goto-char (point-min)) - (org-fc-id-goto id path) - - (org-fc-indent) - ;; Make sure the headline the card is in is expanded - (org-reveal) - (org-fc-narrow) - (org-fc-hide-keyword-times) - (org-fc-hide-drawers) - (org-fc-show-latex) - (org-display-inline-images) - (run-hooks 'org-fc-before-setup-hook) - - (setq org-fc-timestamp (time-to-seconds (current-time))) - (let ((step (funcall (org-fc-type-setup-fn type) position))) - (run-hooks 'org-fc-after-setup-hook) - - ;; If the card has a no-noop flip function, - ;; skip to rate-mode - (let ((flip-fn (org-fc-type-flip-fn type))) - (if (or - (eq step 'rate) - (null flip-fn) - (eq flip-fn #'org-fc-noop)) - (org-fc-review-rate-mode 1) - (org-fc-review-flip-mode 1))))))) - (error - (org-fc-review-quit) - (signal (car err) (cdr err)))) - (message "Review Done") - (org-fc-review-quit))) - -(defmacro org-fc-review-with-current-item (var &rest body) - "Evaluate BODY with the current card bound to VAR. -Before evaluating BODY, check if the heading at point has the -same ID as the current card in the session." - (declare (indent defun)) - `(if org-fc--session - (if-let ((,var (oref org-fc--session current-item))) - (if (string= (plist-get ,var :id) (org-id-get)) - (progn ,@body) - (message "Flashcard ID mismatch")) - (message "No flashcard review is in progress")))) - -(defun org-fc-review-flip () - "Flip the current flashcard." - (interactive) - (condition-case err - (org-fc-review-with-current-item card - (let ((type (plist-get card :type))) - (funcall (org-fc-type-flip-fn type)) - (run-hooks 'org-fc-after-flip-hook) - (org-fc-review-rate-mode))) - (error - (org-fc-review-quit) - (signal (car err) (cdr err))))) - -(defun org-fc-review-rate (rating) - "Rate the card at point with RATING." - (interactive) - (condition-case err - (org-fc-review-with-current-item card - (let* ((path (plist-get card :path)) - (id (plist-get card :id)) - (position (plist-get card :position)) - (now (time-to-seconds (current-time))) - (delta (- now org-fc-timestamp))) - (org-fc-session-add-rating org-fc--session rating) - (org-fc-review-update-data path id position rating delta) - (org-fc-review-reset) - - (if (and (eq rating 'again) org-fc-append-failed-cards) - (org-fc-session-append-card org-fc--session card)) - - (save-buffer) - (if org-fc-reviewing-existing-buffer - (org-fc-review-reset) - (kill-buffer)) - (org-fc-review-next-card))) - (error - (org-fc-review-quit) - (signal (car err) (cdr err))))) - -(defun org-fc-review-rate-again () - "Rate the card at point with 'again'." - (interactive) - (org-fc-review-rate 'again)) - -(defun org-fc-review-rate-hard () - "Rate the card at point with 'hard'." - (interactive) - (org-fc-review-rate 'hard)) - -(defun org-fc-review-rate-good () - "Rate the card at point with 'good'." - (interactive) - (org-fc-review-rate 'good)) - -(defun org-fc-review-rate-easy () - "Rate the card at point with 'easy'." - (interactive) - (org-fc-review-rate 'easy)) - -(defun org-fc-review-skip-card () - "Skip card and proceed to next." - (interactive) - (org-fc-review-reset) - (org-fc-review-next-card)) - -(defun org-fc-review-suspend-card () - "Suspend card and proceed to next." - (interactive) - (org-fc-suspend-card) - ;; Remove all other positions from review session - (with-slots (current-item cards) org-fc--session - (let ((id (plist-get current-item :id))) - (setf cards - (cl-remove-if - (lambda (card) - (string= id (plist-get card :id))) cards)))) - (org-fc-review-reset) - (org-fc-review-next-card)) - -(defun org-fc-review-update-data (path id position rating delta) - "Update the review data of the card. -Also add a new entry in the review history file. PATH, ID, -POSITION identify the position that was reviewed, RATING is a -review rating and DELTA the time in seconds between showing and -rating the card." - (org-fc-with-point-at-entry - ;; If the card is marked as a demo card, don't log its reviews and - ;; don't update its review data - (unless (member org-fc-demo-tag (org-get-tags)) - (let* ((data (org-fc-get-review-data)) - (current (assoc position data #'string=))) - (unless current - (error "No review data found for this position")) - (let ((ease (string-to-number (cl-second current))) - (box (string-to-number (cl-third current))) - (interval (string-to-number (cl-fourth current)))) - (org-fc-review-history-add - (list - (org-fc-timestamp-now) - path - id - position - (format "%.2f" ease) - (format "%d" box) - (format "%.2f" interval) - (symbol-name rating) - (format "%.2f" delta) - (symbol-name org-fc-algorithm))) - (cl-destructuring-bind (next-ease next-box next-interval) - (org-fc-sm2-next-parameters ease box interval rating) - (setcdr - current - (list (format "%.2f" next-ease) - (number-to-string next-box) - (format "%.2f" next-interval) - (org-fc-timestamp-in next-interval))) - (org-fc-set-review-data data))))))) - -(defun org-fc-review-reset () - "Reset the buffer to its state before the review." - (org-fc-review-rate-mode -1) - (org-fc-review-flip-mode -1) - (org-fc-review-edit-mode -1) - (org-fc-reset-header-line) - (org-fc-remove-overlays) - (widen)) - -;;;###autoload -(defun org-fc-review-quit () - "Quit the review, remove all overlays from the buffer." - (interactive) - (org-fc-review-reset) - (run-hooks 'org-fc-after-review-hook) - (org-fc-review-history-save) - (setq org-fc--session nil)) - -;;;###autoload -(defun org-fc-review-edit () - "Edit current flashcard. -Pauses the review, unnarrows the buffer and activates -`org-fc-edit-mode'." - (interactive) - (widen) - (org-fc-remove-overlays) - ;; Queue the current flashcard so it's reviewed a second time - (org-fc-session-prepend-card - org-fc--session - (oref org-fc--session current-item)) - (setf (oref org-fc--session paused) t) - (setf (oref org-fc--session current-item) nil) - (org-fc-review-edit-mode 1)) - ;;; Dashboard (require 'org-fc-dashboard) -- cgit v1.2.3