From 8868d7d1883178b5d196fe0d8eaafb22668343ed Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Thu, 10 Nov 2022 16:23:33 +0900 Subject: [PATCH] Add --separator to customize the info separator --- CHANGELOG.md | 14 +++++-- man/man1/fzf.1 | 12 ++++++ src/options.go | 59 ++++++++++++++--------------- src/terminal.go | 99 ++++++++++++++++++++++++++++++++++++------------ src/util/util.go | 20 ++++++++++ test/test_go.rb | 19 +++++++++- 6 files changed, 162 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed589c76..56d460a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,11 +29,17 @@ CHANGELOG ```sh 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:...` - - Separator can be disabled by adding `:nosep` to `--info` - - `--info=nosep` - - `--info=inline:nosep` + - ANSI color codes are also supported + ```sh + fzf --separator=╸ --color=separator:green + fzf --separator=$(lolcat -f -F 1.4 <<< ▁▁▂▃▄▅▆▆▅▄▃▂▁▁) --info=inline + ``` - Added `--border=bold` and `--border=double` along with `--preview-window=border-bold` and `--preview-window=border-double` diff --git a/man/man1/fzf.1 b/man/man1/fzf.1 index 13976a26..fd934b48 100644 --- a/man/man1/fzf.1 +++ b/man/man1/fzf.1 @@ -343,6 +343,18 @@ Determines the display style of finder info (match counters). .B "--no-info" 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 .BI "--prompt=" "STR" Input prompt (default: '> ') diff --git a/src/options.go b/src/options.go index 7df861c5..5400311a 100644 --- a/src/options.go +++ b/src/options.go @@ -70,7 +70,9 @@ const usage = `usage: fzf [options] (default: 0 or center) --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) - --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: '> ') --pointer=STR Pointer to the current line (default: '>') --marker=STR Multi-select marker (default: '>') @@ -173,14 +175,10 @@ const ( layoutReverseList ) -type infoLayout int -type infoStyle struct { - layout infoLayout - separator bool -} +type infoStyle int const ( - infoDefault infoLayout = iota + infoDefault infoStyle = iota infoInline infoHidden ) @@ -268,6 +266,7 @@ type Options struct { ScrollOff int FileWord bool InfoStyle infoStyle + Separator *string JumpLabels string Prompt string Pointer string @@ -334,7 +333,8 @@ func defaultOptions() *Options { HscrollOff: 10, ScrollOff: 0, FileWord: false, - InfoStyle: infoStyle{layout: infoDefault, separator: true}, + InfoStyle: infoDefault, + Separator: nil, JumpLabels: defaultJumpLabels, Prompt: "> ", Pointer: ">", @@ -1248,26 +1248,17 @@ func parseLayout(str string) layoutType { } func parseInfoStyle(str string) infoStyle { - layout := infoDefault - separator := true - - for _, token := range splitRegexp.Split(strings.ToLower(str), -1) { - switch token { - case "default": - layout = infoDefault - case "inline": - layout = infoInline - case "hidden": - layout = infoHidden - case "nosep": - separator = false - case "sep": - separator = true - default: - errorExit("invalid info style (expected: default|inline|hidden[:nosep])") - } + switch str { + case "default": + return infoDefault + case "inline": + return infoInline + case "hidden": + return infoHidden + default: + errorExit("invalid info style (expected: default|inline|hidden)") } - return infoStyle{layout: layout, separator: separator} + return infoDefault } func parsePreviewWindow(opts *previewOpts, input string) { @@ -1533,11 +1524,17 @@ func parseOptions(opts *Options, allArgs []string) { opts.InfoStyle = parseInfoStyle( nextString(allArgs, &i, "info style required")) case "--no-info": - opts.InfoStyle.layout = infoHidden + opts.InfoStyle = infoHidden case "--inline-info": - opts.InfoStyle.layout = infoInline + opts.InfoStyle = infoInline 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": opts.JumpLabels = nextString(allArgs, &i, "label characters required") validateJumpLabels = true @@ -1701,6 +1698,8 @@ func parseOptions(opts *Options, allArgs []string) { opts.Layout = parseLayout(value) } else if match, value := optString(arg, "--info="); match { opts.InfoStyle = parseInfoStyle(value) + } else if match, value := optString(arg, "--separator="); match { + opts.Separator = &value } else if match, value := optString(arg, "--toggle-sort="); match { parseToggleSort(opts.Keymap, value) } else if match, value := optString(arg, "--expect="); match { diff --git a/src/terminal.go b/src/terminal.go index d81f79dc..d1bae502 100644 --- a/src/terminal.go +++ b/src/terminal.go @@ -107,17 +107,21 @@ type fitpad struct { var emptyLine = itemLine{} +type labelPrinter func(tui.Window, int) + // Terminal represents terminal input/output type Terminal struct { initDelay time.Duration infoStyle infoStyle + separator labelPrinter + separatorLen int spinner []string prompt func() promptLen int - borderLabel func(tui.Window) + borderLabel labelPrinter borderLabelLen int borderLabelOpts labelOpts - previewLabel func(tui.Window) + previewLabel labelPrinter previewLabelLen int previewLabelOpts labelOpts pointer string @@ -498,7 +502,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { if previewBox != nil && opts.Preview.aboveOrBelow() { effectiveMinHeight += 1 + borderLines(opts.Preview.border) } - if opts.InfoStyle.layout != infoDefault { + if opts.InfoStyle != infoDefault { effectiveMinHeight-- } effectiveMinHeight += borderLines(opts.BorderShape) @@ -520,6 +524,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { t := Terminal{ initDelay: delay, infoStyle: opts.InfoStyle, + separator: nil, spinner: makeSpinner(opts.Unicode), queryLen: [2]int{0, 0}, layout: opts.Layout, @@ -597,8 +602,17 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { // Pre-calculated empty pointer and marker signs t.pointerEmpty = strings.Repeat(" ", t.pointerLen) t.markerEmpty = strings.Repeat(" ", t.markerLen) - t.borderLabel, t.borderLabelLen = t.parseBorderLabel(opts.BorderLabel.label) - t.previewLabel, t.previewLabelLen = t.parseBorderLabel(opts.PreviewLabel.label) + t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(opts.BorderLabel.label, &tui.ColBorderLabel, false) + 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 } @@ -629,26 +643,63 @@ func (t *Terminal) MaxFitAndPad(opts *Options) (int, int) { return fit, padHeight } -func (t *Terminal) parseBorderLabel(borderLabel string) (func(tui.Window), int) { - if len(borderLabel) == 0 { +func (t *Terminal) ansiLabelPrinter(str string, color *tui.ColorPair, fill bool) (labelPrinter, int) { + // Nothing to do + if len(str) == 0 { return nil, 0 } - text, colors, _ := extractColor(borderLabel, nil, nil) + + // Extract ANSI color codes + text, colors, _ := extractColor(str, nil, nil) 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} + length := t.displayWidth(runes) + if length == 0 { + return nil, 0 + } result := Result{item: item} - var offsets []colorOffset - borderLabelFn := func(window tui.Window) { + printFn := func(window tui.Window, limit int) { if offsets == nil { // 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 borderLabelFn, borderLabelLen + return printFn, length } 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 { - return t.infoStyle.layout != infoDefault + return t.infoStyle != infoDefault } // Input returns current query string @@ -1051,7 +1102,7 @@ func (t *Terminal) resizeWindows() { } // 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 { return } @@ -1071,7 +1122,7 @@ func (t *Terminal) resizeWindows() { row = window.Height() - 1 } window.Move(row, col) - render(window) + render(window, window.Width()) } } 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() { pos := 0 line := t.promptLine() - switch t.infoStyle.layout { + switch t.infoStyle { case infoDefault: t.move(line+1, 0, true) if t.reading { @@ -1220,12 +1271,10 @@ func (t *Terminal) printInfo() { output = t.trimMessage(output, maxWidth) t.window.CPrint(tui.ColInfo, output) - if t.infoStyle.separator && len(output) < maxWidth-2 { - bar := "─" - if !t.unicode { - bar = "-" - } - t.window.CPrint(tui.ColSeparator, " "+strings.Repeat(bar, maxWidth-len(output)-2)) + fillLength := maxWidth - len(output) - 2 + if t.separatorLen > 0 && fillLength > 0 { + t.window.CPrint(tui.ColSeparator, " ") + t.separator(t.window, fillLength) } } diff --git a/src/util/util.go b/src/util/util.go index a1c37f7a..cb211cbb 100644 --- a/src/util/util.go +++ b/src/util/util.go @@ -153,3 +153,23 @@ func Once(nextResponse bool) func() bool { 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 +} diff --git a/test/test_go.rb b/test/test_go.rb index 7b7046fb..41767fce 100755 --- a/test/test_go.rb +++ b/test/test_go.rb @@ -2380,13 +2380,28 @@ class TestGoFZF < TestBase end end - def test_info_separator + def test_info_separator_unicode tmux.send_keys 'seq 100 | fzf -q55', :Enter tmux.until { assert_includes(_1[-2], ' 1/100 ─') } 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 - 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') } end end