Add change-header and transform-header

Close #3237
This commit is contained in:
Junegunn Choi 2023-04-22 22:01:00 +09:00
parent b6e3f4423b
commit 6be855be6a
No known key found for this signature in database
GPG Key ID: 254BC280FEF9C627
5 changed files with 135 additions and 33 deletions

View File

@ -1,19 +1,22 @@
CHANGELOG CHANGELOG
========= =========
0.39.1 0.40.0
------ ------
- Added `toggle-track` action. Temporarily enabling tracking is useful when - New actions
you want to see the surrounding items by deleting the query string. - Added `change-header(...)`
```sh - Added `transform-header(...)`
export FZF_CTRL_R_OPTS=" - Added `toggle-track` action. Temporarily enabling tracking is useful when
--preview 'echo {}' --preview-window up:3:hidden:wrap you want to see the surrounding items by deleting the query string.
--bind 'ctrl-/:toggle-preview' ```sh
--bind 'ctrl-t:toggle-track' export FZF_CTRL_R_OPTS="
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort' --preview 'echo {}' --preview-window up:3:hidden:wrap
--color header:italic --bind 'ctrl-/:toggle-preview'
--header 'Press CTRL-Y to copy command into clipboard'" --bind 'ctrl-t:toggle-track'
``` --bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--color header:italic
--header 'Press CTRL-Y to copy command into clipboard'"
```
- Fixed `--track` behavior when used with `--tac` - Fixed `--track` behavior when used with `--tac`
- However, using `--track` with `--tac` is not recommended. The resulting - However, using `--track` with `--tac` is not recommended. The resulting
behavior can be very confusing. behavior can be very confusing.

View File

@ -1031,6 +1031,7 @@ A key or an event can be bound to one or more of the following actions.
\fBbeginning-of-line\fR \fIctrl-a home\fR \fBbeginning-of-line\fR \fIctrl-a home\fR
\fBcancel\fR (clear query string if not empty, abort fzf otherwise) \fBcancel\fR (clear query string if not empty, abort fzf otherwise)
\fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string) \fBchange-border-label(...)\fR (change \fB--border-label\fR to the given string)
\fBchange-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
\fBchange-preview(...)\fR (change \fB--preview\fR option) \fBchange-preview(...)\fR (change \fB--preview\fR option)
\fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string) \fBchange-preview-label(...)\fR (change \fB--preview-label\fR to the given string)
\fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|') \fBchange-preview-window(...)\fR (change \fB--preview-window\fR option; rotate through the multiple option sets separated by '|')
@ -1100,6 +1101,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-sort\fR \fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR \fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtransform-border-label(...)\fR (transform border label using an external command) \fBtransform-border-label(...)\fR (transform border label using an external command)
\fBtransform-header(...)\fR (transform header using an external command)
\fBtransform-preview-label(...)\fR (transform preview label using an external command) \fBtransform-preview-label(...)\fR (transform preview label using an external command)
\fBtransform-prompt(...)\fR (transform prompt string using an external command) \fBtransform-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command) \fBtransform-query(...)\fR (transform query string using an external command)

View File

