Compare commits

...

28 Commits

Author SHA1 Message Date
Junegunn Choi
260a65b0fb 0.51.0 2024-05-01 14:14:06 +09:00
Junegunn Choi
835d2fb98c [vim] Fix argument escaping for Windows batch file
Fix #3620
2024-05-01 10:02:26 +09:00
Charlie Vieth
a9811addaa Fix TestOSExitNotAllowed to handle empty GOROOT (#3758)
Fix #3748
2024-05-01 09:15:47 +09:00
dependabot[bot]
ee9d88b637 Bump crate-ci/typos from 1.20.9 to 1.20.10 (#3757)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.9 to 1.20.10.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.20.9...v1.20.10)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 23:10:50 +09:00
Junegunn Choi
194a763c46 Escaping for cmd.exe: always use double quotes 2024-04-28 23:30:44 +09:00
Junegunn Choi
8d74446bef Fix escaping for cmd.exe
Close #3651
Close #2609
2024-04-28 22:03:00 +09:00
Junegunn Choi
7ed6c7905c Determine shell type once by the basename 2024-04-28 20:11:05 +09:00
Junegunn Choi
159a37fa37 Restore CmdLine parameter when running commands using cmd.exe 2024-04-28 16:01:19 +09:00
junegunn
f39ae0e7c1 Deploying to master from @ junegunn/fzf@4a68eac99b 🚀 2024-04-28 00:01:30 +00:00
Junegunn Choi
4a68eac99b Suggest using toggle+up instead of toggle-up 2024-04-27 19:04:30 +09:00
Junegunn Choi
2665580120 Add $FZF_POS environment variable
Close #2175
Close #3753
2024-04-27 18:57:22 +09:00
Junegunn Choi
a4391aeedd Add --with-shell for shelling out with different command and flags (#3746)
Close #3732
2024-04-27 18:36:37 +09:00
dependabot[bot]
b86a967ee2 Bump crate-ci/typos from 1.19.0 to 1.20.9 (#3749)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.19.0 to 1.20.9.
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/v1.19.0...v1.20.9)

---
updated-dependencies:
- dependency-name: crate-ci/typos
  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>
2024-04-27 17:57:11 +09:00
Junegunn Choi
608232568b Add 'change-multi' action
Close #3754
2024-04-27 17:38:06 +09:00
Junegunn Choi
7f85beccb5 [completion] Add undocumented bash variables for completion commands
And allow empty FZF_COMPLETION_DIR_COMMANDS
2024-04-27 14:14:30 +09:00
Junegunn Choi
767f1255ab Make completion.bash load faster
* Reduce number of `__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)`s
* Clean up options in fzf completion
* Remove telnet completion
2024-04-25 16:54:51 +09:00
Junegunn Choi
fddbfe7b0e Fix 'reload' not terminating closed standard input stream
Fix #3750
2024-04-25 16:49:06 +09:00
Junegunn Choi
4ab7fdc28e Merge identical case clauses 2024-04-25 16:34:05 +09:00
Junegunn Choi
e352b68878 Update Dockerfile to use Ubuntu 24.04
As we require Go 1.20 or above.
2024-04-24 18:20:30 +09:00
Junegunn Choi
207deeadba Add -trimpath to build command 2024-04-23 17:24:50 +09:00
Cheng
d18d92f925 Replace fmt.Errorf with no parameters with errors.New (#3747) 2024-04-22 09:35:33 +09:00
junegunn
af3ce47c44 Deploying to master from @ junegunn/fzf@d8bfb6712d 🚀 2024-04-21 00:01:49 +00:00
Junegunn Choi
d8bfb6712d Remove invalid 'result' event when using --sync option
When the search for the initial query doesn't finish immediately
fzf would trigger an invalid 'result' event for an empty query.

  seq 100 | fzf --query 99 --bind result:accept --sync
    # Prints 99

  seq 1000000 | fzf --query 99 --bind result:accept --sync
    # Should print 99, but fzf would print 1
2024-04-20 14:42:43 +09:00
Junegunn Choi
f864f8b5f7 Respect $FZF_DEFAULT_OPTS_FILE in key bindings and completion (#3742)
Fix #3740
2024-04-19 22:40:38 +09:00
Junegunn Choi
31d72efba7 Describe how to build fzf from the latest source using brew 2024-04-18 23:37:12 +09:00
LangLangBart
d169c951f3 fix: Move 'emulate' command outside interactive check (#3736) 2024-04-17 18:03:12 +09:00
Junegunn Choi
90d7e38909 [fzf-tmux] Replace command -v with which
`command -v fzf` prints `alias fzf=...` when `fzf` is an alias.

Fix #3730
2024-04-17 17:29:45 +09:00
hidewrong
938f23e429 Fix typo in comment (#3734)
Signed-off-by: hidewrong <hidewrong@outlook.com>
2024-04-17 17:13:35 +09:00
32 changed files with 611 additions and 358 deletions

View File

@@ -7,4 +7,4 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: crate-ci/typos@v1.19.0 - uses: crate-ci/typos@v1.20.10

View File

@@ -12,6 +12,8 @@ builds:
- darwin - darwin
goarch: goarch:
- amd64 - amd64
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks: hooks:
@@ -19,7 +21,7 @@ builds:
sh -c ' sh -c '
cat > /tmp/fzf-gon-amd64.hcl << EOF cat > /tmp/fzf-gon-amd64.hcl << EOF
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"] source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "junegunn.fzf"
sign { sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
} }
@@ -36,6 +38,8 @@ builds:
- darwin - darwin
goarch: goarch:
- arm64 - arm64
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
hooks: hooks:
@@ -43,7 +47,7 @@ builds:
sh -c ' sh -c '
cat > /tmp/fzf-gon-arm64.hcl << EOF cat > /tmp/fzf-gon-arm64.hcl << EOF
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"] source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
bundle_id = "kr.junegunn.fzf" bundle_id = "junegunn.fzf"
sign { sign {
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)" application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
} }
@@ -71,6 +75,8 @@ builds:
- 5 - 5
- 6 - 6
- 7 - 7
flags:
- -trimpath
ldflags: ldflags:
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}" - "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
ignore: ignore:

View File

@@ -1,6 +1,36 @@
CHANGELOG CHANGELOG
========= =========
0.51.0
------
- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1.
```sh
# Toggle selection to the top or to the bottom
seq 30 | fzf --multi --bind 'load:pos(10)' \
--bind 'shift-up:transform:for _ in $(seq $FZF_POS $FZF_MATCH_COUNT); do echo -n +toggle+up; done' \
--bind 'shift-down:transform:for _ in $(seq 1 $FZF_POS); do echo -n +toggle+down; done'
```
- Added `--with-shell` option to start child processes with a custom shell command and flags
```sh
gem list | fzf --with-shell 'ruby -e' \
--preview 'pp Gem::Specification.find_by_name({1})' \
--bind 'ctrl-o:execute-silent:
spec = Gem::Specification.find_by_name({1})
[spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do
system "open", _1
end
'
```
- Added `change-multi` action for dynamically changing `--multi` option
- `change-multi` - enable multi-select mode with no limit
- `change-multi(NUM)` - enable multi-select mode with a limit
- `change-multi(0)` - disable multi-select mode
- Windows improvements
- `become` action is now supported on Windows
- Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ.
- Fixed argument escaping for Windows cmd.exe. No redundant escaping of backslashes.
- Bug fixes and improvements
0.50.0 0.50.0
------ ------
- Search performance optimization. You can observe 50%+ improvement in some scenarios. - Search performance optimization. You can observe 50%+ improvement in some scenarios.

View File

@@ -1,6 +1,6 @@
FROM --platform=linux/amd64 ubuntu:22.04 FROM ubuntu:24.04
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
RUN gem install --no-document -v 5.14.2 minitest RUN gem install --no-document -v 5.22.3 minitest
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
RUN echo '. ~/.bashrc' >> ~/.bash_profile RUN echo '. ~/.bashrc' >> ~/.bash_profile

View File

@@ -25,7 +25,7 @@ endif
ifeq ($(REVISION),) ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION) $(error Not on git repository; cannot determine $$FZF_REVISION)
endif endif
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath
BINARY32 := fzf-$(GOOS)_386 BINARY32 := fzf-$(GOOS)_386
BINARY64 := fzf-$(GOOS)_amd64 BINARY64 := fzf-$(GOOS)_amd64
@@ -174,12 +174,12 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf cp -f target/$(BINARY) bin/fzf
docker: docker:
docker build -t fzf-arch . docker build -t fzf-ubuntu .
docker run -it fzf-arch tmux docker run -it fzf-ubuntu tmux
docker-test: docker-test:
docker build -t fzf-arch . docker build -t fzf-ubuntu .
docker run -it fzf-arch docker run -it fzf-ubuntu
update: update:
$(GO) get -u $(GO) get -u

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@ fail() {
exit 2 exit 2
} }
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf" fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
[[ -x "$fzf" ]] || fail 'fzf executable not found' [[ -x "$fzf" ]] || fail 'fzf executable not found'
args=() args=()

View File

@@ -2,7 +2,7 @@
set -u set -u
version=0.50.0 version=0.51.0
auto_completion= auto_completion=
key_bindings= key_bindings=
update_config=2 update_config=2

View File

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

View File

@@ -9,7 +9,7 @@ import (
"github.com/junegunn/fzf/src/protector" "github.com/junegunn/fzf/src/protector"
) )
var version string = "0.50" var version string = "0.51"
var revision string = "devel" var revision string = "devel"
//go:embed shell/key-bindings.bash //go:embed shell/key-bindings.bash

View File

@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"fmt" "fmt"
"go/ast" "go/ast"
"go/build" "go/build"
@@ -10,6 +11,7 @@ import (
"go/types" "go/types"
"io/fs" "io/fs"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
@@ -17,6 +19,20 @@ import (
) )
func loadPackages(t *testing.T) []*build.Package { func loadPackages(t *testing.T) []*build.Package {
// If GOROOT is not set, use `go env GOROOT` to determine it since it
// performs more work than just runtime.GOROOT(). For context, running
// the tests with the "-trimpath" flag causes GOROOT to not be set.
ctxt := &build.Default
if ctxt.GOROOT == "" {
cmd := exec.Command("go", "env", "GOROOT")
out, err := cmd.CombinedOutput()
out = bytes.TrimSpace(out)
if err != nil {
t.Fatalf("error running command: %q: %v\n%s", cmd.Args, err, out)
}
ctxt.GOROOT = string(out)
}
wd, err := os.Getwd() wd, err := os.Getwd()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -38,7 +54,7 @@ func loadPackages(t *testing.T) []*build.Package {
if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") { if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
dir := filepath.Dir(path) dir := filepath.Dir(path)
if !seen[dir] { if !seen[dir] {
pkg, err := build.ImportDir(dir, build.ImportComment) pkg, err := ctxt.ImportDir(dir, build.ImportComment)
if err != nil { if err != nil {
return fmt.Errorf("%s: %s", dir, err) return fmt.Errorf("%s: %s", dir, err)
} }

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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf-tmux 1 "Apr 2024" "fzf 0.50.0" "fzf-tmux - open fzf in tmux split pane" .TH fzf-tmux 1 "May 2024" "fzf 0.51.0" "fzf-tmux - open fzf in tmux split pane"
.SH NAME .SH NAME
fzf-tmux - open fzf in tmux split pane 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
.. ..
.TH fzf 1 "Apr 2024" "fzf 0.50.0" "fzf - a command-line fuzzy finder" .TH fzf 1 "May 2024" "fzf 0.51.0" "fzf - a command-line fuzzy finder"
.SH NAME .SH NAME
fzf - a command-line fuzzy finder fzf - a command-line fuzzy finder
@@ -812,12 +812,22 @@ e.g.
.TP .TP
.B "--sync" .B "--sync"
Synchronous search for multi-staged filtering. If specified, fzf will launch Synchronous search for multi-staged filtering. If specified, fzf will launch
ncurses finder only after the input stream is complete. the finder only after the input stream is complete.
.RS .RS
e.g. \fBfzf --multi | fzf --sync\fR e.g. \fBfzf --multi | fzf --sync\fR
.RE .RE
.TP .TP
.B "--with-shell=STR"
Shell command and flags to start child processes with. On *nix Systems, the
default value is \fB$SHELL -c\fR if \fB$SHELL\fR is set, otherwise \fBsh -c\fR.
On Windows, the default value is \fBcmd /v:on/s/c\fR when \fB$SHELL\fR is not
set.
.RS
e.g. \fBgem list | fzf --with-shell 'ruby -e' --preview 'pp Gem::Specification.find_by_name({1})'\fR
.RE
.TP
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]" .B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
Start HTTP server and listen on the given address. It allows external processes Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method. to send actions to perform via POST method.
@@ -932,6 +942,8 @@ you need to protect against DNS rebinding and privilege escalation attacks.
.br .br
.BR 2 " Error" .BR 2 " Error"
.br .br
.BR 127 " Invalid shell command for \fBbecome\fR action"
.br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR" .BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION .SH FIELD INDEX EXPRESSION
@@ -972,6 +984,8 @@ fzf exports the following environment variables to its child processes.
.br .br
.BR FZF_SELECT_COUNT " Number of selected items" .BR FZF_SELECT_COUNT " Number of selected items"
.br .br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br
.BR FZF_QUERY " Current query string" .BR FZF_QUERY " Current query string"
.br .br
.BR FZF_PROMPT " Prompt string" .BR FZF_PROMPT " Prompt string"
@@ -1282,6 +1296,8 @@ A key or an event can be bound to one or more of the following actions.
\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-header(...)\fR (change header to the given string; doesn't affect \fB--header-lines\fR)
\fBchange-multi\fR (enable multi-select mode with no limit)
\fBchange-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
\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 '|')
@@ -1439,8 +1455,6 @@ call.
\fBfzf --bind "enter:become(vim {})"\fR \fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT .SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list \fBreload(...)\fR action is used to dynamically update the input list

View File

@@ -83,12 +83,28 @@ else
endfunction endfunction
endif endif
let s:cmd_control_chars = ['&', '|', '<', '>', '(', ')', '@', '^', '!']
function! s:shellesc_cmd(arg) function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g') let e = '"'
let escaped = substitute(escaped, '%', '%%', 'g') let slashes = 0
let escaped = substitute(escaped, '"', '\\^&', 'g') for c in split(a:arg, '\zs')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g') if c ==# '\'
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"' let slashes += 1
elseif c ==# '"'
let e .= repeat('\', slashes + 1)
let slashes = 0
elseif c ==# '%'
let e .= '%'
elseif index(s:cmd_control_chars, c) >= 0
let e .= '^'
else
let slashes = 0
endif
let e .= c
endfor
let e .= repeat('\', slashes) .'"'
return e
endfunction endfunction
function! fzf#shellescape(arg, ...) function! fzf#shellescape(arg, ...)

View File

@@ -32,6 +32,14 @@ if [[ $- =~ i ]]; then
# To redraw line after fzf closes (printf '\e[5n') # To redraw line after fzf closes (printf '\e[5n')
bind '"\e[0n": redraw-current-line' 2> /dev/null bind '"\e[0n": redraw-current-line' 2> /dev/null
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
@@ -92,128 +100,77 @@ _fzf_opts_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}" prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=" opts="
-h --help -h --help
-x --extended
-e --exact -e --exact
--extended-exact
+x --no-extended +x --no-extended
+e --no-exact
-q --query -q --query
-f --filter -f --filter
--literal --literal
--no-literal
--algo
--scheme --scheme
--expect --expect
--no-expect --disabled
--enabled --no-phony
--disabled --phony
--tiebreak --tiebreak
--bind --bind
--color --color
--toggle-sort
-d --delimiter -d --delimiter
-n --nth -n --nth
--with-nth --with-nth
-s --sort
+s --no-sort +s --no-sort
--track --track
--no-track
--tac --tac
--no-tac
-i -i
+i +i
-m --multi -m --multi
+m --no-multi
--ansi --ansi
--no-ansi
--no-mouse --no-mouse
+c --no-color +c --no-color
+2 --no-256
--black
--no-black
--bold
--no-bold --no-bold
--layout --layout
--reverse --reverse
--no-reverse
--cycle --cycle
--no-cycle
--keep-right --keep-right
--no-keep-right
--hscroll
--no-hscroll --no-hscroll
--hscroll-off --hscroll-off
--scroll-off --scroll-off
--filepath-word --filepath-word
--no-filepath-word
--info --info
--no-info
--inline-info
--no-inline-info
--separator --separator
--no-separator --no-separator
--scrollbar
--no-scrollbar --no-scrollbar
--jump-labels --jump-labels
-1 --select-1 -1 --select-1
+1 --no-select-1
-0 --exit-0 -0 --exit-0
+0 --no-exit-0
--read0 --read0
--no-read0
--print0 --print0
--no-print0
--print-query --print-query
--no-print-query
--prompt --prompt
--pointer --pointer
--marker --marker
--sync --sync
--no-sync
--async
--no-history
--history --history
--history-size --history-size
--no-header
--no-header-lines
--header --header
--header-lines --header-lines
--header-first --header-first
--no-header-first
--ellipsis --ellipsis
--preview --preview
--no-preview
--preview-window --preview-window
--height --height
--min-height --min-height
--no-height
--no-margin
--no-padding
--no-border
--border --border
--no-border-label
--border-label --border-label
--border-label-pos --border-label-pos
--no-preview-label
--preview-label --preview-label
--preview-label-pos --preview-label-pos
--no-unicode --no-unicode
--unicode
--margin --margin
--padding --padding
--tabstop --tabstop
--listen --listen
--no-listen
--clear
--no-clear --no-clear
--version --version
--" --"
case "${prev}" in case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme) --scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") ) COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0 return 0
@@ -335,8 +292,8 @@ __fzf_generic_path_completion() {
[[ -z "$dir" ]] && dir='.' [[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}" [[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$( matches=$(
unset FZF_DEFAULT_COMMAND export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2" unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -F "$1" > /dev/null; then if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover" eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else else
@@ -399,7 +356,10 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
cur=${cur:0:${#cur}-${#trigger}} cur=${cur:0:${#cur}-${#trigger}}
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ') selected=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
selected=${selected% } # Strip trailing space not to repeat "-o nospace" selected=${selected% } # Strip trailing space not to repeat "-o nospace"
if [[ -n "$selected" ]]; then if [[ -n "$selected" ]]; then
COMPREPLY=("$selected") COMPREPLY=("$selected")
@@ -503,8 +463,11 @@ complete -o default -F _fzf_opts_completion fzf
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch. # fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
complete -o default -F _fzf_opts_completion fzf-tmux complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}" d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
a_cmds="
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
# undocumented and subject to change in the future.
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
awk bat cat diff diff3 awk bat cat diff diff3
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby javac ld less more mvim nvim patch perl python ruby
@@ -512,10 +475,11 @@ a_cmds="
basename bunzip2 bzip2 chmod chown curl cp dirname du basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar find git grep gunzip gzip hg jar
ln ls mv open rm rsync scp ln ls mv open rm rsync scp
svn tar unzip zip" svn tar unzip zip"}"
v_cmds="${FZF_COMPLETION_VAR_COMMANDS-export unset printenv}"
# Preserve existing completion # Preserve existing completion
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null) __fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
if type _comp_load > /dev/null 2>&1; then if type _comp_load > /dev/null 2>&1; then
# _comp_load was added in bash-completion 2.12 to replace _completion_loader. # _comp_load was added in bash-completion 2.12 to replace _completion_loader.
@@ -551,10 +515,21 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames" __fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
done done
# Variables
for cmd in $v_cmds; do
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
done
# Aliases
__fzf_defc unalias _fzf_alias_completion "-a"
# Processes
__fzf_defc kill _fzf_proc_completion "-o default -o bashdefault"
# ssh # ssh
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault" __fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
unset cmd d_cmds a_cmds unset cmd d_cmds a_cmds v_cmds
_fzf_setup_completion() { _fzf_setup_completion() {
local kind fn cmd local kind fn cmd
@@ -576,10 +551,4 @@ _fzf_setup_completion() {
done done
} }
# Environment variables / Aliases / Hosts / Process
_fzf_setup_completion 'var' export unset printenv
_fzf_setup_completion 'alias' unalias
_fzf_setup_completion 'host' telnet
_fzf_setup_completion 'proc' kill
fi fi

View File

@@ -9,8 +9,6 @@
# - $FZF_COMPLETION_TRIGGER (default: '**') # - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty) # - $FZF_COMPLETION_OPTS (default: empty)
if [[ -o interactive ]]; then
# Both branches of the following `if` do the same thing -- define # Both branches of the following `if` do the same thing -- define
# __fzf_completion_options such that `eval $__fzf_completion_options` sets # __fzf_completion_options such that `eval $__fzf_completion_options` sets
@@ -75,6 +73,9 @@ fi
# This brace is the start of try-always block. The `always` part is like # This brace is the start of try-always block. The `always` part is like
# `finally` in lesser languages. We use it to *always* restore user options. # `finally` in lesser languages. We use it to *always* restore user options.
{ {
# The 'emulate' command should not be placed inside the interactive if check;
# placing it there fails to disable alias expansion. See #3731.
if [[ -o interactive ]]; then
# To use custom commands instead of find, override _fzf_compgen_{path,dir} # To use custom commands instead of find, override _fzf_compgen_{path,dir}
# #
@@ -93,6 +94,14 @@ fi
########################################################### ###########################################################
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_comprun() { __fzf_comprun() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@" _fzf_comprun "$@"
@@ -146,8 +155,8 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.' [ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}" [ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$( matches=$(
unset FZF_DEFAULT_COMMAND export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else else
@@ -217,7 +226,10 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat type $post > /dev/null 2>&1 || post=cat
_fzf_feed_fifo "$fifo" _fzf_feed_fifo "$fifo"
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ') matches=$(
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
FZF_DEFAULT_OPTS_FILE='' \
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
if [ -n "$matches" ]; then if [ -n "$matches" ]; then
LBUFFER="$lbuf$matches" LBUFFER="$lbuf$matches"
fi fi
@@ -315,7 +327,7 @@ fzf-completion() {
# Trigger sequence given # Trigger sequence given
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}) d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}} [ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
@@ -345,11 +357,10 @@ fzf-completion() {
zle -N fzf-completion zle -N fzf-completion
bindkey '^I' fzf-completion bindkey '^I' fzf-completion
fi
} always { } always {
# Restore the original options. # Restore the original options.
eval $__fzf_completion_options eval $__fzf_completion_options
'unset' '__fzf_completion_options' 'unset' '__fzf_completion_options'
} }
fi

View File

@@ -16,10 +16,19 @@ if [[ $- =~ i ]]; then
# Key bindings # Key bindings
# ------------ # ------------
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
__fzf_select__() { __fzf_select__() {
local opts FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=file,dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m" FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" | FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
while read -r item; do while read -r item; do
printf '%q ' "$item" # escape special chars printf '%q ' "$item" # escape special chars
done done
@@ -37,23 +46,24 @@ fzf-file-widget() {
} }
__fzf_cd__() { __fzf_cd__() {
local opts dir local dir
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --walker=dir,follow,hidden --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
dir=$( dir=$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)" ) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
} }
if command -v perl > /dev/null; then if command -v perl > /dev/null; then
__fzf_history__() { __fzf_history__() {
local output opts script local output script
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++' script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$( output=$(
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 | builtin fc -lnr -2147483648 |
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" | last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then
@@ -64,14 +74,13 @@ if command -v perl > /dev/null; then
} }
else # awk - fallback for POSIX systems else # awk - fallback for POSIX systems
__fzf_history__() { __fzf_history__() {
local output opts script n x y z d local output script n x y z d
if [[ -z $__fzf_awk ]]; then if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk __fzf_awk=awk
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4 # choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null) IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk [[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
fi fi
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries [[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } } script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
NR==1 { b = substr($0, 2); next } NR==1 { b = substr($0, 2); next }
@@ -82,7 +91,8 @@ else # awk - fallback for POSIX systems
set +o pipefail set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )* builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )* command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE" FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
) || return ) || return
READLINE_LINE=${output#*$'\t'} READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then if [[ -z "$READLINE_POINT" ]]; then

View File

@@ -18,6 +18,15 @@ status is-interactive; or exit 0
# ------------ # ------------
function fzf_key_bindings function fzf_key_bindings
function __fzf_defaults
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
echo $FZF_DEFAULT_OPTS $argv[2]
end
# Store current token in $dir as root for the 'find' command # Store current token in $dir as root for the 'find' command
function fzf-file-widget -d "List files and folders" function fzf-file-widget -d "List files and folders"
set -l commandline (__fzf_parse_commandline) set -l commandline (__fzf_parse_commandline)
@@ -27,8 +36,9 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=file,dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
set -lx FZF_DEFAULT_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end end
if [ -z "$result" ] if [ -z "$result" ]
@@ -49,7 +59,8 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history" function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m" set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -l FISH_MAJOR (echo $version | cut -f1 -d.) set -l FISH_MAJOR (echo $version | cut -f1 -d.)
set -l FISH_MINOR (echo $version | cut -f2 -d.) set -l FISH_MINOR (echo $version | cut -f2 -d.)
@@ -76,7 +87,8 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40% test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
begin begin
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --walker=dir,follow,hidden --walker-root='$dir' --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
set -lx FZF_DEFAULT_OPTS_FILE ''
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND" set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result

View File

@@ -11,8 +11,6 @@
# - $FZF_ALT_C_COMMAND # - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS # - $FZF_ALT_C_OPTS
if [[ -o interactive ]]; then
# Key bindings # Key bindings
# ------------ # ------------
@@ -38,12 +36,23 @@ fi
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases' 'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
{ {
if [[ -o interactive ]]; then
__fzf_defaults() {
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
echo "${FZF_DEFAULT_OPTS-} $2"
}
# CTRL-T - Paste the selected file path(s) into the command line # CTRL-T - Paste the selected file path(s) into the command line
__fsel() { __fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local item local item
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=file,dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" < /dev/tty | while read item; do FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
echo -n "${(q)item} " echo -n "${(q)item} "
done done
local ret=$? local ret=$?
@@ -57,7 +66,7 @@ __fzfcmd() {
} }
fzf-file-widget() { fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)" LBUFFER="${LBUFFER}$(__fzf_select)"
local ret=$? local ret=$?
zle reset-prompt zle reset-prompt
return $ret return $ret
@@ -72,7 +81,10 @@ fi
# ALT-C - cd into the selected directory # ALT-C - cd into the selected directory
fzf-cd-widget() { fzf-cd-widget() {
setopt localoptions pipefail no_aliases 2> /dev/null setopt localoptions pipefail no_aliases 2> /dev/null
local dir="$(FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --walker=dir,follow,hidden --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m < /dev/tty)" local dir="$(
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
if [[ -z "$dir" ]]; then if [[ -z "$dir" ]]; then
zle redisplay zle redisplay
return 0 return 0
@@ -97,7 +109,8 @@ fzf-history-widget() {
local selected num local selected num
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' | selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd))" FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
local ret=$? local ret=$?
if [ -n "$selected" ]; then if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected") num=$(awk '{print $1}' <<< "$selected")
@@ -114,10 +127,9 @@ zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget bindkey -M viins '^R' fzf-history-widget
fi
} always { } always {
eval $__fzf_key_bindings_options eval $__fzf_key_bindings_options
'unset' '__fzf_key_bindings_options' 'unset' '__fzf_key_bindings_options'
} }
fi

View File

@@ -26,102 +26,103 @@ func _() {
_ = x[actCancel-15] _ = x[actCancel-15]
_ = x[actChangeBorderLabel-16] _ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17] _ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18] _ = x[actChangeMulti-18]
_ = x[actChangePrompt-19] _ = x[actChangePreviewLabel-19]
_ = x[actChangeQuery-20] _ = x[actChangePrompt-20]
_ = x[actClearScreen-21] _ = x[actChangeQuery-21]
_ = x[actClearQuery-22] _ = x[actClearScreen-22]
_ = x[actClearSelection-23] _ = x[actClearQuery-23]
_ = x[actClose-24] _ = x[actClearSelection-24]
_ = x[actDeleteChar-25] _ = x[actClose-25]
_ = x[actDeleteCharEof-26] _ = x[actDeleteChar-26]
_ = x[actEndOfLine-27] _ = x[actDeleteCharEof-27]
_ = x[actForwardChar-28] _ = x[actEndOfLine-28]
_ = x[actForwardWord-29] _ = x[actForwardChar-29]
_ = x[actKillLine-30] _ = x[actForwardWord-30]
_ = x[actKillWord-31] _ = x[actKillLine-31]
_ = x[actUnixLineDiscard-32] _ = x[actKillWord-32]
_ = x[actUnixWordRubout-33] _ = x[actUnixLineDiscard-33]
_ = x[actYank-34] _ = x[actUnixWordRubout-34]
_ = x[actBackwardKillWord-35] _ = x[actYank-35]
_ = x[actSelectAll-36] _ = x[actBackwardKillWord-36]
_ = x[actDeselectAll-37] _ = x[actSelectAll-37]
_ = x[actToggle-38] _ = x[actDeselectAll-38]
_ = x[actToggleSearch-39] _ = x[actToggle-39]
_ = x[actToggleAll-40] _ = x[actToggleSearch-40]
_ = x[actToggleDown-41] _ = x[actToggleAll-41]
_ = x[actToggleUp-42] _ = x[actToggleDown-42]
_ = x[actToggleIn-43] _ = x[actToggleUp-43]
_ = x[actToggleOut-44] _ = x[actToggleIn-44]
_ = x[actToggleTrack-45] _ = x[actToggleOut-45]
_ = x[actToggleTrackCurrent-46] _ = x[actToggleTrack-46]
_ = x[actToggleHeader-47] _ = x[actToggleTrackCurrent-47]
_ = x[actTrackCurrent-48] _ = x[actToggleHeader-48]
_ = x[actUntrackCurrent-49] _ = x[actTrackCurrent-49]
_ = x[actDown-50] _ = x[actUntrackCurrent-50]
_ = x[actUp-51] _ = x[actDown-51]
_ = x[actPageUp-52] _ = x[actUp-52]
_ = x[actPageDown-53] _ = x[actPageUp-53]
_ = x[actPosition-54] _ = x[actPageDown-54]
_ = x[actHalfPageUp-55] _ = x[actPosition-55]
_ = x[actHalfPageDown-56] _ = x[actHalfPageUp-56]
_ = x[actOffsetUp-57] _ = x[actHalfPageDown-57]
_ = x[actOffsetDown-58] _ = x[actOffsetUp-58]
_ = x[actJump-59] _ = x[actOffsetDown-59]
_ = x[actJumpAccept-60] _ = x[actJump-60]
_ = x[actPrintQuery-61] _ = x[actJumpAccept-61]
_ = x[actRefreshPreview-62] _ = x[actPrintQuery-62]
_ = x[actReplaceQuery-63] _ = x[actRefreshPreview-63]
_ = x[actToggleSort-64] _ = x[actReplaceQuery-64]
_ = x[actShowPreview-65] _ = x[actToggleSort-65]
_ = x[actHidePreview-66] _ = x[actShowPreview-66]
_ = x[actTogglePreview-67] _ = x[actHidePreview-67]
_ = x[actTogglePreviewWrap-68] _ = x[actTogglePreview-68]
_ = x[actTransform-69] _ = x[actTogglePreviewWrap-69]
_ = x[actTransformBorderLabel-70] _ = x[actTransform-70]
_ = x[actTransformHeader-71] _ = x[actTransformBorderLabel-71]
_ = x[actTransformPreviewLabel-72] _ = x[actTransformHeader-72]
_ = x[actTransformPrompt-73] _ = x[actTransformPreviewLabel-73]
_ = x[actTransformQuery-74] _ = x[actTransformPrompt-74]
_ = x[actPreview-75] _ = x[actTransformQuery-75]
_ = x[actChangePreview-76] _ = x[actPreview-76]
_ = x[actChangePreviewWindow-77] _ = x[actChangePreview-77]
_ = x[actPreviewTop-78] _ = x[actChangePreviewWindow-78]
_ = x[actPreviewBottom-79] _ = x[actPreviewTop-79]
_ = x[actPreviewUp-80] _ = x[actPreviewBottom-80]
_ = x[actPreviewDown-81] _ = x[actPreviewUp-81]
_ = x[actPreviewPageUp-82] _ = x[actPreviewDown-82]
_ = x[actPreviewPageDown-83] _ = x[actPreviewPageUp-83]
_ = x[actPreviewHalfPageUp-84] _ = x[actPreviewPageDown-84]
_ = x[actPreviewHalfPageDown-85] _ = x[actPreviewHalfPageUp-85]
_ = x[actPrevHistory-86] _ = x[actPreviewHalfPageDown-86]
_ = x[actPrevSelected-87] _ = x[actPrevHistory-87]
_ = x[actPut-88] _ = x[actPrevSelected-88]
_ = x[actNextHistory-89] _ = x[actPut-89]
_ = x[actNextSelected-90] _ = x[actNextHistory-90]
_ = x[actExecute-91] _ = x[actNextSelected-91]
_ = x[actExecuteSilent-92] _ = x[actExecute-92]
_ = x[actExecuteMulti-93] _ = x[actExecuteSilent-93]
_ = x[actSigStop-94] _ = x[actExecuteMulti-94]
_ = x[actFirst-95] _ = x[actSigStop-95]
_ = x[actLast-96] _ = x[actFirst-96]
_ = x[actReload-97] _ = x[actLast-97]
_ = x[actReloadSync-98] _ = x[actReload-98]
_ = x[actDisableSearch-99] _ = x[actReloadSync-99]
_ = x[actEnableSearch-100] _ = x[actDisableSearch-100]
_ = x[actSelect-101] _ = x[actEnableSearch-101]
_ = x[actDeselect-102] _ = x[actSelect-102]
_ = x[actUnbind-103] _ = x[actDeselect-103]
_ = x[actRebind-104] _ = x[actUnbind-104]
_ = x[actBecome-105] _ = x[actRebind-105]
_ = x[actResponse-106] _ = x[actBecome-106]
_ = x[actShowHeader-107] _ = x[actResponse-107]
_ = x[actHideHeader-108] _ = x[actShowHeader-108]
_ = x[actHideHeader-109]
} }
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader" const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 640, 655, 670, 687, 694, 699, 708, 719, 730, 743, 758, 769, 782, 789, 802, 815, 832, 847, 860, 874, 888, 904, 924, 936, 959, 977, 1001, 1019, 1036, 1046, 1062, 1084, 1097, 1113, 1125, 1139, 1155, 1173, 1193, 1215, 1229, 1244, 1250, 1264, 1279, 1289, 1305, 1320, 1330, 1338, 1345, 1354, 1367, 1383, 1398, 1407, 1418, 1427, 1436, 1445, 1456, 1469, 1482} var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 413, 427, 438, 449, 467, 484, 491, 510, 522, 536, 545, 560, 572, 585, 596, 607, 619, 633, 654, 669, 684, 701, 708, 713, 722, 733, 744, 757, 772, 783, 796, 803, 816, 829, 846, 861, 874, 888, 902, 918, 938, 950, 973, 991, 1015, 1033, 1050, 1060, 1076, 1098, 1111, 1127, 1139, 1153, 1169, 1187, 1207, 1229, 1243, 1258, 1264, 1278, 1293, 1303, 1319, 1334, 1344, 1352, 1359, 1368, 1381, 1397, 1412, 1421, 1432, 1441, 1450, 1459, 1470, 1483, 1496}
func (i actionType) String() string { func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) { if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -121,13 +121,16 @@ func Run(opts *Options, version string, revision string) {
}) })
} }
// Process executor
executor := util.NewExecutor(opts.WithShell)
// Reader // Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader var reader *Reader
if !streamingFilter { if !streamingFilter {
reader = NewReader(func(data []byte) bool { reader = NewReader(func(data []byte) bool {
return chunkList.Push(data) return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil) }, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} }
@@ -178,7 +181,7 @@ func Run(opts *Options, version string, revision string) {
mutex.Unlock() mutex.Unlock()
} }
return false return false
}, eventBox, opts.ReadZero, false) }, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip) reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else { } else {
eventBox.Unwatch(EvtReadNew) eventBox.Unwatch(EvtReadNew)
@@ -209,7 +212,7 @@ func Run(opts *Options, version string, revision string) {
go matcher.Loop() go matcher.Loop()
// Terminal I/O // Terminal I/O
terminal := NewTerminal(opts, eventBox) terminal := NewTerminal(opts, eventBox, executor)
maxFit := 0 // Maximum number of items that can fit on screen maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0 padHeight := 0
heightUnknown := opts.Height.auto heightUnknown := opts.Height.auto
@@ -298,8 +301,7 @@ func Run(opts *Options, version string, revision string) {
total = count total = count
terminal.UpdateCount(total, !reading, value.(*string)) terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync { if opts.Sync {
opts.Sync = false terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
} }
if heightUnknown && !deferred { if heightUnknown && !deferred {
determine(!reading) determine(!reading)
@@ -384,7 +386,7 @@ func Run(opts *Options, version string, revision string) {
determine(val.final) determine(val.final)
} }
} }
terminal.UpdateList(val) terminal.UpdateList(val, true)
} }
} }
} }

