SkillAgentSearch skills...

Focal.nvim

Image hover previews for Neo-tree, Oil, Nvim-tree, Snacks, and Mini.files.

Install / Use

/learn @hmdfrds/Focal.nvim

README

focal.nvim

CI License: MIT Neovim

Universal file preview for Neovim. Hover over a file in any explorer, see a preview.

focal.nvim demo

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) | |:-:|:-:| | image.nvim demo | chafa demo |


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 focal and :FocalStatus for 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 updatetime is 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: updatetime controls 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

  1. Previews not showing? Run :checkhealth focal to verify backends and terminal support.
  2. Previews feel slow? Lower your updatetime (e.g., vim.o.updatetime = 300).
  3. Wrong backend? Set backend = "chafa" or backend = "image.nvim" explicitly.
  4. Inside tmux? Add set -g allow-passthrough on to your tmux.conf.

Contributing

See CONTRIBUTING.md for development setup, testing, and how to add sources/renderers.

License

MIT

Related Skills

View on GitHub
GitHub Stars17
CategoryDevelopment
Updated13h ago
Forks1

Languages

Lua

Security Score

95/100

Audited on Apr 7, 2026

No findings