Add preview border style 'line'

It draws a single line between the preview window and the rest of the
interface. i.e. automatically choose between 'left', 'right', 'top', and
'bottom' depending on the position of the preview window.
This commit is contained in:
Junegunn Choi 2025-01-06 00:44:59 +09:00
parent a5beb08ed7
commit 0e0b868342
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 70 additions and 27 deletions

View File

@ -43,6 +43,7 @@ CHANGELOG
- `change-header-label` - `change-header-label`
- `transform-header-label` - `transform-header-label`
- Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]` - Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]`
- Added new preview border style `line` which draws a single separator line between the preview window and the rest of the interface
- You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup. - You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup.
```sh ```sh
fzf --tmux border-native --bind 'enter:execute:less {}' fzf --tmux border-native --bind 'enter:execute:less {}'

View File

@ -775,7 +775,10 @@ e.g.
.TP .TP
.BI "\-\-preview\-border" [=STYLE] .BI "\-\-preview\-border" [=STYLE]
Short for \fB\-\-preview\-window=border\-STYLE\fR Short for \fB\-\-preview\-window=border\-STYLE\fR. In addition to the other
styles, \fBline\fR style is also supported for preview border, which draws
a single separator line between the preview window and the rest of the
interface.
.TP .TP
.BI "\-\-preview\-label" [=LABEL] .BI "\-\-preview\-label" [=LABEL]
@ -812,7 +815,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
border line. border line.
.TP .TP
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]" .BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-STYLE][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
.RS .RS
.B POSITION: (default: right) .B POSITION: (default: right)
@ -855,6 +858,10 @@ e.g. \fBborder\-rounded\fR (border with rounded edges, default),
\fBborder\-sharp\fR (border with sharp edges), \fBborder\-left\fR, \fBborder\-sharp\fR (border with sharp edges), \fBborder\-left\fR,
\fBborder\-none\fR, etc. \fBborder\-none\fR, etc.
* In addition to the other border styles, \fBborder\-line\fR style is also
supported, which draws a single separator line between the preview window and
the rest of the interface.
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the * \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
preview window. preview window.

View File

