Skip to content

Conversation

@Dook97
Copy link
Contributor

@Dook97 Dook97 commented Jan 5, 2026

Related: #2209 #2208

A manpages picker.

532095391-32866873-d91e-43a7-9aea-8ab6e42721e7.mp4

@echasnovski Submitting as a PR for your consideration whether this is a good
fit for mini.extra. Together with @drowning-cat I think we've made the code
better and more compact. If it is your call that this shouldn't be included
I'll alter the code in #2209 for it to be a custom picker and be done with it −
I've already spent way too much time on this 😅

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 5, 2026

I like your passion. I won't be afraid to say that we like your passion, though I'll speak from my own perspective. Not only do the man pages themselves feel kind of rough in Neovim, especially with their behavior as described in the Show and Tell discussion, but they are also an OS dependent, so the picker may need to be handled and documented differently.

There are also many minor things to think about, such as what to do with hard or soft wrapping, or whether manpages should be opened like help pages by reusing the same window. In general, mini.nvim prefers a slow and steady approach, so I think it's better to wait for this feature, like a good wine, starting with the documentation. Though echasnovski and mini.nvim team may have a different opinion from mine, so let's wait for their response.

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 5, 2026

Sorry if I'm being too impatient. I want to free up some mental bandwidth so that I can actually do some work with my editor, instead of working on it - I'm not great at multitasking 😅 But if the project has a more conservative policy, that's obviously fine.

Thanks for all the help making the picker better.

@echasnovski
Copy link
Member

Thanks for the PR!

All right, I think it might indeed be a reasonable addition to 'mini.extra'. Probably not in its current form, as it still looks a bit complex. In particular, I am not very fond of an additional "scope" option and rather cryptic handling of everything man related.

I'd have to investigate about the best approach here.


I like your passion. I won't be afraid to say that we like your passion, though I'll speak from my own perspective.

Thanks for the positive attitude! I'd also like to clarify: speaking on behalf of the MINI team preferably should be reserved for the team itself. Doing otherwise might create unintended impressions.

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 6, 2026

I am not very fond of an additional "scope" option

The added complexity seemed to me small enough that having it was better than not, but if you disagree I don't mind removing it. Personally I won't be using the option anyway.

@drowning-cat

This comment was marked as off-topic.

@echasnovski
Copy link
Member

I want to defend myself here. I don't see myself as part of the team, a member, a future member, or even closely related to the project. I shared my opinion because I tried rewriting the picker to suit my own taste and liked the idea in general. "We" was not meant to represent the project; it was simply my assumption that you appreciate when people share or contribute to the project.

Sorry, didn't at all mean to make you feel like having to defend yourself. Your contributions to discussions have been very valuable. And you are of course correct that I appreciate seeing people share and/or contribute to MINI.

It might be a result of my personal pet peeve about people using too broad "we" when a more narrow "I" is enough.


The added complexity seemed to me small enough that having it was better than not, but if you disagree I don't mind removing it. Personally I won't be using the option anyway.

@Dook97, it is not only about the local options, but overall code. Like having to go the MiniPick.builtin.help path of custom mappings or rather cryptic item:match("^((.-)%s*%(([^)]+)%).-)%s+%-%s+(.*)$") and string.format("silent re! 2>&- MANWIDTH=%s man %s %s", width, section, cmd).

I'll be looking into this myself, as this will also require testing and documentation. So you can "free up some mental bandwidth" :)

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 6, 2026

FYI the regex is stolen from telescope's implementation.

I'll be looking into this myself, as this will also require testing and documentation. So you can "free up some mental bandwidth" :)

Much appreciated 😅

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 6, 2026

Name

After some initial research, I can say that the complexity of the query is justified. However, the query provided by Telescope needs to be reworked, as it does not discard the /arch part described below, and we also don't actually need the description to be parsed.

There are two main man page viewers: man-db (used in Arch Linux, Ubuntu, Fedora, ...) and mandoc (used in MacOS, Void Linux, Alpine Linux, BSD, ...). Switching between them on Arch Linux and reindexing actually changes how man -k . displays entries.

man-db produces a predictable format:

