Wrap bare Kitty graphics sequences in tmux passthrough

A Kitty graphics command is an APC sequence, which tmux takes as a
request to set the pane title. A bare one therefore never reaches the
terminal and overwrites the title on the way, which is visible on the
border of a floating pane. Programs are expected to wrap it in the tmux
passthrough sequence themselves, but 'kitten icat --clear' does not,
unlike the image data it emits right after.

Sixel and iTerm2 sequences are left alone; tmux renders the former
itself and neither affects the title.

Related: #4870
This commit is contained in:
Junegunn Choi
2026-07-28 10:45:34 +09:00
parent d39595ae66
commit b5f2ba4fd4
2 changed files with 48 additions and 1 deletions
+19 -1
View File
@@ -59,6 +59,8 @@ var passThroughBeginRegex *regexp.Regexp
var passThroughEndTmuxRegex *regexp.Regexp
var ttyin *os.File
var inTmux = len(os.Getenv("TMUX")) > 0
const clearCode string = "\x1b[2J"
// Number of maximum focus events to process synchronously
@@ -4756,6 +4758,22 @@ func findPassThrough(line string) []int {
return []int{loc[0], loc[1] + pos + 2}
}
// tmux takes a bare APC as a request to set the pane title, so a Kitty
// graphics command never reaches the terminal and clobbers the title on the
// way. 'kitten icat --clear' emits one unwrapped. Sixel is left alone.
// https://github.com/junegunn/fzf/issues/4870
func wrapPassThrough(passThrough string, tmux bool) string {
if !tmux || !strings.HasPrefix(passThrough, "\x1b_G") {
return passThrough
}
// Only the sequence is passed through, not the trailing CR
suffix := ""
if strings.HasSuffix(passThrough, "\r") {
passThrough, suffix = passThrough[:len(passThrough)-1], "\r"
}
return "\x1bPtmux;" + strings.ReplaceAll(passThrough, "\x1b", "\x1b\x1b") + "\x1b\\" + suffix
}
func extractPassThroughs(line string) ([]string, string) {
passThroughs := []string{}
transformed := ""
@@ -4996,7 +5014,7 @@ Loop:
} else {
t.pwindow.Move(y, x)
}
t.tui.PassThrough(passThrough)
t.tui.PassThrough(wrapPassThrough(passThrough, inTmux))
if requiredLines > 0 {
if y+requiredLines == height {
+29
View File
@@ -549,6 +549,35 @@ func TestExtractPassthroughs(t *testing.T) {
}
}
func TestWrapPassThrough(t *testing.T) {
for _, passThrough := range []string{
"\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\", // Already wrapped
"\x1bP0;1;0q#0;2;0;0;0#0~~@@vv@@~~@\x1b\\", // Sixel
"\x1b]1337;File=inline=1:AAAA\a", // iTerm2
} {
if got := wrapPassThrough(passThrough, true); got != passThrough {
t.Errorf("should have been left alone: %q -> %q", passThrough, got)
}
}
kitty := "\x1b_Ga=d,d=A\x1b\\"
if got := wrapPassThrough(kitty, false); got != kitty {
t.Errorf("should have been left alone outside of tmux: %q", got)
}
// ESC characters are doubled, and the trailing carriage return is kept
// outside of the wrapper
for _, test := range []struct{ input, want string }{
{kitty, "\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\"},
{kitty + "\r", "\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\\r"},
{"\x1b_Gm=1;\x1bAAA=\x1b\\", "\x1bPtmux;\x1b\x1b_Gm=1;\x1b\x1bAAA=\x1b\x1b\\\x1b\\"},
} {
if got := wrapPassThrough(test.input, true); got != test.want {
t.Errorf("expected %q, got %q", test.want, got)
}
}
}
/* utilities section */
// Item represents one line in fzf UI. Usually it is relative path to files and folders.