mirror of
https://github.com/junegunn/fzf
synced 2026-08-07 19:31:41 +00:00
Break preview lines on IND to support chafa in preview window on tmux
chafa ends each row of a Kitty Unicode placeholder image with CUB + IND instead of a newline, so that an image drawn at a column offset survives ONLCR, which would rewrite the newline as CR NL and pull the cursor back to column 0. Both sequences were dropped, so the image arrived as a single line and was re-wrapped, shifting every row after the first by the width of the wrap sign. Without wrapping it was truncated to its first row. The column is tracked across the breaks and restored with padding, so an image indented by the preview command keeps its indent. Fix #4885
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.74.3
|
||||
------
|
||||
- Fixed an image from a preview command being torn apart when its rows are separated by IND instead of newlines, as `chafa` does under tmux (#4885)
|
||||
|
||||
0.74.2
|
||||
------
|
||||
- Performance optimizations for short queries
|
||||
|
||||
+49
-1
@@ -58,6 +58,7 @@ var offsetTrimCharsRegex *regexp.Regexp
|
||||
var passThroughBeginRegex *regexp.Regexp
|
||||
var passThroughEndTmuxRegex *regexp.Regexp
|
||||
var sixelBeginRegex *regexp.Regexp
|
||||
var cursorBackRegex *regexp.Regexp
|
||||
var ttyin *os.File
|
||||
|
||||
var inTmux = len(os.Getenv("TMUX")) > 0
|
||||
@@ -96,6 +97,9 @@ func init() {
|
||||
passThroughBeginRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b|\x1b(_G|P[0-9;]*q)|\x1b]1337;`)
|
||||
passThroughEndTmuxRegex = regexp.MustCompile(`[^\x1b]\x1b\\`)
|
||||
sixelBeginRegex = regexp.MustCompile(`^\x1bP[0-9;]*q`)
|
||||
|
||||
// CUB right before an IND, used to return to the column a row started on
|
||||
cursorBackRegex = regexp.MustCompile(`\x1b\[([0-9]*)D$`)
|
||||
}
|
||||
|
||||
type jumpMode int
|
||||
@@ -2276,6 +2280,14 @@ func (t *Terminal) displayWidthWithPrefix(str string, prefixWidth int) int {
|
||||
return width
|
||||
}
|
||||
|
||||
// displayWidthWithoutEscapes is displayWidthWithPrefix for a string that may
|
||||
// still carry pass-throughs and ANSI codes, neither of which take any column.
|
||||
func (t *Terminal) displayWidthWithoutEscapes(str string, prefixWidth int) int {
|
||||
_, text := extractPassThroughs(str)
|
||||
stripped, _, _ := extractColor(text, nil, nil)
|
||||
return t.displayWidthWithPrefix(stripped, prefixWidth)
|
||||
}
|
||||
|
||||
const (
|
||||
minWidth = 4
|
||||
minHeight = 3
|
||||
@@ -4849,6 +4861,38 @@ func extractPassThroughs(line string) ([]string, string) {
|
||||
return passThroughs, transformed
|
||||
}
|
||||
|
||||
// splitOnIND breaks a preview line on IND (ESC D), which moves the cursor
|
||||
// down one line, keeping the column. A program drawing at a column offset ends
|
||||
// its rows with IND instead of a newline, because ONLCR would rewrite a newline
|
||||
// as CR NL and snap the cursor to column 0. chafa does this for Kitty Unicode
|
||||
// placeholders, and without the break the whole image collapses into a single
|
||||
// line.
|
||||
//
|
||||
// The column is tracked and re-created with padding so that an indented image
|
||||
// keeps its indent, and a CUB right before the IND is subtracted, which is how
|
||||
// chafa returns to the column its rows start on.
|
||||
func (t *Terminal) splitOnIND(line string) []string {
|
||||
chunks := strings.Split(line, "\x1bD")
|
||||
if len(chunks) == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := make([]string, 0, len(chunks))
|
||||
col := 0
|
||||
for _, chunk := range chunks[:len(chunks)-1] {
|
||||
lines = append(lines, strings.Repeat(" ", col)+chunk+"\n")
|
||||
col += t.displayWidthWithoutEscapes(chunk, col)
|
||||
if match := cursorBackRegex.FindStringSubmatch(chunk); match != nil {
|
||||
back := 1
|
||||
if len(match[1]) > 0 {
|
||||
back, _ = strconv.Atoi(match[1])
|
||||
}
|
||||
col = max(0, col-back)
|
||||
}
|
||||
}
|
||||
return append(lines, strings.Repeat(" ", col)+chunks[len(chunks)-1])
|
||||
}
|
||||
|
||||
// followOffset computes the correct content-line offset for follow mode,
|
||||
// accounting for line wrapping in the preview window.
|
||||
func (t *Terminal) followOffset() int {
|
||||
@@ -6421,7 +6465,11 @@ func (t *Terminal) Loop() error {
|
||||
version--
|
||||
offset = 0
|
||||
}
|
||||
lines = append(lines, line)
|
||||
if split := t.splitOnIND(line); split != nil {
|
||||
lines = append(lines, split...)
|
||||
} else {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
|
||||
|
||||
@@ -851,3 +851,73 @@ func TestWordWrapAnsiLine(t *testing.T) {
|
||||
t.Errorf("Tab wrap: %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitOnIND(t *testing.T) {
|
||||
term := &Terminal{tabstop: 8}
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
line string
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
// Nothing to do, so the caller keeps the line as it read it
|
||||
name: "no IND",
|
||||
line: "foo\n",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
// chafa: CUB returns to the column the row started on
|
||||
name: "rows at column 0",
|
||||
line: "AAA\x1b[3D\x1bDBBB\x1b[3D\x1bDCCC\n",
|
||||
want: []string{"AAA\x1b[3D\n", "BBB\x1b[3D\n", "CCC\n"},
|
||||
},
|
||||
{
|
||||
// 'printf " "; chafa ...'
|
||||
name: "indented rows",
|
||||
line: " AAA\x1b[3D\x1bDBBB\x1b[3D\x1bDCCC\n",
|
||||
want: []string{" AAA\x1b[3D\n", " BBB\x1b[3D\n", " CCC\n"},
|
||||
},
|
||||
{
|
||||
name: "tab indent expands to the tab stop",
|
||||
line: "\tAAA\x1b[3D\x1bDBBB\x1b[3D\x1bDCCC\n",
|
||||
want: []string{"\tAAA\x1b[3D\n", " BBB\x1b[3D\n", " CCC\n"},
|
||||
},
|
||||
{
|
||||
// IND on its own keeps the column
|
||||
name: "bare IND",
|
||||
line: "AB\x1bDCD\n",
|
||||
want: []string{"AB\n", " CD\n"},
|
||||
},
|
||||
{
|
||||
name: "CUB without a parameter moves back one",
|
||||
line: "AB\x1b[D\x1bDCD\n",
|
||||
want: []string{"AB\x1b[D\n", " CD\n"},
|
||||
},
|
||||
{
|
||||
name: "CUB past the left edge is clamped",
|
||||
line: "AB\x1b[9D\x1bDCD\n",
|
||||
want: []string{"AB\x1b[9D\n", "CD\n"},
|
||||
},
|
||||
{
|
||||
name: "SGR codes take no column",
|
||||
line: "\x1b[31mAB\x1b[m\x1b[2D\x1bDCD\n",
|
||||
want: []string{"\x1b[31mAB\x1b[m\x1b[2D\n", "CD\n"},
|
||||
},
|
||||
{
|
||||
name: "pass-throughs take no column",
|
||||
line: "\x1b_Ga=T,c=2,r=1\x1b\\AB\x1b[2D\x1bDCD\n",
|
||||
want: []string{"\x1b_Ga=T,c=2,r=1\x1b\\AB\x1b[2D\n", "CD\n"},
|
||||
},
|
||||
} {
|
||||
got := term.splitOnIND(tc.line)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Errorf("%s: got %q, want %q", tc.name, got, tc.want)
|
||||
continue
|
||||
}
|
||||
for idx, line := range got {
|
||||
if line != tc.want[idx] {
|
||||
t.Errorf("%s: line %d: got %q, want %q", tc.name, idx, line, tc.want[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user