summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunegunn Choi <junegunn.c@gmail.com>2023-04-22 22:01:00 +0900
committerJunegunn Choi <junegunn.c@gmail.com>2023-04-22 22:01:37 +0900
commit6be855be6af102a0f89932e5752ce75aa9713108 (patch)
tree59c7190a02bcd71fb970557a124d518324e3dfe4
parentb6e3f4423bfb489f271282ea858e453ece5ab22b (diff)
Add change-header and transform-header
Close #3237
-rw-r--r--CHANGELOG.md27
-rw-r--r--man/man1/fzf.12
-rw-r--r--src/options.go6
-rw-r--r--src/terminal.go72
-rwxr-xr-xtest/test_go.rb61
5 files changed, 135 insertions, 33 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e487db66..594c5a51 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,19 +1,22 @@
CHANGELOG
=========
-0.39.1
+0.40.0
------
-- Added `toggle-track` action. Temporarily enabling tracking is useful when
- you want to see the surrounding items by deleting the query string.
- ```sh
- export FZF_CTRL_R_OPTS="
- --preview 'echo {}' --preview-window up:3:hidden:wrap
- --bind 'ctrl-/:toggle-preview'
- --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'"
- ```
+- New actions
+ - Added `change-header(...)`
+ - Added `transform-header(...)`
+ - Added `toggle-track` action. Temporarily enabling tracking is useful when
+ you want to see the surrounding items by deleting the query string.
+ ```sh
+ export FZF_CTRL_R_OPTS="
+ --preview 'echo {}' --preview-window up:3:hidden:wrap
+ --bind 'ctrl-/:toggle-preview'
+ --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`
- However, using `--track` with `--tac` is not recommended. The resulting
behavior can be very confusing.
diff --git a/man/man1/fzf.1 b/man/man1/fzf.1
index 834babee..acdde33e 100644
--- a/man/man1/fzf.1
+++ b/man/man1/fzf.1
@@ -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
\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-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-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 '|')
@@ -1100,6 +1101,7 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-sort\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\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-prompt(...)\fR (transform prompt string using an external command)
\fBtransform-query(...)\fR (transform query string using an external command)
diff --git a/src/options.go b/src/options.go
index e09e9b59..3718cf5a 100644
--- a/src/options.go
+++ b/src/options.go
@@ -927,7 +927,7 @@ const (
func init() {
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("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
}
@@ -1249,6 +1249,8 @@ func isExecuteAction(str string) actionType {
return actPreview
case "change-border-label":
return actChangeBorderLabel
+ case "change-header":
+ return actChangeHeader
case "change-preview-label":
return actChangePreviewLabel
case "change-preview-window":
@@ -1273,6 +1275,8 @@ func isExecuteAction(str string) actionType {
return actTransformBorderLabel
case "transform-preview-label":
return actTransformPreviewLabel
+ case "transform-header":
+ return actTransformHeader
case "transform-prompt":
return actTransformPrompt
case "transform-query":
diff --git a/src/terminal.go b/src/terminal.go
index 20a15d9a..e3403a40 100644
--- a/src/terminal.go
+++ b/src/terminal.go
@@ -3,6 +3,7 @@ package fzf
import (
"bufio"
"fmt"
+ "io"
"io/ioutil"
"math"
"os"
@@ -310,6 +311,7 @@ const (
actBackwardWord
actCancel
actChangeBorderLabel
+ actChangeHeader
actChangePreviewLabel
actChangePrompt
actChangeQuery
@@ -356,6 +358,7 @@ const (
actTogglePreview
actTogglePreviewWrap
actTransformBorderLabel
+ actTransformHeader
actTransformPreviewLabel
actTransformPrompt
actTransformQuery
@@ -624,7 +627,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
cycle: opts.Cycle,
headerFirst: opts.HeaderFirst,
headerLines: opts.HeaderLines,
- header: header,
+ header: []string{},
header0: header,
ellipsis: opts.Ellipsis,
ansi: opts.Ansi,
@@ -883,10 +886,21 @@ func reverseStringArray(input []string) []string {
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
func (t *Terminal) UpdateHeader(header []string) {
t.mutex.Lock()
- t.header = append(append([]string{}, t.header0...), header...)
+ t.header = header
t.mutex.Unlock()
t.reqBox.Set(reqHeader, nil)
}
@@ -1345,7 +1359,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
case layoutDefault:
y = h - y - 1
case layoutReverseList:
- n := 2 + len(t.header)
+ n := 2 + len(t.header0) + len(t.header)
if t.noInfoLine() {
n--
}
@@ -1493,7 +1507,7 @@ func (t *Terminal) printInfo() {
}
func (t *Terminal) printHeader() {
- if len(t.header) == 0 {
+ if len(t.header0)+len(t.header) == 0 {
return
}
max := t.window.Height()
@@ -1504,7 +1518,7 @@ func (t *Terminal) printHeader() {
}
}
var state *ansiState
- for idx, lineStr := range t.header {
+ for idx, lineStr := range append(append([]string{}, t.header0...), t.header...) {
line := idx
if !t.headerFirst {
line++
@@ -1538,7 +1552,7 @@ func (t *Terminal) printList() {
if t.layout == layoutDefault {
i = maxy - 1 - j
}
- line := i + 2 + len(t.header)
+ line := i + 2 + len(t.header0) + len(t.header)
if t.noInfoLine() {
line--
}
@@ -2276,12 +2290,12 @@ func (t *Terminal) redraw() {
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 := ""
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
- if !valid && !captureFirstLine {
+ if !valid && !capture {
return line
}
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.refresh()
} else {
- if captureFirstLine {
+ if capture {
out, _ := cmd.StdoutPipe()
reader := bufio.NewReader(out)
cmd.Start()
- line, _ = reader.ReadString('\n')
- line = strings.TrimRight(line, "\r\n")
+ if firstLineOnly {
+ line, _ = reader.ReadString('\n')
+ line = strings.TrimRight(line, "\r\n")
+ } else {
+ bytes, _ := io.ReadAll(reader)
+ line = string(bytes)
+ }
cmd.Wait()
} else {
cmd.Run()
@@ -2921,9 +2940,9 @@ func (t *Terminal) Loop() {
}
}
case actExecute, actExecuteSilent:
- t.executeCommand(a.a, false, a.t == actExecuteSilent, false)
+ t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
case actExecuteMulti:
- t.executeCommand(a.a, true, false, false)
+ t.executeCommand(a.a, true, false, false, false)
case actInvalid:
t.mutex.Unlock()
return false
@@ -2957,11 +2976,11 @@ func (t *Terminal) Loop() {
req(reqPreviewRefresh)
}
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)
req(reqPrompt)
case actTransformQuery:
- query := t.executeCommand(a.a, false, true, true)
+ query := t.executeCommand(a.a, false, true, true, true)
t.input = []rune(query)
t.cx = len(t.input)
case actToggleSort:
@@ -3010,6 +3029,19 @@ func (t *Terminal) Loop() {
case actChangeQuery:
t.input = []rune(a.a)
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:
if t.border != nil {
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
@@ -3022,13 +3054,13 @@ func (t *Terminal) Loop() {
}
case actTransformBorderLabel:
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)
req(reqRedrawBorderLabel)
}
case actTransformPreviewLabel:
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)
req(reqRedrawPreviewLabel)
}
@@ -3358,7 +3390,7 @@ func (t *Terminal) Loop() {
// Translate coordinates
mx -= t.window.Left()
my -= t.window.Top()
- min := 2 + len(t.header)
+ min := 2 + len(t.header0) + len(t.header)
if t.noInfoLine() {
min--
}
@@ -3627,7 +3659,7 @@ func (t *Terminal) vset(o int) bool {
}
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() {
max++
}
diff --git a/test/test_go.rb b/test/test_go.rb
index d1fa2c37..34884550 100755
--- a/test/test_go.rb
+++ b/test/test_go.rb
@@ -1865,6 +1865,67 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal '>', lines.last }
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
tmux.send_keys %(: | #{FZF} --query foo --bind space:change-query:foobar), :Enter
tmux.until { |lines| assert_equal 0, lines.item_count }