From 6be855be6af102a0f89932e5752ce75aa9713108 Mon Sep 17 00:00:00 2001 From: Junegunn Choi Date: Sat, 22 Apr 2023 22:01:00 +0900 Subject: [PATCH] Add change-header and transform-header Close #3237 --- CHANGELOG.md | 27 ++++++++++--------- man/man1/fzf.1 | 2 ++ src/options.go | 6 ++++- src/terminal.go | 72 +++++++++++++++++++++++++++++++++++-------------- test/test_go.rb | 61 +++++++++++++++++++++++++++++++++++++++++ 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 }