Focal.nvim
Image hover previews for Neo-tree, Oil, Nvim-tree, Snacks, and Mini.files.
Install / Use
/learn @hmdfrds/Focal.nvimREADME
focal.nvim
Universal file preview for Neovim. Hover over a file in any explorer, see a preview.

focal.nvim is an extensible preview framework with dual backends (pixel-perfect via image.nvim, Unicode fallback via chafa) and a plugin adapter system that works with any file explorer.
| image.nvim (pixel-perfect) | chafa (universal fallback) |
|:-:|:-:|
|
|
|
Features
- Zero-Friction Hover — automatically previews images when your cursor rests on them
- Multi-Explorer Support — works out of the box with neo-tree, nvim-tree, oil.nvim, snacks.nvim, mini.files
- Dual Backend — pixel-perfect rendering via image.nvim on supported terminals, with automatic chafa fallback for universal Unicode/ANSI preview on any terminal
- Extensible Sources — register your own adapters for unsupported file explorers
- Extensible Renderers — register custom renderers for new file types (PDF, video thumbnails, etc.)
- Content Swap — moving between images keeps the window open and swaps content in-place (no flicker)
- Render Cache — LRU cache makes re-hovering the same image instant
- Runtime Control — enable, disable, toggle previews without restarting
- Manual Trigger —
:FocalShow [path]previews any file on demand - Configurable — border, transparency, position, size constraints, file size limits
- Performance Guard — automatically skips large files to prevent editor freezes
- Diagnostics —
:checkhealth focaland:FocalStatusfor troubleshooting
Requirements
- Neovim >= 0.10
- At least one rendering backend:
- image.nvim — pixel-perfect graphics (Kitty, WezTerm, Ghostty, iTerm2, foot, Konsole)
- chafa — universal Unicode/ANSI fallback (any terminal with 256-color or truecolor)
- A file explorer: neo-tree, nvim-tree, oil.nvim, snacks.nvim, or mini.files
Installation
Using lazy.nvim:
{
"hmdfrds/focal.nvim",
event = "VeryLazy",
dependencies = {
"3rd/image.nvim", -- optional if using chafa backend
},
opts = {
-- See Configuration below
},
}
Generic:
require("focal").setup({})
Tip: Neovim's default
updatetimeis 4000ms, which means previews take 4 seconds to appear. Most users set it lower for responsive LSP diagnostics, which also makes focal snappier:vim.o.updatetime = 300
Configuration
All options with their defaults:
require("focal").setup({
-- Runtime toggle
enabled = true,
-- Window appearance
border = "rounded", -- border style (any valid nvim_open_win border)
winblend = 0, -- transparency (0-100)
zindex = 100, -- float stacking order
title = true, -- show filename in border
-- Size constraints (in terminal cells)
min_width = 10,
min_height = 5,
max_width = 80,
max_height = 40,
max_width_percent = 50, -- max width as % of editor
max_height_percent = 50, -- max height as % of editor
-- Performance
max_file_size_mb = 5, -- skip files larger than this
debounce_ms = 0, -- additional delay after CursorHold (0 = use updatetime)
-- Position
col_offset = 4, -- horizontal gap from cursor
row_offset = 1, -- vertical gap from cursor
-- Renderer override
backend = nil, -- nil/"auto" = auto, "image.nvim", or "chafa"
-- Extension whitelist (nil = all renderer-declared extensions)
extensions = nil, -- e.g., { "png", "jpg" } to restrict
-- Chafa-specific options
chafa = {
format = "symbols", -- chafa --format flag
color_space = nil, -- nil = auto, "rgb", "din99d"
animate = false, -- allow GIF animation
max_output_bytes = 1048576, -- stdout cap (1MB)
},
-- Render timeout (ms). Auto-hides if render takes too long.
render_timeout_ms = 10000,
-- Lifecycle hooks
on_show = nil, -- fun(path: string, renderer: string)
on_hide = nil, -- fun()
})
Note:
updatetimecontrols how quickly previews appear (it's Neovim's CursorHold delay). Many users set it to 300-500ms for responsive LSP diagnostics, which also makes focal more responsive. The default (4000ms) will feel slow.
Custom Sources
Register adapters for unsupported file explorers. Sources can be registered before or after setup() — order doesn't matter:
require("focal").register_source({
filetype = "my_explorer",
get_path = function()
-- return the absolute path of the file under cursor, or nil
local node = require("my_explorer").get_current_node()
if node and node.type == "file" then
return node.absolute_path
end
return nil
end,
})
With lazy.nvim, use opts as normal — focal handles queuing:
{
"hmdfrds/focal.nvim",
event = "VeryLazy",
dependencies = { "3rd/image.nvim" },
opts = {},
init = function()
require("focal").register_source({
filetype = "my_explorer",
get_path = function()
return require("my_explorer").get_file_under_cursor()
end,
})
end,
}
Custom Renderers
Register renderers for new file types:
require("focal").register_renderer({
name = "my-pdf-renderer",
extensions = { "pdf" },
priority = 80,
needs_terminal = true,
is_available = function()
return vim.fn.executable("pdftoppm") == 1
end,
get_geometry = function(path, stat, env)
return { width = env.max_width, height = env.max_height }
end,
render = function(ctx, done)
-- render PDF page 1 as image, display in ctx.buf
done(true, { output = "...", fit = { width = 40, height = 30 } })
end,
clear = function() end,
cleanup = function() end,
})
Commands
| Command | Description |
|---------|-------------|
| :FocalToggle | Toggle previews on/off |
| :FocalEnable | Enable previews |
| :FocalDisable | Disable previews |
| :FocalShow [path] | Preview file under cursor, or a specific file |
| :FocalHide | Dismiss current preview |
| :FocalStatus | Print diagnostic info |
| :checkhealth focal | Full health check |
Troubleshooting
- Previews not showing? Run
:checkhealth focalto verify backends and terminal support. - Previews feel slow? Lower your
updatetime(e.g.,vim.o.updatetime = 300). - Wrong backend? Set
backend = "chafa"orbackend = "image.nvim"explicitly. - Inside tmux? Add
set -g allow-passthrough onto yourtmux.conf.
Contributing
See CONTRIBUTING.md for development setup, testing, and how to add sources/renderers.
License
MIT
Related Skills
node-connect
350.8kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
110.4kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
350.8kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
350.8kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
