mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-26 09:42:02 -07:00
Compare commits
81 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
260a65b0fb | ||
|
835d2fb98c | ||
|
a9811addaa | ||
|
ee9d88b637 | ||
|
194a763c46 | ||
|
8d74446bef | ||
|
7ed6c7905c | ||
|
159a37fa37 | ||
|
f39ae0e7c1 | ||
|
4a68eac99b | ||
|
2665580120 | ||
|
a4391aeedd | ||
|
b86a967ee2 | ||
|
608232568b | ||
|
7f85beccb5 | ||
|
767f1255ab | ||
|
fddbfe7b0e | ||
|
4ab7fdc28e | ||
|
e352b68878 | ||
|
207deeadba | ||
|
d18d92f925 | ||
|
af3ce47c44 | ||
|
d8bfb6712d | ||
|
f864f8b5f7 | ||
|
31d72efba7 | ||
|
d169c951f3 | ||
|
90d7e38909 | ||
|
938f23e429 | ||
|
f97d275413 | ||
|
3acb4ca90e | ||
|
e86b81bbf5 | ||
|
a5447b8b75 | ||
|
7ce6452d83 | ||
|
5643a306bd | ||
|
3c877c504b | ||
|
892d1acccb | ||
|
1a9c282f76 | ||
|
fd1ba46f77 | ||
|
a4745626dd | ||
|
17bb7ad278 | ||
|
152988c17b | ||
|
4cd37fc02b | ||
|
69b9d674a3 | ||
|
bad8061547 | ||
|
62963dcefd | ||
|
68a35e4735 | ||
|
9b9ad77e1c | ||
|
118b4d4a01 | ||
|
da14ab6f16 | ||
|
09a4ca6ab5 | ||
|
8a2df79711 | ||
|
c30e486b64 | ||
|
a575c0c54b | ||
|
77fe96ac0d | ||
|
5234c3759a | ||
|
41b3511ad9 | ||
|
128e4a2e8d | ||
|
07ac90d798 | ||
|
7de87a9b2c | ||
|
dff865239a | ||
|
07f8f70c5b | ||
|
f625c5aabe | ||
|
8a74976c1f | ||
|
b6bfd4a5cb | ||
|
008fb9d258 | ||
|
db6db49ed6 | ||
|
05453881c3 | ||
|
5e47ab9431 | ||
|
ec70acd0b9 | ||
|
25e61056b6 | ||
|
d579e335b5 | ||
|
7e344ceb85 | ||
|
0145b82ea0 | ||
|
b4efe7aab7 | ||
|
9ffe951f6d | ||
|
a5ea4f57bd | ||
|
88f4c16755 | ||
|
c7ee071efa | ||
|
0740ef7ceb | ||
|
b29bd809ac | ||
|
8977c9257a |
22
.github/ISSUE_TEMPLATE.md
vendored
22
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
||||
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
|
||||
|
||||
<!-- Check all that apply [x] -->
|
||||
|
||||
- [ ] I have read through the manual page (`man fzf`)
|
||||
- [ ] I have the latest version of fzf
|
||||
- [ ] I have searched through the existing issues
|
||||
|
||||
## Info
|
||||
|
||||
- OS
|
||||
- [ ] Linux
|
||||
- [ ] Mac OS X
|
||||
- [ ] Windows
|
||||
- [ ] Etc.
|
||||
- Shell
|
||||
- [ ] bash
|
||||
- [ ] zsh
|
||||
- [ ] fish
|
||||
|
||||
## Problem / Steps to reproduce
|
||||
|
49
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Issue Template
|
||||
description: Report a problem or bug related to fzf to help us improve
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
options:
|
||||
- label: I have read through the manual page (`man fzf`)
|
||||
required: true
|
||||
- label: I have searched through the existing issues
|
||||
required: true
|
||||
- label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Output of `fzf --version`
|
||||
placeholder: e.g. 0.48.1 (d579e33)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: OS
|
||||
options:
|
||||
- label: Linux
|
||||
- label: macOS
|
||||
- label: Windows
|
||||
- label: Etc.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Shell
|
||||
options:
|
||||
- label: bash
|
||||
- label: zsh
|
||||
- label: fish
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem / Steps to reproduce
|
||||
validations:
|
||||
required: true
|
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.18
|
||||
go-version: "1.20"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -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.20.10
|
||||
|
@@ -12,6 +12,8 @@ builds:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
@@ -19,11 +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"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
bundle_id = "junegunn.fzf"
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
@@ -40,6 +38,8 @@ builds:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
@@ -47,11 +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"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
bundle_id = "junegunn.fzf"
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
@@ -79,6 +75,8 @@ builds:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
ignore:
|
||||
|
16
BUILD.md
16
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.18 or above
|
||||
- Go 1.20 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
@@ -24,13 +24,23 @@ make build
|
||||
make release
|
||||
```
|
||||
|
||||
> :warning: Makefile uses git commands to determine the version and the
|
||||
> revision information for `fzf --version`. So if you're building fzf from an
|
||||
> [!WARNING]
|
||||
> Makefile uses git commands to determine the version and the revision
|
||||
> information for `fzf --version`. So if you're building fzf from an
|
||||
> environment where its git information is not available, you have to manually
|
||||
> set `$FZF_VERSION` and `$FZF_REVISION`.
|
||||
>
|
||||
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
|
||||
|
||||
> [!TIP]
|
||||
> To build fzf with profiling options enabled, set `TAGS=pprof`
|
||||
>
|
||||
> ```sh
|
||||
> TAGS=pprof make clean install
|
||||
> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \
|
||||
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
|
||||
> ```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
|
112
CHANGELOG.md
112
CHANGELOG.md
@@ -1,6 +1,118 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.51.0
|
||||
------
|
||||
- Added a new environment variable `$FZF_POS` exported to the child processes. It's the vertical position of the cursor in the list starting from 1.
|
||||
```sh
|
||||
# Toggle selection to the top or to the bottom
|
||||
seq 30 | fzf --multi --bind 'load:pos(10)' \
|
||||
--bind 'shift-up:transform:for _ in $(seq $FZF_POS $FZF_MATCH_COUNT); do echo -n +toggle+up; done' \
|
||||
--bind 'shift-down:transform:for _ in $(seq 1 $FZF_POS); do echo -n +toggle+down; done'
|
||||
```
|
||||
- Added `--with-shell` option to start child processes with a custom shell command and flags
|
||||
```sh
|
||||
gem list | fzf --with-shell 'ruby -e' \
|
||||
--preview 'pp Gem::Specification.find_by_name({1})' \
|
||||
--bind 'ctrl-o:execute-silent:
|
||||
spec = Gem::Specification.find_by_name({1})
|
||||
[spec.homepage, *spec.metadata.filter { _1.end_with?("uri") }.values].uniq.each do
|
||||
system "open", _1
|
||||
end
|
||||
'
|
||||
```
|
||||
- Added `change-multi` action for dynamically changing `--multi` option
|
||||
- `change-multi` - enable multi-select mode with no limit
|
||||
- `change-multi(NUM)` - enable multi-select mode with a limit
|
||||
- `change-multi(0)` - disable multi-select mode
|
||||
- Windows improvements
|
||||
- `become` action is now supported on Windows
|
||||
- Unlike in *nix, this does not use `execve(2)`. Instead it spawns a new process and waits for it to finish, so the exact behavior may differ.
|
||||
- Fixed argument escaping for Windows cmd.exe. No redundant escaping of backslashes.
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.50.0
|
||||
------
|
||||
- Search performance optimization. You can observe 50%+ improvement in some scenarios.
|
||||
```
|
||||
$ rg --line-number --no-heading --smart-case . > $DATA
|
||||
|
||||
$ wc < $DATA
|
||||
5520118 26862362 897487793
|
||||
|
||||
$ hyperfine -w 1 -L bin fzf-0.49.0,fzf-7ce6452,fzf-a5447b8,fzf '{bin} --filter "///" < $DATA | head -30'
|
||||
Summary
|
||||
fzf --filter "///" < $DATA | head -30 ran
|
||||
1.16 ± 0.03 times faster than fzf-a5447b8 --filter "///" < $DATA | head -30
|
||||
1.23 ± 0.03 times faster than fzf-7ce6452 --filter "///" < $DATA | head -30
|
||||
1.52 ± 0.03 times faster than fzf-0.49.0 --filter "///" < $DATA | head -30
|
||||
```
|
||||
- Added `jump` and `jump-cancel` events that are triggered when leaving `jump` mode
|
||||
```sh
|
||||
# Default behavior
|
||||
fzf --bind space:jump
|
||||
|
||||
# Same as jump-accept action
|
||||
fzf --bind space:jump,jump:accept
|
||||
|
||||
# Accept on jump, abort on cancel
|
||||
fzf --bind space:jump,jump:accept,jump-cancel:abort
|
||||
|
||||
# Change header on jump-cancel
|
||||
fzf --bind 'space:change-header(Type jump label)+jump,jump-cancel:change-header:Jump cancelled'
|
||||
```
|
||||
- Added a new environment variable `$FZF_KEY` exported to the child processes. It's the name of the last key pressed.
|
||||
```sh
|
||||
fzf --bind 'space:jump,jump:accept,jump-cancel:transform:[[ $FZF_KEY =~ ctrl-c ]] && echo abort'
|
||||
```
|
||||
- fzf can be built with profiling options. See [BUILD.md](BUILD.md) for more information.
|
||||
- Bug fixes
|
||||
|
||||
0.49.0
|
||||
------
|
||||
- Ingestion performance improved by around 40% (more or less depending on options)
|
||||
- `--info=hidden` and `--info=inline-right` will no longer hide the horizontal separator by default. This gives you more flexibility in customizing the layout.
|
||||
```sh
|
||||
fzf --border --info=inline-right
|
||||
fzf --border --info=inline-right --separator ═
|
||||
fzf --border --info=inline-right --no-separator
|
||||
fzf --border --info=hidden
|
||||
fzf --border --info=hidden --separator ━
|
||||
fzf --border --info=hidden --no-separator
|
||||
```
|
||||
- Added two environment variables exported to the child processes
|
||||
- `FZF_PREVIEW_LABEL`
|
||||
- `FZF_BORDER_LABEL`
|
||||
```sh
|
||||
# Use the current value of $FZF_PREVIEW_LABEL to determine which actions to perform
|
||||
git ls-files |
|
||||
fzf --header 'Press CTRL-P to change preview mode' \
|
||||
--bind='ctrl-p:transform:[[ $FZF_PREVIEW_LABEL =~ cat ]] \
|
||||
&& echo "change-preview(git log --color=always \{})+change-preview-label([[ log ]])" \
|
||||
|| echo "change-preview(bat --color=always \{})+change-preview-label([[ cat ]])"'
|
||||
```
|
||||
- Renamed `track` action to `track-current` to highlight the difference between the global tracking state set by `--track` and a one-off tracking action
|
||||
- `track` is still available as an alias
|
||||
- Added `untrack-current` and `toggle-track-current` actions
|
||||
- `*-current` actions are no-op when the global tracking state is set
|
||||
- Bug fixes and minor improvements
|
||||
|
||||
0.48.1
|
||||
------
|
||||
- CTRL-T and ALT-C bindings can be disabled by setting `FZF_CTRL_T_COMMAND` and `FZF_ALT_C_COMMAND` to empty strings respectively when sourcing the script
|
||||
```sh
|
||||
# bash
|
||||
FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --bash)"
|
||||
|
||||
# zsh
|
||||
FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= eval "$(fzf --zsh)"
|
||||
|
||||
# fish
|
||||
fzf --fish | FZF_CTRL_T_COMMAND= FZF_ALT_C_COMMAND= source
|
||||
```
|
||||
- Setting the variables after sourcing the script will have no effect
|
||||
- Bug fixes
|
||||
|
||||
0.48.0
|
||||
------
|
||||
- Shell integration scripts are now embedded in the fzf binary. This simplifies the distribution, and the users are less likely to have problems caused by using incompatible scripts and binaries.
|
||||
|
@@ -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
|
||||
|
||||
|
13
Makefile
13
Makefile
@@ -4,7 +4,7 @@ GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
@@ -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,6 +79,7 @@ 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 \
|
||||
@@ -173,12 +174,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
|
||||
|
@@ -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=()
|
||||
|
8
go.mod
8
go.mod
@@ -1,13 +1,13 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.2
|
||||
github.com/charlievieth/fastwalk v1.0.3
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
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.18.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/sys v0.19.0
|
||||
golang.org/x/term v0.19.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -17,4 +17,4 @@ require (
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
||||
go 1.17
|
||||
go 1.20
|
||||
|
12
go.sum
12
go.sum
@@ -1,5 +1,5 @@
|
||||
github.com/charlievieth/fastwalk v1.0.2 h1:KYWo7xszmoldOGrwdNIeznSzhj9mhgk+6DwHunG99bc=
|
||||
github.com/charlievieth/fastwalk v1.0.2/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
|
||||
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
|
||||
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
@@ -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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.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/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.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
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/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=
|
||||
|
2
install
2
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.48.0
|
||||
version=0.51.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.48.0"
|
||||
$version="0.51.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.48"
|
||||
var version string = "0.51"
|
||||
var revision string = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
174
main_test.go
Normal file
174
main_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/build"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func loadPackages(t *testing.T) []*build.Package {
|
||||
// If GOROOT is not set, use `go env GOROOT` to determine it since it
|
||||
// performs more work than just runtime.GOROOT(). For context, running
|
||||
// the tests with the "-trimpath" flag causes GOROOT to not be set.
|
||||
ctxt := &build.Default
|
||||
if ctxt.GOROOT == "" {
|
||||
cmd := exec.Command("go", "env", "GOROOT")
|
||||
out, err := cmd.CombinedOutput()
|
||||
out = bytes.TrimSpace(out)
|
||||
if err != nil {
|
||||
t.Fatalf("error running command: %q: %v\n%s", cmd.Args, err, out)
|
||||
}
|
||||
ctxt.GOROOT = string(out)
|
||||
}
|
||||
|
||||
wd, err := os.Getwd()
|
||||
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 := ctxt.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 "))
|
||||
}
|
||||
}
|
@@ -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 "Mar 2024" "fzf 0.48.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "May 2024" "fzf 0.51.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
|
@@ -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 "Mar 2024" "fzf 0.48.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "May 2024" "fzf 0.51.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -191,7 +191,7 @@ actions are affected:
|
||||
\fBkill-word\fR
|
||||
.TP
|
||||
.BI "--jump-labels=" "CHARS"
|
||||
Label characters for \fBjump\fR and \fBjump-accept\fR
|
||||
Label characters for \fBjump\fR mode.
|
||||
.SS Layout
|
||||
.TP
|
||||
.BI "--height=" "[~]HEIGHT[%]"
|
||||
@@ -372,20 +372,21 @@ e.g.
|
||||
|
||||
.TP
|
||||
.BI "--info=" "STYLE"
|
||||
Determines the display style of finder info (match counters).
|
||||
Determines the display style of the finder info. (e.g. match counter, loading indicator, etc.)
|
||||
|
||||
.BR default " On the left end of the horizontal separator"
|
||||
.br
|
||||
.BR default " Display on the next line to the prompt"
|
||||
.BR right " On the right end of the horizontal separator"
|
||||
.br
|
||||
.BR right " Display on the right end of the next line to the prompt"
|
||||
.BR hidden " Do not display finder info"
|
||||
.br
|
||||
.BR inline " Display on the same line with the default separator ' < '"
|
||||
.BR inline " After the prompt with the default prefix ' < '"
|
||||
.br
|
||||
.BR inline:SEPARATOR " Display on the same line with a non-default separator"
|
||||
.BR inline:PREFIX " After the prompt with a non-default prefix"
|
||||
.br
|
||||
.BR inline-right " Display on the right end of the same line
|
||||
.BR inline-right " On the right end of the prompt line"
|
||||
.br
|
||||
.BR hidden " Do not display finder info"
|
||||
.BR inline-right:PREFIX " On the right end of the prompt line with a custom prefix"
|
||||
.br
|
||||
|
||||
.TP
|
||||
@@ -811,12 +812,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 /v:on/s/c\fR when \fB$SHELL\fR is not
|
||||
set.
|
||||
|
||||
.RS
|
||||
e.g. \fBgem list | fzf --with-shell 'ruby -e' --preview 'pp Gem::Specification.find_by_name({1})'\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "--listen[=[ADDR:]PORT]" "--listen-unsafe[=[ADDR:]PORT]"
|
||||
Start HTTP server and listen on the given address. It allows external processes
|
||||
to send actions to perform via POST method.
|
||||
@@ -931,6 +942,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
|
||||
@@ -971,12 +984,20 @@ 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"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LABEL " Preview label string"
|
||||
.br
|
||||
.BR FZF_BORDER_LABEL " Border label string"
|
||||
.br
|
||||
.BR FZF_ACTION " The name of the last action performed"
|
||||
.br
|
||||
.BR FZF_KEY " The name of the last key pressed"
|
||||
.br
|
||||
.BR FZF_PORT " Port number when --listen option is used"
|
||||
.br
|
||||
|
||||
@@ -1047,21 +1068,21 @@ e.g.
|
||||
.br
|
||||
\fIctrl-]\fR
|
||||
.br
|
||||
\fIctrl-^\fR (\fIctrl-6\fR)
|
||||
\fIctrl-^\fR (\fIctrl-6\fR)
|
||||
.br
|
||||
\fIctrl-/\fR (\fIctrl-_\fR)
|
||||
\fIctrl-/\fR (\fIctrl-_\fR)
|
||||
.br
|
||||
\fIctrl-alt-[a-z]\fR
|
||||
.br
|
||||
\fIalt-[*]\fR (Any case-sensitive single character is allowed)
|
||||
\fIalt-[*]\fR (Any case-sensitive single character is allowed)
|
||||
.br
|
||||
\fIf[1-12]\fR
|
||||
.br
|
||||
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
|
||||
\fIenter\fR (\fIreturn\fR \fIctrl-m\fR)
|
||||
.br
|
||||
\fIspace\fR
|
||||
.br
|
||||
\fIbspace\fR (\fIbs\fR)
|
||||
\fIbackspace\fR (\fIbspace\fR \fIbs\fR)
|
||||
.br
|
||||
\fIalt-up\fR
|
||||
.br
|
||||
@@ -1075,15 +1096,15 @@ e.g.
|
||||
.br
|
||||
\fIalt-space\fR
|
||||
.br
|
||||
\fIalt-bspace\fR (\fIalt-bs\fR)
|
||||
\fIalt-backspace\fR (\fIalt-bspace\fR \fIalt-bs\fR)
|
||||
.br
|
||||
\fItab\fR
|
||||
.br
|
||||
\fIbtab\fR (\fIshift-tab\fR)
|
||||
\fIshift-tab\fR (\fIbtab\fR)
|
||||
.br
|
||||
\fIesc\fR
|
||||
.br
|
||||
\fIdel\fR
|
||||
\fIdelete\fR (\fIdel\fR)
|
||||
.br
|
||||
\fIup\fR
|
||||
.br
|
||||
@@ -1099,9 +1120,9 @@ e.g.
|
||||
.br
|
||||
\fIinsert\fR
|
||||
.br
|
||||
\fIpgup\fR (\fIpage-up\fR)
|
||||
\fIpage-up\fR (\fIpgup\fR)
|
||||
.br
|
||||
\fIpgdn\fR (\fIpage-down\fR)
|
||||
\fIpage-down\fR (\fIpgdn\fR)
|
||||
.br
|
||||
\fIshift-up\fR
|
||||
.br
|
||||
@@ -1155,6 +1176,7 @@ e.g.
|
||||
\fB# Move cursor to the last item and select all items
|
||||
seq 1000 | fzf --multi --sync --bind start:last+select-all\fR
|
||||
.RE
|
||||
|
||||
\fIload\fR
|
||||
.RS
|
||||
Triggered when the input stream is complete and the initial processing of the
|
||||
@@ -1164,6 +1186,7 @@ e.g.
|
||||
\fB# Change the prompt to "loaded" when the input stream is complete
|
||||
(seq 10; sleep 1; seq 11 20) | fzf --prompt 'Loading> ' --bind 'load:change-prompt:Loaded> '\fR
|
||||
.RE
|
||||
|
||||
\fIresize\fR
|
||||
.RS
|
||||
Triggered when the terminal size is changed.
|
||||
@@ -1171,6 +1194,7 @@ Triggered when the terminal size is changed.
|
||||
e.g.
|
||||
\fBfzf --bind 'resize:transform-header:echo Resized: ${FZF_COLUMNS}x${FZF_LINES}'\fR
|
||||
.RE
|
||||
|
||||
\fIresult\fR
|
||||
.RS
|
||||
Triggered when the filtering for the current query is complete and the result list is ready.
|
||||
@@ -1204,6 +1228,7 @@ e.g.
|
||||
# Beware not to introduce an infinite loop
|
||||
seq 10 | fzf --bind 'focus:up' --cycle\fR
|
||||
.RE
|
||||
|
||||
\fIone\fR
|
||||
.RS
|
||||
Triggered when there's only one match. \fBone:accept\fR binding is comparable
|
||||
@@ -1215,6 +1240,7 @@ e.g.
|
||||
\fB# Automatically select the only match
|
||||
seq 10 | fzf --bind one:accept\fR
|
||||
.RE
|
||||
|
||||
\fIzero\fR
|
||||
.RS
|
||||
Triggered when there's no match. \fBzero:abort\fR binding is comparable to
|
||||
@@ -1236,6 +1262,22 @@ e.g.
|
||||
\fBfzf --bind backward-eof:abort\fR
|
||||
.RE
|
||||
|
||||
\fIjump\fR
|
||||
.RS
|
||||
Triggered when successfully jumped to the target item in \fBjump\fR mode.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind space:jump,jump:accept\fR
|
||||
.RE
|
||||
|
||||
\fIjump-cancel\fR
|
||||
.RS
|
||||
Triggered when \fBjump\fR mode is cancelled.
|
||||
|
||||
e.g.
|
||||
\fBfzf --bind space:jump,jump:accept,jump-cancel:abort\fR
|
||||
.RE
|
||||
|
||||
.SS AVAILABLE ACTIONS:
|
||||
A key or an event can be bound to one or more of the following actions.
|
||||
|
||||
@@ -1254,6 +1296,8 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\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 '|')
|
||||
@@ -1278,7 +1322,6 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBforward-word\fR \fIalt-f shift-right\fR
|
||||
\fBignore\fR
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBjump-accept\fR (jump and accept)
|
||||
\fBkill-line\fR
|
||||
\fBkill-word\fR \fIalt-d\fR
|
||||
\fBlast\fR (move to the last match; same as \fBpos(-1)\fR)
|
||||
@@ -1326,9 +1369,10 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtoggle-preview-wrap\fR
|
||||
\fBtoggle-search\fR (toggle search functionality)
|
||||
\fBtoggle-sort\fR
|
||||
\fBtoggle-track\fR
|
||||
\fBtoggle-track\fR (toggle global tracking option (\fB--track\fR))
|
||||
\fBtoggle-track-current\fR (toggle tracking of the current item)
|
||||
\fBtoggle+up\fR \fIbtab (shift-tab)\fR
|
||||
\fBtrack\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtrack-current\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||
\fBtransform-border-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform-header(...)\fR (transform header using an external command)
|
||||
@@ -1338,6 +1382,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBunbind(...)\fR (unbind bindings)
|
||||
\fBunix-line-discard\fR \fIctrl-u\fR
|
||||
\fBunix-word-rubout\fR \fIctrl-w\fR
|
||||
\fBuntrack-current\fR (stop tracking the current item; no-op if global tracking is enabled)
|
||||
\fBup\fR \fIctrl-k ctrl-p up\fR
|
||||
\fByank\fR \fIctrl-y\fR
|
||||
|
||||
@@ -1410,8 +1455,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
|
||||
|
@@ -83,12 +83,28 @@ else
|
||||
endfunction
|
||||
endif
|
||||
|
||||
let s:cmd_control_chars = ['&', '|', '<', '>', '(', ')', '@', '^', '!']
|
||||
|
||||
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
|
||||
elseif c ==# '%'
|
||||
let e .= '%'
|
||||
elseif index(s:cmd_control_chars, c) >= 0
|
||||
let e .= '^'
|
||||
else
|
||||
let slashes = 0
|
||||
endif
|
||||
let e .= c
|
||||
endfor
|
||||
let e .= repeat('\', slashes) .'"'
|
||||
return e
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(arg, ...)
|
||||
|
@@ -9,7 +9,7 @@
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
[[ $- =~ i ]] || return 0
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
@@ -32,6 +32,14 @@
|
||||
# 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 +100,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
|
||||
@@ -291,7 +248,7 @@ _fzf_handle_dynamic_completion() {
|
||||
"$REPLY" "$@"
|
||||
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_completion_loader "$@"
|
||||
$_fzf_completion_loader "$@"
|
||||
ret=$?
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
@@ -308,6 +265,7 @@ _fzf_handle_dynamic_completion() {
|
||||
eval "$orig_complete"
|
||||
fi
|
||||
fi
|
||||
[[ $ret -eq 0 ]] && return 124
|
||||
return $ret
|
||||
fi
|
||||
}
|
||||
@@ -334,8 +292,8 @@ __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
|
||||
@@ -398,7 +356,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")
|
||||
@@ -502,8 +463,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
|
||||
@@ -511,13 +475,22 @@ 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 _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=1
|
||||
if type _comp_load > /dev/null 2>&1; then
|
||||
# _comp_load was added in bash-completion 2.12 to replace _completion_loader.
|
||||
# We use it without -D option so that it does not use _comp_complete_minimal as the fallback.
|
||||
_fzf_completion_loader=_comp_load
|
||||
elif type __load_completion > /dev/null 2>&1; then
|
||||
# In bash-completion 2.11, _completion_loader internally calls __load_completion
|
||||
# and if it returns a non-zero status, it sets the default 'minimal' completion.
|
||||
_fzf_completion_loader=__load_completion
|
||||
elif type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=_completion_loader
|
||||
fi
|
||||
|
||||
__fzf_defc() {
|
||||
@@ -539,13 +512,24 @@ done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
__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
|
||||
@@ -567,8 +551,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
|
||||
|
@@ -9,8 +9,6 @@
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
|
||||
# Both branches of the following `if` do the same thing -- define
|
||||
# __fzf_completion_options such that `eval $__fzf_completion_options` sets
|
||||
@@ -75,6 +73,9 @@ fi
|
||||
# This brace is the start of try-always block. The `always` part is like
|
||||
# `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 +94,14 @@ fi
|
||||
|
||||
###########################################################
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
@@ -146,8 +155,8 @@ __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
|
||||
@@ -217,7 +226,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 +327,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,6 +357,7 @@ fzf-completion() {
|
||||
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
fi
|
||||
|
||||
} always {
|
||||
# Restore the original options.
|
||||
|
@@ -11,15 +11,24 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
[[ $- =~ i ]] || return 0
|
||||
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)
|
||||
) && printf 'builtin cd -- %q' "$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)
|
||||
) && 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
|
||||
@@ -102,9 +112,11 @@ bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||
|
||||
if (( BASH_VERSINFO[0] < 4 )); then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
|
||||
@@ -112,9 +124,11 @@ if (( BASH_VERSINFO[0] < 4 )); then
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||
@@ -123,6 +137,10 @@ else
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
fi
|
||||
|
||||
fi
|
||||
|
@@ -18,17 +18,27 @@ 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)
|
||||
set -l dir $commandline[1]
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
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 --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.)
|
||||
@@ -70,13 +81,14 @@ function fzf_key_bindings
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
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 --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
|
||||
|
||||
@@ -104,14 +116,22 @@ function fzf_key_bindings
|
||||
end
|
||||
end
|
||||
|
||||
bind \ct fzf-file-widget
|
||||
bind \cr fzf-history-widget
|
||||
bind \ec fzf-cd-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind \ct fzf-file-widget
|
||||
end
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
if bind -M insert > /dev/null 2>&1
|
||||
bind -M insert \ct fzf-file-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
|
@@ -11,8 +11,6 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
|
||||
# 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,43 +66,51 @@ __fzfcmd() {
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
LBUFFER="${LBUFFER}$(__fsel)"
|
||||
LBUFFER="${LBUFFER}$(__fzf_select)"
|
||||
local ret=$?
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-file-widget
|
||||
bindkey -M emacs '^T' fzf-file-widget
|
||||
bindkey -M vicmd '^T' fzf-file-widget
|
||||
bindkey -M viins '^T' fzf-file-widget
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
zle -N fzf-file-widget
|
||||
bindkey -M emacs '^T' fzf-file-widget
|
||||
bindkey -M vicmd '^T' fzf-file-widget
|
||||
bindkey -M viins '^T' fzf-file-widget
|
||||
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
|
||||
fi
|
||||
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||
BUFFER="builtin cd -- ${(q)dir}"
|
||||
BUFFER="builtin cd -- ${(q)dir:a}"
|
||||
zle accept-line
|
||||
local ret=$?
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-cd-widget
|
||||
bindkey -M emacs '\ec' fzf-cd-widget
|
||||
bindkey -M vicmd '\ec' fzf-cd-widget
|
||||
bindkey -M viins '\ec' fzf-cd-widget
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
zle -N fzf-cd-widget
|
||||
bindkey -M emacs '\ec' fzf-cd-widget
|
||||
bindkey -M vicmd '\ec' fzf-cd-widget
|
||||
bindkey -M viins '\ec' fzf-cd-widget
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
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")
|
||||
@@ -110,6 +127,7 @@ 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
|
||||
|
@@ -26,100 +26,103 @@ 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[actToggleHeader-46]
|
||||
_ = x[actTrack-47]
|
||||
_ = x[actDown-48]
|
||||
_ = x[actUp-49]
|
||||
_ = x[actPageUp-50]
|
||||
_ = x[actPageDown-51]
|
||||
_ = x[actPosition-52]
|
||||
_ = x[actHalfPageUp-53]
|
||||
_ = x[actHalfPageDown-54]
|
||||
_ = x[actOffsetUp-55]
|
||||
_ = x[actOffsetDown-56]
|
||||
_ = x[actJump-57]
|
||||
_ = x[actJumpAccept-58]
|
||||
_ = x[actPrintQuery-59]
|
||||
_ = x[actRefreshPreview-60]
|
||||
_ = x[actReplaceQuery-61]
|
||||
_ = x[actToggleSort-62]
|
||||
_ = x[actShowPreview-63]
|
||||
_ = x[actHidePreview-64]
|
||||
_ = x[actTogglePreview-65]
|
||||
_ = x[actTogglePreviewWrap-66]
|
||||
_ = x[actTransform-67]
|
||||
_ = x[actTransformBorderLabel-68]
|
||||
_ = x[actTransformHeader-69]
|
||||
_ = x[actTransformPreviewLabel-70]
|
||||
_ = x[actTransformPrompt-71]
|
||||
_ = x[actTransformQuery-72]
|
||||
_ = x[actPreview-73]
|
||||
_ = x[actChangePreview-74]
|
||||
_ = x[actChangePreviewWindow-75]
|
||||
_ = x[actPreviewTop-76]
|
||||
_ = x[actPreviewBottom-77]
|
||||
_ = x[actPreviewUp-78]
|
||||
_ = x[actPreviewDown-79]
|
||||
_ = x[actPreviewPageUp-80]
|
||||
_ = x[actPreviewPageDown-81]
|
||||
_ = x[actPreviewHalfPageUp-82]
|
||||
_ = x[actPreviewHalfPageDown-83]
|
||||
_ = x[actPrevHistory-84]
|
||||
_ = x[actPrevSelected-85]
|
||||
_ = x[actPut-86]
|
||||
_ = x[actNextHistory-87]
|
||||
_ = x[actNextSelected-88]
|
||||
_ = x[actExecute-89]
|
||||
_ = x[actExecuteSilent-90]
|
||||
_ = x[actExecuteMulti-91]
|
||||
_ = x[actSigStop-92]
|
||||
_ = x[actFirst-93]
|
||||
_ = x[actLast-94]
|
||||
_ = x[actReload-95]
|
||||
_ = x[actReloadSync-96]
|
||||
_ = x[actDisableSearch-97]
|
||||
_ = x[actEnableSearch-98]
|
||||
_ = x[actSelect-99]
|
||||
_ = x[actDeselect-100]
|
||||
_ = x[actUnbind-101]
|
||||
_ = x[actRebind-102]
|
||||
_ = x[actBecome-103]
|
||||
_ = x[actResponse-104]
|
||||
_ = x[actShowHeader-105]
|
||||
_ = x[actHideHeader-106]
|
||||
_ = 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[actForwardChar-29]
|
||||
_ = x[actForwardWord-30]
|
||||
_ = x[actKillLine-31]
|
||||
_ = x[actKillWord-32]
|
||||
_ = x[actUnixLineDiscard-33]
|
||||
_ = x[actUnixWordRubout-34]
|
||||
_ = x[actYank-35]
|
||||
_ = x[actBackwardKillWord-36]
|
||||
_ = x[actSelectAll-37]
|
||||
_ = x[actDeselectAll-38]
|
||||
_ = x[actToggle-39]
|
||||
_ = x[actToggleSearch-40]
|
||||
_ = x[actToggleAll-41]
|
||||
_ = x[actToggleDown-42]
|
||||
_ = x[actToggleUp-43]
|
||||
_ = x[actToggleIn-44]
|
||||
_ = x[actToggleOut-45]
|
||||
_ = x[actToggleTrack-46]
|
||||
_ = x[actToggleTrackCurrent-47]
|
||||
_ = x[actToggleHeader-48]
|
||||
_ = x[actTrackCurrent-49]
|
||||
_ = x[actUntrackCurrent-50]
|
||||
_ = x[actDown-51]
|
||||
_ = x[actUp-52]
|
||||
_ = x[actPageUp-53]
|
||||
_ = x[actPageDown-54]
|
||||
_ = x[actPosition-55]
|
||||
_ = x[actHalfPageUp-56]
|
||||
_ = x[actHalfPageDown-57]
|
||||
_ = x[actOffsetUp-58]
|
||||
_ = x[actOffsetDown-59]
|
||||
_ = x[actJump-60]
|
||||
_ = x[actJumpAccept-61]
|
||||
_ = x[actPrintQuery-62]
|
||||
_ = x[actRefreshPreview-63]
|
||||
_ = x[actReplaceQuery-64]
|
||||
_ = x[actToggleSort-65]
|
||||
_ = x[actShowPreview-66]
|
||||
_ = x[actHidePreview-67]
|
||||
_ = x[actTogglePreview-68]
|
||||
_ = x[actTogglePreviewWrap-69]
|
||||
_ = x[actTransform-70]
|
||||
_ = x[actTransformBorderLabel-71]
|
||||
_ = x[actTransformHeader-72]
|
||||
_ = x[actTransformPreviewLabel-73]
|
||||
_ = x[actTransformPrompt-74]
|
||||
_ = x[actTransformQuery-75]
|
||||
_ = x[actPreview-76]
|
||||
_ = x[actChangePreview-77]
|
||||
_ = x[actChangePreviewWindow-78]
|
||||
_ = x[actPreviewTop-79]
|
||||
_ = x[actPreviewBottom-80]
|
||||
_ = x[actPreviewUp-81]
|
||||
_ = x[actPreviewDown-82]
|
||||
_ = x[actPreviewPageUp-83]
|
||||
_ = x[actPreviewPageDown-84]
|
||||
_ = x[actPreviewHalfPageUp-85]
|
||||
_ = x[actPreviewHalfPageDown-86]
|
||||
_ = x[actPrevHistory-87]
|
||||
_ = x[actPrevSelected-88]
|
||||
_ = x[actPut-89]
|
||||
_ = x[actNextHistory-90]
|
||||
_ = x[actNextSelected-91]
|
||||
_ = x[actExecute-92]
|
||||
_ = x[actExecuteSilent-93]
|
||||
_ = x[actExecuteMulti-94]
|
||||
_ = x[actSigStop-95]
|
||||
_ = x[actFirst-96]
|
||||
_ = x[actLast-97]
|
||||
_ = x[actReload-98]
|
||||
_ = x[actReloadSync-99]
|
||||
_ = x[actDisableSearch-100]
|
||||
_ = x[actEnableSearch-101]
|
||||
_ = x[actSelect-102]
|
||||
_ = x[actDeselect-103]
|
||||
_ = x[actUnbind-104]
|
||||
_ = x[actRebind-105]
|
||||
_ = x[actBecome-106]
|
||||
_ = x[actResponse-107]
|
||||
_ = x[actShowHeader-108]
|
||||
_ = x[actHideHeader-109]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleHeaderactTrackactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 263, 278, 292, 306, 319, 336, 344, 357, 373, 385, 399, 413, 424, 435, 453, 470, 477, 496, 508, 522, 531, 546, 558, 571, 582, 593, 605, 619, 634, 642, 649, 654, 663, 674, 685, 698, 713, 724, 737, 744, 757, 770, 787, 802, 815, 829, 843, 859, 879, 891, 914, 932, 956, 974, 991, 1001, 1017, 1039, 1052, 1068, 1080, 1094, 1110, 1128, 1148, 1170, 1184, 1199, 1205, 1219, 1234, 1244, 1260, 1275, 1285, 1293, 1300, 1309, 1322, 1338, 1353, 1362, 1373, 1382, 1391, 1400, 1411, 1424, 1437}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 413, 427, 438, 449, 467, 484, 491, 510, 522, 536, 545, 560, 572, 585, 596, 607, 619, 633, 654, 669, 684, 701, 708, 713, 722, 733, 744, 757, 772, 783, 796, 803, 816, 829, 846, 861, 874, 888, 902, 918, 938, 950, 973, 991, 1015, 1033, 1050, 1060, 1076, 1098, 1111, 1127, 1139, 1153, 1169, 1187, 1207, 1229, 1243, 1258, 1264, 1278, 1293, 1303, 1319, 1334, 1344, 1352, 1359, 1368, 1381, 1397, 1412, 1421, 1432, 1441, 1450, 1459, 1470, 1483, 1496}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
133
src/algo/algo.go
133
src/algo/algo.go
@@ -153,6 +153,12 @@ var (
|
||||
bonusBoundaryDelimiter int16 = bonusBoundary + 1
|
||||
|
||||
initialCharClass charClass = charWhite
|
||||
|
||||
// A minor optimization that can give 15%+ performance boost
|
||||
asciiCharClasses [unicode.MaxASCII + 1]charClass
|
||||
|
||||
// A minor optimization that can give yet another 5% performance boost
|
||||
bonusMatrix [charNumber + 1][charNumber + 1]int16
|
||||
)
|
||||
|
||||
type charClass int
|
||||
@@ -187,6 +193,27 @@ func Init(scheme string) bool {
|
||||
default:
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= unicode.MaxASCII; i++ {
|
||||
char := rune(i)
|
||||
c := charNonWord
|
||||
if char >= 'a' && char <= 'z' {
|
||||
c = charLower
|
||||
} else if char >= 'A' && char <= 'Z' {
|
||||
c = charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
c = charNumber
|
||||
} else if strings.ContainsRune(whiteChars, char) {
|
||||
c = charWhite
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
c = charDelimiter
|
||||
}
|
||||
asciiCharClasses[i] = c
|
||||
}
|
||||
for i := 0; i <= int(charNumber); i++ {
|
||||
for j := 0; j <= int(charNumber); j++ {
|
||||
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -214,21 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
|
||||
return offset, make([]int32, size)
|
||||
}
|
||||
|
||||
func charClassOfAscii(char rune) charClass {
|
||||
if char >= 'a' && char <= 'z' {
|
||||
return charLower
|
||||
} else if char >= 'A' && char <= 'Z' {
|
||||
return charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
return charNumber
|
||||
} else if strings.ContainsRune(whiteChars, char) {
|
||||
return charWhite
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
|
||||
func charClassOfNonAscii(char rune) charClass {
|
||||
if unicode.IsLower(char) {
|
||||
return charLower
|
||||
@@ -248,7 +260,7 @@ func charClassOfNonAscii(char rune) charClass {
|
||||
|
||||
func charClassOf(char rune) charClass {
|
||||
if char <= unicode.MaxASCII {
|
||||
return charClassOfAscii(char)
|
||||
return asciiCharClasses[char]
|
||||
}
|
||||
return charClassOfNonAscii(char)
|
||||
}
|
||||
@@ -287,7 +299,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
|
||||
if idx == 0 {
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||
return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
@@ -340,30 +352,45 @@ func isAscii(runes []rune) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
|
||||
// Can't determine
|
||||
if !input.IsBytes() {
|
||||
return 0
|
||||
return 0, input.Length()
|
||||
}
|
||||
|
||||
// Not possible
|
||||
if !isAscii(pattern) {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
firstIdx, idx := 0, 0
|
||||
firstIdx, idx, lastIdx := 0, 0, 0
|
||||
var b byte
|
||||
for pidx := 0; pidx < len(pattern); pidx++ {
|
||||
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
|
||||
b = byte(pattern[pidx])
|
||||
idx = trySkip(input, caseSensitive, b, idx)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
if pidx == 0 && idx > 0 {
|
||||
// Step back to find the right bonus point
|
||||
firstIdx = idx - 1
|
||||
}
|
||||
lastIdx = idx
|
||||
idx++
|
||||
}
|
||||
return firstIdx
|
||||
|
||||
// Find the last appearance of the last character of the pattern to limit the search scope
|
||||
bu := b
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
bu = b - 32
|
||||
}
|
||||
scope := input.Bytes()[lastIdx:]
|
||||
for offset := len(scope) - 1; offset > 0; offset-- {
|
||||
if scope[offset] == b || scope[offset] == bu {
|
||||
return firstIdx, lastIdx + offset + 1
|
||||
}
|
||||
}
|
||||
return firstIdx, lastIdx + 1
|
||||
}
|
||||
|
||||
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
|
||||
@@ -412,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
return Result{0, 0, 0}, posArray(withPos, M)
|
||||
}
|
||||
N := input.Length()
|
||||
if M > N {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||
// we fall back to the greedy algorithm.
|
||||
@@ -420,10 +450,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
}
|
||||
|
||||
// Phase 1. Optimized search for ASCII string
|
||||
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if minIdx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
|
||||
N = maxIdx - minIdx
|
||||
|
||||
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
||||
offset16 := 0
|
||||
@@ -436,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
offset32, F := alloc32(offset32, slab, M)
|
||||
// Rune array
|
||||
_, T := alloc32(offset32, slab, N)
|
||||
input.CopyRunes(T)
|
||||
input.CopyRunes(T, minIdx)
|
||||
|
||||
// Phase 2. Calculate bonus for each point
|
||||
maxScore, maxScorePos := int16(0), 0
|
||||
pidx, lastIdx := 0, 0
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
|
||||
Tsub := T[idx:]
|
||||
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
|
||||
for off, char := range Tsub {
|
||||
for off, char := range T {
|
||||
var class charClass
|
||||
if char <= unicode.MaxASCII {
|
||||
class = charClassOfAscii(char)
|
||||
class = asciiCharClasses[char]
|
||||
if !caseSensitive && class == charUpper {
|
||||
char += 32
|
||||
T[off] = char
|
||||
}
|
||||
} else {
|
||||
class = charClassOfNonAscii(char)
|
||||
@@ -459,28 +490,28 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
T[off] = char
|
||||
}
|
||||
|
||||
Tsub[off] = char
|
||||
bonus := bonusFor(prevClass, class)
|
||||
Bsub[off] = bonus
|
||||
bonus := bonusMatrix[prevClass][class]
|
||||
B[off] = bonus
|
||||
prevClass = class
|
||||
|
||||
if char == pchar {
|
||||
if pidx < M {
|
||||
F[pidx] = int32(idx + off)
|
||||
F[pidx] = int32(off)
|
||||
pidx++
|
||||
pchar = pattern[util.Min(pidx, M-1)]
|
||||
}
|
||||
lastIdx = idx + off
|
||||
lastIdx = off
|
||||
}
|
||||
|
||||
if char == pchar0 {
|
||||
score := scoreMatch + bonus*bonusFirstCharMultiplier
|
||||
H0sub[off] = score
|
||||
C0sub[off] = 1
|
||||
H0[off] = score
|
||||
C0[off] = 1
|
||||
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, idx+off
|
||||
maxScore, maxScorePos = score, off
|
||||
if forward && bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
@@ -488,24 +519,24 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
inGap = false
|
||||
} else {
|
||||
if inGap {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||
} else {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
}
|
||||
C0sub[off] = 0
|
||||
C0[off] = 0
|
||||
inGap = true
|
||||
}
|
||||
prevH0 = H0sub[off]
|
||||
prevH0 = H0[off]
|
||||
}
|
||||
if pidx != M {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
if M == 1 {
|
||||
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
|
||||
result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
|
||||
if !withPos {
|
||||
return result, nil
|
||||
}
|
||||
pos := []int{maxScorePos}
|
||||
pos := []int{minIdx + maxScorePos}
|
||||
return result, &pos
|
||||
}
|
||||
|
||||
@@ -602,7 +633,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
}
|
||||
|
||||
if s > s1 && (s > s2 || s == s2 && preferMatch) {
|
||||
*pos = append(*pos, j)
|
||||
*pos = append(*pos, j+minIdx)
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
@@ -615,7 +646,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
// Start offset we return here is only relevant when begin tiebreak is used.
|
||||
// However finding the accurate offset requires backtracking, and we don't
|
||||
// want to pay extra cost for the option that has lost its importance.
|
||||
return Result{j, maxScorePos + 1, int(maxScore)}, pos
|
||||
return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
|
||||
}
|
||||
|
||||
// Implement the same sorting criteria as V2
|
||||
@@ -645,7 +676,7 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
*pos = append(*pos, idx)
|
||||
}
|
||||
score += scoreMatch
|
||||
bonus := bonusFor(prevClass, class)
|
||||
bonus := bonusMatrix[prevClass][class]
|
||||
if consecutive == 0 {
|
||||
firstBonus = bonus
|
||||
} else {
|
||||
@@ -683,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
@@ -777,7 +809,8 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
|
@@ -9,6 +9,10 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Init("default")
|
||||
}
|
||||
|
||||
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
|
||||
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
|
||||
}
|
||||
|
@@ -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 []byte(s) {
|
||||
for _, ch := range sbytes(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, delimiter, remaining
|
||||
|
@@ -14,6 +14,7 @@ const (
|
||||
|
||||
// Reader
|
||||
readerBufferSize = 64 * 1024
|
||||
readerSlabSize = 128 * 1024
|
||||
readerPollIntervalMin = 10 * time.Millisecond
|
||||
readerPollIntervalStep = 5 * time.Millisecond
|
||||
readerPollIntervalMax = 50 * time.Millisecond
|
||||
|
71
src/core.go
71
src/core.go
@@ -3,8 +3,9 @@ package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
@@ -18,8 +19,18 @@ 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) {
|
||||
defer util.RunAtExitFuncs()
|
||||
|
||||
sort := opts.Sort > 0
|
||||
sortCriteria = opts.Criteria
|
||||
|
||||
@@ -29,7 +40,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
} else {
|
||||
fmt.Println(version)
|
||||
}
|
||||
os.Exit(exitOk)
|
||||
util.Exit(exitOk)
|
||||
}
|
||||
|
||||
// Event channel
|
||||
@@ -45,16 +56,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(string(data), lineAnsiState, nil)
|
||||
trimmed, offsets, newState := extractColor(ustring(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
return util.ToChars([]byte(trimmed)), offsets
|
||||
return util.ToChars(sbytes(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(string(data), nil, nil)
|
||||
return util.ToChars([]byte(trimmed)), nil
|
||||
trimmed, _, _ := extractColor(ustring(data), nil, nil)
|
||||
return util.ToChars(sbytes(trimmed)), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,7 +77,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, string(data))
|
||||
header = append(header, ustring(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
@@ -77,7 +88,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(string(data), opts.Delimiter)
|
||||
tokens := Tokenize(ustring(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
if prevLineAnsiState != nil {
|
||||
@@ -101,7 +112,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
item.text, item.colors = ansiProcessor([]byte(transformed))
|
||||
item.text, item.colors = ansiProcessor(sbytes(transformed))
|
||||
item.text.TrimTrailingWhitespaces()
|
||||
item.text.Index = itemIndex
|
||||
item.origText = &data
|
||||
@@ -110,13 +121,16 @@ 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)
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
go reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
}
|
||||
|
||||
@@ -154,17 +168,20 @@ func Run(opts *Options, version string, revision string) {
|
||||
found := false
|
||||
if streamingFilter {
|
||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||
mutex := sync.Mutex{}
|
||||
reader := NewReader(
|
||||
func(runes []byte) bool {
|
||||
item := Item{}
|
||||
if chunkList.trans(&item, runes) {
|
||||
mutex.Lock()
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||
opts.Printer(item.text.ToString())
|
||||
found = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
return false
|
||||
}, eventBox, opts.ReadZero, false)
|
||||
}, eventBox, executor, opts.ReadZero, false)
|
||||
reader.ReadSource(opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
@@ -180,9 +197,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
}
|
||||
if found {
|
||||
os.Exit(exitOk)
|
||||
util.Exit(exitOk)
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
util.Exit(exitNoMatch)
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
@@ -195,7 +212,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
go matcher.Loop()
|
||||
|
||||
// Terminal I/O
|
||||
terminal := NewTerminal(opts, eventBox)
|
||||
terminal := NewTerminal(opts, eventBox, executor)
|
||||
maxFit := 0 // Maximum number of items that can fit on screen
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
@@ -245,11 +262,8 @@ func Run(opts *Options, version string, revision string) {
|
||||
delay := true
|
||||
ticks++
|
||||
input := func() []rune {
|
||||
reloaded := snapshotRevision != inputRevision
|
||||
paused, input := terminal.Input()
|
||||
if reloaded && paused {
|
||||
query = []rune{}
|
||||
} else if !paused {
|
||||
if !paused {
|
||||
query = input
|
||||
}
|
||||
return query
|
||||
@@ -264,7 +278,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
if reading {
|
||||
reader.terminate()
|
||||
}
|
||||
os.Exit(value.(int))
|
||||
util.Exit(value.(int))
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand, nextEnviron)
|
||||
@@ -278,14 +292,16 @@ func Run(opts *Options, version string, revision string) {
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
if snapshotRevision != inputRevision {
|
||||
query = []rune{}
|
||||
}
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
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)
|
||||
@@ -319,10 +335,13 @@ func Run(opts *Options, version string, revision string) {
|
||||
break
|
||||
}
|
||||
if !useSnapshot {
|
||||
newSnapshot, _ := chunkList.Snapshot()
|
||||
newSnapshot, newCount := chunkList.Snapshot()
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || len(newSnapshot) > 0 {
|
||||
if command == nil || newCount > 0 {
|
||||
if snapshotRevision != inputRevision {
|
||||
query = []rune{}
|
||||
}
|
||||
snapshot = newSnapshot
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
@@ -360,14 +379,14 @@ func Run(opts *Options, version string, revision string) {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
}
|
||||
if count > 0 {
|
||||
os.Exit(exitOk)
|
||||
util.Exit(exitOk)
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
util.Exit(exitNoMatch)
|
||||
}
|
||||
determine(val.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val)
|
||||
terminal.UpdateList(val, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
132
src/options.go
132
src/options.go
@@ -52,12 +52,12 @@ const usage = `usage: fzf [options]
|
||||
--hscroll-off=COLS Number of screen columns to keep to the right of the
|
||||
highlighted substring (default: 10)
|
||||
--filepath-word Make word-wise movements respect path separators
|
||||
--jump-labels=CHARS Label characters for jump and jump-accept
|
||||
--jump-labels=CHARS Label characters for jump mode
|
||||
|
||||
Layout
|
||||
--height=[~]HEIGHT[%] Display fzf window below the cursor with the given
|
||||
height instead of using fullscreen.
|
||||
A negative value is calcalated as the terminal height
|
||||
A negative value is calculated as the terminal height
|
||||
minus the given value.
|
||||
If prefixed with '~', fzf will determine the height
|
||||
according to the input size.
|
||||
@@ -75,7 +75,7 @@ const usage = `usage: fzf [options]
|
||||
--margin=MARGIN Screen margin (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--padding=PADDING Padding inside border (TRBL | TB,RL | T,RL,B | T,R,B,L)
|
||||
--info=STYLE Finder info style
|
||||
[default|right|hidden|inline[:SEPARATOR]|inline-right]
|
||||
[default|right|hidden|inline[-right][:PREFIX]]
|
||||
--separator=STR String to form horizontal separator on info line
|
||||
--no-separator Hide info line separator
|
||||
--scrollbar[=C1[C2]] Scrollbar character(s) (each for main and preview window)
|
||||
@@ -120,6 +120,7 @@ const usage = `usage: fzf [options]
|
||||
--read0 Read input delimited by ASCII NUL characters
|
||||
--print0 Print output delimited by ASCII NUL characters
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--with-shell=STR Shell command and flags to start child processes with
|
||||
--listen[=[ADDR:]PORT] Start HTTP server to receive actions (POST /)
|
||||
(To allow remote process execution, use --listen-unsafe)
|
||||
--version Display version information and exit
|
||||
@@ -143,7 +144,7 @@ const usage = `usage: fzf [options]
|
||||
|
||||
`
|
||||
|
||||
const defaultInfoSep = " < "
|
||||
const defaultInfoPrefix = " < "
|
||||
|
||||
// Case denotes case-sensitivity of search
|
||||
type Case int
|
||||
@@ -217,10 +218,6 @@ const (
|
||||
infoHidden
|
||||
)
|
||||
|
||||
func (s infoStyle) noExtraLine() bool {
|
||||
return s == infoInline || s == infoInlineRight || s == infoHidden
|
||||
}
|
||||
|
||||
type labelOpts struct {
|
||||
label string
|
||||
column int
|
||||
@@ -327,7 +324,7 @@ type Options struct {
|
||||
ScrollOff int
|
||||
FileWord bool
|
||||
InfoStyle infoStyle
|
||||
InfoSep string
|
||||
InfoPrefix string
|
||||
Separator *string
|
||||
JumpLabels string
|
||||
Prompt string
|
||||
@@ -360,6 +357,7 @@ type Options struct {
|
||||
Unicode bool
|
||||
Ambidouble bool
|
||||
Tabstop int
|
||||
WithShell string
|
||||
ListenAddr *listenAddress
|
||||
Unsafe bool
|
||||
ClearOnExit bool
|
||||
@@ -367,6 +365,10 @@ type Options struct {
|
||||
WalkerRoot string
|
||||
WalkerSkip []string
|
||||
Version bool
|
||||
CPUProfile string
|
||||
MEMProfile string
|
||||
BlockProfile string
|
||||
MutexProfile string
|
||||
}
|
||||
|
||||
func filterNonEmpty(input []string) []string {
|
||||
@@ -458,14 +460,14 @@ func defaultOptions() *Options {
|
||||
|
||||
func help(code int) {
|
||||
os.Stdout.WriteString(usage)
|
||||
os.Exit(code)
|
||||
util.Exit(code)
|
||||
}
|
||||
|
||||
var errorContext = ""
|
||||
|
||||
func errorExit(msg string) {
|
||||
os.Stderr.WriteString(errorContext + msg + "\n")
|
||||
os.Exit(exitError)
|
||||
util.Exit(exitError)
|
||||
}
|
||||
|
||||
func optString(arg string, prefixes ...string) (bool, string) {
|
||||
@@ -670,8 +672,8 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.CtrlM)
|
||||
case "space":
|
||||
chords[tui.Key(' ')] = key
|
||||
case "bspace", "bs":
|
||||
add(tui.BSpace)
|
||||
case "backspace", "bspace", "bs":
|
||||
add(tui.Backspace)
|
||||
case "ctrl-space":
|
||||
add(tui.CtrlSpace)
|
||||
case "ctrl-delete":
|
||||
@@ -702,12 +704,16 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
add(tui.One)
|
||||
case "zero":
|
||||
add(tui.Zero)
|
||||
case "jump":
|
||||
add(tui.Jump)
|
||||
case "jump-cancel":
|
||||
add(tui.JumpCancel)
|
||||
case "alt-enter", "alt-return":
|
||||
chords[tui.CtrlAltKey('m')] = key
|
||||
case "alt-space":
|
||||
chords[tui.AltKey(' ')] = key
|
||||
case "alt-bs", "alt-bspace":
|
||||
add(tui.AltBS)
|
||||
case "alt-bs", "alt-bspace", "alt-backspace":
|
||||
add(tui.AltBackspace)
|
||||
case "alt-up":
|
||||
add(tui.AltUp)
|
||||
case "alt-down":
|
||||
@@ -719,11 +725,11 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
case "tab":
|
||||
add(tui.Tab)
|
||||
case "btab", "shift-tab":
|
||||
add(tui.BTab)
|
||||
add(tui.ShiftTab)
|
||||
case "esc":
|
||||
add(tui.ESC)
|
||||
case "del":
|
||||
add(tui.Del)
|
||||
add(tui.Esc)
|
||||
case "delete", "del":
|
||||
add(tui.Delete)
|
||||
case "home":
|
||||
add(tui.Home)
|
||||
case "end":
|
||||
@@ -731,27 +737,27 @@ func parseKeyChordsImpl(str string, message string, exit func(string)) map[tui.E
|
||||
case "insert":
|
||||
add(tui.Insert)
|
||||
case "pgup", "page-up":
|
||||
add(tui.PgUp)
|
||||
add(tui.PageUp)
|
||||
case "pgdn", "page-down":
|
||||
add(tui.PgDn)
|
||||
add(tui.PageDown)
|
||||
case "alt-shift-up", "shift-alt-up":
|
||||
add(tui.AltSUp)
|
||||
add(tui.AltShiftUp)
|
||||
case "alt-shift-down", "shift-alt-down":
|
||||
add(tui.AltSDown)
|
||||
add(tui.AltShiftDown)
|
||||
case "alt-shift-left", "shift-alt-left":
|
||||
add(tui.AltSLeft)
|
||||
add(tui.AltShiftLeft)
|
||||
case "alt-shift-right", "shift-alt-right":
|
||||
add(tui.AltSRight)
|
||||
add(tui.AltShiftRight)
|
||||
case "shift-up":
|
||||
add(tui.SUp)
|
||||
add(tui.ShiftUp)
|
||||
case "shift-down":
|
||||
add(tui.SDown)
|
||||
add(tui.ShiftDown)
|
||||
case "shift-left":
|
||||
add(tui.SLeft)
|
||||
add(tui.ShiftLeft)
|
||||
case "shift-right":
|
||||
add(tui.SRight)
|
||||
add(tui.ShiftRight)
|
||||
case "shift-delete":
|
||||
add(tui.SDelete)
|
||||
add(tui.ShiftDelete)
|
||||
case "left-click":
|
||||
add(tui.LeftClick)
|
||||
case "right-click":
|
||||
@@ -1051,7 +1057,7 @@ const (
|
||||
|
||||
func init() {
|
||||
executeRegexp = regexp.MustCompile(
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-preview-window|change-preview|(?:re|un)bind|pos|put)`)
|
||||
`(?si)[:+](become|execute(?:-multi|-silent)?|reload(?:-sync)?|preview|(?:change|transform)-(?:header|query|prompt|border-label|preview-label)|transform|change-(?:preview-window|preview|multi)|(?:re|un)bind|pos|put)`)
|
||||
splitRegexp = regexp.MustCompile("[,:]+")
|
||||
actionNameRegexp = regexp.MustCompile("(?i)^[a-z-]+")
|
||||
}
|
||||
@@ -1211,14 +1217,18 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
appendAction(actToggleSearch)
|
||||
case "toggle-track":
|
||||
appendAction(actToggleTrack)
|
||||
case "toggle-track-current":
|
||||
appendAction(actToggleTrackCurrent)
|
||||
case "toggle-header":
|
||||
appendAction(actToggleHeader)
|
||||
case "show-header":
|
||||
appendAction(actShowHeader)
|
||||
case "hide-header":
|
||||
appendAction(actHideHeader)
|
||||
case "track":
|
||||
appendAction(actTrack)
|
||||
case "track", "track-current":
|
||||
appendAction(actTrackCurrent)
|
||||
case "untrack-current":
|
||||
appendAction(actUntrackCurrent)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
@@ -1298,6 +1308,8 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
if t == actIgnore {
|
||||
if specIndex == 0 && specLower == "" {
|
||||
actions = append(prevActions, actions...)
|
||||
} else if specLower == "change-multi" {
|
||||
appendAction(actChangeMulti)
|
||||
} else {
|
||||
exit("unknown action: " + spec)
|
||||
}
|
||||
@@ -1317,10 +1329,6 @@ func parseActionList(masked string, original string, prevActions []*action, putA
|
||||
actions = append(actions, &action{t: t, a: actionArg})
|
||||
}
|
||||
switch t {
|
||||
case actBecome:
|
||||
if util.IsWindows() {
|
||||
exit("become action is not supported on Windows")
|
||||
}
|
||||
case actUnbind, actRebind:
|
||||
parseKeyChordsImpl(actionArg, spec[0:offset]+" target required", exit)
|
||||
case actChangePreviewWindow:
|
||||
@@ -1399,6 +1407,8 @@ func isExecuteAction(str string) actionType {
|
||||
return actChangePrompt
|
||||
case "change-query":
|
||||
return actChangeQuery
|
||||
case "change-multi":
|
||||
return actChangeMulti
|
||||
case "pos":
|
||||
return actPosition
|
||||
case "execute":
|
||||
@@ -1502,17 +1512,24 @@ func parseInfoStyle(str string) (infoStyle, string) {
|
||||
case "right":
|
||||
return infoRight, ""
|
||||
case "inline":
|
||||
return infoInline, defaultInfoSep
|
||||
return infoInline, defaultInfoPrefix
|
||||
case "inline-right":
|
||||
return infoInlineRight, ""
|
||||
case "hidden":
|
||||
return infoHidden, ""
|
||||
default:
|
||||
prefix := "inline:"
|
||||
if strings.HasPrefix(str, prefix) {
|
||||
return infoInline, strings.ReplaceAll(str[len(prefix):], "\n", " ")
|
||||
type infoSpec struct {
|
||||
name string
|
||||
style infoStyle
|
||||
}
|
||||
errorExit("invalid info style (expected: default|right|hidden|inline[:SEPARATOR]|inline-right)")
|
||||
for _, spec := range []infoSpec{
|
||||
{"inline", infoInline},
|
||||
{"inline-right", infoInlineRight}} {
|
||||
if strings.HasPrefix(str, spec.name+":") {
|
||||
return spec.style, strings.ReplaceAll(str[len(spec.name)+1:], "\n", " ")
|
||||
}
|
||||
}
|
||||
errorExit("invalid info style (expected: default|right|hidden|inline[-right][:PREFIX])")
|
||||
}
|
||||
return infoDefault, ""
|
||||
}
|
||||
@@ -1803,13 +1820,13 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
case "--no-filepath-word":
|
||||
opts.FileWord = false
|
||||
case "--info":
|
||||
opts.InfoStyle, opts.InfoSep = parseInfoStyle(
|
||||
opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(
|
||||
nextString(allArgs, &i, "info style required"))
|
||||
case "--no-info":
|
||||
opts.InfoStyle = infoHidden
|
||||
case "--inline-info":
|
||||
opts.InfoStyle = infoInline
|
||||
opts.InfoSep = defaultInfoSep
|
||||
opts.InfoPrefix = defaultInfoPrefix
|
||||
case "--no-inline-info":
|
||||
opts.InfoStyle = infoDefault
|
||||
case "--separator":
|
||||
@@ -1861,9 +1878,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Marker = firstLine(nextString(allArgs, &i, "selected sign string required"))
|
||||
case "--sync":
|
||||
opts.Sync = true
|
||||
case "--no-sync":
|
||||
opts.Sync = false
|
||||
case "--async":
|
||||
case "--no-sync", "--async":
|
||||
opts.Sync = false
|
||||
case "--no-history":
|
||||
opts.History = nil
|
||||
@@ -1940,6 +1955,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
nextString(allArgs, &i, "padding required (TRBL / TB,RL / T,RL,B / T,R,B,L)"))
|
||||
case "--tabstop":
|
||||
opts.Tabstop = nextInt(allArgs, &i, "tab stop required")
|
||||
case "--with-shell":
|
||||
opts.WithShell = nextString(allArgs, &i, "shell command and flags required")
|
||||
case "--listen", "--listen-unsafe":
|
||||
given, str := optionalNextString(allArgs, &i)
|
||||
addr := defaultListenAddr
|
||||
@@ -1967,6 +1984,14 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.WalkerSkip = filterNonEmpty(strings.Split(nextString(allArgs, &i, "directory names to ignore required"), ","))
|
||||
case "--version":
|
||||
opts.Version = true
|
||||
case "--profile-cpu":
|
||||
opts.CPUProfile = nextString(allArgs, &i, "file path required: cpu")
|
||||
case "--profile-mem":
|
||||
opts.MEMProfile = nextString(allArgs, &i, "file path required: mem")
|
||||
case "--profile-block":
|
||||
opts.BlockProfile = nextString(allArgs, &i, "file path required: block")
|
||||
case "--profile-mutex":
|
||||
opts.MutexProfile = nextString(allArgs, &i, "file path required: mutex")
|
||||
case "--":
|
||||
// Ignored
|
||||
default:
|
||||
@@ -2011,7 +2036,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
} else if match, value := optString(arg, "--layout="); match {
|
||||
opts.Layout = parseLayout(value)
|
||||
} else if match, value := optString(arg, "--info="); match {
|
||||
opts.InfoStyle, opts.InfoSep = parseInfoStyle(value)
|
||||
opts.InfoStyle, opts.InfoPrefix = parseInfoStyle(value)
|
||||
} else if match, value := optString(arg, "--separator="); match {
|
||||
opts.Separator = &value
|
||||
} else if match, value := optString(arg, "--scrollbar="); match {
|
||||
@@ -2048,6 +2073,8 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Padding = parseMargin("padding", value)
|
||||
} else if match, value := optString(arg, "--tabstop="); match {
|
||||
opts.Tabstop = atoi(value)
|
||||
} else if match, value := optString(arg, "--with-shell="); match {
|
||||
opts.WithShell = value
|
||||
} else if match, value := optString(arg, "--listen="); match {
|
||||
addr, err := parseListenAddress(value)
|
||||
if err != nil {
|
||||
@@ -2236,9 +2263,7 @@ func postProcessOptions(opts *Options) {
|
||||
theme.Spinner = boldify(theme.Spinner)
|
||||
}
|
||||
|
||||
if opts.Scheme != "default" {
|
||||
processScheme(opts)
|
||||
}
|
||||
processScheme(opts)
|
||||
}
|
||||
|
||||
func expectsArbitraryString(opt string) bool {
|
||||
@@ -2292,6 +2317,11 @@ func ParseOptions() *Options {
|
||||
errorContext = ""
|
||||
parseOptions(opts, os.Args[1:])
|
||||
|
||||
if err := opts.initProfiling(); err != nil {
|
||||
errorExit("failed to start pprof profiles: " + err.Error())
|
||||
}
|
||||
|
||||
postProcessOptions(opts)
|
||||
|
||||
return opts
|
||||
}
|
||||
|
11
src/options_no_pprof.go
Normal file
11
src/options_no_pprof.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !pprof
|
||||
// +build !pprof
|
||||
|
||||
package fzf
|
||||
|
||||
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 nil
|
||||
}
|
73
src/options_pprof.go
Normal file
73
src/options_pprof.go
Normal file
@@ -0,0 +1,73 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func (o *Options) initProfiling() error {
|
||||
if o.CPUProfile != "" {
|
||||
f, err := os.Create(o.CPUProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create CPU profile: %w", err)
|
||||
}
|
||||
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profile: %w", err)
|
||||
}
|
||||
|
||||
util.AtExit(func() {
|
||||
pprof.StopCPUProfile()
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: closing cpu profile:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stopProfile := func(name string, f *os.File) {
|
||||
if err := pprof.Lookup(name).WriteTo(f, 0); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: could not write %s profile: %v\n", name, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: closing %s profile: %v\n", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if o.MEMProfile != "" {
|
||||
f, err := os.Create(o.MEMProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create MEM profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() {
|
||||
runtime.GC()
|
||||
stopProfile("allocs", f)
|
||||
})
|
||||
}
|
||||
|
||||
if o.BlockProfile != "" {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
f, err := os.Create(o.BlockProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create BLOCK profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() { stopProfile("block", f) })
|
||||
}
|
||||
|
||||
if o.MutexProfile != "" {
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
f, err := os.Create(o.MutexProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create MUTEX profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() { stopProfile("mutex", f) })
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
89
src/options_pprof_test.go
Normal file
89
src/options_pprof_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// runInitProfileTests is an internal flag used TestInitProfiling
|
||||
var runInitProfileTests = flag.Bool("test-init-profile", false, "run init profile tests")
|
||||
|
||||
func TestInitProfiling(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
// Run this test in a separate process since it interferes with
|
||||
// profiling and modifies the global atexit state. Without this
|
||||
// running `go test -bench . -cpuprofile cpu.out` will fail.
|
||||
if !*runInitProfileTests {
|
||||
t.Parallel()
|
||||
|
||||
// Make sure we are not the child process.
|
||||
if os.Getenv("_FZF_CHILD_PROC") != "" {
|
||||
t.Fatal("already running as child process!")
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0],
|
||||
"-test.timeout", "30s",
|
||||
"-test.run", "^"+t.Name()+"$",
|
||||
"-test-init-profile",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "_FZF_CHILD_PROC=1")
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
out = bytes.TrimSpace(out)
|
||||
if err != nil {
|
||||
t.Fatalf("Child test process failed: %v:\n%s", err, out)
|
||||
}
|
||||
// Make sure the test actually ran
|
||||
if bytes.Contains(out, []byte("no tests to run")) {
|
||||
t.Fatalf("Failed to run test %q:\n%s", t.Name(), out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Child process
|
||||
|
||||
tempdir := t.TempDir()
|
||||
t.Cleanup(util.RunAtExitFuncs)
|
||||
|
||||
o := Options{
|
||||
CPUProfile: filepath.Join(tempdir, "cpu.prof"),
|
||||
MEMProfile: filepath.Join(tempdir, "mem.prof"),
|
||||
BlockProfile: filepath.Join(tempdir, "block.prof"),
|
||||
MutexProfile: filepath.Join(tempdir, "mutex.prof"),
|
||||
}
|
||||
if err := o.initProfiling(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
profiles := []string{
|
||||
o.CPUProfile,
|
||||
o.MEMProfile,
|
||||
o.BlockProfile,
|
||||
o.MutexProfile,
|
||||
}
|
||||
for _, name := range profiles {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Errorf("Failed to create profile %s: %v", filepath.Base(name), err)
|
||||
}
|
||||
}
|
||||
|
||||
util.RunAtExitFuncs()
|
||||
|
||||
for _, name := range profiles {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Errorf("Failed to write profile %s: %v", filepath.Base(name), err)
|
||||
}
|
||||
}
|
||||
}
|
@@ -170,8 +170,8 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.CtrlM, "Return")
|
||||
checkEvent(tui.Key(' '), "space")
|
||||
check(tui.Tab, "tab")
|
||||
check(tui.BTab, "btab")
|
||||
check(tui.ESC, "esc")
|
||||
check(tui.ShiftTab, "btab")
|
||||
check(tui.Esc, "esc")
|
||||
check(tui.Up, "up")
|
||||
check(tui.Down, "down")
|
||||
check(tui.Left, "left")
|
||||
@@ -182,16 +182,16 @@ func TestParseKeys(t *testing.T) {
|
||||
t.Error(11)
|
||||
}
|
||||
check(tui.Tab, "Ctrl-I")
|
||||
check(tui.PgUp, "page-up")
|
||||
check(tui.PgDn, "Page-Down")
|
||||
check(tui.PageUp, "page-up")
|
||||
check(tui.PageDown, "Page-Down")
|
||||
check(tui.Home, "Home")
|
||||
check(tui.End, "End")
|
||||
check(tui.AltBS, "Alt-BSpace")
|
||||
check(tui.SLeft, "shift-left")
|
||||
check(tui.SRight, "shift-right")
|
||||
check(tui.BTab, "shift-tab")
|
||||
check(tui.AltBackspace, "Alt-BSpace")
|
||||
check(tui.ShiftLeft, "shift-left")
|
||||
check(tui.ShiftRight, "shift-right")
|
||||
check(tui.ShiftTab, "shift-tab")
|
||||
check(tui.CtrlM, "Enter")
|
||||
check(tui.BSpace, "bspace")
|
||||
check(tui.Backspace, "bspace")
|
||||
}
|
||||
|
||||
func TestParseKeysWithComma(t *testing.T) {
|
||||
|
101
src/reader.go
101
src/reader.go
@@ -1,7 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"os"
|
||||
@@ -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() {
|
||||
@@ -111,32 +112,90 @@ func (r *Reader) ReadSource(root string, opts walkerOpts, ignores []string) {
|
||||
}
|
||||
|
||||
func (r *Reader) feed(src io.Reader) {
|
||||
/*
|
||||
readerSlabSize, ae := strconv.Atoi(os.Getenv("SLAB_KB"))
|
||||
if ae != nil {
|
||||
readerSlabSize = 128 * 1024
|
||||
} else {
|
||||
readerSlabSize *= 1024
|
||||
}
|
||||
readerBufferSize, be := strconv.Atoi(os.Getenv("BUF_KB"))
|
||||
if be != nil {
|
||||
readerBufferSize = 64 * 1024
|
||||
} else {
|
||||
readerBufferSize *= 1024
|
||||
}
|
||||
*/
|
||||
|
||||
delim := byte('\n')
|
||||
trimCR := util.IsWindows()
|
||||
if r.delimNil {
|
||||
delim = '\000'
|
||||
trimCR = false
|
||||
}
|
||||
reader := bufio.NewReaderSize(src, readerBufferSize)
|
||||
|
||||
slab := make([]byte, readerSlabSize)
|
||||
leftover := []byte{}
|
||||
var err error
|
||||
for {
|
||||
// ReadBytes returns err != nil if and only if the returned data does not
|
||||
// end in delim.
|
||||
bytea, err := reader.ReadBytes(delim)
|
||||
byteaLen := len(bytea)
|
||||
if byteaLen > 0 {
|
||||
if err == nil {
|
||||
// get rid of carriage return if under Windows:
|
||||
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
|
||||
bytea = bytea[:byteaLen-2]
|
||||
} else {
|
||||
bytea = bytea[:byteaLen-1]
|
||||
}
|
||||
}
|
||||
if r.pusher(bytea) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
n := 0
|
||||
scope := slab[:util.Min(len(slab), readerBufferSize)]
|
||||
for i := 0; i < 100; i++ {
|
||||
n, err = src.Read(scope)
|
||||
if n > 0 || err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
// We're not making any progress after 100 tries. Stop.
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
buf := slab[:n]
|
||||
slab = slab[n:]
|
||||
|
||||
for len(buf) > 0 {
|
||||
if i := bytes.IndexByte(buf, delim); i >= 0 {
|
||||
// Found the delimiter
|
||||
slice := buf[:i+1]
|
||||
buf = buf[i+1:]
|
||||
if trimCR && len(slice) >= 2 && slice[len(slice)-2] == byte('\r') {
|
||||
slice = slice[:len(slice)-2]
|
||||
} else {
|
||||
slice = slice[:len(slice)-1]
|
||||
}
|
||||
if len(leftover) > 0 {
|
||||
slice = append(leftover, slice...)
|
||||
leftover = []byte{}
|
||||
}
|
||||
if (err == nil || len(slice) > 0) && r.pusher(slice) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
} else {
|
||||
// Could not find the delimiter in the buffer
|
||||
// NOTE: We can further optimize this by keeping track of the cursor
|
||||
// position in the slab so that a straddling item that doesn't go
|
||||
// beyond the boundary of a slab doesn't need to be copied to
|
||||
// another buffer. However, the performance gain is negligible in
|
||||
// practice (< 0.1%) and is not
|
||||
// worth the added complexity.
|
||||
leftover = append(leftover, buf...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
leftover = append(leftover, buf...)
|
||||
break
|
||||
}
|
||||
|
||||
if len(slab) == 0 {
|
||||
slab = make([]byte, readerSlabSize)
|
||||
}
|
||||
}
|
||||
if len(leftover) > 0 && r.pusher(leftover) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +243,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
|
||||
}
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -32,6 +32,7 @@ const (
|
||||
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
|
||||
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
|
||||
httpReadTimeout = 10 * time.Second
|
||||
channelTimeout = 2 * time.Second
|
||||
jsonContentType = "Content-Type: application/json" + crlf
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
@@ -77,7 +78,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
|
||||
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 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)
|
||||
@@ -170,7 +171,7 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
select {
|
||||
case response := <-server.responseChannel:
|
||||
return good(response)
|
||||
case <-time.After(2 * time.Second):
|
||||
case <-time.After(channelTimeout):
|
||||
go func() {
|
||||
// Drain the channel
|
||||
<-server.responseChannel
|
||||
@@ -227,7 +228,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
return bad("no action specified")
|
||||
}
|
||||
|
||||
server.actionChannel <- actions
|
||||
select {
|
||||
case server.actionChannel <- actions:
|
||||
case <-time.After(channelTimeout):
|
||||
return httpUnavailable + crlf
|
||||
}
|
||||
return httpOk + crlf
|
||||
}
|
||||
|
||||
|
317
src/terminal.go
317
src/terminal.go
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"sort"
|
||||
@@ -55,6 +54,9 @@ var actionTypeRegex *regexp.Regexp
|
||||
|
||||
const clearCode string = "\x1b[2J"
|
||||
|
||||
// Number of maximum focus events to process synchronously
|
||||
const maxFocusEvents = 10000
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{fzf:(?:query|action|prompt)}|{\+?f?nf?})`)
|
||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||
@@ -178,7 +180,7 @@ type Status struct {
|
||||
type Terminal struct {
|
||||
initDelay time.Duration
|
||||
infoStyle infoStyle
|
||||
infoSep string
|
||||
infoPrefix string
|
||||
separator labelPrinter
|
||||
separatorLen int
|
||||
spinner []string
|
||||
@@ -242,6 +244,7 @@ type Terminal struct {
|
||||
listenUnsafe bool
|
||||
borderShape tui.BorderShape
|
||||
cleanExit bool
|
||||
executor *util.Executor
|
||||
paused bool
|
||||
border tui.Window
|
||||
window tui.Window
|
||||
@@ -290,6 +293,7 @@ type Terminal struct {
|
||||
executing *util.AtomicBool
|
||||
termSize tui.TermSize
|
||||
lastAction actionType
|
||||
lastKey string
|
||||
lastFocus int32
|
||||
areaLines int
|
||||
areaColumns int
|
||||
@@ -363,6 +367,7 @@ const (
|
||||
actCancel
|
||||
actChangeBorderLabel
|
||||
actChangeHeader
|
||||
actChangeMulti
|
||||
actChangePreviewLabel
|
||||
actChangePrompt
|
||||
actChangeQuery
|
||||
@@ -391,8 +396,10 @@ const (
|
||||
actToggleIn
|
||||
actToggleOut
|
||||
actToggleTrack
|
||||
actToggleTrackCurrent
|
||||
actToggleHeader
|
||||
actTrack
|
||||
actTrackCurrent
|
||||
actUntrackCurrent
|
||||
actDown
|
||||
actUp
|
||||
actPageUp
|
||||
@@ -403,7 +410,7 @@ const (
|
||||
actOffsetUp
|
||||
actOffsetDown
|
||||
actJump
|
||||
actJumpAccept
|
||||
actJumpAccept // XXX Deprecated in favor of jump:accept binding
|
||||
actPrintQuery
|
||||
actRefreshPreview
|
||||
actReplaceQuery
|
||||
@@ -455,14 +462,7 @@ const (
|
||||
)
|
||||
|
||||
func (a actionType) Name() string {
|
||||
name := ""
|
||||
for i, r := range a.String()[3:] {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
name += "-"
|
||||
}
|
||||
name += string(r)
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
return util.ToKebabCase(a.String()[3:])
|
||||
}
|
||||
|
||||
func processExecution(action actionType) bool {
|
||||
@@ -541,14 +541,14 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
add(tui.CtrlC, actAbort)
|
||||
add(tui.CtrlG, actAbort)
|
||||
add(tui.CtrlQ, actAbort)
|
||||
add(tui.ESC, actAbort)
|
||||
add(tui.Esc, actAbort)
|
||||
add(tui.CtrlD, actDeleteCharEof)
|
||||
add(tui.CtrlE, actEndOfLine)
|
||||
add(tui.CtrlF, actForwardChar)
|
||||
add(tui.CtrlH, actBackwardDeleteChar)
|
||||
add(tui.BSpace, actBackwardDeleteChar)
|
||||
add(tui.Backspace, actBackwardDeleteChar)
|
||||
add(tui.Tab, actToggleDown)
|
||||
add(tui.BTab, actToggleUp)
|
||||
add(tui.ShiftTab, actToggleUp)
|
||||
add(tui.CtrlJ, actDown)
|
||||
add(tui.CtrlK, actUp)
|
||||
add(tui.CtrlL, actClearScreen)
|
||||
@@ -563,11 +563,11 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
}
|
||||
|
||||
addEvent(tui.AltKey('b'), actBackwardWord)
|
||||
add(tui.SLeft, actBackwardWord)
|
||||
add(tui.ShiftLeft, actBackwardWord)
|
||||
addEvent(tui.AltKey('f'), actForwardWord)
|
||||
add(tui.SRight, actForwardWord)
|
||||
add(tui.ShiftRight, actForwardWord)
|
||||
addEvent(tui.AltKey('d'), actKillWord)
|
||||
add(tui.AltBS, actBackwardKillWord)
|
||||
add(tui.AltBackspace, actBackwardKillWord)
|
||||
|
||||
add(tui.Up, actUp)
|
||||
add(tui.Down, actDown)
|
||||
@@ -576,12 +576,12 @@ func defaultKeymap() map[tui.Event][]*action {
|
||||
|
||||
add(tui.Home, actBeginningOfLine)
|
||||
add(tui.End, actEndOfLine)
|
||||
add(tui.Del, actDeleteChar)
|
||||
add(tui.PgUp, actPageUp)
|
||||
add(tui.PgDn, actPageDown)
|
||||
add(tui.Delete, actDeleteChar)
|
||||
add(tui.PageUp, actPageUp)
|
||||
add(tui.PageDown, actPageDown)
|
||||
|
||||
add(tui.SUp, actPreviewUp)
|
||||
add(tui.SDown, actPreviewDown)
|
||||
add(tui.ShiftUp, actPreviewUp)
|
||||
add(tui.ShiftDown, actPreviewDown)
|
||||
|
||||
add(tui.Mouse, actMouse)
|
||||
add(tui.LeftClick, actClick)
|
||||
@@ -640,7 +640,7 @@ func evaluateHeight(opts *Options, termHeight int) int {
|
||||
}
|
||||
|
||||
// NewTerminal returns new Terminal object
|
||||
func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor) *Terminal {
|
||||
input := trimQuery(opts.Query)
|
||||
var delay time.Duration
|
||||
if opts.Tac {
|
||||
@@ -672,7 +672,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
if previewBox != nil && opts.Preview.aboveOrBelow() {
|
||||
effectiveMinHeight += 1 + borderLines(opts.Preview.border)
|
||||
}
|
||||
if opts.InfoStyle.noExtraLine() {
|
||||
if noSeparatorLine(opts.InfoStyle, opts.Separator == nil || uniseg.StringWidth(*opts.Separator) > 0) {
|
||||
effectiveMinHeight--
|
||||
}
|
||||
effectiveMinHeight += borderLines(opts.BorderShape)
|
||||
@@ -694,7 +694,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
t := Terminal{
|
||||
initDelay: delay,
|
||||
infoStyle: opts.InfoStyle,
|
||||
infoSep: opts.InfoSep,
|
||||
infoPrefix: opts.InfoPrefix,
|
||||
separator: nil,
|
||||
spinner: makeSpinner(opts.Unicode),
|
||||
promptString: opts.Prompt,
|
||||
@@ -736,6 +736,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
previewLabel: nil,
|
||||
previewLabelOpts: opts.PreviewLabel,
|
||||
cleanExit: opts.ClearOnExit,
|
||||
executor: executor,
|
||||
paused: opts.Phony,
|
||||
cycle: opts.Cycle,
|
||||
headerVisible: true,
|
||||
@@ -773,7 +774,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
theme: opts.Theme,
|
||||
startChan: make(chan fitpad, 1),
|
||||
killChan: make(chan int),
|
||||
serverInputChan: make(chan []*action, 10),
|
||||
serverInputChan: make(chan []*action, 100),
|
||||
serverOutputChan: make(chan string),
|
||||
eventChan: make(chan tui.Event, 6), // (load + result + zero|one) | (focus) | (resize) | (GetChar)
|
||||
tui: renderer,
|
||||
@@ -846,12 +847,16 @@ func (t *Terminal) environ() []string {
|
||||
}
|
||||
env = append(env, "FZF_QUERY="+string(t.input))
|
||||
env = append(env, "FZF_ACTION="+t.lastAction.Name())
|
||||
env = append(env, "FZF_KEY="+t.lastKey)
|
||||
env = append(env, "FZF_PROMPT="+string(t.promptString))
|
||||
env = append(env, "FZF_PREVIEW_LABEL="+t.previewLabelOpts.label)
|
||||
env = append(env, "FZF_BORDER_LABEL="+t.borderLabelOpts.label)
|
||||
env = append(env, fmt.Sprintf("FZF_TOTAL_COUNT=%d", t.count))
|
||||
env = append(env, fmt.Sprintf("FZF_MATCH_COUNT=%d", t.merger.Length()))
|
||||
env = append(env, fmt.Sprintf("FZF_SELECT_COUNT=%d", len(t.selected)))
|
||||
env = append(env, fmt.Sprintf("FZF_LINES=%d", t.areaLines))
|
||||
env = append(env, fmt.Sprintf("FZF_COLUMNS=%d", t.areaColumns))
|
||||
env = append(env, fmt.Sprintf("FZF_POS=%d", util.Min(t.merger.Length(), t.cy+1)))
|
||||
return env
|
||||
}
|
||||
|
||||
@@ -875,7 +880,7 @@ func (t *Terminal) visibleHeaderLines() int {
|
||||
// Extra number of lines needed to display fzf
|
||||
func (t *Terminal) extraLines() int {
|
||||
extra := t.visibleHeaderLines() + 1
|
||||
if !t.noInfoLine() {
|
||||
if !t.noSeparatorLine() {
|
||||
extra++
|
||||
}
|
||||
return extra
|
||||
@@ -981,8 +986,18 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
||||
return output, promptLen
|
||||
}
|
||||
|
||||
func (t *Terminal) noInfoLine() bool {
|
||||
return t.infoStyle.noExtraLine()
|
||||
func noSeparatorLine(style infoStyle, separator bool) bool {
|
||||
switch style {
|
||||
case infoInline:
|
||||
return true
|
||||
case infoHidden, infoInlineRight:
|
||||
return !separator
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Terminal) noSeparatorLine() bool {
|
||||
return noSeparatorLine(t.infoStyle, t.separatorLen > 0)
|
||||
}
|
||||
|
||||
func getScrollbar(total int, height int, offset int) (int, int) {
|
||||
@@ -1055,13 +1070,13 @@ func (t *Terminal) UpdateProgress(progress float32) {
|
||||
}
|
||||
|
||||
// UpdateList updates Merger to display the list
|
||||
func (t *Terminal) UpdateList(merger *Merger) {
|
||||
func (t *Terminal) UpdateList(merger *Merger, triggerResultEvent bool) {
|
||||
t.mutex.Lock()
|
||||
var prevIndex int32 = -1
|
||||
prevIndex := minItem.Index()
|
||||
reset := t.revision != merger.Revision()
|
||||
if !reset && t.track != trackDisabled {
|
||||
if t.merger.Length() > 0 {
|
||||
prevIndex = t.merger.Get(t.cy).item.Index()
|
||||
prevIndex = t.currentIndex()
|
||||
} else if merger.Length() > 0 {
|
||||
prevIndex = merger.First().item.Index()
|
||||
}
|
||||
@@ -1106,7 +1121,7 @@ func (t *Terminal) UpdateList(merger *Merger) {
|
||||
t.eventChan <- one
|
||||
}
|
||||
}
|
||||
if t.hasResultActions {
|
||||
if triggerResultEvent && t.hasResultActions {
|
||||
t.eventChan <- tui.Result.AsEvent()
|
||||
}
|
||||
}
|
||||
@@ -1234,7 +1249,7 @@ func (t *Terminal) adjustMarginAndPadding() (int, int, [4]int, [4]int) {
|
||||
|
||||
minAreaWidth := minWidth
|
||||
minAreaHeight := minHeight
|
||||
if t.noInfoLine() {
|
||||
if t.noSeparatorLine() {
|
||||
minAreaHeight -= 1
|
||||
}
|
||||
if t.needPreviewWindow() {
|
||||
@@ -1515,7 +1530,7 @@ func (t *Terminal) move(y int, x int, clear bool) {
|
||||
y = h - y - 1
|
||||
case layoutReverseList:
|
||||
n := 2 + t.visibleHeaderLines()
|
||||
if t.noInfoLine() {
|
||||
if t.noSeparatorLine() {
|
||||
n--
|
||||
}
|
||||
if y < n {
|
||||
@@ -1555,7 +1570,7 @@ func (t *Terminal) updatePromptOffset() ([]rune, []rune) {
|
||||
func (t *Terminal) promptLine() int {
|
||||
if t.headerFirst {
|
||||
max := t.window.Height() - 1
|
||||
if !t.noInfoLine() {
|
||||
if !t.noSeparatorLine() {
|
||||
max--
|
||||
}
|
||||
return util.Min(t.visibleHeaderLines(), max)
|
||||
@@ -1600,20 +1615,8 @@ func (t *Terminal) printInfo() {
|
||||
t.window.Print(" ") // Clear spinner
|
||||
}
|
||||
}
|
||||
switch t.infoStyle {
|
||||
case infoDefault:
|
||||
t.move(line+1, 0, t.separatorLen == 0)
|
||||
printSpinner()
|
||||
t.move(line+1, 2, false)
|
||||
pos = 2
|
||||
case infoRight:
|
||||
t.move(line+1, 0, false)
|
||||
case infoInlineRight:
|
||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||
t.move(line, pos, true)
|
||||
case infoInline:
|
||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||
str := t.infoSep
|
||||
printInfoPrefix := func() {
|
||||
str := t.infoPrefix
|
||||
maxWidth := t.window.Width() - pos
|
||||
width := util.StringWidth(str)
|
||||
if width > maxWidth {
|
||||
@@ -1628,7 +1631,34 @@ func (t *Terminal) printInfo() {
|
||||
t.window.CPrint(tui.ColPrompt, str)
|
||||
}
|
||||
pos += width
|
||||
}
|
||||
printSeparator := func(fillLength int, pad bool) {
|
||||
// --------_
|
||||
if t.separatorLen > 0 {
|
||||
t.separator(t.window, fillLength)
|
||||
t.window.Print(" ")
|
||||
} else if pad {
|
||||
t.window.Print(strings.Repeat(" ", fillLength+1))
|
||||
}
|
||||
}
|
||||
switch t.infoStyle {
|
||||
case infoDefault:
|
||||
t.move(line+1, 0, t.separatorLen == 0)
|
||||
printSpinner()
|
||||
t.move(line+1, 2, false)
|
||||
pos = 2
|
||||
case infoRight:
|
||||
t.move(line+1, 0, false)
|
||||
case infoInlineRight:
|
||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||
case infoInline:
|
||||
pos = t.promptLen + t.queryLen[0] + t.queryLen[1] + 1
|
||||
printInfoPrefix()
|
||||
case infoHidden:
|
||||
if t.separatorLen > 0 {
|
||||
t.move(line+1, 0, false)
|
||||
printSeparator(t.window.Width()-1, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1642,8 +1672,11 @@ func (t *Terminal) printInfo() {
|
||||
output += " -S"
|
||||
}
|
||||
}
|
||||
if t.track != trackDisabled {
|
||||
switch t.track {
|
||||
case trackEnabled:
|
||||
output += " +T"
|
||||
case trackCurrent:
|
||||
output += " +t"
|
||||
}
|
||||
if t.multi > 0 {
|
||||
if t.multi == maxMulti {
|
||||
@@ -1659,15 +1692,6 @@ func (t *Terminal) printInfo() {
|
||||
output = fmt.Sprintf("[Command failed: %s]", *t.failed)
|
||||
}
|
||||
|
||||
printSeparator := func(fillLength int, pad bool) {
|
||||
// --------_
|
||||
if t.separatorLen > 0 {
|
||||
t.separator(t.window, fillLength)
|
||||
t.window.Print(" ")
|
||||
} else if pad {
|
||||
t.window.Print(strings.Repeat(" ", fillLength+1))
|
||||
}
|
||||
}
|
||||
if t.infoStyle == infoRight {
|
||||
maxWidth := t.window.Width()
|
||||
if t.reading {
|
||||
@@ -1690,19 +1714,35 @@ func (t *Terminal) printInfo() {
|
||||
}
|
||||
|
||||
if t.infoStyle == infoInlineRight {
|
||||
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
|
||||
if pos >= t.window.Width() {
|
||||
return
|
||||
if len(t.infoPrefix) == 0 {
|
||||
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-3)
|
||||
if pos < t.window.Width() {
|
||||
t.move(line, pos, false)
|
||||
printSpinner()
|
||||
pos++
|
||||
}
|
||||
if pos < t.window.Width()-1 {
|
||||
t.window.Print(" ")
|
||||
pos++
|
||||
}
|
||||
} else {
|
||||
pos = util.Max(pos, t.window.Width()-util.StringWidth(output)-util.StringWidth(t.infoPrefix)-1)
|
||||
printInfoPrefix()
|
||||
}
|
||||
t.move(line, pos, false)
|
||||
printSpinner()
|
||||
t.window.Print(" ")
|
||||
pos += 2
|
||||
}
|
||||
|
||||
maxWidth := t.window.Width() - pos
|
||||
output = t.trimMessage(output, maxWidth)
|
||||
t.window.CPrint(tui.ColInfo, output)
|
||||
|
||||
if t.infoStyle == infoInlineRight {
|
||||
if t.separatorLen > 0 {
|
||||
t.move(line+1, 0, false)
|
||||
printSeparator(t.window.Width()-1, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fillLength := maxWidth - len(output) - 2
|
||||
if fillLength > 0 {
|
||||
t.window.CPrint(tui.ColSeparator, " ")
|
||||
@@ -1717,7 +1757,7 @@ func (t *Terminal) printHeader() {
|
||||
max := t.window.Height()
|
||||
if t.headerFirst {
|
||||
max--
|
||||
if !t.noInfoLine() {
|
||||
if !t.noSeparatorLine() {
|
||||
max--
|
||||
}
|
||||
}
|
||||
@@ -1734,7 +1774,7 @@ func (t *Terminal) printHeader() {
|
||||
}
|
||||
if !t.headerFirst {
|
||||
line++
|
||||
if !t.noInfoLine() {
|
||||
if !t.noSeparatorLine() {
|
||||
line++
|
||||
}
|
||||
}
|
||||
@@ -1766,7 +1806,7 @@ func (t *Terminal) printList() {
|
||||
i = maxy - 1 - j
|
||||
}
|
||||
line := i + 2 + t.visibleHeaderLines()
|
||||
if t.noInfoLine() {
|
||||
if t.noSeparatorLine() {
|
||||
line--
|
||||
}
|
||||
if i < count {
|
||||
@@ -2484,6 +2524,7 @@ type replacePlaceholderParams struct {
|
||||
allItems []*Item
|
||||
lastAction actionType
|
||||
prompt string
|
||||
executor *util.Executor
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) string {
|
||||
@@ -2497,6 +2538,7 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
|
||||
allItems: list,
|
||||
lastAction: t.lastAction,
|
||||
prompt: t.promptString,
|
||||
executor: t.executor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2557,7 +2599,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
|
||||
case escaped:
|
||||
return match
|
||||
case match == "{q}" || match == "{fzf:query}":
|
||||
return quoteEntry(params.query)
|
||||
return params.executor.QuoteEntry(params.query)
|
||||
case match == "{}":
|
||||
replace = func(item *Item) string {
|
||||
switch {
|
||||
@@ -2570,13 +2612,13 @@ func replacePlaceholder(params replacePlaceholderParams) string {
|
||||
case flags.file:
|
||||
return item.AsString(params.stripAnsi)
|
||||
default:
|
||||
return quoteEntry(item.AsString(params.stripAnsi))
|
||||
return params.executor.QuoteEntry(item.AsString(params.stripAnsi))
|
||||
}
|
||||
}
|
||||
case match == "{fzf:action}":
|
||||
return params.lastAction.Name()
|
||||
case match == "{fzf:prompt}":
|
||||
return quoteEntry(params.prompt)
|
||||
return params.executor.QuoteEntry(params.prompt)
|
||||
default:
|
||||
// token type and also failover (below)
|
||||
rangeExpressions := strings.Split(match[1:len(match)-1], ",")
|
||||
@@ -2610,7 +2652,7 @@ func replacePlaceholder(params replacePlaceholderParams) string {
|
||||
str = strings.TrimSpace(str)
|
||||
}
|
||||
if !flags.file {
|
||||
str = quoteEntry(str)
|
||||
str = params.executor.QuoteEntry(str)
|
||||
}
|
||||
return str
|
||||
}
|
||||
@@ -2650,7 +2692,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
return line
|
||||
}
|
||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||
cmd := util.ExecCommand(command, false)
|
||||
cmd := t.executor.ExecCommand(command, false)
|
||||
cmd.Env = t.environ()
|
||||
t.executing.Set(true)
|
||||
if !background {
|
||||
@@ -2927,7 +2969,7 @@ func (t *Terminal) Loop() {
|
||||
if items[0] != nil {
|
||||
_, query := t.Input()
|
||||
command := t.replacePlaceholder(commandTemplate, false, string(query), items)
|
||||
cmd := util.ExecCommand(command, true)
|
||||
cmd := t.executor.ExecCommand(command, true)
|
||||
env := t.environ()
|
||||
if pwindowSize.Lines > 0 {
|
||||
lines := fmt.Sprintf("LINES=%d", pwindowSize.Lines)
|
||||
@@ -3092,7 +3134,7 @@ func (t *Terminal) Loop() {
|
||||
switch req {
|
||||
case reqPrompt:
|
||||
t.printPrompt()
|
||||
if t.noInfoLine() {
|
||||
if t.infoStyle == infoInline || t.infoStyle == infoInlineRight {
|
||||
t.printInfo()
|
||||
}
|
||||
case reqInfo:
|
||||
@@ -3248,6 +3290,7 @@ func (t *Terminal) Loop() {
|
||||
t.mutex.Lock()
|
||||
previousInput := t.input
|
||||
previousCx := t.cx
|
||||
t.lastKey = event.KeyName()
|
||||
events := []util.EventType{}
|
||||
req := func(evts ...util.EventType) {
|
||||
for _, event := range evts {
|
||||
@@ -3305,18 +3348,22 @@ func (t *Terminal) Loop() {
|
||||
var doAction func(*action) bool
|
||||
var doActions func(actions []*action) bool
|
||||
doActions = func(actions []*action) bool {
|
||||
currentIndex := t.currentIndex()
|
||||
for _, action := range actions {
|
||||
if !doAction(action) {
|
||||
return false
|
||||
for iter := 0; iter <= maxFocusEvents; iter++ {
|
||||
currentIndex := t.currentIndex()
|
||||
for _, action := range actions {
|
||||
if !doAction(action) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs {
|
||||
if newIndex := t.currentIndex(); newIndex != currentIndex {
|
||||
t.lastFocus = newIndex
|
||||
return doActions(onFocus)
|
||||
if onFocus, prs := t.keymap[tui.Focus.AsEvent()]; prs && iter < maxFocusEvents {
|
||||
if newIndex := t.currentIndex(); newIndex != currentIndex {
|
||||
t.lastFocus = newIndex
|
||||
actions = onFocus
|
||||
continue
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -3329,27 +3376,21 @@ func (t *Terminal) Loop() {
|
||||
valid, list := t.buildPlusList(a.a, false)
|
||||
if valid {
|
||||
command := t.replacePlaceholder(a.a, false, string(t.input), list)
|
||||
shell := os.Getenv("SHELL")
|
||||
if len(shell) == 0 {
|
||||
shell = "sh"
|
||||
}
|
||||
shellPath, err := exec.LookPath(shell)
|
||||
if err == nil {
|
||||
t.tui.Close()
|
||||
if t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
/*
|
||||
FIXME: It is not at all clear why this is required.
|
||||
The following command will report 'not a tty', unless we open
|
||||
/dev/tty *twice* after closing the standard input for 'reload'
|
||||
in Reader.terminate().
|
||||
: | fzf --bind 'start:reload:ls' --bind 'enter:become:tty'
|
||||
*/
|
||||
tui.TtyIn()
|
||||
util.SetStdin(tui.TtyIn())
|
||||
syscall.Exec(shellPath, []string{shell, "-c", command}, os.Environ())
|
||||
t.tui.Close()
|
||||
if t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
|
||||
/*
|
||||
FIXME: It is not at all clear why this is required.
|
||||
The following command will report 'not a tty', unless we open
|
||||
/dev/tty *twice* after closing the standard input for 'reload'
|
||||
in Reader.terminate().
|
||||
|
||||
while : | fzf --bind 'start:reload:ls' --bind 'load:become:tty'; do echo; done
|
||||
*/
|
||||
tui.TtyIn()
|
||||
t.executor.Become(tui.TtyIn(), t.environ(), command)
|
||||
}
|
||||
case actExecute, actExecuteSilent:
|
||||
t.executeCommand(a.a, false, a.t == actExecuteSilent, false, false)
|
||||
@@ -3447,6 +3488,19 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
case actPrintQuery:
|
||||
req(reqPrintQuery)
|
||||
case actChangeMulti:
|
||||
multi := t.multi
|
||||
if a.a == "" {
|
||||
multi = maxMulti
|
||||
} else if n, e := strconv.Atoi(a.a); e == nil && n >= 0 {
|
||||
multi = n
|
||||
}
|
||||
if t.multi > 0 && multi != t.multi {
|
||||
t.selected = make(map[int32]selectedItem)
|
||||
t.version++
|
||||
}
|
||||
t.multi = multi
|
||||
req(reqList, reqInfo)
|
||||
case actChangeQuery:
|
||||
t.input = []rune(a.a)
|
||||
t.cx = len(t.input)
|
||||
@@ -3464,11 +3518,13 @@ func (t *Terminal) Loop() {
|
||||
req(reqHeader)
|
||||
}
|
||||
case actChangeBorderLabel:
|
||||
t.borderLabelOpts.label = a.a
|
||||
if t.border != nil {
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(a.a, &tui.ColBorderLabel, false)
|
||||
req(reqRedrawBorderLabel)
|
||||
}
|
||||
case actChangePreviewLabel:
|
||||
t.previewLabelOpts.label = a.a
|
||||
if t.pborder != nil {
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(a.a, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
@@ -3478,14 +3534,16 @@ func (t *Terminal) Loop() {
|
||||
actions := parseSingleActionList(strings.Trim(body, "\r\n"), func(message string) {})
|
||||
return doActions(actions)
|
||||
case actTransformBorderLabel:
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.borderLabelOpts.label = label
|
||||
if t.border != nil {
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.borderLabel, t.borderLabelLen = t.ansiLabelPrinter(label, &tui.ColBorderLabel, false)
|
||||
req(reqRedrawBorderLabel)
|
||||
}
|
||||
case actTransformPreviewLabel:
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.previewLabelOpts.label = label
|
||||
if t.pborder != nil {
|
||||
label := t.executeCommand(a.a, false, true, true, true)
|
||||
t.previewLabel, t.previewLabelLen = t.ansiLabelPrinter(label, &tui.ColPreviewLabel, false)
|
||||
req(reqRedrawPreviewLabel)
|
||||
}
|
||||
@@ -3771,6 +3829,14 @@ func (t *Terminal) Loop() {
|
||||
t.track = trackEnabled
|
||||
}
|
||||
req(reqInfo)
|
||||
case actToggleTrackCurrent:
|
||||
switch t.track {
|
||||
case trackCurrent:
|
||||
t.track = trackDisabled
|
||||
case trackDisabled:
|
||||
t.track = trackCurrent
|
||||
}
|
||||
req(reqInfo)
|
||||
case actShowHeader:
|
||||
t.headerVisible = true
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
@@ -3780,11 +3846,16 @@ func (t *Terminal) Loop() {
|
||||
case actToggleHeader:
|
||||
t.headerVisible = !t.headerVisible
|
||||
req(reqList, reqInfo, reqPrompt, reqHeader)
|
||||
case actTrack:
|
||||
case actTrackCurrent:
|
||||
if t.track == trackDisabled {
|
||||
t.track = trackCurrent
|
||||
}
|
||||
req(reqInfo)
|
||||
case actUntrackCurrent:
|
||||
if t.track == trackCurrent {
|
||||
t.track = trackDisabled
|
||||
}
|
||||
req(reqInfo)
|
||||
case actEnableSearch:
|
||||
t.paused = false
|
||||
changed = true
|
||||
@@ -3873,7 +3944,7 @@ func (t *Terminal) Loop() {
|
||||
mx -= t.window.Left()
|
||||
my -= t.window.Top()
|
||||
min := 2 + t.visibleHeaderLines()
|
||||
if t.noInfoLine() {
|
||||
if t.noSeparatorLine() {
|
||||
min--
|
||||
}
|
||||
h := t.window.Height()
|
||||
@@ -4045,6 +4116,9 @@ func (t *Terminal) Loop() {
|
||||
// Break out of jump mode if any action is submitted to the server
|
||||
if t.jumping != jumpDisabled {
|
||||
t.jumping = jumpDisabled
|
||||
if acts, prs := t.keymap[tui.JumpCancel.AsEvent()]; prs && !doActions(acts) {
|
||||
continue
|
||||
}
|
||||
req(reqList)
|
||||
}
|
||||
if len(actions) == 0 {
|
||||
@@ -4058,19 +4132,17 @@ func (t *Terminal) Loop() {
|
||||
t.truncateQuery()
|
||||
queryChanged = string(previousInput) != string(t.input)
|
||||
changed = changed || queryChanged
|
||||
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs {
|
||||
if !doActions(onChanges) {
|
||||
continue
|
||||
}
|
||||
if onChanges, prs := t.keymap[tui.Change.AsEvent()]; queryChanged && prs && !doActions(onChanges) {
|
||||
continue
|
||||
}
|
||||
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs {
|
||||
if !doActions(onEOFs) {
|
||||
continue
|
||||
}
|
||||
if onEOFs, prs := t.keymap[tui.BackwardEOF.AsEvent()]; beof && prs && !doActions(onEOFs) {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
jumpEvent := tui.JumpCancel
|
||||
if event.Type == tui.Rune {
|
||||
if idx := strings.IndexRune(t.jumpLabels, event.Char); idx >= 0 && idx < t.maxItems() && idx < t.merger.Length() {
|
||||
jumpEvent = tui.Jump
|
||||
t.cy = idx + t.offset
|
||||
if t.jumping == jumpAcceptEnabled {
|
||||
req(reqClose)
|
||||
@@ -4078,6 +4150,9 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
}
|
||||
t.jumping = jumpDisabled
|
||||
if acts, prs := t.keymap[jumpEvent.AsEvent()]; prs && !doActions(acts) {
|
||||
continue
|
||||
}
|
||||
req(reqList)
|
||||
}
|
||||
|
||||
@@ -4114,7 +4189,7 @@ func (t *Terminal) constrain() {
|
||||
// count of lines can be displayed
|
||||
height := t.maxItems()
|
||||
|
||||
t.cy = util.Constrain(t.cy, 0, count-1)
|
||||
t.cy = util.Constrain(t.cy, 0, util.Max(0, count-1))
|
||||
|
||||
minOffset := util.Max(t.cy-height+1, 0)
|
||||
maxOffset := util.Max(util.Min(count-height, t.cy), 0)
|
||||
@@ -4165,7 +4240,7 @@ func (t *Terminal) vset(o int) bool {
|
||||
|
||||
func (t *Terminal) maxItems() int {
|
||||
max := t.window.Height() - 2 - t.visibleHeaderLines()
|
||||
if t.noInfoLine() {
|
||||
if t.noSeparatorLine() {
|
||||
max++
|
||||
}
|
||||
return util.Max(max, 0)
|
||||
|
@@ -23,6 +23,7 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
|
||||
allItems: allItems,
|
||||
lastAction: actBackwardDeleteCharEof,
|
||||
prompt: "prompt",
|
||||
executor: util.NewExecutor(""),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -244,6 +245,7 @@ func TestQuoteEntry(t *testing.T) {
|
||||
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
||||
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
||||
var effectiveStyle quotes
|
||||
exec := util.NewExecutor("")
|
||||
|
||||
if util.IsWindows() {
|
||||
effectiveStyle = windowsStyle
|
||||
@@ -278,7 +280,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 +319,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}
|
||||
|
||||
|
@@ -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) + "'"
|
||||
}
|
||||
|
@@ -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) + "'"
|
||||
}
|
||||
}
|
||||
|
@@ -91,7 +91,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
|
||||
|
||||
prefixLength := begin
|
||||
for idx := range tokens {
|
||||
chars := util.ToChars([]byte(tokens[idx]))
|
||||
chars := util.ToChars(sbytes(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([]byte(joinTokens(tokens)))
|
||||
chars := util.ToChars(sbytes(joinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
|
120
src/tui/eventtype_string.go
Normal file
120
src/tui/eventtype_string.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Code generated by "stringer -type=EventType"; DO NOT EDIT.
|
||||
|
||||
package tui
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[Rune-0]
|
||||
_ = x[CtrlA-1]
|
||||
_ = x[CtrlB-2]
|
||||
_ = x[CtrlC-3]
|
||||
_ = x[CtrlD-4]
|
||||
_ = x[CtrlE-5]
|
||||
_ = x[CtrlF-6]
|
||||
_ = x[CtrlG-7]
|
||||
_ = x[CtrlH-8]
|
||||
_ = x[Tab-9]
|
||||
_ = x[CtrlJ-10]
|
||||
_ = x[CtrlK-11]
|
||||
_ = x[CtrlL-12]
|
||||
_ = x[CtrlM-13]
|
||||
_ = x[CtrlN-14]
|
||||
_ = x[CtrlO-15]
|
||||
_ = x[CtrlP-16]
|
||||
_ = x[CtrlQ-17]
|
||||
_ = x[CtrlR-18]
|
||||
_ = x[CtrlS-19]
|
||||
_ = x[CtrlT-20]
|
||||
_ = x[CtrlU-21]
|
||||
_ = x[CtrlV-22]
|
||||
_ = x[CtrlW-23]
|
||||
_ = x[CtrlX-24]
|
||||
_ = x[CtrlY-25]
|
||||
_ = x[CtrlZ-26]
|
||||
_ = x[Esc-27]
|
||||
_ = x[CtrlSpace-28]
|
||||
_ = x[CtrlDelete-29]
|
||||
_ = x[CtrlBackSlash-30]
|
||||
_ = x[CtrlRightBracket-31]
|
||||
_ = x[CtrlCaret-32]
|
||||
_ = x[CtrlSlash-33]
|
||||
_ = x[ShiftTab-34]
|
||||
_ = x[Backspace-35]
|
||||
_ = x[Delete-36]
|
||||
_ = x[PageUp-37]
|
||||
_ = x[PageDown-38]
|
||||
_ = x[Up-39]
|
||||
_ = x[Down-40]
|
||||
_ = x[Left-41]
|
||||
_ = x[Right-42]
|
||||
_ = x[Home-43]
|
||||
_ = x[End-44]
|
||||
_ = x[Insert-45]
|
||||
_ = x[ShiftUp-46]
|
||||
_ = x[ShiftDown-47]
|
||||
_ = x[ShiftLeft-48]
|
||||
_ = x[ShiftRight-49]
|
||||
_ = x[ShiftDelete-50]
|
||||
_ = x[F1-51]
|
||||
_ = x[F2-52]
|
||||
_ = x[F3-53]
|
||||
_ = x[F4-54]
|
||||
_ = x[F5-55]
|
||||
_ = x[F6-56]
|
||||
_ = x[F7-57]
|
||||
_ = x[F8-58]
|
||||
_ = x[F9-59]
|
||||
_ = x[F10-60]
|
||||
_ = x[F11-61]
|
||||
_ = x[F12-62]
|
||||
_ = x[AltBackspace-63]
|
||||
_ = x[AltUp-64]
|
||||
_ = x[AltDown-65]
|
||||
_ = x[AltLeft-66]
|
||||
_ = x[AltRight-67]
|
||||
_ = x[AltShiftUp-68]
|
||||
_ = x[AltShiftDown-69]
|
||||
_ = x[AltShiftLeft-70]
|
||||
_ = x[AltShiftRight-71]
|
||||
_ = 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]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
|
||||
|
||||
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}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
return "EventType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _EventType_name[_EventType_index[i]:_EventType_index[i+1]]
|
||||
}
|
@@ -71,7 +71,7 @@ func (r *LightRenderer) csi(code string) string {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
fmt.Fprint(os.Stderr, "\x1b[?25l"+r.queued.String()+"\x1b[?25h")
|
||||
fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
|
||||
}
|
||||
|
||||
retries := 0
|
||||
if c == ESC.Int() || nonblock {
|
||||
if c == Esc.Int() || nonblock {
|
||||
retries = r.escDelay / escPollInterval
|
||||
}
|
||||
buffer = append(buffer, byte(c))
|
||||
@@ -260,7 +260,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) []byte {
|
||||
continue
|
||||
}
|
||||
break
|
||||
} else if c == ESC.Int() && pc != c {
|
||||
} else if c == Esc.Int() && pc != c {
|
||||
retries = r.escDelay / escPollInterval
|
||||
} else {
|
||||
retries = 0
|
||||
@@ -300,7 +300,7 @@ func (r *LightRenderer) GetChar() Event {
|
||||
case CtrlQ.Byte():
|
||||
return Event{CtrlQ, 0, nil}
|
||||
case 127:
|
||||
return Event{BSpace, 0, nil}
|
||||
return Event{Backspace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case 28:
|
||||
@@ -311,7 +311,7 @@ func (r *LightRenderer) GetChar() Event {
|
||||
return Event{CtrlCaret, 0, nil}
|
||||
case 31:
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
case ESC.Byte():
|
||||
case Esc.Byte():
|
||||
ev := r.escSequence(&sz)
|
||||
// Second chance
|
||||
if ev.Type == Invalid {
|
||||
@@ -327,7 +327,7 @@ func (r *LightRenderer) GetChar() Event {
|
||||
}
|
||||
char, rsz := utf8.DecodeRune(r.buffer)
|
||||
if char == utf8.RuneError {
|
||||
return Event{ESC, 0, nil}
|
||||
return Event{Esc, 0, nil}
|
||||
}
|
||||
sz = rsz
|
||||
return Event{Rune, char, nil}
|
||||
@@ -335,7 +335,7 @@ func (r *LightRenderer) GetChar() Event {
|
||||
|
||||
func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
if len(r.buffer) < 2 {
|
||||
return Event{ESC, 0, nil}
|
||||
return Event{Esc, 0, nil}
|
||||
}
|
||||
|
||||
loc := offsetRegexpBegin.FindIndex(r.buffer)
|
||||
@@ -349,15 +349,15 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
|
||||
}
|
||||
alt := false
|
||||
if len(r.buffer) > 2 && r.buffer[1] == ESC.Byte() {
|
||||
if len(r.buffer) > 2 && r.buffer[1] == Esc.Byte() {
|
||||
r.buffer = r.buffer[1:]
|
||||
alt = true
|
||||
}
|
||||
switch r.buffer[1] {
|
||||
case ESC.Byte():
|
||||
return Event{ESC, 0, nil}
|
||||
case Esc.Byte():
|
||||
return Event{Esc, 0, nil}
|
||||
case 127:
|
||||
return Event{AltBS, 0, nil}
|
||||
return Event{AltBackspace, 0, nil}
|
||||
case '[', 'O':
|
||||
if len(r.buffer) < 3 {
|
||||
return Event{Invalid, 0, nil}
|
||||
@@ -386,7 +386,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
return Event{Up, 0, nil}
|
||||
case 'Z':
|
||||
return Event{BTab, 0, nil}
|
||||
return Event{ShiftTab, 0, nil}
|
||||
case 'H':
|
||||
return Event{Home, 0, nil}
|
||||
case 'F':
|
||||
@@ -434,7 +434,7 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case '3':
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{Del, 0, nil}
|
||||
return Event{Delete, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
@@ -442,16 +442,16 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
case '5':
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
case '2':
|
||||
return Event{SDelete, 0, nil}
|
||||
return Event{ShiftDelete, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '4':
|
||||
return Event{End, 0, nil}
|
||||
case '5':
|
||||
return Event{PgUp, 0, nil}
|
||||
return Event{PageUp, 0, nil}
|
||||
case '6':
|
||||
return Event{PgDn, 0, nil}
|
||||
return Event{PageDown, 0, nil}
|
||||
case '7':
|
||||
return Event{Home, 0, nil}
|
||||
case '8':
|
||||
@@ -489,16 +489,29 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '1', '2', '3', '5':
|
||||
case '1', '2', '3', '4', '5':
|
||||
// Kitty iTerm2 WezTerm
|
||||
// SHIFT-ARROW "\e[1;2D"
|
||||
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
|
||||
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
|
||||
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
|
||||
alt := r.buffer[4] == '3'
|
||||
altShift := r.buffer[4] == '1' && r.buffer[5] == '0'
|
||||
char := r.buffer[5]
|
||||
if altShift {
|
||||
altShift := false
|
||||
if r.buffer[4] == '1' && r.buffer[5] == '0' {
|
||||
altShift = true
|
||||
if len(r.buffer) < 7 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 7
|
||||
char = r.buffer[6]
|
||||
} else if r.buffer[4] == '4' {
|
||||
altShift = true
|
||||
if len(r.buffer) < 6 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
char = r.buffer[5]
|
||||
}
|
||||
switch char {
|
||||
case 'A':
|
||||
@@ -506,33 +519,33 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
return Event{AltUp, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltSUp, 0, nil}
|
||||
return Event{AltShiftUp, 0, nil}
|
||||
}
|
||||
return Event{SUp, 0, nil}
|
||||
return Event{ShiftUp, 0, nil}
|
||||
case 'B':
|
||||
if alt {
|
||||
return Event{AltDown, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltSDown, 0, nil}
|
||||
return Event{AltShiftDown, 0, nil}
|
||||
}
|
||||
return Event{SDown, 0, nil}
|
||||
return Event{ShiftDown, 0, nil}
|
||||
case 'C':
|
||||
if alt {
|
||||
return Event{AltRight, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltSRight, 0, nil}
|
||||
return Event{AltShiftRight, 0, nil}
|
||||
}
|
||||
return Event{SRight, 0, nil}
|
||||
return Event{ShiftRight, 0, nil}
|
||||
case 'D':
|
||||
if alt {
|
||||
return Event{AltLeft, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltSLeft, 0, nil}
|
||||
return Event{AltShiftLeft, 0, nil}
|
||||
}
|
||||
return Event{SLeft, 0, nil}
|
||||
return Event{ShiftLeft, 0, nil}
|
||||
}
|
||||
} // r.buffer[4]
|
||||
} // r.buffer[3]
|
||||
|
@@ -58,7 +58,7 @@ func openTtyIn() *os.File {
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
|
||||
os.Exit(2)
|
||||
util.Exit(2)
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
@@ -320,16 +320,16 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
switch ev.Rune() {
|
||||
case 0:
|
||||
if ctrl {
|
||||
return Event{BSpace, 0, nil}
|
||||
return Event{Backspace, 0, nil}
|
||||
}
|
||||
case rune(tcell.KeyCtrlH):
|
||||
switch {
|
||||
case ctrl:
|
||||
return keyfn('h')
|
||||
case alt:
|
||||
return Event{AltBS, 0, nil}
|
||||
return Event{AltBackspace, 0, nil}
|
||||
case none, shift:
|
||||
return Event{BSpace, 0, nil}
|
||||
return Event{Backspace, 0, nil}
|
||||
}
|
||||
}
|
||||
case tcell.KeyCtrlI:
|
||||
@@ -382,17 +382,17 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
// section 3: (Alt)+Backspace2
|
||||
case tcell.KeyBackspace2:
|
||||
if alt {
|
||||
return Event{AltBS, 0, nil}
|
||||
return Event{AltBackspace, 0, nil}
|
||||
}
|
||||
return Event{BSpace, 0, nil}
|
||||
return Event{Backspace, 0, nil}
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
case tcell.KeyUp:
|
||||
if altShift {
|
||||
return Event{AltSUp, 0, nil}
|
||||
return Event{AltShiftUp, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SUp, 0, nil}
|
||||
return Event{ShiftUp, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltUp, 0, nil}
|
||||
@@ -400,10 +400,10 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{Up, 0, nil}
|
||||
case tcell.KeyDown:
|
||||
if altShift {
|
||||
return Event{AltSDown, 0, nil}
|
||||
return Event{AltShiftDown, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SDown, 0, nil}
|
||||
return Event{ShiftDown, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltDown, 0, nil}
|
||||
@@ -411,10 +411,10 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{Down, 0, nil}
|
||||
case tcell.KeyLeft:
|
||||
if altShift {
|
||||
return Event{AltSLeft, 0, nil}
|
||||
return Event{AltShiftLeft, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SLeft, 0, nil}
|
||||
return Event{ShiftLeft, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltLeft, 0, nil}
|
||||
@@ -422,10 +422,10 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{Left, 0, nil}
|
||||
case tcell.KeyRight:
|
||||
if altShift {
|
||||
return Event{AltSRight, 0, nil}
|
||||
return Event{AltShiftRight, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SRight, 0, nil}
|
||||
return Event{ShiftRight, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltRight, 0, nil}
|
||||
@@ -442,17 +442,17 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{SDelete, 0, nil}
|
||||
return Event{ShiftDelete, 0, nil}
|
||||
}
|
||||
return Event{Del, 0, nil}
|
||||
return Event{Delete, 0, nil}
|
||||
case tcell.KeyEnd:
|
||||
return Event{End, 0, nil}
|
||||
case tcell.KeyPgUp:
|
||||
return Event{PgUp, 0, nil}
|
||||
return Event{PageUp, 0, nil}
|
||||
case tcell.KeyPgDn:
|
||||
return Event{PgDn, 0, nil}
|
||||
return Event{PageDown, 0, nil}
|
||||
case tcell.KeyBacktab:
|
||||
return Event{BTab, 0, nil}
|
||||
return Event{ShiftTab, 0, nil}
|
||||
case tcell.KeyF1:
|
||||
return Event{F1, 0, nil}
|
||||
case tcell.KeyF2:
|
||||
@@ -498,7 +498,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
// section 7: Esc
|
||||
case tcell.KeyEsc:
|
||||
return Event{ESC, 0, nil}
|
||||
return Event{Esc, 0, nil}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -102,22 +102,22 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
|
||||
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
|
||||
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
|
||||
@@ -126,8 +126,8 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
@@ -161,11 +161,11 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
|
||||
// section 7: Esc
|
||||
// KeyEsc and KeyEscape are aliases for KeyESC
|
||||
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
|
||||
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
|
||||
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{Esc, 0, nil}}, // unhandled
|
||||
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // actual Ctrl+[ keystroke
|
||||
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
|
||||
|
||||
// section 8: Invalid
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
@@ -259,7 +259,7 @@ Quick reference
|
||||
37 LeftClick
|
||||
38 RightClick
|
||||
39 BTab
|
||||
40 BSpace
|
||||
40 Backspace
|
||||
41 Del
|
||||
42 PgUp
|
||||
43 PgDn
|
||||
@@ -272,7 +272,7 @@ Quick reference
|
||||
50 Insert
|
||||
51 SUp
|
||||
52 SDown
|
||||
53 SLeft
|
||||
53 ShiftLeft
|
||||
54 SRight
|
||||
55 F1
|
||||
56 F2
|
||||
@@ -288,15 +288,15 @@ Quick reference
|
||||
66 F12
|
||||
67 Change
|
||||
68 BackwardEOF
|
||||
69 AltBS
|
||||
69 AltBackspace
|
||||
70 AltUp
|
||||
71 AltDown
|
||||
72 AltLeft
|
||||
73 AltRight
|
||||
74 AltSUp
|
||||
75 AltSDown
|
||||
76 AltSLeft
|
||||
77 AltSRight
|
||||
76 AltShiftLeft
|
||||
77 AltShiftRight
|
||||
78 Alt
|
||||
79 CtrlAlt
|
||||
..
|
||||
|
116
src/tui/tui.go
116
src/tui/tui.go
@@ -6,10 +6,13 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
// Types of user action
|
||||
//
|
||||
//go:generate stringer -type=EventType
|
||||
type EventType int
|
||||
|
||||
const (
|
||||
@@ -41,7 +44,7 @@ const (
|
||||
CtrlX
|
||||
CtrlY
|
||||
CtrlZ
|
||||
ESC
|
||||
Esc
|
||||
CtrlSpace
|
||||
CtrlDelete
|
||||
|
||||
@@ -51,27 +54,12 @@ const (
|
||||
CtrlCaret
|
||||
CtrlSlash
|
||||
|
||||
Invalid
|
||||
Resize
|
||||
Mouse
|
||||
DoubleClick
|
||||
LeftClick
|
||||
RightClick
|
||||
SLeftClick
|
||||
SRightClick
|
||||
ScrollUp
|
||||
ScrollDown
|
||||
SScrollUp
|
||||
SScrollDown
|
||||
PreviewScrollUp
|
||||
PreviewScrollDown
|
||||
ShiftTab
|
||||
Backspace
|
||||
|
||||
BTab
|
||||
BSpace
|
||||
|
||||
Del
|
||||
PgUp
|
||||
PgDn
|
||||
Delete
|
||||
PageUp
|
||||
PageDown
|
||||
|
||||
Up
|
||||
Down
|
||||
@@ -81,11 +69,11 @@ const (
|
||||
End
|
||||
Insert
|
||||
|
||||
SUp
|
||||
SDown
|
||||
SLeft
|
||||
SRight
|
||||
SDelete
|
||||
ShiftUp
|
||||
ShiftDown
|
||||
ShiftLeft
|
||||
ShiftRight
|
||||
ShiftDelete
|
||||
|
||||
F1
|
||||
F2
|
||||
@@ -100,6 +88,38 @@ const (
|
||||
F11
|
||||
F12
|
||||
|
||||
AltBackspace
|
||||
|
||||
AltUp
|
||||
AltDown
|
||||
AltLeft
|
||||
AltRight
|
||||
|
||||
AltShiftUp
|
||||
AltShiftDown
|
||||
AltShiftLeft
|
||||
AltShiftRight
|
||||
|
||||
Alt
|
||||
CtrlAlt
|
||||
|
||||
Invalid
|
||||
|
||||
Mouse
|
||||
DoubleClick
|
||||
LeftClick
|
||||
RightClick
|
||||
SLeftClick
|
||||
SRightClick
|
||||
ScrollUp
|
||||
ScrollDown
|
||||
SScrollUp
|
||||
SScrollDown
|
||||
PreviewScrollUp
|
||||
PreviewScrollDown
|
||||
|
||||
// Events
|
||||
Resize
|
||||
Change
|
||||
BackwardEOF
|
||||
Start
|
||||
@@ -108,21 +128,8 @@ const (
|
||||
One
|
||||
Zero
|
||||
Result
|
||||
|
||||
AltBS
|
||||
|
||||
AltUp
|
||||
AltDown
|
||||
AltLeft
|
||||
AltRight
|
||||
|
||||
AltSUp
|
||||
AltSDown
|
||||
AltSLeft
|
||||
AltSRight
|
||||
|
||||
Alt
|
||||
CtrlAlt
|
||||
Jump
|
||||
JumpCancel
|
||||
)
|
||||
|
||||
func (t EventType) AsEvent() Event {
|
||||
@@ -142,6 +149,31 @@ func (e Event) Comparable() Event {
|
||||
return Event{e.Type, e.Char, nil}
|
||||
}
|
||||
|
||||
func (e Event) KeyName() string {
|
||||
if e.Type >= Invalid {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case Rune:
|
||||
return string(e.Char)
|
||||
case Alt:
|
||||
return "alt-" + string(e.Char)
|
||||
case CtrlAlt:
|
||||
return "ctrl-alt-" + string(e.Char)
|
||||
case CtrlBackSlash:
|
||||
return "ctrl-\\"
|
||||
case CtrlRightBracket:
|
||||
return "ctrl-]"
|
||||
case CtrlCaret:
|
||||
return "ctrl-^"
|
||||
case CtrlSlash:
|
||||
return "ctrl-/"
|
||||
}
|
||||
|
||||
return util.ToKebabCase(e.Type.String())
|
||||
}
|
||||
|
||||
func Key(r rune) Event {
|
||||
return Event{Rune, r, nil}
|
||||
}
|
||||
@@ -646,7 +678,7 @@ func NoColorTheme() *ColorTheme {
|
||||
|
||||
func errorExit(message string) {
|
||||
fmt.Fprintln(os.Stderr, message)
|
||||
os.Exit(2)
|
||||
util.Exit(2)
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
38
src/util/atexit.go
Normal file
38
src/util/atexit.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var atExitFuncs []func()
|
||||
|
||||
// AtExit registers the function fn to be called on program termination.
|
||||
// The functions will be called in reverse order they were registered.
|
||||
func AtExit(fn func()) {
|
||||
if fn == nil {
|
||||
panic("AtExit called with nil func")
|
||||
}
|
||||
once := &sync.Once{}
|
||||
atExitFuncs = append(atExitFuncs, func() {
|
||||
once.Do(fn)
|
||||
})
|
||||
}
|
||||
|
||||
// RunAtExitFuncs runs any functions registered with AtExit().
|
||||
func RunAtExitFuncs() {
|
||||
fns := atExitFuncs
|
||||
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()
|
||||
}
|
24
src/util/atexit_test.go
Normal file
24
src/util/atexit_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAtExit(t *testing.T) {
|
||||
want := []int{3, 2, 1, 0}
|
||||
var called []int
|
||||
for i := 0; i < 4; i++ {
|
||||
n := i
|
||||
AtExit(func() { called = append(called, n) })
|
||||
}
|
||||
RunAtExitFuncs()
|
||||
if !reflect.DeepEqual(called, want) {
|
||||
t.Errorf("AtExit: want call order: %v got: %v", want, called)
|
||||
}
|
||||
|
||||
RunAtExitFuncs()
|
||||
if !reflect.DeepEqual(called, want) {
|
||||
t.Error("AtExit: should only call exit funcs once")
|
||||
}
|
||||
}
|
@@ -163,7 +163,7 @@ func (chars *Chars) ToString() string {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
return string(runes)
|
||||
}
|
||||
return string(chars.slice)
|
||||
return unsafe.String(unsafe.SliceData(chars.slice), len(chars.slice))
|
||||
}
|
||||
|
||||
func (chars *Chars) ToRunes() []rune {
|
||||
@@ -178,12 +178,12 @@ func (chars *Chars) ToRunes() []rune {
|
||||
return runes
|
||||
}
|
||||
|
||||
func (chars *Chars) CopyRunes(dest []rune) {
|
||||
func (chars *Chars) CopyRunes(dest []rune, from int) {
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
copy(dest, runes)
|
||||
copy(dest, runes[from:])
|
||||
return
|
||||
}
|
||||
for idx, b := range chars.slice[:len(dest)] {
|
||||
for idx, b := range chars.slice[from:][:len(dest)] {
|
||||
dest[idx] = rune(b)
|
||||
}
|
||||
}
|
||||
|
@@ -176,3 +176,15 @@ func RepeatToFill(str string, length int, limit int) string {
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// ToKebabCase converts the given CamelCase string to kebab-case
|
||||
func ToKebabCase(s string) string {
|
||||
name := ""
|
||||
for i, r := range s {
|
||||
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||
name += "-"
|
||||
}
|
||||
name += string(r)
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
@@ -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())
|
||||
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)
|
||||
|
@@ -6,62 +6,161 @@ 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{"/v:on/s/c"}
|
||||
} else if strings.HasPrefix(basename, "pwsh") || strings.HasPrefix(basename, "powershell") {
|
||||
shellType = shellTypePowerShell
|
||||
args = []string{"-NoProfile", "-Command"}
|
||||
} else {
|
||||
args = []string{"-c"}
|
||||
}
|
||||
return &Executor{shell: shell, shellType: shellType, args: args}
|
||||
}
|
||||
|
||||
// ExecCommand executes the given command with $SHELL
|
||||
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())
|
||||
Exit(127)
|
||||
}
|
||||
err = cmd.Wait()
|
||||
if err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
Exit(exitError.ExitCode())
|
||||
}
|
||||
}
|
||||
Exit(0)
|
||||
}
|
||||
|
||||
func escapeArg(s string) string {
|
||||
b := make([]byte, 0, len(s)+2)
|
||||
b = append(b, '"')
|
||||
slashes := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
switch c {
|
||||
default:
|
||||
slashes = 0
|
||||
case '\\':
|
||||
slashes++
|
||||
case '&', '|', '<', '>', '(', ')', '@', '^', '%', '!':
|
||||
b = append(b, '^')
|
||||
case '"':
|
||||
for ; slashes > 0; slashes-- {
|
||||
b = append(b, '\\')
|
||||
}
|
||||
b = append(b, '\\')
|
||||
}
|
||||
b = append(b, c)
|
||||
}
|
||||
for ; slashes > 0; slashes-- {
|
||||
b = append(b, '\\')
|
||||
}
|
||||
b = append(b, '"')
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (x *Executor) QuoteEntry(entry string) string {
|
||||
switch x.shellType {
|
||||
case shellTypeCmd:
|
||||
/* Manually tested with the following commands:
|
||||
fzf --preview "echo {}"
|
||||
fzf --preview "type {}"
|
||||
echo .git\refs\| fzf --preview "dir {}"
|
||||
echo .git\refs\\| fzf --preview "dir {}"
|
||||
echo .git\refs\\\| fzf --preview "dir {}"
|
||||
reg query HKCU | fzf --reverse --bind "enter:reload(reg query {})"
|
||||
fzf --disabled --preview "echo {q} {n} {}" --query "&|<>()@^%!"
|
||||
fd -H --no-ignore -td -d 4 | fzf --preview "dir {}"
|
||||
fd -H --no-ignore -td -d 4 | fzf --preview "eza {}" --preview-window up
|
||||
fd -H --no-ignore -td -d 4 | fzf --preview "eza --color=always --tree --level=3 --icons=always {}"
|
||||
fd -H --no-ignore -td -d 4 | fzf --preview ".\eza.exe --color=always --tree --level=3 --icons=always {}" --with-shell "powershell -NoProfile -Command"
|
||||
*/
|
||||
return escapeArg(entry)
|
||||
case shellTypePowerShell:
|
||||
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
||||
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
||||
default:
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
}
|
||||
}
|
||||
|
||||
// KillCommand kills the process for the given command
|
||||
func KillCommand(cmd *exec.Cmd) error {
|
||||
return cmd.Process.Kill()
|
||||
|
171
test/test_go.rb
171
test/test_go.rb
@@ -18,7 +18,6 @@ UNSETS = %w[
|
||||
FZF_ALT_C_COMMAND
|
||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
FZF_API_KEY
|
||||
fish_history
|
||||
].freeze
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
@@ -67,7 +66,7 @@ class Shell
|
||||
end
|
||||
|
||||
def fish
|
||||
UNSETS.map { |v| v + '= ' }.join + ' FZF_DEFAULT_OPTS=--no-scrollbar fish'
|
||||
"unset #{UNSETS.join(' ')}; FZF_DEFAULT_OPTS=--no-scrollbar fish_history= fish"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -426,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/';
|
||||
@@ -1469,6 +1487,19 @@ class TestGoFZF < TestBase
|
||||
assert_equal '3', readonce.chomp
|
||||
end
|
||||
|
||||
def test_jump_events
|
||||
tmux.send_keys "seq 1000 | #{fzf("--multi --jump-labels 12345 --bind 'ctrl-j:jump,jump:preview(echo jumped to {}),jump-cancel:preview(echo jump cancelled at {})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal ' 1000/1000 (0)', lines[-2] }
|
||||
tmux.send_keys 'C-j'
|
||||
tmux.until { |lines| assert_includes lines[-7], '5 5' }
|
||||
tmux.send_keys '3'
|
||||
tmux.until { |lines| assert(lines.any? { _1.include?('jumped to 3') }) }
|
||||
tmux.send_keys 'C-j'
|
||||
tmux.until { |lines| assert_includes lines[-7], '5 5' }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| assert(lines.any? { _1.include?('jump cancelled at 3') }) }
|
||||
end
|
||||
|
||||
def test_pointer
|
||||
tmux.send_keys "seq 10 | #{fzf("--pointer '>>'")}", :Enter
|
||||
# Assert that specified pointer is displayed
|
||||
@@ -1720,7 +1751,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
|
||||
def test_info_hidden
|
||||
tmux.send_keys 'seq 10 | fzf --info=hidden', :Enter
|
||||
tmux.send_keys 'seq 10 | fzf --info=hidden --no-separator', :Enter
|
||||
tmux.until { |lines| assert_equal '> 1', lines[-2] }
|
||||
end
|
||||
|
||||
@@ -1943,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' }
|
||||
@@ -2464,6 +2500,84 @@ class TestGoFZF < TestBase
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case1
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case2
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case3
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case4
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case5
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(echo xx; sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1001, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case6
|
||||
tmux.send_keys "seq 1000 | #{FZF} --disabled --bind 'change:reload:sleep 0.5; seq {q}'", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_equal 9, lines.match_count }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_equal 99, lines.match_count }
|
||||
|
||||
# TODO: How do we verify if an intermediate empty list is not shown?
|
||||
end
|
||||
|
||||
def test_scroll_off
|
||||
tmux.send_keys "seq 1000 | #{FZF} --scroll-off=3 --bind l:last", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.item_count }
|
||||
@@ -2800,6 +2914,27 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
end
|
||||
|
||||
def test_labels_variables
|
||||
tmux.send_keys ': | fzf --border --border-label foobar --preview "echo \$FZF_BORDER_LABEL // \$FZF_PREVIEW_LABEL" --preview-label barfoo --bind "space:change-border-label(barbaz)+change-preview-label(bazbar)+refresh-preview,enter:transform-border-label(echo 123)+transform-preview-label(echo 456)+refresh-preview"', :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─foobar─')
|
||||
assert_includes(_1[1], '─barfoo─')
|
||||
assert_includes(_1[2], ' foobar // barfoo ')
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─barbaz─')
|
||||
assert_includes(_1[1], '─bazbar─')
|
||||
assert_includes(_1[2], ' barbaz // bazbar ')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until do
|
||||
assert_includes(_1[0], '─123─')
|
||||
assert_includes(_1[1], '─456─')
|
||||
assert_includes(_1[2], ' 123 // 456 ')
|
||||
end
|
||||
end
|
||||
|
||||
def test_info_separator_unicode
|
||||
tmux.send_keys 'seq 100 | fzf -q55', :Enter
|
||||
tmux.until { assert_includes(_1[-2], ' 1/100 ─') }
|
||||
@@ -3003,7 +3138,7 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
assert_includes lines[-2], '+t'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
@@ -3015,7 +3150,7 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys '4'
|
||||
tmux.until do |lines|
|
||||
assert_equal 28, lines.match_count
|
||||
refute_includes lines[-2], '+T'
|
||||
refute_includes lines[-2], '+t'
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
@@ -3024,11 +3159,11 @@ class TestGoFZF < TestBase
|
||||
end
|
||||
tmux.send_keys :t
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[-2], '+T'
|
||||
assert_includes lines[-2], '+t'
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[-2], '+T'
|
||||
refute_includes lines[-2], '+t'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3115,6 +3250,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
|
||||
@@ -3308,7 +3454,10 @@ module CompletionTest
|
||||
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'foobar$'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert lines.any_include?('> /tmp/fzf test/foobar')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
|
||||
|
||||
@@ -3343,7 +3492,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
|
||||
|
Reference in New Issue
Block a user