Category Archives: Software

Speeding Up Emacs and Parsing Emacs Lisp from Emacs Lisp

I recently spent a bit of time to clean up all the cruft that my ~/.emacs file and my ~/elisp directory had accumulated. I have been using a multi-file setup to configure my Emacs sessions, since at least 2008. This turned out to be a royal mess after 5+ years of patching stuff without a very clear plan or structure. The total line-count of both my ~/.emacs and all the *.el files I had imported into my ~/elisp directory was almost 20,000 lines of code:

$ wc -l BACKUP/.emacs $( find BACKUP/elisp -name '*.el')
   119 BACKUP/.emacs
    84 BACKUP/elisp/keramida-w3m.el
    90 BACKUP/elisp/keramida-keys.el
   156 BACKUP/elisp/keramida-irc.el
  5449 BACKUP/elisp/erlang.el
   892 BACKUP/elisp/fill-column-indicator.el
   344 BACKUP/elisp/keramida-erc.el
    87 BACKUP/elisp/keramida-chrome.el
    89 BACKUP/elisp/keramida-autoload.el
   141 BACKUP/elisp/keramida-ui.el
    42 BACKUP/elisp/keramida-slime.el
  1082 BACKUP/elisp/ace-jump-mode.el
     2 BACKUP/elisp/scala-mode2/scala-mode2-pkg.el
   907 BACKUP/elisp/scala-mode2/scala-mode2-indent.el
    26 BACKUP/elisp/scala-mode2/scala-mode2-lib.el
   502 BACKUP/elisp/scala-mode2/scala-mode2-fontlock.el
    37 BACKUP/elisp/scala-mode2/scala-mode2-map.el
   808 BACKUP/elisp/scala-mode2/scala-mode2-syntax.el
   111 BACKUP/elisp/scala-mode2/scala-mode2.el
   121 BACKUP/elisp/scala-mode2/scala-mode2-paragraph.el
  1103 BACKUP/elisp/php-mode.el
   142 BACKUP/elisp/themes/cobalt-theme.el
   665 BACKUP/elisp/themes/zenburn-theme.el
   142 BACKUP/elisp/themes/sublime-themes/cobalt-theme.el
    80 BACKUP/elisp/themes/tomorrow-night-blue-theme.el
    80 BACKUP/elisp/themes/tomorrow-night-eighties-theme.el
   115 BACKUP/elisp/themes/tomorrow-theme.el
    80 BACKUP/elisp/themes/tomorrow-night-bright-theme.el
   339 BACKUP/elisp/cmake-mode.el
    95 BACKUP/elisp/keramida-cc-extra.el
  1341 BACKUP/elisp/lua-mode.el
  2324 BACKUP/elisp/markdown-mode.el
   184 BACKUP/elisp/rcirc-notify.el
   167 BACKUP/elisp/keramida-defaults.el
   203 BACKUP/elisp/keramida-hooks.el
    43 BACKUP/elisp/keramida-lang.el
   435 BACKUP/elisp/edit-server.el
   709 BACKUP/elisp/slang-mode.el
    66 BACKUP/elisp/keramida-eshell.el
 19402 total

20,000 lines of code is far too much bloat. It’s obvious that this was getting out of hand, especially if you consider that I had full configuration files for at least two different IRC clients (rcirc and erc) in this ever growing blob of complexity.

What I did was make a backup copy of everything in ~/BACKUP and start over. This time I decided to go a different route from 2008 though. All my configuration lives in a single file, in ~/.emacs, and I threw away any library from my old ~/elisp tree which I haven’t actively used in the past few weeks. I imported the rest of them into the standard user-emacs-directory of modern Emacsen: at ~/.emacs.d/. I also started using eval-after-load pretty extensively, to speed up the startup of Emacs, and only configure extras after the related packages are loaded. This means I could trim down the list of preloaded packages even more.

The result, as I tweeted yesterday was an impressive speedup of the entire startup process of Emacs. Now it can start, load everything and print a message in approximately 0.028 seconds, which is more than 53 times faster than the ~1.5 seconds it required before the cleanup!

I suspected that the main contributor to this speedup was the increased use of eval-after-load forms, but what percentage of the entire file used them?

So I wrote a tiny bit of Emacs Lisp to count how many times each top-level forms appears in my new ~/.emacs file:

