NORMAL ~/wkd/p/lib/help/lib.nvim-deps :set skin=modern utf-8

lib.nvim-deps.txt

Declare, detect and install a plugin's external tools — lib.nvim

doc/lib.nvim-deps.txt — rendered from the plugin's own vimdoc

*lib.nvim-deps.txt*   Declare, detect and install a plugin's external tools

lib.nvim.deps                                                *lib.nvim-deps*

Plugins that degrade gracefully without optional CLI tools (pandoc,
ImageMagick, tesseract, poppler-utils, ...) all end up hand-rolling the same
two things: a vim.fn.executable probe in health.lua, and a hint string
telling the user what to type. lib.nvim.deps replaces both with a declared
spec the plugin ships, and adds the step nobody had: composing the correct
install command for the host's package manager.

Nothing here ever executes an install command. See |lib.nvim-deps-safety|.

CONTENTS *lib.nvim-deps-contents*

  1. Declaring tools ......................... |lib.nvim-deps-declaring|
       docs/INSTALL.md ....................... |lib.nvim-deps-markdown|
       docs/install.json ..................... |lib.nvim-deps-json|
  2. Commands ................................. |lib.nvim-deps-commands|
  3. Safety model ............................. |lib.nvim-deps-safety|
  4. Submodules ............................... |lib.nvim-deps-submodules|
       deps.health ........................... |lib.nvim-deps-health|
       deps.spec ............................. |lib.nvim-deps-spec|
       deps.pm ............................... |lib.nvim-deps-pm|
       deps.install .......................... |lib.nvim-deps-install|
       deps.view ............................. |lib.nvim-deps-view|
       deps.first_run ........................ |lib.nvim-deps-first_run|
       deps.status ........................... |lib.nvim-deps-status|
       deps.require_tool ..................... |lib.nvim-deps-require_tool|
       deps.detect ........................... |lib.nvim-deps-detect|
  5. Resolution ............................... |lib.nvim-deps-resolution|
  6. Integrating a plugin ...................... |lib.nvim-deps-integrating|

1. DECLARING TOOLS *lib.nvim-deps-declaring*

A plugin declares its optional external tools in ONE of two files, both under
its own docs/ directory. Copyable templates live in templates/deps/ in
the lib.nvim repo.

Every tool entry needs three things, all enforced by validation:

    bin     the executable probed on PATH
    why     one sentence saying what this tool unlocks
    pkg     package name per package manager, at least one entry

why being REQUIRED is the point of the whole format, not a nicety: a
manifest entry that doesn't say why a tool matters produces a nameless "tool
missing" report, which is what the hand-rolled hint strings already did.
required defaults to false; see is optional.

docs/INSTALL.md                                      *lib.nvim-deps-markdown*

Ordinary Markdown prose, plus one fenced install-tool block per tool. The
block body is decoded with lib.lua.yaml's minimal subset — no anchors, no
flow style, no block scalars, so keep why on one line. Renders as normal
documentation on GitHub, which is this variant's reason to exist.

    ```install-tool
    bin: pdftotext
    required: false
    why: "Enables the fast plain-text extraction backend."
    pkg:
      apt: poppler-utils
      brew: poppler
      pacman: poppler
    ```
docs/install.json                                        *lib.nvim-deps-json*

Same fields as JSON, decoded with vim.json.decode. No line-oriented
parsing limits: multi-line why, arbitrary extra metadata (unknown fields
are ignored, not rejected), and the fastest parse. Preferred automatically
when a plugin ships both.

    {
      "tools": [
        {
          "bin": "pdftotext",
          "required": false,
          "why": "Enables the fast plain-text extraction backend.",
          "pkg": { "apt": "poppler-utils", "brew": "poppler" }
        }
      ]
    }

2. COMMANDS *lib.nvim-deps-commands*

Registered by require("lib.nvim_usrcmds").setup() via its deps option
(default: true), as routes on the existing :Lib verb rather than a second
top-level command name.

                                                                  *:Lib-deps*
:Lib deps show {plugin}     Tools, each with its why, present/missing on
                            this host, and the command that would install
                            it. Opens a scratch split. Missing-and-required
                            sorts first, then missing, then present.

:Lib deps show              List every plugin that ships a spec at all.

:Lib deps status            The same report across EVERY plugin at once, one
                            line per tool no matter how many plugins want it
                            (see: names them). The view for "I just rolled
                            this config out on a new machine".
                            See |lib.nvim-deps-status|.

:Lib deps install {plugin}  Compose the install command for this host's
                            package manager, ask, then hand it to a
                            terminal. See |lib.nvim-deps-safety|.

