I always liked Gilles’ note taking set up, even though it was not “the right tool” for me. Working in org not only feels more intuitive but allows me to be way more flexible - I can always publish my lectures in multiple forms, without really breaking a sweat…
Simplifying and automating the mundane task of creating, organizing, and finding your static notes so that you can focus on their actual content. It does not alter the way you would otherwise interact with your org files, like other widely used packages.
After setting your org-lectures-dir you can expect it to quickly be populated in
the following form
org-lectures-dir # All your courses will be in here ├── course_<COURSE> # course - specific folder │ └── <lec_type>_<COURSE>_<DATE>.org # lecture files └── course_<COURSE>.org # course description file
I’m keeping this sort because I do not feel like elaborating right now. I’ll add extensive documentation by the end of the week.
(use-package org-lectures
:straight
(:host github :repo "chatziiola/org-lectures")
:config
(setq org-lectures-dir (expand-file-name "/your/path"))
(setq org-lectures-static-course-files-dir "/your/path")
(setq org-lectures-roam-id-integration t)
:general
(lc/leader-keys
"ll" '(org-lectures-find-course :which-keys "lecture")
"lF" '(org-lectures-dired-course-folder :which-keys "Open course's folder"))
)This package allows the user to:
- Easily organize and manage their lecture notes through an interactive prompt.
- Easily navigate between lectures of the same course
- Quckly open the course’s directory (or directories if you use a split configuration)
Check out the cdlatex package, especially if you are in a field with plenty of mathematics.
It allows for the smooth integration of latex inside of org mode.
- User calls
find-course:- A minibuffer appears in which the user can filter courses through:
- Short-Title
- Title
- Professor
- Institution
- If the user selects an existent course go to 2. Otherwise a new course
will be created ( the user must select “New Course” for that:
org-lectures-create-new-course)
- A minibuffer appears in which the user can filter courses through:
org-lectures-open-courseis called:- A minibuffer appears in which the user can filter the selected course’s
lectures through:
- Date
- Professor name
- Lecture Title
- If the user selects an existent lecture: it opens in the same window.
Otherwise a new lecture may be created ( the user must select “New
Lecture” for that
org-lectures-create-new-lecture)
- A minibuffer appears in which the user can filter the selected course’s
lectures through:
- [ ] Maybe implement classes. This is an obvious example of a program that could be improved with OOP.
- [ ] See
org-lectures-find-coursefor improvement (quicker course creation) - [ ] See maybe that you publish the strange keyword library as well, because I do not like using it like that in various places
- [ ] See maybe that you publish the strange keyword library as well, because I do not like using it like that in various places
Header
;;; org-lectures.el --- Chasing simplicity -*- lexical-binding: t -*-
;; This file has been generated from the literate.org file. DO NOT EDIT.
;; Sources are available from https://github.com/chatziiola/org-lectures
;; Copyright (C) 2022-2025 Lamprinos Chatziioannou
;; Author: Lamprinos Chatziioannou
;; Maintainer: Lamprinos Chatziioannou
;; URL: https://github.com/chatziiola/org-lectures
;; Special thanks to:
;; - Gilles Castel (https://castel.dev)
;; - Jethro Kuan (https://github.com/jethrokuan)
;; - David Wilson (https://github.com/daviwil)
;; - Nicolas P. Rougier (https://github.com/rougier)
;; They inspired me not only to modify the "vanilla" setup and create scripts
;; for myself, but also to catch the "bug" of making my setups reproducible and
;; proper—giving back to the amazing Emacs, Org, and FOSS communities.
;; This file is NOT part of GNU Emacs.
License
;; LICENSE
;; This file 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, or (at your option)
;; any later version.
;; This file 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/>.
TODO Commentary
;;; Commentary:
;; For a deep dive into the ideology of the package, look up the README.org file
;; that you should have received along with it.
Let’s dive into the code
;;; Code:
(require 'org)
Lectures are to be stored within my org-roam uni directory
Parameters: ONLY THIS TO BE CHANGED BETWEEN INSTALLATIONS
(defvar org-lectures-dir (expand-file-name "~/org/lectures")
"Lecture and course files directory.
All courses and their respective lecture files are stored in
subfolders.")
(defvar org-lectures-default-institution "A.U.Th"
"Default institution to be used on lecture creation.
This variable should be set once, when starting to use this
notetaking set up.
Setting the property INSTITUTION properly in the course
information file will cause all lectures for that course to be
created with that property (thus overwriting this variable).
Even though it certainly is not always useful , it helps automate
most of the lecture notes for any undergrad.")
(defvar org-lectures-current-semester "5"
"Holds current semester value.
To be updated at the beginning of each semester by the user. This
option is not necessary but helps in the /automatic/ gathering of
data around courses.")
(defvar org-lectures-static-course-files-dir org-lectures-dir
"The path to extra course-related subfolders.
This option defaults to `org-lectures-dir'. Thus,
combining the two /folders/, of static information and
lectures (note-taking) into a single folder. Check the README.org
file for more information on the thinking process behind this
choice.")
(defvar org-lectures-org-roam-id-integration t
"Whether `org-lectures-dir' is a subdirectory of `org-roam-directory'.
If this is true, then upon file creation a unique ID will be
generated, so that course files can be linked and use from within
org-roam.
FIXME. This option is not currently implemented.")
(defvar org-lectures-append-to-inbox nil
"Whether an entry should be added to the users `inbox.org' file, (found in `org-directory')."
)
<2023-04-17 Mon>: New parameter: Note type:
(defvar org-lectures-note-type-alist '(("lecture" . "lec"))
"Contains the note type. Every pair here will be checked.
The format is '(key . regex).
TODO: Implement it in note creation.
"
)<2023-04-17 Mon>: New parameter: Lecture data alist:
(defvar org-lectures-lecture-data-alist '("TITLE" "PROFESSOR" "DATE")
"WARNING: These get added in reverse in the final prompt.
The variable is heavily /under/-tested, so if you decide to use
it be prepared to encounter strange behaviour. It is intended to
be linked to `org-lectures-note-type-alist' in the future, so
that there is no need to differentiate between course note files
in an unnefficient manner.
At the moment due to spaghetti (at times) coding, only three
arguments will get shown. I'm thinking of restructuring this so
that it (maybe) utilizes consult, or simply refactoring so that
it is not so hastily written. In any case, FIXME.
")<2025-10-27 Mon>: New parameter: Course Cache
(defvar org-lectures--course-cache nil
"Cache for the course index data loaded from the index file.")<2025-10-25 Sat>: New parameter: Lecture file template
(defvar org-lectures-file-template
":PROPERTIES:
:ID: %i
:END:
#+TITLE: Διάλεξη:
#+FILETAGS: %t
#+DATE: %d
#+COURSE: %c
#+INSTITUTION: %n
"
"Template for new lecture files.
Use `format-spec` codes:
%i -> ID (e.g., \"lec-<course>-\")
%d -> date (e.g., \"<2025-10-25>\")
%c -> course
%n -> institution
%t -> filetags")<2025-10-25 Sat>: New parameter: Lecture default tags
(defvar org-lectures-default-tag-alist '("lecture" "todo")
"This variable is used when setting the FILETAGS parameter in new lecture files")I wanted to be able to use a slug function just like the one used in org-roam and knowing that it existed there I found no reason to lose time on implementing a different one.
- Source: https://github.com/org-roam/org-roam
- <2025-10-25 Sat>: I opted for a much simpler version:
- Keeps only alphanumeric characters - discards everything else.
- Uses ‘-’ for spaces.
(defun org-lectures-sluggify (s)
"Given a string return it's /sluggified/ version.
It has only one argument, INPUTSTRING, which is self-described"
(let* ((s (downcase (string-trim s)))
(s (replace-regexp-in-string "[^[:alnum:][:space:]-]" "" s))
(s (replace-regexp-in-string "[[:space:]]+" "-" s)))
(replace-regexp-in-string "^-\\|-$" "" s)))Functions to extract the values of keywords set up like the following at the top of org-mode documents
#+COURSE: 18.06 #+PROFESSOR: Gilbert Strang
This function and the next ( even though that one has been slightly modified ) were found in StackOverflow.
(defun ndk/get-keyword-key-value (kwd)
"Only to be used by `org-lectures-get-keyword-value'.
Allows for the extraction of KWD from the current buffer.
Works only for buffers using the Org-Mode syntax."
(let ((data (cadr kwd)))
(list (plist-get data :key)
(plist-get data :value))))
This function is the main function used to take advantage of the syntax shown in the example above.
(defun org-lectures-get-keyword-value (key &optional file)
"Return the value for KEY in an Org buffer.
If FILE is given, find that file and check there. Otherwise, use
the current buffer.
If KEY is a list, return a list of corresponding values."
(if file
(with-temp-buffer
(insert-file-contents file)
(org-lectures--get-keyword-value-from-buffer key))
(org-lectures--get-keyword-value-from-buffer key)))
Now some notes on the latter function:
- At some point I had implemented
(kill-buffer)to avoid having too many open buffers in my emacs instance. That, even though, had its merits, resulted in extremely reduced performance. Now, when first running the script on a specific category (or in general) it may take some time[fn:1]
Find-course: This is the entry point
(defun org-lectures-find-course ()
"Default driver function of `org-lectures.el'."
(interactive)
(let* ((course-answer (org-lectures-select-course-from-list)))
(cond
((string-equal course-answer "NC")
(org-lectures-create-new-course))
(t
(org-lectures-open-course (upcase course-answer))))))
Select course from course prompt
; Minor modification so that I can use it in the publishing functions as well
(defun org-lectures-select-course-from-list ()
"Show a prompt and return the selected course's ID."
(let ((courses (org-lectures-get-course-list)))
(if (not courses)
(let ((selection (completing-read "Select Course: " '("New Course"))))
(if (string-equal selection "New Course") "NC" nil))
(let* (;; Dynamic column widths for pretty alignment
(max-title-width (apply #'max 0 (mapcar (lambda (c) (length (or (plist-get c :title) ""))) courses)))
(max-prof-width (apply #'max 0 (mapcar (lambda (c) (length (or (plist-get c :professor) ""))) courses)))
(max-inst-width (apply #'max 0 (mapcar (lambda (c) (length (or (plist-get c :institution) ""))) courses)))
(vertico-p (and (fboundp 'vertico-mode) vertico-mode))
(format-string-vertico (format "%%-5s %%-%ds │ %%-%ds │ %%-%ds" max-title-width max-prof-width max-inst-width))
(format-string-default (format "%%-5s %%-%ds %%-%ds %%-%ds" max-prof-width max-title-width max-inst-width))
(course-prompt-alist
(append
(mapcar
(lambda (course-plist)
(let* ((course-id (or (plist-get course-plist :course-id) ""))
(professor (or (plist-get course-plist :professor) ""))
(title (or (plist-get course-plist :title) ""))
(institution (or (plist-get course-plist :institution) "")))
(cons (if vertico-p
(format format-string-vertico course-id title professor institution)
(format format-string-default course-id professor title institution))
course-id)))
courses)
(list (cons "New Course" "NC")))))
(let* ((selected-prompt (completing-read "Select Course: " course-prompt-alist)))
(cdr (assoc selected-prompt course-prompt-alist)))))))Get list of courses
(defun org-lectures-get-course-list ()
"Return a list of course property lists from the index."
(let ((index (org-lectures--get-index)))
(mapcar (lambda (course-entry)
(let* ((course-id (car course-entry))
(props (cdr course-entry)))
(list :course-id course-id
:file (plist-get props :file)
:title (plist-get props :title)
:professor (plist-get props :professor)
:institution (plist-get props :institution)))) index)))
Create new course:
(defun org-lectures-create-new-course ()
"Create a new course.
More specifically this function creates:
1. The course info file (course_<course>.org)
2. The course lectures directory (...)
3. TODO anything else here?
Function called through `org-lectures-find-course', when the
creation of a new course is necessary. It prompts the user for
input (short title for the course), up to 4 letters which serve
as the course's ID. It checks whether a course with that ID
already exists and if it does, it uses `org-lectures-open-course'
instead of creating any new files. If, however the filel dows not
exist, and the length of the short title is less than 4 letters a
new org file is created, in `org-lectures-dir', and with
the course's default properties all set up."
(interactive)
(let* ((course (downcase (completing-read "Insert short course title:" ())))
(course-org-file (org-lectures-get-course-info-file course)))
(cond
((file-exists-p course-org-file)
(org-lectures-open-course (upcase course)))
((<= (length course) 4)
(org-open-file course-org-file)
(insert ":PROPERTIES:\n:ID: " course "-course\n:END:\n#+TITLE:\n#+PROFESSOR:\n#+INSTITUTION: " org-lectures-default-institution "\n#+SEMESTER: " org-lectures-current-semester "\n#+FILETAGS: course\n#+COURSE: " (upcase course) "\n")
(save-buffer)
(let ((new-course-entry
`(,(upcase course) . (:title ""
:professor ""
:institution ,org-lectures-default-institution
:file ,course-org-file
:lectures '()))))
(org-lectures--get-index)
(push new-course-entry org-lectures--course-cache)
(org-lectures--write-index-to-file)))
(t
(error "Invalid Course Name. Short title must be less than 5 characters long")))))
(defun org-lectures-open-course-folder (&optional course)
"Open the selected course's folder (with system default).
Works only if inside an org file with the 'COURSE' property, or
when called by `org-lectures-open-course'"
(interactive)
(let* ((course (or course (org-lectures-get-keyword-value "COURSE"))))
(unless (symbolp course)
(message (concat "Course " course " folder opened"))
(shell-command (concat "open " org-lectures-static-course-files-dir "course_" course)))))
(defun org-lectures-dired-course-folder (&optional course)
"Open the selected course's folder (with Dired).
Works only if inside an org file with the 'COURSE' property, or
when called by `org-lectures-open-course'"
(interactive)
(message "org-lectures-dired-course-folder Function will be deprecated in later version")
(let* ((course (or course (org-lectures-get-keyword-value "COURSE"))))
(unless (symbolp course)
(message (concat "Course " course " folder opened")))
(dired (concat org-lectures-static-course-files-dir "course_" course))))
- <2025-10-25 Sat> Deprecated message dired-course-folder. No reason to exist, since keybindings for dired do this much more easily.
(defun org-lectures-open-course (course)
"Get prompt for COURSE lectures.
Open a minibuffer, using `org-lectures-select-lecture-from-course' in which the
user can filter the selected course's lectures, selecting an existing one, or
creating a new one. Gives the option to:
1. Create new lecture
2. Open an already existing lecture
3. Open the course's folder
4. Open the course's info file `course_<course>.org')."
(let* ((lecture-answer (org-lectures-select-lecture-from-course course)))
(if (stringp lecture-answer)
(cond
((string-equal lecture-answer "NL")
(org-lectures-create-new-lecture course))
((string-equal lecture-answer "OF")
(org-lectures-dired-course-folder course))
((string-equal lecture-answer "INFO")
(org-open-file (org-lectures-get-course-info-file course))))
(org-open-file (car (last lecture-answer))))))
(defun org-lectures-select-lecture-from-course (course &optional publish)
"Open a COURSE lecture for viewing or create a new one."
(let* ((course-lectures
(mapcar (lambda (file)
(cons course (append (org-lectures-get-keyword-value org-lectures-lecture-data-alist file)
(list file))))
(org-lectures-get-lecture-file-list course))))
(if (not course-lectures)
(let ((selection (completing-read "Select Lecture: " '("New Lecture" "Open Course Folder" "Course Info"))))
(cond ((string-equal selection "New Lecture") "NL")
((string-equal selection "Open Course Folder") "OF")
((string-equal selection "Course Info") "INFO")
(t nil)))
(let* ((max-date-width (apply #'max 0 (mapcar (lambda (l) (length (or (nth 2 l) ""))) course-lectures)))
(max-title-width (apply #'max 0 (mapcar (lambda (l) (length (or (nth 0 l) ""))) course-lectures)))
(max-prof-width (apply #'max 0 (mapcar (lambda (l) (length (or (nth 1 l) ""))) course-lectures)))
(vertico-p (and (fboundp 'vertico-mode) vertico-mode))
(format-string
(if vertico-p
(format "%%-%ds │ %%-%ds │ %%-%ds" max-date-width max-title-width max-prof-width)
(format "%%-%ds %%-%ds %%-%ds" max-date-width max-title-width max-prof-width)))
(lecture-prompt-list
(append
(mapcar
(lambda (lecture)
(let ((title (or (nth 0 lecture) ""))
(professor (or (nth 1 lecture) ""))
(date (or (nth 2 lecture) "")))
(cons (format format-string date title professor) lecture)))
course-lectures)
(unless publish
(list '("New Lecture" . "NL")
'("Open Course Folder" . "OF")
'("Course Info" . "INFO"))))))
(let* ((selected-prompt (completing-read "Select Lecture: " lecture-prompt-list)))
(cdr (assoc selected-prompt lecture-prompt-list)))))))
- This one is actually more of a macro, but a rather useful one.
(defun org-lectures-get-lecture-file-list (course)
"Return a list of lecture files in COURSE.
If the subdirectory does not exist, it creates it."
(let* ((course-dir (expand-file-name
(concat "course_" course) org-lectures-dir)))
(unless (file-directory-p course-dir)
(make-directory course-dir))
(directory-files
course-dir ;inside the course directory
'full ; recursive
(concat (regexp-opt (mapcar #'cdr org-lectures-note-type-alist)) "_" (upcase course) "_.*\.org")))) ;lecture filenames template
(mapcar #’cdr my-alist) is calling the mapcar function with two arguments: the cdr function and my-alist.
my-alist is the alist that you defined earlier, which is a list of key-value pairs where each key is a string (e.g., “title1”, “title2”) and each value is a string (e.g., “key1”, “key2”).
cdr is a built-in Emacs Lisp function that returns the “cdr” (i.e., the second element) of a cons cell. In this case, cdr is being used to extract the values (i.e., the keys) from my-alist.
So, (mapcar #’cdr my-alist) applies cdr to each key-value pair in my-alist, returning a list of just the values (i.e., the keys).
(regexp-opt …) is calling the regexp-opt function with the list of keys returned by (mapcar #’cdr my-alist) as its argument.
regexp-opt is a built-in Emacs Lisp function that takes a list of strings and returns a regular expression that matches any of the strings. It constructs a regular expression by concatenating the strings and using special characters to indicate alternatives and character sets. The result is a regular expression that can be used to match any of the original strings.
So, (regexp-opt (mapcar #’cdr my-alist)) constructs a regular expression that matches any of the keys in my-alist. This regular expression is used to match the file names in the directory-files call, allowing you to search for files that match any of the keys in your alist.
(defun org-lectures-create-new-lecture (&optional COURSE INSTITUTION)
"Create a new file for COURSE of INSTITUTION.
Populate it according to `org-lectures-file-template'.
Optional arguments exist:
COURSE: to be added in the lecture's '#+COURSE' field,
automatically populated when called through
`org-lectures-open-course'
INSTITUTION: to be added in the lecture's '#+INSTITUTION' field,
automatically populated by 'A.U.Th' if left empty."
(let ((COURSE (or COURSE ""))
(INSTITUTION (or INSTITUTION (org-lectures-get-lecture-institution COURSE)))
(lecture-filename (expand-file-name
;; This function also checks whether such a func exists
(org-lectures-set-lectures-filename COURSE)
(expand-file-name (concat "course_" COURSE) org-lectures-dir))))
(let* ((id (concat "lec-" COURSE "-"))
(date (format-time-string "<%Y-%m-%d>"))
(tags (string-join (seq-map (lambda (x) (cond ((stringp x) x) ((consp x) (car x)) (t nil))) org-lectures-default-tag-alist) " "))
(spec (format-spec-make ?i id ?d date ?c COURSE ?I INSTITUTION ?t tags))
(payload (format-spec org-lectures-file-template spec t)))
(write-region payload nil lecture-filename)
(let* ((new-lecture-entry `(,date . (:title "Διάλεξη:"
:file ,lecture-filename))))
(org-lectures--get-index)
(let ((course-in-cache (assoc COURSE org-lectures--course-cache)))
(when course-in-cache
(setf (plist-get (cdr course-in-cache) :lectures)
(cons new-lecture-entry (plist-get (cdr course-in-cache) :lectures)))))
(org-lectures--write-index-to-file)
(if org-lectures-append-to-inbox
(write-region (concat "\n* ACTION \[\[" lecture-filename "\]\]\n") nil (expand-file-name "inbox.org" org-directory) t))
(org-open-file lecture-filename)))))
(defun org-lectures-get-lecture-institution (course)
"Return the proper institution for a course from the index."
(if (string-blank-p course)
org-lectures-default-institution
(let* ((index (org-lectures--get-index))
(course-data (cdr (assoc course index))))
(or (plist-get course-data :institution)
org-lectures-default-institution))))
(defun org-lectures-get-course-info-file (course)
"Return the filename of that course's info file"
(let* ((lower-file (expand-file-name (concat "course_" (downcase course) ".org") org-lectures-dir ))
(proper-file (expand-file-name (concat "course_" course ".org") org-lectures-dir )))
(if (file-exists-p lower-file) ; remnants of a shady past
lower-file
proper-file)))(defun org-lectures--get-note-type ()
"Interactively select a note type from `org-lectures-note-type-alist'."
(let ((types org-lectures-note-type-alist))
(if (= (length types) 1)
(cdar types)
(let* ((prompt "Select a title: ")
(options (mapcar #'car types))
(choice (completing-read prompt options)))
(cdr (assoc choice types))))))
(defun org-lectures--get-collision-suffix ()
"Prompt user for info if lecture file exists, returning a filename suffix."
(let ((prompt "A lecture with this filename already exists. Enter complementary information (empty appends hour-minute-second): "))
(let ((user-input (read-string prompt)))
(if (string-blank-p user-input)
(format-time-string "%H%M%S" (current-time))
(org-lectures-sluggify user-input)))))
(defun org-lectures-set-lectures-filename (course)
"Return a unique lecture filename using the format:
`notetype_COURSE_DATE[_SUFFIX].org'."
(let* ((note-type (org-lectures--get-note-type))
(date-str (format-time-string "%Y%m%d" (current-time)))
(base-filename (format "%s_%s_%s.org" note-type course date-str))
(course-dir (expand-file-name (concat "course_" course) org-lectures-dir))
(suffix (when (file-exists-p (expand-file-name base-filename course-dir))
(org-lectures--get-collision-suffix))))
(if suffix
(format "%s_%s_%s_%s.org" note-type course date-str suffix) base-filename)))An interesting addition here, could be helpful.
TODO Add options:
- to automatically get rid of the tex files
- to automatically create a pdf folder
- to zip the created pdf folder for easy sharing
(defun org-lectures-publish-course-to-pdf (course)
"Export a list of Org files to LaTeX format, using xelatex as the compiler and including mytex.tex as an extra header.
COURSE: Is the short name for the relative course. "
(setq org-latex-compiler "xelatex")
; This does not work. I need to modify my org-lectures capture template to automatically insert it
; (setq org-latex-header-extra "\\include{~/Github/org-to-latex-export/sample.tex}")
(dolist (file (org-lectures-get-lecture-file-list course))
(message (concat "Starting with " file))
(with-current-buffer (find-file-noselect file)
(org-latex-export-to-pdf))))(defun org-lectures--get-keyword-value-from-buffer (key)
"Return the value(s) for KEY(s) from the current buffer's Org content.
If KEY is a list, return a list of corresponding values."
(let ((keyword-map (org-element-map (org-element-parse-buffer 'greater-element)
'(keyword) #'ndk/get-keyword-key-value)))
(if (listp key)
(mapcar (lambda (k) (cadr (assoc k keyword-map))) key)
(cadr (assoc key keyword-map)))))
(defun org-lectures-rebuild-index ()
"Scan all course and lecture files and rebuild the index.
The index is stored in `.org-lectures-index.el` in `org-lectures-dir`.
This function reads files into temporary buffers and does not leave them open."
(interactive)
(let ((index-file (expand-file-name ".org-lectures-index.el" org-lectures-dir))
(course-files (directory-files org-lectures-dir t "course_.*\\.org$"))
(index-data '()))
(dolist (course-file course-files)
(with-temp-buffer
(insert-file-contents course-file)
(let* ((course-id (org-lectures--get-keyword-value-from-buffer "COURSE"))
(course-title (org-lectures--get-keyword-value-from-buffer "TITLE"))
(course-prof (org-lectures--get-keyword-value-from-buffer "PROFESSOR"))
(course-inst (org-lectures--get-keyword-value-from-buffer "INSTITUTION")))
(when course-id
(let* ((course-lecture-dir (expand-file-name (concat "course_" course-id) org-lectures-dir))
(lecture-files (when (file-directory-p course-lecture-dir)
(directory-files course-lecture-dir t (concat (regexp-opt (mapcar #'cdr org-lectures-note-type-alist)) "_" course-id "_.*\\.org$"))))
(lecture-data '()))
(dolist (lecture-file lecture-files)
(with-temp-buffer
(insert-file-contents lecture-file)
(let* ((lecture-title (org-lectures--get-keyword-value-from-buffer "TITLE"))
(lecture-date (org-lectures--get-keyword-value-from-buffer "DATE")))
(when lecture-date
(push `(,lecture-date . (:title ,lecture-title
:file ,lecture-file))
lecture-data)))))
(push `(,course-id . (:title ,course-title
:professor ,course-prof
:institution ,course-inst
:file ,course-file
:lectures ,lecture-data))
index-data))))))
(with-temp-buffer
(require 'pp)
(pp index-data (current-buffer))
(write-file index-file))
(message "org-lectures index rebuilt.")))
(defun org-lectures--get-index ()
"Load and return the course index.
If the index file does not exist or is stale, it is rebuilt.
The index data is cached in `org-lectures--course-cache`."
(let ((index-file (expand-file-name ".org-lectures-index.el" org-lectures-dir)))
(when (or (not (file-exists-p index-file))
(file-newer-than-file-p org-lectures-dir index-file))
(org-lectures-rebuild-index))
(or org-lectures--course-cache
(with-temp-buffer
(insert-file-contents index-file)
(setq org-lectures--course-cache (read (current-buffer)))))))
(defun org-lectures--write-index-to-file ()
"Write the current in-memory course cache to the index file."
(let ((index-file (expand-file-name ".org-lectures-index.el" org-lectures-dir)))
(with-temp-buffer
(require 'pp)
(pp org-lectures--course-cache (current-buffer))
(write-file index-file))))(provide 'org-lectures)
;;; org-lectures.el ends here