Better filebrowser for mutt serving MH mailbox

Mutt has it's strong sides and also weak sides. One of the weak ones is the file browser, that isn't really designed for actual browsing, as it only displays one level of the file hierarchy at a time. If not entering a path directly, actual browsing is done by:

  • either typing a subpath, then using the browser again on the partial path
  • or one can tell mutt up front about the folders in existance using the mailboxes command (and also automatically populate that, as shown here and here), and then use the Mailboxes-view of the browser, which is a flat list

The problem is that both ways are frustrating to use with an ever growing mailbox. The former makes it hard to see which folders and subfolders have unread mail and to get some overview over the hierarchy in general. The latter is a flat list, which is hard to find anything in if you have a lot of nodes. There is a patchset for mutt adding a sidebar for browsing, but suffers from the same problem.

Some mail setups don't face those problems as they aren't file-system heavy, or use indexers. However, I'm used to MH as a mailstore, and like to drop/filter my mail into many folders and subfolders, the old-school way. So, I decided to come up with some own solution, hooking up vim as a filebrowser. Note, the scripts below work with MH, only, and require nmh to be accessible.

Add this "pretty" macro to your .muttrc. What it does is running nmh's flists command, uses the output to build a dummy directory tree under /tmp with number of unread emails (summed up towards the root), then lets vim do the folder browsing. Once a folder is picked, it hands it back to mutt:

# own filebrowser 
set wait_key=no 
macro index,pager b '\ 
	<shell-escape>\ 
	if [ -d /tmp/mailbox_tree_mirror ]; then rm -r /tmp/mailbox_tree_mirror; fi;\ 
	flists -alpha -recurse |\ 
		sed -E "s@^(.*) has *([0-9]+) .*@\1/\2@" |\ 
		sed -E "s@[+ ]*(/[0-9]+)\$@\1@" |\ 
		awk "\ 
			BEGIN{FS=\"/\"}\ 
			{s=\"\";for(i=1;i<NF;++i){s=s\"/\"\$i;u[s]+=\$NF}}\ 
			END{for(i in u){split(i,d);s=p=\"\";for(j in d){if(d[j]){s=s\"/\"d[j];p=p\"/\"d[j]\" (\"u[s]\")\"}}print p}}" |\ 
		while read l; do mkdir -p "/tmp/mailbox_tree_mirror/$l"; done;\ 
	(cd /tmp/mailbox_tree_mirror/ && vim -S ~/.mutt/browse_files.vim .);\ 
	<enter>\ 
	<enter-command>\ 
	source /tmp/mailbox_tree_mirror/pick\ 
	<enter>'

The macro is bound to b, and makes some assumptions about some paths. Adapt as needed.

Starting vim sources a file I put in ~/.muttrc called browse_files.vim, which works with either netrw (usually comes with vim) or NERDTree:

" This folder browser depends on either netrw or NERDTree
" Usage:
"   - use with muttrc macro building a dummy image of the mailbox hierarchy, then using vim as browser on it
"   - browse with vim; gm (go mailbox) is mapped to select inbox and return to mutt

fu! SwitchToMailboxInMutt()
	" hack to get path and write to file, so mutt can source it
	:silent !print push \'<change-folder>`span class="Statement">pwd|sed 's@^/.*tmp/mailbox_tree_mirror/@=@'|sed -E 's@ \([0-9]+\)(/|$)@/@g'|sed -E 's@( )@\<quote-char\>\1@'g`'\<return\> > /tmp/mailbox_tree_mirror/pick
	:q!
endf


function! AddMuttSyntaxHL(parent)
	exec 'syn match muttUnreadSel # ([0-9]\+)/# containedin='.a:parent.'Dir'
	syn match muttUnreadCnt #([0-9]\+)#   containedin=muttUnreadSel
	syn match muttUnread0   #(0)#         containedin=muttUnreadCnt
	hi link muttUnreadSel Ignore
	hi link muttUnreadCnt Number
	hi link muttUnread0   Comment
endf


function! ApplyMuttNERDTreeBindings()
	" Remove clutter and hook into syntax - call syntax setter directly,
	" as NT hijacks netrw, and thus is kinda delay-loaded, anyways (IIUC).
	let g:NERDTreeMinimalUI=1
	call AddMuttSyntaxHL('NERDTree')

	" hide files, jump to first folder entry
	:normal F
	call cursor(line('.')+4)

	" gm picks this directory
	map gm cd<CR>:call SwitchToMailboxInMutt()<CR>
endf


function! ApplyMuttNetrwBindings()
	" Hook into syntax, needs to happen after netrw's syntax is fully loaded.
	autocmd FileType netrw call AddMuttSyntaxHL('netrw')

	" Use tree mode, only, and hide all files
	let g:netrw_liststyle=3
	let g:netrw_list_hide='.*[^/]$'

	" gm picks this directory - long macro b/c of toggle-behaviour of netrw's browser, didn't find a way to get path under cursor
	map gm <CR>:let s=b:netrw_curdir<CR><CR>:if(strlen(b:netrw_curdir)>strlen(s))\|let s=b:netrw_curdir\|endif<CR>:exec 'cd '.fnameescape(s)<CR>:call SwitchToMailboxInMutt()<CR>
endf


if &ft ==# "nerdtree"
	call ApplyMuttNERDTreeBindings()
else
	call ApplyMuttNetrwBindings()
endif

That's it - instead of having to deal with mutt's browser, I now hit b, browse and when I found the folder I want to browse to, hit gm in vim (goto mailbox), and I'm back in mutt. Also, I get a quick overview of all my folders and the unread emails, even in color!

mutt/mh with vim folder browser