@ -927,7 +927,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`) `(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@ -1249,6 +1249,8 @@ func isExecuteAction(str string) actionType {
return actPreview return actPreview
case "change-border-label": case "change-border-label":
return actChangeBorderLabel return actChangeBorderLabel
case "change-header":
return actChangeHeader
case "change-preview-label": case "change-preview-label":
return actChangePreviewLabel return actChangePreviewLabel
case "change-preview-window": case "change-preview-window":
@ -1273,6 +1275,8 @@ func isExecuteAction(str string) actionType {
return actTransformBorderLabel return actTransformBorderLabel
case "transform-preview-label": case "transform-preview-label":
return actTransformPreviewLabel return actTransformPreviewLabel
case "transform-header":
return actTransformHeader
case "transform-prompt": case "transform-prompt":
return actTransformPrompt return actTransformPrompt
case "transform-query": case "transform-query":

View File

@ -3,6 +3,7 @@ package fzf
import ( import (
"bufio" "bufio"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"math" "math"
"os" "os"
@ -310,6 +311,7 @@ const (
actBackwardWord actBackwardWord
actCancel actCancel
actChangeBorderLabel actChangeBorderLabel
actChangeHeader
actChangePreviewLabel actChangePreviewLabel
actChangePrompt actChangePrompt
actChangeQuery actChangeQuery
@ -356,6 +358,7 @@ const (
actTogglePreview actTogglePreview
actTogglePreviewWrap actTogglePreviewWrap
actTransformBorderLabel actTransformBorderLabel
actTransformHeader
actTransformPreviewLabel actTransformPreviewLabel
actTransformPrompt actTransformPrompt
actTransformQuery actTransformQuery
@ -624,7 +627,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
cycle: opts.Cycle, cycle: opts.Cycle,
headerFirst: opts.HeaderFirst, headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines, headerLines: opts.HeaderLines,
header: header, header: []string{},
header0: header, header0: header,
ellipsis: opts.Ellipsis, ellipsis: opts.Ellipsis,
ansi: opts.Ansi, ansi: opts.Ansi,
@ -883,10 +886,21 @@ func reverseStringArray(input []string) []string {
return reversed return reversed
} }
func (t *Terminal) changeHeader(header string) bool {
lines := strings.Split(strings.TrimSuffix(header, "\n"), "\n")
switch t.layout {
case layoutDefault, layoutReverseList:
lines = reverseStringArray(lines)
}
needFullRedraw := len(t.header0) != len(lines)
t.header0 = lines
return needFullRedraw
}
// UpdateHeader updates the header // UpdateHeader updates the header
func (t *Terminal) UpdateHeader(header []string) { func (t *Terminal) UpdateHeader(header []string) {
t.mutex.Lock() t.mutex.Lock()
t.header = append(append([]string{}, t.header0...), header...) t.header = header
t.mutex.Unlock() t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil) t.reqBox.Set(reqHeader, nil)
} }
@ -1345,7 +1359,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
case layoutDefault: case layoutDefault:
y = h - y - 1 y = h - y - 1
case layoutReverseList: case layoutReverseList:
n := 2 + len(t.header) n := 2 + len(t.header0) + len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
n-- n--
} }
@ -1493,7 +1507,7 @@ func (t *Terminal) printInfo() {
} }
func (t *Terminal) printHeader() { func (t *Terminal) printHeader() {
if len(t.header) == 0 { if len(t.header0)+len(t.header) == 0 {
return return
} }
max := t.window.Height() max := t.window.Height()
@ -1504,7 +1518,7 @@ func (t *Terminal) printHeader() {
} }
} }
var state *ansiState var state *ansiState
for idx, lineStr := range t.header { for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
line := idx line := idx
if !t.headerFirst { if !t.headerFirst {
line++ line++
@ -1538,7 +1552,7 @@ func (t *Terminal) printList() {
if t.layout == layoutDefault { if t.layout == layoutDefault {
i = maxy - 1 - j i = maxy - 1 - j
} }
line := i + 2 + len(t.header) line := i + 2 + len(t.header0) + len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
line-- line--
} }
@ -2276,12 +2290,12 @@ func (t *Terminal) redraw() {
t.printAll() t.printAll()
} }
func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, captureFirstLine bool) string { func (t *Terminal) executeCommand(template string, forcePlus bool, background bool, capture bool, firstLineOnly bool) string {
line := "" line := ""
valid, list := t.buildPlusList(template, forcePlus) valid, list := t.buildPlusList(template, forcePlus)
// captureFirstLine is used for transform-{prompt,query} and we don't want to // 'capture' is used for transform-* and we don't want to
// return an empty string in those cases // return an empty string in those cases
if !valid && !captureFirstLine { if !valid && !capture {
return line return line
} }
command := t.replacePlaceholder(template, forcePlus, string(t.input), list) command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
@ -2298,12 +2312,17 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
t.redraw() t.redraw()
t.refresh() t.refresh()
} else { } else {
if captureFirstLine { if capture {
out, _ := cmd.StdoutPipe() out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out) reader := bufio.NewReader(out)
cmd.Start() cmd.Start()
line, _ = reader.ReadString('\n') if firstLineOnly {
line = strings.TrimRight(line, "\r\n") line, _ = reader.ReadString('\n')
line = strings.TrimRight(line, "\r\n")
} else {
bytes, _ := io.ReadAll(reader)
line = string(bytes)
}
cmd.Wait() cmd.Wait()
} else { } else {
cmd.Run() cmd.Run()
@ -2921,9 +2940,9 @@ func (t *Terminal) Loop() {
} }
} }
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false) t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
case actExecuteMulti: case actExecuteMulti:
t.executeCommand(a.a, true, false, false) t.executeCommand(a.a, true, false, false, false)
case actInvalid: case actInvalid:
t.mutex.Unlock() t.mutex.Unlock()
return false return false
@ -2957,11 +2976,11 @@ func (t *Terminal) Loop() {
req(reqPreviewRefresh) req(reqPreviewRefresh)
} }
case actTransformPrompt: case actTransformPrompt:
prompt := t.executeCommand(a.a, false, true, true) prompt := t.executeCommand(a.a, false, true, true, true)
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) 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:
@ -3010,6 +3029,19 @@ func (t *Terminal) Loop() {
case actChangeQuery: case actChangeQuery:
t.input = []rune(a.a) t.input = []rune(a.a)
t.cx = len(t.input) t.cx = len(t.input)
case actTransformHeader:
header := t.executeCommand(a.a, false, true, true, false)
if t.changeHeader(header) {
req(reqFullRedraw)
} else {
req(reqHeader)
}
case actChangeHeader:
if t.changeHeader(a.a) {
req(reqFullRedraw)
} else {
req(reqHeader)
}
case actChangeBorderLabel: case actChangeBorderLabel:
if t.border != nil { if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false) t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
@ -3022,13 +3054,13 @@ func (t *Terminal) Loop() {
} }
case actTransformBorderLabel: case actTransformBorderLabel:
if t.border != nil { if t.border != nil {
label := t.executeCommand(a.a, false, true, true) label := t.executeCommand(a.a, false, true, true, true)
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:
if t.pborder != nil { if t.pborder != nil {
label := t.executeCommand(a.a, false, true, true) label := t.executeCommand(a.a, false, true, true, true)
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false) t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
req(reqRedrawPreviewLabel) req(reqRedrawPreviewLabel)
} }
@ -3358,7 +3390,7 @@ func (t *Terminal) Loop() {
// Translate coordinates // Translate coordinates
mx -= t.window.Left() mx -= t.window.Left()
my -= t.window.Top() my -= t.window.Top()
min := 2 + len(t.header) min := 2 + len(t.header0) + len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
min-- min--
} }
@ -3627,7 +3659,7 @@ func (t *Terminal) vset(o int) bool {
} }
func (t *Terminal) maxItems() int { func (t *Terminal) maxItems() int {
max := t.window.Height() - 2 - len(t.header) max := t.window.Height() - 2 - len(t.header0) - len(t.header)
if t.noInfoLine() { if t.noInfoLine() {
max++ max++
} }

View File

@ -1865,6 +1865,67 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines.last } tmux.until { |lines| assert_equal '>', lines.last }
end end
def test_change_and_transform_header
[
'space:change-header:$(seq 4)',
'space:transform-header:seq 4'
].each_with_index do |binding, i|
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "#{binding}"), :Enter
expected = <<~OUTPUT
> 3
2
1
bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :Space
expected = <<~OUTPUT
> 3
2
1
1
2
3
4
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
next unless i.zero?
teardown
setup
end
end
def test_change_header
tmux.send_keys %(seq 3 | #{FZF} --header-lines 2 --header bar --bind "space:change-header:$(seq 4)"), :Enter
expected = <<~OUTPUT
> 3
2
1
bar
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
tmux.send_keys :Space
expected = <<~OUTPUT
> 3
2
1
1
2
3
4
1/1
>
OUTPUT
tmux.until { assert_block(expected, _1) }
end
def test_change_query def test_change_query
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
tmux.until { |lines| assert_equal 0, lines.item_count } tmux.until { |lines| assert_equal 0, lines.item_count }