Skip to content

fix(image): support Kitty graphics tmux - #77

Open
fl4vis wants to merge 2 commits into
delphinus:mainfrom
fl4vis:fix/tmux-graphics
Open

fl4vis wants to merge 2 commits into
delphinus:mainfrom
fl4vis:fix/tmux-graphics

Conversation

@fl4vis

@fl4vis fl4vis commented Oct 11, 2026

Copy link
Copy Markdown

What was happening

Kitty graphics did not render inside tmux, even with allow-passthrough enabled.

The plugin sends Kitty graphics commands through vim.api.nvim_ui_send(). This works when Neovim runs directly inside Kitty, but tmux sits between Neovim and the terminal and does not transparently forward Kitty graphics escape sequences.

The plugin correctly detected Kitty support (supports_kitty() returned true), and kitten icat successfully displayed images inside the same tmux session. The missing piece was tmux passthrough framing.

What fixed it

Wrap Kitty graphics commands in tmux DCS passthrough sequences, doubling embedded ESC bytes as required by the protocol.

if vim.env.TMUX and data:find("\x1b_G", 1, true) then
  data = "\x1bPtmux;" .. data:gsub("\x1b", "\x1b\x1b") .. "\x1b\\"
end

The wrapper is applied only when both conditions are met:

  • Neovim is running inside tmux (TMUX is set).
  • The output contains a Kitty graphics command (ESC_G).

Outside tmux, the original output is preserved. Non-graphics writes and the existing image rendering logic remain unchanged.

Verification

  • Mermaid diagrams, images and video render inside tmux with allow-passthrough on.
  • Direct Kitty rendering continues to work.
  • Image placement code works.

Before

Screenshot_11-oct_00-06-32_kitty

After

Screenshot_11-oct_00-07-40_kitty

Known limitation

Images can remain visible after switching tmux windows because Kitty manages graphics placements independently of tmux's screen contents.

tmux must have passthrough enabled:

set -g allow-passthrough on

Kitty graphics do not render inside tmux even with passthrough
enabled. The graphics sequences were sent directly to the terminal,
but tmux intercepted them instead of forwarding them to Kitty.

When running inside tmux, graphics commands are now wrapped in
tmux DCS passthrough sequences, with embedded ESC bytes doubled.

This allows Kitty to receive and render images without changing the
existing rendering logic.
@delphinus

Copy link
Copy Markdown
Owner

Thank you for this, and for the clear write-up with before and after screenshots. tmux users only get alt text today, so I would like to fix this.

I do not want to merge it in this form, though, for these reasons. I do not use tmux myself, so these come from reading the code rather than from running it. Please correct me if I have misread something.

  • Position in split panes. Image placements are written in one batch (display_utils.lua), and that batch contains the cursor save, the cursor move (CSI row;col H), the placement and the cursor restore. With this change the whole batch is passed through, so the cursor move also reaches Kitty directly. The row and column, though, are computed from Neovim's grid, which is the pane's coordinate space. They match the terminal only when the pane sits at the top-left of the window. In a split pane, or with the status bar at the top, I would expect the image to land in the wrong place and cover other panes. Your screenshots show a single pane, which is the case where the two agree.
  • Images left behind. As you noted, placements stay when switching tmux windows. Switching panes or zooming would likely leave them too.
  • Detection. This works when KITTY_WINDOW_ID is inherited by the pane. Without it, supports_kitty() returns false and nothing changes, which is the case in Local PNG images are missing in Kitty/tmux when KITTY_WINDOW_ID is absent #61.

Today a tmux user sees alt text, which is limited but correct. With this change, depending on the layout, some users would instead see images over the wrong part of the screen that stay after they switch away, so I think it would be a step back for them.

There is a plan for this in #61. denny0223 found the same thing there: detection and DCS passthrough alone put the image at the top-left, and placement needs Unicode placeholders. The scope we agreed on is the native backend (no Snacks), no change outside tmux, Neovim 0.12, and a small interface so that vim.ui.img can replace it later (#6). Two PRs are fine: detection and passthrough first, then placeholder placement.

denny0223 has also offered to work on this, but there is no need to coordinate in advance. If you would like to rework this PR into the first step, please go ahead: passthrough plus detection of a Kitty client attached to tmux, with a test that checks the wrapped output rather than unwrapping it. Whichever PR along these lines comes first, I will mention the other person on #61 so that nobody duplicates the work. I would keep the first step unreleased until placement lands, so that nobody gets the misplaced images in between.

Separately, #78 proposes routing every image transmission through one helper so that images can be sent as data over SSH. If that lands first, passthrough fits there naturally. tmux limits how much one passthrough sequence may carry, so each chunk would need to be wrapped on its own. I will leave this PR open while that is sorted out.

Wrap each Kitty graphics command in tmux DCS passthrough instead of
wrapping an entire output batch. Cursor movement remains handled by
tmux, preserving pane-relative placement coordinates.

Detect an xterm-kitty client attached through tmux when
KITTY_WINDOW_ID is unavailable.
@fl4vis

fl4vis commented Oct 13, 2026

Copy link
Copy Markdown
Author

I reworked this into the requested detection-and-transport first step.

  • Kitty graphics commands are now individually wrapped in tmux DCS passthrough; ordinary cursor save, movement, and restore sequences stay outside the wrapper.
  • Each complete Kitty command in a batch gets its own wrapper, so this also composes with future chunked image transmission.
  • When KITTY_WINDOW_ID is absent, native image support now asks tmux for the terminal client attached to the current pane and enables Kitty support only for xterm-kitty.
  • Added tests that inspect raw wrapped output rather than unwrapping it,
    plus Local PNG images are missing in Kitty/tmux when KITTY_WINDOW_ID is absent #61-style detection tests for absent KITTY_WINDOW_ID and a non-Kitty tmux client.

I have intentionally left Unicode-placeholder placement and lifecycle handling out of this change. This should remain unreleased until that follow-up lands.

Screenshot evidence of the Kitty suuport:

Screenshot_13-oct_01-47-05_kitty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants