Add --preview-window follow option

pull/2281/head
Junegunn Choi 4 years ago
parent cbfee31593
commit 2ec382ae0e
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -3,6 +3,15 @@ CHANGELOG
0.24.4 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 - Added `change-prompt` action
```sh ```sh
fzf --prompt 'foo> ' --bind $'a:change-prompt:\x1b[31mbar> ' fzf --prompt 'foo> ' --bind $'a:change-prompt:\x1b[31mbar> '

@ -439,7 +439,7 @@ e.g.
done'\fR done'\fR
.RE .RE
.TP .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 .RS
.B POSITION: (default: right) .B POSITION: (default: right)
@ -448,27 +448,43 @@ e.g.
\fBleft \fBleft
\fBright \fBright
\fRDetermines the layout of the preview window. If the argument contains \fRDetermines the layout of the preview window.
\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.
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. 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 \fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
sharp edges), or \fBnoborder\fR (no border). 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 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 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 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 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. (\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 .RS
e.g. e.g.

@ -83,7 +83,7 @@ const usage = `usage: fzf [options]
--preview=COMMAND Command to preview highlighted line ({}) --preview=COMMAND Command to preview highlighted line ({})
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][:SIZE[%]] [up|down|left|right][:SIZE[%]]
[:[no]wrap][:[no]cycle][:[no]hidden] [:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
[:rounded|sharp|noborder] [:rounded|sharp|noborder]
[:+SCROLL[-OFFSET]] [:+SCROLL[-OFFSET]]
[:default] [:default]
@ -169,6 +169,7 @@ type previewOpts struct {
hidden bool hidden bool
wrap bool wrap bool
cycle bool cycle bool
follow bool
border tui.BorderShape border tui.BorderShape
} }
@ -231,7 +232,7 @@ type Options struct {
} }
func defaultPreviewOpts(command string) previewOpts { 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 { func defaultOptions() *Options {
@ -1081,6 +1082,10 @@ func parsePreviewWindow(opts *previewOpts, input string) {
opts.border = tui.BorderSharp opts.border = tui.BorderSharp
case "noborder": case "noborder":
opts.border = tui.BorderNone opts.border = tui.BorderNone
case "follow":
opts.follow = true
case "nofollow":
opts.follow = false
default: default:
if sizeRegex.MatchString(token) { if sizeRegex.MatchString(token) {
opts.size = parseSize(token, 99, "window size") opts.size = parseSize(token, 99, "window size")

@ -51,6 +51,7 @@ type previewer struct {
enabled bool enabled bool
scrollable bool scrollable bool
final bool final bool
following bool
spinner string spinner string
} }
@ -140,7 +141,7 @@ type Terminal struct {
selected map[int32]selectedItem selected map[int32]selectedItem
version int64 version int64
reqBox *util.EventBox reqBox *util.EventBox
preview previewOpts previewOpts previewOpts
previewer previewer previewer previewer
previewed previewed previewed previewed
previewBox *util.EventBox previewBox *util.EventBox
@ -493,8 +494,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
merger: EmptyMerger, merger: EmptyMerger,
selected: make(map[int32]selectedItem), selected: make(map[int32]selectedItem),
reqBox: util.NewEventBox(), reqBox: util.NewEventBox(),
preview: opts.Preview, previewOpts: opts.Preview,
previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, ""}, previewer: previewer{0, []string{}, 0, previewBox != nil && !opts.Preview.hidden, false, true, false, ""},
previewed: previewed{0, 0, 0, false}, previewed: previewed{0, 0, 0, false},
previewBox: previewBox, previewBox: previewBox,
eventBox: eventBox, 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 minAreaWidth := minWidth
minAreaHeight := minHeight minAreaHeight := minHeight
if previewVisible { if previewVisible {
switch t.preview.position { switch t.previewOpts.position {
case posUp, posDown: case posUp, posDown:
minAreaHeight *= 2 minAreaHeight *= 2
case posLeft, posRight: case posLeft, posRight:
@ -805,8 +806,8 @@ func (t *Terminal) resizeWindows() {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
pwidth := w pwidth := w
pheight := h pheight := h
if t.preview.border != tui.BorderNone { if t.previewOpts.border != tui.BorderNone {
previewBorder := tui.MakeBorderStyle(t.preview.border, t.unicode) previewBorder := tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder) t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
pwidth -= 4 pwidth -= 4
pheight -= 2 pheight -= 2
@ -822,28 +823,28 @@ func (t *Terminal) resizeWindows() {
} }
verticalPad := 2 verticalPad := 2
minPreviewHeight := 3 minPreviewHeight := 3
if t.preview.border == tui.BorderNone { if t.previewOpts.border == tui.BorderNone {
verticalPad = 0 verticalPad = 0
minPreviewHeight = 1 minPreviewHeight = 1
} }
switch t.preview.position { switch t.previewOpts.position {
case posUp: 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( t.window = t.tui.NewWindow(
marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder) marginInt[0]+pheight, marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], width, pheight) createPreviewWindow(marginInt[0], marginInt[3], width, pheight)
case posDown: 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( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width, height-pheight, false, noBorder) marginInt[0], marginInt[3], width, height-pheight, false, noBorder)
createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight) createPreviewWindow(marginInt[0]+height-pheight, marginInt[3], width, pheight)
case posLeft: 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( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder) marginInt[0], marginInt[3]+pwidth, width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
case posRight: 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( t.window = t.tui.NewWindow(
marginInt[0], marginInt[3], width-pwidth, height, false, noBorder) marginInt[0], marginInt[3], width-pwidth, height, false, noBorder)
createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height) createPreviewWindow(marginInt[0], marginInt[3]+width-pwidth, pwidth, height)
@ -1291,7 +1292,7 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
prefixWidth := 0 prefixWidth := 0
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool { _, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
trimmed := []rune(str) trimmed := []rune(str)
if !t.preview.wrap { if !t.previewOpts.wrap {
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X()) trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
} }
str, width := t.processTabs(trimmed, prefixWidth) str, width := t.processTabs(trimmed, prefixWidth)
@ -1559,7 +1560,7 @@ func atopi(s string) int {
} }
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) 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, "-") nums := strings.Split(offsetExpr, "-")
switch len(nums) { switch len(nums) {
case 0: case 0:
@ -2041,7 +2042,7 @@ func (t *Terminal) Loop() {
if focusedIndex != currentIndex || version != t.version { if focusedIndex != currentIndex || version != t.version {
version = t.version version = t.version
focusedIndex = currentIndex focusedIndex = currentIndex
refreshPreview(t.preview.command) refreshPreview(t.previewOpts.command)
} }
case reqJump: case reqJump:
if t.merger.Length() == 0 { if t.merger.Length() == 0 {
@ -2066,10 +2067,15 @@ func (t *Terminal) Loop() {
}) })
case reqPreviewDisplay: case reqPreviewDisplay:
result := value.(previewResult) 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.lines = result.lines
t.previewer.spinner = result.spinner 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.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
} }
t.printPreview() t.printPreview()
@ -2133,9 +2139,10 @@ func (t *Terminal) Loop() {
if !t.previewer.scrollable { if !t.previewer.scrollable {
return return
} }
t.previewer.following = false
newOffset := t.previewer.offset + amount newOffset := t.previewer.offset + amount
numLines := len(t.previewer.lines) numLines := len(t.previewer.lines)
if t.preview.cycle { if t.previewOpts.cycle {
newOffset = (newOffset + numLines) % numLines newOffset = (newOffset + numLines) % numLines
} }
newOffset = util.Constrain(newOffset, 0, numLines-1) newOffset = util.Constrain(newOffset, 0, numLines-1)
@ -2176,17 +2183,17 @@ func (t *Terminal) Loop() {
if t.hasPreviewer() { if t.hasPreviewer() {
togglePreview(!t.previewer.enabled) togglePreview(!t.previewer.enabled)
if 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 { if valid {
t.cancelPreview() t.cancelPreview()
t.previewBox.Set(reqPreviewEnqueue, t.previewBox.Set(reqPreviewEnqueue,
previewRequest{t.preview.command, t.pwindow, list}) previewRequest{t.previewOpts.command, t.pwindow, list})
} }
} }
} }
case actTogglePreviewWrap: case actTogglePreviewWrap:
if t.hasPreviewWindow() { if t.hasPreviewWindow() {
t.preview.wrap = !t.preview.wrap t.previewOpts.wrap = !t.previewOpts.wrap
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actToggleSort: case actToggleSort:
@ -2231,7 +2238,7 @@ func (t *Terminal) Loop() {
togglePreview(true) togglePreview(true)
refreshPreview(a.a) refreshPreview(a.a)
case actRefreshPreview: case actRefreshPreview:
refreshPreview(t.preview.command) refreshPreview(t.previewOpts.command)
case actReplaceQuery: case actReplaceQuery:
if t.cy >= 0 && t.cy < t.merger.Length() { if t.cy >= 0 && t.cy < t.merger.Length() {
t.input = t.merger.Get(t.cy).item.text.ToRunes() t.input = t.merger.Get(t.cy).item.text.ToRunes()
@ -2554,7 +2561,7 @@ func (t *Terminal) Loop() {
if queryChanged { if queryChanged {
if t.isPreviewEnabled() { if t.isPreviewEnabled() {
_, _, q := hasPreviewFlags(t.preview.command) _, _, q := hasPreviewFlags(t.previewOpts.command)
if q { if q {
t.version++ t.version++
} }

@ -1832,6 +1832,11 @@ class TestGoFZF < TestBase
tmux.send_keys 'b' tmux.send_keys 'b'
tmux.until { |lines| assert_equal 'b> foo', lines[-1] } tmux.until { |lines| assert_equal 'b> foo', lines[-1] }
end 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 end
module TestShell module TestShell

Loading…
Cancel
Save