A thin wrapper around Neovim's built-in vim.pack with a declarative, lazy.nvim-style config.
- Built-in Power: Wraps Neovim's native
vim.packwithout reinventing the wheel. - Familiar Syntax: Declarative
lazy.nvim-style configuration for plugins. - Lazy Loading: Native support for
event,ft, andcmdlazy-load triggers. - Zero Dependencies: Pure Lua, requiring only Neovim 0.12+ and Git.
- Auto Update: Built-in periodic update checker with configurable interval.
- Commands:
:Pickcommand with subcommands for managing plugins. - Lightweight: Tiny codebase (~270 lines).
- Neovim >= 0.12 (requires
vim.packfeature) - git installed and available in
$PATH
Clone into any directory and add it to your runtimepath before calling setup():
-- init.lua
vim.opt.rtp:prepend("~/.config/nvim/pack/pick/start/pick.nvim")
-- or wherever you cloned the repoOr bootstrap using vim.pack itself:
local pick_path = vim.fn.stdpath("data") .. "/site/pack/core/opt/pick.nvim"
if not vim.uv.fs_stat(pick_path) then
vim.system({ "git", "clone", "https://github.com/xhconceit/pick.nvim", pick_path }):wait()
end
vim.cmd.packadd("pick.nvim")require("pick").setup({
add = { confirm = false },
plugins = {
-- 1. Load immediately
{
"folke/tokyonight.nvim",
config = function()
vim.cmd.colorscheme("tokyonight")
end,
},
-- 2. Lazy load on event
{
"lewis6991/gitsigns.nvim",
event = "BufReadPost",
config = function()
require("gitsigns").setup()
end,
},
-- 3. Lazy load on command
{
"folke/which-key.nvim",
cmd = "WhichKey",
config = function()
require("which-key").setup()
end,
},
-- 4. With dependencies
{
"neovim/nvim-lspconfig",
ft = { "lua", "python" },
dependencies = {
"williamboman/mason.nvim",
{
"williamboman/mason-lspconfig.nvim",
config = function()
require("mason-lspconfig").setup()
end,
},
},
config = function()
require("lspconfig").lua_ls.setup({})
end,
},
},
})Each entry in plugins supports the following fields:
| Field | Type | Description |
|---|---|---|
[1] |
string |
Plugin source shorthand, e.g. "folke/tokyonight.nvim". |
src |
string |
Plugin source. Takes precedence over [1] if both exist. |
name |
string? |
Optional plugin name. |
version |
string? |
Version constraint (branch, tag, commit, or vim.version.range()). |
data |
any? |
Passed through to vim.pack.Spec.data. |
lazy |
boolean? |
Lazy-load the plugin. Defaults to true when event, ft, or cmd is set. |
event |
string|string[]? |
Load on autocmd event(s). |
ft |
string|string[]? |
Load on filetype(s). |
cmd |
string|string[]? |
Load on user command(s). |
dependencies |
(string|table)[]? |
Plugins to load before this one. Each entry can be a "author/repo" string or a full spec table. |
build |
string|fun()? |
Run after install/update. String is executed as a vim command; function is called directly. |
init |
fun()? |
Runs before the plugin is loaded. Use for vim.g.* or vim.opt.* settings. |
config |
fun()? |
Runs after the plugin is loaded. Use for require("plugin").setup(). |
The top-level add table is passed through to every vim.pack.add() call:
require("pick").setup({
add = {
confirm = false, -- skip install confirmation
-- load: boolean|function (see :help vim.pack.add())
},
plugins = { ... },
})The checker table enables automatic periodic update checks:
require("pick").setup({
checker = {
enabled = true, -- enable auto update check (default: false)
frequency = 86400, -- check interval in seconds (default: 86400 = 1 day)
force = false, -- true: silent update; false: show confirmation buffer (default: false)
},
plugins = { ... },
})| Field | Type | Default | Description |
|---|---|---|---|
enabled |
boolean? |
false |
Enable the auto update checker. |
frequency |
number? |
86400 |
Check interval in seconds. |
force |
boolean? |
false |
If true, updates are applied silently. If false, a confirmation buffer is shown. |
The last check time is persisted in stdpath("state")/pick.json, so it survives across sessions. When force = true, the checker reschedules itself for long-running sessions; when force = false, it only triggers once per Neovim startup (to avoid repeatedly popping up the confirmation buffer).
When event, ft, or cmd is specified (or lazy = true), the plugin is registered with vim.pack.add(..., { load = false }) and loaded later via a trigger:
eventβ creates a one-shotautocmd; plugin loads when the event fires.ftβ creates a one-shotFileTypeautocmd with the given pattern(s).cmdβ registers placeholder user commands; the first invocation loads the plugin and re-executes the command.
Multiple triggers can coexist (e.g. event + cmd). The plugin is loaded only once regardless of which trigger fires first.
The dependencies field ensures that required plugins are installed and loaded before the main plugin:
{
"hrsh7th/nvim-cmp",
event = "InsertEnter",
dependencies = {
"hrsh7th/cmp-nvim-lsp",
"hrsh7th/cmp-buffer",
},
config = function()
require("cmp").setup({ ... })
end,
}Each dependency can be a simple "author/repo" string, or a table with its own name, version, and config fields.
Note: pick.nvim does not resolve nested or transitive dependencies. Keep your dependency lists flat.
The build field runs after a plugin is installed or updated (via the PackChanged event):
- String β executed as a vim command (e.g.
":TSUpdate") - Function β called directly
{
"nvim-treesitter/nvim-treesitter",
event = "BufReadPost",
build = ":TSUpdate",
config = function()
require("nvim-treesitter.configs").setup({
ensure_installed = { "lua", "vimdoc" },
})
end,
},
{
"neovim/nvim-lspconfig",
ft = { "lua", "python" },
config = function()
require("lspconfig").lua_ls.setup({})
require("lspconfig").pyright.setup({})
end,
}pick.nvim provides a :Pick command with the following subcommands:
| Command | Description |
|---|---|
:Pick |
Update all plugins (same as :Pick update). |
:Pick update [name ...] |
Update all or specified plugins. Shows a confirmation buffer. |
:Pick del <name ...> |
Delete specified plugin(s). |
:Pick list |
List all managed plugins with their status. |
:Pick check |
Manually trigger an update check (resets the checker timer). |
All subcommands support Tab completion for both subcommand names and plugin names.
Main entry point. Registers all plugins and sets up lazy-load triggers.
Thin wrapper around vim.pack.update().
Thin wrapper around vim.pack.del().
Thin wrapper around vim.pack.get().
If you want to use pick.nvim alongside lazy.nvim, be aware that lazy.nvim resets packpath by default via performance.reset_packpath. This removes the site/pack directory that vim.pack relies on.
To avoid conflicts, disable this in your lazy.nvim config:
require("lazy").setup({
-- your plugins ...
}, {
performance = {
reset_packpath = false,
},
})Additionally, pick.nvim must be loaded before lazy.nvim. Otherwise, lazy.nvim will have already modified packpath during its initialization, and the plugins registered by pick.nvim will not be discoverable. In your init.lua, call require("pick").setup({...}) first, then require("lazy").setup({...}).
Alternatively, keep the two sets of plugins completely separate β use lazy.nvim for plugins that need its advanced features, and pick.nvim for simple ones managed by vim.pack.
- lazy.nvim β The modern plugin manager that inspired pick.nvim's declarative configuration style.
MIT