summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2022-09-08 01:01:22 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2022-09-28 23:22:31 +0900
commit22cbd9fa58512ffdcc975bab37a55467d5e10968 (patch)
treebddf8d579d8fe88abc4a6014f1a0e936d48be7c7
parent984049586a5e5409131e4cb4600115f4a6b9c669 (diff)
Implement height range (--height ~[VALUE][%])
Close #2953
-rw-r--r--CHANGELOG.md30
-rw-r--r--Makefile1
-rw-r--r--man/man1/fzf.16
-rw-r--r--src/core.go35
-rw-r--r--src/options.go63
-rw-r--r--src/terminal.go151
-rw-r--r--src/tui/dummy.go13
-rw-r--r--src/tui/light.go7
-rw-r--r--src/tui/tcell.go2
-rw-r--r--src/tui/tui.go1
-rwxr-xr-xtest/test_go.rb87
11 files changed, 329 insertions, 67 deletions
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