change-preview-window to take multiple option sets separated by '|'

So you can "rotate" through the different options with a single binding.

  fzf --preview 'cat {}' \
      --bind 'ctrl-/:change-preview-window(70%|down,40%,border-horizontal|hidden|)'

Close #2376
pull/2682/head
Junegunn Choi 3 years ago
parent 20b4e6953e
commit 43f0d0cacd
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -7,6 +7,10 @@ CHANGELOG
- cf. `preview(...)` is a one-off action that doesn't change the default - cf. `preview(...)` is a one-off action that doesn't change the default
preview command preview command
- Added `change-preview-window(...)` action - Added `change-preview-window(...)` action
- You can rotate through the different options separated by `|`
```sh
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-horizontal|hidden|right)'
```
0.28.0 0.28.0
------ ------

@ -821,7 +821,7 @@ A key or an event can be bound to one or more of the following actions.
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-preview(...)\fR (change \fB--preview\fR option) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option) \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
\fBchange-prompt(...)\fR (change prompt to the given string) \fBchange-prompt(...)\fR (change prompt to the given string)
\fBclear-screen\fR \fIctrl-l\fR \fBclear-screen\fR \fIctrl-l\fR
\fBclear-selection\fR (clear multi-selection) \fBclear-selection\fR (clear multi-selection)
@ -972,7 +972,6 @@ commands in addition to the default preview command given by \fB--preview\fR
option. option.
e.g. e.g.
# Default preview command with an extra preview binding # Default preview command with an extra preview binding
fzf --preview 'file {}' --bind '?:preview:cat {}' fzf --preview 'file {}' --bind '?:preview:cat {}'
@ -983,6 +982,22 @@ e.g.
# Preview window hidden by default, it appears when you first hit '?' # Preview window hidden by default, it appears when you first hit '?'
fzf --bind '?:preview:cat {}' --preview-window hidden fzf --bind '?:preview:cat {}' --preview-window hidden
.SS CHANGE PREVIEW WINDOW ATTRIBUTES
\fBchange-preview-window\fR action can be used to change the properties of the
preview window. Unlike the \fB--preview-window\fR option, you can specify
multiple sets of options separated by '|' characters.
e.g.
# Rotate through the options using CTRL-/
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(right,70%|down,40%,border-horizontal|hidden|right)'
# The default properties given by `--preview-window` are inherited, so an empty string in the list is interpreted as the default
fzf --preview 'cat {}' --preview-window 'right,40%,border-left' --bind 'ctrl-/:change-preview-window(70%|down,border-top|hidden|)'
# This is equivalent to toggle-preview action
fzf --preview 'cat {}' --bind 'ctrl-/:change-preview-window(hidden|)'
.SH AUTHOR .SH AUTHOR
Junegunn Choi (\fIjunegunn.c@gmail.com\fR) Junegunn Choi (\fIjunegunn.c@gmail.com\fR)

