Vim as $MANPAGER
Long, long ago, in a hostel room far, far away, I once read about using Vim as
the pager for man
. It involved using some script which made vim
behave like
less
(or something like that). I’d stumbled upon it while trying to make
reading manpages more comfortable, with syntax colouring, navigation, etc.
Of late, with Vim.SE for support, I’ve been customizing Vim more and more.
I’ve made a Git repo of my Vim files, taken baby steps in automating tasks I
often do, and so on. While looking through the recent posts in Unix.SE, I came
across this post which suggested using your editor as the pager. That
kicked up the dusty cobwebs in my decrepit memory module, and I remembered that
old attempt at using Vim for reading manpages. So, I set about trying to make
Vim man
’s pager. Why did I submit myself to such cruel and unusual punishment?
- I like Vim.
- I have it customized to my liking.
- It is powerful. The search is way better than anything
less
or your average manpage browser (likeyelp
) can offer. - It can browse to other manpages mentioned using tag navigation (
<c-]>
,<c-t>
).
The post suggested setting $MANPAGER
to a combination of col
and vim
:
export MANPAGER="col -b | vim -c 'set ft=man nomod nolist ignorecase' -"
For decidedly non-obvious reasons, it’s not likely to work for you. Why?
Because GNU man
doesn’t support piped commands in $MANPAGER
– BSD’s man
does (that’s +1 for you OSX folks). From man man
:
MANPAGER, PAGER
If $MANPAGER or $PAGER is set ($MANPAGER is used in preference),
its value is used as the name of the program used to display the
manual page. By default, pager -s is used.
The value may be a simple command name or a command with
arguments, and may use shell quoting (backslashes, single
quotes, or double quotes). It may not use pipes to connect
multiple commands; if you need that, use a wrapper script, which
may take the file to display either as an argument or on
standard input.
I tried the suggested solution (using a wrapper script), which worked fine.
However, it created a problem: I use Git to manage my dotfiles. I’d rather not
rely on stuff outside the repo. Stuff installed by package managers and
differences per distro are a fact of life and have to be handled, but I’d rather
not take pains over what I add to it. One obvious solution is to wrap the
command in sh -c
:
MANPAGER='sh -c "col -b | vim -c \'set ft=man nomod nolist ignorecase\' -"'
Ugly. I also hate having to deal with quoting.
At this point, it struck me: Why should I run this via a pipe? Once Vim starts,
I can perfectly well use %! col -b
to do the job. So:
MANPAGER='vim -c "%! col -b" -c "set ft=man nomod nolist ignorecase" -'
Nice!
Now, other considerations started popping up. You can easily quite less
(and
by extension, man
), by pressing q, or CtrlC.
Vim usually considers a buffer read from stdin
to be modified. Therefore, to
quit a manpage, you’d have to do :q!
, not just :q
. Thankfully, one of the
options set (nomod
) tells Vim that the buffer hasn’t been modified.
Therefore, we can just use :q
:
nnoremap q :q<CR>
Other considerations arise:
- The buffer is modifiable. There’s no reason for it to be so.
- The buffer doesn’t have a name. It would be convenient to see the name of the manpage.
- You don’t want swapfiles hanging around from manpages.
As I pondered over this, I realised that these are settings I’d want to apply to
a manpage no matter how I opened it. Hence, they should really be in Vim’s
filetype settings for man
. So, I created a ~/.vim/ftplugin/man.vim
,
containing:
1
2
3
4
5
6
7
8
9
10
11
12
function! PrepManPager()
if !empty ($MAN_PN)
silent %! col -b
file $MAN_PN
endif
setlocal nomodified
setlocal nomodifiable
setlocal readonly
setlocal nolist
setlocal noswapfile
endfunction
autocmd VimEnter * PrepMan()
I picked VimEnter
since it runs after any commands specified using
-c
are run, so I can get it to run after the filetype has been set.
However, I realised that:
- I wanted to apply some of these settings to manpages irrespective of how they were opened; and
- I’d rather not specify
set ft=man
from the command line, keeping an eye on using Vim as a general-purpose pager; - Using
VimEnter *
felt wrong.
A bit of experimentation later, I found that:
man
doesn’t seem to ever provide a filename as an argument, irrespective of what the manpage says.man
setsMAN_PN
to the manpage name (man(1)
, for example)
Knowing that I’m reading from stdin
and that MAN_PN
is set (to the manpage
name!), I came up with this version:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
" vimrc
if !empty($MAN_PN)
autocmd StdinReadPost * set ft=man | file $MAN_PN
endif
" ftplugin/man.vim
setlocal nolist
setlocal readonly
setlocal buftype=nofile
setlocal bufhidden=hide
setlocal noswapfile
setlocal nomodifiable
function! PrepManPager()
setlocal modifiable
if !empty ($MAN_PN)
silent %! col -b -x
endif
setlocal nomodified
setlocal nomodifiable
endfunction
autocmd BufWinEnter $MAN_PN call PrepManPager()
nnoremap q :qa<CR>
nnoremap <Space> <PageDown>
map <expr> <CR> winnr('$') == 1 ? ':vs<CR><C-]>' : '<C-]>'
with:
export MANPAGER="vim -"
Beautiful!
What does this do?
- In the main
vimrc
, I check if I’m reading fromstdin
and ifMAN_PN
is set. If so, set the filetype toman
and the filename to the contents ofMAN_PN
. - In the filetype-specific setting, use an
autocmd
the relies on the filename being$MAN_PN
to applycol -b
. - Set
nomodified
to tell Vim that the buffer hasn’t been modified, and make it a read-only, non-modifiable, scratch buffer. - Also, map
q
to:qa
, so that I can quit all opened manpages, and Space to Page Down, in keeping with the usual behaviour ofless
. col -b
’s use of tabs led to messed up alignment. I had to use-x
(replace tabs with spaces) so that, for example,man ascii
showed up properly.
Finally, man man
opens up pretty much as I’d like it to.
Why “pretty much”? man
obeys MANWIDTH
, so I can get a manpage formatted
exactly as wide as I want. If open a manpage within Vim, however (by navigating
the tags, for example), the page is formatted for the full width of Vim. :(
Secondly, Vim leaves this annoying message:
$ man man
Vim: Reading from stdin...
$
For the moment, I’ve adopted the decidedly un-Vim-like solution of opening a split before navigating to any tags - half the terminal width is fine for me. That’s what the last mapping in the above snippet does: check if I have only one window open, and if so, open a new window before jumping to the tag - with the added benefit using to the thoroughly intuitive (to me) Enter key for jumping to the named page. As a happy side effect, I get to see exactly where I was in the new window! :)
I have no idea how to suppress the stdin
message from Vim itself.
All told:
Footnote
This is my first blog post using Jekyll. Writing it, I have learned quite a bit, which I will write about in another post soon.