simfish:

Emacs/Learn

LitDoc workflow for learning and reviewing information using Emacs and Anki

cover image


Requirements


Introduction

This workflow is useful for working with Anki, a FSRS based flash card management & review system.

Dependencies

(specifications->manifest
 '( "anki" ))

Customization

Anki base directory,

(defcustom learn:anki-dir
 (expand-file-name "${HOME}/Documents/KB/anki")
 "Anki base directory.
 The directory is storage location for Anki add-ons, database, and user preferences.")

Anki add-ons locations,

(defcustom learn:anki-addons '(
  (:name "anki-connect"
   :url  "https://codeberg.org/distroconfig/learn"
   :dir "anki-addons/anki-connect" )
   
   (:name "review-heatmap"
    :url  "https://codeberg.org/distroconfig/learn"
    :dir  "anki-addons/review-heatmap"))
    
 "List of Anki add-ons to be auto installed")

Anki card template Snippet. Use <ac key-binding to expand the template

# -*- mode: snippet -*-
# name:anki-card  
# key:<ac 
# --
${1:*} Q#${2:Number}
:PROPERTIES:
:ANKI_DECK: Default
:ANKI_NOTE_TYPE: Basic
:END:
${3:**} Front
${4:Question}
${5:**} Back
${6:Answer}

Key Bindings

Anki is used for reviewing notes. To launch Anki from within Emacs use,

(kb::def :states  'normal
  "a"    '(:ignore t                     :which-key "anki")
  "aa"   '(learn::anki-open              :which-key "start"))

To manage Anki cards using Anki Editor use,

(kb::def :states  'normal
  :keymaps 'org-mode-map
  "a"    '(:ignore t                     :which-key "anki")
  "aa"   '(learn::anki-open              :which-key "start")
  "ai"   '(learn::anki-insert            :which-key "insert card")
  "ap"   '(learn::anki-push              :which-key "push notes to Anki DB"))

Runtime Environment

Anki is run using GUIX shell configured with the following options,

#--container
#--network
#--writeable-root
#--emulate-fhs
--load-path="${LITDOC_CACHEDIR}/learn/share/scheme/site-lisp"
#--share=${ANKI_BASE}=/opt/anki/data

Functions

Open Anki

(defun learn::anki-open ()
   "Start Anki Program"
   (interactive)
   (let* ((anki-base-dir         (format "ANKI_BASE=%s" (substitute-in-file-name  learn:anki-dir)))
          (anki-sh               (substitute-in-file-name "${LITDOC_CACHEDIR}/learn/bin/anki.sh")) 
          (process-environment   (cons anki-base-dir process-environment)))
     (start-process-shell-command "Anki" "**Anki**" anki-sh)))

Insert Anki Note,

(defun learn::anki-insert ()
  "Insert Anki note"
  (interactive)
  (require 'anki-editor)
  (anki-editor-insert-note))

Pushing Anki notes,

(defun learn::anki-push ()
    "Push anki cards"
    (interactive)
    (require 'anki-editor)
    (anki-editor-push-notes '(4)))

Integrate Anki with org agenda. See notes-anki-api for details. Use agenda view hook to add a TODO state which notifies the user tasks available for a review.

(defun learn::anki-query-cards ()
  "Add Anki Due Reviews into Anki Card"
  (require 'request)
  (let* ((anki-base-dir         (format "ANKI_BASE=%s" (substitute-in-file-name  learn:anki-dir)))
         (anki-py-script        (substitute-in-file-name
                                 "${LITDOC_CACHEDIR}/learn/bin/run-anki-server.sh"))
         (process-environment   (cons  anki-base-dir process-environment))
         (proc                  (start-process-shell-command
                                 "Anki API"
                                 "**Anki API**"
                                 anki-py-script)))
          (end-of-buffer)
          (newline)
          (insert (propertize " Review Tasks" 'face 'org-agenda-structure))
          (newline)
          (insert "  Reading Anki database ...")
          (set-process-sentinel proc (lambda (p signal)
              (request "http://localhost:8383/anki/due/default"
              :parser 'json-read
              :success
               (cl-function (lambda (&key data &allow-other-keys)
                 (let ((anki-task-count 0)
                       (buffer-read-only       nil)
                       (org-link-elisp-confirm-function  t)
                       (org-link-descriptive   t))
                       (when (get-buffer "*Org Agenda*")
                             (switch-to-buffer "*Org Agenda*")
                             (end-of-buffer)
                             (beginning-of-line)
                             (delete-region (point) (point-max))
                            ;; New
                            (setq  anki-task-count    (+ (string-to-number (assoc-default 'new data))
                                                         (string-to-number (assoc-default 'learn data))
                                                         (string-to-number (assoc-default 'review data))))
                            (if (> anki-task-count 0)
                               (progn
                                 (newline)
                                 (insert (format "   Anki:\tThere are %s flash cards awating your attention\t"  anki-task-count))
                                 (let ((start (point)))
                                    (insert (format "   [[elisp:learn::anki-open][Open]]"))
                                    (goto-char start)
                                    (org-activate-links (point-max))))
                               (insert "\n   Anki:\tYou have no flash cards due for a review !!!")
                               ))))))))))
;; Hook it so your temporary note appears every time agenda is refreshed
(add-hook 'org-agenda-finalize-hook 'learn::anki-query-cards)

Managing Anki add-ons,

(defun anki::root-dir ()
    "Anki root directory"
    (substitute-in-file-name learn:anki-dir))

(defun anki::addons-dir ()
    "Locate Anki addons21 directory."
    (let* ((root-dir   (anki::root-dir))
           (addons-dir (expand-file-name "addons21" root-dir)))
      (unless (file-directory-p addons-dir)
              (warn "Could not find addons21 directory: %s" addons-dir))
    addons-dir))

(defun anki::install-addon (addon url dir)
    "Downloads Anki addon-on ADDON from a directory DIR hosted in a git repo URL"
    (let* ((addons-dir (anki::addons-dir))
           (addon-dir  (expand-file-name addon addons-dir))
           (script     (substitute-in-file-name "${LITDOC_CACHEDIR}/learn/bin/anki-addon.sh"))
           (cmd        (format "%s %s %s %s %s" script addon url dir addon-dir)))
           (start-process-shell-command
              "anki:addon-installer"
              "**Anki**"
              cmd)))
         
(defun anki::install-addons (&optional update)
  "Install Anki add-ons listed in `learn:anki-addons` customization variabl"
  (interactive)
  (let* ((addons      (if (and (not (null learn:anki-addons))
                               (listp learn:anki-addons))  
                           learn:anki-addons
                           '()))
         (addons-dir  (anki::addons-dir)))
  (cl-loop for addon-info in addons  do
    (let* ((addon-name             (plist-get addon-info :name))
           (addon-url              (plist-get addon-info :url))
           (addon-dir              (plist-get addon-info :dir))
           (addon-install-dir      (expand-file-name addon-name addons-dir))
           (addon-install-dir-old  (expand-file-name (format "%s.old" addon-name)
                                    addons-dir)))
    (cond 
       (;; addon does not xist?
        (not (file-directory-p addon-install-dir))
        (anki::download-addon addon-name addon-url addon-install-dir)
        (log::info "[LEARN]: addon <%s> is ready" addon-name))
      (;; update existing addon directory?
        (and (file-directory-p addon-install-dir)
             (not (null update)))
        (rename-file addon-install-dir  addon-install-dir-old)
        (anki::install-addon adodn-name addon-url addon-dir)
        (delete-directory addon-dir-old)
        (log::info "[LEARN]: addon <%s> is up-to-date" addon-name))
      (;; default do nothing
        (log::info "[LEARN]: addon <%s> is ready" addon-name)
        t))))))

Shell Scripts

Utility script for launching Anki,

#!/usr/bin/env bash
GUIX_PROFILE="${LITDOC_CACHEDIR}/learn/guix/learn/learn"
source "${GUIX_PROFILE}/etc/profile"
unset GUIX_PROFILE
exec anki

Utility script for downloading and installing Anki add-ons directly downloaded from source repository

#!/usr/bin/env sh
addon_name="${1}"
repo_url="${2}"
dir="${3}"
addon_dir="${4}"
checkout_dir="$(mktemp --dry-run)"

echo "ADDON: ${addon_name}"
echo "URL: ${repo_url}"
echo "DIR: ${dir}"
echo "DEST_DIR: ${addon_dir}"
echo "CHCKOUT_DIR: ${checkout_dir}"

if [ -d "${addon_dir}" ]; then
   echo "ERROR: Anki add-on(${addon_name}) already exists @: ${addon_dir}"
   exit 1
fi

cd /tmp
git clone -q "${repo_url}" "${checkout_dir}"
cd "${checkout_dir}"
mv ${dir} ${addon_dir}
cd /tmp
rm -rf ${checkout_dir}

Packages

Package: anki-editor

Anki editor is Emacs package for creating Anki cards from within Emacs.

 (use-package anki-editor
  :guix (:name emacs-anki-editor)
  :demand t
  :after org
  :config
  (setq anki-editor-create-decks t)
  (setq anki-editor-org-tags-as-anki-tags t))

Package: anki

There are no GNU/Guix packages for easily integrating Anki with this notes workflow. An ad-hoc solution is provided instead until the necessary packages are available in the official Guix package channel.

;; -*- mode: scheme; fill-column: 80; comment-column: 50; -*-
;; SPDX-License-Identifier: GPL-3.0-or-later
;; Copyright © 2026 Aron Gile <aronggile@gmail.com>
(define-module (anki)
  #:use-module (guix)
  #:use-module (guix packages)
  #:use-module (guix build-system copy)
  #:use-module (guix build utils)
  #:use-module (guix download)
  #:use-module (ice-9 format)
  #:use-module ((guix licenses) #:prefix license:))

(define %anki-repo-url-format
  "https://github.com/ankitects/anki/releases/download/~a/anki-~a-linux-x86_64.zst")
(define %anki-version "26.05b2")

(define-public anki
  (package (name "anki")
           (version %anki-version)
           (build-system  copy-build-system)
           (source
            (origin (method url-fetch)
                    (uri (format #f %anki-repo-url-format version version))
                    (sha256 (base32 "1zw5r56469bawr2anyzlkdg2p89fm9crb7x01j23hi6nzrfih682"))))
             (arguments (list
                  #:validate-runpath? #f
                  #:phases
                  #~(modify-phases %standard-phases
                  (add-after
                  'unpack 'decompress
                  (lambda* (#:key source outputs #:allow-other-keys)
                          (let* ((tar-file    (format #f "anki-~a-linux-x86_64" #$%anki-version)))
                          (display (invoke "file" tar-file))
                          (invoke  "tar" "-xf" tar-file)
                          (chdir  "anki-linux")
                          #t)))
                   (add-after
                   'install 'setup-bin
                    (lambda* (#:key source outputs #:allow-other-keys)
                             (let* ((out-dir (assoc-ref outputs "out"))
                                    (target  (string-append out-dir "/share/anki/anki"))
                                    (link    (string-append out-dir "/bin/anki")))
                                    (invoke  "mkdir" "-p" (string-append out-dir "/bin"))
                                    (invoke  "ls" "-la" out-dir)
                                    (invoke  "ln" "-s" target link)))))
                  #:install-plan
                  #~(list
                   `("anki"          "share/anki/anki")
                   `("anki.1"        "share/anki/anki.1")
                   `("anki.png"      "share/anki/anki.png")
                   `("anki.xml"      "share/anki/anki.xml")
                   `("anki.xpm"      "share/anki/anki.xpm")
                   `("app"           "share/anki/app")
                   `("app_packages"  "share/anki/app_packages")
                   `("python"        "share/anki/python"))))

           (home-page "https://github.com/ankitects/anki")
           (synopsis "Anki is a smart spaced repetition flashcard program." )
           (description
            (string-append
             "Anki is a flashcard program that helps you spend more time on challenging material"
             "and less on what you already know."))
           (license  license:gpl3+)))

Package: anki-python

#!/usr/bin/env bash
guix ld build _anki-api
guix ld run   _anki-api