Add --separator to customize the info separator

pull/3046/head
Junegunn Choi 2 years ago
parent 2eec9892be
commit 8868d7d188
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627

@ -29,11 +29,17 @@ CHANGELOG
```sh ```sh
fzf --preview 'cat {}' --border --preview-label=' Preview ' --preview-label-pos=2 fzf --preview 'cat {}' --border --preview-label=' Preview ' --preview-label-pos=2
``` ```
- Info panel (counter) will be followed by a horizontal separator by default - Info panel (match counter) will be followed by a horizontal separator by
default
- Use `--no-separator` or `--separator=''` to hide the separator
- You can specify an arbitrary string that is repeated to form the
horizontal separator. e.g. `--separator=╸`
- The color of the separator can be customized via `--color=separator:...` - The color of the separator can be customized via `--color=separator:...`
- Separator can be disabled by adding `:nosep` to `--info` - ANSI color codes are also supported
- `--info=nosep` ```sh
- `--info=inline:nosep` fzf --separator=╸ --color=separator:green
fzf --separator=$(lolcat -f -F 1.4 <<< ▁▁▂▃▄▅▆▆▅▄▃▂▁▁) --info=inline
```
- Added `--border=bold` and `--border=double` along with - Added `--border=bold` and `--border=double` along with
`--preview-window=border-bold` and `--preview-window=border-double` `--preview-window=border-bold` and `--preview-window=border-double`

@ -343,6 +343,18 @@ Determines the display style of finder info (match counters).
.B "--no-info" .B "--no-info"
A synonym for \fB--info=hidden\fB A synonym for \fB--info=hidden\fB
.TP
.BI "--separator=" "STR"
The given string will be repeated to form the horizontal separator on the info
line (default: '─' or '-' depending on \fB--no-unicode\fR).
ANSI color codes are supported.
.TP
.B "--no-separator"
Do not display horizontal separator on the info line. A synonym for
\fB--separator=''\fB
.TP .TP
.BI "--prompt=" "STR" .BI "--prompt=" "STR"
Input prompt (default: '> ') Input prompt (default: '> ')

