Maciej Janicki's website

2020-09-20 desktop dmenu shell

Having all PDFs just a few keystrokes away

I tend to have a lot of PDF files on my computer: books, science papers, presentations… I often need to quickly open one of them: check a theoretical detail in a book related to what I’m doing or a reference in a paper I’m reading. With the help of dmenu and a short shellscript, I have found a way to make this process as quick and unintrusive as possible.

dmenu: a quick introduction

dmenu is a fantastic program that serves only the purpose of “choosing one option out of a list of possibilities”. It is very similar to fzf that I’ve mentioned before, but operating in a graphical window manager instead of the text terminal. Here’s a minimal example:

echo -e "apples\nbananas\ngrapes\noranges\npears\npineapple" | dmenu

This will show the following menu on the top of the screen:

dmenu

Once you start typing, the selection will be narrowed down to the entries containing the typed string. The chosen item will be printed on standard output.

The basic solution

In our case, the entries in the list are going to be paths of PDF files on the system. I store all my documents in the directory ~/Documents, so I can obtain a list of all of them with the following command:

find ~/Documents -name '*.pdf'

Let’s combine this with dmenu to be able to choose one file from the list:

find ~/Documents -name '*.pdf' | dmenu -i -l 20

The parameter -i makes dmenu match case-insensitively and -l 20 displays one entry per line in 20 lines (instead of displaying all entries in one line as in the example).

The output of the command is the chosen file. The only missing part now is to pass the filename to your PDF reader of choice. Mine is zathura, so I do the following:

FILENAME=$(find ~/Documents -name '*.pdf' | dmenu -i -l 20) \
&& exec zathura "$FILENAME"

And here’s the result:

dmenu

Just save it to a shellscript file, bind it to a key in your window manager and there you go!

…except…

If you’re a purist like me, you may be disturbed be the need to call the find command every time this script is executed. If the ~/Documents directory is large, it might be quite slow and involve a lot of unnecessary disk reads. A reasonable solution would be to keep the list of all files in a text file (say, index.txt) and only update it if the content of the directory changes. That’s why we’ll change the script to the following:

if [ ! -f ~/Documents/index.txt ] || [ ~/Documents -nt ~/Documents/index.txt ]; then
  find ~/Documents -name '*.pdf' > ~/Documents/index.txt
fi
FILENAME=$(dmenu -i -l 20 < ~/Documents/index.txt) \
&& exec zathura "$FILENAME"

The condition in the first line makes use of two tests: ! -f is true when the file does not exist, while -nt checks whether the argument on the left is “newer than” the one on the right (in the sense of the “last modified” filesystem entry). Every time you make some modification to the documents directory (like add a new file), its “last modified” date will be updated and this condition will be fulfilled on the next check. In this case, we update the index. This way we can use the index file as input to dmenu and avoid calling find unnecessarily.

It is easy to generalize this script to handle different directories, file extensions and opening programs (movies, images, what have you). This will be left as an exercise for the reader.