Skip to content

fix(kitty): translate paths for a Windows-side terminal under WSL - #377

Open
szsolt wants to merge 1 commit into
3rd:masterfrom
szsolt:wsl-transmit-file-path
Open

fix(kitty): translate paths for a Windows-side terminal under WSL#377
szsolt wants to merge 1 commit into
3rd:masterfrom
szsolt:wsl-transmit-file-path

Conversation

@szsolt

@szsolt szsolt commented Jul 31, 2026

Copy link
Copy Markdown

Problem

When Neovim runs inside WSL but the terminal emulator is a Windows program β€” in practice
WezTerm, the one Windows terminal with a working kitty-graphics implementation (Windows
Terminal has not implemented it; kitty
has no Windows build) β€” images either render as the wrong image or don't appear at all.

The kitty backend transmits with transmit_medium = file (t=f), which hands the terminal
a path and lets it open the file itself. That assumes both processes see the same
filesystem. Here they don't: Neovim sends /tmp/foo.png, and the Windows-side terminal
resolves it against the Windows filesystem, where it doesn't exist.

The transmit therefore does nothing. Because the failure is silent, the placement that
follows draws whatever still occupies that image id β€” which is why the symptom is usually a
stale or unrelated image rather than an obvious blank.

Not affected: a Linux-native terminal under WSLg, where both sides share the filesystem and
t=f already works β€” see the gating note below, since that case must be excluded rather
than merely left alone. Also unaffected is t=d, which reads the file on the Neovim side.

Evidence

Bare protocol probe outside Neovim, same image, same terminal (WezTerm on Windows,
Ubuntu-24.04 under WSL2), varying only how the data is referenced:

transmit result
t=f with /tmp/probe.png nothing rendered
t=f with \\wsl.localhost\Ubuntu-24.04\tmp\probe.png renders correctly
t=d with inlined bytes renders correctly

So the path is the variable, not the payload or the placement.

Change

Translate the path with wslpath -w immediately before it's handed to the terminal, gated
on the terminal actually being Windows-side:

local is_WSL = (vim.env.WSL_DISTRO_NAME ~= nil) or (vim.env.WSL_INTEROP ~= nil)
local is_linux_side_terminal = (vim.env.WEZTERM_PANE ~= nil)
  or (vim.env.WEZTERM_EXECUTABLE ~= nil)
  or (vim.env.KITTY_WINDOW_ID ~= nil)
  or (vim.env.KITTY_INSTALLATION_DIR ~= nil)
  or (vim.env.GHOSTTY_RESOURCES_DIR ~= nil)
local needs_windows_path = is_WSL and not is_linux_side_terminal

Gating on WSL alone would be wrong, not just imprecise: a Linux process cannot open a
\\wsl.localhost\... path, so a Linux-native terminal under WSLg would go from working to
broken. Verified locally β€” cat "$(wslpath -w /tmp/f)" fails, and the path has to be
converted back with wslpath -u to be readable.

The discriminator is that a terminal running Linux-side sets its own variables in our
environment, while a Windows-side one has no way to. On this machine TERM_PROGRAM=WezTerm
is set but WEZTERM_PANE is empty, which is exactly the Windows-side signature; a
Linux-native WezTerm always sets WEZTERM_PANE.

Results are cached per path, since wslpath is a subprocess and the same file is
retransmitted across renders.

No effect outside that combination, or if wslpath isn't available β€” all return the path
unchanged.

Reproducing

Worth knowing before testing: init.lua skips write_graphics entirely on a transmit-cache
hit, so an image already resident in the terminal keeps rendering correctly even with this
fix disabled. Use a fresh terminal window, or delete stored images (a=d,d=A) first,
otherwise the bug looks absent.

Notes

The translation sits in write_graphics, which is the single point where a path reaches
the terminal (init.lua:63 is the only call passing one; the other calls send control-only
payloads). Remote images are covered without extra code, as they're downloaded to
vim.fn.tempname() first.

One limitation worth naming: the env-var check infers where the terminal runs rather than
observing it, since the process on the other end of the pty isn't introspectable from here.
It's correct for the terminals listed, but a Linux-side terminal that sets no identifying
variable would be misclassified as Windows-side and get a UNC path it can't open. Falling
back to t=d on a failed transmit would be the robust fix, but the protocol gives no
success signal to react to without a round-trip. Happy to add other terminals' variables if
you know of ones worth including.

@szsolt
szsolt force-pushed the wsl-transmit-file-path branch from b03d937 to 21f5ee8 Compare July 31, 2026 05:37
@szsolt szsolt changed the title fix(kitty): translate WSL paths for transmit_medium=file fix(kitty): translate paths for a Windows-side terminal under WSL Jul 31, 2026
When Neovim runs inside WSL but the terminal emulator is a Windows program --
in practice WezTerm, the one Windows terminal implementing this protocol --
transmit_medium=file hands it a path the two processes don't agree on: Neovim
sends /tmp/foo.png and the terminal resolves it against the Windows filesystem,
where it doesn't exist. The transmit silently does nothing, and the placement
that follows draws whatever still occupies that image id -- so the symptom is
usually a stale or unrelated image, not a blank.

Verified with a bare protocol probe: t=f with /tmp/x.png renders nothing, while
the same command with \\wsl.localhost\<distro>\tmp\x.png renders correctly, as
does t=d with inlined bytes. So the path is the variable, not the payload.

Translate via wslpath -w, cached per path since the same file is retransmitted
across renders.

Gate on the terminal actually being Windows-side rather than on WSL alone: a
Linux-native terminal under WSLg shares our filesystem and cannot open a UNC
path, so translating there would break a working setup. Linux-side terminals set
their own env vars in our process (WEZTERM_PANE, KITTY_WINDOW_ID, ...) and a
Windows-side one cannot, so their absence is the signal. Verified both ways:
WezTerm on Windows gets the UNC path, kitty under WSLg keeps /tmp/... and both
render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@szsolt
szsolt force-pushed the wsl-transmit-file-path branch from 21f5ee8 to 239f62d Compare July 31, 2026 05:44
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.

1 participant