:Lib deps install           The same, for everything missing across every
                            plugin.

<Tab> completes plugin names that actually ship a spec.

:Lib deps show opens a popup (via |lib.nvim-kit|'s viewer, when
installed — falls back to a read-only scratch split otherwise) with three
keymaps:

    i        Install the tool under the cursor. Inline with live streamed
             output when the package manager needs no elevation (brew,
             scoop, most winget); otherwise the same confirm-then-terminal
             handoff as bulk install, since a backgrounded job has no
             interactive stdin for a sudo password. See |lib.nvim-deps-safety|.
    <CR>     Expand/collapse that tool's install output.
    I        Install everything missing (same as :Lib deps install).
    q/<Esc>  Close.

A tool's line flips from [missing] to [ok] the moment its inline
install finishes successfully.

3. SAFETY MODEL *lib.nvim-deps-safety*

Requiring any module under lib.nvim.deps registers nothing and touches
nothing. plan() only probes. The single function that opens anything is
deps.install.run, and its contract is:

    1. Reachable only from an explicit user action (:Lib deps install),
       never from require or a setup().
    2. Asks for confirmation, showing the exact command first.
    3. Opens a terminal running your shell and TYPES the command into it
       WITHOUT a trailing newline. You press Enter.

Point 3 is why this does not use jobstart/vim.system: a backgrounded
privileged install with its password prompt swallowed is the failure mode
worth designing out. A sudo prompt is answered at a real terminal.

Two further omissions, both deliberate:

    No -y/--noconfirm   The package manager keeps its own confirmation
                            prompt. Seeing what apt is about to pull in
                            beats suppressing it for tidiness.

    No elevation logic      Beyond prefixing sudo (only when the manager
                            needs root, the process is not already root,
                            and sudo is on PATH). On Windows, elevation
                            is the package manager's own business.

4. SUBMODULES *lib.nvim-deps-submodules*

deps.health                                            *lib.nvim-deps-health*

Replaces the hand-rolled check_exe/probe loop in a plugin's own
health.lua. Probes executables via lib.nvim.core's memoized has_exec,
and Python modules by shelling out to the first of python3/python/py.

    require("lib.nvim.deps.health").report({
      { bin = "pdftotext", required = false, hint = "install poppler-utils" },
      { python_module = "pdfplumber", hint = "pip install pdfplumber" },
    })

    -- or straight from a parsed spec, reusing each tool's `why` as the hint:
    require("lib.nvim.deps.health").from_tools(result.tools)
deps.spec                                                *lib.nvim-deps-spec*
    local spec = require("lib.nvim.deps.spec")

    spec.parse_markdown(text)   --> { tools = {...}, errors = {...} }
    spec.parse_json(text)       --> same shape
    spec.load(path)             --> dispatches on the .json extension
    spec.find("pdfport.nvim")   --> path to that plugin's spec, or nil
    spec.plugins()              --> every plugin shipping a spec, sorted
errors is never nil: a malformed or incomplete entry is reported rather
than silently dropped, and a file can be partly valid.

deps.pm                                                    *lib.nvim-deps-pm*
    local pm = require("lib.nvim.deps.pm")

    pm.detect()      --> the manager to use here, or nil
    pm.available()   --> every manager present, in this OS's preference order
    pm.get("apt")    --> a manager definition by id, installed or not
    pm.commands(pm.get("brew"), { "poppler" })  --> { { "brew", "install", ... } }
    pm.render(argv)  --> a copy-pasteable shell string
Known ids, matching the keys a tool's pkg map uses: apt, dnf, pacman,
zypper, apk, brew, winget, scoop, choco. WSL is treated as plain Linux —
inside WSL the distro's own manager is the right answer.

commands returns a LIST of argv lists, not one. winget cannot take
several packages per invocation (extra tokens become query terms), so it
gets one command per package while every other manager gets one combined
command. Callers must not assume a single command.

deps.install                                          *lib.nvim-deps-install*
    local install = require("lib.nvim.deps.install")

    local plan = install.plan(result.tools)
    -- plan.manager / present / missing / installable / unsupported
    --    / packages / commands

    install.run(plan)   -- asks, then hands off to a terminal
plan() is pure. installable vs unsupported splits the missing tools by
whether this host's manager has a declared package name, so a tool with no
brew entry is reported rather than quietly dropped from the command.

run() returns false — with a notification, opening nothing — when there is
no package manager, nothing missing, or nothing installable.

deps.view                                                *lib.nvim-deps-view*
    require("lib.nvim.deps.view").lines("pdfport.nvim", result)  --> string[]
    require("lib.nvim.deps.view").show("pdfport.nvim", result)
lines() is pure and does the whole job; show() only puts its output in a
named scratch split. A why under 20 characters gets a visible nudge: the
parser can enforce that why exists, but not that it says anything useful,
so that judgement is surfaced where the plugin author will see it instead of
pretended to be validation.

deps.first_run                                      *lib.nvim-deps-first_run*
    require("lib.nvim.deps").show_once("pdfport.nvim")
Shows a plugin's declared-tools popup once, ever — persisted across
restarts via lib.nvim.cache.disk (namespace lib.nvim.deps.first_run).
No-op on every call after the first, including when there was nothing to
show (no spec, empty spec, or nothing declared is missing) — those still
mark the plugin seen, since this is a one-time welcome, not an ongoing
watcher. Call it from the CONSUMING PLUGIN's own setup(), never from
require — see |lib.nvim-deps-integrating|.

    require("lib.nvim.deps.first_run").seen(name)         --> boolean
    require("lib.nvim.deps.first_run").mark_seen(name)
    require("lib.nvim.deps.first_run").reset(name)        --> forget one, or all with no arg

                                                    *:Lib-deps-reset-first-run*
:Lib deps reset-first-run [plugin]  Forget the "seen" state so the popup
                                    shows again on the next show_once.

Opting out — set either anywhere in your own config, no
require("lib.nvim.deps") call needed:

    vim.g.lib_nvim_deps_disable_first_run = true              -- every plugin
    vim.g.lib_nvim_deps_disabled_plugins = { "gopath.nvim" }  -- just these
vim.g on purpose, not a setup() option: a user who installed one
plugin alone (lib.nvim pulled in only as its transitive dependency) has
no lib.nvim config block to put an option into, so the toggle must not
require one. Read fresh on every show_once call — order-independent.
Neither variable is retroactive: it stops FUTURE calls, it does not mark
anything as already-seen, so removing the opt-out later resumes normally.

deps.status *lib.nvim-deps-status*

Every declared tool, across every plugin that declares one, merged into a
single report. Backs :Lib deps status.

    local status = require("lib.nvim.deps.status")

    status.collect()   -- { tools, sources, plugins, failed } -- the merge
    status.lines()     -- string[]
    status.show()      -- the popup `:Lib deps show` opens, with i / I
    status.install()   -- plan + confirm + terminal, everything missing
:Lib deps show {plugin} answers "what does THIS plugin want", which is the
right question once you already suspect a plugin. It is the wrong one after
cloning a config onto a new machine, where the question is "what is missing
HERE" — and :Lib deps show with no argument lists which plugins ship a
spec, not what any of them lacks.

It also fixes a timing problem: the first-run popup (|lib.nvim-deps-first_run|)
rides on a plugin's setup(), so a lazy-loaded plugin shows it whenever it
first happens to load. spec.find reads lazy.nvim's registry, so this report
sees pending plugins without waiting for them (|lib.nvim-deps-resolution|).

THE MERGE. One tool with several declarers (curl is commonly wanted by
four or five) must appear ONCE — a report listing it five times buries what
else is missing — while still saying who wants it:

    required            true if ANY declarer requires it. Installing it
                        satisfies everyone; skipping it breaks that plugin.
    pkg                 unioned, first declaration wins a key. Two plugins
                        naming different packages for one binary is a bug in
                        one of them; preferring the later would hide it.
    bin_alternatives    unioned — the same program, whoever asked for it.
    why                 the first declarer's sentence. Concatenating several
                        would be a paragraph per tool.
    see                 rewritten to "wanted by a.nvim, b.nvim".

Rendering is deps.view's, unchanged: the merge produces one tool list and
one parse result, which is what view.show already takes — so the popup,
the i/I keymaps and the live streaming work here without a second
implementation.

A plugin spec.plugins() lists but whose spec cannot be read lands in
failed and is named in a warning rather than dropped: "absent from the
report" and "declares nothing" would otherwise look identical.

deps.require_tool *lib.nvim-deps-require_tool*

The failure moment. Everything else here is forward-looking — |:checkhealth|,
the first-run popup and :Lib deps show all answer "what might I need?",
read before anything breaks. The moment that actually reaches a user is the
one where a command does nothing.

    local curl = require("lib.nvim.deps").require_tool("language.nvim", "curl")
    if not curl then return end
    [language.nvim] curl not found — Carries the thesaurus lookup requests.
      install:  sudo apt install curl
      or run:   :Lib deps install language.nvim
IT RETURNS THE NAME, NOT A BOOLEAN. A tool declaring bin_alternatives is
gs on Linux and gswin64c on Windows, and a caller just told "yes, it's
here" still has to know which spelling to spawn. The returned name answers
both, and stays truthy for the if not ... then return end guard.

Options:

    silent       -- answer without reporting (caller has its own error path)
    throttle_ms  -- quiet window per (plugin, tool); default 5000, <=0 off
    manager      -- compose the install line for this manager
The throttle is a BURST GUARD, not a mute: a caller checking one tool per
file across a 200-file batch would otherwise produce 200 identical
notifications, while a user who re-runs a command a minute later has asked
twice and should be told twice.

For failure paths that hand their error upwards instead of notifying — a
callback taking (value, err), a result.errors list — lines gives the
same wording with nothing attached:

    local rt = require("lib.nvim.deps.require_tool")
    on_done({ ok = false, err = table.concat(rt.lines("nvim", "pandoc"), " ") })
Works before a plugin ships a spec: the check still runs and still reports,
it just has no why or install command to add — so call sites can move over
first and the spec can follow.

Nothing here installs anything: the message names the command and points at
:Lib deps install. Offering to install AT the failure moment would answer
a question the user did not ask.

Convenience wrappers on require("lib.nvim.deps") itself: plugins()
(re-export of spec.plugins()), show(plugin_name) (resolve + view.show)
and install_for(plugin_name) (resolve + install.plan/run) — each
notifies and returns false/nil rather than throwing when the plugin
ships no spec or its spec file cannot be read.

deps.detect *lib.nvim-deps-detect*

Is a declared tool present on this host, under any of the names it goes
by? A tool does not always have one binary name (Ghostscript is gs on
Linux/macOS, gswin64c/gswin32c on Windows) — bin stays canonical for
display and for the pkg map key; bin_alternatives only widens the
"is it here" probe. Used internally by deps.health, deps.install and
deps.view.

    local detect = require("lib.nvim.deps.detect")

    detect.names(tool)     --> { bin, ...bin_alternatives } (canonical first)
    detect.found(tool)     --> boolean
    detect.found_as(tool)  --> the name it was actually found under, or nil
    detect.forget(tool)    --> drop the memoized PATH probe for this tool

5. RESOLUTION *lib.nvim-deps-resolution*

spec.find and spec.plugins resolve in two steps:

    1. 'runtimepath' — manager-agnostic. Covers packer, vim-plug, mini.deps,
       a symlinked dev checkout, and every LOADED lazy.nvim plugin. The
       plugin name must match a whole path segment, so pdfport.nvim does
       not match a directory named my-pdfport.nvim-fork.

    2. lazy.nvim's plugin registry, consulted through pcall only if step 1
       missed and lazy.nvim is present.

Step 2 is not redundant. lazy.nvim puts a plugin on 'runtimepath' only once
it actually LOADS. Measured against a real config while building this: 120
plugins configured, 44 loaded at startup, 76 pending — and all 76 were
resolvable through lazy's dir while being absent from 'runtimepath'. A
runtimepath-only lookup would have silently failed for the majority of a
lazy-loading user's plugins, and "silently" is what makes it serious: a spec
that cannot be found looks exactly like a plugin that ships none.

lib.nvim does not take a dependency on lazy.nvim for this — step 1 remains
the primary path, and other managers put plugins on 'runtimepath' eagerly.

6. INTEGRATING A PLUGIN *lib.nvim-deps-integrating*

Three independent, optional additions — pick any subset:

    1. Ship docs/install.json (or docs/INSTALL.md), see
       |lib.nvim-deps-declaring|. Required for the other two to have
       anything to show.

    2. In the plugin's own health.lua, inside M.check():

        require("lib.nvim.deps.health").report_for("your-plugin.nvim")
       One line, replaces re-listing the same tools by hand a second time.

    3. In the plugin's own setup(), typically near the end:

        function M.setup(opts)
          ...
          require("lib.nvim.deps").show_once("your-plugin.nvim")
        end
       Shows the popup once, ever, the first time the plugin initializes
       after being installed. See |lib.nvim-deps-first_run|.

None of these make a spec file mandatory, and none run at require time —
a plugin with no spec, or that skips (2)/(3) entirely, behaves exactly as
it did before lib.nvim.deps existed.

keys

j / k
next / previous line
gg / G
first / last line
⏎
open the line under the cursor
/
search the plugins
:
command line — Tab completes
:help x
vimdoc of a plugin (:e x = plugin page)
:ls · :log · :stack
plugin list · activity stream · dependency graph
:colo x
colorscheme
:set skin=
modern | tui
?
this help
esc
close