(defun file-forms-list (file-name)
  (let ((file-forms nil))
    ;; Keep reading Lisp expressions, until we hit EOF,
    ;; and just add one entry for each toplevel form
    ;; to `file-forms'.
    (condition-case err
          (insert-file file-name)
          (goto-char (point-min))
          (while (< (point) (point-max))
            (let* ((expr (read (current-buffer)))
                   (form (first expr)))
              (setq file-forms (cons form file-forms)))))
      (end-of-file nil))
    (reverse file-forms)))

(defun file-forms-alist (file-name)
  (let ((forms-table (make-hash-table :test #'equal)))
    ;; Build a hash that maps form-name => count for all the
    ;; top-level forms of the `file-name' file.
    (dolist (form (file-forms-list file-name))
      (let ((form-name (format "%s" form)))
        (puthash form-name (1+ (gethash form-name forms-table 0))
    ;; Convert the hash table to an alist of the form:
    ;;    ((form-name . count) (form-name-2 . count-2) ...)
    (let ((forms-alist nil))
      (maphash (lambda (form-name form-count)
                 (setq forms-alist (cons (cons form-name form-count)

  (insert "\n")
  (insert (format "%7s %s\n" "COUNT" "FORM-NAME"))
  (let ((total-forms 0))
    (dolist (fc (sort (file-forms-alist "~/.emacs")
                      (lambda (left right)
                        (> (cdr left) (cdr right)))))
      (insert (format "%7d %s\n" (cdr fc) (car fc)))
      (setq total-forms (+ total-forms (cdr fc))))
    (insert (format "%7d %s\n" total-forms "TOTAL"))))

Evaluating this in a scratch buffer shows output like this:

   32 setq-default
   24 eval-after-load
   14 set-face-attribute
   14 global-set-key
    5 autoload
    4 require
    4 setq
    4 put
    3 defun
    2 when
    1 add-hook
    1 let
    1 set-display-table-slot
    1 fset
    1 tool-bar-mode
    1 scroll-bar-mode
    1 menu-bar-mode
    1 ido-mode
    1 global-hl-line-mode
    1 show-paren-mode
    1 iswitchb-mode
    1 global-font-lock-mode
    1 cua-mode
    1 column-number-mode
    1 add-to-list
    1 prefer-coding-system
  122 TOTAL

This showed that I’m still using a lot of setq-default forms: 26.23% of the top-level forms are of this type. Some of these may still be candidates for lazy initialization, since I can see that many of them are indeed mode-specific, like these two:

(setq-default diff-switches "-u")
(setq-default ps-font-size '(8 . 10))

But eval-after-load is a close second, with 19.67% of all the top-level forms. That seems to agree with the original idea of speeding up the startup of everything by delaying package-loading and configuration until it’s actually needed.

10 of the remaining forms are one-off mode setting calls, like (tool-bar-mode -1), so 8.2% of the total calls is probably going to stay this way for a long time. That’s probably ok though, since the list includes several features I find really useful, very very often.

Saving Space in Rsnapshot Archives with Hardlinks

The hardlink package is quite handy on Linux systems. It appears to do a nice job saving disk space in my local rsnapshot archives (25 GB reported as “saved” after today’s run on a week of daily snapshots). The stress put on my external USB backup disk wasn’t too extreme either, as reported by Munin’s IO/sec graphs for /dev/sdc:

IO/sec graph for a USB disk, while hardlink was running on a collection of rsnapshot daily archives.

IO/sec graph for a USB disk, while hardlink was running on a collection of rsnapshot daily archives.

That’s a lot of read operations, and then a percentage of them converted back to writes, when hardlink(1) discovers duplicate files and replaces them with hard links. Which is exactly the sort of behavior we’d expect from this sort of thing.

The hardlink invocation that triggered these I/O operation was quite mundane:

# time hardlink -f -m -v *home.*
[output stats ellided]
        1380.243 real   17.525 user     47.975 sys

Having run for a little over 23m it managed to hardlink enough files in my daily laptop backup snapshots to save 25 GB of disk space. I think it was worth the time vs. space trade-off :-)

Saving & Restoring Mate Terminal’s Color-Profile Information

Terminal palettes come in infinite variations. Almost everyone has a “favorite” palette, and if you are like me, you probably hate losing all your finely tuned terminal colors, because you wanted to experiment with this new color scheme and forgot to save your original color profile. The script I’m about to describe can save all color-related information for mate-terminal, and can be easily adapted to work at least for gnome-terminal in GNOME 2.X desktops.

The GUI-Based Way of Saving Profile State

One way to save your current color preferences, along with everything else related to the current terminal profile, is to create a full copy of your mate-terminal profile. This can be done through the “Profiles” dialog of mate-terminal. First you navigate to the “Edit ▸ Profiles” menu entry:

Profile menu of mate-terminal

Profile menu of mate-terminal

Then you create a new profile, based on your current one:

Profile dialog of mate-terminal

Profile dialog of mate-terminal

This way you can mess with any of the two profiles, either the original or the new copy, without worrying that you will lose any important settings.

The Scripted Way of Saving Color Settings

The good aspect of having a GUI to save your profile state is that it’s accessible to everyone, and very easy to use. One of the disadvantages is that your settings are all stored in your config database, which also contains a gazillion other options and is not necessarily easy to backup and restore in one step. A simple search for ‘delete your “.config” directory gnome‘ yields many web pages where people recommend deleting the entire ~/.config directory and starting over. This makes me feel rather cautious about depending on always having the full contents of .config around, so I started looking for an alternative way of saving mate-terminal color information: one that I can reliably script myself; one that stores results in a plain text file that I can read easily, fast and with any tool.

So I wrote the shell script shown below. The main idea behind the script is that it should be possible to run a single command and get as output a set of mateconftool-2 commands that will instantly restore my color settings. This way I can save my color profile information by e.g. typing: >

Then if I mess with my color settings, I can just run the resulting script to restore them:


Finding the right mate configuration database keys was not very hard. I originally saw a shell script that tweaks gconf2 database keys when I was experimenting with the solarized color theme for gnome-terminal. There is a nice set of shell scripts and palettes at Github, created by Sigurd Gartmann, that contains a full set of gconf2 keys for gnome-terminal’s color information, as part of its installed scripts. The keys are listed in the and shell scripts of the gnome-terminal-colors-solarized project.

Adapting the keys and wrapping them in a bit of shell script code, I came up with the following script.

Note: The shell script is also available online, as part of my “miscellaneous scripts” collection, at:


# Copyright (C) 2013, Giorgos Keramidas <>
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.

# Dump an executable form of all the mate-terminal keys associated with
# colors.  Running the resulting script should restore all color-related
# state of the terminal to whatever it was at the time the script was
# created.
# Inspired by:
# The setup scripts of solarized theme for gnome-terminal from:

# ----- startup code ---------------------------------------------------

# Save the original program invocation name, and the real path of the
# startup directory, for later use.
progdir=$( cd $(dirname "$0") ; /bin/pwd -P )
progname=$( basename "$0" )

# ----- misc functions -------------------------------------------------

# err exitval message
#   Display message to stderr and to the logfile, if any, and then
#   exit with exitval as the return code of the script.

    log "$0: ERROR: $*"
    exit $exitval

# warn message
#   Display message to stderr and the log file.
    log "$0: WARNING: $*"

# info message
#   Display informational message to stderr and to the logfile.
    log "$0: INFO: $*"

# debug message
#   Output message to stderr if debug_output_enabled is set to
#   'yes', 'true' or '1'.  Please AVOID calling any shell subroutine
#   that may recursively call debug().
    case ${debug_enabled} in
        log "$0: DEBUG: $*"

# log message
#   Print a log message to standard error.  If ${LOGFILE} is set
#   Output message to "${LOGFILE}" if it is set and is writable.
    __timestamp="`date -u '+%Y-%m-%d %H:%M:%S'`"
    __msg="${__timestamp} [${progname}] -- $*"
    echo >&2 "${__msg}" 2>&1
    if [ -n "${LOGFILE}" ]; then
        echo "${__msg}" >> "${LOGFILE}"

# ----- main script body ------------------------------------------------

# The gconf-compatible tool to use for reading and writing gconf keys
# for the MATE desktop, and the application name under /apps/ to
# configure.  These are provisionaly set to work for the MATE desktop,
# but they can also be tweaked to work for GNOME 2.X by setting:
#   conftool='gconftool-2'
#   appname='gnome-terminal'


# Basic command-line sanity checking.
if test $# -ne 0 && test $# -ne 1 ; then
    echo >&2 "usage: ${progname} [ PROFILE ]"
    exit 1

# The name of the profile we are dumping can be passed as a command line
# argument, or auto-detected by peeking at:
# '/apps/${appname}/global/default_profile'
if test $# -eq 1 ; then
    profile=$( ${conftool} --get "${key}" 2>/dev/null )
    if test $? -ne 0 ; then
        debug "Cannot read configuration key: ${key}"
        err 1 "Cannot detect default profile name."
    unset key

# Verify that the profile we are looking for really exists, by trying to
# read at least one key from it:
# '/apps/${appname}/profiles/${profile}/foreground_color'
${conftool} --get "${key}" > /dev/null 2>&1
if test $? -ne 0 ; then
    debug "Cannot read configuration key: ${key}"
    err 1 "Profile ${profile} cannot be found."
unset key

# dumpkey TYPE KEY
#   Dump a configuration key to standard output, as a shell command that
#   will _set_ it to its current value, using the associated type.
    if test $# -ne 2 || test -z "$1" || test -z "$2" ; then
        debug "dumpkey() requires exactly 2 non-empty arguments,"
        debug "but it was invoked with:"
        debug "    \"$1\""
        debug "    \"$2\""
        return 1

    __value=$( ${conftool} --get "${__key}" )
    if test $? -ne 0 ; then
        err 1 "Cannot read key \"${__key}\""
    echo "${conftool} --set --type \"${__type}\""                       \
        "\"${__key}\" \"${__value}\""

dumpkey "string" "/apps/${appname}/profiles/${profile}/background_color"
dumpkey "string" "/apps/${appname}/profiles/${profile}/bold_color"
dumpkey "bool"   "/apps/${appname}/profiles/${profile}/bold_color_same_as_fg"
dumpkey "string" "/apps/${appname}/profiles/${profile}/foreground_color"
dumpkey "string" "/apps/${appname}/profiles/${profile}/palette"

Using The Script

Using the script should be pretty easy to discern by now, but here’s a sample run from my laptop:

$ sh 
mateconftool-2 --set --type "string" "/apps/mate-terminal/profiles/Default/background_color" "#000000000000"
mateconftool-2 --set --type "string" "/apps/mate-terminal/profiles/Default/bold_color" "#000000000000"
mateconftool-2 --set --type "bool" "/apps/mate-terminal/profiles/Default/bold_color_same_as_fg" "true"
mateconftool-2 --set --type "string" "/apps/mate-terminal/profiles/Default/foreground_color" "#E8E8E8E8ECEC"
mateconftool-2 --set --type "string" "/apps/mate-terminal/profiles/Default/palette" "#000000000000:#D7D700000000:#5F5F87870000:#CFCFA7A70000:#26268B8BD2D2:#ADAD7F7FA8A8:#2A2AB1B1A8A8:#D3D3D7D7CFCF:#555557575353:#DCDC32322F2F:#9595C9C90000:#F5F5D9D90000:#00008787FFFF:#CDCD9F9FC8C8:#4A4AE1E1D8D8:#EEEEEEEEECEC"

The color values are slurped directly from the MATE desktop’s configuration database entries for mate-terminal. Redirecting this output to a plain text file yields a nice, compact sh(1)-compatible script, which restores the colors of the “Default” mate-terminal profile to the values of the script, i.e. the color setup I was using when this color profile was saved:

# Save the current color settings.
sh > ~/

# Some time later... Restore them once more.
sh ~/

Fixing Shifted-Arrow Keys in 256-Color Terminals on Linux

The terminfo entry for “xterm-256color” that ships by default as part of ncurses-base on Debian Linux and its derivatives is a bit annoying. In particular, shifted up-arrow key presses work fine in some programs, but fail in others. It’s a bit of a gamble if Shift-Up works in joe, pico, vim, emacs, mutt, slrn, or what have you.

THis afternoon I got bored enough of losing my selected region in Emacs, because I forgot that I was typing in a terminal launched by a Linux desktop. SO I thought “what the heck… let’s give the FreeBSD termcap entry for xterm-256color a try”:

keramida> scp bsd:/etc/termcap /tmp/termcap-bsd
keramida> captoinfo -e $(                                  \
  echo $( grep '^xterm' termcap | sed -e 's/[:|].*//' ) |  \
  sed -e 's/ /,/g'                                         \
  ) /tmp/termcap  > /tmp/terminfo.src
keramida> tic /tmp/terminfo.src

Restarted my terminal, and quite unsurprisingly, the problem of Shift-Up keys was gone.

The broken xterm-256color terminfo entry from /lib/terminfo/x/xterm-256color is now shadowed by ~/.terminfo/x/xterm-256color, and I can happily keep typing without having to worry about losing mental state because of this annoying little misfeature of Linux terminfo entries.

The official terminfo database sources[1], also work fine. So now I think some extra digging is required to see what ncurses-base ships with. There’s definitely something broken in the terminfo entry of ncurses-base, but it will be nice to know which terminal capabilities the Linux package botched.


Mate desktop support for xdg-open (tiny patch)

The Mate desktop really lives up to its promise of a “traditional desktop”, compatible in look and feel with Gnome 2.X versions.

After running it for a few hours now, I’m sold. I never really liked Unity very much, so Mate feels like ‘home’.

There are a few minor things that need tweaking though, mostly because the desktop is no longer called ‘Gnome’. One of them is the default xdg-open script that ships with the xdg-utils package. The current version of the script assumes that the desktop is called one of ‘KDE’, ‘LXDE’, ‘gnome’ or something else. The detectDE() function fails to recognize Mate, because it tries to look for GNOME_DESKTOP_SESSION_ID in its environment, and this is obviously no longer there for Mate.

The following patch fixes that, by checking for the string 'mate' in DESKTOP_SESSION instead, and adding a tiny bit of dispatch code based on DE='mate' further down, to call a new open_mate() function.

--- xdg-open.orig	2013-03-18 12:39:07.955516232 +0100
+++ xdg-open	2013-03-18 12:38:45.955515638 +0100
@@ -308,6 +308,7 @@
     elif `dbus-send --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.GetNameOwner string:org.gnome.SessionManager > /dev/null 2>&1` ; then DE=gnome;
     elif xprop -root _DT_SAVE_MODE 2> /dev/null | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce;
     elif [ x"$DESKTOP_SESSION" = x"LXDE" ]; then DE=lxde;
+    elif [ x"$DESKTOP_SESSION" = x'mate' ]; then DE=mate;
     else DE=""
@@ -371,6 +372,21 @@
+    if gvfs-open --help 2>/dev/null 1>&2; then
+        gvfs-open "$1"
+    else
+        mate-open "$1"
+    fi
+    if [ $? -eq 0 ]; then
+        exit_success
+    else
+        exit_failure_operation_failed
+    fi
     exo-open "$1"
@@ -545,6 +561,10 @@
     open_gnome "$url"
+    mate)
+    open_mate "$url"
+    ;;
     open_xfce "$url"

Now this is a tiny problem, but it’s one that improves the usability of Mate for every-day stuff. One of the things that this fixes it the handling of magnet: links in Chrome. When Chrome tries to open magnet: links without this patch, it seems to “do nothing” but open a new tab and stop. With this patch applied, xdg-open works fine for magnet links (depending on your current MIME application settings too, of course), and Chrome happily opens torrent links in Mate desktops without a glitch.

Automatically Joining Password-Enabled Channels in ERC

If, like me, you are using ERC to chat with your IRC friends all over the world, then the next few paragraphs describe how you can set up automatically joining channels in a secure manner, even if the channels require a key.

Being able to auto-join channels after connecting to an IRC server is a must have for any serious IRC client. This way the user doesn’t have to laboriously type /join commands again and again, every time the IRC client start, or when there’s a network problem and the IRC server temporarily disconnects. Typing long lists of channel names gets old really fast if one has to type stuff like this for example:

/join #public
/join #secret somekeyword
/join #interest
/join #hobby hobbykey

Most serious IRC clients have some form of ‘autojoin’ feature, that lets you configure the channels they should join when connected to a specific IRC network or to a specific IRC server. ERC supports auto-joining with a loadable module called erc-join. You can load this module in your ERC startup code, and then use erc-autojoin-channels-alist to specify which channels to join on a per-server basis.

Loading the erc-join module and enabling auto-join support is the easy bit:

(require 'erc-join)             ; autojoin support is implemented by erc-join.el
(erc-autojoin-enable)           ; enable channel autojoin support, by default

Setting up the list of channels in erc-autojoin-channel-alist is also easy, but it does lend itself to a bit of upfront explanation. When auto-join code looks at erc-autojoin-channel-alist it expects to find there a list of ‘server patterns’ and their associated channels. So its value should look like this:


When a connection to a new IRC server is established, erc-join has set things up so that erc-autojoin-channels is called. The erc-autojoin-channels function iterates through erc-autojoin-channels-alist and checks if the server pattern matches the current server-name. If it does, it calls erc-server-join-channel for each of the channels attached to this server pattern.

The server pattern can be a regular expression and pattern matching for the ‘server’ part of each erc-autojoin-channels-alist item does not stop when it finds a match. This means that you can mix and match server patterns in all sorts of amusing ways, e.g. to set up a channel that is automatically joined for all IRC networks, by having an erc-autojoin-channels-alist entry whose server pattern is ".*":

(setq erc-autojoin-channels-alist
      '((".*" . "#everywhere")))

Now, for the interesting part. What happens when you want to automatically join channels that require a key. That’s where the real magic behind erc-server-join-channel kicks in, and lets you store your passwords in a secure place, like e.g. a PGP-encrypted ~/.authinfo file in your home directory!

Every time erc-server-join-channel is called it tries to look up a matching entry for the current IRC server and channel names in what Emacs calls “authentication sources”. These are, basically, files in ‘netrc’ file format (traditionally used by FTP clients). If a matching entry is found for the current combination of server and channel in one of the configured netrc files, the password key of this netrc file entry is used as the key for joining the IRC channel. The service name used for netrc entry lookup is also conveniently set to “irc”, so that ERC will not pick up unrelated passwords from your netrc files.

A valid netrc file entry, one which ERC can use to locate the key for an IRC channel, looks like this:

machine login "#mychannel" password "supersecret" port irc

If this line is present in your ~/.authinfo or ~/.netrc file, and one of the erc-autojoin-channels-alist entries specifies that “#mychannel” should be automatically joined when connected to the IRC server “”, then ERC will use “supersecret” as the key for joining the channel. This is a much safer alternative than typing the channel passwords yourself at an ERC prompt (because the channel passwords may then be logged on disk, if you have enabled server-buffer logging), and it’s definitely safer than listing the password in your ~/.emacs startup code or other place that can be world-readable.

Putting it All Together

To summarize, configuring ERC to automatically join channels “#public” and “#secret” when connecting to IRC server “”, and to use the secret key “supersecret” for the “#secret” channel, you can put the following in your ERC startup code:

(require 'erc-join)

(setq erc-autojoin-channels-alist
      '(("" . "#public" "#secret"))))

and the following in your ~/.authinfo or ~/.netrc file:

machine login "#secret" password "supersecret" port irc

Mutt-like Scrolling for Gnus

Mutt scrolls the index of email folders up or down, one line at a time, with the press of a single key: ‘<‘ or ‘>’. This is a very convenient way to skim through email folder listings, so I wrote a small bit of Emacs Lisp to do the same in Gnus tonight.

;; Scrolling like mutt for group, summary, and article buffers.
;; Being able to scroll the current buffer view by one line with a
;; single key, rather than having to guess a random number and recenter
;; with `C-u NUM C-l' is _very_ convenient.  Mutt binds scrolling by one
;; line to '<' and '>', and it's something I often miss when working
;; with Gnus buffers.  Thanks to the practically infinite customizability
;; of Gnus, this doesn't have to be an annoyance anymore.

(defun keramida-mutt-like-scrolling ()
  "Set up '<' and '>' keys to scroll down/up one line, like mutt."
  ;; mutt-like scrolling of summary buffers with '<' and '>' keys.
  (local-set-key (kbd ">") 'scroll-up-line)
  (local-set-key (kbd "<") 'scroll-down-line))

(add-hook 'gnus-group-mode-hook 'keramida-mutt-like-scrolling)
(add-hook 'gnus-summary-mode-hook 'keramida-mutt-like-scrolling)
(add-hook 'gnus-article-prepare-hook 'keramida-mutt-like-scrolling)

This is now the latest addition to my ~/.gnus startup code, and we’re one step closer to making Gnus behave like my favorite old-time mailer.