From b1a0ab8086f061640948299b9ed90a6b0c61c143 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Mon, 23 Oct 2023 01:01:47 +0900 Subject: [PATCH] Experimental Sixel support (#2544) --- CHANGELOG.md | 20 ++++++++++++++++++++ man/man1/fzf.1 | 20 +++++++++++++++++++- src/options.go | 9 +++++++-- src/terminal.go | 28 +++++++++++++++++++++++----- src/tui/dummy.go | 3 +++ src/tui/light.go | 11 ++++++++--- src/tui/light_unix.go | 17 +++++++++++++++++ src/tui/tcell.go | 10 ++++++++++ src/tui/tui.go | 10 ++++++++++ 9 files changed, 117 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cdd6c8..8fa9543 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 27d687b..735fa7f 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 b4f74e9..8325882 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 9fac977..ab385e2 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 352e2b0..d893a74 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 cc828fa..6b7eaaf 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 6dc058c..b9e433c 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 0c3d469..54feaf1 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 4625e9b..69ae8a1 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()