View File

@@ -57,7 +57,7 @@ const usage = `usage: fzf [options]
Layout Layout
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given --height=[~]HEIGHT[%] Display fzf window below the cursor with the given
height instead of using fullscreen. height instead of using fullscreen.
A negative value is calcalated as the terminal height A negative value is calculated as the terminal height
minus the given value. minus the given value.
If prefixed with '~', fzf will determine the height If prefixed with '~', fzf will determine the height
according to the input size. according to the input size.
@@ -120,6 +120,7 @@ const usage = `usage: fzf [options]
--read0 Read input delimited by ASCII NUL characters --read0 Read input delimited by ASCII NUL characters
--print0 Print output delimited by ASCII NUL characters --print0 Print output delimited by ASCII NUL characters
--sync Synchronous search for multi-staged filtering --sync Synchronous search for multi-staged filtering
--with-shell=STR Shell command and flags to start child processes with
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /) --listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
(To allow remote process execution, use --listen-unsafe) (To allow remote process execution, use --listen-unsafe)
--version Display version information and exit --version Display version information and exit
@@ -356,6 +357,7 @@ type Options struct {
Unicode bool Unicode bool
Ambidouble bool Ambidouble bool
Tabstop int Tabstop int
WithShell string
ListenAddr *listenAddress ListenAddr *listenAddress
Unsafe bool Unsafe bool
ClearOnExit bool ClearOnExit bool
@@ -1055,7 +1057,7 @@ const (
func init() { func init() {
executeRegexp = regexp.MustCompile( executeRegexp = regexp.MustCompile(
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|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)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put)`)
splitRegexp = regexp.MustCompile("[,:]+") splitRegexp = regexp.MustCompile("[,:]+")
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+") actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
} }
@@ -1306,6 +1308,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
if t == actIgnore { if t == actIgnore {
if specIndex == 0 && specLower == "" { if specIndex == 0 && specLower == "" {
actions = append(prevActions, actions...) actions = append(prevActions, actions...)
} else if specLower == "change-multi" {
appendAction(actChangeMulti)
} else { } else {
exit("unknown action: " + spec) exit("unknown action: " + spec)
} }
@@ -1325,10 +1329,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
actions = append(actions, &action{t: t, a: actionArg}) actions = append(actions, &action{t: t, a: actionArg})
} }
switch t { switch t {
case actBecome:
if util.IsWindows() {
exit("become action is not supported on Windows")
}
case actUnbind, actRebind: case actUnbind, actRebind:
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit) parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
case actChangePreviewWindow: case actChangePreviewWindow:
@@ -1407,6 +1407,8 @@ func isExecuteAction(str string) actionType {
return actChangePrompt return actChangePrompt
case "change-query": case "change-query":
return actChangeQuery return actChangeQuery
case "change-multi":
return actChangeMulti
case "pos": case "pos":
return actPosition return actPosition
case "execute": case "execute":
@@ -1876,9 +1878,7 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required")) opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
case "--sync": case "--sync":
opts.Sync = true opts.Sync = true
case "--no-sync": case "--no-sync", "--async":
opts.Sync = false
case "--async":
opts.Sync = false opts.Sync = false
case "--no-history": case "--no-history":
opts.History = nil opts.History = nil
@@ -1955,6 +1955,8 @@ func parseOptions(opts *Options, allArgs []string) {
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)")) nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
case "--tabstop": case "--tabstop":
opts.Tabstop = nextInt(allArgs, &i, "tab stop required") opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
case "--with-shell":
opts.WithShell = nextString(allArgs, &i, "shell command and flags required")
case "--listen", "--listen-unsafe": case "--listen", "--listen-unsafe":
given, str := optionalNextString(allArgs, &i) given, str := optionalNextString(allArgs, &i)
addr := defaultListenAddr addr := defaultListenAddr
@@ -2071,6 +2073,8 @@ func parseOptions(opts *Options, allArgs []string) {
opts.Padding = parseMargin("padding", value) opts.Padding = parseMargin("padding", value)
} else if match, value := optString(arg, "--tabstop="); match { } else if match, value := optString(arg, "--tabstop="); match {
opts.Tabstop = atoi(value) opts.Tabstop = atoi(value)
} else if match, value := optString(arg, "--with-shell="); match {
opts.WithShell = value
} else if match, value := optString(arg, "--listen="); match { } else if match, value := optString(arg, "--listen="); match {
addr, err := parseListenAddress(value) addr, err := parseListenAddress(value)
if err != nil { if err != nil {

View File

@@ -18,6 +18,7 @@ import (
// Reader reads from command or standard input // Reader reads from command or standard input
type Reader struct { type Reader struct {
pusher func([]byte) bool pusher func([]byte) bool
executor *util.Executor
eventBox *util.EventBox eventBox *util.EventBox
delimNil bool delimNil bool
event int32 event int32
@@ -30,8 +31,8 @@ type Reader struct {
} }
// NewReader returns new Reader object // NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader { func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait} return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
} }
func (r *Reader) startEventPoller() { func (r *Reader) startEventPoller() {
@@ -147,7 +148,7 @@ func (r *Reader) feed(src io.Reader) {
} }
// We're not making any progress after 100 tries. Stop. // We're not making any progress after 100 tries. Stop.
if n == 0 && err == nil { if n == 0 {
break break
} }
@@ -242,7 +243,7 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock() r.mutex.Lock()
r.killed = false r.killed = false
r.command = &command r.command = &command
r.exec = util.ExecCommand(command, true) r.exec = r.executor.ExecCommand(command, true)
if environ != nil { if environ != nil {
r.exec.Env = environ r.exec.Env = environ
} }

View File

@@ -10,9 +10,10 @@ import (
func TestReadFromCommand(t *testing.T) { func TestReadFromCommand(t *testing.T) {
strs := []string{} strs := []string{}
eb := util.NewEventBox() eb := util.NewEventBox()
exec := util.NewExecutor("")
reader := NewReader( reader := NewReader(
func(s []byte) bool { strs = append(strs, string(s)); return true }, func(s []byte) bool { strs = append(strs, string(s)); return true },
eb, false, true) eb, exec, false, true)
reader.startEventPoller() reader.startEventPoller()

View File

@@ -78,7 +78,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
port := address.port port := address.port
apiKey := os.Getenv("FZF_API_KEY") apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 { if !address.IsLocal() && len(apiKey) == 0 {
return port, fmt.Errorf("FZF_API_KEY is required to allow remote access") return port, errors.New("FZF_API_KEY is required to allow remote access")
} }
addrStr := fmt.Sprintf("%s:%d", host, port) addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr) listener, err := net.Listen("tcp", addrStr)

View File

@@ -7,7 +7,6 @@ import (
"io" "io"
"math" "math"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"regexp" "regexp"
"sort" "sort"
@@ -245,6 +244,7 @@ type Terminal struct {
listenUnsafe bool listenUnsafe bool
borderShape tui.BorderShape borderShape tui.BorderShape
cleanExit bool cleanExit bool
executor *util.Executor
paused bool paused bool
border tui.Window border tui.Window
window tui.Window window tui.Window
@@ -367,6 +367,7 @@ const (
actCancel actCancel
actChangeBorderLabel actChangeBorderLabel
actChangeHeader actChangeHeader
actChangeMulti
actChangePreviewLabel actChangePreviewLabel
actChangePrompt actChangePrompt
actChangeQuery actChangeQuery
@@ -639,7 +640,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
} }
// NewTerminal returns new Terminal object // NewTerminal returns new Terminal object
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal { func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal {
input := trimQuery(opts.Query) input := trimQuery(opts.Query)
var delay time.Duration var delay time.Duration
if opts.Tac { if opts.Tac {
@@ -735,6 +736,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
previewLabel: nil, previewLabel: nil,
previewLabelOpts: opts.PreviewLabel, previewLabelOpts: opts.PreviewLabel,
cleanExit: opts.ClearOnExit, cleanExit: opts.ClearOnExit,
executor: executor,
paused: opts.Phony, paused: opts.Phony,
cycle: opts.Cycle, cycle: opts.Cycle,
headerVisible: true, headerVisible: true,
@@ -854,6 +856,7 @@ func (t *Terminal) environ() []string {
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected))) env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines)) env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns)) env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1)))
return env return env
} }
@@ -1067,7 +1070,7 @@ func (t *Terminal) UpdateProgress(progress float32) {
} }
// UpdateList updates Merger to display the list // UpdateList updates Merger to display the list
func (t *Terminal) UpdateList(merger *Merger) { func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
t.mutex.Lock() t.mutex.Lock()
prevIndex := minItem.Index() prevIndex := minItem.Index()
reset := t.revision != merger.Revision() reset := t.revision != merger.Revision()
@@ -1118,7 +1121,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
t.eventChan <- one t.eventChan <- one
} }
} }
if t.hasResultActions { if triggerResultEvent && t.hasResultActions {
t.eventChan <- tui.Result.AsEvent() t.eventChan <- tui.Result.AsEvent()
} }
} }
@@ -2521,6 +2524,7 @@ type replacePlaceholderParams struct {
allItems []*Item allItems []*Item
lastAction actionType lastAction actionType
prompt string prompt string
executor *util.Executor
} }
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string { func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
@@ -2534,6 +2538,7 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
allItems: list, allItems: list,
lastAction: t.lastAction, lastAction: t.lastAction,
prompt: t.promptString, prompt: t.promptString,
executor: t.executor,
}) })
} }
@@ -2594,7 +2599,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
case escaped: case escaped:
return match return match
case match == "{q}" || match == "{fzf:query}": case match == "{q}" || match == "{fzf:query}":
return quoteEntry(params.query) return params.executor.QuoteEntry(params.query)
case match == "{}": case match == "{}":
replace = func(item *Item) string { replace = func(item *Item) string {
switch { switch {
@@ -2607,13 +2612,13 @@ func replacePlaceholder(params replacePlaceholderParams) string {
case flags.file: case flags.file:
return item.AsString(params.stripAnsi) return item.AsString(params.stripAnsi)
default: default:
return quoteEntry(item.AsString(params.stripAnsi)) return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
} }
} }
case match == "{fzf:action}": case match == "{fzf:action}":
return params.lastAction.Name() return params.lastAction.Name()
case match == "{fzf:prompt}": case match == "{fzf:prompt}":
return quoteEntry(params.prompt) return params.executor.QuoteEntry(params.prompt)
default: default:
// token type and also failover (below) // token type and also failover (below)
rangeExpressions := strings.Split(match[1:len(match)-1], ",") rangeExpressions := strings.Split(match[1:len(match)-1], ",")
@@ -2647,7 +2652,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
str = strings.TrimSpace(str) str = strings.TrimSpace(str)
} }
if !flags.file { if !flags.file {
str = quoteEntry(str) str = params.executor.QuoteEntry(str)
} }
return str return str
} }
@@ -2687,7 +2692,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
return line return line
} }
command := t.replacePlaceholder(template, forcePlus, string(t.input), list) command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
cmd := util.ExecCommand(command, false) cmd := t.executor.ExecCommand(command, false)
cmd.Env = t.environ() cmd.Env = t.environ()
t.executing.Set(true) t.executing.Set(true)
if !background { if !background {
@@ -2964,7 +2969,7 @@ func (t *Terminal) Loop() {
if items[0] != nil { if items[0] != nil {
_, query := t.Input() _, query := t.Input()
command := t.replacePlaceholder(commandTemplate, false, string(query), items) command := t.replacePlaceholder(commandTemplate, false, string(query), items)
cmd := util.ExecCommand(command, true) cmd := t.executor.ExecCommand(command, true)
env := t.environ() env := t.environ()
if pwindowSize.Lines > 0 { if pwindowSize.Lines > 0 {
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines) lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
@@ -3371,27 +3376,21 @@ func (t *Terminal) Loop() {
valid, list := t.buildPlusList(a.a, false) valid, list := t.buildPlusList(a.a, false)
if valid { if valid {
command := t.replacePlaceholder(a.a, false, string(t.input), list) command := t.replacePlaceholder(a.a, false, string(t.input), list)
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
shellPath, err := exec.LookPath(shell)
if err == nil {
t.tui.Close() t.tui.Close()
if t.history != nil { if t.history != nil {
t.history.append(string(t.input)) t.history.append(string(t.input))
} }
/* /*
FIXME: It is not at all clear why this is required. FIXME: It is not at all clear why this is required.
The following command will report 'not a tty', unless we open The following command will report 'not a tty', unless we open
/dev/tty *twice* after closing the standard input for 'reload' /dev/tty *twice* after closing the standard input for 'reload'
in Reader.terminate(). in Reader.terminate().
: | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
while : | fzf --bind 'start:reload:ls' --bind 'load:become:tty'; do echo; done
*/ */
tui.TtyIn() tui.TtyIn()
util.SetStdin(tui.TtyIn()) t.executor.Become(tui.TtyIn(), t.environ(), command)
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
}
} }
case actExecute, actExecuteSilent: case actExecute, actExecuteSilent:
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false) t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
@@ -3489,6 +3488,19 @@ func (t *Terminal) Loop() {
} }
case actPrintQuery: case actPrintQuery:
req(reqPrintQuery) req(reqPrintQuery)
case actChangeMulti:
multi := t.multi
if a.a == "" {
multi = maxMulti
} else if n, e := strconv.Atoi(a.a); e == nil && n >= 0 {
multi = n
}
if t.multi > 0 && multi != t.multi {
t.selected = make(map[int32]selectedItem)
t.version++
}
t.multi = multi
req(reqList, reqInfo)
case actChangeQuery: case actChangeQuery:
t.input = []rune(a.a) t.input = []rune(a.a)
t.cx = len(t.input) t.cx = len(t.input)

View File

@@ -23,6 +23,7 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
allItems: allItems, allItems: allItems,
lastAction: actBackwardDeleteCharEof, lastAction: actBackwardDeleteCharEof,
prompt: "prompt", prompt: "prompt",
executor: util.NewExecutor(""),
}) })
} }
@@ -244,6 +245,7 @@ func TestQuoteEntry(t *testing.T) {
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`} unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`} windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
var effectiveStyle quotes var effectiveStyle quotes
exec := util.NewExecutor("")
if util.IsWindows() { if util.IsWindows() {
effectiveStyle = windowsStyle effectiveStyle = windowsStyle
@@ -278,7 +280,7 @@ func TestQuoteEntry(t *testing.T) {
} }
for input, expected := range tests { for input, expected := range tests {
escaped := quoteEntry(input) escaped := exec.QuoteEntry(input)
expected = templateToString(expected, effectiveStyle) expected = templateToString(expected, effectiveStyle)
if escaped != expected { if escaped != expected {
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped) t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
@@ -317,9 +319,9 @@ func TestUnixCommands(t *testing.T) {
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows // purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
func TestWindowsCommands(t *testing.T) { func TestWindowsCommands(t *testing.T) {
if !util.IsWindows() { // XXX Deprecated
t.SkipNow() t.SkipNow()
}
tests := []testCase{ tests := []testCase{
// reference: give{template, query, items}, want{output OR match} // reference: give{template, query, items}, want{output OR match}

View File

@@ -5,26 +5,11 @@ package fzf
import ( import (
"os" "os"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
var escaper *strings.Replacer
func init() {
tokens := strings.Split(os.Getenv("SHELL"), "/")
if tokens[len(tokens)-1] == "fish" {
// https://fishshell.com/docs/current/language.html#quotes
// > The only meaningful escape sequences in single quotes are \', which
// > escapes a single quote and \\, which escapes the backslash symbol.
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
} else {
escaper = strings.NewReplacer("'", "'\\''")
}
}
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGWINCH) signal.Notify(resizeChan, syscall.SIGWINCH)
} }
@@ -41,7 +26,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) { func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT) signal.Notify(resizeChan, syscall.SIGCONT)
} }
func quoteEntry(entry string) string {
return "'" + escaper.Replace(entry) + "'"
}

View File

@@ -4,8 +4,6 @@ package fzf
import ( import (
"os" "os"
"regexp"
"strings"
) )
func notifyOnResize(resizeChan chan<- os.Signal) { func notifyOnResize(resizeChan chan<- os.Signal) {
@@ -19,27 +17,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) { func notifyOnCont(resizeChan chan<- os.Signal) {
// NOOP // NOOP
} }
func quoteEntry(entry string) string {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
}
if strings.Contains(shell, "cmd") {
// backslash escaping is done here for applications
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
escaped := strings.Replace(entry, `\`, `\\`, -1)
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
// caret is the escape character for cmd shell
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
return r.ReplaceAllStringFunc(escaped, func(match string) string {
return "^" + match
})
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
escaped := strings.Replace(entry, `"`, `\"`, -1)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
} else {
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
}

View File

@@ -3,31 +3,71 @@
package util package util
import ( import (
"fmt"
"os" "os"
"os/exec" "os/exec"
"strings"
"syscall" "syscall"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
// ExecCommand executes the given command with $SHELL type Executor struct {
func ExecCommand(command string, setpgid bool) *exec.Cmd { shell string
args []string
escaper *strings.Replacer
}
func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")
args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
args = args[1:]
} else {
if len(shell) == 0 { if len(shell) == 0 {
shell = "sh" shell = "sh"
} }
return ExecCommandWith(shell, command, setpgid) args = []string{"-c"}
} }
// ExecCommandWith executes the given command with the specified shell var escaper *strings.Replacer
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd { tokens := strings.Split(shell, "/")
cmd := exec.Command(shell, "-c", command) if tokens[len(tokens)-1] == "fish" {
// https://fishshell.com/docs/current/language.html#quotes
// > The only meaningful escape sequences in single quotes are \', which
// > escapes a single quote and \\, which escapes the backslash symbol.
escaper = strings.NewReplacer("\\", "\\\\", "'", "\\'")
} else {
escaper = strings.NewReplacer("'", "'\\''")
}
return &Executor{shell, args, escaper}
}
// ExecCommand executes the given command with $SHELL
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
cmd := exec.Command(x.shell, append(x.args, command)...)
if setpgid { if setpgid {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
} }
return cmd return cmd
} }
func (x *Executor) QuoteEntry(entry string) string {
return "'" + x.escaper.Replace(entry) + "'"
}
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
shellPath, err := exec.LookPath(x.shell)
if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
Exit(127)
}
args := append([]string{shellPath}, append(x.args, command)...)
SetStdin(stdin)
syscall.Exec(shellPath, args, environ)
}
// KillCommand kills the process for the given command // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)

