diff --git a/src/options.go b/src/options.go index e4a6aa39..350ab94d 100644 --- a/src/options.go +++ b/src/options.go @@ -207,6 +207,14 @@ type previewOpts struct { alternative *previewOpts } +func (o *previewOpts) Visible() bool { + return o.size.size > 0 || o.alternative != nil && o.alternative.size.size > 0 +} + +func (o *previewOpts) Toggle() { + o.hidden = !o.hidden +} + func parseLabelPosition(opts *labelOpts, arg string) { opts.column = 0 opts.bottom = false diff --git a/src/terminal.go b/src/terminal.go index 28823675..53175d96 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -195,6 +195,7 @@ type Terminal struct { reqBox *util.EventBox initialPreviewOpts previewOpts previewOpts previewOpts + activePreviewOpts *previewOpts previewer previewer previewed previewed previewBox *util.EventBox @@ -241,7 +242,6 @@ const ( reqJump reqRefresh reqReinit - reqRedraw reqFullRedraw reqClose reqPrintQuery @@ -494,7 +494,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { delay = initialDelay } var previewBox *util.EventBox - showPreviewWindow := len(opts.Preview.command) > 0 && !opts.Preview.hidden // We need to start previewer if HTTP server is enabled even when --preview option is not specified if len(opts.Preview.command) > 0 || hasPreviewAction(opts) || opts.ListenPort > 0 { previewBox = util.NewEventBox() @@ -602,7 +601,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { reqBox: util.NewEventBox(), initialPreviewOpts: opts.Preview, previewOpts: opts.Preview, - previewer: previewer{0, []string{}, 0, showPreviewWindow, false, true, false, "", []bool{}}, + previewer: previewer{0, []string{}, 0, len(opts.Preview.command) > 0, false, true, false, "", []bool{}}, previewed: previewed{0, 0, 0, false}, previewBox: previewBox, eventBox: eventBox, @@ -985,7 +984,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) { if t.noInfoLine() { minAreaHeight -= 1 } - if t.isPreviewVisible() { + if t.mayNeedPreviewWindow() { minPreviewHeight := 1 + borderLines(t.previewOpts.border) minPreviewWidth := 5 switch t.previewOpts.position { @@ -1003,7 +1002,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) { return screenWidth, screenHeight, marginInt, paddingInt } -func (t *Terminal) resizeWindows() { +func (t *Terminal) resizeWindows(forcePreview bool) { screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding() width := screenWidth - marginInt[1] - marginInt[3] height := screenHeight - marginInt[0] - marginInt[2] @@ -1067,9 +1066,13 @@ func (t *Terminal) resizeWindows() { // Set up preview window noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) - if t.isPreviewVisible() { - var resizePreviewWindows func(previewOpts previewOpts) - resizePreviewWindows = func(previewOpts previewOpts) { + if t.mayNeedPreviewWindow() { + var resizePreviewWindows func(previewOpts *previewOpts) + resizePreviewWindows = func(previewOpts *previewOpts) { + t.activePreviewOpts = previewOpts + if previewOpts.size.size == 0 { + return + } hasThreshold := previewOpts.threshold > 0 && previewOpts.alternative != nil createPreviewWindow := func(y int, x int, w int, h int) { pwidth := w @@ -1124,11 +1127,21 @@ func (t *Terminal) resizeWindows() { case posUp, posDown: pheight := calculateSize(height, previewOpts.size, minHeight, minPreviewHeight, verticalPad) if hasThreshold && pheight < previewOpts.threshold { + t.activePreviewOpts = previewOpts.alternative + if forcePreview { + previewOpts.alternative.hidden = false + } if !previewOpts.alternative.hidden { - resizePreviewWindows(*previewOpts.alternative) + resizePreviewWindows(previewOpts.alternative) } return } + if forcePreview { + previewOpts.hidden = false + } + if previewOpts.hidden { + return + } // Put scrollbar closer to the right border for consistent look if t.borderShape.HasRight() { width++ @@ -1145,11 +1158,21 @@ func (t *Terminal) resizeWindows() { case posLeft, posRight: pwidth := calculateSize(width, previewOpts.size, minWidth, 5, 4) if hasThreshold && pwidth < previewOpts.threshold { + t.activePreviewOpts = previewOpts.alternative + if forcePreview { + previewOpts.alternative.hidden = false + } if !previewOpts.alternative.hidden { - resizePreviewWindows(*previewOpts.alternative) + resizePreviewWindows(previewOpts.alternative) } return } + if forcePreview { + previewOpts.hidden = false + } + if previewOpts.hidden { + return + } if previewOpts.position == posLeft { // Put scrollbar closer to the right border for consistent look if t.borderShape.HasRight() { @@ -1171,7 +1194,7 @@ func (t *Terminal) resizeWindows() { } } } - resizePreviewWindows(t.previewOpts) + resizePreviewWindows(&t.previewOpts) } // Without preview window @@ -1857,7 +1880,7 @@ func (t *Terminal) processTabs(runes []rune, prefixWidth int) (string, int) { } func (t *Terminal) printAll() { - t.resizeWindows() + t.resizeWindows(false) t.printList() t.printPrompt() t.printInfo() @@ -2142,10 +2165,8 @@ func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, pr }) } -func (t *Terminal) redraw(clear bool) { - if clear { - t.tui.Clear() - } +func (t *Terminal) redraw() { + t.tui.Clear() t.tui.Refresh() t.printAll() } @@ -2168,7 +2189,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo t.tui.Pause(true) cmd.Run() t.tui.Resume(true, false) - t.redraw(true) + t.redraw() t.refresh() } else { t.tui.Pause(false) @@ -2193,16 +2214,17 @@ func (t *Terminal) hasPreviewer() bool { return t.previewBox != nil } -func (t *Terminal) isPreviewEnabled() bool { - return t.hasPreviewer() && t.previewer.enabled +func (t *Terminal) mayNeedPreviewWindow() bool { + return t.hasPreviewer() && t.previewer.enabled && t.previewOpts.Visible() } -func (t *Terminal) isPreviewVisible() bool { - return t.isPreviewEnabled() && t.previewOpts.size.size > 0 +// Check if previewer is currently in action (invisible previewer with size 0 or visible previewer) +func (t *Terminal) isPreviewEnabled() bool { + return t.hasPreviewer() && t.previewer.enabled && (!t.previewOpts.Visible() || t.pwindow != nil) } func (t *Terminal) hasPreviewWindow() bool { - return t.pwindow != nil && t.isPreviewEnabled() + return t.pwindow != nil } func (t *Terminal) currentItem() *Item { @@ -2306,7 +2328,7 @@ func (t *Terminal) Loop() { pad := fitpad.pad t.tui.Resize(func(termHeight int) int { contentHeight := fit + t.extraLines() - if t.hasPreviewer() { + if t.mayNeedPreviewWindow() { if t.previewOpts.aboveOrBelow() { if t.previewOpts.size.percent { newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size)) @@ -2354,7 +2376,7 @@ func (t *Terminal) Loop() { t.mutex.Lock() t.initFunc() - t.resizeWindows() + t.resizeWindows(false) t.printPrompt() t.printInfo() t.printHeader() @@ -2598,11 +2620,9 @@ func (t *Terminal) Loop() { t.suppress = false case reqReinit: t.tui.Resume(t.fullscreen, t.sigstop) - t.redraw(true) - case reqRedraw: - t.redraw(false) + t.redraw() case reqFullRedraw: - t.redraw(true) + t.redraw() case reqClose: exit(func() int { if t.output() { @@ -2701,15 +2721,9 @@ func (t *Terminal) Loop() { } } } - togglePreview := func(enabled bool) bool { - if t.previewer.enabled != enabled { - t.previewer.enabled = enabled - // We need to immediately update t.pwindow so we don't use reqRedraw - t.resizeWindows() - req(reqPrompt, reqList, reqInfo, reqHeader) - return true - } - return false + updatePreviewWindow := func(forcePreview bool) { + t.resizeWindows(forcePreview) + req(reqPrompt, reqList, reqInfo, reqHeader) } toggle := func() bool { current := t.currentItem() @@ -2773,8 +2787,13 @@ func (t *Terminal) Loop() { return false case actTogglePreview: if t.hasPreviewer() { - togglePreview(!t.previewer.enabled) - if t.previewer.enabled { + if t.activePreviewOpts != nil { + t.activePreviewOpts.Toggle() + } else if !t.previewOpts.Visible() { + t.previewer.enabled = !t.previewer.enabled + } + updatePreviewWindow(false) + if t.isPreviewEnabled() { valid, list := t.buildPlusList(t.previewOpts.command, false, false) if valid { t.cancelPreview() @@ -2848,7 +2867,8 @@ func (t *Terminal) Loop() { t.prompt, t.promptLen = t.parsePrompt(a.a) req(reqPrompt) case actPreview: - togglePreview(true) + t.previewer.enabled = true + updatePreviewWindow(true) refreshPreview(a.a) case actRefreshPreview: refreshPreview(t.previewOpts.command) @@ -2910,8 +2930,9 @@ func (t *Terminal) Loop() { req(reqList, reqInfo) } case actClose: - if t.isPreviewEnabled() { - togglePreview(false) + if t.hasPreviewWindow() { + t.activePreviewOpts.Toggle() + updatePreviewWindow(false) } else { req(reqQuit) } @@ -3258,7 +3279,8 @@ func (t *Terminal) Loop() { } case actChangePreview: if t.previewOpts.command != a.a { - togglePreview(len(a.a) > 0) + t.previewer.enabled = len(a.a) > 0 + updatePreviewWindow(false) t.previewOpts.command = a.a refreshPreview(t.previewOpts.command) } @@ -3275,25 +3297,23 @@ func (t *Terminal) Loop() { a.a = strings.Join(append(tokens[1:], tokens[0]), "|") } - if t.previewOpts.hidden { - togglePreview(false) - } else { - // Full redraw - if !currentPreviewOpts.sameLayout(t.previewOpts) { - if togglePreview(true) { - refreshPreview(t.previewOpts.command) - } else { - req(reqRedraw) - } - } else if !currentPreviewOpts.sameContentLayout(t.previewOpts) { - t.previewed.version = 0 + // Full redraw + if !currentPreviewOpts.sameLayout(t.previewOpts) { + wasHidden := t.pwindow == nil + updatePreviewWindow(false) + if wasHidden && t.pwindow != nil { + refreshPreview(t.previewOpts.command) + } else { req(reqPreviewRefresh) } + } else if !currentPreviewOpts.sameContentLayout(t.previewOpts) { + t.previewed.version = 0 + req(reqPreviewRefresh) + } - // Adjust scroll offset - if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll { - scrollPreviewTo(t.evaluateScrollOffset()) - } + // Adjust scroll offset + if t.hasPreviewWindow() && currentPreviewOpts.scroll != t.previewOpts.scroll { + scrollPreviewTo(t.evaluateScrollOffset()) } case actNextSelected, actPrevSelected: if len(t.selected) > 0 { diff --git a/test/test_go.rb b/test/test_go.rb index 142df094..8031e81e 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -1502,7 +1502,7 @@ class TestGoFZF < TestBase rescue StandardError nil end - tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0), :Enter + tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter tmux.until do |lines| assert_equal 100, lines.item_count assert_equal ' 100/100', lines[1] @@ -1512,17 +1512,17 @@ class TestGoFZF < TestBase assert_path_exists tempname assert_equal %w[1], File.readlines(tempname, chomp: true) end - tmux.send_keys :Down - tmux.until { |lines| assert_equal '> 2', lines[3] } + tmux.send_keys :Space, :Down, :Down + tmux.until { |lines| assert_equal '> 3', lines[4] } wait do assert_path_exists tempname - assert_equal %w[1 2], File.readlines(tempname, chomp: true) + assert_equal %w[1], File.readlines(tempname, chomp: true) end - tmux.send_keys :Down - tmux.until { |lines| assert_equal '> 3', lines[4] } + tmux.send_keys :Space, :Down + tmux.until { |lines| assert_equal '> 4', lines[5] } wait do assert_path_exists tempname - assert_equal %w[1 2 3], File.readlines(tempname, chomp: true) + assert_equal %w[1 3 4], File.readlines(tempname, chomp: true) end end @@ -2405,7 +2405,7 @@ class TestGoFZF < TestBase end def test_start_event - tmux.send_keys 'seq 100 | fzf --multi --sync --preview-window border-none --bind "start:select-all+last+preview(echo welcome)"', :Enter + tmux.send_keys 'seq 100 | fzf --multi --sync --preview-window hidden:border-none --bind "start:select-all+last+preview(echo welcome)"', :Enter tmux.until do |lines| assert_match(/>100.*welcome/, lines[0]) assert_includes(lines[-2], '100/100 (100)') @@ -2495,6 +2495,14 @@ class TestGoFZF < TestBase tmux.until { |lines| assert_equal 100, lines.item_count } tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] } end + + def test_toggle_alternative_preview_window + tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter + tmux.until { |lines| assert_equal 10, lines.item_count } + tmux.until { |lines| refute_includes lines, '/1/1/' } + tmux.send_keys :Space + tmux.until { |lines| assert_includes lines, '/1/1/' } + end end module TestShell