diff --git a/CHANGELOG.md b/CHANGELOG.md index 8475402..89e09bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ CHANGELOG - cf. `preview(...)` is a one-off action that doesn't change the default preview command - 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 ------ diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index c87d2c7..4920dba 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -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 \fBcancel\fR (clear query string if not empty, abort fzf otherwise) \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) \fBclear-screen\fR \fIctrl-l\fR \fBclear-selection\fR (clear multi-selection) @@ -972,7 +972,6 @@ commands in addition to the default preview command given by \fB--preview\fR option. e.g. - # Default preview command with an extra preview binding fzf --preview 'file {}' --bind '?:preview:cat {}' @@ -983,6 +982,22 @@ e.g. # Preview window hidden by default, it appears when you first hit '?' 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 Junegunn Choi (\fIjunegunn.c@gmail.com\fR) diff --git a/src/options.go b/src/options.go index 63b385c..b3bcdea 100644 --- a/src/options.go +++ b/src/options.go @@ -224,7 +224,7 @@ type Options struct { Filter *string ToggleSort bool Expect map[tui.Event]string - Keymap map[tui.Event][]action + Keymap map[tui.Event][]*action Preview previewOpts PrintQuery bool ReadZero bool @@ -287,7 +287,7 @@ func defaultOptions() *Options { Filter: nil, ToggleSort: false, Expect: make(map[tui.Event]string), - Keymap: make(map[tui.Event][]action), + Keymap: make(map[tui.Event][]*action), Preview: defaultPreviewOpts(""), PrintQuery: 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)(\([^)]*\)|\[[^\]]*\]|~[^~]*~|![^!]*!|@[^@]*@|\#[^\#]*\#|\$[^\$]*\$|%[^%]*%|\^[^\^]*\^|&[^&]*&|\*[^\*]*\*|;[^;]*;|/[^/]*/|\|[^\|]*\|)`) } -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 { symbol := ":" if strings.HasPrefix(src, "+") { @@ -854,7 +854,7 @@ func parseKeymap(keymap map[tui.Event][]action, str string) { idx2 := len(pair[0]) + 1 specs := strings.Split(pair[1], "+") - actions := make([]action, 0, len(specs)) + actions := make([]*action, 0, len(specs)) appendAction := func(types ...actionType) { actions = append(actions, toActions(types...)...) } @@ -1033,20 +1033,22 @@ func parseKeymap(keymap map[tui.Event][]action, str string) { if spec[offset] == ':' { if specIndex == len(specs)-1 { actionArg = spec[offset+1:] - actions = append(actions, action{t: t, a: actionArg}) + actions = append(actions, &action{t: t, a: actionArg}) } else { prevSpec = spec + "+" continue } } else { 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 { parseKeyChords(actionArg, "unbind target required") } else if t == actChangePreviewWindow { 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 } -func parseToggleSort(keymap map[tui.Event][]action, str string) { +func parseToggleSort(keymap map[tui.Event][]*action, str string) { keys := parseKeyChords(str, "key name required") if len(keys) != 1 { errorExit("multiple keys specified") @@ -1656,7 +1658,7 @@ func postProcessOptions(opts *Options) { // Extend the default key map keymap := defaultKeymap() for key, actions := range opts.Keymap { - lastChangePreviewWindow := action{t: actIgnore} + var lastChangePreviewWindow *action for _, act := range actions { switch act.t { case actToggleSort: @@ -1670,8 +1672,8 @@ func postProcessOptions(opts *Options) { // 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,+20)+preview(sleep 3; cat {}) - if lastChangePreviewWindow.t == actChangePreviewWindow { - reordered := []action{lastChangePreviewWindow} + if lastChangePreviewWindow != nil { + reordered := []*action{lastChangePreviewWindow} for _, act := range actions { if act.t != actChangePreviewWindow { reordered = append(reordered, act) diff --git a/src/terminal.go b/src/terminal.go index 804e2b2..5e89cd4 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -135,7 +135,7 @@ type Terminal struct { toggleSort bool delimiter Delimiter expect map[tui.Event]string - keymap map[tui.Event][]action + keymap map[tui.Event][]*action pressed string printQuery bool history *History @@ -217,6 +217,7 @@ const ( reqRefresh reqReinit reqRedraw + reqFullRedraw reqClose reqPrintQuery reqPreviewEnqueue @@ -229,6 +230,7 @@ const ( type action struct { t actionType a string + c int } type actionType int @@ -340,16 +342,16 @@ type previewResult struct { spinner string } -func toActions(types ...actionType) []action { - actions := make([]action, len(types)) +func toActions(types ...actionType) []*action { + actions := make([]*action, len(types)) for idx, t := range types { - actions[idx] = action{t: t, a: ""} + actions[idx] = &action{t: t, a: ""} } return actions } -func defaultKeymap() map[tui.Event][]action { - keymap := make(map[tui.Event][]action) +func defaultKeymap() map[tui.Event][]*action { + keymap := make(map[tui.Event][]*action) add := func(e tui.EventType, a actionType) { keymap[e.AsEvent()] = toActions(a) } @@ -1778,8 +1780,10 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr }) } -func (t *Terminal) redraw() { - t.tui.Clear() +func (t *Terminal) redraw(clear bool) { + if clear { + t.tui.Clear() + } t.tui.Refresh() t.printAll() } @@ -1799,7 +1803,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo t.tui.Pause(true) cmd.Run() t.tui.Resume(true, false) - t.redraw() + t.redraw(true) t.refresh() } else { t.tui.Pause(false) @@ -1944,7 +1948,7 @@ func (t *Terminal) Loop() { go func() { for { <-resizeChan - t.reqBox.Set(reqRedraw, nil) + t.reqBox.Set(reqFullRedraw, nil) } }() @@ -2194,9 +2198,11 @@ func (t *Terminal) Loop() { t.suppress = false case reqReinit: t.tui.Resume(t.fullscreen, t.sigstop) - t.redraw() + t.redraw(true) case reqRedraw: - t.redraw() + t.redraw(false) + case reqFullRedraw: + t.redraw(true) case reqClose: exit(func() int { if t.output() { @@ -2268,7 +2274,6 @@ func (t *Terminal) Loop() { if t.previewer.enabled != enabled { t.previewer.enabled = enabled // We need to immediately update t.pwindow so we don't use reqRedraw - t.tui.Clear() t.resizeWindows() req(reqPrompt, reqList, reqInfo, reqHeader) 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()] } - var doAction func(action) bool - doActions := func(actions []action) bool { + var doAction func(*action) bool + doActions := func(actions []*action) bool { for _, action := range actions { if !doAction(action) { return false @@ -2323,7 +2328,7 @@ func (t *Terminal) Loop() { } return true } - doAction = func(a action) bool { + doAction = func(a *action) bool { switch a.t { case actIgnore: case actExecute, actExecuteSilent: @@ -2503,14 +2508,14 @@ func (t *Terminal) Loop() { } case actToggleIn: 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: 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: if t.multi > 0 && t.merger.Length() > 0 && toggle() { t.vmove(-1, true) @@ -2534,7 +2539,7 @@ func (t *Terminal) Loop() { req(reqClose) } case actClearScreen: - req(reqRedraw) + req(reqFullRedraw) case actClearQuery: t.input = []rune{} t.cx = 0 @@ -2732,7 +2737,14 @@ func (t *Terminal) Loop() { // Reset preview options and apply the additional options 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 { togglePreview(false) @@ -2761,7 +2773,7 @@ func (t *Terminal) Loop() { if t.jumping == jumpDisabled { actions := t.keymap[event.Comparable()] if len(actions) == 0 && event.Type == tui.Rune { - doAction(action{t: actRune}) + doAction(&action{t: actRune}) } else if !doActions(actions) { continue } diff --git a/test/test_go.rb b/test/test_go.rb index 3fc66f9..95759bf 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2191,6 +2191,23 @@ class TestGoFZF < TestBase assert_equal ' 3', lines[1] 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 module TestShell