Projects STRLCPY fzf Commits fe2f963b
🤬
  • ■ ■ ■ ■ ■ ■
    CHANGELOG.md
    1 1  CHANGELOG
    2 2  =========
    3 3   
     4 +0.34.0
     5 +------
     6 +- Added support for `--height` range. If the `--height` value is prefixed with
     7 + `~`, fzf will automatically determine the height in the range according to
     8 + the input size.
     9 + ```sh
     10 + seq 1 | fzf --height ~70% --border --padding 1 --margin 1
     11 + seq 10 | fzf --height ~70% --border --padding 1 --margin 1
     12 + seq 100 | fzf --height ~70% --border --padding 1 --margin 1
     13 + ```
     14 + - This has a few limitations
     15 + - Not compatible with `--preview-window up|down`
     16 + - Not compatible with percent top/bottom margin/padding
     17 + ```sh
     18 + # This won't work (percent top/bottom margin)
     19 + fzf --height ~50% --border --margin 5%,10%
     20 + 
     21 + # This works (fixed top/bottom margin)
     22 + fzf --height ~50% --border --margin 5,10%
     23 + ```
     24 + - fzf will block until it can determine the right height for the input
     25 + ```sh
     26 + # fzf will open after 2 seconds
     27 + (sleep 2; seq 10) | fzf --height ~100%
     28 + ```
     29 + 
    4 30  0.33.0
    5 31  ------
    6 32  - Added `--scheme=[default|path|history]` option to choose scoring scheme
    skipped 1309 lines
  • ■ ■ ■ ■ ■
    man/man1/fzf.1
    skipped 176 lines
    177 177  Label characters for \fBjump\fR and \fBjump-accept\fR
    178 178  .SS Layout
    179 179  .TP
    180  -.BI "--height=" "HEIGHT[%]"
     180 +.BI "--height=" "[~]HEIGHT[%]"
    181 181  Display fzf window below the cursor with the given height instead of using
    182  -the full screen.
     182 +the full screen. When prefixed with \fB~\fB, fzf will automatically determine
     183 +the height in the range according to the input size.
    183 184  .TP
    184 185  .BI "--min-height=" "HEIGHT"
    185 186  Minimum height when \fB--height\fR is given in percent (default: 10).
    skipped 862 lines
  • ■ ■ ■ ■ ■
    src/core.go
    skipped 193 lines
    194 194   
    195 195   // Terminal I/O
    196 196   terminal := NewTerminal(opts, eventBox)
    197  - deferred := opts.Select1 || opts.Exit0
     197 + maxFit := 0 // Maximum number of items that can fit on screen
     198 + padHeight := 0
     199 + autoHeight := opts.Height.auto
     200 + if autoHeight {
     201 + maxFit, padHeight = terminal.MaxFitAndPad(opts)
     202 + }
     203 + deferred := opts.Select1 || opts.Exit0 || autoHeight
    198 204   go terminal.Loop()
    199 205   if !deferred {
    200  - terminal.startChan <- true
     206 + terminal.startChan <- fitpad{-1, -1}
    201 207   }
    202 208   
    203 209   // Event coordination
    skipped 90 lines
    294 300   case *Merger:
    295 301   if deferred {
    296 302   count := val.Length()
     303 + determine := func() {
     304 + if autoHeight {
     305 + if count >= maxFit || val.final {
     306 + terminal.startChan <- fitpad{util.Min(count, maxFit), padHeight}
     307 + deferred = false
     308 + }
     309 + } else {
     310 + terminal.startChan <- fitpad{-1, -1}
     311 + deferred = false
     312 + }
     313 + }
    297 314   if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
    298  - deferred = false
    299  - terminal.startChan <- true
     315 + determine()
    300 316   } else if val.final {
    301 317   if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
    302 318   if opts.PrintQuery {
    skipped 10 lines
    313 329   }
    314 330   os.Exit(exitNoMatch)
    315 331   }
    316  - deferred = false
    317  - terminal.startChan <- true
     332 + determine()
     333 + } else if autoHeight {
     334 + determine()
    318 335   }
    319 336   }
    320 337   terminal.UpdateList(val, clearSelection())
    skipped 14 lines
  • ■ ■ ■ ■ ■
    src/options.go
    skipped 52 lines
    53 53   --jump-labels=CHARS Label characters for jump and jump-accept
    54 54   
    55 55   Layout
    56  - --height=HEIGHT[%] Display fzf window below the cursor with the given
    57  - height instead of using fullscreen
     56 + --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
     57 + height instead of using fullscreen.
     58 + If prefixed with '~', fzf will determine the height
     59 + according to the input size.
    58 60   --min-height=HEIGHT Minimum height when --height is given in percent
    59 61   (default: 10)
    60 62   --layout=LAYOUT Choose layout: [default|reverse|reverse-list]
    skipped 70 lines
    131 133   byEnd
    132 134  )
    133 135   
     136 +type heightSpec struct {
     137 + size float64
     138 + percent bool
     139 + auto bool
     140 +}
     141 + 
    134 142  type sizeSpec struct {
    135 143   size float64
    136 144   percent bool
    skipped 43 lines
    180 188   alternative *previewOpts
    181 189  }
    182 190   
     191 +func (a previewOpts) aboveOrBelow() bool {
     192 + return a.size.size > 0 && (a.position == posUp || a.position == posDown)
     193 +}
     194 + 
    183 195  func (a previewOpts) sameLayout(b previewOpts) bool {
    184 196   return a.size == b.size && a.position == b.position && a.border == b.border && a.hidden == b.hidden && a.threshold == b.threshold &&
    185 197   (a.alternative != nil && b.alternative != nil && a.alternative.sameLayout(*b.alternative) ||
    skipped 25 lines
    211 223   Theme *tui.ColorTheme
    212 224   Black bool
    213 225   Bold bool
    214  - Height sizeSpec
     226 + Height heightSpec
    215 227   MinHeight int
    216 228   Layout layoutType
    217 229   Cycle bool
    skipped 858 lines
    1076 1088   }
    1077 1089   if t == actUnbind || t == actRebind {
    1078 1090   parseKeyChords(actionArg, spec[0:offset]+" target required")
    1079  - } else if t == actChangePreviewWindow {
    1080  - opts := previewOpts{}
    1081  - for _, arg := range strings.Split(actionArg, "|") {
    1082  - parsePreviewWindow(&opts, arg)
    1083  - }
    1084 1091   }
    1085 1092   }
    1086 1093   }
    skipped 73 lines
    1160 1167   return sizeSpec{val, percent}
    1161 1168  }
    1162 1169   
    1163  -func parseHeight(str string) sizeSpec {
     1170 +func parseHeight(str string) heightSpec {
     1171 + heightSpec := heightSpec{}
     1172 + if strings.HasPrefix(str, "~") {
     1173 + heightSpec.auto = true
     1174 + str = str[1:]
     1175 + }
     1176 + 
    1164 1177   size := parseSize(str, 100, "height")
    1165  - return size
     1178 + heightSpec.size = size.size
     1179 + heightSpec.percent = size.percent
     1180 + return heightSpec
    1166 1181  }
    1167 1182   
    1168 1183  func parseLayout(str string) layoutType {
    skipped 356 lines
    1525 1540   parsePreviewWindow(&opts.Preview,
    1526 1541   nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
    1527 1542   case "--height":
    1528  - opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
     1543 + opts.Height = parseHeight(nextString(allArgs, &i, "height required: [~]HEIGHT[%]"))
    1529 1544   case "--min-height":
    1530 1545   opts.MinHeight = nextInt(allArgs, &i, "height required: HEIGHT")
    1531 1546   case "--no-height":
    1532  - opts.Height = sizeSpec{}
     1547 + opts.Height = heightSpec{}
    1533 1548   case "--no-margin":
    1534 1549   opts.Margin = defaultMargin()
    1535 1550   case "--no-padding":
    skipped 173 lines
    1709 1724   }
    1710 1725   
    1711 1726   // Extend the default key map
     1727 + initialPreviewEnabled := len(opts.Preview.command) > 0
     1728 + previewEnabled := initialPreviewEnabled || hasPreviewAction(opts)
    1712 1729   keymap := defaultKeymap()
    1713 1730   for key, actions := range opts.Keymap {
    1714 1731   var lastChangePreviewWindow *action
    skipped 4 lines
    1719 1736   opts.ToggleSort = true
    1720 1737   case actChangePreviewWindow:
    1721 1738   lastChangePreviewWindow = act
     1739 + if !previewEnabled {
     1740 + // Doesn't matter
     1741 + continue
     1742 + }
     1743 + opts := previewOpts{}
     1744 + for _, arg := range strings.Split(act.a, "|") {
     1745 + // Make sure that each expression is valid
     1746 + parsePreviewWindow(&opts, arg)
     1747 + }
    1722 1748   }
    1723 1749   }
     1750 + 
    1724 1751   // Re-organize actions so that we only keep the last change-preview-window
    1725 1752   // and it comes first in the list.
    1726 1753   // * change-preview-window(up,+10)+preview(sleep 3; cat {})+change-preview-window(up,+20)
    skipped 10 lines
    1737 1764   keymap[key] = actions
    1738 1765   }
    1739 1766   opts.Keymap = keymap
     1767 + 
     1768 + if opts.Height.auto {
     1769 + if initialPreviewEnabled {
     1770 + if opts.Preview.aboveOrBelow() {
     1771 + errorExit("height range is not compatible with preview-window=up|down")
     1772 + }
     1773 + // Disable alternative up/down layout
     1774 + if opts.Preview.alternative != nil && opts.Preview.alternative.aboveOrBelow() {
     1775 + opts.Preview.alternative = nil
     1776 + }
     1777 + }
     1778 + for _, s := range []sizeSpec{opts.Margin[0], opts.Margin[2]} {
     1779 + if s.percent {
     1780 + errorExit("height range is not compatible with top/bottom percent margin")
     1781 + }
     1782 + }
     1783 + for _, s := range []sizeSpec{opts.Padding[0], opts.Padding[2]} {
     1784 + if s.percent {
     1785 + errorExit("height range is not compatible with top/bottom percent padding")
     1786 + }
     1787 + }
     1788 + }
    1740 1789   
    1741 1790   // If we're not using extended search mode, --nth option becomes irrelevant
    1742 1791   // if it contains the whole range
    skipped 65 lines
  • ■ ■ ■ ■ ■ ■
    src/terminal.go
    skipped 99 lines
    100 100   result Result
    101 101  }
    102 102   
     103 +type fitpad struct {
     104 + fit int
     105 + pad int
     106 +}
     107 + 
    103 108  var emptyLine = itemLine{}
    104 109   
    105 110  // Terminal represents terminal input/output
    skipped 77 lines
    183 188   prevLines []itemLine
    184 189   suppress bool
    185 190   sigstop bool
    186  - startChan chan bool
     191 + startChan chan fitpad
    187 192   killChan chan int
    188 193   slab *util.Slab
    189 194   theme *tui.ColorTheme
    skipped 249 lines
    439 444   return []string{`-`, `\`, `|`, `/`, `-`, `\`, `|`, `/`}
    440 445  }
    441 446   
     447 +func evaluateHeight(opts *Options, termHeight int) int {
     448 + if opts.Height.percent {
     449 + return util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
     450 + }
     451 + return int(opts.Height.size)
     452 +}
     453 + 
    442 454  // NewTerminal returns new Terminal object
    443 455  func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
    444 456   input := trimQuery(opts.Query)
    skipped 20 lines
    465 477   strongAttr = tui.AttrRegular
    466 478   }
    467 479   var renderer tui.Renderer
    468  - fullscreen := opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100
     480 + fullscreen := !opts.Height.auto && (opts.Height.size == 0 || opts.Height.percent && opts.Height.size == 100)
    469 481   if fullscreen {
    470 482   if tui.HasFullscreenRenderer() {
    471 483   renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
    skipped 3 lines
    475 487   }
    476 488   } else {
    477 489   maxHeightFunc := func(termHeight int) int {
    478  - var maxHeight int
    479  - if opts.Height.percent {
    480  - maxHeight = util.Max(int(opts.Height.size*float64(termHeight)/100.0), opts.MinHeight)
    481  - } else {
    482  - maxHeight = int(opts.Height.size)
    483  - }
    484  - 
     490 + // Minimum height required to render fzf excluding margin and padding
    485 491   effectiveMinHeight := minHeight
    486  - if previewBox != nil && (opts.Preview.position == posUp || opts.Preview.position == posDown) {
    487  - effectiveMinHeight *= 2
     492 + if previewBox != nil && opts.Preview.aboveOrBelow() {
     493 + effectiveMinHeight += 1 + borderLines(opts.Preview.border)
    488 494   }
    489 495   if opts.InfoStyle != infoDefault {
    490 496   effectiveMinHeight--
    491 497   }
    492  - if opts.BorderShape != tui.BorderNone {
    493  - effectiveMinHeight += 2
    494  - }
    495  - return util.Min(termHeight, util.Max(maxHeight, effectiveMinHeight))
     498 + effectiveMinHeight += borderLines(opts.BorderShape)
     499 + return util.Min(termHeight, util.Max(evaluateHeight(opts, termHeight), effectiveMinHeight))
    496 500   }
    497 501   renderer = tui.NewLightRenderer(opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit, false, maxHeightFunc)
    498 502   }
    skipped 73 lines
    572 576   sigstop: false,
    573 577   slab: util.MakeSlab(slab16Size, slab32Size),
    574 578   theme: opts.Theme,
    575  - startChan: make(chan bool, 1),
     579 + startChan: make(chan fitpad, 1),
    576 580   killChan: make(chan int),
    577 581   tui: renderer,
    578 582   initFunc: func() { renderer.Init() },
    skipped 8 lines
    587 591   return &t
    588 592  }
    589 593   
     594 +func borderLines(shape tui.BorderShape) int {
     595 + switch shape {
     596 + case tui.BorderHorizontal, tui.BorderRounded, tui.BorderSharp:
     597 + return 2
     598 + case tui.BorderTop, tui.BorderBottom:
     599 + return 1
     600 + }
     601 + return 0
     602 +}
     603 + 
     604 +// Extra number of lines needed to display fzf
     605 +func (t *Terminal) extraLines() int {
     606 + extra := len(t.header0) + t.headerLines + 1
     607 + if !t.noInfoLine() {
     608 + extra++
     609 + }
     610 + return extra
     611 +}
     612 + 
     613 +func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) {
     614 + _, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
     615 + padHeight := marginInt[0] + marginInt[2] + paddingInt[0] + paddingInt[2]
     616 + fit := screenHeight - padHeight - t.extraLines()
     617 + return fit, padHeight
     618 +}
     619 + 
    590 620  func (t *Terminal) parsePrompt(prompt string) (func(), int) {
    591 621   var state *ansiState
    592 622   trimmed, colors, _ := extractColor(prompt, state, nil)
    skipped 132 lines
    725 755   
    726 756  const (
    727 757   minWidth = 4
    728  - minHeight = 4
     758 + minHeight = 3
    729 759  )
    730 760   
    731 761  func calculateSize(base int, size sizeSpec, occupied int, minSize int, pad int) int {
    skipped 4 lines
    736 766   return util.Constrain(int(size.size)+pad, minSize, max)
    737 767  }
    738 768   
    739  -func (t *Terminal) resizeWindows() {
     769 +func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
    740 770   screenWidth := t.tui.MaxX()
    741 771   screenHeight := t.tui.MaxY()
    742  - t.prevLines = make([]itemLine, screenHeight)
    743  - 
    744 772   marginInt := [4]int{} // TRBL
    745 773   paddingInt := [4]int{} // TRBL
    746 774   sizeSpecToInt := func(index int, spec sizeSpec) int {
    skipped 42 lines
    789 817   }
    790 818   
    791 819   adjust := func(idx1 int, idx2 int, max int, min int) {
    792  - if max >= min {
    793  - margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2]
    794  - if max-margin < min {
    795  - desired := max - min
    796  - paddingInt[idx1] = desired * paddingInt[idx1] / margin
    797  - paddingInt[idx2] = desired * paddingInt[idx2] / margin
    798  - marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin)
    799  - marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin)
    800  - }
     820 + if min > max {
     821 + min = max
     822 + }
     823 + margin := marginInt[idx1] + marginInt[idx2] + paddingInt[idx1] + paddingInt[idx2]
     824 + if max-margin < min {
     825 + desired := max - min
     826 + paddingInt[idx1] = desired * paddingInt[idx1] / margin
     827 + paddingInt[idx2] = desired * paddingInt[idx2] / margin
     828 + marginInt[idx1] = util.Max(extraMargin[idx1], desired*marginInt[idx1]/margin)
     829 + marginInt[idx2] = util.Max(extraMargin[idx2], desired*marginInt[idx2]/margin)
    801 830   }
    802 831   }
    803 832   
    804 833   previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0
    805 834   minAreaWidth := minWidth
    806 835   minAreaHeight := minHeight
     836 + if t.noInfoLine() {
     837 + minAreaHeight -= 1
     838 + }
    807 839   if previewVisible {
     840 + minPreviewHeight := 1 + borderLines(t.previewOpts.border)
     841 + minPreviewWidth := 5
    808 842   switch t.previewOpts.position {
    809 843   case posUp, posDown:
    810  - minAreaHeight *= 2
     844 + minAreaHeight += minPreviewHeight
     845 + minAreaWidth = util.Max(minPreviewWidth, minAreaWidth)
    811 846   case posLeft, posRight:
    812  - minAreaWidth *= 2
     847 + minAreaWidth += minPreviewWidth
     848 + minAreaHeight = util.Max(minPreviewHeight, minAreaHeight)
    813 849   }
    814 850   }
    815 851   adjust(1, 3, screenWidth, minAreaWidth)
    816 852   adjust(0, 2, screenHeight, minAreaHeight)
     853 + 
     854 + return screenWidth, screenHeight, marginInt, paddingInt
     855 +}
     856 + 
     857 +func (t *Terminal) resizeWindows() {
     858 + screenWidth, screenHeight, marginInt, paddingInt := t.adjustMarginAndPadding()
     859 + width := screenWidth - marginInt[1] - marginInt[3]
     860 + height := screenHeight - marginInt[0] - marginInt[2]
     861 + 
     862 + t.prevLines = make([]itemLine, screenHeight)
    817 863   if t.border != nil {
    818 864   t.border.Close()
    819 865   }
    skipped 12 lines
    832 878   // Reset preview version so that full redraw occurs
    833 879   t.previewed.version = 0
    834 880   
    835  - width := screenWidth - marginInt[1] - marginInt[3]
    836  - height := screenHeight - marginInt[0] - marginInt[2]
    837 881   switch t.borderShape {
    838 882   case tui.BorderHorizontal:
    839 883   t.border = t.tui.NewWindow(
    skipped 25 lines
    865 909   false, tui.MakeBorderStyle(t.borderShape, t.unicode))
    866 910   }
    867 911   
    868  - // Add padding
     912 + // Add padding to margin
    869 913   for idx, val := range paddingInt {
    870 914   marginInt[idx] += val
    871 915   }
    872  - width = screenWidth - marginInt[1] - marginInt[3]
    873  - height = screenHeight - marginInt[0] - marginInt[2]
     916 + width -= paddingInt[1] + paddingInt[3]
     917 + height -= paddingInt[0] + paddingInt[2]
    874 918   
    875 919   // Set up preview window
     920 + previewVisible := t.isPreviewEnabled() && t.previewOpts.size.size > 0
    876 921   noBorder := tui.MakeBorderStyle(tui.BorderNone, t.unicode)
    877 922   if previewVisible {
    878 923   var resizePreviewWindows func(previewOpts previewOpts)
    skipped 1083 lines
    1962 2007  // Loop is called to start Terminal I/O
    1963 2008  func (t *Terminal) Loop() {
    1964 2009   // prof := profile.Start(profile.ProfilePath("/tmp/"))
    1965  - <-t.startChan
     2010 + fitpad := <-t.startChan
     2011 + fit := fitpad.fit
     2012 + if fit >= 0 {
     2013 + pad := fitpad.pad
     2014 + t.tui.Resize(func(termHeight int) int {
     2015 + height := fit + t.extraLines() + pad
     2016 + if t.hasPreviewer() {
     2017 + height = util.Max(height, 1+borderLines(t.previewOpts.border)+pad)
     2018 + }
     2019 + return util.Min(termHeight, height)
     2020 + })
     2021 + }
    1966 2022   { // Late initialization
    1967 2023   intChan := make(chan os.Signal, 1)
    1968 2024   signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
    skipped 970 lines
  • ■ ■ ■ ■ ■ ■
    src/tui/dummy.go
    skipped 26 lines
    27 27   StrikeThrough = Attr(1 << 7)
    28 28  )
    29 29   
    30  -func (r *FullscreenRenderer) Init() {}
    31  -func (r *FullscreenRenderer) Pause(bool) {}
    32  -func (r *FullscreenRenderer) Resume(bool, bool) {}
    33  -func (r *FullscreenRenderer) Clear() {}
    34  -func (r *FullscreenRenderer) Refresh() {}
    35  -func (r *FullscreenRenderer) Close() {}
     30 +func (r *FullscreenRenderer) Init() {}
     31 +func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
     32 +func (r *FullscreenRenderer) Pause(bool) {}
     33 +func (r *FullscreenRenderer) Resume(bool, bool) {}
     34 +func (r *FullscreenRenderer) Clear() {}
     35 +func (r *FullscreenRenderer) Refresh() {}
     36 +func (r *FullscreenRenderer) Close() {}
    36 37   
    37 38  func (r *FullscreenRenderer) GetChar() Event { return Event{} }
    38 39  func (r *FullscreenRenderer) MaxX() int { return 0 }
    skipped 8 lines
  • ■ ■ ■ ■ ■ ■
    src/tui/light.go
    skipped 188 lines
    189 189   }
    190 190  }
    191 191   
     192 +func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
     193 + r.maxHeightFunc = maxHeightFunc
     194 +}
     195 + 
    192 196  func (r *LightRenderer) makeSpace() {
    193 197   r.stderr("\n")
    194 198   r.csi("G")
    skipped 481 lines
    676 680  }
    677 681   
    678 682  func (r *LightRenderer) MaxY() int {
     683 + if r.height == 0 {
     684 + r.updateTerminalSize()
     685 + }
    679 686   return r.height
    680 687  }
    681 688   
    skipped 339 lines
  • ■ ■ ■ ■ ■
    src/tui/tui.go
    skipped 357 lines
    358 358   
    359 359  type Renderer interface {
    360 360   Init()
     361 + Resize(maxHeightFunc func(int) int)
    361 362   Pause(clear bool)
    362 363   Resume(clear bool, sigcont bool)
    363 364   Clear()
    skipped 263 lines
Please wait...
Page is in error, reload to recover