Switchboard lets you run project commands without leaving Neovim. It picks the right command based on the file or project and runs it in a split, floating window, or tmux.
It’s designed so the same keybinds work across different languages and projects.
Added bonus, check out this shell script to use the local .commands defined actions from shell interactively. Basically running same commands in shell on different projects for similar actions.
- Runs commands in splits, floating windows, or tmux
- Supports project-aware editor actions (dynamic vim keybinds)
- Chooses commands based on file type or project
- Falls back to
:termif tmux isn’t available - Lets you define your own commands (build, run, or anything else)
- Supports local project-specific overrides
Using the built-in package manager (Neovim 0.12+):
vim.pack.add({
'https://github.com/karnull/switchboard.nvim',
})You define what “run”, “build”, or any editor action means per language or project:
require('switchboard').setup({
commands = {
lazygit = "lazygit",
},
build_run_config = {{
extension = {'py'},
commands = {
run = 'uv run main',
build = 'uv run pyinstaller bin ./*/__main__.py',
},
binds = {
divide = 'I#<Esc>79A=<Esc>o',
import = 'Iimport ',
}
},{
extension = {'c', 'cpp', 'h'},
cd_root = true,
commands = {
run = 'make run',
build = 'make',
debug = 'gdb ./bin',
},
binds = {
divide = 'I//<Esc>78A=<Esc>o',
import = 'I#include ',
}
}}
})Then you bind keys once, and reuse them everywhere:
-- Commands:
vim.keymap.set('n', '<F5>', ':Switchboard split run<CR>', { silent = true })
vim.keymap.set('n', '<F7>', ':Switchboard vsplit debug<CR>', { silent = true }) -- works only for C/C++ projects
vim.keymap.set('n', '<F6>', ':Switchboard quickfix build<CR>', { silent = true }) -- :make extended
vim.keymap.set('n', '<leader>g', ':Switchboard overlay lazygit<CR>', { silent = true }) -- works for all projects
-- Keybinds:
vim.keymap.set('n', '<leader>id', ':Switchboard bind divide<CR>', { silent = true })
vim.keymap.set({'n','i'}, '<leader>ii', '<Esc>:Switchboard bind import<CR>', { silent = true })
-- Append <Esc> before :Switchboard for insert mode maps
Switchboard handles the rest.
:Switchboard <mode> <command-name>Modes:
overlay– floating window overlaysplit– opens a horizontal splitvsplit– opens a vertical splitbackground– background tmux window (or background term buffer)quickfix- run the command in quickfix mode and:copen
Commands come from:
- global neovim config
- file type config - in neovim config
- project overrides - in project root
So run in a Python project can mean something completely different from run in Rust.
:Switchboard bind <bind-name>Binds run configured editor actions. For example, divide can insert a Python-style divider in Python files and a C-style divider in C files.
require('switchboard').setup({
-- General settings (optional)
save_session = false, -- Save files before executing
build_run_window_title = "build", -- Tmux window name
notify_missing_project_config = false,
local_config = ".switchboard-config",
-- Window sizing (optional)
new_pane_everytime = false,
side_width_percent = 50,
bottom_height_percent = 30,
overlay_width_percent = 80,
overlay_height_percent = 80,
overlay_sleep = -1, -- -1 = no auto-close
build_run_config = {{
...
}},
-- Override individual projects configs
-- Example: for zephyr projects, use zephyr to build instead of set C/C++ configs
project_override_config = {{
project_base_dir = '~/Projects/MyProject',
commands = {
build = 'west build',
}
}}
})Alternatively, have a look at my personal config.
You can override commands per project by adding a file at the root:
-- .switchboard-config
return {
cd_root = true,
commands = {
run = 'npm start',
build = 'npm run build',
test = 'npm test',
},
binds = {
import = 'ggOimport <Esc>',
}
}In short: define commands and editor actions once, use the same keybinds everywhere, and let Switchboard adapt to the project you’re in.