Experimental Sixel support (#2544)

This commit is contained in:
Junegunn Choi 2023-10-23 01:01:47 +09:00
parent a33749eb71
commit b1a0ab8086
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
9 changed files with 117 additions and 11 deletions

View File

@ -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

View File

@ -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

View File

@ -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":

View File

@ -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

View File

@ -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 }

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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()