View File

@@ -6,62 +6,161 @@ import (
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
"path/filepath"
"strings" "strings"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
) )
var shellPath atomic.Value type shellType int
const (
shellTypeUnknown shellType = iota
shellTypeCmd
shellTypePowerShell
)
type Executor struct {
shell string
shellType shellType
args []string
shellPath atomic.Value
}
func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL")
args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
} else if len(shell) == 0 {
shell = "cmd"
}
shellType := shellTypeUnknown
basename := filepath.Base(shell)
if len(args) > 0 {
args = args[1:]
} else if strings.HasPrefix(basename, "cmd") {
shellType = shellTypeCmd
args = []string{"/v:on/s/c"}
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
shellType = shellTypePowerShell
args = []string{"-NoProfile", "-Command"}
} else {
args = []string{"-c"}
}
return &Executor{shell: shell, shellType: shellType, args: args}
}
// ExecCommand executes the given command with $SHELL // ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd { // FIXME: setpgid is unused. We set it in the Unix implementation so that we
var shell string // can kill preview process with its child processes at once.
if cached := shellPath.Load(); cached != nil { // NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
shell := x.shell
if cached := x.shellPath.Load(); cached != nil {
shell = cached.(string) shell = cached.(string)
} else { } else {
shell = os.Getenv("SHELL") if strings.Contains(shell, "/") {
if len(shell) == 0 {
shell = "cmd"
} else if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output() out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil { if err == nil {
shell = strings.Trim(string(out), "\n") shell = strings.Trim(string(out), "\n")
} }
} }
shellPath.Store(shell) x.shellPath.Store(shell)
} }
return ExecCommandWith(shell, command, setpgid)
}
// ExecCommandWith executes the given command with the specified shell
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
// can kill preview process with its child processes at once.
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
// but it is left as is now because no adverse effect has been observed.
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
var cmd *exec.Cmd var cmd *exec.Cmd
if strings.Contains(shell, "cmd") { if x.shellType == shellTypeCmd {
cmd = exec.Command(shell) cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false, HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command), CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
CreationFlags: 0, CreationFlags: 0,
} }
return cmd
}
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
} else { } else {
cmd = exec.Command(shell, "-c", command) cmd = exec.Command(shell, append(x.args, command)...)
}
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false, HideWindow: false,
CreationFlags: 0, CreationFlags: 0,
} }
}
return cmd return cmd
} }
func (x *Executor) Become(stdin *os.File, environ []string, command string) {
cmd := x.ExecCommand(command, false)
cmd.Stdin = stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = environ
err := cmd.Start()
if err != nil {
fmt.Fprintf(os.Stderr, "fzf (become): %s\n", err.Error())
Exit(127)
}
err = cmd.Wait()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
Exit(exitError.ExitCode())
}
}
Exit(0)
}
func escapeArg(s string) string {
b := make([]byte, 0, len(s)+2)
b = append(b, '"')
slashes := 0
for i := 0; i < len(s); i++ {
c := s[i]
switch c {
default:
slashes = 0
case '\\':
slashes++
case '&', '|', '<', '>', '(', ')', '@', '^', '%', '!':
b = append(b, '^')
case '"':
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '\\')
}
b = append(b, c)
}
for ; slashes > 0; slashes-- {
b = append(b, '\\')
}
b = append(b, '"')
return string(b)
}
func (x *Executor) QuoteEntry(entry string) string {
switch x.shellType {
case shellTypeCmd:
/* Manually tested with the following commands:
fzf --preview "echo {}"
fzf --preview "type {}"
echo .git\refs\| fzf --preview "dir {}"
echo .git\refs\\| fzf --preview "dir {}"
echo .git\refs\\\| fzf --preview "dir {}"
reg query HKCU | fzf --reverse --bind "enter:reload(reg query {})"
fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!"
fd -H --no-ignore -td -d 4 | fzf --preview "dir {}"
fd -H --no-ignore -td -d 4 | fzf --preview "eza {}" --preview-window up
fd -H --no-ignore -td -d 4 | fzf --preview "eza --color=always --tree --level=3 --icons=always {}"
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
*/
return escapeArg(entry)
case shellTypePowerShell:
escaped := strings.Replace(entry, `"`, `\"`, -1)
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
default:
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
}
}
// KillCommand kills the process for the given command // KillCommand kills the process for the given command
func KillCommand(cmd *exec.Cmd) error { func KillCommand(cmd *exec.Cmd) error {
return cmd.Process.Kill() return cmd.Process.Kill()

View File

@@ -425,6 +425,25 @@ class TestGoFZF < TestBase
end end
end end
def test_multi_action
tmux.send_keys "seq 10 | #{FZF} --bind 'a:change-multi,b:change-multi(3),c:change-multi(xxx),d:change-multi(0)'", :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 ') }
tmux.send_keys 'a'
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (0)') }
tmux.send_keys 'b'
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (0/3)') }
tmux.send_keys :BTab
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (1/3)') }
tmux.send_keys 'c'
tmux.send_keys :BTab
tmux.until { |lines| assert lines[-2]&.start_with?(' 10/10 (2/3)') }
tmux.send_keys 'd'
tmux.until do |lines|
assert lines[-2]&.start_with?(' 10/10 ') && !lines[-2]&.include?('(')
end
end
def test_with_nth def test_with_nth
[true, false].each do |multi| [true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/'; tmux.send_keys "(echo ' 1st 2nd 3rd/';
@@ -1955,6 +1974,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal 10, lines.item_count } tmux.until { |lines| assert_equal 10, lines.item_count }
end end
def test_reload_should_terminate_standard_input_stream
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
tmux.until { |lines| assert_equal 100, lines.item_count }
end
def test_clear_list_when_header_lines_changed_due_to_reload def test_clear_list_when_header_lines_changed_due_to_reload
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
tmux.until { |lines| assert_includes lines, ' 9' } tmux.until { |lines| assert_includes lines, ' 9' }
@@ -3226,6 +3250,17 @@ class TestGoFZF < TestBase
tmux.send_keys :Up tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' } tmux.until { |lines| assert_includes lines, '> 2' }
end end
def test_fzf_pos
tmux.send_keys "seq 100 | #{FZF} --preview 'echo $FZF_POS / $FZF_MATCH_COUNT'", :Enter
tmux.until { |lines| assert(lines.any? { |line| line.include?('1 / 100') }) }
tmux.send_keys :Up
tmux.until { |lines| assert(lines.any? { |line| line.include?('2 / 100') }) }
tmux.send_keys '99'
tmux.until { |lines| assert(lines.any? { |line| line.include?('1 / 1') }) }
tmux.send_keys '99'
tmux.until { |lines| assert(lines.any? { |line| line.include?('0 / 0') }) }
end
end end
module TestShell module TestShell
@@ -3457,7 +3492,11 @@ module CompletionTest
tmux.until { |lines| assert_operator lines.match_count, :>, 0 } tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Tab, :Tab # Tab does not work here tmux.send_keys :Tab, :Tab # Tab does not work here
tmux.send_keys 55 tmux.send_keys 55
tmux.until { |lines| assert_equal 1, lines.match_count } tmux.until do |lines|
assert_equal 1, lines.match_count
assert_includes lines, '> 55'
assert_includes lines, '> /tmp/fzf-test/d55'
end
tmux.send_keys :Enter tmux.send_keys :Enter
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] } tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.send_keys :xx tmux.send_keys :xx