Mutt, the Vim Way

I’ve recently made the switch to managing my email from the terminal, and settled on mutt as my client of choice. Luckily, it’s well-documented, but like any fully-featured terminal utility, it still takes some time to find your way around. Below are the customizations I’ve made to my muttrc to make things as ‘vimmy’ as possible.

But First: The Unfixable

In vim, you can usually rely on <Esc> / <C-c> / <C-[> to get back to home base if you ever unwittingly back yourself into a corner.

In mutt, the corresponding command is <C-g>. There is no way to remap other keys to this function, so it’s just one little nugget of emacs influence you’re going to have to get used to.

My muttrc

Or at least, the part that makes it vimmy:

# Navigation
# ----------------------------------------------------

bind generic             z         noop
bind index,pager,attach  g         noop
bind index,pager         d         noop
bind index,pager         s         noop
bind index,pager         c         noop
bind generic,pager       t         noop

bind generic,index,pager \Cf       next-page
bind generic,index,pager \Cb       previous-page
bind generic             gg        first-entry
bind generic,index       G         last-entry
bind pager               gg        top
bind pager               G         bottom
bind generic,pager       \Cy       previous-line
bind generic,index,pager \Ce       next-line
bind generic,index,pager \Cd       half-down
bind generic,index,pager \Cu       half-up
bind generic             zt        current-top
bind generic             zz        current-middle
bind generic             zb        current-bottom
bind index               za        collapse-thread
bind index               zA        collapse-all
bind index,pager         N         search-opposite
bind index               <Backtab> previous-new-then-unread

# Go to folder...
macro index,pager gi "<change-folder>=INBOX<enter>"       "open inbox"
macro index,pager gd "<change-folder>=Drafts<enter>"      "open drafts"
macro index,pager gs "<change-folder>=Sent<enter>"        "open sent"
macro index,pager gt "<change-folder>$trash<enter>"       "open trash"
macro index,pager gf "<change-folder>?"                   "open mailbox..."

# Actions
# ----------------------------------------------------

bind  index,pager    a   group-reply
macro index,pager    dd  "<delete-message><sync-mailbox>"                                 "move message to trash"
macro index,pager    dat "<delete-thread><sync-mailbox>"                                  "move thread to trash"
macro index,pager    ss  ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015<save-message>?"                                                                                                                                     "save message to a mailbox"
macro index          sat ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015'q<untag-pattern>.\\015\"\015<mark-message>q<enter><untag-pattern>.<enter><tag-thread><tag-prefix-cond><save-message>?"                                    "save thread to a mailbox"
macro index          \;s ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015<tag-prefix-cond><save-message>?"                                                                                                                    "save tagged messages to a mailbox"
macro pager          sat ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015<display-message>\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015'q<untag-pattern>.\\015<display-message>\"\015<exit><mark-message>q<enter><untag-pattern>.<enter><tag-thread><tag-prefix><save-message>?" "save thread to a mailbox"
macro index,pager    cc  ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015<copy-message>?"                                                                                                                                     "copy message to a mailbox"
macro index          cat ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015'q<untag-pattern>.\\015\"\015<mark-message>q<enter><untag-pattern>.<enter><tag-thread><tag-prefix-cond><copy-message>?"                                    "copy thread to a mailbox"
macro index          \;c ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015\"\015<tag-prefix-cond><copy-message>?"                                                                                                                    "copy tagged messages to a mailbox"
macro pager          cat ":macro browser \\015 \"\<select-entry\>\<sync-mailbox\>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015<display-message>\"\015:macro browser q \"<exit>:bind browser \\\\015 select-entry\\015:bind browser q exit\\015'q<untag-pattern>.\\015<display-message>\"\015<exit><mark-message>q<enter><untag-pattern>.<enter><tag-thread><tag-prefix><copy-message>?" "copy thread to a mailbox"
bind  generic        tt  tag-entry
bind  index          tat tag-thread
bind  pager          tt  tag-message
macro pager          tat "<exit><mark-message>q<enter><tag-thread>'q<display-message>"    "tag-thread"
macro index,pager    gx  "<pipe-message>urlview<Enter>"                                   "call urlview to extract URLs out of a message"
macro attach,compose gx  "<pipe-entry>urlview<Enter>"                                     "call urlview to extract URLs out of a message"

# Command Line
# ----------------------------------------------------

bind editor \Cp history-up
bind editor \Cn history-down

Conflicts with Default Bindings

This configuration overrides some default bindings, and it may behoove you to know what you’re missing. Here’s a line-by-line breakdown:

bind generic,index,pager \Cf       next-page
bind generic,index,pager \Cb       previous-page
  • By default, ^F is bound to forget-passphrase.

    This default binding is unlikely to be used (it’s only needed when accessing a remote POP/IMAP server directly, which is less common than managing a local mirror of your mailstore, synced with a tool like mbsync or OfflineIMAP).

  • By default, ^B is bound to call urlview to extract URLs out of a message (i.e., visit links).

    This configuration remaps this function to gx.

bind generic             gg        first-entry
bind generic,index       G         last-entry
bind pager               gg        top
bind pager               G         bottom
  • By default, g is bound to group-reply (i.e., reply-all).

    This configuration remaps this function to a (à la Gmail).

  • By default, G is bound to fetch-mail.

    This default binding is unlikely to be used (it retrieves mail from a POP server; see above).

bind generic,pager       \Cy       previous-line
bind generic,index,pager \Ce       next-line
  • By default, ^Y is not bound.

    ^Y is normally intercepted by the terminal as a DSUSP signal. In order to enable this keybinding in mutt, DSUSP must be disabled in the terminal. To do this, add the function definition below to your .bashrc:

    mutt()
    {
      old=$(stty -g)                               # Capture old termio params
      stty dsusp undef                             # Disable DSUSP
      trap "rc=$?; stty $old; exit $rc" 0 1 2 3 15 # Restore termios on interrupt
      /usr/local/bin/mutt "$@"                     # Run mutt
      stty $old                                    # Restore termios on exit
    }
    
  • By default, ^E is bound to edit-type.

    This default binding is unlikely to be used (it’s for manually editing the MIME type of an email attachment).

bind generic,index,pager \Cd       half-down
bind generic,index,pager \Cu       half-up
  • By default, ^D is bound to delete-thread.

    This configuration remaps this function to dat (for “delete a thread”).

  • By default, ^U is bound to undelete-thread.

    This default binding is rendered superfluous by this configuration. (By default, “deleting” a message in mutt only flags it for deletion; until changes are registered with $/sync-mailbox, the actual mailstore remains unaffected, and messages may still be “undeleted”. Once changes are registered, flagged messages are either moved to a trash folder if the trash configuration variable is set, or irrecoverably purged if not. This configuration alters the default behavior and registers such changes automatically, relying on the user to set the trash variable if she wishes to retain the option to recover deleted messages.)

bind generic             zt        current-top
bind generic             zz        current-middle
bind generic             zb        current-bottom
bind index               za        collapse-thread
bind index               zA        collapse-all
  • By default, z is unbound (this setting does not conflict with any default key bindings).

bind index,pager         N         search-opposite
  • By default, N is bound to toggle-new.

    This default binding is redundant; the function can also be executed with wn.

bind index               <Backtab> previous-new-then-unread
  • By default, <Backtab> is unbound (this setting does not conflict with any default key bindings).

Actions / Command Line

The remaining sections have no conflicts with default key bindings, though the very long macros deserve some explanation — see this post for more.