Thursday, July 25, 2013

Customizing the mode line: relative path



Some time ago I thought that it might be useful if the mode line of
buffers showed not only the buffer name (which is just the file name
most of the time), but also a piece of information about location of
the file.  For example, relative path with respect to some fixed
ancestor directory.

I found this useful when working on my first Ruby gem (an exercise).
I often had to type M-x pwd to see what file I'm editing.


Implementation

If you're a curious reader who wants to have exhausting information
right now, I send you here. This is GNU info documentation on the
mode line format which Emacs uses to display the mode line (the mode
line is the bottom bar that you see in each buffer).  Certainly, you can
read everything from inside Emacs, with info reader.

So, most buffers use the same mode-line-format, which has complex
structure and allows for deep customization.  Ultimately, it is possible
to assign to mode-line-format anything you want (any valid format).
In this case, you would replace all the functionality which is made
available by the standard format with your one.

We won't go this way, as there are some better alternatives.  Let's look
at the value of this variable:



The thing that we need is the (buffer-local) variable
mode-line-buffer-identification.  By default, it is equal to (#("%12b"
...)), where by "..." I mean some string properties.  The string
literal "%12b" means "buffer name padded with spaces to 12
characters".  The text properties make it possible to switch among
buffers with the mouse and view context help when the mouse is over
buffer name:


So, what we're going to do is to leave the text properties with no
change.  The string itself should be changed to what we need.  Here's
an example of a function which does the trick:


(add-hook 'find-file-hook 'eld-adjust-mode-line-to-show-relative-paths t nil)

(defun eld-adjust-mode-line-to-show-relative-paths ()
  (assert (singletonp mode-line-buffer-identification))
  (let ((rel-name (file-relative-name buffer-file-name
                                      "/path/to/base/directory/")))
    (unless (string-prefix-p ".." rel-name)
      (setq mode-line-buffer-identification
            (list
              (apply #'propertize
                     rel-name
                     (text-properties-at 0 (car mode-line-buffer-identification))))))))

"/path/to/base/directory" is hard-coded, which is okay for the first
version.  The logic, as you see, is very simple: 1) compute relative path
to the base directory (it can be anything), 2) if it does not start
with "..", then we're inside that directory's subtree and should
change mode-line-buffer-identification.  Text propeties are copied into
the new string. (We exploit the fact that they are the same for the whole
string, that's why it's enough to take propeties of the 0th character only).
Otherwise, do nothing.

Our function gets added to the find-file-hook in order to operate when a
new file is opened.


Improvements

Of course, much better solution would be to maintain, say, a list of
directories which are considered to be "base".  Then, in
eld-adjust-mode-line-to-show-relative-paths, we will be checking the
buffer-file-name against each of the directories and will be looking
for the closest one that contains the current file.