diff --git a/CHANGELOG.md b/CHANGELOG.md index 29323b31..55f85a61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,36 @@ CHANGELOG ========= +0.34.0 +------ +- Added support for adaptive `--height`. If the `--height` value is prefixed + with `~`, fzf will automatically determine the height in the range according + to the input size. + ```sh + seq 1 | fzf --height ~70% --border --padding 1 --margin 1 + seq 10 | fzf --height ~70% --border --padding 1 --margin 1 + seq 100 | fzf --height ~70% --border --padding 1 --margin 1 + ``` + - There are a few limitations + - Not compatible with percent top/bottom margin/padding + ```sh + # This is not allowed (top/bottom margin in percent value) + fzf --height ~50% --border --margin 5%,10% + + # This is allowed (top/bottom margin in fixed value) + fzf --height ~50% --border --margin 2,10% + ``` + - fzf will not start until it can determine the right height for the input + ```sh + # fzf will open immediately + (sleep 2; seq 10) | fzf --height 50% + + # fzf will open after 2 seconds + (sleep 2; seq 10) | fzf --height ~50% + (sleep 2; seq 1000) | fzf --height ~50% + ``` +- Fixed tcell renderer used to render full-screen fzf on Windows + 0.33.0 ------ - Added `--scheme=[default|path|history]` option to choose scoring scheme diff --git a/Makefile b/Makefile index 435c9ca8..15808154 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,7 @@ target/$(BINARYLOONG64): $(SOURCES) GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@ bin/fzf: target/$(BINARY) | bin + -rm -f bin/fzf cp -f target/$(BINARY) bin/fzf docker: diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index a6455cc2..c8b961de 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -177,9 +177,11 @@ actions are affected: Label characters for \fBjump\fR and \fBjump-accept\fR .SS Layout .TP -.BI "--height=" "HEIGHT[%]" +.BI "--height=" "[~]HEIGHT[%]" Display fzf window below the cursor with the given height instead of using -the full screen. +the full screen. When prefixed with \fB~\fR, fzf will automatically determine +the height in the range according to the input size. Note that adaptive height +is not compatible with top/bottom margin and padding given in percent size. .TP .BI "--min-height=" "HEIGHT" Minimum height when \fB--height\fR is given in percent (default: 10). diff --git a/src/core.go b/src/core.go index ee55f1e3..2ddddc35 100644 --- a/src/core.go +++ b/src/core.go @@ -194,10 +194,17 @@ func Run(opts *Options, version string, revision string) { // Terminal I/O terminal := NewTerminal(opts, eventBox) + maxFit := 0 // Maximum number of items that can fit on screen + padHeight := 0 + heightUnknown := opts.Height.auto + if heightUnknown { + maxFit, padHeight = terminal.MaxFitAndPad(opts) + } deferred := opts.Select1 || opts.Exit0 go terminal.Loop() - if !deferred { - terminal.startChan <- true + if !deferred && !heightUnknown { + // Start right away + terminal.startChan <- fitpad{-1, -1} } // Event coordination @@ -216,7 +223,19 @@ func Run(opts *Options, version string, revision string) { go reader.restart(command) } eventBox.Watch(EvtReadNew) + total := 0 query := []rune{} + determine := func(final bool) { + if heightUnknown { + if total >= maxFit || final { + heightUnknown = false + terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight} + } + } else if deferred { + deferred = false + terminal.startChan <- fitpad{-1, -1} + } + } for { delay := true ticks++ @@ -249,11 +268,15 @@ func Run(opts *Options, version string, revision string) { reading = reading && evt == EvtReadNew } snapshot, count := chunkList.Snapshot() - terminal.UpdateCount(count, !reading, value.(*string)) + total = count + terminal.UpdateCount(total, !reading, value.(*string)) if opts.Sync { opts.Sync = false terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false) } + if heightUnknown && !deferred { + determine(!reading) + } reset := clearCache() matcher.Reset(snapshot, input(reset), false, !reading, sort, reset) @@ -295,8 +318,7 @@ func Run(opts *Options, version string, revision string) { if deferred { count := val.Length() if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 { - deferred = false - terminal.startChan <- true + determine(val.final) } else if val.final { if opts.Exit0 && count == 0 || opts.Select1 && count == 1 { if opts.PrintQuery { @@ -313,8 +335,7 @@ func Run(opts *Options, version string, revision string) { } os.Exit(exitNoMatch) } - deferred = false - terminal.startChan <- true + determine(val.final) } } terminal.UpdateList(val, clearSelection()) diff --git a/src/options.go b/src/options.go index 41782fa4..e17efc96 100644 --- a/src/options.go +++ b/src/options.go @@ -53,8 +53,10 @@ const usage = `usage: fzf [options] --jump-labels=CHARS Label characters for jump and jump-accept Layout - --height=HEIGHT[%] Display fzf window below the cursor with the given - height instead of using fullscreen + --height=[~]HEIGHT[%] Display fzf window below the cursor with the given + height instead of using fullscreen. + If prefixed with '~', fzf will determine the height + according to the input size. --min-height=HEIGHT Minimum height when --height is given in percent (default: 10) --layout=LAYOUT Choose layout: [default|reverse|reverse-list] @@ -131,6 +133,12 @@ const ( byEnd ) +type heightSpec struct { + size float64 + percent bool + auto bool +} + type sizeSpec struct { size float64 percent bool @@ -180,6 +188,10 @@ type previewOpts struct { alternative *previewOpts } +func (a previewOpts) aboveOrBelow() bool { + return a.size.size > 0 && (a.position == posUp || a.position == posDown) +} + func (a previewOpts) sameLayout(b previewOpts) bool { return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden && a.threshold == b.threshold && (a.alternative != nil && b.alternative != nil && a.alternative.sameLayout(*b.alternative) || @@ -211,7 +223,7 @@ type Options struct { Theme *tui.ColorTheme Black bool Bold bool - Height sizeSpec + Height heightSpec MinHeight int Layout layoutType Cycle bool @@ -1076,11 +1088,6 @@ func parseKeymap(keymap map[tui.Event][]*action, str string) { } if t == actUnbind || t == actRebind { parseKeyChords(actionArg, spec[0:offset]+" target required") - } else if t == actChangePreviewWindow { - opts := previewOpts{} - for _, arg := range strings.Split(actionArg, "|") { - parsePreviewWindow(&opts, arg) - } } } } @@ -1160,9 +1167,17 @@ func parseSize(str string, maxPercent float64, label string) sizeSpec { return sizeSpec{val, percent} } -func parseHeight(str string) sizeSpec { +func parseHeight(str string) heightSpec { + heightSpec := heightSpec{} + if strings.HasPrefix(str, "~") { + heightSpec.auto = true + str = str[1:] + } + size := parseSize(str, 100, "height") - return size + heightSpec.size = size.size + heightSpec.percent = size.percent + return heightSpec } func parseLayout(str string) layoutType { @@ -1525,11 +1540,11 @@ func parseOptions(opts *Options, allArgs []string) { parsePreviewWindow(&opts.Preview, nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")) case "--height": - opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]")) + opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]")) case "--min-height": opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT") case "--no-height": - opts.Height = sizeSpec{} + opts.Height = heightSpec{} case "--no-margin": opts.Margin = defaultMargin() case "--no-padding": @@ -1709,6 +1724,7 @@ func postProcessOptions(opts *Options) { } // Extend the default key map + previewEnabled := len(opts.Preview.command) > 0 || hasPreviewAction(opts) keymap := defaultKeymap() for key, actions := range opts.Keymap { var lastChangePreviewWindow *action @@ -1719,8 +1735,18 @@ func postProcessOptions(opts *Options) { opts.ToggleSort = true case actChangePreviewWindow: lastChangePreviewWindow = act + if !previewEnabled { + // Doesn't matter + continue + } + opts := previewOpts{} + for _, arg := range strings.Split(act.a, "|") { + // Make sure that each expression is valid + parsePreviewWindow(&opts, arg) + } } } + // Re-organize actions so that we only keep the last change-preview-window // and it comes first in the list. // * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20) @@ -1738,6 +1764,19 @@ func postProcessOptions(opts *Options) { } opts.Keymap = keymap + if opts.Height.auto { + for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} { + if s.percent { + errorExit("adaptive height is not compatible with top/bottom percent margin") + } + } + for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} { + if s.percent { + errorExit("adaptive height is not compatible with top/bottom percent padding") + } + } + } + // If we're not using extended search mode, --nth option becomes irrelevant // if it contains the whole range if !opts.Extended || len(opts.Nth) == 1 { diff --git a/src/terminal.go b/src/terminal.go index 981f13a4..493c8b9f 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -100,6 +100,11 @@ type itemLine struct { result Result } +type fitpad struct { + fit int + pad int +} + var emptyLine = itemLine{} // Terminal represents terminal input/output @@ -183,7 +188,7 @@ type Terminal struct { prevLines []itemLine suppress bool sigstop bool - startChan chan bool + startChan chan fitpad killChan chan int slab *util.Slab theme *tui.ColorTheme @@ -439,6 +444,13 @@ func makeSpinner(unicode bool) []string { return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`} } +func evaluateHeight(opts *Options, termHeight int) int { + if opts.Height.percent { + return util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight) + } + return int(opts.Height.size) +} + // NewTerminal returns new Terminal object func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { input := trimQuery(opts.Query) @@ -465,7 +477,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { strongAttr = tui.AttrRegular } var renderer tui.Renderer - fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100 + fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100) if fullscreen { if tui.HasFullscreenRenderer() { renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse) @@ -475,24 +487,16 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { } } else { maxHeightFunc := func(termHeight int) int { - var maxHeight int - if opts.Height.percent { - maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight) - } else { - maxHeight = int(opts.Height.size) - } - + // Minimum height required to render fzf excluding margin and padding effectiveMinHeight := minHeight - if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) { - effectiveMinHeight *= 2 + if previewBox != nil && opts.Preview.aboveOrBelow() { + effectiveMinHeight += 1 + borderLines(opts.Preview.border) } if opts.InfoStyle != infoDefault { effectiveMinHeight-- } - if opts.BorderShape != tui.BorderNone { - effectiveMinHeight += 2 - } - return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight)) + effectiveMinHeight += borderLines(opts.BorderShape) + return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight)) } renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc) } @@ -572,7 +576,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { sigstop: false, slab: util.MakeSlab(slab16Size, slab32Size), theme: opts.Theme, - startChan: make(chan bool, 1), + startChan: make(chan fitpad, 1), killChan: make(chan int), tui: renderer, initFunc: func() { renderer.Init() }, @@ -587,6 +591,32 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { return &t } +func borderLines(shape tui.BorderShape) int { + switch shape { + case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp: + return 2 + case tui.BorderTop, tui.BorderBottom: + return 1 + } + return 0 +} + +// Extra number of lines needed to display fzf +func (t *Terminal) extraLines() int { + extra := len(t.header0) + t.headerLines + 1 + if !t.noInfoLine() { + extra++ + } + return extra +} + +func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) { + _, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding() + padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2] + fit := screenHeight - padHeight - t.extraLines() + return fit, padHeight +} + func (t *Terminal) parsePrompt(prompt string) (func(), int) { var state *ansiState trimmed, colors, _ := extractColor(prompt, state, nil) @@ -725,22 +755,23 @@ func (t *Terminal) displayWidth(runes []rune) int { const ( minWidth = 4 - minHeight = 4 + minHeight = 3 ) func calculateSize(base int, size sizeSpec, occupied int, minSize int, pad int) int { max := base - occupied + if max < minSize { + max = minSize + } if size.percent { return util.Constrain(int(float64(base)*0.01*size.size), minSize, max) } return util.Constrain(int(size.size)+pad, minSize, max) } -func (t *Terminal) resizeWindows() { +func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) { screenWidth := t.tui.MaxX() screenHeight := t.tui.MaxY() - t.prevLines = make([]itemLine, screenHeight) - marginInt := [4]int{} // TRBL paddingInt := [4]int{} // TRBL sizeSpecToInt := func(index int, spec sizeSpec) int { @@ -789,31 +820,48 @@ func (t *Terminal) resizeWindows() { } adjust := func(idx1 int, idx2 int, max int, min int) { - if max >= min { - margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2] - if max-margin < min { - desired := max - min - paddingInt[idx1] = desired * paddingInt[idx1] / margin - paddingInt[idx2] = desired * paddingInt[idx2] / margin - marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin) - marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin) - } + if min > max { + min = max + } + margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2] + if max-margin < min { + desired := max - min + paddingInt[idx1] = desired * paddingInt[idx1] / margin + paddingInt[idx2] = desired * paddingInt[idx2] / margin + marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin) + marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin) } } - previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0 minAreaWidth := minWidth minAreaHeight := minHeight - if previewVisible { + if t.noInfoLine() { + minAreaHeight -= 1 + } + if t.isPreviewVisible() { + minPreviewHeight := 1 + borderLines(t.previewOpts.border) + minPreviewWidth := 5 switch t.previewOpts.position { case posUp, posDown: - minAreaHeight *= 2 + minAreaHeight += minPreviewHeight + minAreaWidth = util.Max(minPreviewWidth, minAreaWidth) case posLeft, posRight: - minAreaWidth *= 2 + minAreaWidth += minPreviewWidth + minAreaHeight = util.Max(minPreviewHeight, minAreaHeight) } } adjust(1, 3, screenWidth, minAreaWidth) adjust(0, 2, screenHeight, minAreaHeight) + + return screenWidth, screenHeight, marginInt, paddingInt +} + +func (t *Terminal) resizeWindows() { + screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding() + width := screenWidth - marginInt[1] - marginInt[3] + height := screenHeight - marginInt[0] - marginInt[2] + + t.prevLines = make([]itemLine, screenHeight) if t.border != nil { t.border.Close() } @@ -832,8 +880,6 @@ func (t *Terminal) resizeWindows() { // Reset preview version so that full redraw occurs t.previewed.version = 0 - width := screenWidth - marginInt[1] - marginInt[3] - height := screenHeight - marginInt[0] - marginInt[2] switch t.borderShape { case tui.BorderHorizontal: t.border = t.tui.NewWindow( @@ -865,16 +911,16 @@ func (t *Terminal) resizeWindows() { false, tui.MakeBorderStyle(t.borderShape, t.unicode)) } - // Add padding + // Add padding to margin for idx, val := range paddingInt { marginInt[idx] += val } - width = screenWidth - marginInt[1] - marginInt[3] - height = screenHeight - marginInt[0] - marginInt[2] + width -= paddingInt[1] + paddingInt[3] + height -= paddingInt[0] + paddingInt[2] // Set up preview window noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode) - if previewVisible { + if t.isPreviewVisible() { var resizePreviewWindows func(previewOpts previewOpts) resizePreviewWindows = func(previewOpts previewOpts) { hasThreshold := previewOpts.threshold > 0 && previewOpts.alternative != nil @@ -1863,6 +1909,10 @@ func (t *Terminal) isPreviewEnabled() bool { return t.hasPreviewer() && t.previewer.enabled } +func (t *Terminal) isPreviewVisible() bool { + return t.isPreviewEnabled() && t.previewOpts.size.size > 0 +} + func (t *Terminal) hasPreviewWindow() bool { return t.pwindow != nil && t.isPreviewEnabled() } @@ -1962,7 +2012,28 @@ func (t *Terminal) cancelPreview() { // Loop is called to start Terminal I/O func (t *Terminal) Loop() { // prof := profile.Start(profile.ProfilePath("/tmp/")) - <-t.startChan + fitpad := <-t.startChan + fit := fitpad.fit + if fit >= 0 { + pad := fitpad.pad + t.tui.Resize(func(termHeight int) int { + contentHeight := fit + t.extraLines() + if t.hasPreviewer() { + if t.previewOpts.aboveOrBelow() { + if t.previewOpts.size.percent { + newContentHeight := int(float64(contentHeight) * 100. / (100. - t.previewOpts.size.size)) + contentHeight = util.Max(contentHeight+1+borderLines(t.previewOpts.border), newContentHeight) + } else { + contentHeight += int(t.previewOpts.size.size) + borderLines(t.previewOpts.border) + } + } else { + // Minimum height if preview window can appear + contentHeight = util.Max(contentHeight, 1+borderLines(t.previewOpts.border)) + } + } + return util.Min(termHeight, contentHeight+pad) + }) + } { // Late initialization intChan := make(chan os.Signal, 1) signal.Notify(intChan, os.Interrupt, syscall.SIGTERM) diff --git a/src/tui/dummy.go b/src/tui/dummy.go index 297a887e..adecd6fc 100644 --- a/src/tui/dummy.go +++ b/src/tui/dummy.go @@ -27,12 +27,13 @@ const ( StrikeThrough = Attr(1 << 7) ) -func (r *FullscreenRenderer) Init() {} -func (r *FullscreenRenderer) Pause(bool) {} -func (r *FullscreenRenderer) Resume(bool, bool) {} -func (r *FullscreenRenderer) Clear() {} -func (r *FullscreenRenderer) Refresh() {} -func (r *FullscreenRenderer) Close() {} +func (r *FullscreenRenderer) Init() {} +func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} +func (r *FullscreenRenderer) Pause(bool) {} +func (r *FullscreenRenderer) Resume(bool, bool) {} +func (r *FullscreenRenderer) Clear() {} +func (r *FullscreenRenderer) Refresh() {} +func (r *FullscreenRenderer) Close() {} func (r *FullscreenRenderer) GetChar() Event { return Event{} } func (r *FullscreenRenderer) MaxX() int { return 0 } diff --git a/src/tui/light.go b/src/tui/light.go index 0546caa8..20b7b9d5 100644 --- a/src/tui/light.go +++ b/src/tui/light.go @@ -189,6 +189,10 @@ func (r *LightRenderer) Init() { } } +func (r *LightRenderer) Resize(maxHeightFunc func(int) int) { + r.maxHeightFunc = maxHeightFunc +} + func (r *LightRenderer) makeSpace() { r.stderr("\n") r.csi("G") @@ -676,6 +680,9 @@ func (r *LightRenderer) MaxX() int { } func (r *LightRenderer) MaxY() int { + if r.height == 0 { + r.updateTerminalSize() + } return r.height } diff --git a/src/tui/tcell.go b/src/tui/tcell.go index 85ef1dd8..1f9a832b 100644 --- a/src/tui/tcell.go +++ b/src/tui/tcell.go @@ -99,6 +99,8 @@ const ( AttrClear = Attr(1 << 8) ) +func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {} + func (r *FullscreenRenderer) defaultTheme() *ColorTheme { if _screen.Colors() >= 256 { return Dark256 diff --git a/src/tui/tui.go b/src/tui/tui.go index eb09da40..c6d71c12 100644 --- a/src/tui/tui.go +++ b/src/tui/tui.go @@ -358,6 +358,7 @@ func MakeTransparentBorder() BorderStyle { type Renderer interface { Init() + Resize(maxHeightFunc func(int) int) Pause(clear bool) Resume(clear bool, sigcont bool) Clear() diff --git a/test/test_go.rb b/test/test_go.rb index 6371e0e2..bb6a4c8d 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2245,6 +2245,93 @@ class TestGoFZF < TestBase tmux.until { |lines| assert_equal 1, lines.match_count } tmux.until { |lines| assert_match(/^> SNIPSNIP.*SNIPSNIP$/, lines[-3]) } end + + def assert_block(expected, lines) + cols = expected.lines.map(&:chomp).map(&:length).max + actual = lines.reverse.take(expected.lines.length).reverse.map { _1[0, cols].rstrip + "\n" }.join + assert_equal expected, actual + end + + def test_height_range_fit + tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border', :Enter + expected = <<~OUTPUT + ╭────────── + │ 3 + │ 2 + │ > 1 + │ > < 3/3 + ╰────────── + OUTPUT + tmux.until { assert_block(expected, _1) } + end + + def test_height_range_fit_preview_above + tmux.send_keys 'seq 3 | fzf --height ~100% --info=inline --border --preview "seq {}" --preview-window up,60%', :Enter + expected = <<~OUTPUT + ╭────────── + │ ╭──────── + │ │ 1 + │ │ + │ │ + │ │ + │ ╰──────── + │ 3 + │ 2 + │ > 1 + │ > < 3/3 + ╰────────── + OUTPUT + tmux.until { assert_block(expected, _1) } + end + + def test_height_range_fit_preview_above_alternative + tmux.send_keys 'seq 3 | fzf --height ~100% --border=sharp --preview "seq {}" --preview-window up,40%,border-bottom --padding 1 --exit-0 --header hello --header-lines=2', :Enter + expected = <<~OUTPUT + ┌───────── + │ + │ 1 + │ 2 + │ 3 + │ ─────── + │ > 3 + │ 2 + │ 1 + │ hello + │ 1/1 + │ > + │ + └───────── + OUTPUT + tmux.until { assert_block(expected, _1) } + end + + def test_height_range_fit_preview_left + tmux.send_keys "seq 3 | fzf --height ~100% --border=vertical --preview 'seq {}' --preview-window left,5,border-right --padding 1 --exit-0 --header $'hello\\nworld' --header-lines=2", :Enter + expected = <<~OUTPUT + │ + │ 1 │> 3 + │ 2 │ 2 + │ 3 │ 1 + │ │ hello + │ │ world + │ │ 1/1 + │ │> + │ + OUTPUT + tmux.until { assert_block(expected, _1) } + end + + def test_height_range_overflow + tmux.send_keys 'seq 100 | fzf --height ~5 --info=inline --border', :Enter + expected = <<~OUTPUT + ╭────────────── + │ 2 + │ > 1 + │ > < 100/100 + ╰────────────── + OUTPUT + tmux.until { assert_block(expected, _1) } + end end module TestShell