summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2022-11-10 16:23:33 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2022-11-10 16:23:33 +0900
commit8868d7d1883178b5d196fe0d8eaafb22668343ed (patch)
tree14d1badb6050651e28819806bb1d31646b3f24b6
parent2eec9892be78bc5fbf8d0cdcd5b90317f426e944 (diff)
Add --separator to customize the info separator
-rw-r--r--CHANGELOG.md14
-rw-r--r--man/man1/fzf.112
-rw-r--r--src/options.go59
-rw-r--r--src/terminal.go99
-rw-r--r--src/util/util.go20
-rwxr-xr-xtest/test_go.rb19
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
@@ -344,6 +344,18 @@ Determines the display style of finder info (match counters).
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: '> ')
.TP
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