doc/images.txt — rendered from the plugin's own vimdoc
*images.txt* Show images inside Neovim via the iTerm2 protocol (OSC 1337) *images.nvim* Author: Stefan Bartl Repo: https://github.com/StefanBartl/images.nvim
CONTENTS
1. Introduction ......................... |images-introduction| 2. Requirements ......................... |images-requirements| 3. Setup ................................ |images-setup| 4. Commands ............................. |images-commands| 5. Keymaps .............................. |images-keymaps| 6. Configuration ........................ |images-configuration| 7. Lua API .............................. |images-api| 8. Integrations ......................... |images-integrations| 9. Troubleshooting ...................... |images-troubleshooting|
1. INTRODUCTION
images.nvim draws images in the terminal without leaving Neovim: hover a markdown link, double-click it, or paste a screenshot from the clipboard straight into the document. Unlike snacks.image and image.nvim, which speak only the Kitty graphics protocol, images.nvim uses the iTerm2 inline image protocol (OSC 1337). On native Windows Neovim in WezTerm, Kitty APC sequences coming from Neovim are never drawn, which makes those plugins unusable there. OSC 1337 works. Images are drawn over the text and cleared on the next cursor movement (this isdisplay.hover_mode = "overlay", the default). True inline rendering inside the text flow requires Unicode placeholders, which only Kitty and Ghostty implement — neither ships for Windows.display.hover_mode = "float"is a middle ground: a small floating window under the cursor instead of the text overlay, for:Image show/hover only — see |images-display-config|. WezTerm decodes PNG/JPEG/GIF/WebP/BMP itself. SVG is the one format it cannot, so it is rasterized to a cached PNG via ImageMagick before drawing. Without it, opening an .svg reports a clear error rather than failing silently. That is one of the places where ImageMagick is a requirement rather than an improvement, alongside |:Image| redact, the file operations (|images-fileops|), the ASCII fallback below, and |:Image| export unless pdfport.nvim is installed — see |images-magick|. When the terminal check fails, |:Image| show/hover draw a colored block-character grid instead of a silently ineffective OSC 1337 sequence — see |images-ascii-config|. Remote images (http://…, https://…) are supported by |:Image| show and by hovering a markdown link, but off by default — see |images-remote-config|.:Image gallery/compare/pickers/zendo not resolve remote images yet.
2. REQUIREMENTS
- Neovim 0.10+ for|vim.base64|- API level 14 for|nvim_ui_send()|- A terminal that implements OSC 1337 (WezTerm, iTerm2, Konsole) - lib.nvim https://github.com/StefanBartl/lib.nvim For:Image pasteadditionally: Windows powershell.exe (ships with the system) macOS pngpaste Linux wl-paste (Wayland) or xclip (X11) *images-magick* ImageMagick (magickon PATH) appears in two roles. As an IMPROVEMENT it adds:Image info's dimensions and:Image compare's relative scaling; its absence degrades the result rather than removing the command. As a REQUIREMENT it has no fallback at all: SVG display the one format WezTerm cannot decode itself; an .svg is rasterized to a cached PNG first the ASCII fallback |images-ascii-config| — reading pixel colours out of a raster file needs a real decoder:Image redactthe boxes are burned in by an image operation |images-fileops|:Image scale/optimise/convertARE image operations, end to end:Image exportunless pdfport.nvim is installed, in which case it routes through that insteadtesseractis required for:Image ocr(|images-ocr|), and poppler'spdftoppm— through pdfport.nvim — for previewing a PDF entry as its first page. All of them are declared indocs/install.json; `:Lib deps show images.nvim` reports which are present. Run:checkhealth imagesto verify all of the above.
3. SETUP
{
"StefanBartl/images.nvim",
dependencies = { "StefanBartl/lib.nvim" },
cmd = { "Image" },
ft = { "markdown", "vimwiki", "norg", "text" },
opts = {},
}
Both triggers, not justcmd: the filetypes are what put the hover keymap and the double-click handler in place in a Markdown buffer no command has been run in yet. Calling |images.setup()| is optional when usingopts.
4. COMMANDS
*:Image* :Image Show the image under the cursor. Accepts a markdown link () or a bare filename. With a range (:'<,'>Image), shows a gallery of the images in that range instead. :Image show [{path}] Show {path}. Without an argument, behaves like the bare:Image. {path} may be an http(s) URL ifdisplay.remote.enabledis true. :Image list Collect every image link in the buffer and offer them in a picker (lib.nvim's UI kit when available, else|vim.ui.select()|). With a single hit, shows it directly. Accepts a range::'<,'>Image listscans only the selection. :Image gallery [{cols}] Show every image of the buffer side by side in a grid. Without {cols} the column count is derived from the number of images, capped at four — beyond that the tiles get too small to read. With a range, only the images in that range. :Image next Jump to the next image link of the buffer and show it. :Image prev Same, backwards. Both wrap around. The first call in a buffer starts from the image nearest the cursor. :Image info [{path}] Format, dimensions and file size. Dimensions require ImageMagick; without it, size and modification time are still shown. :Image pin Keep the current image on screen instead of clearing it on the next cursor movement.:Image clearor a second:Image pinreleases it. :Image paste [env|abs|rel|repos] [{name}] [path={mode}] Save the clipboard image next to the document and insert a markdown link at the cursor. The target directory and the filename followpaste.dirandpaste.name_template— unless a "Resources" or "Ressourcen" folder already exists next to the document, in which case that one is reused instead of creatingpaste.dir(see |images-paste-config|). No image in the clipboard leaves no folder behind either way. With {name}, that filename is used directly (.pngforced, same rules aspaste.ask_filename) instead of asking or falling back topaste.name_template.path={mode}picks how the inserted LINK spells out the image's location (never where the file itself is written):relative(default, unchanged from before this argument existed),absolute(full filesystem path; short:abs),env(rooted at an environment variable --$NVIM_CONFIG_DIR,$REPOS_DIRor yourpaste.env_roots-- else relative),repos(rooted at$REPOS_DIR, falling back torelativeoutside it and erroring if$REPOS_DIRis unset), or anything else, used literally as a custom prefix (path=/static/img). Withoutpath=...,paste.default_path_modedecides — see |images-paste-config|. A bare first word (env,abs,rel,repos) is the same aspath=; any other first word is the file name. Afterwards the cursor sits inside the link's empty alt text in insert mode (paste.link_cursor). :Image screenshot Take a screenshot interactively and insert it like:Image paste, without needing the clipboard as an intermediate step. See |images-screenshot-config|. :Image replace [{path}] Overwrite {path} — or the image under the cursor — with the current clipboard content. The link stays unchanged; useful to update a stale screenshot in-place instead of pasting a new file and link. :Image export [{path}] Export {path} — or the image under the cursor — as a PDF next to the source file (photo.png->photo.pdf), overwriting an existing PDF of the same name without asking. With pdfport.nvim installed it routes through that instead — asynchronous, and lossless viaimg2pdfwhen available. Without it, ImageMagick is required (see |images-magick|). :Image redact [{path}] Open {path} — or the image under the cursor — in a censor mode: a full-screen window (like |:Image| zen) with a selection grid over the image. Enter Visual mode (v/<C-v>), move to the opposite corner of what needs blacking out, <CR> marks it — repeat for more boxes,uremoves the last one,wburns them in (ImageMagick) and writes a new file (photo.png->photo.redacted.png); the source file is never touched.q/<Esc> cancels without writing. Selection happens entirely in terminal cells (no pixel-precise mouse input exists in a terminal), so each box is grown by a configurable safety margin before burning (display.redact.padding_cells, default 1) — over-redacting is the safe failure mode. Requires ImageMagick, with no fallback (see |images-magick|). :Image scale {size} [{path}] *images-fileops* Write a resized copy of {path} — or of the image under the cursor — next to the source (photo.png->photo.scaled.png). {size} is an ImageMagick geometry:50%,800x600,800x,x600,800x600!(force, ignores the aspect ratio) or a bare800(fits inside 800x800). The original is never touched. The geometry is validated beforemagickruns. That is not politeness:magicktreats an unparseable-resizeargument as NO resize and exits 0, so a typo would otherwise produce a.scaled.copy at the original size, looking exactly like success. Noteimages.scale(the module) is something else entirely — display arithmetic that never writes a file. This command maps toimages.convert.resize. :Image optimise [{path}] [--quality={n}] Write a smaller copy of {path} — or of the image under the cursor — next to the source (photo.png->photo.optimised.png). Metadata is stripped (-strip: camera EXIF, colour profiles, and on a screenshot the window title), and PNG additionally gets ImageMagick's highest compression level, which is lossless. JPEG is re-encoded whatever happens —magickcannot strip a JPEG without decoding it. Without {n} ImageMagick carries the source's own quality setting over, which keeps that re-encode as close to a no-op as the format allows;--quality={n}(1-100) buys size with quality. A copy that turns out NOT to be smaller is deleted rather than left next to the original, and the message says so with both sizes. :Image convert {format} [{path}] Write a copy of {path} — or of the image under the cursor — in another format, on the same stem (photo.jpg->photo.png). {format} is <Tab>-completed from the configuredextensions(minussvg, since rasterising into an SVG wrapper is not a conversion) pluspdfport.nvimincluded — one PDF path, not two that could drift. Converting a file to the format it already has is refused: that would be an in-place edit of the source, which none of these operations does. :Image ocr [{path}] [--lang={code}] *images-ocr* Read the text out of {path} — or out of the image under the cursor — withtesseract, and open it in amarkdownscratch split below. The buffer is named after the source image and reused, so a second run on the same screenshot replaces the previous result rather than stacking windows.--lang/-loverridesocr.langfor this call. A split rather than a preview popup on purpose: recognised text is raw material — you correct a misread character, select a paragraph and run:Translate(language.nvim), yank a stack trace, write it out next to a ticket. Requirestesseractplus the language data for whateverocr.langnames; those install separately from the binary, and:checkhealth imageslists what is present. On Windows the UB-Mannheim installer leaves "Add to PATH" unticked, so images.nvim also probesC:/Program Files/Tesseract-OCR/;ocr.binoverrides both. SVG input goes through the cached SVG->PNG conversion first, since tesseract reads raster formats only. :Image orphans Find image files inpaste.dirthat no link in the buffer points to anymore, and offer to delete one. Only considerspaste.dir, not the whole project — the one directory this plugin itself writes into. Deletion always asks for confirmation first. :Image pickers [{scope}] [{dir}] Browse image files under {scope} ("cfile": the current file's directory, "cwd": the working directory, "path": {dir} explicitly) and show one. With snacks.picker installed, previews the highlighted file live; without it, falls back to a plain list with no preview. {scope} defaults to "cwd". With snacks, <Tab> multi-selects — confirming with more than one selected shows them all as a gallery (|images.gallery()|) instead of a single image. :Image compare [{scope}] [{dir}] Same scan as:Image pickers, but picks two images: mark the first (<M-c>, or <CR>), search again, then <CR> shows both side by side.q/<Esc> closes. With ImageMagick, the smaller image (by pixel diagonal) is shown proportionally smaller — centered in its half rather than stretched to fill it — so an icon next to a large photo still looks like an icon. Without ImageMagick both fill their half, same as every other command here. :Image zen [{path}] Show {path} — or the image under the cursor — full screen, in a real editable window rather than a transient preview float. Stays open next to a snacks hover popup, since it is not wired to close on focus loss like one. :Image draw {position} [{path}] Draw {path} — or the image under the cursor — at a named position in the current window: "full" fills it, any of the nine anchors ("top-left", "top", "top-right", "center-left", "center", "center-right", "bottom-left", "bottom", "bottom-right") centers a smaller, scaled box there instead. The reliable, positioned single-shot primitive:Image zen/the hover float/redact/the picker preview all build on internally — see |images.draw()|. :Image calibrate *images-calibrate* Measure how this terminal actually places an image, interactively. Draws a generated test card that exactly fills a framed window;hjkl/arrows nudge it one cell per press,+/-adjustdisplay.cell_aspectin 0.01 steps,rresets, <CR> accepts,qcancels. The result is offered for saving understdpath("data")— per machine, never in the syncedsetup()spec — and is merged on every later start. An explicitsetup()option always outranks a stored measurement, checked independently forterminal_paddingandcell_aspect. A remaining offset smaller than one cell is reported as the protocol limit it is;display.draw_insetcovers it. Run it once per terminal setup, not per project. :Image debug {mode} [{path}] Measure a misplaced draw rather than guessing at it. {mode} is one of: report log the coordinates actually sent, per draw columns tell a constant offset (whichdisplay.terminal_paddingcan absorb) from a scaling one (which it cannot) float check whether a window is where Neovim says it is disarm undoreport's instrumentation ofimages.terminal.drawreportis the one mode that leaves something behind between calls: it wrapsimages.terminal.drawto log every draw, and the wrapper stays in place untildisarmremoves it. :Image check Report whether this terminal can display images, and re-run the detection (useful after settingdisplay.assume_supported). :Image clear Remove displayed images and release a pin. Also closes an open:Image zenwindow. The command name follows thecommandoption. The whole verb accepts a range (range = true): the bare form andgalleryuse it to scope to the given lines,listto filter its picker to them; the other subcommands ignore it.
5. KEYMAPS
Registered buffer-locally for the filetypes inkeymaps.filetypes. <leader>im Show the image under the cursor. <leader>ig Show all images of the buffer side by side. <leader>in Next image. <leader>ip Previous image. <leader>iv Paste the clipboard image and insert the link. <leader>is Take a screenshot and insert the link. <2-LeftMouse> Double-click a markdown link to show the image. A double-click that does not hit an image link falls through to the normal word selection. If the buffer already has a buffer-local<2-LeftMouse>from another plugin (markdown.nvim binds one on the same filetypes), images.nvim leaves it alone rather than overwriting it — such handlers route the image case here anyway, while the reverse is not true. Every entry acceptsfalseto disable that single mapping. *images-keymaps-whichkey* With which-key (https://github.com/folke/which-key.nvim) installed, the longest common prefix of the configured keys (<leader>iby default) is registered as a named group — computed from whateverkeymapsactually resolve to, so a fully remapped set of keys still groups correctly. Skipped when fewer than two keys share a prefix, or when the prefix would itself equal one of the mapped keys (which would show both an action and a group under the same key).
6. CONFIGURATION
require("images").setup({
command = "Image",
extensions = { "png", "jpg", "jpeg", "gif", "webp", "bmp", "svg" },
deps_popup = true,
display = {
max_cols = 60,
max_rows = 25,
cell_aspect = 0,
draw_inset = 1,
terminal_padding = { row = 0, col = 0 },
gallery_gap = 1,
hover_mode = "overlay",
assume_supported = false,
clear_events = {
"CursorMoved", "CursorMovedI", "InsertEnter",
"BufLeave", "WinScrolled",
},
browse_exclude = { ".deps", "node_modules" },
browse_max_entries = 20000,
gopath_fallback = true,
zen = { width = 0.9, height = 0.85 },
remote = {
enabled = false,
timeout_ms = 10000,
max_bytes = 20 * 1024 * 1024,
cache_ttl_s = 24 * 60 * 60,
},
screenshot = {
windows_timeout_ms = 60000,
windows_poll_interval_ms = 600,
},
redact = {
padding_cells = 1,
},
ascii_fallback = {
enabled = true,
},
},
paste = {
dir = "assets",
existing_dir_names = { "Resources", "Ressourcen" },
name_template = "%s-%d.png",
link_template = "",
ask_alt_text = false,
alt_link_template = "",
ask_filename = false,
default_path_mode = "relative", -- or "env"|"absolute"|"repos"|prefix|false
env_roots = {}, -- extra roots for "env": { WIKI_DIR = "E:/wiki" }
link_cursor = { enable = true, startinsert = true, path_cursor = "end" },
},
ocr = {
lang = "eng",
args = {},
bin = nil,
},
pdf = {
enabled = true,
page = 1,
dpi = 120,
},
menu = {
enable = true,
},
integrations = {
ui_menu = true,
},
keymaps = {
show = "<leader>im",
gallery = "<leader>ig",
next = "<leader>in",
prev = "<leader>ip",
paste = "<leader>iv",
screenshot = "<leader>is",
double_click = true,
filetypes = { "markdown", "vimwiki", "norg", "text" },
},
})
*images-display-config*display.max_colsanddisplay.max_rowsare given in terminal cells, not pixels. The terminal scales the image into that box while preserving the aspect ratio, so the pixel size of a cell never has to be known.display.hover_modecontrols how |images.show()|/hover displays a single image — not the gallery, which always uses its own grid layout: "overlay" (default) Draw over the text (the original behavior). Clears on the nextdisplay.clear_eventsevent. "float" A small, unfocused floating window under the cursor instead. Same clear timing and `:Image pinbehavior, same underlyingimages.terminal` draw call — only the container differs. Uses the same window-then-draw-at-its-geometry technique as:Image zen, just cursor-positioned and small instead of centered and large. The window is not focusable and does not steal focus, so it behaves like a glance rather than a view to switch into — for that, use:Image zeninstead. *images-placement-config*display.draw_inset,display.terminal_paddinganddisplay.cell_aspectare one topic. OSC 1337 has no pixel offset — an image is positioned withCSI row;col H, which addresses whole cells only — and neither the cell size nor the terminal's window padding can be read from inside Neovim (TermResponseforwards no plain CSI replies,nvim_list_uis()reports cells). draw_inset Cells of margin kept free all round; default 1. Absorbs the SUB-CELL remainder, which no plugin can correct, by keeping the image centred inside its frame instead of flush against it. 0 draws flush. terminal_padding Whole-cell row/column offset; default {0, 0}. Absorbs a SYSTEMATIC offset. Raisingdraw_insetto paper over one of these wastes space and still looks off. Negative values move the image up/left. cell_aspect Pixel aspect ratio (width/height) of one cell; default 0, meaning the 0.5 assumption fromimages.scale. This is a SHAPE, not an offset — a wrong value shows as a letterbox strip along one edge that no nudging removes.:Image calibratemeasuresterminal_paddingandcell_aspecttogether and stores them per machine; see |images-calibrate|.:Image redactalways draws flush regardless ofdraw_inset, because its cell-to-pixel mapping depends on it. *images-browse-config*display.browse_excludenames directories:Image pickers/:Image compareskip while scanning, in addition to ".git" (always skipped). The scan stops afterdisplay.browse_max_entriesvisited entries (default 20000) as a safety net against an accidental scan of a huge tree — the results found up to that point are still used. *images-gopath-config*display.gopath_fallback(default true) asks gopath.nvim's cursor resolver what path the cursor is on, after Markdown links and <figure> blocks and before Vim's own <cfile>. Only a result gopath confirms exists on disk, with an extension fromextensions, is accepted. A no-op without gopath.nvim installed; false disables it even with it installed. *images-deps-config*deps_popup(default true) controls the one-off "which CLI tools does this plugin want, and why" popup on the firstsetup()after installation, via lib.nvim's deps module.menu.enable(default true) controls whetherimages.integrations.menureturns any right-click entries at all; without nvzone/menu installed it is inert either way.integrations.ui_menu = falsekeeps ui.nvim's right-click menu from composing the fly-out while other hosts still getitems().display.zensizes the:Image zenwindow as a fraction (0-1) of the editor's width/height — that is the MAXIMUM box. The window actually opened is shrunk to the image's own aspect ratio inside it (when ImageMagick can read the image's pixel size; otherwise the fraction alone decides, as before), so the image fills its window instead of leaving empty space on one side. *images-capability-config* Before the first draw, images.nvim checks whether the terminal is one that implements OSC 1337 (WezTerm, iTerm2, Konsole), detected from environment variables. When it is not, a warning is issued **once per session** and the image is drawn anyway. Deliberately not a hard block: the protocol has no capability query, so the detection is a heuristic, and a false negative would break a working setup. Setdisplay.assume_supported = trueto silence the warning on a terminal that works but is not on the list.:Image checkre-runs the detection. *images-ascii-config* When the capability check fails (and only then),:Image show/hover trydisplay.ascii_fallbackbefore falling back to the warning above: each terminal cell becomes a "█" character with its own true-color foreground, sampled from the image via ImageMagick (`magick … -resize COLSxROWS! -alpha off -depth 8 RGB:-`) and drawn with per-cell|nvim_buf_set_extmark()|highlights — the same solid-block technique graphics-protocol-less terminal viewers (chafa, viu) use, not a brightness character ramp. `images.scale. fit_cells()picks the cell box, the same aspect-ratio math:Image redact` uses. Requires ImageMagick — one of the deliberate exceptions to "ImageMagick improves, never enables" (see |images-magick|). Without it, or withdisplay.ascii_fallback.enabled = false, behavior is unchanged: the capability warning fires once per session and a draw is still attempted (the detection is a heuristic, see above). Scope is deliberately narrow, same boundary as remote images below: only:Image show/hover get it, notgallery/compare/pickers/zen. *images-remote-config*display.remote.enabled(default false) allows |:Image| show and hover to download an http(s) image before drawing it. Off by default: opening a document should not silently make a network request just because it contains an image link, the same reasoning email clients apply to "load remote images".display.remote.timeout_msand.max_bytesbound the download; results are cached by URL understdpath("cache")/images.nvim/remotefordisplay.remote.cache_ttl_sseconds (default a day) before a re-fetch is attempted, so a URL whose content changes is not served stale forever. Usescurlif found, elsewget; neither present is reported as an error, not a silent no-op. Only the single-image path resolves remote images —:Image gallery,compare,pickersandzendo not (yet); a remote link inside a buffer is simply not found when those scan for images. *images-screenshot-config*display.screenshot.windows_timeout_ms/.windows_poll_interval_msonly matter on Windows. There is no documented way to have the modern Snipping Tool write directly to a file, so:Image screenshotlaunches it (ms-screenclip:) and polls the clipboard for a new image — different from whatever was there before — everywindows_poll_interval_ms, up towindows_timeout_ms. On macOS and Linux the underlying tool (screencapture -i,grim+slurp, ormaim -s) writes to the target file directly and these two options do not apply.:Image screenshotruns asynchronously end to end — it never blocks Neovim's UI, even while waiting up to a minute for the Windows polling to finish, unlike a plainvim.system(...):wait()would. *images-paste-config*paste.diris relative to the document; an empty string writes the file next to it.paste.name_templatereceives the document stem and a timestamp,paste.link_templatethe path relative to the document. Before usingpaste.dir,:Image paste/screenshotcheck the document's own directory for an existing folder named (case-insensitively) one ofpaste.existing_dir_names(default{ "Resources", "Ressourcen" }) and, if found, use that instead — so a folder you already maintain does not get a second, parallelassetsnext to it. Set it to{}to disable the check and always usepaste.dir. Either way, the target directory is only created (or reused) once the image is actually written — a clipboard without an image, or a cancelled:Image screenshot, leaves no directory behind.paste.ask_alt_text = trueprompts for alt text before inserting the link (via lib.nvim's UI kit when available,|vim.fn.input()|otherwise), usingpaste.alt_link_template(alt text, then path) instead ofpaste.link_templatewhen the answer is non-empty. Cancelling the prompt still inserts the link without alt text — the file is already written by then.paste.ask_filename = trueprompts for a filename before writing the clipboard image, prefilled with whatpaste.name_templatewould produce. Any path component in the answer (directories,..) is dropped — only the name itself is kept — and the extension is always forced to.png, regardless of what was typed, since that is what the clipboard write produces either way. Unlike the alt-text prompt, cancelling here does nothing: nothing has been read from the clipboard yet at this point.:Image paste {name}sanitizes and uses {name} the same way, without prompting — it takes priority overpaste.ask_filename, since a name given on the command line makes the prompt redundant.paste.default_path_mode(default"relative") picks how the inserted link's path is spelled out when:Image pasteis not given apath=...argument — see the four choices under |images-commands| ("relative", "absolute", "repos", or a literal custom prefix). This only ever changes the LINK text, never where the file itself is written (stillpaste.dir/an existing resource folder, per the rules above). Setting it tofalsemeans "ask every time" instead of assuming"relative": aui.kit.selectprompt (falling back tovim.ui.select) offers the four choices, and picking "custom prefix…" opens a second, free-text prompt (ui.kit.input, falling back to|vim.fn.input()|) for the literal prefix.
7. LUA API
images.show({path}) *images.show()*
Show the image at {path}. Relative paths are resolved against
the buffer directory and the working directory. {path} may
be an http(s) URL if display.remote.enabled is true; see
|images-remote-config|.
Returns boolean.
images.hover() *images.hover()*
Show the image under the cursor. Returns boolean, false when
no image link was found.
images.list({first}, {last}) *images.list()*
Pick from the image links of the buffer, optionally limited to
the line range {first}..{last}.
images.gallery({paths}, {columns}) *images.gallery()*
Show {paths} side by side. With {paths} nil, every image of
the current buffer. {columns} nil derives the grid from the
image count. Returns boolean.
images.step({delta}) *images.step()*
Move to the next ({delta} = 1) or previous ({delta} = -1)
image of the buffer and show it. Wraps around.
images.info({path}) *images.info()*
Show metadata for {path}, or for the image under the cursor
when nil.
images.pin({on}) *images.pin()*
Enable/disable automatic clearing. {on} nil toggles.
Returns the new state.
images.recheck() *images.recheck()*
Re-run the terminal capability detection and report the
result. Returns the capability table.
images.paste({name}, {force_ask}, {path_mode}) *images.paste()*
Save the clipboard image and insert the link. With {name},
that filename is used directly instead of asking or falling
back to paste.name_template — see :Image paste {name}.
{path_mode} is "relative"|"absolute"|"repos"|a custom prefix
(see :Image paste's path=... argument); nil follows
paste.default_path_mode, asking interactively when that is
false.
images.scale({spec}, {path}) *images.scale()*
Write a resized copy next to the source. {spec} is a
geometry, see |images-fileops|. Returns true when the
resize was started; the result arrives asynchronously.
Not to be confused with the images.scale *module*, which
is display arithmetic — this maps to
images.convert.resize.
images.optimise({path}, {opts}) *images.optimise()*
Write a smaller copy next to the source. {opts} accepts
quality (1-100) for lossy formats. Returns true when the
optimisation was started. A copy that is not smaller is
deleted and reported as such. See |images-fileops|.
images.convert({format}, {path}) *images.convert()*
Write a copy in another format, on the same stem. Returns
true when the conversion was started. pdf takes the same
route as |images.export()|. See |images-fileops|.
images.ocr({path}, {opts}) *images.ocr()*
Read the text out of {path} — or out of the image under the
cursor — and open it in a scratch split. {opts} accepts
lang and args, both overriding the ocr configuration
for this call. Returns true when OCR was started; the
result arrives asynchronously. See |images-ocr|.
images.screenshot() *images.screenshot()*
Take a screenshot interactively and insert the link, like
|images.paste()| but skipping the clipboard as an
intermediate step. Returns immediately; the result arrives
asynchronously. See |images-screenshot-config|.
images.replace({path}) *images.replace()*
Overwrite {path} — or the image under the cursor when nil —
with the clipboard content. The link is left unchanged.
images.export({path}) *images.export()*
Export {path} — or the image under the cursor when nil — as a
PDF next to the source file. Returns true when the export
was started; the result (success or failure, including a
missing ImageMagick/pdfport.nvim) arrives asynchronously via
notify. false only when no image was found at all.
images.redact({path}) *images.redact()*
Open {path} — or the image under the cursor when nil — in
the censor mode described under |:Image| redact. Returns
true if the window opened, false (with a notification)
if no image was found or the image's pixel dimensions
could not be determined (ImageMagick missing).
images.orphans() *images.orphans()*
Find images in paste.dir without a referencing link and
offer one for deletion (with confirmation).
images.browse({scope}, {arg}) *images.browse()*
Scan for images under {scope} ("cfile"|"cwd"|"path") and show
a picker (snacks.picker if installed, else a plain list).
{arg} is the directory for scope "path".
images.compare({scope}, {arg}) *images.compare()*
Same scan as |images.browse()|, but for picking two images to
view side by side. See :Image compare.
images.zen({path}) *images.zen()*
Show {path} — or the image under the cursor when nil — full
screen in a real editable window. Returns boolean.
images.draw({target}, {position}, {path}, {opts}) *images.draw()*
Draw {path} — or the image under the cursor when nil —
reliably in {target} at {position}. See |:Image| draw for
the position names.
{target}: a window handle, a buffer handle (resolved to a
window currently showing it), or nil for the current window.
{opts}: { scale?, defer?, on_done? } — {scale} (0 <
scale <= 1) overrides the default box size for a non-"full"
position; {defer} = true schedules the draw for the next
event-loop tick, needed when {target} was opened/populated
in the same tick as this call (nvim does not repaint until
control returns to the main loop, so a synchronous draw
right after opening a window gets painted over); {on_done}
(ok, err) always runs exactly once, synchronously unless
{defer} is set. Returns ok — with defer = true, whether
the call was accepted, not whether the draw already
happened; use {on_done} for that result.
images.gallery_range({first}, {last}, {columns}) *images.gallery_range()*
Show the images between lines {first} and {last} side by
side. Thin wrapper around |images.gallery()| that resolves a
range into a path list first.
images.statusline({opts}) *images.statusline()*
A short indicator string for a statusline: empty when
nothing is shown, an icon otherwise, with a suffix appended
while pinned. {opts}: { icon?, pinned_suffix? }.
images.clear() *images.clear()*
Remove a displayed image.
images.setup({opts}) *images.setup()*
Merge {opts} over the defaults and register commands, keymaps
and autocmds.
8. INTEGRATIONS
markdown.nvim Used for link resolution when present, via apcall. Falls back to an internal resolver, so it is never required. Also a consumer of |images.browse()|'sdraw_in_window(): itsmiimage handler and (with snacks.picker):Markdown links showboth use it for their own in-Neovim previews.draw_in_window()itself now just delegates to |images.draw()| — a new consumer should call that directly. snacks.nvim Its picker is used by:Image pickers/:Image comparefor a live image thumbnail per entry, via a custom preview function (not snacks.image's own, which is Kitty-only). Optional — without it, both commands fall back to a plain list. lib.nvim Provides the:Imagecommand grammar and the picker behind:Image list. ui.nvim Providesui.kit.comparebehind:Image compare— a reusable "pick two, view them side by side" component, not images.nvim-specific. pickers.nvim The inverse of:Image pickers: pickers.nvim owns the picker, and images.nvim draws the entries that happen to be images into its preview window. It consumesimages.integrations.picker—available(),is_previewable(),preview(winid, file, opts), plusis_image(),is_pdf(),extensions()andclear().opts.on_readyis when a PDF placeholder has to come down -- the picture covers its own box and no more, andopts.on_doneis already too late to edit the buffer. One-directional: nothing here needs pickers.nvim. Wired on snacks and telescope; fzf-lua previews images through its ownpreviewers.builtin.extensionsinstead. pdfport.nvim Both directions.:Image exportmakes a PDF through itscreate()API;images.pdfreads one back throughrender_page(), so apdftoppm, which is what pdfport shells out to; without either piece a PDF is simply not claimed and the host keeps its own preview.pdf = { enabled = false }says the same on a machine that has both. Pages are cached instdpath("cache")/images.nvim/pdf, keyed by path, mtime, page and dpi. filetree.nvim Can use images.nvim as the backend of its preview feature. open.nvim Can route image targets here.
9. TROUBLESHOOTING
Nothing is drawn
Verify the terminal itself first, outside Neovim, with a tool that
speaks the same protocol: OSC 1337, the iTerm2 inline-image protocol
(not the Kitty graphics protocol). Any POSIX shell will do:
printf '\033]1337;File=inline=1;preserveAspectRatio=1:%s\a' \
"$(base64 < picture.png | tr -d '\n')"
The same in PowerShell (Windows):
$b = [Convert]::ToBase64String(
[IO.File]::ReadAllBytes((Resolve-Path picture.png).Path))
[Console]::Write("$([char]27)]1337;File=inline=1;" +
"preserveAspectRatio=1:$b$([char]7)")
A terminal that ships its own tool works as well:imgcat picture.png(iTerm2) orwezterm imgcat picture.png(WezTerm). If that shows nothing, the terminal does not do OSC 1337 — with ImageMagick installed,:Image show/hover already fall back to a block-character rendering instead (|images-ascii-config|); without it, this plugin cannot help. If OSC 1337 works but Neovim does not, run:checkhealth images.
A colored block grid appears instead of the real image
Expected on a terminal without OSC 1337 — that is the ASCII fallback
(|images-ascii-config|), not a bug. :Image check confirms whether the
terminal was detected as OSC-1337-capable.
The image appears below the statusline
That is what happens without cursor positioning. If you see it, the
positioning sequence did not reach the terminal — check for a multiplexer
(tmux needs set -g allow-passthrough on).
A window opens but stays empty, or shows only its first rows
nvim_ui_send()writes to the terminal at once, while Neovim's own repaint waits for the main loop, so a window opened and drawn into in the same tick gets painted over its own image. Two mechanisms cover that, because one alone is not enough:images.terminal.draw()flushes anything already pending before sending, and every window-opening path (zen, hover float, redact) defers its draw by one tick — a flush before sending cannot cover the repaint that opening the window itself triggers. A caller of |images.draw()| that opens its own window passesopts.defer = truefor the same reason. If an empty window still appears from a built-in path, the terminal is most likely ignoring the sequence entirely; run the terminal check under "Nothing is drawn" above.
The image leaves empty space at one edge of the `:Image zen` window
preserveAspectRatio=1scales the image DOWN to fit the sent cell box, but never grows the box to match — a window wider or taller than the (scaled) image just shows nothing in the remainder.:Image zensizes its window to the image's own aspect ratio for exactly this reason (images.scale.fit_cells()); without ImageMagick installed the pixel size cannot be read and the window falls back to the plaindisplay.zenfraction.
The image does not disappear
:Image clearforces a repaint. If it returns on its own, an event indisplay.clear_eventsis missing for your workflow.
`:Image paste` says there is no image in the clipboard
A copied FILE is not an image in the clipboard — the clipboard must hold
bitmap data, as it does after a screenshot. On Windows the helper runs
powershell.exe -STA; the STA thread is required or the clipboard API
always returns null.
An .svg says "ImageMagick (`magick` not found)"
Install ImageMagick and make sure magick is on PATH — WezTerm cannot
decode SVG itself, so there is no fallback for this one format.
A URL says "remote images are disabled"
Expected: display.remote.enabled defaults to false, on purpose (see
|images-remote-config|). Set it to true to allow the download.
`:Image gallery`/`compare` silently skip a remote link
Expected for now — only |images.show()|/hover resolve remote images. A link with a URL target is treated the same as any other unresolvable link by the scanning commands.
`:Image screenshot` times out on Windows without an obvious reason
There is no documented way to make the modern Snipping Tool write
directly to a file, so this plugin waits for a new image to appear in
the clipboard after launching it — this is the least certain of the
three platforms this plugin supports. If it keeps timing out, `:Image
paste` (manual screenshot, then paste) is the unchanged, proven
fallback. Raise display.screenshot.windows_timeout_ms if you just need
more time to make the selection.
`:Image screenshot` does nothing on Linux
Needsgrim+slurp(Wayland) ormaim(X11).:checkhealth imagesreports which, if either, was found.