pj.nvim
February 2, 2026 ยท View on GitHub
A Neovim plugin for quickly finding and navigating to projects using pj.
Features
- ๐ Fast project discovery using the pj binary
- ๐จ Multiple picker UIs: Snacks, Telescope, fzf-lua, tv (television), mini.pick
- ๐ Fuzzy search through your projects
- ๐ Instantly switch to project directories
- ๐พ Leverages pj's intelligent caching
- ๐ฏ Icon support with Nerd Fonts
- โจ๏ธ Consistent keybindings across all pickers
- ๐ช Split, vsplit, and tab support
- ๐๏ธ Tab-local directory changing (matches Snacks behavior)
- ๐ผ Optional session manager integration (auto-session, persistence.nvim)
- ๐ง Extensible architecture for adding more pickers
- ๐ฆ Automatic pj binary installation - Downloads the correct binary for your platform
- ๐ Automatic updates - Keeps the pj binary up to date automatically
Requirements
Core:
- Neovim >= 0.9.0
curl- For automatic binary download (optional if pj is installed manually)
pj Binary: The pj binary is automatically downloaded on first use. No manual installation required! The plugin detects your platform (macOS, Linux, or Windows) and architecture (Intel or ARM) and downloads the appropriate binary.
Important: If you have pj installed globally (in your PATH), it will be used instead of the auto-downloaded version. The plugin prefers system binaries by default (configurable via prefer_system option).
If you prefer to install pj manually, see Manual pj Installation.
Picker UI (choose one or more):
- Snacks.nvim - For
snackspicker (default) - Telescope.nvim - For
telescopepicker - fzf-lua - For
fzf_luapicker - television - For
tvpicker - mini.pick or mini.nvim - For
mini.pickpicker
Optional:
- Nerd Fonts - For icon display
- auto-session - For session management
- persistence.nvim - Alternative session manager
Installation
With Snacks (default picker)
-- Using lazy.nvim
{
"josephschmitt/pj.nvim",
dependencies = {
"folke/snacks.nvim",
},
cmd = { "Pj", "PjCd" },
keys = {
{ "<leader>fp", "<cmd>Pj<cr>", desc = "Find Projects" },
},
opts = {},
}
With Telescope
-- Using lazy.nvim
{
"josephschmitt/pj.nvim",
dependencies = {
"nvim-telescope/telescope.nvim",
},
cmd = { "Pj", "PjCd" },
keys = {
{ "<leader>fp", "<cmd>Pj<cr>", desc = "Find Projects" },
},
opts = {
picker = { type = "telescope" },
},
}
With fzf-lua
-- Using lazy.nvim
{
"josephschmitt/pj.nvim",
dependencies = {
"ibhagwan/fzf-lua",
},
cmd = { "Pj", "PjCd" },
keys = {
{ "<leader>fp", "<cmd>Pj<cr>", desc = "Find Projects" },
},
opts = {
picker = { type = "fzf_lua" },
},
}
With television
-- Using lazy.nvim
{
"josephschmitt/pj.nvim",
cmd = { "Pj", "PjCd" },
keys = {
{ "<leader>fp", "<cmd>Pj<cr>", desc = "Find Projects" },
},
opts = {
picker = { type = "tv" },
},
}
Note: The tv picker requires the television binary to be installed. It does not require tv.nvim.
With mini.pick
-- Using lazy.nvim
{
"josephschmitt/pj.nvim",
dependencies = {
"nvim-mini/mini.pick", -- or "nvim-mini/mini.nvim"
},
cmd = { "Pj", "PjCd" },
keys = {
{ "<leader>fp", "<cmd>Pj<cr>", desc = "Find Projects" },
},
opts = {
picker = { type = "mini.pick" },
},
}
With All Pickers (for flexibility)
-- Using lazy.nvim
{
"josephschmitt/pj.nvim",
dependencies = {
"folke/snacks.nvim",
"nvim-telescope/telescope.nvim",
"ibhagwan/fzf-lua",
"nvim-mini/mini.pick",
},
cmd = { "Pj", "PjCd" },
keys = {
{ "<leader>fp", "<cmd>Pj<cr>", desc = "Find Projects" },
},
opts = {
-- You can switch between pickers anytime by changing the type
picker = { type = "snacks" }, -- or "telescope", "fzf_lua", "tv", or "mini.pick"
},
}
Configuration
Default Configuration
require("pj").setup({
-- pj binary settings
pj = {
cmd = "auto", -- "auto" (default), "pj", or "/path/to/pj"
args = {}, -- Additional arguments to pass to pj
icons = true, -- Use icons in the picker
cache = true, -- Use pj's built-in cache
-- Auto-download settings (used when cmd = "auto")
auto = {
prefer_system = true, -- Use system binary if available (default: true)
check_updates = true, -- Check for newer versions periodically
auto_update = true, -- Automatically install updates when found
update_interval = 7, -- Days between update checks
github_repo = "josephschmitt/pj", -- GitHub repo for releases
},
},
-- Picker settings
picker = {
type = "snacks", -- Picker type: "snacks", "telescope", "fzf_lua", "tv", or "mini.pick"
-- fzf-lua specific settings
fzf_lua = {
winopts = {
height = 0.85,
width = 0.80,
},
preview = {
enabled = false, -- Enable preview window
cmd = "ls -la", -- Command to show preview
},
},
-- telescope specific settings
telescope = {
theme = nil, -- "dropdown", "ivy", "cursor", or nil for default
layout_config = {
width = 0.8,
height = 0.9,
},
previewer = false, -- Enable file previewer
},
-- tv (television) specific settings
tv = {
tv_binary = "tv", -- Path to tv binary
preview = {
enabled = false, -- Enable preview window
cmd = "ls -la {}", -- Preview command (use {} as placeholder)
size = 50, -- Preview window size percentage
},
},
},
-- Behavior settings
behavior = {
cd_on_select = true, -- Change directory when selecting a project
cd_scope = "tab", -- "tab" (tcd) or "global" (cd)
close_on_select = true, -- Close picker after selection
notify_on_error = true, -- Show error notifications
session_manager = nil, -- nil, "auto-session", or "persistence"
},
-- Keymaps (within the picker)
keymaps = {
open = "<CR>", -- Open project
split = "<C-x>", -- Open in horizontal split
vsplit = "<C-v>", -- Open in vertical split
tab = "<C-t>", -- Open in new tab
depth_increase = "<C-l>", -- Increase search depth
depth_decrease = "<C-h>", -- Decrease search depth
},
-- Depth settings for project tree display
depth = {
initial = nil, -- Starting depth (nil = use pj's default of 3)
min = 1, -- Minimum depth
max = 10, -- Maximum depth
},
})
Session Management Integration
pj.nvim can integrate with session manager plugins to automatically restore your workspace when switching projects. This matches the behavior of Snacks' projects picker.
With auto-session
require("pj").setup({
behavior = {
session_manager = "auto-session",
cd_scope = "tab", -- Recommended with session managers
},
})
With persistence.nvim
require("pj").setup({
behavior = {
session_manager = "persistence",
cd_scope = "tab",
},
})
When a session manager is configured:
- Selecting a project will try to restore its session
- If no session exists, it just changes the directory
- Open buffers are closed/restored based on the session
- Your workspace state (windows, buffers, etc.) is preserved per-project
Simple directory change (no sessions)
require("pj").setup({
behavior = {
session_manager = nil, -- Disable session management
cd_scope = "global", -- Use global cd instead of tcd
},
})
This simpler mode just changes the working directory without affecting buffers.
Picker-Specific Configuration
Using Telescope with dropdown theme
require("pj").setup({
picker = {
type = "telescope",
telescope = {
theme = "dropdown",
previewer = true,
layout_config = {
width = 0.9,
height = 0.8,
},
},
},
})
Using fzf-lua with preview
require("pj").setup({
picker = {
type = "fzf_lua",
fzf_lua = {
winopts = {
height = 0.9,
width = 0.9,
preview = {
layout = "vertical",
vertical = "up:45%",
},
},
preview = {
enabled = true,
cmd = "tree -C -L 2",
},
},
},
})
Using television with preview
require("pj").setup({
picker = {
type = "tv",
tv = {
preview = {
enabled = true,
cmd = "tree -C -L 2 {}",
size = 70,
},
},
},
})
Using mini.pick with custom window
require("pj").setup({
picker = {
type = "mini.pick",
mini = {
window = {
config = {
width = 80,
height = 20,
},
},
},
},
})
Custom pj configuration
require("pj").setup({
pj = {
args = { "--path", "~/work", "--path", "~/personal" },
icons = true,
},
picker = {
type = "telescope", -- Use your preferred picker
},
})
Disable automatic directory change
require("pj").setup({
behavior = {
cd_on_select = false,
},
})
Use a specific pj binary
-- Use system pj (must be in PATH)
require("pj").setup({
pj = { cmd = "pj" },
})
-- Or specify exact path
require("pj").setup({
pj = { cmd = "/usr/local/bin/pj" },
})
Disable auto-download (require manual installation)
require("pj").setup({
pj = {
cmd = "pj", -- Will error if pj is not in PATH
},
})
Usage
Commands
:Pj- Open the project picker:Pj depth=N- Open picker at specific depth (e.g.,:Pj depth=2):PjCd- Open picker (alias for changing directory):PjCheckUpdates- Check for pj binary updates:PjUpdate- Update pj binary to latest version:PjReinstall- Force reinstall pj binary (for troubleshooting):PjUninstall- Remove auto-downloaded pj binary
Keymaps
The plugin doesn't set any global keymaps by default. Add your own:
vim.keymap.set("n", "<leader>fp", "<cmd>Pj<cr>", { desc = "Find Projects" })
vim.keymap.set("n", "<leader>fP", "<cmd>PjCd<cr>", { desc = "Change to Project" })
Lua API
-- Open the picker
require("pj").open()
-- Open with runtime options
require("pj").open({ no_cache = true })
-- Change directory only
require("pj").cd()
How It Works
- Project Discovery: The plugin calls the
pjbinary to discover projects in your configured directories - Caching: pj uses intelligent caching to make subsequent searches nearly instant
- Fuzzy Search: Snacks.nvim provides a beautiful picker UI with fuzzy search
- Navigation: Selecting a project changes Neovim's working directory to that project
Health Check
Run :checkhealth pj to verify your installation and configuration.
The health check will verify:
- pj binary status (auto-download mode, system binary, or custom path)
- Platform detection and compatibility
- Your configured picker is available (Snacks/Telescope/fzf-lua/tv/mini.pick)
- Configuration is valid
- Projects can be discovered
- Show all available pickers and their status
Troubleshooting
Binary Priority
When pj.cmd = "auto" (the default), the plugin uses binaries in this order:
- System binary - If pj is found in your PATH (when
prefer_system = true) - Auto-downloaded binary - Falls back to cached binary if no system binary found
- Auto-download - Downloads binary on first use if neither exists
This means if you install pj globally later, it will automatically be used instead of the auto-downloaded version.
"pj binary not found"
By default, pj.nvim automatically downloads the pj binary on first use. If auto-download fails:
- Check curl is installed - Required for downloading
- Check your internet connection
- Run
:checkhealth pj- Shows detailed status and troubleshooting info
You can also manually trigger a download:
:lua require('pj.binary').ensure_binary()
Manual pj Installation
If you prefer to install pj manually instead of using auto-download:
# Using Go
go install github.com/josephschmitt/pj@latest
# Or download from GitHub releases
# https://github.com/josephschmitt/pj/releases
Then configure pj.nvim to use the system binary:
require("pj").setup({
pj = { cmd = "pj" } -- Use system binary instead of auto-download
})
Or specify a custom path:
require("pj").setup({
pj = { cmd = "/path/to/pj" }
})
Picker not found errors
If you get errors about missing picker dependencies:
For Snacks picker:
-- Make sure snacks.nvim is installed
{
"folke/snacks.nvim",
-- your snacks config
}
For Telescope picker:
-- Make sure telescope is installed
{
"nvim-telescope/telescope.nvim",
dependencies = { "nvim-lua/plenary.nvim" }
}
For fzf-lua picker:
-- Make sure fzf-lua is installed
{
"ibhagwan/fzf-lua",
dependencies = { "nvim-tree/nvim-web-devicons" } -- optional for icons
}
For tv picker:
# Install television binary (choose one method)
# macOS (Homebrew)
brew install television
# Cargo (Rust)
cargo install television
# Or download from: https://github.com/alexpasmantier/television/releases
For mini.pick picker:
-- Make sure mini.pick is installed
{
"nvim-mini/mini.pick",
-- or use the full mini.nvim if you want other mini modules
-- "nvim-mini/mini.nvim",
}
Switching between pickers
You can easily switch pickers by changing the configuration:
require("pj").setup({
picker = {
type = "telescope", -- Change to "snacks", "telescope", "fzf_lua", "tv", or "mini.pick"
},
})
"No projects found"
Check your pj configuration at ~/.config/pj/config.yaml:
search_paths:
- ~/projects
- ~/code
- ~/development
markers:
- .git
- go.mod
- package.json
Run pj in your terminal to verify it's working correctly.
Icons not showing
Make sure you have a Nerd Font installed and configured in your terminal.
Future Enhancements
- Support for Telescope picker
- Support for FZF picker
- Support for television picker
- Automatic pj binary installation
- Automatic pj binary updates
- Custom project actions
- Recent projects tracking
- Project-specific configurations
Development
Running Tests
The plugin uses mini.test for testing.
# Install test dependencies
make deps
# Run all tests
make test
# Run a specific test file
make test-file FILE=tests/unit/depth_spec.lua
Test Structure
tests/
โโโ init.lua # Test runner entry point
โโโ helpers.lua # Shared test utilities and mocks
โโโ minimal_init.lua # Minimal Neovim config for tests
โโโ unit/ # Unit tests
โ โโโ config_spec.lua # Configuration tests
โ โโโ depth_spec.lua # Depth state machine tests
โ โโโ finder_spec.lua # Parser function tests
โ โโโ icons_spec.lua # Icon lookup tests
โ โโโ session_spec.lua # Session/directory switching tests
โ โโโ utils_spec.lua # Utility function tests
โโโ fixtures/ # Test data
โโโ projects.json # Sample JSON output from pj
โโโ projects.txt # Sample text output with icons
โโโ projects_no_icons.txt
Git Hooks
This project uses lefthook for pre-push hooks to run tests automatically.
# Install lefthook (choose one)
brew install lefthook
# or: npm install -g lefthook
# or: go install github.com/evilmartians/lefthook@latest
# Enable hooks for this repo
lefthook install
Once installed, tests will run automatically before each git push.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Before submitting, please ensure tests pass:
make test
License
MIT
Related Projects
- pj - The underlying project finder
- Snacks.nvim - The picker UI framework