Compare commits

..

15 Commits

Author SHA1 Message Date
Junegunn Choi
fb76893e18 0.40.0 2023-05-01 01:59:21 +09:00
Junegunn Choi
88d812fe82 Do not display trailing carriage returns in the preview window
Close #3269
2023-04-30 18:14:40 +09:00
Junegunn Choi
77f9f4664a Fix search not triggered when query change and reload happen at the same time
Fix #3268
2023-04-30 18:14:40 +09:00
dependabot[bot]
5c2f85c39e Bump golang.org/x/term from 0.6.0 to 0.7.0 (#3249)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/term/releases)
- [Commits](https://github.com/golang/term/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 23:24:56 +09:00
dependabot[bot]
ac4d22cd12 Bump golang.org/x/sys from 0.6.0 to 0.7.0 (#3248)
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.6.0 to 0.7.0.
- [Release notes](https://github.com/golang/sys/releases)
- [Commits](https://github.com/golang/sys/compare/v0.6.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/sys
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-04-28 23:20:28 +09:00
Junegunn Choi
cf95e44cb4 Add 'zero' event
Close #3263
2023-04-26 15:13:08 +09:00
Junegunn Choi
65dd2bb429 Add 'track' action 2023-04-22 23:42:09 +09:00
Junegunn Choi
6be855be6a Add change-header and transform-header
Close #3237
2023-04-22 22:01:37 +09:00
Junegunn Choi
b6e3f4423b [man] Suggest setting RUNEWIDTH_EASTASIAN to 0 or 1
Close #2389
2023-04-22 16:35:46 +09:00
Junegunn Choi
0c61d81713 Add toggle-track action 2023-04-22 15:48:51 +09:00
Junegunn Choi
7c6f5dba63 Fixed --track when used with --tac
Fix #3234
2023-04-22 15:09:43 +09:00
psarlov
44cfc7e62a [vim] Add check for powershell 7 users (#3257)
Co-authored-by: Pavel Sarlov <psarlov@asteasolutions.com>
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2023-04-21 20:23:03 +09:00
Junegunn Choi
96670d5f16 Disallow using --track with --tac
Close #3234
2023-04-12 13:23:10 +09:00
Junegunn Choi
36b971ee4e [fzf-tmux] Try awk before bc 2023-04-11 16:29:29 +09:00
Junegunn Choi
f1a9629652 [fzf-tmux] Use awk if bc is not found
Fix #3235
2023-04-06 13:43:18 +09:00
16 changed files with 378 additions and 63 deletions

View File

@@ -1,6 +1,38 @@
CHANGELOG
=========
0.40.0
------
- Added `zero` event that is triggered when there's no match
```sh
# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3
```
- New actions
- Added `track` action which makes fzf track the current item when the
search result is updated. If the user manually moves the cursor, or the
item is not in the updated search result, tracking is automatically
disabled. Tracking is useful when you want to see the surrounding items
by deleting the query string.
```sh
# Narrow down the list with a query, point to a command,
# and hit CTRL-T to see its surrounding commands.
export FZF_CTRL_R_OPTS="
--preview 'echo {}' --preview-window up:3:hidden:wrap
--bind 'ctrl-/:toggle-preview'
--bind 'ctrl-t:track+clear-query'
--bind 'ctrl-y:execute-silent(echo -n {2..} | pbcopy)+abort'
--color header:italic
--header 'Press CTRL-Y to copy command into clipboard'"
```
- Added `change-header(...)`
- Added `transform-header(...)`
- Added `toggle-track` action
- Fixed `--track` behavior when used with `--tac`
- However, using `--track` with `--tac` is not recommended. The resulting
behavior can be very confusing.
- Bug fixes and improvements
0.39.0
------
- Added `one` event that is triggered when there's only one match
@@ -176,7 +208,7 @@ CHANGELOG
- Added color name `preview-label` for `--preview-label` (defaults to `label`
for `--border-label`)
- Better support for (Windows) terminals where each box-drawing character
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `1`.
takes 2 columns. Set `RUNEWIDTH_EASTASIAN` environment variable to `0` or `1`.
- On Vim, the variable will be automatically set if `&ambiwidth` is `double`
- Behavior changes
- fzf will always execute the preview command if the command template

View File

@@ -180,7 +180,7 @@ trap 'cleanup' EXIT
envs="export TERM=$TERM "
if [[ "$opt" =~ "-E" ]]; then
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
if [[ $(bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
opt="-B $opt"
elif [[ $tmux_version = 3.2 ]]; then

4
go.mod
View File

@@ -7,8 +7,8 @@ require (
github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.4
github.com/saracen/walker v0.1.3
golang.org/x/sys v0.6.0
golang.org/x/term v0.6.0
golang.org/x/sys v0.7.0
golang.org/x/term v0.7.0
)
require (

8
go.sum
View File

@@ -32,12 +32,12 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

View File

@@ -2,7 +2,7 @@
set -u
version=0.39.0
version=0.40.0
auto_completion=
key_bindings=
update_config=2

View File

@@ -1,4 +1,4 @@
$version="0.39.0"
$version="0.40.0"
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition

View File

@@ -5,7 +5,7 @@ import (
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.39"
var version string = "0.40"
var revision string = "devel"
func main() {

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf-tmux 1 "Apr 2023" "fzf 0.39.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "May 2023" "fzf 0.40.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME
fzf-tmux - open fzf in tmux split pane

View File

@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
..
.TH fzf 1 "Apr 2023" "fzf 0.39.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "May 2023" "fzf 0.40.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -94,7 +94,10 @@ Do not sort the result
.TP
.B "--track"
Make fzf track the current selection when the result list is updated.
This can be useful when browsing logs using fzf with sorting disabled.
This can be useful when browsing logs using fzf with sorting disabled. It is
not recommended to use this option with \fB--tac\fR as the resulting behavior
can be confusing. Also, consider using \fBtrack\fR action instead of this
option.
.RS
e.g.
@@ -241,8 +244,9 @@ Draw border around the finder
.br
If you use a terminal emulator where each box-drawing character takes
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR to \fB1\fR. If the border is
still not properly rendered, set \fB--no-unicode\fR.
2 columns, try setting \fBRUNEWIDTH_EASTASIAN\fR environment variable to
\fB0\fR or \fB1\fR. If the border is still not properly rendered, set
\fB--no-unicode\fR.
.TP
.BI "--border-label" [=LABEL]
@@ -1004,6 +1008,17 @@ e.g.
\fB# Automatically select the only match
seq 10 | fzf --bind one:accept\fR
.RE
\fIzero\fR
.RS
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
\fB--exit-0\fR option, but the difference is that \fB--exit-0\fR is only
effective before the interactive finder starts but \fBzero\fR event is
triggered by the interactive finder.
e.g.
\fB# Reload the candidate list when there's no match
echo $RANDOM | fzf --bind 'zero:reload(echo $RANDOM)+clear-query' --height 3\fR
.RE
\fIbackward-eof\fR
.RS
@@ -1030,6 +1045,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 '|')
@@ -1097,8 +1113,11 @@ A key or an event can be bound to one or more of the following actions.
\fBtoggle-preview-wrap\fR
\fBtoggle-search\fR (toggle search functionality)
\fBtoggle-sort\fR
\fBtoggle-track\fR
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
\fBtrack\fR (track the current item; automatically disabled if focus changes)
\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)

View File

@@ -164,7 +164,7 @@ function s:get_version(bin)
if has_key(s:versions, a:bin)
return s:versions[a:bin]
end
let command = (&shell =~ 'powershell' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
let output = systemlist(command)
if v:shell_error || empty(output)
return ''

View File

@@ -299,10 +299,12 @@ func Run(opts *Options, version string, revision string) {
case EvtSearchNew:
var command *string
var changed bool
switch val := value.(type) {
case searchRequest:
sort = val.sort
command = val.command
changed = val.changed
if command != nil {
useSnapshot = val.sync
}
@@ -314,10 +316,17 @@ func Run(opts *Options, version string, revision string) {
} else {
restart(*command)
}
}
if !changed {
break
}
if !useSnapshot {
snapshot, _ = chunkList.Snapshot()
newSnapshot, _ := chunkList.Snapshot()
// We want to avoid showing empty list when reload is triggered
// and the query string is changed at the same time i.e. command != nil && changed
if command == nil || len(newSnapshot) > 0 {
snapshot = newSnapshot
}
}
reset := !useSnapshot && clearCache()
matcher.Reset(snapshot, input(reset), true, !reading, sort, reset)

View File

@@ -60,17 +60,30 @@ func (mg *Merger) Length() int {
return mg.count
}
func (mg *Merger) First() Result {
if mg.tac && !mg.sorted {
return mg.Get(mg.count - 1)
}
return mg.Get(0)
}
// FindIndex returns the index of the item with the given item index
func (mg *Merger) FindIndex(itemIndex int32) int {
index := -1
if mg.pass {
return int(itemIndex)
}
for i := 0; i < mg.count; i++ {
if mg.Get(i).item.Index() == itemIndex {
return i
index = int(itemIndex)
if mg.tac {
index = mg.count - index - 1
}
} else {
for i := 0; i < mg.count; i++ {
if mg.Get(i).item.Index() == itemIndex {
index = i
break
}
}
}
return -1
return index
}
// Get returns the pointer to the Result object indexed by the given integer

View File

@@ -165,6 +165,14 @@ func defaultMargin() [4]sizeSpec {
return [4]sizeSpec{}
}
type trackOption int
const (
trackDisabled trackOption = iota
trackEnabled
trackCurrent
)
type windowPosition int
const (
@@ -267,7 +275,7 @@ type Options struct {
WithNth []Range
Delimiter Delimiter
Sort int
Track bool
Track trackOption
Tac bool
Criteria []criterion
Multi int
@@ -340,7 +348,7 @@ func defaultOptions() *Options {
WithNth: make([]Range, 0),
Delimiter: Delimiter{},
Sort: 1000,
Track: false,
Track: trackDisabled,
Tac: false,
Criteria: []criterion{byScore, byLength},
Multi: 0,
@@ -624,6 +632,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
add(tui.Focus)
case "one":
add(tui.One)
case "zero":
add(tui.Zero)
case "alt-enter", "alt-return":
chords[tui.CtrlAltKey('m')] = key
case "alt-space":
@@ -927,7 +937,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-]+")
}
@@ -1083,6 +1093,10 @@ func parseActionList(masked string, original string, prevActions []*action, putA
appendAction(actToggleAll)
case "toggle-search":
appendAction(actToggleSearch)
case "toggle-track":
appendAction(actToggleTrack)
case "track":
appendAction(actTrack)
case "select":
appendAction(actSelect)
case "select-all":
@@ -1247,6 +1261,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":
@@ -1271,6 +1287,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":
@@ -1568,9 +1586,9 @@ func parseOptions(opts *Options, allArgs []string) {
case "+s", "--no-sort":
opts.Sort = 0
case "--track":
opts.Track = true
opts.Track = trackEnabled
case "--no-track":
opts.Track = false
opts.Track = trackDisabled
case "--tac":
opts.Tac = true
case "--no-tac":

View File

@@ -3,6 +3,7 @@ package fzf
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"math"
"os"
@@ -183,7 +184,7 @@ type Terminal struct {
multi int
sort bool
toggleSort bool
track bool
track trackOption
delimiter Delimiter
expect map[tui.Event]string
keymap map[tui.Event][]*action
@@ -310,6 +311,7 @@ const (
actBackwardWord
actCancel
actChangeBorderLabel
actChangeHeader
actChangePreviewLabel
actChangePrompt
actChangeQuery
@@ -337,6 +339,8 @@ const (
actToggleUp
actToggleIn
actToggleOut
actToggleTrack
actTrack
actDown
actUp
actPageUp
@@ -355,6 +359,7 @@ const (
actTogglePreview
actTogglePreviewWrap
actTransformBorderLabel
actTransformHeader
actTransformPreviewLabel
actTransformPrompt
actTransformQuery
@@ -403,6 +408,7 @@ type searchRequest struct {
sort bool
sync bool
command *string
changed bool
}
type previewRequest struct {
@@ -623,7 +629,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,
@@ -882,10 +888,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)
}
@@ -907,8 +924,12 @@ func (t *Terminal) UpdateProgress(progress float32) {
func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.mutex.Lock()
var prevIndex int32 = -1
if !reset && t.track && t.merger.Length() > 0 {
prevIndex = t.merger.Get(t.cy).item.Index()
if !reset && t.track != trackDisabled {
if t.merger.Length() > 0 {
prevIndex = t.merger.Get(t.cy).item.Index()
} else if merger.Length() > 0 {
prevIndex = merger.First().item.Index()
}
}
t.progress = 100
t.merger = merger
@@ -916,7 +937,7 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
t.selected = make(map[int32]selectedItem)
t.version++
}
if t.hasLoadActions && t.triggerLoad {
if t.triggerLoad {
t.triggerLoad = false
t.eventChan <- tui.Load.AsEvent()
}
@@ -927,15 +948,27 @@ func (t *Terminal) UpdateList(merger *Merger, reset bool) {
if i >= 0 {
t.cy = i
t.offset = t.cy - pos
} else if t.track == trackCurrent {
t.track = trackDisabled
t.cy = pos
t.offset = 0
} else if t.cy > count {
// Try to keep the vertical position when the list shrinks
t.cy = count - util.Min(count, t.maxItems()) + pos
}
}
if !t.reading && t.merger.Length() == 1 {
one := tui.One.AsEvent()
if _, prs := t.keymap[one]; prs {
t.eventChan <- one
if !t.reading {
switch t.merger.Length() {
case 0:
zero := tui.Zero.AsEvent()
if _, prs := t.keymap[zero]; prs {
t.eventChan <- zero
}
case 1:
one := tui.One.AsEvent()
if _, prs := t.keymap[one]; prs {
t.eventChan <- one
}
}
}
t.mutex.Unlock()
@@ -1340,7 +1373,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--
}
@@ -1460,6 +1493,9 @@ func (t *Terminal) printInfo() {
output += " -S"
}
}
if t.track != trackDisabled {
output += " +T"
}
if t.multi > 0 {
if t.multi == maxMulti {
output += fmt.Sprintf(" (%d)", len(t.selected))
@@ -1485,7 +1521,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()
@@ -1496,7 +1532,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++
@@ -1530,7 +1566,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--
}
@@ -1836,7 +1872,7 @@ func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unc
if ansi != nil {
ansi.lbg = -1
}
line = strings.TrimSuffix(line, "\n")
line = strings.TrimRight(line, "\r\n")
if lineNo >= height || t.pwindow.Y() == height-1 && t.pwindow.X() > 0 {
t.previewed.filled = true
t.previewer.scrollable = true
@@ -2268,12 +2304,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)
@@ -2290,12 +2326,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()
@@ -2706,6 +2747,10 @@ func (t *Terminal) Loop() {
currentIndex = currentItem.Index()
}
focusChanged := focusedIndex != currentIndex
if focusChanged && t.track == trackCurrent {
t.track = trackDisabled
t.printInfo()
}
if onFocus != nil && focusChanged {
t.serverChan <- onFocus
}
@@ -2818,7 +2863,7 @@ func (t *Terminal) Loop() {
}
select {
case event = <-t.eventChan:
needBarrier = event != tui.Load.AsEvent()
needBarrier = !event.Is(tui.Load, tui.One, tui.Zero)
case actions = <-t.serverChan:
event = tui.Invalid.AsEvent()
needBarrier = false
@@ -2913,9 +2958,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
@@ -2949,11 +2994,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:
@@ -3002,6 +3047,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)
@@ -3014,13 +3072,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)
}
@@ -3270,6 +3328,19 @@ func (t *Terminal) Loop() {
t.paused = !t.paused
changed = !t.paused
req(reqPrompt)
case actToggleTrack:
switch t.track {
case trackEnabled:
t.track = trackDisabled
case trackDisabled:
t.track = trackEnabled
}
req(reqInfo)
case actTrack:
if t.track == trackDisabled {
t.track = trackCurrent
}
req(reqInfo)
case actEnableSearch:
t.paused = false
changed = true
@@ -3347,7 +3418,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--
}
@@ -3552,7 +3623,7 @@ func (t *Terminal) Loop() {
t.mutex.Unlock() // Must be unlocked before touching reqBox
if changed || newCommand != nil {
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand})
t.eventBox.Set(EvtSearchNew, searchRequest{sort: t.sort, sync: reloadSync, command: newCommand, changed: changed})
}
for _, event := range events {
t.reqBox.Set(event, nil)
@@ -3616,7 +3687,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++
}

View File

@@ -94,6 +94,7 @@ const (
Load
Focus
One
Zero
AltBS
@@ -283,6 +284,15 @@ type Event struct {
MouseEvent *MouseEvent
}
func (e Event) Is(types ...EventType) bool {
for _, t := range types {
if e.Type == t {
return true
}
}
return false
}
type MouseEvent struct {
Y int
X int

View File

@@ -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 }
@@ -2681,7 +2742,7 @@ class TestGoFZF < TestBase
end
def test_track
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track", :Enter
tmux.send_keys "seq 1000 | #{FZF} --query 555 --track --bind t:toggle-track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
@@ -2701,20 +2762,97 @@ class TestGoFZF < TestBase
assert_equal 1000, lines.match_count
assert_equal '> 555', lines[index]
end
tmux.send_keys '555'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
assert_includes lines[-2], '+T'
end
tmux.send_keys 't'
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys 't'
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 1000, lines.match_count
assert_includes lines, '> 5'
end
end
def test_one
tmux.send_keys "seq 10 | #{FZF} --bind 'one:preview:echo {} is the only match'", :Enter
def test_track_action
tmux.send_keys "seq 1000 | #{FZF} --query 555 --bind t:track", :Enter
tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 555'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 28, lines.match_count
assert_includes lines, '> 55'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 55'
end
# Automatically disabled when the tracking item is no longer visible
tmux.send_keys '4'
tmux.until do |lines|
assert_equal 28, lines.match_count
refute_includes lines[-2], '+T'
end
tmux.send_keys :BSpace
tmux.until do |lines|
assert_equal 271, lines.match_count
assert_includes lines, '> 5'
end
tmux.send_keys :t
tmux.until do |lines|
assert_includes lines[-2], '+T'
end
tmux.send_keys :Up
tmux.until do |lines|
refute_includes lines[-2], '+T'
end
end
def test_one_and_zero
tmux.send_keys "seq 10 | #{FZF} --bind 'zero:preview(echo no match),one:preview(echo {} is the only match)'", :Enter
tmux.send_keys '1'
tmux.until do |lines|
assert_equal 2, lines.match_count
refute(lines.any? { _1.include?('only match') })
refute(lines.any? { _1.include?('no match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 1, lines.match_count
assert(lines.any? { _1.include?('only match') })
end
tmux.send_keys '0'
tmux.until do |lines|
assert_equal 0, lines.match_count
assert(lines.any? { _1.include?('no match') })
end
end
def test_height_range_with_exit_0
@@ -2723,6 +2861,11 @@ class TestGoFZF < TestBase
tmux.send_keys :c
tmux.until { |lines| assert_equal 0, lines.match_count }
end
def test_reload_and_change
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
tmux.until { |lines| assert_equal 1, lines.match_count }
end
end
module TestShell