alacritty (1)           - A fast, cross-platform, OpenGL terminal emulator.
alacritty (5)           - TOML configuration file format.
alacritty-bindings (5)  - Default configuration file bindings.
alacritty-msg (1)       - Send messages to Alacritty.

On the other hand, mandoc can display results in various forms:

Alacritty example
alacritty, Alacritty(1) - A fast, cross-platform, OpenGL terminal emulator.
alacritty-msg(1) - Send messages to Alacritty.
alacritty, Alacritty(5) - TOML configuration file format.
alacritty-bindings(5) - Default configuration file bindings.

Void Linux search:

abidb(1)
afterstep_faq(1, 1x)
alacritty, Alacritty(1)

OpenBSD search:

amd64_iopl(2/amd64)
a64l, l64a(3)

From Void Linux apropos man page for mandoc (see https://man.voidlinux.org/apropos.1):

Each output line is formatted as

name[, name...](sec) - description

Where “name” is the manual's name, “sec” is the manual section, and “description” is a short summary.
If an architecture is specified, it’s displayed as

name(sec/arch) - description

Entries are separated by a comma followed by a space , , but I did not find any entries that use a comma in their names, so using just comma , should be fine.

There might be errors in the output (Void Linux):

image

In rare cases, strange entries may appear (Void Linux search):

acl_extended_file_nofollow, acl_extended_file, acl_extended_file,(3)
tpm2_clearcontrol, tpm2_clearcontrol(1)(1)
wslupath, wslupath(DEPRECATED)(1)
kubeadm-init-phase-kubelet-finalize-experimental-cert-rotation, kubeadm(1, kubernetes)

Pattern

My take on parsing:

if not vim.startswith(item, "man: ") then
  local names, sects = str:match("^(.-)%s-(%b())%s+%- ")
  sects = sects:sub(2, -2)
  local name = names:gsub(", .*", "")
  local sect = sects:gsub(", .*", ""):gsub("/.*", "")
end

Width

Seems like mandoc does not support the MANWIDTH env variable:

# man-db
MANWIDTH=100 man 1 tmux
# mandoc
man -O width=100 1 tmux

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 7, 2026

we also don't actually need the description.

I find it useful. You might not know exactly the manpage you're looking for and the description can contain keywords by which it may be found.

Seems like mandoc does not support the MANWIDTH env variable:

It would be optimal if we could just use the Man plugin instead of running external commands. That's the way telescope does it too, but it seems to clash with something about how mini.pick handles things 🤷

@echasnovski
Copy link
Member

After some initial research, I can say that the complexity of the query is justified.
...
Seems like mandoc does not support the MANWIDTH env variable:

Thanks, both of these are helpful. The different format can be addressed on a case-by-case basis.

I have a solution that looks reasonable. It should handle commas and needs one character change to handle abidb(1) (no space). Not sure if amd64_iopl(2/amd64) needs special handling. Won't man 2/amd64 amd64_iopl work in this case?

It would be optimal if we could just use the Man plugin instead of running external commands. That's the way telescope does it too, but it seems to clash with something about how mini.pick handles things 🤷

It can be made to work with :Man, but it is visibly slow and opens listed buffer for each manpage (which has unwanted side effects). So I think settling on solution that doesn't rely on it at all is better.

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 7, 2026

Won't man 2/amd64 amd64_iopl work in this case?

image

I tried it on an online virtual machine. It printed an error and also opened the man page (I think as a fallback).

P.S. I don't have man pages with the /arch suffix to test this on my machine (Arch Linux).

@echasnovski
Copy link
Member

I tried it on an online virtual machine. It printed an error and also opened the man page (I think as a fallback).

That will be super helpful. Thanks!

echasnovski added a commit that referenced this pull request Jan 8, 2026
echasnovski added a commit that referenced this pull request Jan 8, 2026
Resolve #2212

Co-authored-by: =?UTF-8?q?Jan=20Dosko=C4=8Dil?= <30779172+Dook97@users.noreply.github.com>
Co-authored-by: Drowning Cat <shamal.ma.personal@gmail.com>
@echasnovski
Copy link
Member

There should now be MiniExtra.pickers.manpages. Its implementation is quite different from this PRs, but I've added both of you as commit co-authors.

@drowning-cat, I am not able to see the comments on f0b5293, but I did manage to read them from e-mail notifications. Both points (man (1) and man (1p) being meaningfully different and trying to get not shortened list of items) should be addressed in the final version.

Thank you again for the idea and enthusiasm!

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 9, 2026

@echasnovski Thank you for your hard work.

Small comments:

  • I forgot to mention that mandoc is used by default on MacOS, but I don't have a Mac computer to test it.
  • I think we can omit section:gsub('[/%,].*$', '') and simply use section:match('%w+').

@echasnovski
Copy link
Member

  • I forgot to mention that mandoc should be used on MacOS, but I don't have a Mac to test it.

The dependency on man is documented, so it is not a huge deal. If it can be accounted by a simple man -> mandoc on MacOS, it will be easy to fix. But I'd need a confirmation.

  • I think we can omit section:gsub('[/%,].*$', '') and simply use section:match('%w+').

That won't hurt as a backup at least.

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 9, 2026

On my system at least (Arch linux), the picker ignores manpages stored in /usr/local/man. It's weird, because it finds them just fine in shell. I have MANPATH set to "", so that shouldn't be distorting anything. I'll investigate some more.

$ man -k . | wc -l
28251

...while :Pick manpages only shows 28246, which corresponds to the 5 lacking manpages I have in /usr/local/man.

It's even weirder, because man --path gives the same output when invoked from the shell as when invoked via vim.system().

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 9, 2026

Removing spawn_opts from the pick.builtin.cli() invocation fixes this for some reason.

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 9, 2026

That won't hurt as a backup at least.

Also true, but it only works if the sequence doesn't contain any word characters, like ""/, but I didn't find such entries.

The dependency on man is documented, so it is not a huge deal. If it can be accounted by a simple man -> mandoc on MacOS, it will be easy to fix. But I'd need a confirmation.

Both mandb and mandoc support the man command, but the flags and behavior are slightly different.

There are two issues you should be aware of:

  1. Resolved (see comment).

    When running on a small enough screen, man backed by mandb will show concealed entries that cause parsing errors (see discussioncomment). This can be fixed with the -l flag, but man backed by mandoc uses -l in a completely different way, which causes the process to hang on my machine.

  2. The env variable MANWIDTH doesn't work with man backed by mandoc, but it can be replaced with -O width=<num>, which is not supported by mandb.

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 9, 2026

Also I think it'd be better to include the -l option in the picker command. Long manpage names get truncated otherwise (see here).

@echasnovski
Copy link
Member

I have MANPATH set to "", so that shouldn't be distorting anything.
...
Removing spawn_opts from the pick.builtin.cli() invocation fixes this for some reason.

Does applying the following patch fix this?

diff --git a/lua/mini/extra.lua b/lua/mini/extra.lua
index ce11c33c..f6de43e4 100644
--- a/lua/mini/extra.lua
+++ b/lua/mini/extra.lua
@@ -1280,7 +1280,7 @@ MiniExtra.pickers.manpages = function(local_opts, opts)
 
   local source = { name = 'Manpages', choose = choose, preview = preview }
   opts = vim.tbl_deep_extend('force', { source = source }, opts or {})
-  local spawn_opts = { env = { 'MANWIDTH=999' } }
+  local spawn_opts = { env = { 'MANWIDTH=999', 'MANPATH=' .. vim.env.MANPATH } }
   return pick.builtin.cli({ command = { 'man', '-k', '.' }, spawn_opts = spawn_opts }, opts)
 end
 

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 9, 2026

No, that doesn't seem to help. vim.env.MANPATH is actually nil for me. Even if I do

diff --git a/lua/mini/extra.lua b/lua/mini/extra.lua
index ce11c33..aff07aa 100644
--- a/lua/mini/extra.lua
+++ b/lua/mini/extra.lua
@@ -1280,7 +1280,7 @@ MiniExtra.pickers.manpages = function(local_opts, opts)
 
   local source = { name = 'Manpages', choose = choose, preview = preview }
   opts = vim.tbl_deep_extend('force', { source = source }, opts or {})
-  local spawn_opts = { env = { 'MANWIDTH=999' } }
+  local spawn_opts = { env = { 'MANWIDTH=999', 'MANPATH=' .. (vim.env.MANPATH or '') } }
   return pick.builtin.cli({ command = { 'man', '-k', '.' }, spawn_opts = spawn_opts }, opts)
 end

The manpages are still not visible.

@echasnovski
Copy link
Member

Also I think it'd be better to include the -l option in the picker command. Long manpage names get truncated otherwise (see here).

For now, I would go with what both 'ibhagwan/fzf-lua' and 'folke/snacks.nvim#picker' are using.

The -l option doesn't look like it should be responsible for shortening anything.

Besides, I don't like the "... but ..." part from here:

  1. This can be fixed with the -l flag, but man backed by mandoc uses -l in a completely different way, which causes the process to hang on my machine.

If it doesn't work, then it doesn't work.

2. The env variable MANWIDTH doesn't work with man backed by mandoc, but it can be replaced with -O width=<num>, which is not supported by mandb.

If there is a concise and reliable way to account for this, then this can be discussed. Otherwise let it be (semi)known limitation.

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 9, 2026

  1. Setting MANWIDTH=999 as a spawn option fixes the issue with the -l option. It was a false alert.

  2. For someone with a Mac computer to test:

local is_mandoc = vim.fn.executable("mandoc") == 1

if is_mandoc then
  vim.list_extend(show_man_cmd, { "-O", "width=" .. width })
end

@drowning-cat

This comment was marked as resolved.

@echasnovski
Copy link
Member

Both vim.system and !man return the correct number of entries, but the picker does not:

vim.system() by default inherits all environment variables, but vim.loop.spawn does not. So this is a matter of identifying the correct environment variable that controls this. At the very least, inheriting all environment variables (like here) is possible and should fix this, but I'd rather identify the issue.

Can it be PATH = vim.env.PATH, MANPATH = vim.env.MANPATH?

@drowning-cat
Copy link
Contributor

drowning-cat commented Jan 9, 2026

PATH with no MAN is the main culprit. Strangely enough, I came to almost the same conclusion, but it took me 10 20 times longer.

@echasnovski
Copy link
Member

PATH with no MAN is the main culprit. Strangely enough, I came to almost the same conclusion, but it took me 10 20 times longer.

Great! Could you check if the following patch fixes the issue?

diff --git a/lua/mini/extra.lua b/lua/mini/extra.lua
index ce11c33c..9fe4958a 100644
--- a/lua/mini/extra.lua
+++ b/lua/mini/extra.lua
@@ -1280,8 +1280,10 @@ MiniExtra.pickers.manpages = function(local_opts, opts)
 
   local source = { name = 'Manpages', choose = choose, preview = preview }
   opts = vim.tbl_deep_extend('force', { source = source }, opts or {})
-  local spawn_opts = { env = { 'MANWIDTH=999' } }
-  return pick.builtin.cli({ command = { 'man', '-k', '.' }, spawn_opts = spawn_opts }, opts)
+  local env = { 'MANWIDTH=999' }
+  table.insert(env, vim.env.PATH ~= nil and ('PATH=' .. vim.env.PATH) or nil)
+  table.insert(env, vim.env.MANPATH ~= nil and ('MANPATH=' .. vim.env.MANPATH) or nil)
+  return pick.builtin.cli({ command = { 'man', '-k', '.' }, spawn_opts = { env = env } }, opts)
 end
 
 --- Neovim marks picker

@drowning-cat
Copy link
Contributor

I've always wanted to say this: no blockers from my side.

@echasnovski
Copy link
Member

No, that doesn't seem to help. vim.env.MANPATH is actually nil for me. Even if I do

@Dook97, does the above patch with both PATH and MANPATH solve this for you?

@Dook97
Copy link
Contributor Author

Dook97 commented Jan 10, 2026

Yes, that works!

@echasnovski
Copy link
Member

All right. After 8dccba8 I hope it should work as expected on both Linux and MacOS. The polishing of this already took longer than expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants