Compare commits

...

52 Commits

Author SHA1 Message Date
Junegunn Choi
bcda25a513 0.52.0 2024-05-08 00:15:30 +09:00
Junegunn Choi
8256fcde15 Minor fixup 2024-05-07 23:49:30 +09:00
Junegunn Choi
af65aa298a Add color names: selected-{fg,bg,hl} 2024-05-07 23:38:06 +09:00
Junegunn Choi
6834d17844 [vim] Revert 7411da8d5a
Fix #3777
2024-05-07 20:16:18 +09:00
Junegunn Choi
ed511d7867 [install] tar --no-same-owner
Close #3776
2024-05-07 20:00:13 +09:00
Junegunn Choi
cd8d736a9f [shell] Add $FZF_COMPLETION_{DIR,PATH}_OPTS
To allow separately overriding 'walker' options.

Close #3778
2024-05-07 19:31:56 +09:00
Junegunn Choi
0952b2dfd4 Rename --cursor-line to --highlight-line 2024-05-07 19:22:39 +09:00
Junegunn Choi
4bedd33c59 Refactor the code to remove global variables 2024-05-07 16:58:17 +09:00
Junegunn Choi
c5fb0c43f9 Add --cursor-line to highlight the whole current line
Similar to 'set cursorline' of Vim.
2024-05-07 01:34:35 +09:00
Junegunn Choi
9e4780510e Add current-{fg,bg,hl} as synonyms for {fg,bg,hl}+ 2024-05-07 01:26:25 +09:00
Junegunn Choi
e8405f40fe Refactor the code so that fzf can be used as a library (#3769) 2024-05-07 01:06:42 +09:00
dependabot[bot]
065b9e6fb2 Bump golang.org/x/term from 0.19.0 to 0.20.0 (#3774)
Bumps [golang.org/x/term](https://github.com/golang/term) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/term/compare/v0.19.0...v0.20.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>
2024-05-06 22:51:10 +09:00
dependabot[bot]
98141ca7d8 Bump crate-ci/typos from 1.20.10 to 1.21.0 (#3772)
Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.21.0.
- [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.10...v1.21.0)

---
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-05-06 22:50:50 +09:00
Junegunn Choi
501577ab28 Fix flaky test case 2024-05-06 13:59:16 +09:00
Junegunn Choi
5669f48343 Do not enable delayed expansion mode when running cmd.exe
And simplify the argument escaping code. Fix #3764.

This may breaks some existing use cases, but the mode causes too much
trouble when escaping arguments and it makes some things not possible.

  # Now you can pass special characters to rg process without any escaping problems: &|<>()@^%!
  fzf --ansi --disabled --bind "change:reload:rg --column --line-number --no-heading --color=always --smart-case -- {q}"

  # No sudden expansion of the arguments on '!'
  fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!" --prompt "&|<>()@^%!"
2024-05-06 13:46:06 +09:00
Junegunn Choi
24ff66d4a9 Fix change-preview reset by change-preview-window
Fix #3770
2024-05-06 09:40:02 +09:00
Junegunn Choi
bf184449bc Count $FZF_CLICK_HEADER_LINE from top to bottom
Regardless of `--layout`.

https://github.com/junegunn/fzf/pull/3768#issuecomment-2094806558
2024-05-06 09:27:58 +09:00
Kuremu
7b98c2c653 Add click-header event for reporting clicks within header (#3768)
Sets $FZF_CLICK_HEADER_LINE and $FZF_CLICK_HEADER_COLUMN env vars with
coordinates of the last click inside and relative to the header and
fires click-header event.

Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-05 18:56:43 +09:00
Junegunn Choi
b6add2a257 Fix rendering of preview window border of tcell renderer
(sleep 1; find .) |
    go run -tags tcell main.go --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 17:09:00 +09:00
Junegunn Choi
2bd41f1330 Reduce flicking when changing the size of the preview window with --border
(sleep 1; find .) |
    fzf --bind 'space:change-preview-window(60%|70%|80%|90%|border-left|border-right|border-vertical|border-top|border-horizontal|border-bottom|border-sharp|border-double|border-block|hidden|left|up|down|right|up|down|)' \
        --preview 'cat {}' --color bg:red,preview-bg:blue \
        --border --margin 3
2024-05-05 16:49:30 +09:00
Junegunn Choi
c37cd11ca5 Remove unnecessary flicking when changing the size of the preview window
fzf --bind 'space:change-preview-window(60%|70%|80%|90%|hidden|)' --preview 'cat {}'
2024-05-05 11:10:54 +09:00
Junegunn Choi
9dee8edc0c Clear characters on 1-column margin after the preview window on the left 2024-05-05 11:06:50 +09:00
Junegunn Choi
f6aa28c380 Fix --info inline-right not properly clearing the previous output
(seq 100000; sleep 1) | fzf --info inline-right --bind load:change-query:x
2024-05-03 12:18:34 +09:00
cyqsimon
dba1644518 Fix unreliable GOOS detection (#3763)
Co-authored-by: Junegunn Choi <junegunn.c@gmail.com>
2024-05-02 20:52:27 +09:00
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
56 changed files with 1984 additions and 1302 deletions

View File

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

View File

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

View File

@@ -1,6 +1,64 @@
CHANGELOG
=========
0.52.0
------
- Added `--highlight-line` to highlight the whole current line (à la `set cursorline` of Vim)
- Added color names for selected lines: `selected-fg`, `selected-bg`, and `selected-hl`
```sh
fzf --border --multi --info inline-right --layout reverse --marker ▏ --pointer ▌ --prompt '▌ ' \
--highlight-line --color gutter:-1,selected-bg:238,selected-fg:146,current-fg:189
```
- Added `click-header` event that is triggered when the header section is clicked. When the event is triggered, `$FZF_CLICK_HEADER_COLUMN` and `$FZF_CLICK_HEADER_LINE` are set.
```sh
fd --type f |
fzf --header $'[Files] [Directories]' --header-first \
--bind 'click-header:transform:
(( FZF_CLICK_HEADER_COLUMN <= 7 )) && echo "reload(fd --type f)"
(( FZF_CLICK_HEADER_COLUMN >= 9 )) && echo "reload(fd --type d)"
'
```
- Add `$FZF_COMPLETION_{DIR,PATH}_OPTS` for separately customizing the behavior of fuzzy completion
```sh
# Set --walker options without 'follow' not to follow symbolic links
FZF_COMPLETION_PATH_OPTS="--walker=file,dir,hidden"
FZF_COMPLETION_DIR_OPTS="--walker=dir,hidden"
```
- Fixed Windows argument escaping
- Bug fixes and improvements
- The code was heavily refactored to allow using fzf as a library in Go programs. The API is still experimental and subject to change.
- https://gist.github.com/junegunn/193990b65be48a38aac6ac49d5669170
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
------
- 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 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 '. ~/.bashrc' >> ~/.bash_profile

View File

@@ -1,6 +1,6 @@
SHELL := bash
GO ?= go
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
GOOS ?= $(shell $(GO) env GOOS)
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
ROOT_DIR := $(shell dirname $(MAKEFILE))
@@ -25,7 +25,7 @@ endif
ifeq ($(REVISION),)
$(error Not on git repository; cannot determine $$FZF_REVISION)
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
BINARY64 := fzf-$(GOOS)_amd64
@@ -79,7 +79,6 @@ all: target/$(BINARY)
test: $(SOURCES)
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
github.com/junegunn/fzf \
github.com/junegunn/fzf/src \
github.com/junegunn/fzf/src/algo \
github.com/junegunn/fzf/src/tui \
@@ -174,12 +173,12 @@ bin/fzf: target/$(BINARY) | bin
cp -f target/$(BINARY) bin/fzf
docker:
docker build -t fzf-arch .
docker run -it fzf-arch tmux
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu tmux
docker-test:
docker build -t fzf-arch .
docker run -it fzf-arch
docker build -t fzf-ubuntu .
docker run -it fzf-ubuntu
update:
$(GO) get -u

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@ fail() {
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'
args=()

4
go.mod
View File

@@ -6,8 +6,8 @@ require (
github.com/mattn/go-isatty v0.0.20
github.com/mattn/go-shellwords v1.0.12
github.com/rivo/uniseg v0.4.7
golang.org/x/sys v0.19.0
golang.org/x/term v0.19.0
golang.org/x/sys v0.20.0
golang.org/x/term v0.20.0
)
require (

8
go.sum
View File

@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
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.50.0
version=0.52.0
auto_completion=
key_bindings=
update_config=2
@@ -115,7 +115,7 @@ link_fzf_in_path() {
try_curl() {
command -v curl > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
curl -fL $1 | tar -xzf -
curl -fL $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
@@ -125,7 +125,7 @@ try_curl() {
try_wget() {
command -v wget > /dev/null &&
if [[ $1 =~ tar.gz$ ]]; then
wget -O - $1 | tar -xzf -
wget -O - $1 | tar --no-same-owner -xzf -
else
local temp=${TMPDIR:-/tmp}/fzf.zip
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"

View File

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

35
main.go
View File

@@ -3,14 +3,15 @@ package main
import (
_ "embed"
"fmt"
"os"
"strings"
fzf "github.com/junegunn/fzf/src"
"github.com/junegunn/fzf/src/protector"
)
var version string = "0.50"
var revision string = "devel"
var version = "0.52"
var revision = "devel"
//go:embed shell/key-bindings.bash
var bashKeyBindings []byte
@@ -33,9 +34,21 @@ func printScript(label string, content []byte) {
fmt.Println("### end: " + label + " ###")
}
func exit(code int, err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err.Error())
}
os.Exit(code)
}
func main() {
protector.Protect()
options := fzf.ParseOptions()
options, err := fzf.ParseOptions(true, os.Args[1:])
if err != nil {
exit(fzf.ExitError, err)
return
}
if options.Bash {
printScript("key-bindings.bash", bashKeyBindings)
printScript("completion.bash", bashCompletion)
@@ -51,5 +64,19 @@ func main() {
fmt.Println("fzf_key_bindings")
return
}
fzf.Run(options, version, revision)
if options.Help {
fmt.Print(fzf.Usage)
return
}
if options.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
return
}
code, err := fzf.Run(options)
exit(code, err)
}

View File

@@ -1,158 +0,0 @@
package main
import (
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
func loadPackages(t *testing.T) []*build.Package {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
var pkgs []*build.Package
seen := make(map[string]bool)
err = filepath.WalkDir(wd, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
name := d.Name()
if d.IsDir() {
if name == "" || name[0] == '.' || name[0] == '_' || name == "vendor" || name == "tmp" {
return filepath.SkipDir
}
return nil
}
if d.Type().IsRegular() && filepath.Ext(name) == ".go" && !strings.HasSuffix(name, "_test.go") {
dir := filepath.Dir(path)
if !seen[dir] {
pkg, err := build.ImportDir(dir, build.ImportComment)
if err != nil {
return fmt.Errorf("%s: %s", dir, err)
}
if pkg.ImportPath == "" || pkg.ImportPath == "." {
importPath, err := filepath.Rel(wd, dir)
if err != nil {
t.Fatal(err)
}
pkg.ImportPath = filepath.ToSlash(filepath.Join("github.com/junegunn/fzf", importPath))
}
pkgs = append(pkgs, pkg)
seen[dir] = true
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
sort.Slice(pkgs, func(i, j int) bool {
return pkgs[i].ImportPath < pkgs[j].ImportPath
})
return pkgs
}
var sourceImporter = importer.ForCompiler(token.NewFileSet(), "source", nil)
func checkPackageForOsExit(t *testing.T, bpkg *build.Package, allowed map[string]int) (errOsExit bool) {
var files []*ast.File
fset := token.NewFileSet()
for _, name := range bpkg.GoFiles {
filename := filepath.Join(bpkg.Dir, name)
af, err := parser.ParseFile(fset, filename, nil, parser.ParseComments)
if err != nil {
t.Fatal(err)
}
files = append(files, af)
}
info := types.Info{
Uses: make(map[*ast.Ident]types.Object),
}
conf := types.Config{
Importer: sourceImporter,
}
_, err := conf.Check(bpkg.Name, fset, files, &info)
if err != nil {
t.Fatal(err)
}
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for id, obj := range info.Uses {
if obj.Pkg() != nil && obj.Pkg().Name() == "os" && obj.Name() == "Exit" {
pos := fset.Position(id.Pos())
name, err := filepath.Rel(wd, pos.Filename)
if err != nil {
t.Log(err)
name = pos.Filename
}
name = filepath.ToSlash(name)
// Check if the usage is allowed
if allowed[name] > 0 {
allowed[name]--
continue
}
t.Errorf("os.Exit referenced at: %s:%d:%d", name, pos.Line, pos.Column)
errOsExit = true
}
}
return errOsExit
}
// Enforce that src/util.Exit() is used instead of os.Exit by prohibiting
// references to it anywhere else in the fzf code base.
func TestOSExitNotAllowed(t *testing.T) {
if testing.Short() {
t.Skip("skipping: short test")
}
allowed := map[string]int{
"src/util/atexit.go": 1, // os.Exit allowed 1 time in "atexit.go"
}
var errOsExit bool
for _, pkg := range loadPackages(t) {
t.Run(pkg.ImportPath, func(t *testing.T) {
if checkPackageForOsExit(t, pkg, allowed) {
errOsExit = true
}
})
}
if t.Failed() && errOsExit {
var names []string
for name := range allowed {
names = append(names, fmt.Sprintf("%q", name))
}
sort.Strings(names)
const errMsg = `
Test failed because os.Exit was referenced outside of the following files:
%s
Use github.com/junegunn/fzf/src/util.Exit() instead to exit the program.
This is enforced because calling os.Exit() prevents the functions
registered with util.AtExit() from running.`
t.Errorf(errMsg, strings.Join(names, "\n "))
}
}

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 2024" "fzf 0.50.0" "fzf-tmux - open fzf in tmux split pane"
.TH fzf-tmux 1 "May 2024" "fzf 0.52.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 2024" "fzf 0.50.0" "fzf - a command-line fuzzy finder"
.TH fzf 1 "May 2024" "fzf 0.52.0" "fzf - a command-line fuzzy finder"
.SH NAME
fzf - a command-line fuzzy finder
@@ -464,14 +464,17 @@ color mappings.
.B COLOR NAMES:
\fBfg \fRText
\fBselected-fg \fRSelected line text
\fBpreview-fg \fRPreview window text
\fBbg \fRBackground
\fBselected-bg \fRSelected line background
\fBpreview-bg \fRPreview window background
\fBhl \fRHighlighted substrings
\fBfg+ \fRText (current line)
\fBbg+ \fRBackground (current line)
\fBselected-hl \fRHighlighted substrings in the selected line
\fBcurrent-fg (fg+) \fRText (current line)
\fBcurrent-bg (bg+) \fRBackground (current line)
\fBgutter \fRGutter on the left
\fBhl+ \fRHighlighted substrings (current line)
\fBcurrent-hl (hl+) \fRHighlighted substrings (current line)
\fBquery \fRQuery string
\fBdisabled \fRQuery string when search is disabled (\fB--disabled\fR)
\fBinfo \fRInfo line (match counters)
@@ -534,6 +537,9 @@ color mappings.
--color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
.RE
.TP
.B "--highlight-line"
Highlight the whole current line
.TP
.B "--no-bold"
Do not use bold text
.TP
@@ -812,12 +818,22 @@ e.g.
.TP
.B "--sync"
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
e.g. \fBfzf --multi | fzf --sync\fR
.RE
.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 /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]"
Start HTTP server and listen on the given address. It allows external processes
to send actions to perform via POST method.
@@ -932,6 +948,8 @@ you need to protect against DNS rebinding and privilege escalation attacks.
.br
.BR 2 " Error"
.br
.BR 127 " Invalid shell command for \fBbecome\fR action"
.br
.BR 130 " Interrupted with \fBCTRL-C\fR or \fBESC\fR"
.SH FIELD INDEX EXPRESSION
@@ -972,6 +990,8 @@ fzf exports the following environment variables to its child processes.
.br
.BR FZF_SELECT_COUNT " Number of selected items"
.br
.BR FZF_POS " Vertical position of the cursor in the list starting from 1"
.br
.BR FZF_QUERY " Current query string"
.br
.BR FZF_PROMPT " Prompt string"
@@ -1264,6 +1284,15 @@ e.g.
\fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR
.RE
\fIclick-header\fR
.RS
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
e.g.
\fBprintf "head1\\nhead2" | fzf --header-lines=2 --bind 'click-header:transform-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR
.RE
.SS AVAILABLE ACTIONS:
A key or an event can be bound to one or more of the following actions.
@@ -1282,6 +1311,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)
\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-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-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 '|')
@@ -1439,8 +1470,6 @@ call.
\fBfzf --bind "enter:become(vim {})"\fR
\fBbecome(...)\fR is not supported on Windows.
.SS RELOAD INPUT
\fBreload(...)\fR action is used to dynamically update the input list

View File

@@ -59,12 +59,9 @@ if s:is_win
return iconv(a:str, &encoding, 'cp'.s:codepage)
endfunction
function! s:wrap_cmds(cmds)
return map([
\ '@echo off',
\ 'setlocal enabledelayedexpansion']
return map(['@echo off']
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
\ + ['endlocal'],
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
\ '<SID>enc_to_cp(v:val."\r")')
endfunction
else
@@ -84,11 +81,21 @@ else
endif
function! s:shellesc_cmd(arg)
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
let escaped = substitute(escaped, '%', '%%', 'g')
let escaped = substitute(escaped, '"', '\\^&', 'g')
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
let e = '"'
let slashes = 0
for c in split(a:arg, '\zs')
if c ==# '\'
let slashes += 1
elseif c ==# '"'
let e .= repeat('\', slashes + 1)
let slashes = 0
else
let slashes = 0
endif
let e .= c
endfor
let e .= repeat('\', slashes) .'"'
return e
endfunction
function! fzf#shellescape(arg, ...)
@@ -501,19 +508,19 @@ try
endif
if has_key(dict, 'source')
let source = remove(dict, 'source')
let source = dict.source
let type = type(source)
if type == 1
let source_command = source
let prefix = '('.source.')|'
elseif type == 3
let temps.input = s:fzf_tempname()
call s:writefile(source, temps.input)
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
else
throw 'Invalid source type'
endif
else
let source_command = ''
let prefix = ''
endif
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
@@ -537,11 +544,7 @@ try
endif
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
let prev_default_command = $FZF_DEFAULT_COMMAND
if len(source_command)
let $FZF_DEFAULT_COMMAND = source_command
endif
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
if use_term
return s:execute_term(dict, command, temps)
@@ -552,14 +555,6 @@ try
call s:callback(dict, lines)
return lines
finally
if exists('source_command') && len(source_command)
if len(prev_default_command)
let $FZF_DEFAULT_COMMAND = prev_default_command
else
let $FZF_DEFAULT_COMMAND = ''
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
endif
endif
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
endtry
endfunction
@@ -589,8 +584,8 @@ function! s:fzf_tmux(dict)
endif
endfor
endif
return printf('LINES=%d COLUMNS=%d %s %s - --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size)
return printf('LINES=%d COLUMNS=%d %s %s %s --',
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
endfunction
function! s:splittable(dict)
@@ -720,7 +715,8 @@ function! s:execute(dict, command, use_height, temps) abort
let a:temps.shellscript = shellscript
endif
if a:use_height
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command))
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
else
execute 'silent !'.command
endif

View File

@@ -4,10 +4,12 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.bash
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
if [[ $- =~ i ]]; then
@@ -32,6 +34,14 @@ if [[ $- =~ i ]]; then
# To redraw line after fzf closes (printf '\e[5n')
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() {
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
_fzf_comprun "$@"
@@ -92,128 +102,77 @@ _fzf_opts_completion() {
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="
-h --help
-x --extended
-e --exact
--extended-exact
+x --no-extended
+e --no-exact
-q --query
-f --filter
--literal
--no-literal
--algo
--scheme
--expect
--no-expect
--enabled --no-phony
--disabled --phony
--disabled
--tiebreak
--bind
--color
--toggle-sort
-d --delimiter
-n --nth
--with-nth
-s --sort
+s --no-sort
--track
--no-track
--tac
--no-tac
-i
+i
-m --multi
+m --no-multi
--ansi
--no-ansi
--no-mouse
+c --no-color
+2 --no-256
--black
--no-black
--bold
--no-bold
--layout
--reverse
--no-reverse
--cycle
--no-cycle
--keep-right
--no-keep-right
--hscroll
--no-hscroll
--hscroll-off
--scroll-off
--filepath-word
--no-filepath-word
--info
--no-info
--inline-info
--no-inline-info
--separator
--no-separator
--scrollbar
--no-scrollbar
--jump-labels
-1 --select-1
+1 --no-select-1
-0 --exit-0
+0 --no-exit-0
--read0
--no-read0
--print0
--no-print0
--print-query
--no-print-query
--prompt
--pointer
--marker
--sync
--no-sync
--async
--no-history
--history
--history-size
--no-header
--no-header-lines
--header
--header-lines
--header-first
--no-header-first
--ellipsis
--preview
--no-preview
--preview-window
--height
--min-height
--no-height
--no-margin
--no-padding
--no-border
--border
--no-border-label
--border-label
--border-label-pos
--no-preview-label
--preview-label
--preview-label-pos
--no-unicode
--unicode
--margin
--padding
--tabstop
--listen
--no-listen
--clear
--no-clear
--version
--"
case "${prev}" in
--algo)
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
return 0
;;
--scheme)
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
return 0
@@ -335,13 +294,19 @@ __fzf_generic_path_completion() {
[[ -z "$dir" ]] && dir='.'
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
matches=$(
unset FZF_DEFAULT_COMMAND
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-} $2"
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -F "$1" > /dev/null; then
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
else
[[ $1 =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir"
if [[ $1 =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
fi | while read -r item; do
printf "%q " "${item%$3}$3"
done
@@ -399,7 +364,10 @@ _fzf_complete() {
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
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"
if [[ -n "$selected" ]]; then
COMPREPLY=("$selected")
@@ -503,8 +471,11 @@ complete -o default -F _fzf_opts_completion fzf
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
complete -o default -F _fzf_opts_completion fzf-tmux
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
a_cmds="
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
# 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
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
javac ld less more mvim nvim patch perl python ruby
@@ -512,10 +483,11 @@ a_cmds="
basename bunzip2 bzip2 chmod chown curl cp dirname du
find git grep gunzip gzip hg jar
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
__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
# _comp_load was added in bash-completion 2.12 to replace _completion_loader.
@@ -551,10 +523,21 @@ for cmd in $d_cmds; do
__fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
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
__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() {
local kind fn cmd
@@ -576,10 +559,4 @@ _fzf_setup_completion() {
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

View File

@@ -4,12 +4,12 @@
# / __/ / /_/ __/
# /_/ /___/_/ completion.zsh
#
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: '-d 40%')
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
if [[ -o interactive ]]; then
# - $FZF_TMUX (default: 0)
# - $FZF_TMUX_OPTS (default: empty)
# - $FZF_COMPLETION_TRIGGER (default: '**')
# - $FZF_COMPLETION_OPTS (default: empty)
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
# Both branches of the following `if` do the same thing -- define
@@ -75,6 +75,9 @@ fi
# 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.
{
# 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}
#
@@ -93,6 +96,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() {
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
_fzf_comprun "$@"
@@ -146,13 +157,19 @@ __fzf_generic_path_completion() {
[ -z "$dir" ] && dir='.'
[ "$dir" != "/" ] && dir="${dir/%\//}"
matches=$(
unset FZF_DEFAULT_COMMAND
export FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}"
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
if declare -f "$compgen" > /dev/null; then
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
else
[[ $compgen =~ dir ]] && walker=dir,follow || walker=file,dir,follow,hidden
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" < /dev/tty
if [[ $compgen =~ dir ]]; then
walker=dir,follow
rest=${FZF_COMPLETION_DIR_OPTS-}
else
walker=file,dir,follow,hidden
rest=${FZF_COMPLETION_PATH_OPTS-}
fi
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
fi | while read item; do
item="${item%$suffix}$suffix"
echo -n "${(q)item} "
@@ -217,7 +234,10 @@ _fzf_complete() {
type $post > /dev/null 2>&1 || post=cat
_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
LBUFFER="$lbuf$matches"
fi
@@ -315,7 +335,7 @@ fzf-completion() {
# Trigger sequence given
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}}
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
@@ -345,11 +365,10 @@ fzf-completion() {
zle -N fzf-completion
bindkey '^I' fzf-completion
fi
} always {
# Restore the original options.
eval $__fzf_completion_options
'unset' '__fzf_completion_options'
}
fi

View File

@@ -16,10 +16,19 @@ if [[ $- =~ i ]]; then
# 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__() {
local opts
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_COMMAND=${FZF_CTRL_T_COMMAND:-} FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
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) "$@" |
while read -r item; do
printf '%q ' "$item" # escape special chars
done
@@ -37,23 +46,24 @@ fzf-file-widget() {
}
__fzf_cd__() {
local opts 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"
local 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)"
}
if command -v perl > /dev/null; then
__fzf_history__() {
local output opts 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"
local output script
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
output=$(
set +o pipefail
builtin fc -lnr -2147483648 |
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
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then
@@ -64,14 +74,13 @@ if command -v perl > /dev/null; then
}
else # awk - fallback for POSIX systems
__fzf_history__() {
local output opts script n x y z d
local output script n x y z d
if [[ -z $__fzf_awk ]]; then
__fzf_awk=awk
# 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)
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
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
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 }
@@ -82,7 +91,8 @@ else # awk - fallback for POSIX systems
set +o pipefail
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
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
READLINE_LINE=${output#*$'\t'}
if [[ -z "$READLINE_POINT" ]]; then

View File

@@ -18,6 +18,15 @@ status is-interactive; or exit 0
# ------------
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
function fzf-file-widget -d "List files and folders"
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%
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_OPTS_FILE ''
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
end
if [ -z "$result" ]
@@ -49,7 +59,8 @@ function fzf_key_bindings
function fzf-history-widget -d "Show command history"
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
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_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%
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"
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result

View File

@@ -11,8 +11,6 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
if [[ -o interactive ]]; then
# Key bindings
# ------------
@@ -38,12 +36,23 @@ fi
'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
__fsel() {
__fzf_select() {
setopt localoptions pipefail no_aliases 2> /dev/null
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} "
done
local ret=$?
@@ -57,7 +66,7 @@ __fzfcmd() {
}
fzf-file-widget() {
LBUFFER="${LBUFFER}$(__fsel)"
LBUFFER="${LBUFFER}$(__fzf_select)"
local ret=$?
zle reset-prompt
return $ret
@@ -72,7 +81,10 @@ fi
# ALT-C - cd into the selected directory
fzf-cd-widget() {
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
zle redisplay
return 0
@@ -97,7 +109,8 @@ fzf-history-widget() {
local selected num
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 }' |
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=$?
if [ -n "$selected" ]; then
num=$(awk '{print $1}' <<< "$selected")
@@ -114,10 +127,9 @@ zle -N fzf-history-widget
bindkey -M emacs '^R' fzf-history-widget
bindkey -M vicmd '^R' fzf-history-widget
bindkey -M viins '^R' fzf-history-widget
fi
} always {
eval $__fzf_key_bindings_options
'unset' '__fzf_key_bindings_options'
}
fi

View File

@@ -26,102 +26,104 @@ func _() {
_ = x[actCancel-15]
_ = x[actChangeBorderLabel-16]
_ = x[actChangeHeader-17]
_ = x[actChangePreviewLabel-18]
_ = x[actChangePrompt-19]
_ = x[actChangeQuery-20]
_ = x[actClearScreen-21]
_ = x[actClearQuery-22]
_ = x[actClearSelection-23]
_ = x[actClose-24]
_ = x[actDeleteChar-25]
_ = x[actDeleteCharEof-26]
_ = x[actEndOfLine-27]
_ = x[actForwardChar-28]
_ = x[actForwardWord-29]
_ = x[actKillLine-30]
_ = x[actKillWord-31]
_ = x[actUnixLineDiscard-32]
_ = x[actUnixWordRubout-33]
_ = x[actYank-34]
_ = x[actBackwardKillWord-35]
_ = x[actSelectAll-36]
_ = x[actDeselectAll-37]
_ = x[actToggle-38]
_ = x[actToggleSearch-39]
_ = x[actToggleAll-40]
_ = x[actToggleDown-41]
_ = x[actToggleUp-42]
_ = x[actToggleIn-43]
_ = x[actToggleOut-44]
_ = x[actToggleTrack-45]
_ = x[actToggleTrackCurrent-46]
_ = x[actToggleHeader-47]
_ = x[actTrackCurrent-48]
_ = x[actUntrackCurrent-49]
_ = x[actDown-50]
_ = x[actUp-51]
_ = x[actPageUp-52]
_ = x[actPageDown-53]
_ = x[actPosition-54]
_ = x[actHalfPageUp-55]
_ = x[actHalfPageDown-56]
_ = x[actOffsetUp-57]
_ = x[actOffsetDown-58]
_ = x[actJump-59]
_ = x[actJumpAccept-60]
_ = x[actPrintQuery-61]
_ = x[actRefreshPreview-62]
_ = x[actReplaceQuery-63]
_ = x[actToggleSort-64]
_ = x[actShowPreview-65]
_ = x[actHidePreview-66]
_ = x[actTogglePreview-67]
_ = x[actTogglePreviewWrap-68]
_ = x[actTransform-69]
_ = x[actTransformBorderLabel-70]
_ = x[actTransformHeader-71]
_ = x[actTransformPreviewLabel-72]
_ = x[actTransformPrompt-73]
_ = x[actTransformQuery-74]
_ = x[actPreview-75]
_ = x[actChangePreview-76]
_ = x[actChangePreviewWindow-77]
_ = x[actPreviewTop-78]
_ = x[actPreviewBottom-79]
_ = x[actPreviewUp-80]
_ = x[actPreviewDown-81]
_ = x[actPreviewPageUp-82]
_ = x[actPreviewPageDown-83]
_ = x[actPreviewHalfPageUp-84]
_ = x[actPreviewHalfPageDown-85]
_ = x[actPrevHistory-86]
_ = x[actPrevSelected-87]
_ = x[actPut-88]
_ = x[actNextHistory-89]
_ = x[actNextSelected-90]
_ = x[actExecute-91]
_ = x[actExecuteSilent-92]
_ = x[actExecuteMulti-93]
_ = x[actSigStop-94]
_ = x[actFirst-95]
_ = x[actLast-96]
_ = x[actReload-97]
_ = x[actReloadSync-98]
_ = x[actDisableSearch-99]
_ = x[actEnableSearch-100]
_ = x[actSelect-101]
_ = x[actDeselect-102]
_ = x[actUnbind-103]
_ = x[actRebind-104]
_ = x[actBecome-105]
_ = x[actResponse-106]
_ = x[actShowHeader-107]
_ = x[actHideHeader-108]
_ = x[actChangeMulti-18]
_ = x[actChangePreviewLabel-19]
_ = x[actChangePrompt-20]
_ = x[actChangeQuery-21]
_ = x[actClearScreen-22]
_ = x[actClearQuery-23]
_ = x[actClearSelection-24]
_ = x[actClose-25]
_ = x[actDeleteChar-26]
_ = x[actDeleteCharEof-27]
_ = x[actEndOfLine-28]
_ = x[actFatal-29]
_ = x[actForwardChar-30]
_ = x[actForwardWord-31]
_ = x[actKillLine-32]
_ = x[actKillWord-33]
_ = x[actUnixLineDiscard-34]
_ = x[actUnixWordRubout-35]
_ = x[actYank-36]
_ = x[actBackwardKillWord-37]
_ = x[actSelectAll-38]
_ = x[actDeselectAll-39]
_ = x[actToggle-40]
_ = x[actToggleSearch-41]
_ = x[actToggleAll-42]
_ = x[actToggleDown-43]
_ = x[actToggleUp-44]
_ = x[actToggleIn-45]
_ = x[actToggleOut-46]
_ = x[actToggleTrack-47]
_ = x[actToggleTrackCurrent-48]
_ = x[actToggleHeader-49]
_ = x[actTrackCurrent-50]
_ = x[actUntrackCurrent-51]
_ = x[actDown-52]
_ = x[actUp-53]
_ = x[actPageUp-54]
_ = x[actPageDown-55]
_ = x[actPosition-56]
_ = x[actHalfPageUp-57]
_ = x[actHalfPageDown-58]
_ = x[actOffsetUp-59]
_ = x[actOffsetDown-60]
_ = x[actJump-61]
_ = x[actJumpAccept-62]
_ = x[actPrintQuery-63]
_ = x[actRefreshPreview-64]
_ = x[actReplaceQuery-65]
_ = x[actToggleSort-66]
_ = x[actShowPreview-67]
_ = x[actHidePreview-68]
_ = x[actTogglePreview-69]
_ = x[actTogglePreviewWrap-70]
_ = x[actTransform-71]
_ = x[actTransformBorderLabel-72]
_ = x[actTransformHeader-73]
_ = x[actTransformPreviewLabel-74]
_ = x[actTransformPrompt-75]
_ = x[actTransformQuery-76]
_ = x[actPreview-77]
_ = x[actChangePreview-78]
_ = x[actChangePreviewWindow-79]
_ = x[actPreviewTop-80]
_ = x[actPreviewBottom-81]
_ = x[actPreviewUp-82]
_ = x[actPreviewDown-83]
_ = x[actPreviewPageUp-84]
_ = x[actPreviewPageDown-85]
_ = x[actPreviewHalfPageUp-86]
_ = x[actPreviewHalfPageDown-87]
_ = x[actPrevHistory-88]
_ = x[actPrevSelected-89]
_ = x[actPut-90]
_ = x[actNextHistory-91]
_ = x[actNextSelected-92]
_ = x[actExecute-93]
_ = x[actExecuteSilent-94]
_ = x[actExecuteMulti-95]
_ = x[actSigStop-96]
_ = x[actFirst-97]
_ = x[actLast-98]
_ = x[actReload-99]
_ = x[actReloadSync-100]
_ = x[actDisableSearch-101]
_ = x[actEnableSearch-102]
_ = x[actSelect-103]
_ = x[actDeselect-104]
_ = x[actUnbind-105]
_ = x[actRebind-106]
_ = x[actBecome-107]
_ = x[actResponse-108]
_ = x[actShowHeader-109]
_ = x[actHideHeader-110]
}
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
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, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
func (i actionType) String() string {
if i < 0 || i >= actionType(len(_actionType_index)-1) {

View File

@@ -152,7 +152,7 @@ var (
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
bonusBoundaryDelimiter int16 = bonusBoundary + 1
initialCharClass charClass = charWhite
initialCharClass = charWhite
// A minor optimization that can give 15%+ performance boost
asciiCharClasses [unicode.MaxASCII + 1]charClass

View File

@@ -3,7 +3,7 @@
package algo
var normalized map[rune]rune = map[rune]rune{
var normalized = map[rune]rune{
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER

View File

@@ -292,7 +292,7 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
var remaining string
i := -1
var i int
if delimiter == 0 {
// Faster than strings.IndexAny(";:")
i = strings.IndexByte(s, ';')
@@ -312,7 +312,7 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
// Inlined version of strconv.Atoi() that only handles positive
// integers and does not allocate on error.
code := 0
for _, ch := range sbytes(s) {
for _, ch := range stringBytes(s) {
ch -= '0'
if ch > 9 {
return -1, delimiter, remaining
@@ -350,7 +350,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
state256 := 0
ptr := &state.fg
var delimiter byte = 0
var delimiter byte
count := 0
for len(ansiCode) != 0 {
var num int

View File

@@ -12,8 +12,14 @@ type ChunkCache struct {
}
// NewChunkCache returns a new ChunkCache
func NewChunkCache() ChunkCache {
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
func NewChunkCache() *ChunkCache {
return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
}
func (cc *ChunkCache) Clear() {
cc.mutex.Lock()
cc.cache = make(map[*Chunk]*queryCache)
cc.mutex.Unlock()
}
// Add adds the list to the cache

View File

@@ -67,9 +67,9 @@ const (
)
const (
exitCancel = -1
exitOk = 0
exitNoMatch = 1
exitError = 2
exitInterrupt = 130
ExitCancel = -1
ExitOk = 0
ExitNoMatch = 1
ExitError = 2
ExitInterrupt = 130
)

View File

@@ -2,10 +2,8 @@
package fzf
import (
"fmt"
"sync"
"time"
"unsafe"
"github.com/junegunn/fzf/src/util"
)
@@ -19,30 +17,24 @@ Matcher -> EvtSearchFin -> Terminal (update list)
Matcher -> EvtHeader -> Terminal (update header)
*/
func ustring(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}
func sbytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
// Run starts fzf
func Run(opts *Options, version string, revision string) {
func Run(opts *Options) (int, error) {
if err := postProcessOptions(opts); err != nil {
return ExitError, err
}
defer util.RunAtExitFuncs()
// Output channel given
if opts.Output != nil {
opts.Printer = func(str string) {
opts.Output <- str
}
}
sort := opts.Sort > 0
sortCriteria = opts.Criteria
if opts.Version {
if len(revision) > 0 {
fmt.Printf("%s (%s)\n", version, revision)
} else {
fmt.Println(version)
}
util.Exit(exitOk)
}
// Event channel
eventBox := util.NewEventBox()
@@ -56,16 +48,16 @@ func Run(opts *Options, version string, revision string) {
if opts.Theme.Colored {
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
prevLineAnsiState = lineAnsiState
trimmed, offsets, newState := extractColor(ustring(data), lineAnsiState, nil)
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
lineAnsiState = newState
return util.ToChars(sbytes(trimmed)), offsets
return util.ToChars(stringBytes(trimmed)), offsets
}
} else {
// When color is disabled but ansi option is given,
// we simply strip out ANSI codes from the input
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
trimmed, _, _ := extractColor(ustring(data), nil, nil)
return util.ToChars(sbytes(trimmed)), nil
trimmed, _, _ := extractColor(byteString(data), nil, nil)
return util.ToChars(stringBytes(trimmed)), nil
}
}
}
@@ -77,7 +69,7 @@ func Run(opts *Options, version string, revision string) {
if len(opts.WithNth) == 0 {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
if len(header) < opts.HeaderLines {
header = append(header, ustring(data))
header = append(header, byteString(data))
eventBox.Set(EvtHeader, header)
return false
}
@@ -88,7 +80,7 @@ func Run(opts *Options, version string, revision string) {
})
} else {
chunkList = NewChunkList(func(item *Item, data []byte) bool {
tokens := Tokenize(ustring(data), opts.Delimiter)
tokens := Tokenize(byteString(data), opts.Delimiter)
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
var ansiState *ansiState
if prevLineAnsiState != nil {
@@ -112,7 +104,7 @@ func Run(opts *Options, version string, revision string) {
eventBox.Set(EvtHeader, header)
return false
}
item.text, item.colors = ansiProcessor(sbytes(transformed))
item.text, item.colors = ansiProcessor(stringBytes(transformed))
item.text.TrimTrailingWhitespaces()
item.text.Index = itemIndex
item.origText = &data
@@ -121,14 +113,17 @@ func Run(opts *Options, version string, revision string) {
})
}
// Process executor
executor := util.NewExecutor(opts.WithShell)
// Reader
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
var reader *Reader
if !streamingFilter {
reader = NewReader(func(data []byte) bool {
return chunkList.Push(data)
}, eventBox, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}
// Matcher
@@ -144,14 +139,16 @@ func Run(opts *Options, version string, revision string) {
forward = true
}
}
cache := NewChunkCache()
patternCache := make(map[string]*Pattern)
patternBuilder := func(runes []rune) *Pattern {
return BuildPattern(
return BuildPattern(cache, patternCache,
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
}
inputRevision := 0
snapshotRevision := 0
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
// Filtering mode
if opts.Filter != nil {
@@ -178,8 +175,8 @@ func Run(opts *Options, version string, revision string) {
mutex.Unlock()
}
return false
}, eventBox, opts.ReadZero, false)
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
}, eventBox, executor, opts.ReadZero, false)
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
} else {
eventBox.Unwatch(EvtReadNew)
eventBox.WaitFor(EvtReadFin)
@@ -194,9 +191,9 @@ func Run(opts *Options, version string, revision string) {
}
}
if found {
util.Exit(exitOk)
return ExitOk, nil
}
util.Exit(exitNoMatch)
return ExitNoMatch, nil
}
// Synchronous search
@@ -207,9 +204,13 @@ func Run(opts *Options, version string, revision string) {
// Go interactive
go matcher.Loop()
defer matcher.Stop()
// Terminal I/O
terminal := NewTerminal(opts, eventBox)
terminal, err := NewTerminal(opts, eventBox, executor)
if err != nil {
return ExitError, err
}
maxFit := 0 // Maximum number of items that can fit on screen
padHeight := 0
heightUnknown := opts.Height.auto
@@ -226,7 +227,7 @@ func Run(opts *Options, version string, revision string) {
// Event coordination
reading := true
ticks := 0
var nextCommand *string
var nextCommand *commandSpec
var nextEnviron []string
eventBox.Watch(EvtReadNew)
total := 0
@@ -247,7 +248,7 @@ func Run(opts *Options, version string, revision string) {
useSnapshot := false
var snapshot []*Chunk
var count int
restart := func(command string, environ []string) {
restart := func(command commandSpec, environ []string) {
reading = true
chunkList.Clear()
itemIndex = 0
@@ -255,6 +256,9 @@ func Run(opts *Options, version string, revision string) {
header = make([]string, 0, opts.HeaderLines)
go reader.restart(command, environ)
}
exitCode := ExitOk
stop := false
for {
delay := true
ticks++
@@ -275,7 +279,11 @@ func Run(opts *Options, version string, revision string) {
if reading {
reader.terminate()
}
util.Exit(value.(int))
quitSignal := value.(quitSignal)
exitCode = quitSignal.code
err = quitSignal.err
stop = true
return
case EvtReadNew, EvtReadFin:
if evt == EvtReadFin && nextCommand != nil {
restart(*nextCommand, nextEnviron)
@@ -298,8 +306,7 @@ func Run(opts *Options, version string, revision string) {
total = count
terminal.UpdateCount(total, !reading, value.(*string))
if opts.Sync {
opts.Sync = false
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
}
if heightUnknown && !deferred {
determine(!reading)
@@ -307,7 +314,7 @@ func Run(opts *Options, version string, revision string) {
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
case EvtSearchNew:
var command *string
var command *commandSpec
var environ []string
var changed bool
switch val := value.(type) {
@@ -376,20 +383,24 @@ func Run(opts *Options, version string, revision string) {
for i := 0; i < count; i++ {
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
}
if count > 0 {
util.Exit(exitOk)
if count == 0 {
exitCode = ExitNoMatch
}
util.Exit(exitNoMatch)
stop = true
return
}
determine(val.final)
}
}
terminal.UpdateList(val)
terminal.UpdateList(val, true)
}
}
}
events.Clear()
})
if stop {
break
}
if delay && reading {
dur := util.DurWithin(
time.Duration(ticks)*coordinatorDelayStep,
@@ -397,4 +408,5 @@ func Run(opts *Options, version string, revision string) {
time.Sleep(dur)
}
}
return exitCode, err
}

35
src/functions.go Normal file
View File

@@ -0,0 +1,35 @@
package fzf
import (
"os"
"strings"
"unsafe"
)
func writeTemporaryFile(data []string, printSep string) string {
f, err := os.CreateTemp("", "fzf-preview-*")
if err != nil {
// Unable to create temporary file
// FIXME: Should we terminate the program?
return ""
}
defer f.Close()
f.WriteString(strings.Join(data, printSep))
f.WriteString(printSep)
return f.Name()
}
func removeFiles(files []string) {
for _, filename := range files {
os.Remove(filename)
}
}
func stringBytes(data string) []byte {
return unsafe.Slice(unsafe.StringData(data), len(data))
}
func byteString(data []byte) string {
return unsafe.String(unsafe.SliceData(data), len(data))
}

View File

@@ -21,6 +21,7 @@ type MatchRequest struct {
// Matcher is responsible for performing search
type Matcher struct {
cache *ChunkCache
patternBuilder func([]rune) *Pattern
sort bool
tac bool
@@ -38,10 +39,11 @@ const (
)
// NewMatcher returns a new Matcher
func NewMatcher(patternBuilder func([]rune) *Pattern,
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
return &Matcher{
cache: cache,
patternBuilder: patternBuilder,
sort: sort,
tac: tac,
@@ -60,8 +62,13 @@ func (m *Matcher) Loop() {
for {
var request MatchRequest
stop := false
m.reqBox.Wait(func(events *util.Events) {
for _, val := range *events {
for t, val := range *events {
if t == reqQuit {
stop = true
return
}
switch val := val.(type) {
case MatchRequest:
request = val
@@ -71,12 +78,15 @@ func (m *Matcher) Loop() {
}
events.Clear()
})
if stop {
break
}
if request.sort != m.sort || request.revision != m.revision {
m.sort = request.sort
m.revision = request.revision
m.mergerCache = make(map[string]*Merger)
clearChunkCache()
m.cache.Clear()
}
// Restart search
@@ -236,3 +246,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
}
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
}
func (m *Matcher) Stop() {
m.reqBox.Set(reqQuit, nil)
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,9 +3,11 @@
package fzf
import "errors"
func (o *Options) initProfiling() error {
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
errorExit("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
return errors.New("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
}
return nil
}

View File

@@ -80,7 +80,7 @@ func TestDelimiterRegexRegexCaret(t *testing.T) {
func TestSplitNth(t *testing.T) {
{
ranges := splitNth("..")
ranges, _ := splitNth("..")
if len(ranges) != 1 ||
ranges[0].begin != rangeEllipsis ||
ranges[0].end != rangeEllipsis {
@@ -88,7 +88,7 @@ func TestSplitNth(t *testing.T) {
}
}
{
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
if len(ranges) != 10 ||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
@@ -137,7 +137,7 @@ func TestIrrelevantNth(t *testing.T) {
}
func TestParseKeys(t *testing.T) {
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
checkEvent := func(e tui.Event, s string) {
if pairs[e] != s {
t.Errorf("%s != %s", pairs[e], s)
@@ -163,7 +163,7 @@ func TestParseKeys(t *testing.T) {
checkEvent(tui.AltKey(' '), "alt-SPACE")
// Synonyms
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
if len(pairs) != 9 {
t.Error(9)
}
@@ -177,7 +177,7 @@ func TestParseKeys(t *testing.T) {
check(tui.Left, "left")
check(tui.Right, "right")
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
if len(pairs) != 11 {
t.Error(11)
}
@@ -206,40 +206,40 @@ func TestParseKeysWithComma(t *testing.T) {
}
}
pairs := parseKeyChords(",", "")
pairs, _ := parseKeyChords(",", "")
checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,a,b", "")
pairs, _ = parseKeyChords(",,a,b", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,b,,", "")
pairs, _ = parseKeyChords("a,b,,", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b", "")
pairs, _ = parseKeyChords("a,,,b", "")
checkN(len(pairs), 3)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords("a,,,b,c", "")
pairs, _ = parseKeyChords("a,,,b,c", "")
checkN(len(pairs), 4)
check(pairs, tui.Key('a'), "a")
check(pairs, tui.Key('b'), "b")
check(pairs, tui.Key('c'), "c")
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",,,", "")
pairs, _ = parseKeyChords(",,,", "")
checkN(len(pairs), 1)
check(pairs, tui.Key(','), ",")
pairs = parseKeyChords(",ALT-,,", "")
pairs, _ = parseKeyChords(",ALT-,,", "")
checkN(len(pairs), 1)
check(pairs, tui.AltKey(','), "ALT-,")
}
@@ -262,17 +262,13 @@ func TestBind(t *testing.T) {
}
}
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
errorString := ""
errorFn := func(e string) {
errorString = e
}
parseKeymap(keymap,
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
"x:Execute(foo+bar),X:execute/bar+baz/"+
",f1:+first,f1:+top"+
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn)
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
check(tui.CtrlA.AsEvent(), "", actKillLine)
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
check(tui.Key('c'), "", actPageUp)
@@ -290,20 +286,17 @@ func TestBind(t *testing.T) {
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn)
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
}
parseKeymap(keymap, "f1:abort", errorFn)
parseKeymap(keymap, "f1:abort")
check(tui.F1.AsEvent(), "", actAbort)
if len(errorString) > 0 {
t.Errorf("error parsing keymap: %s", errorString)
}
}
func TestColorSpec(t *testing.T) {
theme := tui.Dark256
dark := parseTheme(theme, "dark")
dark, _ := parseTheme(theme, "dark")
if *dark != *theme {
t.Errorf("colors should be equivalent")
}
@@ -311,7 +304,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent")
}
light := parseTheme(theme, "dark,light")
light, _ := parseTheme(theme, "dark,light")
if *light == *theme {
t.Errorf("should not be equivalent")
}
@@ -322,7 +315,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("point should not be equivalent")
}
customized := parseTheme(theme, "fg:231,bg:232")
customized, _ := parseTheme(theme, "fg:231,bg:232")
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
t.Errorf("color not customized")
}
@@ -335,7 +328,7 @@ func TestColorSpec(t *testing.T) {
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
}
customized = parseTheme(theme, "fg:231,dark,bg:232")
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
t.Errorf("color not customized")
}
@@ -475,7 +468,7 @@ func TestValidateSign(t *testing.T) {
}
func TestParseSingleActionList(t *testing.T) {
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {})
actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
if len(actions) != 4 {
t.Errorf("Invalid number of actions parsed:%d", len(actions))
}
@@ -491,11 +484,8 @@ func TestParseSingleActionList(t *testing.T) {
}
func TestParseSingleActionListError(t *testing.T) {
err := ""
parseSingleActionList("change-query(foobar)baz", func(e string) {
err = e
})
if len(err) == 0 {
_, err := parseSingleActionList("change-query(foobar)baz")
if err == nil {
t.Errorf("Failed to detect error")
}
}

View File

@@ -60,32 +60,17 @@ type Pattern struct {
delimiter Delimiter
nth []Range
procFun map[termType]algo.Algo
cache *ChunkCache
}
var (
_patternCache map[string]*Pattern
_splitRegex *regexp.Regexp
_cache ChunkCache
)
var _splitRegex *regexp.Regexp
func init() {
_splitRegex = regexp.MustCompile(" +")
clearPatternCache()
clearChunkCache()
}
func clearPatternCache() {
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
_patternCache = make(map[string]*Pattern)
}
func clearChunkCache() {
_cache = NewChunkCache()
}
// BuildPattern builds Pattern object from the given arguments
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
var asString string
@@ -98,7 +83,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
asString = string(runes)
}
cached, found := _patternCache[asString]
// We can uniquely identify the pattern for a given string since
// search mode and caseMode do not change while the program is running
cached, found := patternCache[asString]
if found {
return cached
}
@@ -153,6 +140,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
cacheable: cacheable,
nth: nth,
delimiter: delimiter,
cache: cache,
procFun: make(map[termType]algo.Algo)}
ptr.cacheKey = ptr.buildCacheKey()
@@ -162,7 +150,7 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
ptr.procFun[termPrefix] = algo.PrefixMatch
ptr.procFun[termSuffix] = algo.SuffixMatch
_patternCache[asString] = ptr
patternCache[asString] = ptr
return ptr
}
@@ -282,18 +270,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
// ChunkCache: Exact match
cacheKey := p.CacheKey()
if p.cacheable {
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
return cached
}
}
// Prefix/suffix cache
space := _cache.Search(chunk, cacheKey)
space := p.cache.Search(chunk, cacheKey)
matches := p.matchChunk(chunk, space, slab)
if p.cacheable {
_cache.Add(chunk, cacheKey, matches)
p.cache.Add(chunk, cacheKey, matches)
}
return matches
}

View File

@@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
}
}
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
withPos, cacheable, nth, delimiter, runes)
}
func TestExact(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
[]Range{}, Delimiter{}, []rune("'abc"))
chars := util.ToChars([]byte("aabbcc abc"))
res, pos := algo.ExactMatchNaive(
@@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
}
func TestEqual(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
match := func(str string, sidxExpected int, eidxExpected int) {
chars := util.ToChars([]byte(str))
@@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
}
func TestCaseSensitivity(t *testing.T) {
defer clearPatternCache()
clearPatternCache()
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
clearPatternCache()
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
clearPatternCache()
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
@@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
}
func TestOrigTextAndTransformed(t *testing.T) {
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
tokens := Tokenize("junegunn", Delimiter{})
trans := Transform(tokens, []Range{{1, 1}})
@@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
func TestCacheKey(t *testing.T) {
test := func(extended bool, patStr string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if pat.cacheable != cacheable {
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
}
clearPatternCache()
}
test(false, "foo !bar", "foo !bar", true)
test(false, "foo | bar !baz", "foo | bar !baz", true)
@@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
func TestCacheable(t *testing.T) {
test := func(fuzzy bool, str string, expected string, cacheable bool) {
clearPatternCache()
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
if pat.CacheKey() != expected {
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
}
if cacheable != pat.cacheable {
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
}
clearPatternCache()
}
test(true, "foo bar", "foo\tbar", true)
test(true, "foo 'bar", "foo\tbar", false)

View File

@@ -3,6 +3,4 @@
package protector
// Protect calls OS specific protections like pledge on OpenBSD
func Protect() {
return
}
func Protect() {}

View File

@@ -18,6 +18,7 @@ import (
// Reader reads from command or standard input
type Reader struct {
pusher func([]byte) bool
executor *util.Executor
eventBox *util.EventBox
delimNil bool
event int32
@@ -30,8 +31,8 @@ type Reader struct {
}
// NewReader returns new Reader object
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
}
func (r *Reader) startEventPoller() {
@@ -85,18 +86,34 @@ func (r *Reader) terminate() {
r.mutex.Unlock()
}
func (r *Reader) restart(command string, environ []string) {
func (r *Reader) restart(command commandSpec, environ []string) {
r.event = int32(EvtReady)
r.startEventPoller()
success := r.readFromCommand(command, environ)
success := r.readFromCommand(command.command, environ)
r.fin(success)
removeFiles(command.tempFiles)
}
func (r *Reader) readChannel(inputChan chan string) bool {
for {
item, more := <-inputChan
if !more {
break
}
if r.pusher([]byte(item)) {
atomic.StoreInt32(&r.event, int32(EvtReadNew))
}
}
return true
}
// ReadSource reads data from the default command or from standard input
func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
r.startEventPoller()
var success bool
if util.IsTty() {
if inputChan != nil {
success = r.readChannel(inputChan)
} else if util.IsTty() {
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
if len(cmd) == 0 {
success = r.readFiles(root, opts, ignores)
@@ -147,7 +164,7 @@ func (r *Reader) feed(src io.Reader) {
}
// We're not making any progress after 100 tries. Stop.
if n == 0 && err == nil {
if n == 0 {
break
}
@@ -242,7 +259,7 @@ func (r *Reader) readFromCommand(command string, environ []string) bool {
r.mutex.Lock()
r.killed = false
r.command = &command
r.exec = util.ExecCommand(command, true)
r.exec = r.executor.ExecCommand(command, true)
if environ != nil {
r.exec.Env = environ
}

View File

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

View File

@@ -73,28 +73,28 @@ func parseListenAddress(address string) (listenAddress, error) {
return listenAddress{parts[0], port}, nil
}
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (int, error) {
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
host := address.host
port := address.port
apiKey := os.Getenv("FZF_API_KEY")
if !address.IsLocal() && len(apiKey) == 0 {
return port, fmt.Errorf("FZF_API_KEY is required to allow remote access")
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
}
addrStr := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", addrStr)
if err != nil {
return port, fmt.Errorf("failed to listen on %s", addrStr)
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
}
if port == 0 {
addr := listener.Addr().String()
parts := strings.Split(addr, ":")
if len(parts) < 2 {
return port, fmt.Errorf("cannot extract port: %s", addr)
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
}
var err error
port, err = strconv.Atoi(parts[len(parts)-1])
if err != nil {
return port, err
return nil, port, err
}
}
@@ -109,18 +109,16 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
conn, err := listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) {
break
} else {
continue
return
}
continue
}
conn.Write([]byte(server.handleHttpRequest(conn)))
conn.Close()
}
listener.Close()
}()
return port, nil
return listener, port, nil
}
// Here we are writing a simplistic HTTP server without using net/http
@@ -217,12 +215,9 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
}
body = body[:contentLength]
errorMessage := ""
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
errorMessage = message
})
if len(errorMessage) > 0 {
return bad(errorMessage)
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
if err != nil {
return bad(err.Error())
}
if len(actions) == 0 {
return bad("no action specified")

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -5,26 +5,11 @@ package fzf
import (
"os"
"os/signal"
"strings"
"syscall"
"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) {
signal.Notify(resizeChan, syscall.SIGWINCH)
}
@@ -41,7 +26,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
signal.Notify(resizeChan, syscall.SIGCONT)
}
func quoteEntry(entry string) string {
return "'" + escaper.Replace(entry) + "'"
}

View File

@@ -4,8 +4,6 @@ package fzf
import (
"os"
"regexp"
"strings"
)
func notifyOnResize(resizeChan chan<- os.Signal) {
@@ -19,27 +17,3 @@ func notifyStop(p *os.Process) {
func notifyOnCont(resizeChan chan<- os.Signal) {
// 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

@@ -91,7 +91,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
prefixLength := begin
for idx := range tokens {
chars := util.ToChars(sbytes(tokens[idx]))
chars := util.ToChars(stringBytes(tokens[idx]))
ret[idx] = Token{&chars, int32(prefixLength)}
prefixLength += chars.Length()
}
@@ -187,7 +187,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
if r.begin == r.end {
idx := r.begin
if idx == rangeEllipsis {
chars := util.ToChars(sbytes(joinTokens(tokens)))
chars := util.ToChars(stringBytes(joinTokens(tokens)))
parts = append(parts, &chars)
} else {
if idx < 0 {

View File

@@ -71,14 +71,14 @@ func TestTransform(t *testing.T) {
{
tokens := Tokenize(input, Delimiter{})
{
ranges := splitNth("1,2,3")
ranges, _ := splitNth("1,2,3")
tx := Transform(tokens, ranges)
if joinTokens(tx) != "abc: def: ghi: " {
t.Errorf("%s", tx)
}
}
{
ranges := splitNth("1..2,3,2..,1")
ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
len(tx) != 4 ||
@@ -93,7 +93,7 @@ func TestTransform(t *testing.T) {
{
tokens := Tokenize(input, delimiterRegexp(":"))
{
ranges := splitNth("1..2,3,2..,1")
ranges, _ := splitNth("1..2,3,2..,1")
tx := Transform(tokens, ranges)
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
len(tx) != 4 ||
@@ -108,5 +108,6 @@ func TestTransform(t *testing.T) {
}
func TestTransformIndexOutOfBounds(t *testing.T) {
Transform([]Token{}, splitNth("1"))
s, _ := splitNth("1")
Transform([]Token{}, s)
}

View File

@@ -8,7 +8,7 @@ func HasFullscreenRenderer() bool {
return false
}
var DefaultBorderShape BorderShape = BorderRounded
var DefaultBorderShape = BorderRounded
func (a Attr) Merge(b Attr) Attr {
return a | b
@@ -29,7 +29,7 @@ const (
StrikeThrough = Attr(1 << 7)
)
func (r *FullscreenRenderer) Init() {}
func (r *FullscreenRenderer) Init() error { return nil }
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
func (r *FullscreenRenderer) Pause(bool) {}
func (r *FullscreenRenderer) Resume(bool, bool) {}

View File

@@ -83,34 +83,35 @@ func _() {
_ = x[Alt-72]
_ = x[CtrlAlt-73]
_ = x[Invalid-74]
_ = x[Mouse-75]
_ = x[DoubleClick-76]
_ = x[LeftClick-77]
_ = x[RightClick-78]
_ = x[SLeftClick-79]
_ = x[SRightClick-80]
_ = x[ScrollUp-81]
_ = x[ScrollDown-82]
_ = x[SScrollUp-83]
_ = x[SScrollDown-84]
_ = x[PreviewScrollUp-85]
_ = x[PreviewScrollDown-86]
_ = x[Resize-87]
_ = x[Change-88]
_ = x[BackwardEOF-89]
_ = x[Start-90]
_ = x[Load-91]
_ = x[Focus-92]
_ = x[One-93]
_ = x[Zero-94]
_ = x[Result-95]
_ = x[Jump-96]
_ = x[JumpCancel-97]
_ = x[Fatal-75]
_ = x[Mouse-76]
_ = x[DoubleClick-77]
_ = x[LeftClick-78]
_ = x[RightClick-79]
_ = x[SLeftClick-80]
_ = x[SRightClick-81]
_ = x[ScrollUp-82]
_ = x[ScrollDown-83]
_ = x[SScrollUp-84]
_ = x[SScrollDown-85]
_ = x[PreviewScrollUp-86]
_ = x[PreviewScrollDown-87]
_ = x[Resize-88]
_ = x[Change-89]
_ = x[BackwardEOF-90]
_ = x[Start-91]
_ = x[Load-92]
_ = x[Focus-93]
_ = x[One-94]
_ = x[Zero-95]
_ = x[Result-96]
_ = x[Jump-97]
_ = x[JumpCancel-98]
}
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 458, 467, 477, 487, 498, 506, 516, 525, 536, 551, 568, 574, 580, 591, 596, 600, 605, 608, 612, 618, 622, 632}
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
func (i EventType) String() string {
if i < 0 || i >= EventType(len(_EventType_index)-1) {

View File

@@ -2,6 +2,7 @@ package tui
import (
"bytes"
"errors"
"fmt"
"os"
"regexp"
@@ -10,6 +11,7 @@ import (
"time"
"unicode/utf8"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
"golang.org/x/term"
@@ -27,8 +29,8 @@ const (
const consoleDevice string = "/dev/tty"
var offsetRegexp *regexp.Regexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin *regexp.Regexp = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
func (r *LightRenderer) PassThrough(str string) {
r.queued.WriteString("\x1b7" + str + "\x1b8")
@@ -78,6 +80,7 @@ func (r *LightRenderer) flush() {
// Light renderer
type LightRenderer struct {
closed *util.AtomicBool
theme *ColorTheme
mouse bool
forceBlack bool
@@ -123,19 +126,24 @@ type LightWindow struct {
bg Color
}
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) Renderer {
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
in, err := openTtyIn()
if err != nil {
return nil, err
}
r := LightRenderer{
closed: util.NewAtomicBool(false),
theme: theme,
forceBlack: forceBlack,
mouse: mouse,
clearOnExit: clearOnExit,
ttyin: openTtyIn(),
ttyin: in,
yoffset: 0,
tabstop: tabstop,
fullscreen: fullscreen,
upOneLine: false,
maxHeightFunc: maxHeightFunc}
return &r
return &r, nil
}
func repeat(r rune, times int) string {
@@ -153,11 +161,11 @@ func atoi(s string, defaultValue int) int {
return value
}
func (r *LightRenderer) Init() {
func (r *LightRenderer) Init() error {
r.escDelay = atoi(os.Getenv("ESCDELAY"), defaultEscDelay)
if err := r.initPlatform(); err != nil {
errorExit(err.Error())
return err
}
r.updateTerminalSize()
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
@@ -195,6 +203,7 @@ func (r *LightRenderer) Init() {
if !r.fullscreen && r.mouse {
r.yoffset, _ = r.findOffset()
}
return nil
}
func (r *LightRenderer) Resize(maxHeightFunc func(int) int) {
@@ -233,15 +242,16 @@ func getEnv(name string, defaultValue int) int {
return atoi(env, defaultValue)
}
func (r *LightRenderer) getBytes() []byte {
return r.getBytesInternal(r.buffer, false)
func (r *LightRenderer) getBytes() ([]byte, error) {
bytes, err := r.getBytesInternal(r.buffer, false)
return bytes, err
}
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte, error) {
c, ok := r.getch(nonblock)
if !nonblock && !ok {
r.Close()
errorExit("Failed to read " + consoleDevice)
return nil, errors.New("failed to read " + consoleDevice)
}
retries := 0
@@ -272,19 +282,23 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
// so terminate fzf immediately.
if len(buffer) > maxInputBuffer {
r.Close()
panic(fmt.Sprintf("Input buffer overflow (%d): %v", len(buffer), buffer))
return nil, fmt.Errorf("input buffer overflow (%d): %v", len(buffer), buffer)
}
}
return buffer
return buffer, nil
}
func (r *LightRenderer) GetChar() Event {
var err error
if len(r.buffer) == 0 {
r.buffer = r.getBytes()
r.buffer, err = r.getBytes()
if err != nil {
return Event{Fatal, 0, nil}
}
}
if len(r.buffer) == 0 {
panic("Empty buffer")
return Event{Fatal, 0, nil}
}
sz := 1
@@ -315,7 +329,9 @@ func (r *LightRenderer) GetChar() Event {
ev := r.escSequence(&sz)
// Second chance
if ev.Type == Invalid {
r.buffer = r.getBytes()
if r.buffer, err = r.getBytes(); err != nil {
return Event{Fatal, 0, nil}
}
ev = r.escSequence(&sz)
}
return ev
@@ -738,6 +754,7 @@ func (r *LightRenderer) Close() {
r.flush()
r.closePlatform()
r.restoreTerminal()
r.closed.Set(true)
}
func (r *LightRenderer) Top() int {
@@ -821,44 +838,32 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
color = ColPreviewBorder
}
hw := runeWidth(w.border.top)
pad := repeat(' ', w.width/hw)
w.Move(0, 0)
if top {
w.Move(0, 0)
w.CPrint(color, repeat(w.border.top, w.width/hw))
} else {
w.CPrint(color, pad)
}
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, pad)
}
w.Move(w.height-1, 0)
if bottom {
w.Move(w.height-1, 0)
w.CPrint(color, repeat(w.border.bottom, w.width/hw))
} else {
w.CPrint(color, pad)
}
}
func (w *LightWindow) drawBorderVertical(left, right bool) {
width := w.width - 2
if !left || !right {
width++
}
vw := runeWidth(w.border.left)
color := ColBorder
if w.preview {
color = ColPreviewBorder
}
for y := 0; y < w.height; y++ {
w.Move(y, 0)
if left {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, " ") // Margin
}
w.CPrint(color, repeat(' ', width))
if right {
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}
@@ -880,7 +885,10 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
for y := 1; y < w.height-1; y++ {
w.Move(y, 0)
w.CPrint(color, string(w.border.left))
w.CPrint(color, repeat(' ', w.width-vw*2))
w.CPrint(color, " ") // Margin
w.Move(y, w.width-vw-1)
w.CPrint(color, " ") // Margin
w.CPrint(color, string(w.border.right))
}
}

View File

@@ -3,7 +3,7 @@
package tui
import (
"fmt"
"errors"
"os"
"os/exec"
"strings"
@@ -48,19 +48,18 @@ func (r *LightRenderer) closePlatform() {
// NOOP
}
func openTtyIn() *os.File {
func openTtyIn() (*os.File, error) {
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
if err != nil {
tty := ttyname()
if len(tty) > 0 {
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
return in
return in, nil
}
}
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
util.Exit(2)
return nil, errors.New("failed to open " + consoleDevice)
}
return in
return in, nil
}
func (r *LightRenderer) setupTerminal() {
@@ -86,9 +85,14 @@ func (r *LightRenderer) updateTerminalSize() {
func (r *LightRenderer) findOffset() (row int, col int) {
r.csi("6n")
r.flush()
var err error
bytes := []byte{}
for tries := 0; tries < offsetPollTries; tries++ {
bytes = r.getBytesInternal(bytes, tries > 0)
bytes, err = r.getBytesInternal(bytes, tries > 0)
if err != nil {
return -1, -1
}
offsets := offsetRegexp.FindSubmatch(bytes)
if len(offsets) > 3 {
// Add anything we skipped over to the input buffer

View File

@@ -72,7 +72,7 @@ func (r *LightRenderer) initPlatform() error {
go func() {
fd := int(r.inHandle)
b := make([]byte, 1)
for {
for !r.closed.Get() {
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
@@ -91,9 +91,9 @@ func (r *LightRenderer) closePlatform() {
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
}
func openTtyIn() *os.File {
func openTtyIn() (*os.File, error) {
// not used
return nil
return nil, nil
}
func (r *LightRenderer) setupTerminal() error {

View File

@@ -7,7 +7,6 @@ import (
"time"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
"github.com/junegunn/fzf/src/util"
"github.com/rivo/uniseg"
@@ -146,13 +145,13 @@ var (
_initialResize bool = true
)
func (r *FullscreenRenderer) initScreen() {
func (r *FullscreenRenderer) initScreen() error {
s, e := tcell.NewScreen()
if e != nil {
errorExit(e.Error())
return e
}
if e = s.Init(); e != nil {
errorExit(e.Error())
return e
}
if r.mouse {
s.EnableMouse()
@@ -160,16 +159,21 @@ func (r *FullscreenRenderer) initScreen() {
s.DisableMouse()
}
_screen = s
return nil
}
func (r *FullscreenRenderer) Init() {
func (r *FullscreenRenderer) Init() error {
if os.Getenv("TERM") == "cygwin" {
os.Setenv("TERM", "")
}
encoding.Register()
r.initScreen()
if err := r.initScreen(); err != nil {
return err
}
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
return nil
}
func (r *FullscreenRenderer) Top() int {
@@ -561,7 +565,11 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
}
func (w *TcellWindow) Erase() {
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
if w.borderStyle.shape.HasLeft() {
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
} else {
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
}
w.drawBorder(false)
}

View File

@@ -1,8 +1,6 @@
package tui
import (
"fmt"
"os"
"strconv"
"time"
@@ -104,6 +102,7 @@ const (
CtrlAlt
Invalid
Fatal
Mouse
DoubleClick
@@ -130,6 +129,7 @@ const (
Result
Jump
JumpCancel
ClickHeader
)
func (t EventType) AsEvent() Event {
@@ -303,6 +303,9 @@ type ColorTheme struct {
Disabled ColorAttr
Fg ColorAttr
Bg ColorAttr
SelectedFg ColorAttr
SelectedBg ColorAttr
SelectedMatch ColorAttr
PreviewFg ColorAttr
PreviewBg ColorAttr
DarkBg ColorAttr
@@ -314,7 +317,7 @@ type ColorTheme struct {
Spinner ColorAttr
Info ColorAttr
Cursor ColorAttr
Selected ColorAttr
Marker ColorAttr
Header ColorAttr
Separator ColorAttr
Scrollbar ColorAttr
@@ -368,6 +371,14 @@ const (
BorderRight
)
func (s BorderShape) HasLeft() bool {
switch s {
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
return false
}
return true
}
func (s BorderShape) HasRight() bool {
switch s {
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
@@ -516,7 +527,7 @@ type TermSize struct {
}
type Renderer interface {
Init()
Init() error
Resize(maxHeightFunc func(int) int)
Pause(clear bool)
Resume(clear bool, sigcont bool)
@@ -595,12 +606,14 @@ var (
ColMatch ColorPair
ColCursor ColorPair
ColCursorEmpty ColorPair
ColMarker ColorPair
ColSelected ColorPair
ColSelectedMatch ColorPair
ColCurrent ColorPair
ColCurrentMatch ColorPair
ColCurrentCursor ColorPair
ColCurrentCursorEmpty ColorPair
ColCurrentSelected ColorPair
ColCurrentMarker ColorPair
ColCurrentSelectedEmpty ColorPair
ColSpinner ColorPair
ColInfo ColorPair
@@ -622,6 +635,9 @@ func EmptyTheme() *ColorTheme {
Input: ColorAttr{colUndefined, AttrUndefined},
Fg: ColorAttr{colUndefined, AttrUndefined},
Bg: ColorAttr{colUndefined, AttrUndefined},
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
DarkBg: ColorAttr{colUndefined, AttrUndefined},
Prompt: ColorAttr{colUndefined, AttrUndefined},
Match: ColorAttr{colUndefined, AttrUndefined},
@@ -630,7 +646,7 @@ func EmptyTheme() *ColorTheme {
Spinner: ColorAttr{colUndefined, AttrUndefined},
Info: ColorAttr{colUndefined, AttrUndefined},
Cursor: ColorAttr{colUndefined, AttrUndefined},
Selected: ColorAttr{colUndefined, AttrUndefined},
Marker: ColorAttr{colUndefined, AttrUndefined},
Header: ColorAttr{colUndefined, AttrUndefined},
Border: ColorAttr{colUndefined, AttrUndefined},
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
@@ -652,6 +668,9 @@ func NoColorTheme() *ColorTheme {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colDefault, AttrUndefined},
Prompt: ColorAttr{colDefault, AttrUndefined},
Match: ColorAttr{colDefault, Underline},
@@ -660,7 +679,7 @@ func NoColorTheme() *ColorTheme {
Spinner: ColorAttr{colDefault, AttrUndefined},
Info: ColorAttr{colDefault, AttrUndefined},
Cursor: ColorAttr{colDefault, AttrUndefined},
Selected: ColorAttr{colDefault, AttrUndefined},
Marker: ColorAttr{colDefault, AttrUndefined},
Header: ColorAttr{colDefault, AttrUndefined},
Border: ColorAttr{colDefault, AttrUndefined},
BorderLabel: ColorAttr{colDefault, AttrUndefined},
@@ -676,17 +695,15 @@ func NoColorTheme() *ColorTheme {
}
}
func errorExit(message string) {
fmt.Fprintln(os.Stderr, message)
util.Exit(2)
}
func init() {
Default16 = &ColorTheme{
Colored: true,
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{colBlack, AttrUndefined},
Prompt: ColorAttr{colBlue, AttrUndefined},
Match: ColorAttr{colGreen, AttrUndefined},
@@ -695,7 +712,7 @@ func init() {
Spinner: ColorAttr{colGreen, AttrUndefined},
Info: ColorAttr{colWhite, AttrUndefined},
Cursor: ColorAttr{colRed, AttrUndefined},
Selected: ColorAttr{colMagenta, AttrUndefined},
Marker: ColorAttr{colMagenta, AttrUndefined},
Header: ColorAttr{colCyan, AttrUndefined},
Border: ColorAttr{colBlack, AttrUndefined},
BorderLabel: ColorAttr{colWhite, AttrUndefined},
@@ -714,6 +731,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{236, AttrUndefined},
Prompt: ColorAttr{110, AttrUndefined},
Match: ColorAttr{108, AttrUndefined},
@@ -722,7 +742,7 @@ func init() {
Spinner: ColorAttr{148, AttrUndefined},
Info: ColorAttr{144, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{109, AttrUndefined},
Border: ColorAttr{59, AttrUndefined},
BorderLabel: ColorAttr{145, AttrUndefined},
@@ -741,6 +761,9 @@ func init() {
Input: ColorAttr{colDefault, AttrUndefined},
Fg: ColorAttr{colDefault, AttrUndefined},
Bg: ColorAttr{colDefault, AttrUndefined},
SelectedFg: ColorAttr{colDefault, AttrUndefined},
SelectedBg: ColorAttr{colDefault, AttrUndefined},
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
DarkBg: ColorAttr{251, AttrUndefined},
Prompt: ColorAttr{25, AttrUndefined},
Match: ColorAttr{66, AttrUndefined},
@@ -749,7 +772,7 @@ func init() {
Spinner: ColorAttr{65, AttrUndefined},
Info: ColorAttr{101, AttrUndefined},
Cursor: ColorAttr{161, AttrUndefined},
Selected: ColorAttr{168, AttrUndefined},
Marker: ColorAttr{168, AttrUndefined},
Header: ColorAttr{31, AttrUndefined},
Border: ColorAttr{145, AttrUndefined},
BorderLabel: ColorAttr{59, AttrUndefined},
@@ -791,12 +814,15 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
theme.Info = o(baseTheme.Info, theme.Info)
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
theme.Selected = o(baseTheme.Selected, theme.Selected)
theme.Marker = o(baseTheme.Marker, theme.Marker)
theme.Header = o(baseTheme.Header, theme.Header)
theme.Border = o(baseTheme.Border, theme.Border)
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
// These colors are not defined in the base themes
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch)
theme.Disabled = o(theme.Input, theme.Disabled)
theme.Gutter = o(theme.DarkBg, theme.Gutter)
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
@@ -822,17 +848,23 @@ func initPalette(theme *ColorTheme) {
ColPrompt = pair(theme.Prompt, theme.Bg)
ColNormal = pair(theme.Fg, theme.Bg)
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
ColInput = pair(theme.Input, theme.Bg)
ColDisabled = pair(theme.Disabled, theme.Bg)
ColMatch = pair(theme.Match, theme.Bg)
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
ColCursor = pair(theme.Cursor, theme.Gutter)
ColCursorEmpty = pair(blank, theme.Gutter)
ColSelected = pair(theme.Selected, theme.Gutter)
if theme.SelectedBg.Color != theme.Bg.Color {
ColMarker = pair(theme.Marker, theme.SelectedBg)
} else {
ColMarker = pair(theme.Marker, theme.Gutter)
}
ColCurrent = pair(theme.Current, theme.DarkBg)
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
ColSpinner = pair(theme.Spinner, theme.Bg)
ColInfo = pair(theme.Info, theme.Bg)

View File

@@ -1,7 +1,6 @@
package util
import (
"os"
"sync"
)
@@ -25,14 +24,5 @@ func RunAtExitFuncs() {
for i := len(fns) - 1; i >= 0; i-- {
fns[i]()
}
}
// Exit executes any functions registered with AtExit() then exits the program
// with os.Exit(code).
//
// NOTE: It must be used instead of os.Exit() since calling os.Exit() terminates
// the program before any of the AtExit functions can run.
func Exit(code int) {
defer os.Exit(code)
RunAtExitFuncs()
atExitFuncs = nil
}

View File

@@ -3,31 +3,71 @@
package util
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"golang.org/x/sys/unix"
)
// ExecCommand executes the given command with $SHELL
func ExecCommand(command string, setpgid bool) *exec.Cmd {
shell := os.Getenv("SHELL")
if len(shell) == 0 {
shell = "sh"
}
return ExecCommandWith(shell, command, setpgid)
type Executor struct {
shell string
args []string
escaper *strings.Replacer
}
// ExecCommandWith executes the given command with the specified shell
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
cmd := exec.Command(shell, "-c", command)
func NewExecutor(withShell string) *Executor {
shell := os.Getenv("SHELL")
args := strings.Fields(withShell)
if len(args) > 0 {
shell = args[0]
args = args[1:]
} else {
if len(shell) == 0 {
shell = "sh"
}
args = []string{"-c"}
}
var escaper *strings.Replacer
tokens := strings.Split(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("'", "'\\''")
}
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 {
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
}
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())
os.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
func KillCommand(cmd *exec.Cmd) error {
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)

View File

@@ -6,62 +6,159 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync/atomic"
"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{"/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
func ExecCommand(command string, setpgid bool) *exec.Cmd {
var shell string
if cached := shellPath.Load(); cached != nil {
// 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 (x *Executor) ExecCommand(command string, setpgid bool) *exec.Cmd {
shell := x.shell
if cached := x.shellPath.Load(); cached != nil {
shell = cached.(string)
} else {
shell = os.Getenv("SHELL")
if len(shell) == 0 {
shell = "cmd"
} else if strings.Contains(shell, "/") {
if strings.Contains(shell, "/") {
out, err := exec.Command("cygpath", "-w", shell).Output()
if err == nil {
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
if strings.Contains(shell, "cmd") {
if x.shellType == shellTypeCmd {
cmd = exec.Command(shell)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
CmdLine: fmt.Sprintf(`%s "%s"`, strings.Join(x.args, " "), command),
CreationFlags: 0,
}
return cmd
}
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
} else {
cmd = exec.Command(shell, "-c", command)
}
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CreationFlags: 0,
cmd = exec.Command(shell, append(x.args, command)...)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: false,
CreationFlags: 0,
}
}
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())
os.Exit(127)
}
err = cmd.Wait()
if err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
os.Exit(exitError.ExitCode())
}
}
os.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 '"':
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
func KillCommand(cmd *exec.Cmd) error {
return cmd.Process.Kill()

View File

@@ -425,6 +425,25 @@ class TestGoFZF < TestBase
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
[true, false].each do |multi|
tmux.send_keys "(echo ' 1st 2nd 3rd/';
@@ -538,7 +557,7 @@ class TestGoFZF < TestBase
def test_expect
test = lambda do |key, feed, expected = key|
tmux.send_keys "seq 1 100 | #{fzf(:expect, key)}", :Enter
tmux.send_keys "seq 1 100 | #{fzf(:expect, key, :prompt, "[#{key}]")}", :Enter
tmux.until { |lines| assert_equal ' 100/100', lines[-2] }
tmux.send_keys '55'
tmux.until { |lines| assert_equal ' 1/100', lines[-2] }
@@ -1955,6 +1974,11 @@ class TestGoFZF < TestBase
tmux.until { |lines| assert_equal 10, lines.item_count }
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
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
tmux.until { |lines| assert_includes lines, ' 9' }
@@ -2669,6 +2693,13 @@ class TestGoFZF < TestBase
end
end
def test_change_preview_window_should_not_reset_change_preview
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
tmux.until { |lines| assert_includes lines, 'hello' }
tmux.send_keys :Enter
tmux.until { |lines| assert_includes lines, '│ hello' }
end
def test_change_preview_window_rotate
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
"a:change-preview-window(right|down|up|hidden|)'", :Enter
@@ -2946,6 +2977,13 @@ class TestGoFZF < TestBase
tmux.until { assert_match(%r{[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] 100/100}, _1[-1]) }
end
def test_info_inline_right_clearance
tmux.send_keys "seq 100000 | #{FZF} --info inline-right", :Enter
tmux.until { assert_match(%r{100000/100000}, _1[-1]) }
tmux.send_keys 'x'
tmux.until { assert_match(%r{ 0/100000}, _1[-1]) }
end
def test_prev_next_selected
tmux.send_keys 'seq 10 | fzf --multi --bind ctrl-n:next-selected,ctrl-p:prev-selected', :Enter
tmux.until { |lines| assert_equal 10, lines.item_count }
@@ -3226,6 +3264,17 @@ class TestGoFZF < TestBase
tmux.send_keys :Up
tmux.until { |lines| assert_includes lines, '> 2' }
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
module TestShell
@@ -3457,7 +3506,11 @@ module CompletionTest
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
tmux.send_keys :Tab, :Tab # Tab does not work here
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.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
tmux.send_keys :xx