mirror of
https://github.com/Llewellynvdm/fzf.git
synced 2024-06-03 07:50:49 +00:00
parent
b6e3f4423b
commit
6be855be6a
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -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.
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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":
|
||||||
|
|
|
@ -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++
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
Loading…
Reference in New Issue
Block a user