mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2025-02-02 20:18:31 +00:00
Add --separator to customize the info separator
This commit is contained in:
parent
2eec9892be
commit
8868d7d188
14
CHANGELOG.md
14
CHANGELOG.md
@ -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 {
|
||||||
|
103
src/terminal.go
103
src/terminal.go
@ -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)
|
|
||||||
runes := []rune(text)
|
|
||||||
item := &Item{text: util.RunesToChars(runes), colors: colors}
|
|
||||||
result := Result{item: item}
|
|
||||||
|
|
||||||
|
// 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
|
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…
x
Reference in New Issue
Block a user