Splitting the subject in Gnus summary subject lines
I’ve spent a lot of time using Gnus in Emacs to read Linux development mailing lists in the past few months. Many of the messages are 1 or more patches, and the subject lines start with tags that look like
[PATCH something...] foo bar bletch
It occurred to me that it might be nice to split the subject into the part with the tag and the part that follows, and vertically align the subject without the tag, like this:
[PATCH something...] foo bar bletch
[PATCH other thing...] blah blah blah
So, I set forth to figure out how to do this.
First, the summary line format is controlled by variable
gnus-summary-line-format
. Gnus interprets the various
formatting characters in gnus-summary-line-format
to print
the summary line. None of the formatting characters provided a direct
way to split the subject line. I could have just split the subject
line at a fixed point, but that would often have too much with the tag
part, or perhaps, with the case of no tag at all, some portion of the
subject.
I needed a way to call an Emacs Lisp function to split the subject string for me. Fortunately, the Gnus documentation in section 4.1.1 Summary Buffer Lines documents ‘u’ as a user defined specifier.
‘u’
User defined specifier. The next character in the format string
should be a letter. Gnus will call the function
‘gnus-user-format-function-X’, where X is the letter following
‘%u’. The function will be passed the current header as argument.
The function should return a string, which will be inserted into
the summary just like information from any other summary specifier.
I used ‘t’ for tag and ‘s’ for subject. So I had to use “%ut %us” to get the pieces I wanted.
First, I needed to define gnus-user-format-function-t
and gnus-user-format-function-s
, and they should take
the header as the argument. I assumed this meant the subject header,
but that turned out to be wrong. I think that documentation should
says “headers”, as the functions were passed a vector of information.
More on this in a bit.
My first attempt to at gnus-user-format-function-t
assumed the header passed in was the subject header. This was myopic
on my part, because a user defined header needn’t be limited to what
I’m interested in. At any rate, I first did something like this for
gnus-user-format-function-t
:
(defun gnus-user-format-function-t (subject)
"Return tag from subject"
(let
((cb (string-match "]" subject)))
(if cb
(substring subject 0 (1+ cb))
nil)
)
)
Then I replaced “%s” in gnus-summary-line-format
with
“%ut”. On entering a group, I got an error. I set the “Enter debugger
on error” option and tried again, and noted that the complaint was
that subject wasn’t a string. Hmm. On examination, I saw a vector on
information as the parameter, and the subject header was in
position 1. So try 2 was this:
(defun gnus-user-format-function-t (headers)
"Return tag from subject"
(let
((subject (aref headers 1))
(cb (string-match "]" subject)))
(if cb
(substring subject 0 (1+ cb))
nil)
)
)
Okay, that’s closer, but my next error was that
subject
wasn’t defined. Looking at the definition of
let
, it says this:
let is a special-form in ‘C source code’.
(let VARLIST BODY...)
Bind variables according to VARLIST then eval BODY.
The value of the last form in BODY is returned.
Each element of VARLIST is a symbol (which is bound to nil)
or a list (SYMBOL VALUEFORM) (which binds SYMBOL to the value of VALUEFORM).
All the VALUEFORMs are evalled before any symbols are bound.
Oh, so I can use subject in the body, but not in the VALUEFORM’s. Got it.
Next version was good, at first:
(defun gnus-user-format-function-t (headers)
"Return tag from subject"
(let
((subject (aref headers 1))
(cb (string-match "]" (aref headers 1))))
(if cb
(substring subject 0 (1+ cb))
nil)
)
)
This worked great on subjects that had tags. But on subjects without a
tag, it returned nil
, and the formatter barfed trying
to format nil
as a 40 character string. Oops.
So, final version is
(defun gnus-user-format-function-t (headers)
"Return tag from subject"
(let
((subject (aref headers 1))
(cb (string-match "]" (aref headers 1))))
(if cb
(substring subject 0 (1+ cb))
"")
)
)
gnus-user-format-function-s
is similar, but gets the
untagged subject:
(defun gnus-user-format-function-s (headers)
"Return subject - tag"
(let
((subject (aref headers 1))
(cb (string-match "]" (aref headers 1))))
(if cb
(substring subject (+ cb 2))
subject
)
)
)
Finally, I updated gnus-summary-line-format
in the
use-package gnus
part of config.org:
(gnus-summary-line-format "%U%R%4i %11&user-date; %I%(%[%4L: %-23,23f%]%) %-40,40ut %us\12")
This says use 40 characters for the tag, left-justified and filled on the right with blanks, followed by a space, then the untagged subject.
I also defined gnus-user-format-function-s
and
gnus-user-format-function-t
in the :init section of
use-package gnus
.
Follow up
On Mastodon, Omar Antolin pointed out I could use let*
rather than let
, and then use subject
when assigning cb
. I’ve done that. It works, is more
legible and maintainable, and likely more efficient.