summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md26
-rwxr-xr-xbin/fzf-preview.sh30
-rw-r--r--man/man1/fzf.130
-rw-r--r--src/options.go9
-rw-r--r--src/terminal.go136
-rw-r--r--src/tui/dummy.go4
-rw-r--r--src/tui/light.go4
-rw-r--r--src/tui/light_unix.go6
-rw-r--r--src/tui/tcell.go5
-rw-r--r--src/tui/tui.go12
10 files changed, 169 insertions, 93 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8fa95437..706fda9a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,27 +3,21 @@ 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
- ```
+- (Experimental) Sixel image support in preview window (not available on Windows)
+ - `$FZF_PREVIEW_PIXEL_WIDTH` and `$FZF_PREVIEW_PIXEL_HEIGHT` are set to
+ the pixel width and height of the preview window
+ - [bin/fzf-preview.sh](bin/fzf-preview.sh) is added to demonstrate how to
+ display an image using Kitty image protocol or Sixel. You can use it
+ like so:
+ ```sh
+ fzf --preview='fzf-preview.sh {}'
+ ```
- Bug fixes
0.43.0
------
- (Experimental) Added support for Kitty image protocol in the preview window
+ (not available on Windows)
```sh
fzf --preview='
if file --mime-type {} | grep -qF image/; then
diff --git a/bin/fzf-preview.sh b/bin/fzf-preview.sh
new file mode 100755
index 00000000..82c0b25b
--- /dev/null
+++ b/bin/fzf-preview.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+#
+# The purpose of this script is to demonstrate how to preview a file or an
+# image in the preview window of fzf.
+
+file=$1
+type=$(file --mime-type "$file")
+
+if [[ ! $type =~ image/ ]]; then
+ # Sometimes bat is installed as batcat.
+ if command -v batcat > /dev/null; then
+ batname="batcat"
+ elif command -v bat > /dev/null; then
+ batname="bat"
+ else
+ cat "$1"
+ exit
+ fi
+
+ ${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
+elif [[ $KITTY_WINDOW_ID ]]; then
+ # 'memory' is the fastest option but if you want the image to be scrollable,
+ # you have to use 'stream'
+ kitty icat --clear --transfer-mode=memory --stdin=no --place="${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0" "$file" | sed \$d
+ echo -en "\e[m"
+elif [[ -n $FZF_PREVIEW_PIXEL_WIDTH ]]; then
+ convert "$file" -resize "${FZF_PREVIEW_PIXEL_WIDTH}x${FZF_PREVIEW_PIXEL_HEIGHT}>" -dither FloydSteinberg sixel:-
+else
+ file "$file"
+fi
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 735fa7f6..8666ec02 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -592,34 +592,12 @@ e.g.
sleep 0.01
done'\fR
-Since 0.43.0, fzf has experimental support for Kitty graphics protocol,
-so if you use Kitty, you can make fzf display an image in the preview window.
+fzf has experimental support for Kitty graphics protocol and Sixel graphics.
+The following example uses https://github.com/junegunn/fzf/blob/master/bin/fzf-preview.sh
+script to render an image using either of the protocols inside the preview window.
e.g.
- \fBfzf --preview='
- if file --mime-type {} | grep -qF "image/"; then
- kitty icat --clear --transfer-mode=memory --stdin=no --place=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}@0x0 {} | sed \\$d
- else
- bat --color=always {}
- fi
- '\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
+ \fBfzf --preview='fzf-preview.sh {}'
.RE
diff --git a/src/options.go b/src/options.go
index 83258820..b4f74e95 100644
--- a/src/options.go
+++ b/src/options.go
@@ -219,7 +219,6 @@ type previewOpts struct {
scroll string
hidden bool
wrap bool
- clear bool
cycle bool
follow bool
border tui.BorderShape
@@ -341,7 +340,7 @@ type Options struct {
}
func defaultPreviewOpts(command string) previewOpts {
- return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
+ return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
}
func defaultOptions() *Options {
@@ -1455,10 +1454,6 @@ func parsePreviewWindowImpl(opts *previewOpts, input string, exit func(string))
opts.wrap = true
case "nowrap":
opts.wrap = false
- case "clear":
- opts.clear = true
- case "noclear":
- opts.clear = false
case "cycle":
opts.cycle = true
case "nocycle":
@@ -1793,7 +1788,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Preview.command = ""
case "--preview-window":
parsePreviewWindow(&opts.Preview,
- 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]"))
+ nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
case "--height":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]"))
case "--min-height":
diff --git a/src/terminal.go b/src/terminal.go
index ab385e2d..504f4fd1 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -121,10 +121,12 @@ type previewer struct {
}
type previewed struct {
- version int64
- numLines int
- offset int
- filled bool
+ version int64
+ numLines int
+ offset int
+ filled bool
+ wipe bool
+ wireframe bool
}
type eachLine struct {
@@ -278,6 +280,7 @@ type Terminal struct {
theme *tui.ColorTheme
tui tui.Renderer
executing *util.AtomicBool
+ termSize tui.TermSize
}
type selectedItem struct {
@@ -308,6 +311,7 @@ const (
reqRefresh
reqReinit
reqFullRedraw
+ reqResize
reqRedrawBorderLabel
reqRedrawPreviewLabel
reqClose
@@ -447,7 +451,7 @@ type searchRequest struct {
type previewRequest struct {
template string
- pwindow tui.Window
+ pwindowSize tui.TermSize
scrollOffset int
list []*Item
}
@@ -687,7 +691,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
initialPreviewOpts: opts.Preview,
previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, false, true, disabledState, "", []bool{}},
- previewed: previewed{0, 0, 0, false},
+ previewed: previewed{0, 0, 0, false, false, false},
previewBox: previewBox,
eventBox: eventBox,
mutex: sync.Mutex{},
@@ -1930,7 +1934,7 @@ func (t *Terminal) renderPreviewSpinner() {
}
func (t *Terminal) renderPreviewArea(unchanged bool) {
- if t.previewOpts.clear {
+ if t.previewed.wipe && t.previewed.version != t.previewer.version {
t.pwindow.Erase()
} else if unchanged {
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
@@ -1951,15 +1955,11 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
body = t.previewer.lines[headerLines:]
// Always redraw header
t.renderPreviewText(height, header, 0, false)
- if t.previewOpts.clear {
- t.pwindow.Move(t.pwindow.Y(), 0)
- } else {
- t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
- }
+ t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
}
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
- if !unchanged && !t.previewOpts.clear {
+ if !unchanged {
t.pwindow.FinishFill()
}
@@ -1972,10 +1972,29 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
t.renderPreviewScrollbar(headerLines, barLength, barStart)
}
+func (t *Terminal) makeImageBorder(width int, top bool) string {
+ tl := "┌"
+ tr := "┐"
+ v := "╎"
+ h := "╌"
+ if !t.unicode {
+ tl = "+"
+ tr = "+"
+ h = "-"
+ v = "|"
+ }
+ repeat := util.Max(0, width-2)
+ if top {
+ return tl + strings.Repeat(h, repeat) + tr
+ }
+ return v + strings.Repeat(" ", repeat) + v
+}
+
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
maxWidth := t.pwindow.Width()
var ansi *ansiState
spinnerRedraw := t.pwindow.Y() == 0
+Loop:
for _, line := range lines {
var lbg tui.Color = -1
if ansi != nil {
@@ -1993,16 +2012,59 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
t.previewer.scrollable = true
break
} else if lineNo >= 0 {
+ x := t.pwindow.X()
+ y := t.pwindow.Y()
if spinnerRedraw && lineNo > 0 {
spinnerRedraw = false
- y := t.pwindow.Y()
- x := t.pwindow.X()
t.renderPreviewSpinner()
t.pwindow.Move(y, x)
}
for _, passThrough := range passThroughs {
+ // Handling Sixel output
+ requiredLines := 0
+ if strings.HasPrefix(passThrough, "\x1bP") {
+ t.previewed.wipe = true
+ if t.termSize.PxHeight > 0 {
+ rows := util.Max(0, strings.Count(passThrough, "-")-1)
+ requiredLines = int(math.Ceil(float64(rows*6*t.termSize.Lines) / float64(t.termSize.PxHeight)))
+ }
+ }
+
+ // Overflow
+ if requiredLines > 0 && y+requiredLines > height {
+ top := true
+ for ; y < height; y++ {
+ t.pwindow.MoveAndClear(y, 0)
+ t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, t.makeImageBorder(maxWidth, top))
+ top = false
+ }
+ t.previewed.wireframe = true
+ t.previewed.filled = true
+ t.previewer.scrollable = true
+ continue
+ }
+
+ if t.previewed.wireframe {
+ t.previewed.wireframe = false
+ for i := y + 1; i < height; i++ {
+ t.pwindow.MoveAndClear(i, 0)
+ }
+ }
+ t.pwindow.MoveAndClear(y, x)
t.tui.PassThrough(passThrough)
+
+ if requiredLines > 0 {
+ if y+requiredLines == height {
+ t.pwindow.Move(y+requiredLines, 0)
+ t.previewed.filled = true
+ t.previewer.scrollable = true
+ break Loop
+ } else {
+ t.pwindow.MoveAndClear(y+requiredLines, 0)
+ }
+ }
}
+
if len(passThroughs) > 0 && len(line) == 0 {
continue
}
@@ -2100,6 +2162,7 @@ func (t *Terminal) printPreview() {
t.previewed.numLines = numLines
t.previewed.version = t.previewer.version
t.previewed.offset = t.previewer.offset
+ t.previewed.wipe = false
}
func (t *Terminal) printPreviewDelayed() {
@@ -2580,6 +2643,19 @@ func (t *Terminal) cancelPreview() {
t.killPreview(exitCancel)
}
+func (t *Terminal) pwindowSize() tui.TermSize {
+ if t.pwindow == nil {
+ return tui.TermSize{}
+ }
+ size := tui.TermSize{Lines: t.pwindow.Height(), Columns: t.pwindow.Width()}
+
+ if t.termSize.PxWidth > 0 {
+ size.PxWidth = size.Columns * t.termSize.PxWidth / t.termSize.Columns
+ size.PxHeight = size.Lines * t.termSize.PxHeight / t.termSize.Lines
+ }
+ return size
+}
+
// Loop is called to start Terminal I/O
func (t *Terminal) Loop() {
// prof := profile.Start(profile.ProfilePath("/tmp/"))
@@ -2631,12 +2707,13 @@ func (t *Terminal) Loop() {
go func() {
for {
<-resizeChan
- t.reqBox.Set(reqFullRedraw, nil)
+ t.reqBox.Set(reqResize, nil)
}
}()
t.mutex.Lock()
t.initFunc()
+ t.termSize = t.tui.Size()
t.resizeWindows(false)
t.printPrompt()
t.printInfo()
@@ -2669,7 +2746,7 @@ func (t *Terminal) Loop() {
for {
var items []*Item
var commandTemplate string
- var pwindow tui.Window
+ var pwindowSize tui.TermSize
initialOffset := 0
t.previewBox.Wait(func(events *util.Events) {
for req, value := range *events {
@@ -2679,7 +2756,7 @@ func (t *Terminal) Loop() {
commandTemplate = request.template
initialOffset = request.scrollOffset
items = request.list
- pwindow = request.pwindow
+ pwindowSize = request.pwindowSize
}
}
events.Clear()
@@ -2691,18 +2768,16 @@ func (t *Terminal) Loop() {
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true)
env := t.environ()
- if pwindow != nil {
- height := pwindow.Height()
- lines := fmt.Sprintf("LINES=%d", height)
- columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
+ if pwindowSize.Lines > 0 {
+ lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
+ columns := fmt.Sprintf("COLUMNS=%d", pwindowSize.Columns)
env = append(env, lines)
env = append(env, "FZF_PREVIEW_"+lines)
env = append(env, 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))
+ if pwindowSize.PxWidth > 0 {
+ env = append(env, fmt.Sprintf("FZF_PREVIEW_PIXEL_WIDTH=%d", pwindowSize.PxWidth))
+ env = append(env, fmt.Sprintf("FZF_PREVIEW_PIXEL_HEIGHT=%d", pwindowSize.PxHeight))
}
}
cmd.Env = env
@@ -2831,7 +2906,7 @@ func (t *Terminal) Loop() {
if len(command) > 0 && t.canPreview() {
_, list := t.buildPlusList(command, false)
t.cancelPreview()
- t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindow, t.evaluateScrollOffset(), list})
+ t.previewBox.Set(reqPreviewEnqueue, previewRequest{command, t.pwindowSize(), t.evaluateScrollOffset(), list})
}
}
@@ -2899,7 +2974,10 @@ func (t *Terminal) Loop() {
case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw()
- case reqFullRedraw:
+ case reqResize, reqFullRedraw:
+ if req == reqResize {
+ t.termSize = t.tui.Size()
+ }
wasHidden := t.pwindow == nil
t.redraw()
if wasHidden && t.hasPreviewWindow() {
@@ -3116,7 +3194,7 @@ func (t *Terminal) Loop() {
if valid {
t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue,
- previewRequest{t.previewOpts.command, t.pwindow, t.evaluateScrollOffset(), list})
+ previewRequest{t.previewOpts.command, t.pwindowSize(), t.evaluateScrollOffset(), list})
}
} else {
// Discard the preview content so that it won't accidentally appear
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index d893a747..13c2aeec 100644
--- a/src/tui/dummy.go
+++ b/src/tui/dummy.go
@@ -38,9 +38,7 @@ func (r *FullscreenRenderer) Clear() {}
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
func (r *FullscreenRenderer) Refresh() {}
func (r *FullscreenRenderer) Close() {}
-func (r *FullscreenRenderer) Size() (termSize, error) {
- return termSize{}, nil
-}
+func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 }
diff --git a/src/tui/light.go b/src/tui/light.go
index 6b7eaaf4..e5080d1d 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -1092,7 +1092,9 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
}
func (w *LightWindow) FinishFill() {
- w.MoveAndClear(w.posy, w.posx)
+ if w.posy < w.height {
+ w.MoveAndClear(w.posy, w.posx)
+ }
for y := w.posy + 1; y < w.height; y++ {
w.MoveAndClear(y, 0)
}
diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go
index 4ca847b4..46188869 100644
--- a/src/tui/light_unix.go
+++ b/src/tui/light_unix.go
@@ -110,10 +110,10 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
return int(b[0]), true
}
-func (r *LightRenderer) Size() (termSize, error) {
+func (r *LightRenderer) Size() TermSize {
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
if err != nil {
- return termSize{}, err
+ return TermSize{}
}
- return termSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}, nil
+ return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
}
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 54feaf16..cd723e32 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -203,9 +203,10 @@ func (r *FullscreenRenderer) Refresh() {
// noop
}
-func (r *FullscreenRenderer) Size() (termSize, error) {
+// TODO: Pixel width and height not implemented
+func (r *FullscreenRenderer) Size() TermSize {
cols, lines := _screen.Size()
- return termSize{lines, cols, 0, 0}, error("Not implemented")
+ return TermSize{lines, cols, 0, 0}
}
func (r *FullscreenRenderer) GetChar() Event {
diff --git a/src/tui/tui.go b/src/tui/tui.go
index 69ae8a1a..2ebb5e72 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -473,11 +473,11 @@ func MakeTransparentBorder() BorderStyle {
bottomRight: ' '}
}
-type termSize struct {
- Lines int
- Columns int
- Width int
- Height int
+type TermSize struct {
+ Lines int
+ Columns int
+ PxWidth int
+ PxHeight int
}
type Renderer interface {
@@ -497,7 +497,7 @@ type Renderer interface {
MaxX() int
MaxY() int
- Size() (termSize, error)
+ Size() TermSize
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
}