Experimental Sixel support (#2544)

pull/3495/head
Junegunn Choi 8 months ago
parent a33749eb71
commit b1a0ab8086
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -1,6 +1,26 @@
CHANGELOG CHANGELOG
========= =========
0.43.1
------
- (Experimental) Added support for Sixel graphics in the preview window
```sh
# 1. $FZF_PREVIEW_WIDTH and $FZF_PREVIEW_HEIGHT will be set to the pixel width
# and height of the preview window
# 2. Special preview window flag 'clear' is added to always completely
# erase the preview window. This is similar to https://github.com/vifm/vifm/issues/588.
fzf --preview='
if file --mime-type {} | grep -qvF image/; then
bat --color=always {}
elif [[ -n $FZF_PREVIEW_WIDTH ]]; then
convert {} -resize ${FZF_PREVIEW_WIDTH}x${FZF_PREVIEW_HEIGHT} sixel:-
else
echo "Cannot display image data (unsupported platform)"
fi
' --preview-window clear
```
- Bug fixes
0.43.0 0.43.0
------ ------
- (Experimental) Added support for Kitty image protocol in the preview window - (Experimental) Added support for Kitty image protocol in the preview window

@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Oct 2023" "fzf 0.43.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "Oct 2023" "fzf 0.43.1" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@ -603,6 +603,24 @@ e.g.
bat --color=always {} bat --color=always {}
fi fi
'\fR '\fR
fzf also has experimental support for Sixel graphics.
e.g.
\fB# 1. $FZF_PREVIEW_WIDTH and $FZF_PREVIEW_HEIGHT will be set to
# the pixel width and height of the preview window
# 2. Special preview window flag 'clear' is needed to always completely
# erase the preview window
fzf --preview='
if file --mime-type {} | grep -qvF image/; then
bat --color=always {}
elif [[ -n $FZF_PREVIEW_WIDTH ]]; then
convert {} -resize ${FZF_PREVIEW_WIDTH}x${FZF_PREVIEW_HEIGHT} sixel:-
else
echo "Cannot display image data (unsupported platform)"
fi
' --preview-window clear\fR
.RE .RE
.TP .TP

@ -219,6 +219,7 @@ type previewOpts struct {
scroll string scroll string
hidden bool hidden bool
wrap bool wrap bool
clear bool
cycle bool cycle bool
follow bool follow bool
border tui.BorderShape border tui.BorderShape
@ -340,7 +341,7 @@ type Options struct {
} }
func defaultPreviewOpts(command string) previewOpts { func defaultPreviewOpts(command string) previewOpts {
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil} return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
} }
func defaultOptions() *Options { func defaultOptions() *Options {
@ -1454,6 +1455,10 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
opts.wrap = true opts.wrap = true
case "nowrap": case "nowrap":
opts.wrap = false opts.wrap = false
case "clear":
opts.clear = true
case "noclear":
opts.clear = false
case "cycle": case "cycle":
opts.cycle = true opts.cycle = true
case "nocycle": case "nocycle":
@ -1788,7 +1793,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
parsePreviewWindow(&opts.Preview, parsePreviewWindow(&opts.Preview,
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")) nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,clear][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
case "--height": case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]")) opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]"))
case "--min-height": case "--min-height":

@ -65,7 +65,8 @@ func init() {
// Parts of the preview output that should be passed through to the terminal // Parts of the preview output that should be passed through to the terminal
// * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it // * https://github.com/tmux/tmux/wiki/FAQ#what-is-the-passthrough-escape-sequence-and-how-do-i-use-it
// * https://sw.kovidgoyal.net/kitty/graphics-protocol // * https://sw.kovidgoyal.net/kitty/graphics-protocol
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b_G.*?\x1b\\`) // * https://en.wikipedia.org/wiki/Sixel
passThroughRegex = regexp.MustCompile(`\x1bPtmux;\x1b\x1b.*?[^\x1b]\x1b\\|\x1b(_G|P[0-9;]*q).*?\x1b\\`)
} }
type jumpMode int type jumpMode int
@ -1929,11 +1930,15 @@ func (t *Terminal) renderPreviewSpinner() {
} }
func (t *Terminal) renderPreviewArea(unchanged bool) { func (t *Terminal) renderPreviewArea(unchanged bool) {
if unchanged { if t.previewOpts.clear {
t.pwindow.Erase()
} else if unchanged {
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
} else { } else {
t.previewed.filled = false t.previewed.filled = false
t.pwindow.Erase() // We don't erase the window here to avoid flickering during scroll
t.pwindow.DrawBorder()
t.pwindow.Move(0, 0)
} }
height := t.pwindow.Height() height := t.pwindow.Height()
@ -1946,11 +1951,15 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
body = t.previewer.lines[headerLines:] body = t.previewer.lines[headerLines:]
// Always redraw header // Always redraw header
t.renderPreviewText(height, header, 0, false) t.renderPreviewText(height, header, 0, false)
t.pwindow.MoveAndClear(t.pwindow.Y(), 0) if t.previewOpts.clear {
t.pwindow.Move(t.pwindow.Y(), 0)
} else {
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
}
} }
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged) t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
if !unchanged { if !unchanged && !t.previewOpts.clear {
t.pwindow.FinishFill() t.pwindow.FinishFill()
} }
@ -1994,6 +2003,10 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
for _, passThrough := range passThroughs { for _, passThrough := range passThroughs {
t.tui.PassThrough(passThrough) t.tui.PassThrough(passThrough)
} }
if len(passThroughs) > 0 && len(line) == 0 {
continue
}
var fillRet tui.FillReturn var fillRet tui.FillReturn
prefixWidth := 0 prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
@ -2686,6 +2699,11 @@ func (t *Terminal) Loop() {
env = append(env, "FZF_PREVIEW_"+lines) env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, columns) env = append(env, columns)
env = append(env, "FZF_PREVIEW_"+columns) env = append(env, "FZF_PREVIEW_"+columns)
size, err := t.tui.Size()
if err == nil {
env = append(env, fmt.Sprintf("FZF_PREVIEW_WIDTH=%d", pwindow.Width()*size.Width/size.Columns))
env = append(env, fmt.Sprintf("FZF_PREVIEW_HEIGHT=%d", height*size.Height/size.Lines))
}
} }
cmd.Env = env cmd.Env = env

@ -38,6 +38,9 @@ func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false } func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) Refresh() {} func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) Close() {}
func (r *FullscreenRenderer) Size() (termSize, error) {
return termSize{}, nil
}
func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 } func (r *FullscreenRenderer) MaxX() int { return 0 }

@ -32,7 +32,7 @@ var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R") var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) { func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString(str) r.queued.WriteString("\x1b7" + str + "\x1b8")
r.flush() r.flush()
} }
@ -756,6 +756,10 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
return w return w
} }
func (w *LightWindow) DrawBorder() {
w.drawBorder(false)
}
func (w *LightWindow) DrawHBorder() { func (w *LightWindow) DrawHBorder() {
w.drawBorder(true) w.drawBorder(true)
} }
@ -1095,7 +1099,8 @@ func (w *LightWindow) FinishFill() {
} }
func (w *LightWindow) Erase() { func (w *LightWindow) Erase() {
w.drawBorder(false) w.DrawBorder()
// We don't erase the window here to avoid flickering during scroll w.Move(0, 0)
w.FinishFill()
w.Move(0, 0) w.Move(0, 0)
} }

@ -8,6 +8,7 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"syscall" "syscall"
"unsafe"
"github.com/junegunn/fzf/src/util" "github.com/junegunn/fzf/src/util"
"golang.org/x/term" "golang.org/x/term"
@ -108,3 +109,19 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
} }
return int(b[0]), true return int(b[0]), true
} }
type window struct {
lines uint16
columns uint16
width uint16
height uint16
}
func (r *LightRenderer) Size() (termSize, error) {
w := new(window)
_, _, err := syscall.Syscall(syscall.SYS_IOCTL, r.ttyin.Fd(), syscall.TIOCGWINSZ, uintptr(unsafe.Pointer(w)))
if err != 0 {
return termSize{}, err
}
return termSize{int(w.lines), int(w.columns), int(w.width), int(w.height)}, nil
}

@ -203,6 +203,11 @@ func (r *FullscreenRenderer) Refresh() {
// noop // noop
} }
func (r *FullscreenRenderer) Size() (termSize, error) {
cols, lines := _screen.Size()
return termSize{lines, cols, 0, 0}, error("Not implemented")
}
func (r *FullscreenRenderer) GetChar() Event { func (r *FullscreenRenderer) GetChar() Event {
ev := _screen.PollEvent() ev := _screen.PollEvent()
switch ev := ev.(type) { switch ev := ev.(type) {
@ -541,6 +546,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
} }
func (w *TcellWindow) Erase() { func (w *TcellWindow) Erase() {
w.drawBorder(false)
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ') fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
} }
@ -692,6 +698,10 @@ func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
return w.fillString(str, NewColorPair(fg, bg, a)) return w.fillString(str, NewColorPair(fg, bg, a))
} }
func (w *TcellWindow) DrawBorder() {
w.drawBorder(false)
}
func (w *TcellWindow) DrawHBorder() { func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true) w.drawBorder(true)
} }

@ -473,6 +473,13 @@ func MakeTransparentBorder() BorderStyle {
bottomRight: ' '} bottomRight: ' '}
} }
type termSize struct {
Lines int
Columns int
Width int
Height int
}
type Renderer interface { type Renderer interface {
Init() Init()
Resize(maxHeightFunc func(int) int) Resize(maxHeightFunc func(int) int)
@ -490,6 +497,8 @@ type Renderer interface {
MaxX() int MaxX() int
MaxY() int MaxY() int
Size() (termSize, error)
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
} }
@ -499,6 +508,7 @@ type Window interface {
Width() int Width() int
Height() int Height() int
DrawBorder()
DrawHBorder() DrawHBorder()
Refresh() Refresh()
FinishFill() FinishFill()

Loading…
Cancel
Save