Add --info-command for customizing the input text

Close #3866
This commit is contained in:
Junegunn Choi 2024-06-20 00:53:18 +09:00
parent d9c028c934
commit 540632bb9e
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 110 additions and 22 deletions

View File

@ -3,6 +3,13 @@ CHANGELOG
0.54.0 0.54.0
------ ------
- Added `--info-command` option for customizing info text
```sh
# Prepend the current cursor position in yellow
fzf --info-command='echo -e "\x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO 💛"'
```
- `$FZF_INFO` is set to the original info text
- ANSI color codes are supported
- Better cache management and improved rendering for `--tail` - Better cache management and improved rendering for `--tail`
- Improved `--sync` behavior - Improved `--sync` behavior
- When `--sync` is provided, fzf will not render the interface until the initial filtering and the associated actions (bound to any of `start`, `load`, `result`, or `focus`) are complete. - When `--sync` is provided, fzf will not render the interface until the initial filtering and the associated actions (bound to any of `start`, `load`, `result`, or `focus`) are complete.

View File

@ -452,6 +452,16 @@ Determines the display style of the finder info. (e.g. match counter, loading in
.BR inline-right:PREFIX " On the right end of the prompt line with a custom prefix" .BR inline-right:PREFIX " On the right end of the prompt line with a custom prefix"
.br .br
.TP
.BI "--info-command=" "COMMAND"
Command to generate the finder info. The command runs synchronously and block
the UI until completion, so make sure that it's fast. ANSI color codes are
supported. \fB$FZF_INFO\f$ variable is set to the original info text.
e.g.
\fB# Prepend the current cursor position in yellow
fzf --info-command='echo -e "\\x1b[33;1m$FZF_POS\\x1b[m/$FZF_INFO 💛"'\fR
.TP .TP
.B "--no-info" .B "--no-info"
A synonym for \fB--info=hidden\fB A synonym for \fB--info=hidden\fB

View File

@ -88,6 +88,7 @@ Usage: fzf [options]
--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 --info=STYLE Finder info style
[default|right|hidden|inline[-right][:PREFIX]] [default|right|hidden|inline[-right][:PREFIX]]
--info-command=COMMAND Command to generate info line
--separator=STR String to form horizontal separator on info line --separator=STR String to form horizontal separator on info line
--no-separator Hide info line separator --no-separator Hide info line separator
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window) --scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
@ -443,6 +444,7 @@ type Options struct {
FileWord bool FileWord bool
InfoStyle infoStyle InfoStyle infoStyle
InfoPrefix string InfoPrefix string
InfoCommand string
Separator *string Separator *string
JumpLabels string JumpLabels string
Prompt string Prompt string
@ -2189,6 +2191,12 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil { if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(str); err != nil {
return err return err
} }
case "--info-command":
if opts.InfoCommand, err = nextString(allArgs, &i, "info command required"); err != nil {
return err
}
case "--no-info-command":
opts.InfoCommand = ""
case "--no-info": case "--no-info":
opts.InfoStyle = infoHidden opts.InfoStyle = infoHidden
case "--inline-info": case "--inline-info":
@ -2543,6 +2551,8 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil { if opts.InfoStyle, opts.InfoPrefix, err = parseInfoStyle(value); err != nil {
return err return err
} }
} else if match, value := optString(arg, "--info-command="); match {
opts.InfoCommand = value
} else if match, value := optString(arg, "--separator="); match { } else if match, value := optString(arg, "--separator="); match {
opts.Separator = &value opts.Separator = &value
} else if match, value := optString(arg, "--scrollbar="); match { } else if match, value := optString(arg, "--scrollbar="); match {

View File

@ -207,6 +207,7 @@ type Status struct {
// Terminal represents terminal input/output // Terminal represents terminal input/output
type Terminal struct { type Terminal struct {
initDelay time.Duration initDelay time.Duration
infoCommand string
infoStyle infoStyle infoStyle infoStyle
infoPrefix string infoPrefix string
separator labelPrinter separator labelPrinter
@ -753,6 +754,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
t := Terminal{ t := Terminal{
initDelay: delay, initDelay: delay,
infoCommand: opts.InfoCommand,
infoStyle: opts.InfoStyle, infoStyle: opts.InfoStyle,
infoPrefix: opts.InfoPrefix, infoPrefix: opts.InfoPrefix,
separator: nil, separator: nil,
@ -1840,6 +1842,12 @@ func (t *Terminal) printInfo() {
if t.failed != nil && t.count == 0 { if t.failed != nil && t.count == 0 {
output = fmt.Sprintf("[Command failed: %s]", *t.failed) output = fmt.Sprintf("[Command failed: %s]", *t.failed)
} }
var outputPrinter labelPrinter
var outputLen int
if t.infoCommand != "" {
output = t.executeCommand(t.infoCommand, false, true, true, true, output)
outputPrinter, outputLen = t.ansiLabelPrinter(output, &tui.ColInfo, false)
}
if t.infoStyle == infoRight { if t.infoStyle == infoRight {
maxWidth := t.window.Width() maxWidth := t.window.Width()
@ -1847,8 +1855,13 @@ func (t *Terminal) printInfo() {
// Need space for spinner and a margin column // Need space for spinner and a margin column
maxWidth -= 2 maxWidth -= 2
} }
var fillLength int
if outputPrinter == nil {
output = t.trimMessage(output, maxWidth) output = t.trimMessage(output, maxWidth)
fillLength := t.window.Width() - len(output) - 2 fillLength = t.window.Width() - len(output) - 2
} else {
fillLength = t.window.Width() - outputLen - 2
}
if t.reading { if t.reading {
if fillLength >= 2 { if fillLength >= 2 {
printSeparator(fillLength-2, true) printSeparator(fillLength-2, true)
@ -1858,15 +1871,22 @@ func (t *Terminal) printInfo() {
} else if fillLength >= 0 { } else if fillLength >= 0 {
printSeparator(fillLength, true) printSeparator(fillLength, true)
} }
if outputPrinter == nil {
t.window.CPrint(tui.ColInfo, output) t.window.CPrint(tui.ColInfo, output)
} else {
outputPrinter(t.window, maxWidth)
}
t.window.Print(" ") // Margin t.window.Print(" ") // Margin
return return
} }
if t.infoStyle == infoInlineRight { if t.infoStyle == infoInlineRight {
if outputPrinter == nil {
outputLen = util.StringWidth(output)
}
if len(t.infoPrefix) == 0 { if len(t.infoPrefix) == 0 {
move(line, pos, false) move(line, pos, false)
newPos := util.Max(pos, t.window.Width()-util.StringWidth(output)-3) newPos := util.Max(pos, t.window.Width()-outputLen-3)
t.window.Print(strings.Repeat(" ", newPos-pos)) t.window.Print(strings.Repeat(" ", newPos-pos))
pos = newPos pos = newPos
if pos < t.window.Width() { if pos < t.window.Width() {
@ -1878,14 +1898,18 @@ func (t *Terminal) printInfo() {
pos++ pos++
} }
} else { } else {
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-util.StringWidth(t.infoPrefix)-1) pos = util.Max(pos, t.window.Width()-outputLen-util.StringWidth(t.infoPrefix)-1)
printInfoPrefix() printInfoPrefix()
} }
} }
maxWidth := t.window.Width() - pos maxWidth := t.window.Width() - pos
if outputPrinter == nil {
output = t.trimMessage(output, maxWidth) output = t.trimMessage(output, maxWidth)
t.window.CPrint(tui.ColInfo, output) t.window.CPrint(tui.ColInfo, output)
} else {
outputPrinter(t.window, maxWidth)
}
if t.infoStyle == infoInlineRight { if t.infoStyle == infoInlineRight {
if t.separatorLen > 0 { if t.separatorLen > 0 {
@ -1895,7 +1919,7 @@ func (t *Terminal) printInfo() {
return return
} }
fillLength := maxWidth - len(output) - 2 fillLength := maxWidth - outputLen - 2
if fillLength > 0 { if fillLength > 0 {
t.window.CPrint(tui.ColSeparator, " ") t.window.CPrint(tui.ColSeparator, " ")
printSeparator(fillLength, false) printSeparator(fillLength, false)
@ -2983,7 +3007,7 @@ func (t *Terminal) fullRedraw() {
t.printAll() t.printAll()
} }
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool, info string) string {
line := "" line := ""
valid, list := t.buildPlusList(template, forcePlus) valid, list := t.buildPlusList(template, forcePlus)
// 'capture' is used for transform-* and we don't want to // 'capture' is used for transform-* and we don't want to
@ -2994,6 +3018,9 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
command, tempFiles := t.replacePlaceholder(template, forcePlus, string(t.input), list) command, tempFiles := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := t.executor.ExecCommand(command, false) cmd := t.executor.ExecCommand(command, false)
cmd.Env = t.environ() cmd.Env = t.environ()
if len(info) > 0 {
cmd.Env = append(cmd.Env, "FZF_INFO="+info)
}
t.executing.Set(true) t.executing.Set(true)
if !background { if !background {
// Open a separate handle for tty input // Open a separate handle for tty input
@ -3021,17 +3048,20 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
} }
t.mutex.Unlock() t.mutex.Unlock()
if len(info) == 0 {
t.uiMutex.Lock() t.uiMutex.Lock()
}
t.tui.Pause(true) t.tui.Pause(true)
cmd.Run() cmd.Run()
t.tui.Resume(true, false) t.tui.Resume(true, false)
t.mutex.Lock() t.mutex.Lock()
t.fullRedraw() t.fullRedraw()
t.flush() t.flush()
t.uiMutex.Unlock()
} else { } else {
t.mutex.Unlock() t.mutex.Unlock()
if len(info) == 0 {
t.uiMutex.Lock() t.uiMutex.Lock()
}
if capture { if capture {
out, _ := cmd.StdoutPipe() out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out) reader := bufio.NewReader(out)
@ -3048,6 +3078,8 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
cmd.Run() cmd.Run()
} }
t.mutex.Lock() t.mutex.Lock()
}
if len(info) == 0 {
t.uiMutex.Unlock() t.uiMutex.Unlock()
} }
t.executing.Set(false) t.executing.Set(false)
@ -3528,13 +3560,20 @@ func (t *Terminal) Loop() error {
t.printList() t.printList()
currentIndex := t.currentIndex() currentIndex := t.currentIndex()
focusChanged := focusedIndex != currentIndex focusChanged := focusedIndex != currentIndex
printInfo := false
if focusChanged && t.track == trackCurrent { if focusChanged && t.track == trackCurrent {
t.track = trackDisabled t.track = trackDisabled
t.printInfo() printInfo = true
} }
if t.hasFocusActions && focusChanged && currentIndex != t.lastFocus { if (t.hasFocusActions || t.infoCommand != "") && focusChanged && currentIndex != t.lastFocus {
t.lastFocus = currentIndex t.lastFocus = currentIndex
t.eventChan <- tui.Focus.AsEvent() t.eventChan <- tui.Focus.AsEvent()
if t.infoCommand != "" {
printInfo = true
}
}
if printInfo {
t.printInfo()
} }
if focusChanged || version != t.version { if focusChanged || version != t.version {
version = t.version version = t.version
@ -3809,9 +3848,9 @@ func (t *Terminal) Loop() error {
} }
} }
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false) t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false, "")
case actExecuteMulti: case actExecuteMulti:
t.executeCommand(a.a, true, false, false, false) t.executeCommand(a.a, true, false, false, false, "")
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
@ -3852,12 +3891,12 @@ func (t *Terminal) Loop() error {
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actTransformPrompt: case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true, true) prompt := t.executeCommand(a.a, false, true, true, true, "")
t.promptString = prompt t.promptString = prompt
t.prompt, t.promptLen = t.parsePrompt(prompt) t.prompt, t.promptLen = t.parsePrompt(prompt)
req(reqPrompt) req(reqPrompt)
case actTransformQuery: case actTransformQuery:
query := t.executeCommand(a.a, false, true, true, true) query := t.executeCommand(a.a, false, true, true, true, "")
t.input = []rune(query) t.input = []rune(query)
t.cx = len(t.input) t.cx = len(t.input)
case actToggleSort: case actToggleSort:
@ -3921,7 +3960,7 @@ func (t *Terminal) Loop() error {
t.input = []rune(a.a) t.input = []rune(a.a)
t.cx = len(t.input) t.cx = len(t.input)
case actTransformHeader: case actTransformHeader:
header := t.executeCommand(a.a, false, true, true, false) header := t.executeCommand(a.a, false, true, true, false, "")
if t.changeHeader(header) { if t.changeHeader(header) {
req(reqFullRedraw) req(reqFullRedraw)
} else { } else {
@ -3946,19 +3985,19 @@ func (t *Terminal) Loop() error {
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
case actTransform: case actTransform:
body := t.executeCommand(a.a, false, true, true, false) body := t.executeCommand(a.a, false, true, true, false, "")
if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil { if actions, err := parseSingleActionList(strings.Trim(body, "\r\n")); err == nil {
return doActions(actions) return doActions(actions)
} }
case actTransformBorderLabel: case actTransformBorderLabel:
label := t.executeCommand(a.a, false, true, true, true) label := t.executeCommand(a.a, false, true, true, true, "")
t.borderLabelOpts.label = label t.borderLabelOpts.label = label
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
req(reqRedrawBorderLabel) req(reqRedrawBorderLabel)
} }
case actTransformPreviewLabel: case actTransformPreviewLabel:
label := t.executeCommand(a.a, false, true, true, true) label := t.executeCommand(a.a, false, true, true, true, "")
t.previewLabelOpts.label = label t.previewLabelOpts.label = label
if t.pborder != nil { if t.pborder != nil {
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)

View File

@ -2978,6 +2978,28 @@ class TestGoFZF < TestBase
tmux.until { assert_match(%r{ 0/100000}, _1[-1]) } tmux.until { assert_match(%r{ 0/100000}, _1[-1]) }
end end
def test_info_command
tmux.send_keys(%(seq 10000 | #{FZF} --separator x --info-command 'echo -e "--\\x1b[33m$FZF_POS\\x1b[m/$FZF_INFO--"'), :Enter)
tmux.until { assert_match(%r{^ --1/10000/10000-- xx}, _1[-2]) }
tmux.send_keys :Up
tmux.until { assert_match(%r{^ --2/10000/10000-- xx}, _1[-2]) }
end
def test_info_command_inline
tmux.send_keys(%(seq 10000 | #{FZF} --separator x --info-command 'echo -e "--\\x1b[33m$FZF_POS\\x1b[m/$FZF_INFO--"' --info inline:xx), :Enter)
tmux.until { assert_match(%r{^> xx--1/10000/10000-- xx}, _1[-1]) }
end
def test_info_command_right
tmux.send_keys(%(seq 10000 | #{FZF} --separator x --info-command 'echo -e "--\\x1b[33m$FZF_POS\\x1b[m/$FZF_INFO--"' --info right), :Enter)
tmux.until { assert_match(%r{xx --1/10000/10000-- *$}, _1[-2]) }
end
def test_info_command_inline_right
tmux.send_keys(%(seq 10000 | #{FZF} --info-command 'echo -e "--\\x1b[33m$FZF_POS\\x1b[m/$FZF_INFO--"' --info inline-right), :Enter)
tmux.until { assert_match(%r{ --1/10000/10000-- *$}, _1[-1]) }
end
def test_prev_next_selected def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count } tmux.until { |lines| assert_equal 10, lines.item_count }