pmdm - a poor’s man desktop-mode replacement for Emacs

../../../_images/emacs-logo.jpeg

As usual, much time has elapsed without updating the blog. I had some ideas in my head but was too lazy to write anything.

Anyway, I come back today with possibly my preferred software application… want to guess? Linux? GNOME? Firefox? even my own lfm? Well, the only application which really matters!

In fact, as time goes by, I appreciate emacs more and more. And as I’ve mentioned somewhere, I can spend a great part of my decreasing spare and idle hours tweaking here or there in my .emacs file configuration and testing new modules - MELPA, so much pain have you inflicted.

Ok, less ramblings and let’s go into detail.

Problems with emacs daemon, desktop-mode, and perspective

I’m using emacs --daemon since a couple of years ago. emacs starts with my GNOME graphical session (nowdays as user systemd service) and ends with it. Very convenient.

I also used desktop-mode, which can save your opened files, frames sizes and much more at emacs session ending and recover all of them the next time you start emacs. It worked more or less for me with emacs daemon mode.

But desktop-mode definitively stopped working when I added perspective to my configuration. [Copying project page description: “perspective provides tagged workspaces in Emacs”.]

Usually this wouldn’t be a serious inconvenient as I use helm-mini (or helm-recentf) to open recent files quickly. But… hey! this is emacs, let’s write a new and superfluous package (and learn in the process)!

Writing pmdm, a simple replacement for saving and restoring opened files

I need to confess that I don’t know much about emacs-lisp, in fact the only language I can consider myself proficient enough nowdays is python, but coding simple stuff in elisp (or haskell, btw) is a good way to relax myself for a couple of hours.

Ok, so we need to write a function to get all buffers containing live files and save their name onto a file. Then, another function to read the stored file names and open them.

Sound simple, uhm?

Let’s start step by step.

Getting the name of opened files

We can get all opened buffers with buffer-list, which returns a list (of course, this is emacs-lisp, there are lists everywhere ;). And buffer-file-name can be used to get the name of the file for the buffer.

Let’s run these functions interactively using ielm and show their output:

ELISP> (mapcar 'buffer-file-name (buffer-list))
(nil "/home/inigo/devel/web.inigo/blog/drafts/pmdm_a_poor_s_man_desktop_mode_replacement_for_emacs.rst" "/home/inigo/devel/emacs/pmdm/pmdm.el" nil nil nil nil nil "/home/inigo/.emacsmine/my-dotemacs.el" nil "/home/inigo/personal/agenda/todo.org" nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil nil "/home/inigo/personal/agenda/diary" nil nil nil nil nil nil nil nil)

Lots of buffers. Those nil elements correspond to buffers with no file, so we need to filter them out:

ELISP> (delq nil (mapcar 'buffer-file-name (buffer-list)))
("/home/inigo/personal/agenda/todo.org" "/home/inigo/devel/web.inigo/blog/drafts/pmdm_a_poor_s_man_desktop_mode_replacement_for_emacs.rst" "/home/inigo/devel/emacs/pmdm/pmdm.el" "/home/inigo/.emacsmine/my-dotemacs.el")

Much better.

Writing the list to a file

Now we have to save this list into a file.

Diving into elisp documentation we can find a write-region function with this syntax:

(write-region START END FILENAME &optional APPEND VISIT LOCKNAME MUSTBENEW)

it looks like it works with current buffer, so we could create a temporary buffer, insert the list as a string (prin1-to-string), select the text and save it finally.

It’s not very clear from the definition above, but reading the description carefully we noted the function can also be used by passing a string as the first parameter, so we don’t need the buffer!

(write-region TEXT nil FILENAME &optional APPEND VISIT LOCKNAME MUSTBENEW)

We also add some comments to the file in case someone read it. We use format to format and concatenate the text to be written.

And now we have our first function finished!

(defun pmdm/write-opened-files()
  (interactive)
  (let ((files (delq nil (mapcar 'buffer-file-name (buffer-list)))))
    (write-region (format ";; PMDM file.\n;; Please do not edit manually.\n%s"
                          (prin1-to-string files))
                  nil
                  "~/.emacs.d/.pmdm-files")))

Reading it back

In Emacs, the best way to read a file content is to insert it in a temporary file. Something as fast as:

(with-temp-buffer
  (insert-file-contents "~/.emacs.d/.pmdm-files")

We can delete the comments in the file with:

(delete-matching-lines "^;; ")

and get the list with our files to open:

(buffer-substring-no-properties (point-min) (point-max))

Wow, very easy… but there is problem here… what we have is the string representation of the list, not the list itself!

After half an hour thinking and searching internet the solution was simple, the read function can cast the string into a list.

This is how the finished function looks like:

(defun pmdm~read-files-list ()
  (with-temp-buffer
    (insert-file-contents "~/.emacs.d/.pmdm-files")
    (delete-matching-lines "^;; ")
    (read (buffer-substring-no-properties (point-min) (point-max)))))

[Please consider the security implications of blindly reading a file from the file system and using the contents without any check.]

And finally, visit the files

We loop the files list returned by previous function with dolist and call find-file-noselect to open them in the background.

(dolist (file (pmdm~read-files-list))
  (find-file-noselect file))

Though in emacs it’s harmless trying to load an already opened file, it’s better to check and avoid it. We know how to get a list of currently opened files so for our new file we will check if it is present in the already-opened-files list.

(let ((opened-files (delq nil (mapcar 'buffer-file-name (buffer-list))))
      (files (pmdm~read-files-list)))
  (dolist (file files)
    (unless (member file opened-files)
      (find-file-noselect file))))

We can enhance the function displaying how many files we have opened, so we add a variable that we’ll increment for each new opened file.

(defun pmdm/load-files ()
  (interactive)
  (let ((opened-files (delq nil (mapcar 'buffer-file-name (buffer-list))))
        (files (pmdm~read-files-list))
        (count 0))
    (dolist (file files)
      (unless (member file opened-files)
        (find-file-noselect file)
        (setq count (1+ count))))
    (message (if (zerop count)
                 "No files opened"
               (format "%d file%s opened" count (if (> count 1) "s" ""))))))

Final thoughts

Add functions comments, use of defvar instead of hard-coding file name and voilà, we have the final version of pmdm.el.

I know/suppose there are a lot of better implementations of this same idea out here. But using them would be less fun than coding it ourselves!