Generalized Abstraction

Automating Post Creation with Emacs

Friday, November 27 2020

I love Emacs and that's probably no surprise if you've read any of my posts about Emacs or Lisp. And in fact, one of the things I often preach about when going on about how great Emacs is, is how you can customize everything! This ultimately means being able to optimize your workflow to the nth degree.

When it comes to having a blog, one of the biggest minor hassles with writing posts is, well, creating the file. I need to find the directory where my posts live, figure out what the date is, physically create the file, and populate the top with the metadata used by the Julia package Franklin.

In order to simplify this, I decided to write an Elisp function that I could map to a key sequence. Now, whenever I am ready to write a new post, rather than fumbling around in the file system and preparing the file, I only need to hit Control+F11! I type in the name of my post in common language and I get a new file in the right directory, pre-populated with the necessary metadata, and voila, I can immediately get to it.

Let's Write a Little Lisp

(defun aether-create-post (filename)
  (interactive "M")
  (let ((post-file-name (mapconcat 
    'identity 
    `(,(format-time-string "%Y-%m-%d")
      ,(concat (replace-regexp-in-string " " "-" filename)
               ".md"))
    "-")) )) 
    (find-file (mapconcat 'identity
                          `(,blog-posts-directory ,post-file-name) 
                          "/" )
                          s)
  )

If you're not familiar with Lisp-code, then this definitely might look a bit confusing. Most people look at all of the parenthesis and start thinking, "What the hell is going on here?" So, let's break this down a little bit.

First of all, in Ruby a similar function would be written as follows. Since there's no equivalent for (interactive) or (find-file) we'll write the following as a function which takes an argument filname and simply returns the opened file.

def aether_create_post (filename)
  filename.gsub!(" ", "-")  << ".md"
  post_file_path = [BLOG_POSTS_DIRECTORY, filename].join("/")
  File.open(post_file_path)
end

In Lisp, we use parentheses as delimiters separating blocks of code and expressions, so expressions are all going to be wrapped in them. Therefore, we start off by using the defun keyword to state that we are defining a new function. This is followed by a name for the function and then arguments, just like we did in the Ruby version. You might also notice that in the Ruby version, we're using more underscores while we use lots of dashes in the Lisp version. Lisp code doesn't use infix operators so we tend to use dashes much more plentifully than in any other languages.

Another small note about the naming. A common way to engage in a sort of namespacing in Lisp code is to preface ones function name with some sort of self-identifier. The most common one I've seen is using my/ or my- such as my/version-of-this-function (yes, Lisp allows slashes in function names!). In specific Emacs distributions, it is common to preface function names with the name of the distribution. In my case, I've written a distribution called Aether so the functions which are part of the Aether code base I will generally name prefaced with aether-.

The next expression (interactive) is something specific to Elisp which tells us that we are working with an "interactive" function meaning that we're writing a function where the user can directly interact with the Emacs application such as providing real-time input through a form or by passing input from a highlight region. In our case, we use the interactive code M which means we'll be typing a string into the mini-buffer. This is accessible via the argument we referenced, filename. Therefore, upon running this function, we will immediately get a prompt in the mini-buffer where we type in the name of the file such as, "new posts for emacs," with spaces and all.

On the next line, we use the let binding to define a variable which will be local to the current code block. There are several ways to define variables in Lisp such as setq and defvar, but if we were to leverage either of those, we would find ourselves with a variable defined in the global scope, ultimately accessible to other code outside of this function. Scope is a topic I could fill an entire post with so for the sake of brevity I'll just say that we don't want to do that.

We define a variable called post-file-name which is comprised of a couple of parts. We generate a string with the current date in the format of YYYY-MM-DD, we replace all spaces with dashes in the filename string, which we just provided in real-time, and tag a ".md" on the end of it. Finally, we join together these two prepared strings with a dash. Our expected result should be something with the form of, 2020-11-27-some-post-name.md. We immediately use our local variable post-file-name in the next part where we perform another concatenation of a pre-defined constant containing the path to where my posts live and the prepared value of post-file-name by joining the two with a slash. This result is passed as an argument to the find-file function which is an Emacs function to open a file into the editor.

Worth Mentioning

There are a couple of points worth mentioning which slightly different in the Ruby version I used as an example. While I have defined the file path and opened the file, I have only created a file buffer which I can start working in immediately, but I haven't actually saved the file yet. I decided not to save the file the function because there have been plenty of times that I created a new post just to notice that I had to do something else and wouldn't have time to write then. I ultimately don't want to find myself with a plethora of empty posts that I started but never had an opportunity to finish working on.

So, that's how it's done. Another example of how easy it is to optimize your workflow through Emacs customization. In typical academic fashion, I'll leave how to associate this function to a key sequence as an exercise to the reader.

Feel free to contact me and let me know how bad my code is or how I've made numerous grammatical errors. I'm always happy to talk shop!