Open-source project
hakonharnes/img-clip.nvim avatar
hakonharnes/img-clip.nvim

img-clip.nvim: paste images into Markdown, LaTeX and Typst from Neovim

Embed images into any markup language, like LaTeX, Markdown or Typst

944 stars42 forksLuaMIT

At a glance

What is it?
img-clip.nvim is a Lua plugin for Neovim that reads an image from the system clipboard or a file, optionally processes it with a shell command, and inserts a filetype-aware snippet with the right path, label and cursor position.
Who is it for?
img-clip.nvim suits anyone who writes technical documents in Neovim and wants an image pasted from a screenshot tool to land as a correct snippet rather than a hand-edited path, and its per-filetype templates plus Lua API make it a good fit for people who already script their editor. It is a poor fit if you are on Linux without xclip or wl-clipboard, on macOS without pngpaste, or if you need image processing that no shell command can express.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 51 days ago.
What is it written in?
Mainly Lua, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem is the gap between a screenshot and the markup around it

Write a paper or a technical note in Neovim and you will hit this loop: take a screenshot, switch to the editor, save the file somewhere sensible, work out the relative path, type the markup by hand, then place the cursor inside the figure environment rather than after it. The screenshot takes two seconds, the markup takes a minute, and the path is wrong often enough to be annoying.

img-clip.nvim collapses that loop. The plugin is described as a way to embed images into any markup language, like LaTeX, Markdown or Typst, and the feature list names the paths an image can arrive by: pasted from the system clipboard, dragged and dropped from a web browser or file explorer, downloaded from the web, or supplied as a file path or URL through the API.

The output is not just a path. The plugin inserts a template, and the templates are per filetype, so the same paste produces `![$CURSOR]($FILE_PATH)` in Markdown and a full figure environment in LaTeX. That is the difference between a path dumper and a document tool.

The intended user is obvious and narrow: a Neovim user who writes documents as text and wants images handled with the same keystroke as everything else. The plugin is written in Lua, uses a Neovim health check, and its configuration is a Lua table, so it fits an editor configuration that is already code rather than a GUI document editor.

Reading the clipboard depends on a platform helper you must install

Neovim cannot read image data from the system clipboard on its own, so img-clip.nvim delegates that to an external tool, and which tool depends on your platform. On Linux the requirement is xclip for x11 or wl-clipboard for wayland. On macOS it is pngpaste. On Windows there is no additional requirement.

That is the plugin's one hard external dependency, and the documentation flags it clearly. After installation you should run `:checkhealth img-clip` to ensure requirements are satisfied. This is the step most people should do first, because a paste that silently does nothing on Linux is almost always a missing xclip or a wayland session without wl-clipboard.

The macOS and Windows story also hints at what the plugin cannot do for you. It does not implement a clipboard format negotiation layer of its own; it asks a helper binary for the bytes. If you are on a Linux desktop running a terminal multiplexer over ssh, the clipboard helper has to run on the machine where you took the screenshot, and the plugin documentation says nothing about remote sessions. Treat the platform list as the actual support matrix rather than assuming universal coverage.

Once the bytes are in hand, the rest of the pipeline is platform independent: file naming, directory placement, processing, templating and insertion all behave the same everywhere.

Templates decide where the cursor lands, and that is the whole trick

The default template is the bare string `$FILE_PATH`, so a configuration that changes nothing still inserts a path. Real use comes from overriding it, and the placeholders are the interesting part.

`$FILE_PATH` is the resolved path to the saved image, which can be relative or absolute depending on `use_absolute_path` and `relative_to_current_file`. `$CURSOR` marks where the cursor should end up after insertion, and `$LABEL` provides a label placeholder used by the LaTeX, Typst and Org templates for a figure identifier. Two options control the mechanics around the cursor: `insert_mode_after_paste` and `insert_template_after_cursor` decide whether the editor is left in insert mode and whether the template is placed after the cursor position or before it.

The LaTeX template shows the full shape of the idea:

lua
      template = [[
\begin{figure}[h]
  \centering
  \includegraphics[width=0.8\textwidth]{$FILE_PATH}
  \caption{$CURSOR}
  \label{fig:$LABEL}
\end{figure}
    ]], ---@type string | fun(context: table): string

A pasted image in a LaTeX file therefore arrives as a complete figure block with the cursor sitting inside the caption, ready for a caption, and a label placeholder waiting for a name. The Typst, Org, reStructuredText, AsciiDoc and HTML templates follow the same convention, each with its own syntax and its own mix of placeholders.

The trade-off is that templates are strings, not a structured model, so a document format that needs a fifth placeholder is not supported without patching the plugin. For the listed languages it is enough.

File, URL or Base64 decides whether the document stays portable

The plugin can embed an image in three ways, and the choice is a real trade-off rather than a preference.

A file reference writes the image to disk under `dir_path` with a generated `file_name` and `extension`, and inserts a path to it. The default `dir_path` is "assets", the default `file_name` pattern is "%Y-%m-%d-%H-%M-%S", and the default extension is "png". This keeps the document small and the repository reviewable, but it means the document is only correct on a machine that has that assets directory.

A Base64 embedding inlines the image data in the document itself, which produces a self-contained file at the cost of size. The option `embed_image_as_base64` is false by default, and `max_base64_size` is 10.

A URL reference embeds a web address. `download_images` is true by default in the base configuration, which means a URL given to the plugin is fetched and stored locally rather than linked, while the Markdown and Vimwiki filetype entries turn it off so those documents keep pointing at the remote image.

Formats are constrained to a list, defaulting to `{ "jpeg", "jpg", "png" }`, with the LaTeX entry adding "pdf".

Per-project, per-directory and per-filetype settings all live in one table

