images.nvim · View & render · vimdoc

:help images

Show images inside Neovim via the iTerm2 protocol (OSC 1337)

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 *images-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-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
is display.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/zen do not resolve remote images yet.

2. REQUIREMENTS *images-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 paste additionally:

    Windows   powershell.exe (ships with the system)
    macOS     pngpaste
    Linux     wl-paste (Wayland) or xclip (X11)

                                                             *images-magick*
ImageMagick (magick on 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 redact         the boxes are burned in by an image operation
    |images-fileops|        :Image scale/optimise/convert ARE image
                            operations, end to end
    :Image export         unless pdfport.nvim is installed, in which case
                            it routes through that instead

tesseract is required for :Image ocr (|images-ocr|), and poppler's
pdftoppm — through pdfport.nvim — for previewing a PDF entry as its first
page. All of them are declared in docs/install.json; `:Lib deps show
images.nvim` reports which are present.

Run :checkhealth images to verify all of the above.

3. SETUP *images-setup*

    {
      "StefanBartl/images.nvim",
      dependencies = { "StefanBartl/lib.nvim" },
      cmd = { "Image" },
      ft = { "markdown", "vimwiki", "norg", "text" },
      opts = {},
    }
Both triggers, not just cmd: 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 using opts.

4. COMMANDS *images-commands*

                                                                      *:Image*
:Image                  Show the image under the cursor. Accepts a markdown
                        link (![alt](path)) 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 if
                        display.remote.enabled is 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 list scans 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 clear or a
                        second :Image pin releases 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 follow paste.dir and
                        paste.name_template — unless a "Resources" or
                        "Ressourcen" folder already exists next to the
                        document, in which case that one is reused instead
                        of creating paste.dir (see |images-paste-config|).
                        No image in the clipboard leaves no folder behind
                        either way. With {name}, that filename is used
                        directly (.png forced, same rules as
                        paste.ask_filename) instead of asking or falling
                        back to paste.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_DIR or your
                        paste.env_roots -- else relative), repos (rooted
                        at $REPOS_DIR, falling back
                        to relative outside it and erroring if $REPOS_DIR
                        is unset), or anything else, used literally as a
                        custom prefix (path=/static/img). Without
                        path=..., paste.default_path_mode decides — see
                        |images-paste-config|. A bare first word
                        (env, abs, rel, repos) is the same as
                        path=; 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 via img2pdf when 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, u removes the last one,
                        w burns 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 bare 800 (fits inside 800x800).
                        The original is never touched.

                        The geometry is validated before magick runs.
                        That is not politeness: magick treats an
                        unparseable -resize argument as NO resize and
                        exits 0, so a typo would otherwise produce a
                        .scaled. copy at the original size, looking
                        exactly like success.

                        Note images.scale (the module) is something else
                        entirely — display arithmetic that never writes a
                        file. This command maps to images.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 — magick
                        cannot 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 configured extensions
                        (minus svg, since rasterising into an SVG wrapper
                        is not a conversion) plus pdf.

                        pdf runs through the same route as |:Image|
                        export, pdfport.nvim included — 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 — with tesseract, and open it in
                        a markdown scratch 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/-l overrides ocr.lang for 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.

                        Requires tesseract plus the language data for
                        whatever ocr.lang names; those install separately
                        from the binary, and :checkhealth images lists
                        what is present. On Windows the UB-Mannheim
                        installer leaves "Add to PATH" unticked, so
                        images.nvim also probes
                        C:/Program Files/Tesseract-OCR/; ocr.bin
                        overrides both. SVG input goes through the cached
                        SVG->PNG conversion first, since tesseract reads
                        raster formats only.

:Image orphans          Find image files in paste.dir that no link in the
                        buffer points to anymore, and offer to delete one.
                        Only considers paste.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, +/- adjust
                        display.cell_aspect in 0.01 steps, r resets, <CR>
                        accepts, q cancels. The result is offered for
                        saving under stdpath("data") — per machine, never
                        in the synced setup() spec — and is merged on every
                        later start. An explicit setup() option always
                        outranks a stored measurement, checked independently
                        for terminal_padding and cell_aspect. A remaining
                        offset smaller than one cell is reported as the
                        protocol limit it is; display.draw_inset covers 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 (which
                                        display.terminal_padding can
                                        absorb) from a scaling one (which it
                                        cannot)
                            float       check whether a window is where
                                        Neovim says it is
                            disarm      undo report's instrumentation of
                                        images.terminal.draw

                        report is the one mode that leaves something
                        behind between calls: it wraps
                        images.terminal.draw to log every draw, and the
                        wrapper stays in place until disarm removes it.

:Image check            Report whether this terminal can display images, and
                        re-run the detection (useful after setting
                        display.assume_supported).

:Image clear            Remove displayed images and release a pin. Also
                        closes an open :Image zen window.

The command name follows the command option. The whole verb accepts a
range (range = true): the bare form and gallery use it to scope to the
given lines, list to filter its picker to them; the other subcommands
ignore it.

5. KEYMAPS *images-keymaps*