@ -70,7 +70,9 @@ const usage = `usage: fzf [options]
(default: 0 or center) (default: 0 or center)
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L) --margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L) --padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
--info=STYLE Finder info style [default|inline|hidden[:nosep]] --info=STYLE Finder info style [default|inline|hidden]
--separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator
--prompt=STR Input prompt (default: '> ') --prompt=STR Input prompt (default: '> ')
--pointer=STR Pointer to the current line (default: '>') --pointer=STR Pointer to the current line (default: '>')
--marker=STR Multi-select marker (default: '>') --marker=STR Multi-select marker (default: '>')
@ -173,14 +175,10 @@ const (
layoutReverseList layoutReverseList
) )
type infoLayout int type infoStyle int
type infoStyle struct {
layout infoLayout
separator bool
}
const ( const (
infoDefault infoLayout = iota infoDefault infoStyle = iota
infoInline infoInline
infoHidden infoHidden
) )
@ -268,6 +266,7 @@ type Options struct {
ScrollOff int ScrollOff int
FileWord bool FileWord bool
InfoStyle infoStyle InfoStyle infoStyle
Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
Pointer string Pointer string
@ -334,7 +333,8 @@ func defaultOptions() *Options {
HscrollOff: 10, HscrollOff: 10,
ScrollOff: 0, ScrollOff: 0,
FileWord: false, FileWord: false,
InfoStyle: infoStyle{layout: infoDefault, separator: true}, InfoStyle: infoDefault,
Separator: nil,
JumpLabels: defaultJumpLabels, JumpLabels: defaultJumpLabels,
Prompt: "> ", Prompt: "> ",
Pointer: ">", Pointer: ">",
@ -1248,26 +1248,17 @@ func parseLayout(str string) layoutType {
} }
func parseInfoStyle(str string) infoStyle { func parseInfoStyle(str string) infoStyle {
layout := infoDefault switch str {
separator := true case "default":
return infoDefault
for _, token := range splitRegexp.Split(strings.ToLower(str), -1) { case "inline":
switch token { return infoInline
case "default": case "hidden":
layout = infoDefault return infoHidden
case "inline": default:
layout = infoInline errorExit("invalid info style (expected: default|inline|hidden)")
case "hidden":
layout = infoHidden
case "nosep":
separator = false
case "sep":
separator = true
default:
errorExit("invalid info style (expected: default|inline|hidden[:nosep])")
}
} }
return infoStyle{layout: layout, separator: separator} return infoDefault
} }
func parsePreviewWindow(opts *previewOpts, input string) { func parsePreviewWindow(opts *previewOpts, input string) {
@ -1533,11 +1524,17 @@ func parseOptions(opts *Options, allArgs []string) {
opts.InfoStyle = parseInfoStyle( opts.InfoStyle = parseInfoStyle(
nextString(allArgs, &i, "info style required")) nextString(allArgs, &i, "info style required"))
case "--no-info": case "--no-info":
opts.InfoStyle.layout = infoHidden opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
opts.InfoStyle.layout = infoInline opts.InfoStyle = infoInline
case "--no-inline-info": case "--no-inline-info":
opts.InfoStyle.layout = infoDefault opts.InfoStyle = infoDefault
case "--separator":
separator := nextString(allArgs, &i, "separator character required")
opts.Separator = &separator
case "--no-separator":
nosep := ""
opts.Separator = &nosep
case "--jump-labels": case "--jump-labels":
opts.JumpLabels = nextString(allArgs, &i, "label characters required") opts.JumpLabels = nextString(allArgs, &i, "label characters required")
validateJumpLabels = true validateJumpLabels = true
@ -1701,6 +1698,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Layout = parseLayout(value) opts.Layout = parseLayout(value)
} else if match, value := optString(arg, "--info="); match { } else if match, value := optString(arg, "--info="); match {
opts.InfoStyle = parseInfoStyle(value) opts.InfoStyle = parseInfoStyle(value)
} else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value
} else if match, value := optString(arg, "--toggle-sort="); match { } else if match, value := optString(arg, "--toggle-sort="); match {
parseToggleSort(opts.Keymap, value) parseToggleSort(opts.Keymap, value)
} else if match, value := optString(arg, "--expect="); match { } else if match, value := optString(arg, "--expect="); match {

@ -107,17 +107,21 @@ type fitpad struct {
var emptyLine = itemLine{} var emptyLine = itemLine{}
type labelPrinter func(tui.Window, int)
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoStyle infoStyle infoStyle infoStyle
separator labelPrinter
separatorLen int
spinner []string spinner []string
prompt func() prompt func()
promptLen int promptLen int
borderLabel func(tui.Window) borderLabel labelPrinter
borderLabelLen int borderLabelLen int
borderLabelOpts labelOpts borderLabelOpts labelOpts
previewLabel func(tui.Window) previewLabel labelPrinter
previewLabelLen int previewLabelLen int
previewLabelOpts labelOpts previewLabelOpts labelOpts
pointer string pointer string
@ -498,7 +502,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
if previewBox != nil && opts.Preview.aboveOrBelow() { if previewBox != nil && opts.Preview.aboveOrBelow() {
effectiveMinHeight += 1 + borderLines(opts.Preview.border) effectiveMinHeight += 1 + borderLines(opts.Preview.border)
} }
if opts.InfoStyle.layout != infoDefault { if opts.InfoStyle != infoDefault {
effectiveMinHeight-- effectiveMinHeight--
} }
effectiveMinHeight += borderLines(opts.BorderShape) effectiveMinHeight += borderLines(opts.BorderShape)
@ -520,6 +524,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
separator: nil,
spinner: makeSpinner(opts.Unicode), spinner: makeSpinner(opts.Unicode),
queryLen: [2]int{0, 0}, queryLen: [2]int{0, 0},
layout: opts.Layout, layout: opts.Layout,
@ -597,8 +602,17 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
// Pre-calculated empty pointer and marker signs // Pre-calculated empty pointer and marker signs
t.pointerEmpty = strings.Repeat(" ", t.pointerLen) t.pointerEmpty = strings.Repeat(" ", t.pointerLen)
t.markerEmpty = strings.Repeat(" ", t.markerLen) t.markerEmpty = strings.Repeat(" ", t.markerLen)
t.borderLabel, t.borderLabelLen = t.parseBorderLabel(opts.BorderLabel.label) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(opts.BorderLabel.label, &tui.ColBorderLabel, false)
t.previewLabel, t.previewLabelLen = t.parseBorderLabel(opts.PreviewLabel.label) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(opts.PreviewLabel.label, &tui.ColBorderLabel, false)
if opts.Separator == nil || len(*opts.Separator) > 0 {
bar := "─"
if opts.Separator != nil {
bar = *opts.Separator
} else if !t.unicode {
bar = "-"
}
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
}
return &t return &t
} }
@ -629,26 +643,63 @@ func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) {
return fit, padHeight return fit, padHeight
} }
func (t *Terminal) parseBorderLabel(borderLabel string) (func(tui.Window), int) { func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool) (labelPrinter, int) {
if len(borderLabel) == 0 { // Nothing to do
if len(str) == 0 {
return nil, 0 return nil, 0
} }
text, colors, _ := extractColor(borderLabel, nil, nil)
// Extract ANSI color codes
text, colors, _ := extractColor(str, nil, nil)
runes := []rune(text) runes := []rune(text)
// Simpler printer for strings without ANSI colors or tab characters
if colors == nil && strings.IndexRune(str, '\t') < 0 {
length := runewidth.StringWidth(str)
if length == 0 {
return nil, 0
}
printFn := func(window tui.Window, limit int) {
if length > limit {
trimmedRunes, _ := t.trimRight(runes, limit)
window.CPrint(*color, string(trimmedRunes))
} else if fill {
window.CPrint(*color, util.RepeatToFill(str, length, limit))
} else {
window.CPrint(*color, str)
}
}
return printFn, len(text)
}
// Printer that correctly handles ANSI color codes and tab characters
item := &Item{text: util.RunesToChars(runes), colors: colors} item := &Item{text: util.RunesToChars(runes), colors: colors}
length := t.displayWidth(runes)
if length == 0 {
return nil, 0
}
result := Result{item: item} result := Result{item: item}
var offsets []colorOffset var offsets []colorOffset
borderLabelFn := func(window tui.Window) { printFn := func(window tui.Window, limit int) {
if offsets == nil { if offsets == nil {
// tui.Col* are not initialized until renderer.Init() // tui.Col* are not initialized until renderer.Init()
offsets = result.colorOffsets(nil, t.theme, tui.ColBorderLabel, tui.ColBorderLabel, false) offsets = result.colorOffsets(nil, t.theme, *color, *color, false)
}
for limit > 0 {
if length > limit {
trimmedRunes, _ := t.trimRight(runes, limit)
t.printColoredString(window, trimmedRunes, offsets, *color)
break
} else if fill {
t.printColoredString(window, runes, offsets, *color)
limit -= length
} else {
t.printColoredString(window, runes, offsets, *color)
break
}
} }
text, _ := t.trimRight(runes, window.Width())
t.printColoredString(window, text, offsets, tui.ColBorderLabel)
} }
borderLabelLen := runewidth.StringWidth(text) return printFn, length
return borderLabelFn, borderLabelLen
} }
func (t *Terminal) parsePrompt(prompt string) (func(), int) { func (t *Terminal) parsePrompt(prompt string) (func(), int) {
@ -684,7 +735,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
} }
func (t *Terminal) noInfoLine() bool { func (t *Terminal) noInfoLine() bool {
return t.infoStyle.layout != infoDefault return t.infoStyle != infoDefault
} }
// Input returns current query string // Input returns current query string
@ -1051,7 +1102,7 @@ func (t *Terminal) resizeWindows() {
} }
// Print border label // Print border label
printLabel := func(window tui.Window, render func(tui.Window), opts labelOpts, length int, borderShape tui.BorderShape) { printLabel := func(window tui.Window, render labelPrinter, opts labelOpts, length int, borderShape tui.BorderShape) {
if window == nil || render == nil { if window == nil || render == nil {
return return
} }
@ -1071,7 +1122,7 @@ func (t *Terminal) resizeWindows() {
row = window.Height() - 1 row = window.Height() - 1
} }
window.Move(row, col) window.Move(row, col)
render(window) render(window, window.Width())
} }
} }
printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape) printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape)
@ -1167,7 +1218,7 @@ func (t *Terminal) trimMessage(message string, maxWidth int) string {
func (t *Terminal) printInfo() { func (t *Terminal) printInfo() {
pos := 0 pos := 0
line := t.promptLine() line := t.promptLine()
switch t.infoStyle.layout { switch t.infoStyle {
case infoDefault: case infoDefault:
t.move(line+1, 0, true) t.move(line+1, 0, true)
if t.reading { if t.reading {
@ -1220,12 +1271,10 @@ func (t *Terminal) printInfo() {
output = t.trimMessage(output, maxWidth) output = t.trimMessage(output, maxWidth)
t.window.CPrint(tui.ColInfo, output) t.window.CPrint(tui.ColInfo, output)
if t.infoStyle.separator && len(output) < maxWidth-2 { fillLength := maxWidth - len(output) - 2
bar := "─" if t.separatorLen > 0 && fillLength > 0 {
if !t.unicode { t.window.CPrint(tui.ColSeparator, " ")
bar = "-" t.separator(t.window, fillLength)
}
t.window.CPrint(tui.ColSeparator, " "+strings.Repeat(bar, maxWidth-len(output)-2))
} }
} }

@ -153,3 +153,23 @@ func Once(nextResponse bool) func() bool {
return prevState return prevState
} }
} }
// RepeatToFill repeats the given string to fill the given width
func RepeatToFill(str string, length int, limit int) string {
times := limit / length
rest := limit % length
output := strings.Repeat(str, times)
if rest > 0 {
for _, r := range str {
rest -= runewidth.RuneWidth(r)
if rest < 0 {
break
}
output += string(r)
if rest == 0 {
break
}
}
}
return output
}

@ -2380,13 +2380,28 @@ class TestGoFZF < TestBase
end end
end end
def test_info_separator def test_info_separator_unicode
tmux.send_keys 'seq 100 | fzf -q55', :Enter tmux.send_keys 'seq 100 | fzf -q55', :Enter
tmux.until { assert_includes(_1[-2], ' 1/100 ─') } tmux.until { assert_includes(_1[-2], ' 1/100 ─') }
end end
def test_info_separator_no_unicode
tmux.send_keys 'seq 100 | fzf -q55 --no-unicode', :Enter
tmux.until { assert_includes(_1[-2], ' 1/100 -') }
end
def test_info_separator_repeat
tmux.send_keys 'seq 100 | fzf -q55 --separator _-', :Enter
tmux.until { assert_includes(_1[-2], ' 1/100 _-_-') }
end
def test_info_separator_ansi_colors_and_tabs
tmux.send_keys "seq 100 | fzf -q55 --tabstop 4 --separator $'\\x1b[33ma\\tb'", :Enter
tmux.until { assert_includes(_1[-2], ' 1/100 a ba ba') }
end
def test_info_no_separator def test_info_no_separator
tmux.send_keys 'seq 100 | fzf -q55 --info nosep', :Enter tmux.send_keys 'seq 100 | fzf -q55 --no-separator', :Enter
tmux.until { assert(_1[-2] == ' 1/100') } tmux.until { assert(_1[-2] == ' 1/100') }
end end
end end

Loading…
Cancel
Save