mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-06-03 07:50:49 +00:00
Add --preview-window follow option
This commit is contained in:
parent
cbfee31593
commit
2ec382ae0e
|
@ -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…
Reference in New Issue
Block a user