LitDoc workflow for learning and reviewing information using Emacs and Anki
Emacs/Learn
Requirements
- Git: distributed version control system
- GNU/GUIX: a functional package management system
- LitDoc: literate programming utility
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/dataFunctions
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 ankiUtility 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