@ -146,10 +146,12 @@ Usage: fzf [options]
--preview-window=OPT Preview window layout (default: right:50%) --preview-window=OPT Preview window layout (default: right:50%)
[up|down|left|right][,SIZE[%]] [up|down|left|right][,SIZE[%]]
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info] [,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
[,[no]hidden][,border-BORDER_OPT] [,[no]hidden][,border-STYLE]
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES] [,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)] [,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
--preview-border[=STYLE] Short for --preview-window=border-STYLE --preview-border[=STYLE] Short for --preview-window=border-STYLE
[rounded|sharp|bold|block|thinblock|double|horizontal|vertical|
top|bottom|left|right|line|none] (default: rounded)
--preview-label=LABEL --preview-label=LABEL
--preview-label-pos=N Same as --border-label and --border-label-pos, --preview-label-pos=N Same as --border-label and --border-label-pos,
but for preview window but for preview window
@ -313,6 +315,10 @@ func (o *previewOpts) Toggle() {
o.hidden = !o.hidden o.hidden = !o.hidden
} }
func (o *previewOpts) HasBorderRight() bool {
return o.border.HasRight() || o.border == tui.BorderLine && o.position == posLeft
}
func defaultTmuxOptions(index int) *tmuxOptions { func defaultTmuxOptions(index int) *tmuxOptions {
return &tmuxOptions{ return &tmuxOptions{
position: posCenter, position: posCenter,
@ -832,8 +838,13 @@ func processScheme(opts *Options) error {
return nil return nil
} }
func parseBorder(str string, optional bool) (tui.BorderShape, error) { func parseBorder(str string, optional bool, allowLine bool) (tui.BorderShape, error) {
switch str { switch str {
case "line":
if !allowLine {
return tui.BorderNone, errors.New("'line' is only allowed for preview border")
}
return tui.BorderLine, nil
case "rounded": case "rounded":
return tui.BorderRounded, nil return tui.BorderRounded, nil
case "sharp": case "sharp":
@ -1900,6 +1911,8 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
opts.position = posRight opts.position = posRight
case "rounded", "border", "border-rounded": case "rounded", "border", "border-rounded":
opts.border = tui.BorderRounded opts.border = tui.BorderRounded
case "border-line":
opts.border = tui.BorderLine
case "sharp", "border-sharp": case "sharp", "border-sharp":
opts.border = tui.BorderSharp opts.border = tui.BorderSharp
case "border-bold": case "border-bold":
@ -2501,7 +2514,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
case "--no-preview": case "--no-preview":
opts.Preview.command = "" opts.Preview.command = ""
case "--preview-window": case "--preview-window":
str, err := nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]") str, err := nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-STYLE][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]")
if err != nil { if err != nil {
return err return err
} }
@ -2512,7 +2525,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.Preview.border = tui.BorderNone opts.Preview.border = tui.BorderNone
case "--preview-border": case "--preview-border":
hasArg, arg := optionalNextString(allArgs, &i) hasArg, arg := optionalNextString(allArgs, &i)
if opts.Preview.border, err = parseBorder(arg, !hasArg); err != nil { if opts.Preview.border, err = parseBorder(arg, !hasArg, true); err != nil {
return err return err
} }
case "--height": case "--height":
@ -2537,12 +2550,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.BorderShape = tui.BorderNone opts.BorderShape = tui.BorderNone
case "--border": case "--border":
hasArg, arg := optionalNextString(allArgs, &i) hasArg, arg := optionalNextString(allArgs, &i)
if opts.BorderShape, err = parseBorder(arg, !hasArg); err != nil { if opts.BorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
return err return err
} }
case "--list-border": case "--list-border":
hasArg, arg := optionalNextString(allArgs, &i) hasArg, arg := optionalNextString(allArgs, &i)
if opts.ListBorderShape, err = parseBorder(arg, !hasArg); err != nil { if opts.ListBorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
return err return err
} }
case "--no-list-border": case "--no-list-border":
@ -2566,7 +2579,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.HeaderBorderShape = tui.BorderNone opts.HeaderBorderShape = tui.BorderNone
case "--header-border": case "--header-border":
hasArg, arg := optionalNextString(allArgs, &i) hasArg, arg := optionalNextString(allArgs, &i)
if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg); err != nil { if opts.HeaderBorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
return err return err
} }
case "--no-header-label": case "--no-header-label":
@ -2587,7 +2600,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
opts.InputBorderShape = tui.BorderNone opts.InputBorderShape = tui.BorderNone
case "--input-border": case "--input-border":
hasArg, arg := optionalNextString(allArgs, &i) hasArg, arg := optionalNextString(allArgs, &i)
if opts.InputBorderShape, err = parseBorder(arg, !hasArg); err != nil { if opts.InputBorderShape, err = parseBorder(arg, !hasArg, false); err != nil {
return err return err
} }
case "--no-input-label": case "--no-input-label":
@ -2738,15 +2751,15 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
} else if match, value := optString(arg, "-d", "--delimiter="); match { } else if match, value := optString(arg, "-d", "--delimiter="); match {
opts.Delimiter = delimiterRegexp(value) opts.Delimiter = delimiterRegexp(value)
} else if match, value := optString(arg, "--border="); match { } else if match, value := optString(arg, "--border="); match {
if opts.BorderShape, err = parseBorder(value, false); err != nil { if opts.BorderShape, err = parseBorder(value, false, false); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--preview-border="); match { } else if match, value := optString(arg, "--preview-border="); match {
if opts.Preview.border, err = parseBorder(value, false); err != nil { if opts.Preview.border, err = parseBorder(value, false, true); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--list-border="); match { } else if match, value := optString(arg, "--list-border="); match {
if opts.ListBorderShape, err = parseBorder(value, false); err != nil { if opts.ListBorderShape, err = parseBorder(value, false, false); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--list-label="); match { } else if match, value := optString(arg, "--list-label="); match {
@ -2756,7 +2769,7 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
return err return err
} }
} else if match, value := optString(arg, "--input-border="); match { } else if match, value := optString(arg, "--input-border="); match {
if opts.InputBorderShape, err = parseBorder(value, false); err != nil { if opts.InputBorderShape, err = parseBorder(value, false, false); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--input-label="); match { } else if match, value := optString(arg, "--input-label="); match {

View File

@ -1513,7 +1513,7 @@ func (t *Terminal) minPreviewSize(opts *previewOpts) (int, int) {
switch opts.position { switch opts.position {
case posLeft, posRight: case posLeft, posRight:
if len(t.scrollbar) > 0 && !opts.border.HasRight() { if len(t.scrollbar) > 0 && !opts.HasBorderRight() {
// Need a column to show scrollbar // Need a column to show scrollbar
minPreviewWidth++ minPreviewWidth++
} }
@ -1757,17 +1757,30 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
createPreviewWindow := func(y int, x int, w int, h int) { createPreviewWindow := func(y int, x int, w int, h int) {
pwidth := w pwidth := w
pheight := h pheight := h
previewBorder := tui.MakeBorderStyle(previewOpts.border, t.unicode) shape := previewOpts.border
if shape == tui.BorderLine {
switch previewOpts.position {
case posUp:
shape = tui.BorderBottom
case posDown:
shape = tui.BorderTop
case posLeft:
shape = tui.BorderRight
case posRight:
shape = tui.BorderLeft
}
}
previewBorder := tui.MakeBorderStyle(shape, t.unicode)
t.pborder = t.tui.NewWindow(y, x, w, h, tui.WindowPreview, previewBorder, false) t.pborder = t.tui.NewWindow(y, x, w, h, tui.WindowPreview, previewBorder, false)
pwidth -= borderColumns(previewOpts.border, bw) pwidth -= borderColumns(shape, bw)
pheight -= borderLines(previewOpts.border) pheight -= borderLines(shape)
if previewOpts.border.HasLeft() { if shape.HasLeft() {
x += 1 + bw x += 1 + bw
} }
if previewOpts.border.HasTop() { if shape.HasTop() {
y += 1 y += 1
} }
if len(t.scrollbar) > 0 && !previewOpts.border.HasRight() { if len(t.scrollbar) > 0 && !shape.HasRight() {
// Need a column to show scrollbar // Need a column to show scrollbar
pwidth -= 1 pwidth -= 1
} }
@ -1800,6 +1813,14 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
if previewOpts.hidden { if previewOpts.hidden {
return return
} }
// If none of the inner borders has the right side, but the outer border does, increase the width by 1 column
stickToRight = t.borderShape.HasRight() &&
!previewOpts.HasBorderRight() && !t.listBorderShape.HasRight() && !t.inputBorderShape.HasRight() &&
(!t.headerVisible || !t.headerBorderShape.HasRight() || t.visibleHeaderLines() == 0)
if stickToRight {
innerWidth++
width++
}
maxPreviewLines := availableLines maxPreviewLines := availableLines
if t.wborder != nil { if t.wborder != nil {
@ -1870,7 +1891,7 @@ func (t *Terminal) resizeWindows(forcePreview bool, redrawBorder bool) {
createPreviewWindow(marginInt[0], marginInt[3], pwidth, height) createPreviewWindow(marginInt[0], marginInt[3], pwidth, height)
} else { } else {
// NOTE: fzf --preview 'cat {}' --preview-window border-left --border // NOTE: fzf --preview 'cat {}' --preview-window border-left --border
stickToRight = !previewOpts.border.HasRight() && t.borderShape.HasRight() stickToRight = !previewOpts.HasBorderRight() && t.borderShape.HasRight()
if stickToRight { if stickToRight {
innerWidth++ innerWidth++
width++ width++
@ -3187,7 +3208,7 @@ func (t *Terminal) renderPreviewScrollbar(yoff int, barLength int, barStart int)
t.previewer.xw = xw t.previewer.xw = xw
} }
xshift := -1 - t.borderWidth xshift := -1 - t.borderWidth
if !t.activePreviewOpts.border.HasRight() { if !t.activePreviewOpts.HasBorderRight() {
xshift = -1 xshift = -1
} }
yshift := 1 yshift := 1

View File

@ -358,6 +358,7 @@ type BorderShape int
const ( const (
BorderUndefined BorderShape = iota BorderUndefined BorderShape = iota
BorderLine
BorderNone BorderNone
BorderRounded BorderRounded
BorderSharp BorderSharp
@ -375,7 +376,7 @@ const (
func (s BorderShape) HasLeft() bool { func (s BorderShape) HasLeft() bool {
switch s { switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left case BorderNone, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false return false
} }
return true return true
@ -383,7 +384,7 @@ func (s BorderShape) HasLeft() bool {
func (s BorderShape) HasRight() bool { func (s BorderShape) HasRight() bool {
switch s { switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right case BorderNone, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
return false return false
} }
return true return true
@ -391,7 +392,7 @@ func (s BorderShape) HasRight() bool {
func (s BorderShape) HasTop() bool { func (s BorderShape) HasTop() bool {
switch s { switch s {
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top case BorderNone, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
return false return false
} }
return true return true
@ -399,7 +400,7 @@ func (s BorderShape) HasTop() bool {
func (s BorderShape) HasBottom() bool { func (s BorderShape) HasBottom() bool {
switch s { switch s {
case BorderNone, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom case BorderNone, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
return false return false
} }
return true return true