The configuration is one Lua table with a `default` section, a `filetypes` section, and three more: `files`, `dirs` and `custom`, which the project labels as file, directory and custom triggered options. The base section holds the file and directory options, logging, template, prompt, base64, image and drag and drop groups.

Built-in filetype entries cover markdown, vimwiki, html, tex, typst, rst, asciidoc and org. The markdown entry, for instance, sets a different template, turns on `url_encode_path` and turns off `download_images`:

lua
    markdown = {
      url_encode_path = true, ---@type boolean | fun(): boolean
      template = "![$CURSOR]($FILE_PATH)", ---@type string | fun(context: table): string
      download_images = false, ---@type boolean | fun(): boolean
    },

Any option value can be a static value or a function that generates it dynamically, which is the escape hatch for the filetype table. The given example sets the directory to match the name of the currently open file:

lua
dir_path = function()
  return vim.fn.expand("%:t:r")
end,

That pattern extends to the file, directory and custom tables, so the resolution order from most specific to most general is a design decision the user makes by what they fill in. If you rely on several of them at once, check the result with the `ImgClipConfig` command, which prints the current configuration.

process_cmd turns pasting into a pipeline you control

The `process_cmd` option is the most extensible part of the plugin. It takes a shell command that reads image data from standard input and writes processed data to standard output, and it runs before the image is saved or embedded as Base64. The documented example pipes through ImageMagick:

bash
process_cmd = "convert - -quality 85 -" -- compress the image with 85% quality

Because the contract is standard input to standard output, the command does not have to be ImageMagick. Anything that reads bytes on stdin and writes bytes on stdout works, which means the same hook can be used for compression, format conversion, redaction or adding a watermark, and it does not require the plugin to know about any of it.

The default is an empty string, so no processing happens out of the box.

Debugging a misbehaving pipeline is handled by the `ImgClipDebug` command, which prints the debug log including the output of shell commands. The `verbose` option is enabled by default, so this log is populated out of the box. Combined with the `copy_images` option, which defaults to false, and the drag and drop group with `enabled` true and `insert_mode` false, the plugin gives you enough instrumentation to see what it actually did rather than guessing at the failure.

A Lua API instead of a keybinding, and a project that still ships 0.x tags

Everything the commands do is reachable from Lua, with options passed directly so a caller can override configuration for one call:

lua
require("img-clip").paste_image(opts?, input?) -- input is optional and can be a file path or URL

The documented example combines a per-call override with an explicit input path:

lua
require("img-clip").paste_image({ use_absolute_path = false, file_name = "image.png" }, "/path/to/file.png")

This is where the plugin stops being a keybinding and becomes a component. The project ships example integrations for plugins such as telescope.nvim and oil.nvim, and the API is the reason those exist: a file picker can call `paste_image` with the path of the selected entry. The documentation does not describe how those integrations are installed, so treat them as reference implementations to copy rather than plugins to configure.

For maintenance, the picture is a small project with recent work but old tags. The repository is not archived, and the last push was on 2026-09-02, which is well inside the last six months, so the code is moving even though the newest release tag is v0.6.0 from 2025-02-23, preceded by v0.5.0 in 2024-08 and v0.4.1 in 2024-04. That gap between the last push and the last tag means `main` carries work that is not in a tagged release, so pinning to a tag gives you an older plugin than the branch.

The licence is MIT, which is permissive and easy to comply with. The repository root carries a Makefile with lint targets, `luacheck lua/` and `stylua --color always --check lua/`, plus a `stylua.toml` and a `.luacheckrc`, and it has a `tests/` directory driven by PlenaryBustedDirectory through the `test` target.

Editorial conclusion

img-clip.nvim suits anyone who writes technical documents in Neovim and wants an image pasted from a screenshot tool to land as a correct snippet rather than a hand-edited path, and its per-filetype templates plus Lua API make it a good fit for people who already script their editor. It is a poor fit if you are on Linux without xclip or wl-clipboard, on macOS without pngpaste, or if you need image processing that no shell command can express. Before adopting, run :checkhealth img-clip to confirm the clipboard requirement is met on your machine, decide whether images are stored as files or as Base64 in the document, and read the ImgClipDebug output once with a real paste to see what the template expanded to. The last push was on 2026-09-02 and the newest tagged release is v0.6.0 from 2025-02-23.

Frequently asked questions

How do I install img-clip.nvim with lazy.nvim?

Add the plugin spec to your lazy.nvim configuration, set the event and keys as in the README example, and leave the opts table empty to use the default settings. The suggested keymap binds <leader>p to the PasteImage command.

What do I need installed on Linux for img-clip.nvim to paste images?

xclip for an x11 session or wl-clipboard for wayland. On macOS the requirement is pngpaste, and on Windows there is no additional requirement. Run :checkhealth img-clip after installation to confirm.

Can img-clip.nvim insert a LaTeX figure environment?

Yes, the tex filetype configuration includes a template that produces a figure environment with centering, includegraphics, a caption placeholder for the cursor and a label placeholder, and it adds pdf to the accepted formats.

How do I embed an image as Base64 instead of a file?

Set embed_image_as_base64 to true. The plugin also exposes a max_base64_size option, set to 10 by default.

Can I run a command to resize or compress the image on paste?

Yes, use the process_cmd option. The command should read the image data from standard input and write the processed data to standard output, and the documented example uses convert - -quality 85 - for ImageMagick.

What license is img-clip.nvim released under?

The licence field on the repository says MIT, and the repository root contains the LICENSE file along with a Makefile for luacheck and stylua checks.

Official sources

  1. hakonharnes/img-clip.nvim on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/hakonharnes-img-clip-nvim.svg)](https://hysenlabs.com/projects/hakonharnes-img-clip-nvim)

Was this useful?