@ -224,7 +224,7 @@ type Options struct {
Filter *string Filter *string
ToggleSort bool ToggleSort bool
Expect map[tui.Event]string Expect map[tui.Event]string
Keymap map[tui.Event][]action Keymap map[tui.Event][]*action
Preview previewOpts Preview previewOpts
PrintQuery bool PrintQuery bool
ReadZero bool ReadZero bool
@ -287,7 +287,7 @@ func defaultOptions() *Options {
Filter: nil, Filter: nil,
ToggleSort: false, ToggleSort: false,
Expect: make(map[tui.Event]string), Expect: make(map[tui.Event]string),
Keymap: make(map[tui.Event][]action), Keymap: make(map[tui.Event][]*action),
Preview: defaultPreviewOpts(""), Preview: defaultPreviewOpts(""),
PrintQuery: false, PrintQuery: false,
ReadZero: false, ReadZero: false,
@ -798,7 +798,7 @@ func init() {
`(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) `(?si)[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind):.+|[:+](execute(?:-multi|-silent)?|reload|preview|change-prompt|change-preview-window|change-preview|unbind)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`)
} }
func parseKeymap(keymap map[tui.Event][]action, str string) { func parseKeymap(keymap map[tui.Event][]*action, str string) {
masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string { masked := executeRegexp.ReplaceAllStringFunc(str, func(src string) string {
symbol := ":" symbol := ":"
if strings.HasPrefix(src, "+") { if strings.HasPrefix(src, "+") {
@ -854,7 +854,7 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
idx2 := len(pair[0]) + 1 idx2 := len(pair[0]) + 1
specs := strings.Split(pair[1], "+") specs := strings.Split(pair[1], "+")
actions := make([]action, 0, len(specs)) actions := make([]*action, 0, len(specs))
appendAction := func(types ...actionType) { appendAction := func(types ...actionType) {
actions = append(actions, toActions(types...)...) actions = append(actions, toActions(types...)...)
} }
@ -1033,20 +1033,22 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
if spec[offset] == ':' { if spec[offset] == ':' {
if specIndex == len(specs)-1 { if specIndex == len(specs)-1 {
actionArg = spec[offset+1:] actionArg = spec[offset+1:]
actions = append(actions, action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} else { } else {
prevSpec = spec + "+" prevSpec = spec + "+"
continue continue
} }
} else { } else {
actionArg = spec[offset+1 : len(spec)-1] actionArg = spec[offset+1 : len(spec)-1]
actions = append(actions, action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} }
if t == actUnbind { if t == actUnbind {
parseKeyChords(actionArg, "unbind target required") parseKeyChords(actionArg, "unbind target required")
} else if t == actChangePreviewWindow { } else if t == actChangePreviewWindow {
opts := previewOpts{} opts := previewOpts{}
parsePreviewWindow(&opts, actionArg) for _, arg := range strings.Split(actionArg, "|") {
parsePreviewWindow(&opts, arg)
}
} }
} }
} }
@ -1088,7 +1090,7 @@ func isExecuteAction(str string) actionType {
return actIgnore return actIgnore
} }
func parseToggleSort(keymap map[tui.Event][]action, str string) { func parseToggleSort(keymap map[tui.Event][]*action, str string) {
keys := parseKeyChords(str, "key name required") keys := parseKeyChords(str, "key name required")
if len(keys) != 1 { if len(keys) != 1 {
errorExit("multiple keys specified") errorExit("multiple keys specified")
@ -1656,7 +1658,7 @@ func postProcessOptions(opts *Options) {
// Extend the default key map // Extend the default key map
keymap := defaultKeymap() keymap := defaultKeymap()
for key, actions := range opts.Keymap { for key, actions := range opts.Keymap {
lastChangePreviewWindow := action{t: actIgnore} var lastChangePreviewWindow *action
for _, act := range actions { for _, act := range actions {
switch act.t { switch act.t {
case actToggleSort: case actToggleSort:
@ -1670,8 +1672,8 @@ func postProcessOptions(opts *Options) {
// and it comes first in the list. // and it comes first in the list.
// * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20) // * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
// -> change-preview-window(up,+20)+preview(sleep 3; cat {}) // -> change-preview-window(up,+20)+preview(sleep 3; cat {})
if lastChangePreviewWindow.t == actChangePreviewWindow { if lastChangePreviewWindow != nil {
reordered := []action{lastChangePreviewWindow} reordered := []*action{lastChangePreviewWindow}
for _, act := range actions { for _, act := range actions {
if act.t != actChangePreviewWindow { if act.t != actChangePreviewWindow {
reordered = append(reordered, act) reordered = append(reordered, act)

@ -135,7 +135,7 @@ type Terminal struct {
toggleSort bool toggleSort bool
delimiter Delimiter delimiter Delimiter
expect map[tui.Event]string expect map[tui.Event]string
keymap map[tui.Event][]action keymap map[tui.Event][]*action
pressed string pressed string
printQuery bool printQuery bool
history *History history *History
@ -217,6 +217,7 @@ const (
reqRefresh reqRefresh
reqReinit reqReinit
reqRedraw reqRedraw
reqFullRedraw
reqClose reqClose
reqPrintQuery reqPrintQuery
reqPreviewEnqueue reqPreviewEnqueue
@ -229,6 +230,7 @@ const (
type action struct { type action struct {
t actionType t actionType
a string a string
c int
} }
type actionType int type actionType int
@ -340,16 +342,16 @@ type previewResult struct {
spinner string spinner string
} }
func toActions(types ...actionType) []action { func toActions(types ...actionType) []*action {
actions := make([]action, len(types)) actions := make([]*action, len(types))
for idx, t := range types { for idx, t := range types {
actions[idx] = action{t: t, a: ""} actions[idx] = &action{t: t, a: ""}
} }
return actions return actions
} }
func defaultKeymap() map[tui.Event][]action { func defaultKeymap() map[tui.Event][]*action {
keymap := make(map[tui.Event][]action) keymap := make(map[tui.Event][]*action)
add := func(e tui.EventType, a actionType) { add := func(e tui.EventType, a actionType) {
keymap[e.AsEvent()] = toActions(a) keymap[e.AsEvent()] = toActions(a)
} }
@ -1778,8 +1780,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr
}) })
} }
func (t *Terminal) redraw() { func (t *Terminal) redraw(clear bool) {
t.tui.Clear() if clear {
t.tui.Clear()
}
t.tui.Refresh() t.tui.Refresh()
t.printAll() t.printAll()
} }
@ -1799,7 +1803,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.tui.Pause(true) t.tui.Pause(true)
cmd.Run() cmd.Run()
t.tui.Resume(true, false) t.tui.Resume(true, false)
t.redraw() t.redraw(true)
t.refresh() t.refresh()
} else { } else {
t.tui.Pause(false) t.tui.Pause(false)
@ -1944,7 +1948,7 @@ func (t *Terminal) Loop() {
go func() { go func() {
for { for {
<-resizeChan <-resizeChan
t.reqBox.Set(reqRedraw, nil) t.reqBox.Set(reqFullRedraw, nil)
} }
}() }()
@ -2194,9 +2198,11 @@ func (t *Terminal) Loop() {
t.suppress = false t.suppress = false
case reqReinit: case reqReinit:
t.tui.Resume(t.fullscreen, t.sigstop) t.tui.Resume(t.fullscreen, t.sigstop)
t.redraw() t.redraw(true)
case reqRedraw: case reqRedraw:
t.redraw() t.redraw(false)
case reqFullRedraw:
t.redraw(true)
case reqClose: case reqClose:
exit(func() int { exit(func() int {
if t.output() { if t.output() {
@ -2268,7 +2274,6 @@ func (t *Terminal) Loop() {
if t.previewer.enabled != enabled { if t.previewer.enabled != enabled {
t.previewer.enabled = enabled t.previewer.enabled = enabled
// We need to immediately update t.pwindow so we don't use reqRedraw // We need to immediately update t.pwindow so we don't use reqRedraw
t.tui.Clear()
t.resizeWindows() t.resizeWindows()
req(reqPrompt, reqList, reqInfo, reqHeader) req(reqPrompt, reqList, reqInfo, reqHeader)
return true return true
@ -2310,12 +2315,12 @@ func (t *Terminal) Loop() {
} }
} }
actionsFor := func(eventType tui.EventType) []action { actionsFor := func(eventType tui.EventType) []*action {
return t.keymap[eventType.AsEvent()] return t.keymap[eventType.AsEvent()]
} }
var doAction func(action) bool var doAction func(*action) bool
doActions := func(actions []action) bool { doActions := func(actions []*action) bool {
for _, action := range actions { for _, action := range actions {
if !doAction(action) { if !doAction(action) {
return false return false
@ -2323,7 +2328,7 @@ func (t *Terminal) Loop() {
} }
return true return true
} }
doAction = func(a action) bool { doAction = func(a *action) bool {
switch a.t { switch a.t {
case actIgnore: case actIgnore:
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
@ -2503,14 +2508,14 @@ func (t *Terminal) Loop() {
} }
case actToggleIn: case actToggleIn:
if t.layout != layoutDefault { if t.layout != layoutDefault {
return doAction(action{t: actToggleUp}) return doAction(&action{t: actToggleUp})
} }
return doAction(action{t: actToggleDown}) return doAction(&action{t: actToggleDown})
case actToggleOut: case actToggleOut:
if t.layout != layoutDefault { if t.layout != layoutDefault {
return doAction(action{t: actToggleDown}) return doAction(&action{t: actToggleDown})
} }
return doAction(action{t: actToggleUp}) return doAction(&action{t: actToggleUp})
case actToggleDown: case actToggleDown:
if t.multi > 0 && t.merger.Length() > 0 && toggle() { if t.multi > 0 && t.merger.Length() > 0 && toggle() {
t.vmove(-1, true) t.vmove(-1, true)
@ -2534,7 +2539,7 @@ func (t *Terminal) Loop() {
req(reqClose) req(reqClose)
} }
case actClearScreen: case actClearScreen:
req(reqRedraw) req(reqFullRedraw)
case actClearQuery: case actClearQuery:
t.input = []rune{} t.input = []rune{}
t.cx = 0 t.cx = 0
@ -2732,7 +2737,14 @@ func (t *Terminal) Loop() {
// Reset preview options and apply the additional options // Reset preview options and apply the additional options
t.previewOpts = t.initialPreviewOpts t.previewOpts = t.initialPreviewOpts
parsePreviewWindow(&t.previewOpts, a.a)
// Split window options
tokens := strings.Split(a.a, "|")
parsePreviewWindow(&t.previewOpts, tokens[0])
if len(tokens) > 1 {
a.a = strings.Join(append(tokens[1:], tokens[0]), "|")
a.c++
}
if t.previewOpts.hidden { if t.previewOpts.hidden {
togglePreview(false) togglePreview(false)
@ -2761,7 +2773,7 @@ func (t *Terminal) Loop() {
if t.jumping == jumpDisabled { if t.jumping == jumpDisabled {
actions := t.keymap[event.Comparable()] actions := t.keymap[event.Comparable()]
if len(actions) == 0 && event.Type == tui.Rune { if len(actions) == 0 && event.Type == tui.Rune {
doAction(action{t: actRune}) doAction(&action{t: actRune})
} else if !doActions(actions) { } else if !doActions(actions) {
continue continue
} }

@ -2191,6 +2191,23 @@ class TestGoFZF < TestBase
assert_equal ' 3', lines[1] assert_equal ' 3', lines[1]
end end
end end
def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter
3.times do
tmux.until { |lines| lines[0].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[0].end_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| lines[-1].start_with?('hello') }
tmux.send_keys 'a'
tmux.until { |lines| assert_equal 'hello', lines[0] }
tmux.send_keys 'a'
tmux.until { |lines| refute_includes lines[0], 'hello' }
tmux.send_keys 'a'
end
end
end end
module TestShell module TestShell

Loading…
Cancel
Save