Learning Emacs Lisp
After using super-links for almost a year, custom IDs and backlinks became an inseparable part of my workflow. It’s the only part I’ve adopted from the whole Zettelkasten/org-roam craze. Org-mode’s built-in custom IDs don’t make sense, so I decided to create better custom IDs and teach myself some Emacs-lisp in the process.
My idea was to contain the buffer’s name with the current date (ISO formatted) in the ID. For example, if I’m looking at the buffer I’m writing this post in: taonaw.2021-12-19.1730.
Here’s the process I followed.
Getting ISO style time
This is how I get the time in the desired format, like 2021-12-19.1730 (17:30 is 5:30 PM).
((format-time-string "%Y-%m-%-d.%H%M"(current-time))
There are two functions here:
current-time
gets the current time in Emacs. Kind of. It returns the time in seconds since 01/01/1970 (the Unix Epoch.)format-time-string
formats the current time into something more human-readable, in this case, ISO format. The operators above are common across various programming languages. From the help text onformat-time-string
:
%Y is the year, %y within the century, %C the century.
%G is the year corresponding to the ISO week, %g within the century.
%m is the numeric month.
%b and %h are the locale’s abbreviated month name, %B the full name. (%h is not supported on MS-Windows.)
%d is the day of the month, zero-padded, %e is blank-padded.
…and so on. %H for hours, %M (capital M) for minutes.
Getting it into a Variable
In order to do something with our date above, we need to assign it a variable. For this we use the function Let
.
Let
sets variables and their values within the list it creates. The variables do not exist outside of this list. What’s a list? well, think of it as a fence made from parentheses.
For example: let ((dog)(cat)(mouse))
creates three variables. We also assign them a value at the same time: let ((dog woof)(cat meow)(mouse squeak))
. Let
is usually what we want to use in a function because the values are unique to that function. In our case, that would be the date we created above. This is how it looks like:
(let ((timestring (format-time-string "%Y-%m-%-d.%H%M"(current-time)))))
we create only one variable, timestring, and then we use the piece of code from above to assign a value to it. Look closely at the parentheses, and the order makes sense:
- Use the function
current-time
. - Use
format-time-string
on the result of this function to format the time into a yyyy-mm-dd.hhmm. - Store this string we just created (the value) in a variable, “timestring”
In order to be sure we have the value we want inside timestring, we can ask Emacs to display it in our messages buffer, with message timestring
.
The end result looks like this:
(let ((timestring (format-time-string "%Y-%m-%-d.%H%M"(current-time))))
(message timestring))
Why do we need double parentheses after the let
? It doesn’t look like it makes any difference…?
Remember the lists from our example. Let
is meant to be used for a couple of items at once. Emacs doesn’t care if we have one item or a hundred: the way we mark them is the same. Since each item in the list is marked with its own parentheses, timestring gets its own pair. Look closely and you will see that timestring’s parentheses end after the format-time-string
function; the next one belongs to message timestring
.
Lists are important in Emacs-Lisp. As a matter of fact, That’s what its name stands for: LISt Processing language. It’s right there in the manual.
It took me some time to understand the logic behind it all (with additional extra help), but at the end, you can see it in action.
Getting the File (Buffer) Name
To add the buffer’s name to the ID we need a different function, buffer-file-name
. Since we’ll be working with files, we’ll also need file-name and its components. Before we dive in, it’s important to understand how Emacs “understands” files. From the help text:
Emacs considers a file name as having two main parts: the directory name part, and the nondirectory part (or file name within the directory). Either part may be empty. Concatenating these two parts reproduces the original file name.
Since we’re just interested in the file without the path or the extension, we had to clean it up a bit1.
Here’s the code, explanation follows:
(let ((filename (file-name-nondirectory (file-name-sans-extension (buffer-file-name)))))
(message filename))
We already know about let
. Here, we’re creating a variable named “filename” and giving it a value through these functions, by the order of the parentheses:
buffer-file-name
does exactly what the name implies, gives you the file that belongs to the buffer you’re visiting, complete with a full path.- First part of “cleaning” the name is on the second level,
file-name-sans-extension
, which is also pretty straightforward: it gets read of the extension of the file. - Next,
file-name-nondirectory
gives out the name of the file without the directory it’s in. This finishes the “cleaning” and leaves us with just the name of the file without its extension and without the directory. - The name of the file name without the directory and extension is stored in “filename”.
- Like before, to verify we have the right value, we print the variable we created in the messages buffer.
Connecting the two Variables
Since we want to get a value that connects the file’s name with the date, we need to connect our variables: filename + timestring.
We already know that let
creates lists, so we will build a new let
that contains both “filename” and “timestring,” using the code above.
We then need to connect them together into a new string, the filename + timestring part. We do that with the concat
function.
the code:
(let ((filename (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))
(timestring (format-time-string "%Y-%m-%d.%H%M.%S"(current-time))))
(let ((ID (concat filename "." timestring)))
(message ID)))
Lots of parentheses which need to be followed carefully. Emacs has a few solutions for this, like show-paran-mode
.
The first let
opens up with the instruction above, but instead of closing the let
after buffer-file-name
like we did before, we open another let
inside the existing one. This let
creates a new variable, “ID,” which in turn is getting value from the concat
function. The concat takes filename, adds a period at the end of it, and then adds timestring at the end. This gets us what we want.
When I look at the function now and follow the parentheses, an order emerges. Can you see it? It starts with buffer-file-name
and current-time
simultaneously, and follows an order of procedures all the way to message ID
.
Making it All Work
Now that we have both parts in place working together, we need to “wrap” the code in a function, so we can call it and use it:
(defun filename_ID()
(interactive)
(let ((head (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))
(tail (format-time-string "%Y-%m-%d.%H%M.%S"(current-time))))
(let ((filename (concat head "." tail)))
(message filename)))
)
Here’s what was added:
defun
is how we define a function in Emacs, followed by a name: filname_ID in this case.
interactive
is a special function part (per the help text, this is actually a deceleration. I am not sure yet how this compares to a function, so if you have an idea, feel free to let me know) which allows us to call the function later. In our case, we want to store this function in Emacs' configuration so that we can call it later on when it’s time to create a custom ID.
Using in Capture: Next Steps
When I wanted to use this function inside my capture templates, I encountered a problem: the capture buffer does not have a file associated with it, so the function does not work. Even if we were to use buffer-name
(see footnotes), this will still not work well: the capture templates use an “inbox” file, which is a temporary location. I want the ID of the headers to reflect their final position, like my wiki.org or weekly files such as 21_50.org.
To get around this problem I started exploring the idea of using a function as a target pointer for my capture templates, and found out something interesting: the target in capture templates is usually defined as a path, such as ~/Documents/Org-files/
or similar. However, you don’t have to. You can put in a name of a variable you assign a value of a path.
For example, I can set a path using the following 2:
(setq capture-path "~/Documents/Org-files/tasks.org")
Later, in the capture template, all I have to do is something like this:
("t" "task" entry (file+headline capture-path "Tasks") "** TODO %? %^g\n")
to create a task as a secondary header under my “Tasks” heading inside the tasks.org file. In turn, this allows creating another function that could define the target dynamically. In my case, I could use something similar to what we did earlier with timestring to generate a yy_mm
.org value for the variable I will use later as a target. Seems like I have some more brainstorming to do.
Footnotes
-
As I was writing this post, looking into the help documents, I realized there are better functions to use here:
buffer-name
will return the name of the buffer, which works even if the buffer does not have a file.file-name-base
will give out the name of the file without the path and without the extension without the need to “clean it up.” I’m glad I went through the steps above though as a learning experience. ↩︎ -
Notice that we use
setq
here. Unlikelet
,setq
sets a variable that remains beyond the immediate expression it was called in. This is necessary since the capture templates are their own functions, and will use this pre-defined variable. ↩︎