As I am currently writing my thesis manuscript, I needed a comfy and easy-to-use environment to write LaTeX without too much hassle. However, as I am a computer scientist, I obviously failed and instead conjured a complicated setup made of Nix https://nixos.org/ and Emacs. Since it adds non-trivial functionalities to my editor, I thought it could be useful to someone if I shared the config.
Setup & context
I write my thesis in a git repository with a Nix flake configuration. If you are unfamiliar with Nix, or
even
Nix flakes in particular, know that they allow me to create a software environment per-folder, instead of
having
every single dependency of every single project available everywhere at any time on my machine. It's tidier,
and
it allows for multiple versions of the same software to co-exist peacefully. My Nix flake basically pulls up
the most barebone LaTeX stack, with hand-picked libraries that only satisfy my own dependencies. This avoids
pulling the entirety of the LaTeX live repository and ending up with gigabytes of dependencies. Since I have
this Nix flake,
building my thesis document is also nixified: through nix build
, I can launch the
building
process,
calling the LaTeX compiler and ending up with a single PDF file.
I write my thesis using Emacs, since it's the editor I have used for most of my life. This blog post will probably not interest you if you don't use it, since it pertains to writing macros and functions for Emacs. A neat feature of Emacs is that it allows you to specify a line at the top of a file that basically tells the editor: "Ask the user if they want to execute this piece of Emacs Lisp code right now". This allows us to execute specific code when opening specific files, without needing to write a fully-fledged Emacs library.
Small disclaimer: the autoloader I present
has been created over the years of my PhD and is not perfect by any means. It is also tailored to the
structure of my project: LaTeX files are potentially in subdirectories, and the root of the project always
contains a flake.nix
file. Feel free to modify it and
improve it yourself!
Executing Elisp when opening a file
The first thing we need to do is to allow executing Elisp directly when opening a file in Emacs. You could execute the code manually yourself whenever you open a project file, but it's tedious. Fortunately, Emacs allows you to do just that, by writing a comment at the top of any file with this format:
% -*- eval: ... -*-
I used LaTeX here since this is what I use, but any kind of comment in any language containing this syntax works. When opening a file that contains such a comment, Emacs will ask you if you want to execute the code specified there. Since it's dangerous to auto-execute code directly, it will always ask first, unless you manually specify that this file is to be treated as safe and have the code loaded automatically.
With this, it's easy to create an auto-loaded script: use (load-file "autoloader.el")
after the
eval
directive to
execute the code contained within the autoloader.el
located in the current directory when
opening the LaTeX file!
Triggering compilation when saving a file
Emacs has this very handy tool called "hooks". They allow to register functions to be executed whenever a certain action happens. For example, you can set up a hook to execute a function whenever a file is opened. In our case, the hook that interests us is the one that is executed whenever a specific file is saved. It is used like this:
'after-save-hook (lambda () (message "I'm saved!")) nil t) (add-hook
The first argument specifies which hook to use (in our case, after-save-hook
). The second is the
function to execute; here I use a lambda to create an anonymous function, but you can also specify a named
function. I have no idea what the third argument is supposed to be (the manual says it's depth
,
but no explanation is given). The fourth however specifies that this hook is buffer-local, i.e. only for the
current file and not for every file.
Executing shell commands asynchronously
As my document grew, LaTeX compilation time increased by a lot. If I used the standard command spawning
mechanism of Emacs, I would have to wait a minute or so whenever I save my file, preventing me to do other
stuff
while waiting for the PDF to actualize. Instead, we need to meddle with asynchronous commands. The syntax is
quite simple: there is a high-level wrapper around the nasty low-level stuff, start-process
. It
returns a process object, which you can use to perform checks and other process management operations. One
such operation is adding a "sentinel": a function executed whenever the process changes (more details here https://www.gnu.org/software/emacs/manual/html_node/elisp/Sentinels.html). Since
we need the process object, we can do a let
binding:
let
("build-pdf" "build-pdf" "nix" "build")))
((proc (start-process if process-live-p proc)
(lambda () (message "Process has changed!")))) (set-process-sentinel proc (
Display errors instead of silently failing
For the longest time, I accepted that I needed to wait for the compilation to end, and if the PDF didn't
look
like it changed, I had to run nix build
manually, get the command that outputs the full log,
and
execute it myself. However I eventually grew tired of that, and I automated everything. In the process
sentinel function, you can check whether the process has exited with a non-zero code (which happens when
nix build
fails). The problem is that the entire log is not displayed when using
nix build
; instead, the program outputs a command with the path to the log (which can't be
determined easily as the Nix hash of the project is part of that path) that you have to execute to get the
entire thing. So we need to do the following:
- Check if the process has exited with an error.
- Extract the log command from the output of the process
- Execute the log command and capture the output
- Display the output to the user
I ended up with this:
;; Consume all output of the process
(while (accept-process-output process))let*
(;; Get the last line of the error output, which contains the command to display the logs
"build-pdf" (goto-char (point-max))
((line (with-current-buffer -1)
(forward-line 'line)))
(thing-at-point ;; Parse the line to get the log command only
and (string-match "'\\(.*?\\)'" line)
(command (save-match-data (1 line))))
(match-string ;; Setup a buffer to redirect the log command output to
progn
(output-buffer ("*latex-error*")
(kill-buffer "*latex-error*")))
(get-buffer-create ;; Split the command into tokens, since we need to apply them to the command spawning function
(command-words (split-string command));; Spawn the log process, set PAGER to cat to disable paging.
(err-process
(with-environment-variables"PAGER" "cat"))
((;; Clear the log buffer in case it wasn't empty.
"*latex-error*" (erase-buffer))
(with-current-buffer ;; Execute the log command and get output into *latex-error*.
apply 'start-process (append (list "latex-error" "*latex-error*") command-words (list "-L"))))))
(;; Display the popup window.
"*latex-error*" "")) (with-help-window
Hopefully the comments are self-explanatory enough; but basically, based on the buffer we set up to capture
the output of the compilation process (here build-pdf
), we get its last line, and get the
substring in parenthesis, since the command is within the parenthesis of the last line of the output. Then,
we setup another buffer, and call the command to get the log. The actual command executed is
nix log <path>
, and by default it uses the system pager to display the file, which
completely breaks output capture by Emacs. So we need to specify to not use the pager, which is done by
setting the environment variable PAGER
to cat
. At the end, we display the buffer
containing the full logs to the user using with-help-window
, which sets up a bunch of minor
modes that allow, among other things, to quickly close the buffer using the Q key.
Putting it all together
Here is the entirety of the script. It is also available as a standalone file here ./autoloader.el. Feel free to use it!
Small detail: as of now, this only works if you have a thesis.pdf
buffer opened in Emacs, since
I use Emacs' PDF viewer to look at the end result side-by-side with the LaTeX code. You can remove the part
about reverting the buffer if that is not important for you! You'll need to modify how
my/copy-pdf
works though.
defun my/copy-pdf ()
("Copies the PDF from the ./result/ folder to ./"
;; Travel up until at the root.
not (file-exists-p "flake.nix"))
(while ("../"))
(cd ;; Copy file and set up permissions.
"./" t nil nil nil)
(copy-file buffer-file-name "./" (file-name-nondirectory buffer-file-name)) #o644))
(chmod (concat
defun my/refresh-pdf (process signal)
("Reverts the presentation.pdf buffer and copy PDF to ./, or display errors"
;; Confirm the process is complete
when (memq (process-status process) '(exit signal))
(;; Check if compilation ended with an error
if (eq (process-exit-status process) 0)
(;; No error: revert any buffer named "thesis.pdf" and copy it to ./.
"thesis.pdf"
(with-current-buffer
(my/copy-pdf)nil t)
(revert-buffer "PDF compiled successfully"))
(message ;; Compilation failed: let's setup an error message
progn
(;; Consume all output of the process
(while (accept-process-output process))let*
(;; Get the last line of the error output, which contains the command to display the logs
"build-pdf" (goto-char (point-max))
((line (with-current-buffer -1)
(forward-line 'line)))
(thing-at-point ;; Parse the line to get the log command only
and (string-match "'\\(.*?\\)'" line)
(command (save-match-data (1 line))))
(match-string
;; Setup a buffer to redirect the log command output to
progn
(output-buffer ("*latex-error*")
(kill-buffer "*latex-error*")))
(get-buffer-create ;; Split the command into tokens, since we need to apply them to the command spawning function
(command-words (split-string command));; Spawn the log process, set PAGER to cat to disable paging.
(err-process
(with-environment-variables"PAGER" "cat"))
((;; Clear the log buffer in case it wasn't empty.
"*latex-error*" (erase-buffer))
(with-current-buffer ;; Execute the log command and get output into *latex-error*.
apply 'start-process (append (list "latex-error" "*latex-error*") command-words (list "-L"))))))
(;; Display the popup window.
"*latex-error*" ""))))))
(with-help-window
defun my/build-pdf-and-revert ()
("Builds the PDF with nix build and refreshes the output"
;; Travel up to the root.
not (file-exists-p "flake.nix"))
(while ("../"))
(cd let*
(;; Start the process calling nix build
"build-pdf" "build-pdf" "nix" "build")))
((proc (start-process if (process-live-p proc)
(;; Setup hook to execute when the process is finished
#'my/refresh-pdf))))
(set-process-sentinel proc
;; Setup a hook to execute the build function whenever the current file is saved.
'after-save-hook (lambda () (my/build-pdf-and-revert)) nil t) (add-hook