From 2ec382ae0eda85f89a4b977aa39425fb7e710df1 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 5 Dec 2020 21:16:35 +0900 Subject: [PATCH] Add --preview-window follow option --- CHANGELOG.md | 9 ++++++++ man/man1/fzf.1 | 36 +++++++++++++++++++++++--------- src/options.go | 9 ++++++-- src/terminal.go | 55 ++++++++++++++++++++++++++++--------------------- test/test_go.rb | 5 +++++ 5 files changed, 78 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7609316..9d2bf95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ CHANGELOG 0.24.4 ------ +- Added `--preview-window` option `follow` + ```sh + # Preview window will automatically scroll to the bottom + fzf --preview-window follow --preview 'for i in $(seq 100000); do + echo "$i" + sleep 0.01 + (( i % 300 == 0 )) && printf "\033[2J" + done' + ``` - Added `change-prompt` action ```sh fzf --prompt 'foo> ' --bind $'a:change-prompt:\x1b[31mbar> ' diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index f779ad5..eeb8394 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -439,7 +439,7 @@ e.g. done'\fR .RE .TP -.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]" +.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]" .RS .B POSITION: (default: right) @@ -448,27 +448,43 @@ e.g. \fBleft \fBright -\fRDetermines the layout of the preview window. If the argument contains -\fB:hidden\fR, the preview window will be hidden by default until -\fBtoggle-preview\fR action is triggered. Long lines are truncated by default. -Line wrap can be enabled with \fB:wrap\fR flag. Cyclic scrolling is enabled -with \fB:cycle\fR flag. +\fRDetermines the layout of the preview window. -If size is given as 0, preview window will not be visible, but fzf will still +* If the argument contains \fB:hidden\fR, the preview window will be hidden by +default until \fBtoggle-preview\fR action is triggered. + +* If size is given as 0, preview window will not be visible, but fzf will still execute the command in the background. -To change the style of the border of the preview window, specify one of +* Long lines are truncated by default. Line wrap can be enabled with +\fB:wrap\fR flag. + +* Preview window will automatically scroll to the bottom when \fB:follow\fR +flag is set, similarly to how \fBtail -f\fR works. + +.RS +e.g. + \fBfzf --preview-window follow --preview 'for i in $(seq 100000); do + echo "$i" + sleep 0.01 + (( i % 300 == 0 )) && printf "\\033[2J" + done'\fR +.RE + +* Cyclic scrolling is enabled with \fB:cycle\fR flag. + +* To change the style of the border of the preview window, specify one of \fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with sharp edges), or \fBnoborder\fR (no border). -\fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview +* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview window. \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is for adjusting the base offset so that you can see the text above it. It should be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form (\fB-/INTEGER\fR) for specifying a fraction of the preview window height. -\fBdefault\fR resets all options previously set to the default. +* \fBdefault\fR resets all options previously set to the default. .RS e.g. diff --git a/src/options.go b/src/options.go index b3fb78c..0607e5d 100644 --- a/src/options.go +++ b/src/options.go @@ -83,7 +83,7 @@ const usage = `usage: fzf [options] --preview=COMMAND Command to preview highlighted line ({}) --preview-window=OPT Preview window layout (default: right:50%) [up|down|left|right][:SIZE[%]] - [:[no]wrap][:[no]cycle][:[no]hidden] + [:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden] [:rounded|sharp|noborder] [:+SCROLL[-OFFSET]] [:default] @@ -169,6 +169,7 @@ type previewOpts struct { hidden bool wrap bool cycle bool + follow bool border tui.BorderShape } @@ -231,7 +232,7 @@ type Options struct { } func defaultPreviewOpts(command string) previewOpts { - return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, tui.BorderRounded} + return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded} } func defaultOptions() *Options { @@ -1081,6 +1082,10 @@ func parsePreviewWindow(opts *previewOpts, input string) { opts.border = tui.BorderSharp case "noborder": opts.border = tui.BorderNone + case "follow": + opts.follow = true + case "nofollow": + opts.follow = false default: if sizeRegex.MatchString(token) { opts.size = parseSize(token, 99, "window size") diff --git a/src/terminal.go b/src/terminal.go index 0853548..83b3a7b 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -51,6 +51,7 @@ type previewer struct { enabled bool scrollable bool final bool + following bool spinner string } @@ -140,7 +141,7 @@ type Terminal struct { selected map[int32]selectedItem version int64 reqBox *util.EventBox - preview previewOpts + previewOpts previewOpts previewer previewer previewed previewed previewBox *util.EventBox @@ -493,8 +494,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { merger: EmptyMerger, selected: make(map[int32]selectedItem), reqBox: util.NewEventBox(), - preview: opts.Preview, - previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, ""}, + previewOpts: opts.Preview, + previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""}, previewed: previewed{0, 0, 0, false}, previewBox: previewBox, eventBox: eventBox, @@ -732,11 +733,11 @@ func (t *Terminal) resizeWindows() { } } - previewVisible := t.isPreviewEnabled() && t.preview.size.size > 0 + previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0 minAreaWidth := minWidth minAreaHeight := minHeight if previewVisible { - switch t.preview.position { + switch t.previewOpts.position { case posUp, posDown: minAreaHeight *= 2 case posLeft, posRight: @@ -805,8 +806,8 @@ func (t *Terminal) resizeWindows() { createPreviewWindow := func(y int, x int, w int, h int) { pwidth := w pheight := h - if t.preview.border != tui.BorderNone { - previewBorder := tui.MakeBorderStyle(t.preview.border, t.unicode) + if t.previewOpts.border != tui.BorderNone { + previewBorder := tui.MakeBorderStyle(t.previewOpts.border, t.unicode) t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) pwidth -= 4 pheight -= 2 @@ -822,28 +823,28 @@ func (t *Terminal) resizeWindows() { } verticalPad := 2 minPreviewHeight := 3 - if t.preview.border == tui.BorderNone { + if t.previewOpts.border == tui.BorderNone { verticalPad = 0 minPreviewHeight = 1 } - switch t.preview.position { + switch t.previewOpts.position { case posUp: - pheight := calculateSize(height, t.preview.size, minHeight, minPreviewHeight, verticalPad) + pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad) t.window = t.tui.NewWindow( marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder) createPreviewWindow(marginInt[0], marginInt[3], width, pheight) case posDown: - pheight := calculateSize(height, t.preview.size, minHeight, minPreviewHeight, verticalPad) + pheight := calculateSize(height, t.previewOpts.size, minHeight, minPreviewHeight, verticalPad) t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width, height-pheight, false, noBorder) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) case posLeft: - pwidth := calculateSize(width, t.preview.size, minWidth, 5, 4) + pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4) t.window = t.tui.NewWindow( marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) case posRight: - pwidth := calculateSize(width, t.preview.size, minWidth, 5, 4) + pwidth := calculateSize(width, t.previewOpts.size, minWidth, 5, 4) t.window = t.tui.NewWindow( marginInt[0], marginInt[3], width-pwidth, height, false, noBorder) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) @@ -1291,7 +1292,7 @@ func (t *Terminal) renderPreviewText(unchanged bool) { prefixWidth := 0 _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { trimmed := []rune(str) - if !t.preview.wrap { + if !t.previewOpts.wrap { trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) } str, width := t.processTabs(trimmed, prefixWidth) @@ -1559,7 +1560,7 @@ func atopi(s string) int { } func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int { - offsetExpr := t.replacePlaceholder(t.preview.scroll, false, "", list) + offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list) nums := strings.Split(offsetExpr, "-") switch len(nums) { case 0: @@ -2041,7 +2042,7 @@ func (t *Terminal) Loop() { if focusedIndex != currentIndex || version != t.version { version = t.version focusedIndex = currentIndex - refreshPreview(t.preview.command) + refreshPreview(t.previewOpts.command) } case reqJump: if t.merger.Length() == 0 { @@ -2066,10 +2067,15 @@ func (t *Terminal) Loop() { }) case reqPreviewDisplay: result := value.(previewResult) - t.previewer.version = result.version + if t.previewer.version != result.version { + t.previewer.version = result.version + t.previewer.following = t.previewOpts.follow + } t.previewer.lines = result.lines t.previewer.spinner = result.spinner - if result.offset >= 0 { + if t.previewer.following { + t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height() + } else if result.offset >= 0 { t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1) } t.printPreview() @@ -2133,9 +2139,10 @@ func (t *Terminal) Loop() { if !t.previewer.scrollable { return } + t.previewer.following = false newOffset := t.previewer.offset + amount numLines := len(t.previewer.lines) - if t.preview.cycle { + if t.previewOpts.cycle { newOffset = (newOffset + numLines) % numLines } newOffset = util.Constrain(newOffset, 0, numLines-1) @@ -2176,17 +2183,17 @@ func (t *Terminal) Loop() { if t.hasPreviewer() { togglePreview(!t.previewer.enabled) if t.previewer.enabled { - valid, list := t.buildPlusList(t.preview.command, false) + valid, list := t.buildPlusList(t.previewOpts.command, false) if valid { t.cancelPreview() t.previewBox.Set(reqPreviewEnqueue, - previewRequest{t.preview.command, t.pwindow, list}) + previewRequest{t.previewOpts.command, t.pwindow, list}) } } } case actTogglePreviewWrap: if t.hasPreviewWindow() { - t.preview.wrap = !t.preview.wrap + t.previewOpts.wrap = !t.previewOpts.wrap req(reqPreviewRefresh) } case actToggleSort: @@ -2231,7 +2238,7 @@ func (t *Terminal) Loop() { togglePreview(true) refreshPreview(a.a) case actRefreshPreview: - refreshPreview(t.preview.command) + refreshPreview(t.previewOpts.command) case actReplaceQuery: if t.cy >= 0 && t.cy < t.merger.Length() { t.input = t.merger.Get(t.cy).item.text.ToRunes() @@ -2554,7 +2561,7 @@ func (t *Terminal) Loop() { if queryChanged { if t.isPreviewEnabled() { - _, _, q := hasPreviewFlags(t.preview.command) + _, _, q := hasPreviewFlags(t.previewOpts.command) if q { t.version++ } diff --git a/test/test_go.rb b/test/test_go.rb index d9d1e1b..416692e 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1832,6 +1832,11 @@ class TestGoFZF < TestBase tmux.send_keys 'b' tmux.until { |lines| assert_equal 'b> foo', lines[-1] } end + + def test_preview_window_follow + tmux.send_keys "#{FZF} --preview 'seq 1000 | nl' --preview-window down:noborder:follow", :Enter + tmux.until { |lines| assert_equal '1000 1000', lines[-1].strip } + end end module TestShell