2015-03-14 mu4e and human-friendly date format

I’m using the mu4e email client. One of the best things about having my mail moved to Emacs is the ease of tweaking my setup. Since I always have troubles with mental translation between a calendar date and expressions like “today”, “tomorrow”, “yesterday” and so on, I decided that Emacs might help me with that.

The default way mu4e displays the date is the so-called “human date”: time of day for today’s mail and date otherwise. This is not really that great, though: for once, it is not extremely helpful when I check my email just after midnight (which happens a lot), and I don’t have any visual clues about which non-today’s email is “recent”, neither. So I came with this simple hack (note: see below for an update!):

(defsubst mu4e~headers-human-date (msg)
  "Show a 'human' date.
If the date is today or yesterday, show the time, otherwise, show
the date. The formats used for date and time are
`mu4e-headers-date-format' and `mu4e-headers-time-format'."
  (let ((date (mu4e-msg-field msg :date)))
    (if (equal date '(0 0 0))
      "None"
      (let ((day1 (decode-time date))
	     (day2 (decode-time (current-time))))
	(cond ((and
		(eq (nth 3 day1) (nth 3 day2))	;; day
		(eq (nth 4 day1) (nth 4 day2))	;; month
		(eq (nth 5 day1) (nth 5 day2))) ;; year
	       (format-time-string mu4e-headers-time-format date))
	      ((eq (- (time-to-days (current-time)) (time-to-days date)) 1)
	       (format-time-string mu4e-headers-yesterday-time-format date))
	      (t
	       (format-time-string mu4e-headers-date-format date)))))))

(defcustom mu4e-headers-yesterday-time-format "Y-%X"
  "Time format to use in the headers view for yesterday's
messages.  In the format of `format-time-string'."
  :type  'string
  :group 'mu4e-headers)

Now the “human date” displays yesterday’s emails with a timestamp preceded by Y-. (Notice that this is defsubst, so you need to re-evaluate the defuns of all functions calling mu4e~headers-human-date. Happily, there’s only one of them;-).

I’ve been using this now for a few days and I have to say I like it very much. (The only drawback is that it slows down displaying of large email lists, but I usually display at most two days’ worth of mail, and the slowdown is not really noticeable anyway in my experience.)

What I especially like about this is how much time I needed to pull this trick off. Starting with the idea, I needed just 16 minutes (yes, I use Org-mode clocking)! This was first grepping the manual for the :human-date field; then grepping the sources for names of the function which use it; then checking the function responsible for this format; then checking in the Emacs Lisp Reference for functions operating on time data; then thinking for a moment about the implementation (I decided that subtracting the results of time-to-days is easier than manually checking for last/first days of months and years, although this is definitely not the fastest way from the computer point of view); then actually implementing it (abo-abo’s lispy is great, for instance, it allows for instant conversion between cond and (potentially nested) ifs), and rudimentary testing. This is exactly the power of easy customization thanks to source-openness and self-documentingness of Emacs.

Take that, Outlook. Or even Thunderbird. Or even Mutt.

Update: after sharing the above snippet on the mu-discuss mailing list, Dirk-Jan C. Binnema (the author of mu and mu4e) corrected a small and stupid bug (this is already taken care in the code above) and suggested using mu4e’s custom headers. After a short exchange (I had a problem setting it up due to a quoting mistake) the following code emerged:

(defun mu4e~headers-more-human-date (msg)
  "Show a 'more human' date.  If the date is today or yesterday,
show the time, otherwise, show the date. The formats used for
date and time are `mu4e-headers-date-format' and
`mu4e-headers-time-format'."
  (let ((date (mu4e-msg-field msg :date)))
    (if (equal date '(0 0 0))
      "None"
      (let ((day1 (decode-time date))
	     (day2 (decode-time (current-time))))
	(cond ((and
		(eq (nth 3 day1) (nth 3 day2))	;; day
		(eq (nth 4 day1) (nth 4 day2))	;; month
		(eq (nth 5 day1) (nth 5 day2))) ;; year
	       (format-time-string mu4e-headers-time-format date))
	      ((eq (- (time-to-days (current-time)) (time-to-days date)) 1)
	       (format-time-string mu4e-headers-yesterday-time-format date))
	      (t
	       (format-time-string mu4e-headers-date-format date)))))))

(defcustom mu4e-headers-yesterday-time-format "Y-%X"
  "Time format to use in the headers view for yesterday's
messages.  In the format of `format-time-string'."
  :type  'string
  :group 'mu4e-headers)

(add-to-list 'mu4e-header-info-custom
	     '(:more-human-date .
			 (:name "Date"
			  :shortname "Date"
			  :help "Date in even more human-friendly format"
			  :function mu4e~headers-more-human-date)))

(setq mu4e-headers-fields '((:more-human-date . 12)
 (:flags . 6)
 (:mailing-list . 10)
 (:from . 22)
 (:subject)))

As you can see, here we avoid modifying mu4e’s internal function – instead, we copy it and modify the copy – and we don’t lose the possibility of using the original :human-date.

Also, since writing the above paragraph about the speed of my code, I did in fact look into the code of time-to-days. It turns out that my implementation indeed is a very inefficient one – but since it seems to work fast enough, I decided not to change it.

CategoryEnglish, CategoryBlog, CategoryEmacs