Registered buffer-locally for the filetypes in keymaps.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 accepts false to 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>i by default) is
registered as a named group — computed from whatever keymaps actually
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 *images-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 = "![](%s)",
        ask_alt_text = false,
        alt_link_template = "![%s](%s)",
        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_cols and display.max_rows are 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_mode controls 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 next display.clear_events event.
    "float"                A small, unfocused floating window under the
                           cursor instead. Same clear timing and `:Image
                           pin behavior, same underlying images.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 zen instead.

                                                    *images-placement-config*
display.draw_inset, display.terminal_padding and display.cell_aspect
are one topic. OSC 1337 has no pixel offset — an image is positioned with
CSI 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
(TermResponse forwards 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. Raising draw_inset to
                        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 from
                        images.scale. This is a SHAPE, not an offset — a
                        wrong value shows as a letterbox strip along one edge
                        that no nudging removes.

:Image calibrate measures terminal_padding and cell_aspect together and
stores them per machine; see |images-calibrate|. :Image redact always draws
flush regardless of draw_inset, because its cell-to-pixel mapping depends
on it.

                                                       *images-browse-config*
display.browse_exclude names directories :Image pickers/:Image compare
skip while scanning, in addition to ".git" (always skipped). The scan stops
after display.browse_max_entries visited 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 from extensions, 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 first setup() after installation, via
lib.nvim's deps module. menu.enable (default true) controls whether
images.integrations.menu returns any right-click entries at all; without
nvzone/menu installed it is inert either way. integrations.ui_menu = false
keeps ui.nvim's right-click menu from composing the fly-out while other hosts
still get items().

display.zen sizes the :Image zen window 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.
Set display.assume_supported = true to silence the warning on a terminal
that works but is not on the list. :Image check re-runs the detection.

                                                        *images-ascii-config*
When the capability check fails (and only then), :Image show/hover try
display.ascii_fallback before 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 with
display.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, not gallery/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_ms and .max_bytes bound the
download; results are cached by URL under
stdpath("cache")/images.nvim/remote for display.remote.cache_ttl_s
seconds (default a day) before a re-fetch is attempted, so a URL whose
content changes is not served stale forever. Uses curl if found, else
wget; neither present is reported as an error, not a silent no-op.

Only the single-image path resolves remote images — :Image gallery,
compare, pickers and zen do 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_ms only
matter on Windows. There is no documented way to have the modern Snipping
Tool write directly to a file, so :Image screenshot launches it
(ms-screenclip:) and polls the clipboard for a new image — different
from whatever was there before — every windows_poll_interval_ms, up to
windows_timeout_ms. On macOS and Linux the underlying tool
(screencapture -i, grim+slurp, or maim -s) writes to the target file
directly and these two options do not apply.

:Image screenshot runs 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 plain vim.system(...):wait() would.

                                                         *images-paste-config*
paste.dir is relative to the document; an empty string writes the file next
to it. paste.name_template receives the document stem and a timestamp,
paste.link_template the path relative to the document.

Before using paste.dir, :Image paste/screenshot check the document's
own directory for an existing folder named (case-insensitively) one of
paste.existing_dir_names (default { "Resources", "Ressourcen" }) and, if
found, use that instead — so a folder you already maintain does not get a
second, parallel assets next to it. Set it to {} to disable the check and
always use paste.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 = true prompts for alt text before inserting the link
(via lib.nvim's UI kit when available, |vim.fn.input()| otherwise), using
paste.alt_link_template (alt text, then path) instead of paste.link_template
when 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 = true prompts for a filename before writing the
clipboard image, prefilled with what paste.name_template would 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 over paste.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 paste is not given a path=...
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 (still paste.dir/an
existing resource folder, per the rules above). Setting it to false means
"ask every time" instead of assuming "relative": a ui.kit.select prompt
(falling back to vim.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-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 *images-integrations*

markdown.nvim   Used for link resolution when present, via a pcall. Falls
                back to an internal resolver, so it is never required. Also a
                consumer of |images.browse()|'s draw_in_window(): its mi
                image handler and (with snacks.picker) :Markdown links show
                both 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 compare for 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 :Image command grammar and the picker behind
                :Image list.
ui.nvim         Provides ui.kit.compare behind :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 consumes
                images.integrations.picker — available(),
                is_previewable(), preview(winid, file, opts), plus
                is_image(), is_pdf(), extensions() and clear().
                opts.on_ready is when a PDF placeholder has to come down --
                the picture covers its own box and no more, and opts.on_done
                is 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 own
                previewers.builtin.extensions instead.

pdfport.nvim    Both directions. :Image export makes a PDF through its
                create() API; images.pdf reads one back through
                render_page(), so a .pdf entry in a host's picker previews
                as its first page instead of as bytes. The page half also
                needs poppler's pdftoppm, 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 in
                stdpath("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 *images-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) or wezterm 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 passes opts.defer = true for
    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=1 scales 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 zen sizes
    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 plain
    display.zen fraction.

The image does not disappear

    :Image clear forces a repaint. If it returns on its own, an event in
    display.clear_events is 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

    Needs grim+slurp (Wayland) or maim (X11). :checkhealth images
    reports which, if either, was found.