Live-preview Neovim colorschemes before applying them.
theme-peeper.nvim captures a colorscheme inside a child Neovim process, reads the resulting highlight groups, and renders an isolated preview in your current session. Your active colorscheme is not changed until you confirm the selection.
- Live colorscheme preview before applying
- Best experience with
snacks.nvim - Optional Telescope picker support
- Built-in
vim.ui.selectfallback - Isolated floating preview window
- Child-process highlight capture
- Uses your current
runtimepath,background,termguicolors, and safe scalar globals - Configurable preview profiles
- Custom preview sample lines, spans, and highlight groups
- Custom apply function
- Custom picker support
- Custom previewer support
- In-memory capture cache
- Neovim
0.10+ nvimexecutable available on$PATH
Optional:
theme-peeper.nvim uses vim.system(), so Neovim 0.10+ is required.
This is the intended setup. Snacks gives Theme Peeper the best live-preview experience because the preview updates as you move through the picker.
{
"JohnnyJumper/theme-peeper.nvim",
dependencies = {
{
"folke/snacks.nvim",
opts = {
picker = {},
},
},
},
keys = {
{
"<leader>tp",
function()
require("theme_peeper").select()
end,
desc = "Theme Peeper",
},
},
opts = {
picker = "snacks",
previewer = "float",
preview = {
profile = "code",
max_height = 24,
border = "rounded",
placement = "center",
},
pickers = {
snacks = {
width = 56,
max_height = 12,
preview_width = 80,
preview_max_height = 24,
},
},
},
}Theme Peeper also works without extra dependencies by using vim.ui.select.
{
"JohnnyJumper/theme-peeper.nvim",
keys = {
{
"<leader>tp",
function()
require("theme_peeper").select()
end,
desc = "Theme Peeper",
},
},
opts = {
picker = "builtin",
previewer = "float",
},
}The built-in picker is a fallback. It is useful, but it does not provide the same smooth live-preview flow as Snacks.
Open the theme picker:
:ThemePeepPreview a specific colorscheme:
:ThemePeepPreview kanagawaOpen the picker from Lua:
require("theme_peeper").select()Preview a theme from Lua:
require("theme_peeper").peek("kanagawa")Apply a theme from Lua:
require("theme_peeper").apply("kanagawa")| Command | Description |
|---|---|
:ThemePeep |
Open the configured theme picker |
:ThemePeepPreview <theme> |
Preview a specific colorscheme |
require("theme_peeper").setup({
picker = "snacks",
pickers = {
snacks = {
width = 56,
max_height = 12,
preview_width = 80,
preview_max_height = 24,
},
},
})Snacks is the recommended picker backend.
When the selection changes, Theme Peeper captures the selected colorscheme and renders the preview beside the picker when possible.
If Snacks is not installed or not available, Theme Peeper falls back to the built-in picker.
require("theme_peeper").setup({
picker = "builtin",
})The built-in picker uses vim.ui.select.
It requires no dependencies. It is the safest fallback, but the experience is more limited than Snacks.
require("theme_peeper").setup({
picker = "telescope",
pickers = {
telescope = {
width = 56,
max_height = 12,
},
},
})Telescope selection movement is wired through Theme Peeper mappings so the preview updates as the selection changes.
If Telescope is not installed or not available, Theme Peeper falls back to the built-in picker.
Example full setup:
require("theme_peeper").setup({
picker = "snacks",
previewer = "float",
apply = function(theme)
vim.cmd.colorscheme(theme)
end,
capture = {
globals = {
-- Example:
-- some_theme_option = "dark",
-- some_theme_transparent = true,
},
},
preview = {
profile = "code",
max_height = 24,
zindex = 80,
border = "rounded",
placement = "center",
},
cache = {
enabled = true,
},
mappings = {
preview = {
close = { "q", "<Esc>" },
},
telescope = {
next = {
insert = { "<Down>", "<C-n>" },
normal = { "j", "<Down>" },
},
previous = {
insert = { "<Up>", "<C-p>" },
normal = { "k", "<Up>" },
},
},
},
pickers = {
builtin = {
max_height = 12,
},
snacks = {
width = 56,
max_height = 12,
row = 0.5,
col = 0.5,
preview_width = 80,
preview_max_height = 24,
},
telescope = {
width = 56,
max_height = 12,
},
},
})Theme Peeper includes built-in preview profiles.
require("theme_peeper").setup({
preview = {
profile = "code",
},
})Available profiles:
| Profile | Description |
|---|---|
code |
General code-oriented preview |
diagnostics |
Diagnostics and code sample preview |
ui |
UI highlight groups such as floats, menus, statusline, separators, and line numbers |
minimal |
Compact preview |
Example:
require("theme_peeper").setup({
preview = {
profile = "ui",
},
})You can provide custom preview lines and highlight spans.
require("theme_peeper").setup({
preview = {
sample_lines = {
"package main",
"",
"func main() {",
' println("hello")',
"}",
},
spans = {
{ line = 1, word = "package", group = "Keyword" },
{ line = 1, word = "main", group = "Identifier" },
{ line = 3, word = "func", group = "Keyword" },
{ line = 3, word = "main", group = "Function" },
{ line = 4, word = '"hello"', group = "String" },
},
},
})A span can target text by word:
{ line = 1, word = "local", group = "Keyword" }By Lua pattern:
{ line = 1, pattern = "function%s+[%w_]+", group = "Function" }By explicit columns:
{ line = 1, start_col = 0, end_col = 5, group = "Keyword" }Columns are zero-based.
Use groups when you want the preview namespace to define additional highlight groups even if they are not directly used by spans.
require("theme_peeper").setup({
preview = {
groups = {
"DiffAdd",
"DiffChange",
"DiffDelete",
"GitSignsAdd",
"GitSignsChange",
"GitSignsDelete",
},
},
})By default, confirming a theme runs:
vim.cmd.colorscheme(theme)Override apply when your theme switch needs extra work.
require("theme_peeper").setup({
apply = function(theme)
vim.cmd.colorscheme(theme)
vim.notify("Applied colorscheme: " .. theme)
end,
})This is useful when applying a theme also needs to update plugin state, persist a config value, or refresh UI components.
Theme Peeper captures themes by launching a child Neovim process.
The child process receives:
- Current
runtimepath - Current
termguicolors - Current
background - Safe scalar
vim.gvalues - Explicit
capture.globals
Then it runs:
vim.cmd.colorscheme(payload.theme)After that, it reads the effective highlight groups and returns them to the parent process.
This keeps your current Neovim session unchanged while still producing a realistic preview.
Some themes use global variables for configuration.
You can pass extra globals into the child process:
require("theme_peeper").setup({
capture = {
globals = {
some_theme_option = "mocha",
some_theme_transparent = true,
},
},
})Explicit globals override inherited globals with the same name.
Theme Peeper does not replay arbitrary Lua setup code inside the child process.
For example, this setup is not automatically replayed:
require("some-theme").setup({
style = "dark",
integrations = {
telescope = true,
},
})If a theme depends on setup-time Lua tables or functions, the preview may not perfectly match your fully configured session.
Themes that expose scalar globals or mainly rely on normal colorscheme files should preview closely.
Theme captures are cached in memory.
Caching is enabled by default:
require("theme_peeper").setup({
cache = {
enabled = true,
},
})Disable cache globally:
require("theme_peeper").setup({
cache = {
enabled = false,
},
})Disable cache for a single preview call:
require("theme_peeper").preview("kanagawa", {
cache = false,
})The cache key includes the capture payload. Changes to inherited globals, explicit globals, runtimepath, background, or termguicolors can create a new cache entry.
Default float preview configuration:
require("theme_peeper").setup({
preview = {
profile = "code",
max_height = 24,
zindex = 80,
border = "rounded",
placement = "center",
},
})Supported placement values:
| Placement | Description |
|---|---|
center |
Center the preview in the editor |
attached |
Attach the preview to an anchor, placing it below when there is space and beside it when there is not |
below |
Use anchor-based placement; currently follows the same auto-fit behavior as attached |
Picker integrations may override placement internally to keep the picker and preview visually connected.
Default preview mappings:
require("theme_peeper").setup({
mappings = {
preview = {
close = { "q", "<Esc>" },
},
},
})Disable preview mappings:
require("theme_peeper").setup({
mappings = {
preview = false,
},
})Use custom close keys:
require("theme_peeper").setup({
mappings = {
preview = {
close = { "q", "<C-c>" },
},
},
})Default Telescope movement mappings:
require("theme_peeper").setup({
mappings = {
telescope = {
next = {
insert = { "<Down>", "<C-n>" },
normal = { "j", "<Down>" },
},
previous = {
insert = { "<Up>", "<C-p>" },
normal = { "k", "<Up>" },
},
},
},
})Disable Theme Peeper Telescope mappings:
require("theme_peeper").setup({
mappings = {
telescope = false,
},
})You can provide a custom picker function.
require("theme_peeper").setup({
picker = function(actions, opts)
local themes = actions.list()
vim.ui.select(themes, {
prompt = "Select colorscheme",
}, function(theme)
if not theme then
actions.close()
return
end
actions.confirm(theme)
end)
end,
})A custom picker receives:
| Argument | Description |
|---|---|
actions |
Theme Peeper action API |
opts |
Picker options |
Available actions:
actions.list()
actions.capture(theme, opts)
actions.render(opts)
actions.preview(theme, opts)
actions.apply(theme)
actions.confirm(theme, opts)
actions.close()You can provide a custom previewer function.
require("theme_peeper").setup({
previewer = function(ctx)
-- ctx.theme
-- ctx.captured
-- ctx.opts
-- ctx.actions
-- ctx.render
-- ctx.buf
-- ctx.win
return ctx.render({
buf = ctx.buf,
win = ctx.win,
captured = ctx.captured,
preview = ctx.opts,
})
end,
})A custom previewer receives:
| Field | Description |
|---|---|
ctx.theme |
Theme name being previewed |
ctx.captured |
Captured highlight data |
ctx.opts |
Merged preview options |
ctx.actions |
Theme Peeper action API |
ctx.render |
Renderer function |
ctx.buf |
Optional buffer passed by caller |
ctx.win |
Optional window passed by caller |
Configure the plugin.
require("theme_peeper").setup({
picker = "snacks",
})Open the configured picker.
require("theme_peeper").select()Preview a theme by name.
local ok, err = require("theme_peeper").preview("kanagawa")With options:
require("theme_peeper").preview("kanagawa", {
cache = false,
placement = "center",
})Preview a theme by name and notify on error.
require("theme_peeper").peek("kanagawa")Apply a theme using the configured apply function.
require("theme_peeper").apply("kanagawa")Close the preview and apply a theme.
require("theme_peeper").confirm("kanagawa")Keep the preview open after confirming:
require("theme_peeper").confirm("kanagawa", {
close_preview = false,
})Capture a theme and return its highlight data.
local captured, err = require("theme_peeper").capture("kanagawa")Close the active preview window.
require("theme_peeper").close()Return available colorschemes.
local themes = require("theme_peeper").list()Make sure the colorscheme name is valid:
:colorscheme <Tab>Then try:
:ThemePeepPreview exact-theme-nameThe child process receives runtimepath, basic editor options, safe scalar globals, and explicit capture globals.
It does not replay arbitrary Lua setup code.
If your theme requires setup code, make sure its relevant options are available through globals or provide explicit capture.globals.
Make sure snacks.nvim is installed and its picker module is enabled.
Example:
{
"folke/snacks.nvim",
opts = {
picker = {},
},
}Make sure telescope.nvim is installed and loadable before opening Theme Peeper.
The first preview of a colorscheme starts a child Neovim process and captures highlights.
After that, cached previews should be faster.
Theme Peeper avoids applying themes directly during preview.
Instead, it:
- Starts a child Neovim process
- Loads the target colorscheme there
- Captures effective highlight groups
- Sends the captured data back to the parent process
- Renders the preview using an isolated highlight namespace
The selected theme is only applied when you confirm it.
MIT
