summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-10-23 01:01:47 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-10-23 01:05:30 +0900
commitb1a0ab8086f061640948299b9ed90a6b0c61c143 (patch)
tree2940e66c6686c9486faf5fa8907b69d32e325b9a
parenta33749eb717aeda07173051435cce605060c81ee (diff)
Experimental Sixel support (#2544)
-rw-r--r--CHANGELOG.md20
-rw-r--r--man/man1/fzf.120
-rw-r--r--src/options.go9
-rw-r--r--src/terminal.go28
-rw-r--r--src/tui/dummy.go3
-rw-r--r--src/tui/light.go11
-rw-r--r--src/tui/light_unix.go17
-rw-r--r--src/tui/tcell.go10
-rw-r--r--src/tui/tui.go10
9 files changed, 117 insertions, 11 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3cdd6c82..8fa95437 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,26 @@
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
------
- (Experimental) Added support for Kitty image protocol in the preview window
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 27d687b8..735fa7f6 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -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
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
fzf - a command-line fuzzy finder
@@ -603,6 +603,24 @@ e.g.
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
+
.RE
.TP
diff --git a/src/options.go b/src/options.go
index b4f74e95..83258820 100644
--- a/src/options.go
+++ b/src/options.go
@@ -219,6 +219,7 @@ type previewOpts struct {
scroll string
hidden bool
wrap bool
+ clear bool
cycle bool
follow bool
border tui.BorderShape
@@ -340,7 +341,7 @@ type Options struct {
}
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 {
@@ -1454,6 +1455,10 @@ 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":
@@ -1788,7 +1793,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][,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":
opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]"))
case "--min-height":
diff --git a/src/terminal.go b/src/terminal.go
index 9fac9779..ab385e2d 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -65,7 +65,8 @@ func init() {
// 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://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
@@ -1929,11 +1930,15 @@ func (t *Terminal) renderPreviewSpinner() {
}
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
} else {
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()
@@ -1946,11 +1951,15 @@ func (t *Terminal) renderPreviewArea(unchanged bool) {
body = t.previewer.lines[headerLines:]
// Always redraw header
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)
- if !unchanged {
+ if !unchanged && !t.previewOpts.clear {
t.pwindow.FinishFill()
}
@@ -1994,6 +2003,10 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
for _, passThrough := range passThroughs {
t.tui.PassThrough(passThrough)
}
+ if len(passThroughs) > 0 && len(line) == 0 {
+ continue
+ }
+
var fillRet tui.FillReturn
prefixWidth := 0
_, _, 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, 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
diff --git a/src/tui/dummy.go b/src/tui/dummy.go
index 352e2b09..d893a747 100644
--- a/src/tui/dummy.go
+++ b/src/tui/dummy.go
@@ -38,6 +38,9 @@ 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) GetChar() Event { return Event{} }
func (r *FullscreenRenderer) MaxX() int { return 0 }
diff --git a/src/tui/light.go b/src/tui/light.go
index cc828fa9..6b7eaaf4 100644
--- a/src/tui/light.go
+++ b/src/tui/light.go
@@ -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")
func (r *LightRenderer) PassThrough(str string) {
- r.queued.WriteString(str)
+ r.queued.WriteString("\x1b7" + str + "\x1b8")
r.flush()
}
@@ -756,6 +756,10 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
return w
}
+func (w *LightWindow) DrawBorder() {
+ w.drawBorder(false)
+}
+
func (w *LightWindow) DrawHBorder() {
w.drawBorder(true)
}
@@ -1095,7 +1099,8 @@ func (w *LightWindow) FinishFill() {
}
func (w *LightWindow) Erase() {
- w.drawBorder(false)
- // We don't erase the window here to avoid flickering during scroll
+ w.DrawBorder()
+ w.Move(0, 0)
+ w.FinishFill()
w.Move(0, 0)
}
diff --git a/src/tui/light_unix.go b/src/tui/light_unix.go
index 6dc058c8..b9e433cc 100644
--- a/src/tui/light_unix.go
+++ b/src/tui/light_unix.go
@@ -8,6 +8,7 @@ import (
"os/exec"
"strings"
"syscall"
+ "unsafe"
"github.com/junegunn/fzf/src/util"
"golang.org/x/term"
@@ -108,3 +109,19 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
}
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
+}
diff --git a/src/tui/tcell.go b/src/tui/tcell.go
index 0c3d4694..54feaf16 100644
--- a/src/tui/tcell.go
+++ b/src/tui/tcell.go
@@ -203,6 +203,11 @@ func (r *FullscreenRenderer) Refresh() {
// 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 {
ev := _screen.PollEvent()
switch ev := ev.(type) {
@@ -541,6 +546,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
+ w.drawBorder(false)
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))
}
+func (w *TcellWindow) DrawBorder() {
+ w.drawBorder(false)
+}
+
func (w *TcellWindow) DrawHBorder() {
w.drawBorder(true)
}
diff --git a/src/tui/tui.go b/src/tui/tui.go
index 4625e9b5..69ae8a1a 100644
--- a/src/tui/tui.go
+++ b/src/tui/tui.go
@@ -473,6 +473,13 @@ func MakeTransparentBorder() BorderStyle {
bottomRight: ' '}
}
+type termSize struct {
+ Lines int
+ Columns int
+ Width int
+ Height int
+}
+
type Renderer interface {
Init()
Resize(maxHeightFunc func(int) int)
@@ -490,6 +497,8 @@ type Renderer interface {
MaxX() int
MaxY() int
+ Size() (termSize, error)
+
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
}
@@ -499,6 +508,7 @@ type Window interface {
Width() int
Height() int
+ DrawBorder()
DrawHBorder()
Refresh()
FinishFill()