mirror of
https://github.com/junegunn/fzf.git
synced 2026-08-01 05:30:30 +08:00
Do not split preview header through an image
--preview-window ~N draws the header in its own pass, resuming the body in the middle of an image: an image arrives as one line but takes up many rows. ~N is typically global and meant for text, while the same preview command also renders images.
This commit is contained in:
+60
-6
@@ -57,6 +57,7 @@ var offsetComponentRegex *regexp.Regexp
|
||||
var offsetTrimCharsRegex *regexp.Regexp
|
||||
var passThroughBeginRegex *regexp.Regexp
|
||||
var passThroughEndTmuxRegex *regexp.Regexp
|
||||
var sixelBeginRegex *regexp.Regexp
|
||||
var ttyin *os.File
|
||||
|
||||
var inTmux = len(os.Getenv("TMUX")) > 0
|
||||
@@ -94,6 +95,7 @@ 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`)
|
||||
}
|
||||
|
||||
type jumpMode int
|
||||
@@ -4677,15 +4679,23 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
height := t.pwindow.Height()
|
||||
body := t.previewer.lines
|
||||
headerLines := t.activePreviewOpts.headerLines
|
||||
lineNo := -t.previewer.offset + headerLines
|
||||
// Scrollbar is sized from the body alone, split off or not
|
||||
scrollLines := len(body)
|
||||
// Do not enable preview header lines if it's value is too large
|
||||
if headerLines > 0 && headerLines < min(len(body), height) {
|
||||
scrollLines -= headerLines
|
||||
header := t.previewer.lines[0:headerLines]
|
||||
body = t.previewer.lines[headerLines:]
|
||||
// Always redraw header
|
||||
t.renderPreviewText(height, header, 0, false)
|
||||
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
|
||||
// A separate header pass would resume the body inside an image, which
|
||||
// takes up more rows than the line it arrives on
|
||||
if !containsImage(header) {
|
||||
// Always redraw header
|
||||
t.renderPreviewText(height, header, 0, false)
|
||||
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
|
||||
body = t.previewer.lines[headerLines:]
|
||||
}
|
||||
}
|
||||
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
|
||||
t.renderPreviewText(height, body, lineNo, unchanged)
|
||||
|
||||
if !unchanged {
|
||||
t.pwindow.FinishFill()
|
||||
@@ -4696,7 +4706,7 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
}
|
||||
|
||||
effectiveHeight := height - headerLines
|
||||
barLength, barStart := getScrollbar(1, len(body), effectiveHeight, min(len(body)-effectiveHeight, t.previewer.offset-headerLines))
|
||||
barLength, barStart := getScrollbar(1, scrollLines, effectiveHeight, min(scrollLines-effectiveHeight, t.previewer.offset-headerLines))
|
||||
t.renderPreviewScrollbar(headerLines, barLength, barStart)
|
||||
}
|
||||
|
||||
@@ -4776,6 +4786,50 @@ func wrapPassThrough(passThrough string, tmux bool) string {
|
||||
return "\x1bPtmux;" + strings.ReplaceAll(passThrough, "\x1b", "\x1b\x1b") + "\x1b\\" + suffix
|
||||
}
|
||||
|
||||
// Whether the sequence draws an image. Kitty commands that only transmit or
|
||||
// delete do not
|
||||
func isImagePassThrough(passThrough string) bool {
|
||||
// Unwrap the tmux passthrough sequence, in which every ESC is doubled
|
||||
if after, ok := strings.CutPrefix(passThrough, "\x1bPtmux;"); ok {
|
||||
passThrough = strings.ReplaceAll(after, "\x1b\x1b", "\x1b")
|
||||
}
|
||||
if after, ok := strings.CutPrefix(passThrough, "\x1b_G"); ok {
|
||||
// Control data ends at the payload delimiter or at the terminator
|
||||
keys := after
|
||||
if index := strings.IndexAny(keys, ";\x1b"); index >= 0 {
|
||||
keys = keys[:index]
|
||||
}
|
||||
for _, key := range strings.Split(keys, ",") {
|
||||
// Transmit and display, or put an image already transmitted
|
||||
if key == "a=T" || key == "a=p" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
if after, ok := strings.CutPrefix(passThrough, "\x1b]1337;"); ok {
|
||||
return strings.HasPrefix(after, "File=") || strings.HasPrefix(after, "MultipartFile=")
|
||||
}
|
||||
return sixelBeginRegex.MatchString(passThrough)
|
||||
}
|
||||
|
||||
// Whether any line carries an image
|
||||
func containsImage(lines []string) bool {
|
||||
for _, line := range lines {
|
||||
for {
|
||||
loc := findPassThrough(line)
|
||||
if loc == nil {
|
||||
break
|
||||
}
|
||||
if isImagePassThrough(line[loc[0]:loc[1]]) {
|
||||
return true
|
||||
}
|
||||
line = line[loc[1]:]
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func extractPassThroughs(line string) ([]string, string) {
|
||||
passThroughs := []string{}
|
||||
transformed := ""
|
||||
|
||||
@@ -578,6 +578,60 @@ func TestWrapPassThrough(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsImagePassThrough(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
given string
|
||||
image bool
|
||||
}{
|
||||
// Kitty
|
||||
{"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\", true},
|
||||
{"\x1b_Ga=p,i=1\x1b\\", true},
|
||||
// The action key is not always first, and a put carries no payload
|
||||
{"\x1b_Gi=1,a=p\x1b\\", true},
|
||||
{"\x1b_Ga=p\x1b\\", true},
|
||||
{"\x1b_Ga=T,f=100\x1b\\\r", true},
|
||||
{"\x1b_Gi=1,a=d\x1b\\", false},
|
||||
{"\x1b_Ga=d,d=A\x1b\\", false}, // 'kitten icat --clear'
|
||||
{"\x1b_Ga=q,i=1\x1b\\", false}, // query
|
||||
{"\x1b_Gi=1,f=100\x1b\\", false}, // transmit only, the default action
|
||||
{"\x1b_Gm=1;AAAA\x1b\\", false}, // continuation chunk
|
||||
{"\x1b_Ga=f,i=1;AAAA\x1b\\", false}, // animation frame
|
||||
{"\x1b_Ga=T,U=1,f=32;AAAA\x1b\\", true}, // unicode placeholder
|
||||
// Wrapped in the tmux passthrough sequence
|
||||
{"\x1bPtmux;\x1b\x1b_Ga=T,f=100\x1b\x1b\\\x1b\\", true},
|
||||
{"\x1bPtmux;\x1b\x1b_Ga=d,d=A\x1b\x1b\\\x1b\\", false},
|
||||
{"\x1bPtmux;\x1b\x1b_Gi=1,a=p\x1b\x1b\\\x1b\\", true},
|
||||
// iTerm2
|
||||
{"\x1b]1337;File=inline=1:AAAA\a", true},
|
||||
{"\x1b]1337;MultipartFile=inline=1\a", true},
|
||||
{"\x1b]1337;SetUserVar=foo=YmFy\a", false},
|
||||
{"\x1b]1337;CurrentDir=/tmp\a", false},
|
||||
// Sixel
|
||||
{"\x1bP0;1;0q#0;2;0;0;0#0~~@@vv@@~~@\x1b\\", true},
|
||||
{"\x1bPq#0~~\x1b\\", true},
|
||||
{"\x1bPtmux;\x1b\x1bP0;1;0q#0~~\x1b\x1b\\\x1b\\", true},
|
||||
{"", false},
|
||||
} {
|
||||
if actual := isImagePassThrough(tc.given); actual != tc.image {
|
||||
t.Errorf("expected %v for %q, got %v", tc.image, tc.given, actual)
|
||||
}
|
||||
}
|
||||
|
||||
if containsImage([]string{"foo", "bar"}) {
|
||||
t.Error("plain text carries no image")
|
||||
}
|
||||
if !containsImage([]string{"foo", "bar\x1b_Ga=T,f=100\x1b\\baz"}) {
|
||||
t.Error("failed to find an image in a line")
|
||||
}
|
||||
if containsImage([]string{"foo\x1b_Ga=d,d=A\x1b\\"}) {
|
||||
t.Error("a delete command is not an image")
|
||||
}
|
||||
// The image is not the first passthrough on the line
|
||||
if !containsImage([]string{"\x1b_Ga=d,d=A\x1b\\text\x1b_Ga=T,f=100;AAAA\x1b\\"}) {
|
||||
t.Error("failed to look past an earlier passthrough")
|
||||
}
|
||||
}
|
||||
|
||||
/* utilities section */
|
||||
|
||||
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||
|
||||
Reference in New Issue
Block a user