DEV Community

MoniqueLive
MoniqueLive

Posted on

Finding man pages from Neovim with Telescope and/or mini.pick

I use man pages all the time, but sometimes I do not know exactly what I want to open.

printf is a good example.

There is printf(1) for the shell command, printf(3) for the C library function, and depending on what is installed locally there may be even more entries around it.

I wanted something simple inside Neovim:

  • list local man pages
  • fuzzy search them
  • see the section before opening
  • open using Neovim's built-in :Man

So I made a small plugin: man.nvim.

Source:

What it does

The plugin starts with the system database:

apropos .
Enter fullscreen mode Exit fullscreen mode

Then it parses the output into picker items like:

[1 User commands] printf(1) - format and print data
[3 Library calls] printf(3) - formatted output conversion
Enter fullscreen mode Exit fullscreen mode

The important part is that selecting an entry still delegates to Neovim:

:Man 3 printf
Enter fullscreen mode Exit fullscreen mode

No custom renderer.
No terminal buffer pretending to be man.
No reimplementing man page navigation.

Just discovery through a picker, opening through :Man.

Why not just use :Man?

:Man is great once I know what I want.

But I often want to explore what is installed locally. I may remember part of the name, or the description, or the section.

With the picker I can type things like:

printf
3 printf
formatted output
Enter fullscreen mode Exit fullscreen mode

And the item text includes the section label, so it is easy to see if I am about to open the command, library call, file format, etc.

Telescope version

The main branch exposes a Telescope extension.

Setup:

require('telescope').setup()
require('man_nvim').setup()
Enter fullscreen mode Exit fullscreen mode

Usage:

:Telescope man
Enter fullscreen mode Exit fullscreen mode

Internally it uses a table finder. Each parsed man page becomes an entry with data like:

{
  name = 'printf',
  section = '3',
  ref = 'printf(3)',
  description = 'formatted output conversion',
}
Enter fullscreen mode Exit fullscreen mode

The display and ordinal use the same searchable text:

'[3 Library calls] printf(3) - formatted output conversion'
Enter fullscreen mode Exit fullscreen mode

That keeps Telescope doing what Telescope is good at: prompt, sorting, preview, mappings.

mini.pick version

There is also a mini.picker branch using mini.pick.

Setup:

require('mini.pick').setup()
require('man_nvim').setup()
Enter fullscreen mode Exit fullscreen mode

Usage:

:Pick man
Enter fullscreen mode Exit fullscreen mode

The interesting part is that most of the plugin is shared conceptually:

  • parse apropos output
  • split aliases
  • sort by man section
  • build the :Man command
  • show a small preview
  • open the selected page

Only the picker integration changes.

Aliases

Some apropos entries have a lot of aliases.

For example, shells can produce lines like:

builtin(1), !(1), %(1), .(1), :(1), [(1) - shell built-in commands
Enter fullscreen mode Exit fullscreen mode

The plugin creates searchable items for the aliases, but opens the canonical page.

So if I search for !, I can find !(1), but selecting it opens builtin(1).

That matters because some aliases are not real standalone man pages. They are documented under the first entry in that apropos line.

Filtering noisy results

One thing that got annoying very quickly: Tcl/Tk results.

Sometimes a search like format shows useful results, but also a bunch of Tcl-related entries that I do not care about at that moment.

So I added negative prompt terms.

In both Telescope and mini.pick versions:

format -tcl
format -tcl -tk
Enter fullscreen mode Exit fullscreen mode

This means:

  • search for format
  • exclude entries containing tcl
  • optionally also exclude tk

The negative terms use smartcase too:

-tcl  # excludes tcl, Tcl, TCL, etc when smartcase allows it
-Tcl  # case-sensitive exclusion
Enter fullscreen mode Exit fullscreen mode

The Telescope version implements this as a sorter wrapper. It removes the negative terms from the prompt, filters entries, then delegates the positive query back to Telescope's normal sorter.

The mini.pick version implements this as a custom source.match. It filters entries first, then passes the positive query back into MiniPick.default_match.

That way I keep each picker's normal matching behavior instead of replacing it with my own fuzzy matcher.

Opening mappings

The mappings are intentionally boring:

<CR>  :vertical Man {section} {name}
<C-x> :Man {section} {name}
<C-v> :vertical Man {section} {name}
<C-t> :tab Man {section} {name}
Enter fullscreen mode Exit fullscreen mode

<CR> opens vertically by default because that is how I usually want docs next to code.

<C-x> opens with regular :Man for the classic horizontal split habit.

Portability notes

The plugin is meant to work on macOS, Linux, and FreeBSD.

That mostly means avoiding clever assumptions.

apropos output can be noisy. On my macOS machine I saw lines like makewhatis: ... No such file or directory, so the parser simply ignores lines that do not look like man entries.

Sections are also not always just 1 through 9. There can be things like:

3p
3posix
n
Enter fullscreen mode Exit fullscreen mode

So the parser accepts section names inside parentheses instead of assuming one digit.

For opening pages, the plugin lets Neovim and the system man command handle the hard part.

Installation

With lazy.nvim, for the Telescope version:

{
  'moniquelive/man.nvim',
  dependencies = { 'nvim-telescope/telescope.nvim' },
  config = function()
    require('man_nvim').setup()
  end,
}
Enter fullscreen mode Exit fullscreen mode

For the mini.pick version:

{
  'moniquelive/man.nvim',
  branch = 'mini.picker',
  dependencies = { 'echasnovski/mini.pick' },
  config = function()
    require('mini.pick').setup()
    require('man_nvim').setup()
  end,
}
Enter fullscreen mode Exit fullscreen mode

With packer.nvim, for Telescope:

use({
  'moniquelive/man.nvim',
  requires = { 'nvim-telescope/telescope.nvim' },
  config = function()
    require('man_nvim').setup()
  end,
})
Enter fullscreen mode Exit fullscreen mode

For mini.pick:

use({
  'moniquelive/man.nvim',
  branch = 'mini.picker',
  requires = { 'echasnovski/mini.pick' },
  config = function()
    require('mini.pick').setup()
    require('man_nvim').setup()
  end,
})
Enter fullscreen mode Exit fullscreen mode

With vim-plug, for Telescope:

Plug 'nvim-telescope/telescope.nvim'
Plug 'moniquelive/man.nvim'
Enter fullscreen mode Exit fullscreen mode

Then somewhere after plugins load:

require('man_nvim').setup()
Enter fullscreen mode Exit fullscreen mode

For mini.pick:

Plug 'echasnovski/mini.pick'
Plug 'moniquelive/man.nvim', { 'branch': 'mini.picker' }
Enter fullscreen mode Exit fullscreen mode

Then:

require('mini.pick').setup()
require('man_nvim').setup()
Enter fullscreen mode Exit fullscreen mode

Final thoughts

This is a small plugin, but it fits my workflow nicely.

I did not want to replace man.
I only wanted a better way to discover local man pages from inside Neovim.

Telescope and mini.pick both work well for this. The fun part was keeping the core behavior simple, and only changing the picker layer.

If I extend this next, I may add a filesystem fallback for machines where the apropos database is missing or stale.

But for now: fuzzy search local docs, filter away noise, open with :Man.

Profit!

Top comments (0)