mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-15 06:43:47 -05:00
Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19759ed36e | ||
|
|
1a7ae8e7b9 | ||
|
|
da1f645670 | ||
|
|
3a2015ee26 | ||
|
|
c440418ce6 | ||
|
|
3d37a5ba1d | ||
|
|
15f4cfb6d9 | ||
|
|
be36de2482 | ||
|
|
391237f7df | ||
|
|
977e5effd9 | ||
|
|
8b36a4cb19 | ||
|
|
c8cd94a772 | ||
|
|
764316a53d | ||
|
|
2048fd4042 | ||
|
|
f84b3de24b | ||
|
|
6a1f3ec08b | ||
|
|
2e353aee96 | ||
|
|
8edfd14a37 | ||
|
|
1a191ec6f7 | ||
|
|
e7171e94b4 | ||
|
|
398d937419 | ||
|
|
34fe5ab143 | ||
|
|
1b08f43f82 | ||
|
|
b24a2e2fdc | ||
|
|
4c4c6e626e | ||
|
|
7310370a31 | ||
|
|
8ae94f0059 | ||
|
|
8fccf20892 | ||
|
|
5a874ae241 | ||
|
|
f4e1ed25f2 | ||
|
|
cbfbb49ab4 | ||
|
|
489b16efce | ||
|
|
b82c1693c0 | ||
|
|
019bfc4e35 | ||
|
|
dfda5c054a | ||
|
|
f657169616 | ||
|
|
4c06da8b70 | ||
|
|
9fe2393a00 | ||
|
|
e2e8d94b14 | ||
|
|
4f9a7f8c87 | ||
|
|
bb0502ff44 | ||
|
|
c256442245 | ||
|
|
1137404190 | ||
|
|
d57c6d0284 | ||
|
|
76bbf57b3d | ||
|
|
806a47a7cc | ||
|
|
29851c18aa | ||
|
|
dea950c2c8 | ||
|
|
a367dfb22e | ||
|
|
9fe1a7b373 | ||
|
|
8e2d21c548 | ||
|
|
bedf1cd357 | ||
|
|
13f180a70c |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]
|
||||
7
.github/workflows/linux.yml
vendored
7
.github/workflows/linux.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: [1.14, 1.15]
|
||||
go: [1.14, 1.16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
@@ -23,16 +23,13 @@ jobs:
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
|
||||
- name: fish-actions/install-fish
|
||||
uses: fish-actions/install-fish@v1.0.0
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1.62.0
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh tmux
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
|
||||
2
.github/workflows/macos.yml
vendored
2
.github/workflows/macos.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: [1.14, 1.15]
|
||||
go: [1.14, 1.16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
|
||||
11
.gon.hcl
11
.gon.hcl
@@ -1,11 +0,0 @@
|
||||
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
|
||||
sign {
|
||||
application_identity = "Apple Development: junegunn.c@gmail.com"
|
||||
}
|
||||
@@ -14,10 +14,18 @@ builds:
|
||||
- amd64
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: gon .gon.hcl
|
||||
|
||||
- goos:
|
||||
- id: fzf-macos-arm
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
|
||||
- id: fzf
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
@@ -44,6 +52,8 @@ builds:
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
builds:
|
||||
- fzf
|
||||
format: tar.gz
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
@@ -51,12 +61,61 @@ archives:
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
signs:
|
||||
- id: fzf-macos-sign
|
||||
ids: [fzf-macos]
|
||||
artifacts: all
|
||||
cmd: sh
|
||||
args:
|
||||
- "-c"
|
||||
- |-
|
||||
cat > /tmp/fzf-gon-amd64.hcl << EOF
|
||||
source = ["./dist/fzf-macos_darwin_amd64/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
zip {
|
||||
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon-amd64.hcl
|
||||
|
||||
- id: fzf-macos-arm-sign
|
||||
ids: [fzf-macos-arm]
|
||||
artifacts: all
|
||||
cmd: sh
|
||||
args:
|
||||
- "-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"
|
||||
}
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
zip {
|
||||
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon-arm64.hcl
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Tag }}'
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-devel"
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -1,6 +1,55 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.27.0
|
||||
------
|
||||
- More border options for `--preview-window`
|
||||
```sh
|
||||
fzf --preview 'cat {}' --preview-window border-left
|
||||
fzf --preview 'cat {}' --preview-window border-left --border horizontal
|
||||
fzf --preview 'cat {}' --preview-window top:border-bottom
|
||||
fzf --preview 'cat {}' --preview-window top:border-horizontal
|
||||
```
|
||||
- Automatically set `/dev/tty` as STDIN on execute action
|
||||
```sh
|
||||
# Redirect /dev/tty to suppress "Vim: Warning: Input is not from a terminal"
|
||||
# ls | fzf --bind "enter:execute(vim {} < /dev/tty)"
|
||||
|
||||
# "< /dev/tty" part is no longer needed
|
||||
ls | fzf --bind "enter:execute(vim {})"
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
- Signed and notarized macOS binaries
|
||||
(Huge thanks to [BACKERS.md](https://github.com/junegunn/junegunn/blob/main/BACKERS.md)!)
|
||||
|
||||
0.26.0
|
||||
------
|
||||
- Added support for fixed header in preview window
|
||||
```sh
|
||||
# Display top 3 lines as the fixed header
|
||||
fzf --preview 'bat --style=header,grid --color=always {}' --preview-window '~3'
|
||||
```
|
||||
- More advanced preview offset expression to better support the fixed header
|
||||
```sh
|
||||
# Preview with bat, matching line in the middle of the window below
|
||||
# the fixed header of the top 3 lines
|
||||
#
|
||||
# ~3 Top 3 lines as the fixed header
|
||||
# +{2} Base scroll offset extracted from the second field
|
||||
# +3 Extra offset to compensate for the 3-line header
|
||||
# /2 Put in the middle of the preview area
|
||||
#
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : \
|
||||
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \
|
||||
--preview-window '~3:+{2}+3/2'
|
||||
```
|
||||
- Added `select` and `deselect` action for unconditionally selecting or
|
||||
deselecting a single item in `--multi` mode. Complements `toggle` action.
|
||||
- Sigificant performance improvement in ANSI code processing
|
||||
- Bug fixes and improvements
|
||||
- Built with Go 1.16
|
||||
|
||||
0.25.1
|
||||
------
|
||||
- Added `close` action
|
||||
|
||||
6
Makefile
6
Makefile
@@ -66,12 +66,16 @@ endif
|
||||
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/src \
|
||||
github.com/junegunn/fzf/src/algo \
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
build:
|
||||
@@ -152,4 +156,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all build release test install clean docker docker-test update
|
||||
.PHONY: all build release test bench install clean docker docker-test update
|
||||
|
||||
14
README.md
14
README.md
@@ -25,7 +25,7 @@ Table of Contents
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Installation](#installation)
|
||||
* [Using Homebrew or Linuxbrew](#using-homebrew-or-linuxbrew)
|
||||
* [Using Homebrew](#using-homebrew)
|
||||
* [Using git](#using-git)
|
||||
* [Using Linux package managers](#using-linux-package-managers)
|
||||
* [Windows](#windows)
|
||||
@@ -84,9 +84,9 @@ stuff.
|
||||
|
||||
[bin]: https://github.com/junegunn/fzf/releases
|
||||
|
||||
### Using Homebrew or Linuxbrew
|
||||
### Using Homebrew
|
||||
|
||||
You can use [Homebrew](http://brew.sh/) or [Linuxbrew](http://linuxbrew.sh/)
|
||||
You can use [Homebrew](http://brew.sh/) (on macOS or Linux)
|
||||
to install fzf.
|
||||
|
||||
```sh
|
||||
@@ -411,7 +411,7 @@ unalias **<TAB>
|
||||
export FZF_COMPLETION_TRIGGER='~~'
|
||||
|
||||
# Options to fzf command
|
||||
export FZF_COMPLETION_OPTS='+c -x'
|
||||
export FZF_COMPLETION_OPTS='--border --info=inline'
|
||||
|
||||
# Use fd (https://github.com/sharkdp/fd) instead of the default find
|
||||
# command for listing path candidates.
|
||||
@@ -572,8 +572,8 @@ FZF_DEFAULT_COMMAND='find . -type f' \
|
||||
The following example uses fzf as the selector interface for ripgrep. We bound
|
||||
`reload` action to `change` event, so every time you type on fzf, the ripgrep
|
||||
process will restart with the updated query string denoted by the placeholder
|
||||
expression `{q}`. Also, note that we used `--phony` option so that fzf doesn't
|
||||
perform any secondary filtering.
|
||||
expression `{q}`. Also, note that we used `--disabled` option so that fzf
|
||||
doesn't perform any secondary filtering.
|
||||
|
||||
```sh
|
||||
INITIAL_QUERY=""
|
||||
@@ -615,7 +615,7 @@ You can customize the size, position, and border of the preview window using
|
||||
|
||||
```bash
|
||||
fzf --height 40% --layout reverse --info inline --border \
|
||||
--preview 'file {}' --preview-window down:1:noborder \
|
||||
--preview 'file {}' --preview-window up,1,border-horizontal \
|
||||
--color 'fg:#bbccdd,fg+:#ddeeff,bg:#334455,preview-bg:#223344,border:#778899'
|
||||
```
|
||||
|
||||
|
||||
11
bin/fzf-tmux
11
bin/fzf-tmux
@@ -204,7 +204,16 @@ if [[ "$opt" =~ "-K -E" ]]; then
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
|
||||
|
||||
# tmux dropped the support for `-K`, `-R` to popup command
|
||||
# TODO: We can remove this once tmux 3.2 is released
|
||||
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
|
||||
opt="${opt/-K/}"
|
||||
else
|
||||
opt="${opt} -R"
|
||||
fi
|
||||
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
|
||||
exit $?
|
||||
fi
|
||||
|
||||
|
||||
16
go.mod
16
go.mod
@@ -2,14 +2,16 @@ module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12
|
||||
github.com/mattn/go-runewidth v0.0.9
|
||||
github.com/mattn/go-shellwords v1.0.10
|
||||
github.com/saracen/walker v0.1.1
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 // indirect
|
||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.12
|
||||
github.com/mattn/go-shellwords v1.0.11
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/saracen/walker v0.1.2
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
)
|
||||
|
||||
go 1.13
|
||||
|
||||
42
go.sum
42
go.sum
@@ -2,34 +2,32 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
||||
github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/saracen/walker v0.1.1 h1:Ou2QIKTWqo0QxhtuHVmtObbmhjMCEUyJ82xp0uV+MGI=
|
||||
github.com/saracen/walker v0.1.1/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
github.com/mattn/go-runewidth v0.0.12 h1:Y41i/hVW3Pgwr8gV+J23B9YEY0zxjptBuCWEaxmAOow=
|
||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||
github.com/mattn/go-shellwords v1.0.11 h1:vCoR9VPpsk/TZFW2JwK5I9S0xdrtUq2bph6/YjEPnaw=
|
||||
github.com/mattn/go-shellwords v1.0.11/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
|
||||
github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1 h1:/DtoiOYKoQCcIFXQjz07RnWNPRCbqmSXSpgEzhC9ZHM=
|
||||
golang.org/x/sys v0.0.0-20201026173827-119d4633e4d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
5
install
5
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.25.1
|
||||
version=0.27.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -168,7 +168,8 @@ archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ *64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.25.1"
|
||||
$version="0.27.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.25"
|
||||
var version string = "0.27"
|
||||
var revision string = "devel"
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -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 "Feb 2021" "fzf 0.25.1" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf-tmux 1 "Apr 2021" "fzf 0.27.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 "Feb 2021" "fzf 0.25.1" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Apr 2021" "fzf 0.27.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -203,6 +203,8 @@ Draw border around the finder
|
||||
.br
|
||||
.BR right
|
||||
.br
|
||||
.BR none
|
||||
.br
|
||||
|
||||
.TP
|
||||
.B "--no-unicode"
|
||||
@@ -442,7 +444,7 @@ e.g.
|
||||
done'\fR
|
||||
.RE
|
||||
.TP
|
||||
.BI "--preview-window=" "[POSITION][:SIZE[%]][:rounded|sharp|noborder][:[no]wrap][:[no]follow][:[no]cycle][:[no]hidden][:+SCROLL[-OFFSET]][:default]"
|
||||
.BI "--preview-window=" "[POSITION][,SIZE[%]][,border-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
@@ -477,35 +479,51 @@ e.g.
|
||||
* Cyclic scrolling is enabled with \fB:cycle\fR flag.
|
||||
|
||||
* To change the style of the border of the preview window, specify one of
|
||||
\fBrounded\fR (border with rounded edges, default), \fBsharp\fR (border with
|
||||
sharp edges), or \fBnoborder\fR (no border).
|
||||
the options for \fB--border\fR with \fBborder-\fR prefix.
|
||||
e.g. \fBborder-rounded\fR (border with rounded edges, default),
|
||||
\fBborder-sharp\fR (border with sharp edges), \fBborder-left\fR,
|
||||
\fBborder-none\fR, etc.
|
||||
|
||||
* \fB+SCROLL[-OFFSET]\fR determines the initial scroll offset of the preview
|
||||
window. \fBSCROLL\fR can be either a numeric integer or a single-field index
|
||||
expression that refers to a numeric integer. The optional \fB-OFFSET\fR part is
|
||||
for adjusting the base offset so that you can see the text above it. It should
|
||||
be given as a numeric integer (\fB-INTEGER\fR), or as a denominator form
|
||||
(\fB-/INTEGER\fR) for specifying a fraction of the preview window height.
|
||||
* \fB[:+SCROLL[OFFSETS][/DENOM]]\fR determines the initial scroll offset of the
|
||||
preview window.
|
||||
|
||||
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer.
|
||||
|
||||
- The optional \fBOFFSETS\fR part is for adjusting the base offset. It should be given as a series of signed integers (\fB-INTEGER\fR or \fB+INTEGER\fR).
|
||||
|
||||
- The final \fB/DENOM\fR part is for specifying a fraction of the preview window height.
|
||||
|
||||
* \fB~HEADER_LINES\fR keeps the top N lines as the fixed header so that they
|
||||
are always visible.
|
||||
|
||||
* \fBdefault\fR resets all options previously set to the default.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
\fB# Non-default scroll window positions and sizes
|
||||
fzf --preview="head {}" --preview-window=up:30%
|
||||
fzf --preview="file {}" --preview-window=down:1
|
||||
fzf --preview="head {}" --preview-window=up,30%
|
||||
fzf --preview="file {}" --preview-window=down,1
|
||||
|
||||
# Initial scroll offset is set to the line number of each line of
|
||||
# git grep output *minus* 5 lines (-5)
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : --preview 'nl {1}' --preview-window +{2}-5
|
||||
fzf --delimiter : --preview 'nl {1}' --preview-window '+{2}-5'
|
||||
|
||||
# Preview with bat, matching line in the middle of the window (-/2)
|
||||
# Preview with bat, matching line in the middle of the window below
|
||||
# the fixed header of the top 3 lines
|
||||
#
|
||||
# ~3 Top 3 lines as the fixed header
|
||||
# +{2} Base scroll offset extracted from the second field
|
||||
# +3 Extra offset to compensate for the 3-line header
|
||||
# /2 Put in the middle of the preview area
|
||||
#
|
||||
git grep --line-number '' |
|
||||
fzf --delimiter : \\
|
||||
--preview 'bat --style=numbers --color=always --highlight-line {2} {1}' \\
|
||||
--preview-window +{2}-/2\fR
|
||||
--preview 'bat --style=full --color=always --highlight-line {2} {1}' \\
|
||||
--preview-window '~3,+{2}+3/2'
|
||||
|
||||
# Display top 3 lines as the fixed header
|
||||
fzf --preview 'bat --style=full --color=always {}' --preview-window '~3'\fR
|
||||
.RE
|
||||
|
||||
.SS Scripting
|
||||
@@ -784,6 +802,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBclear-query\fR (clear query string)
|
||||
\fBdelete-char\fR \fIdel\fR
|
||||
\fBdelete-char/eof\fR \fIctrl-d\fR (same as \fBdelete-char\fR except aborts fzf if query is empty)
|
||||
\fBdeselect\fR
|
||||
\fBdeselect-all\fR (deselect all matches)
|
||||
\fBdisable-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl-j ctrl-n down\fR
|
||||
@@ -819,6 +838,7 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBrefresh-preview\fR
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreplace-query\fR (replace query string with the current selection)
|
||||
\fBselect\fR
|
||||
\fBselect-all\fR (select all matches)
|
||||
\fBtoggle\fR (\fIright-click\fR)
|
||||
\fBtoggle-all\fR (toggle all matches)
|
||||
|
||||
101
plugin/fzf.vim
101
plugin/fzf.vim
@@ -154,26 +154,51 @@ function! fzf#install()
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! s:version_requirement(val, min)
|
||||
let val = split(a:val, '\.')
|
||||
let min = split(a:min, '\.')
|
||||
for idx in range(0, len(min) - 1)
|
||||
let v = get(val, idx, 0)
|
||||
if v < min[idx] | return 0
|
||||
elseif v > min[idx] | return 1
|
||||
let s:versions = {}
|
||||
function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = a:bin . ' --version'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
endif
|
||||
let ver = matchstr(output[-1], '[0-9.]\+')
|
||||
let s:versions[a:bin] = ver
|
||||
return ver
|
||||
endfunction
|
||||
|
||||
function! s:compare_versions(a, b)
|
||||
let a = split(a:a, '\.')
|
||||
let b = split(a:b, '\.')
|
||||
for idx in range(0, max([len(a), len(b)]) - 1)
|
||||
let v1 = str2nr(get(a, idx, 0))
|
||||
let v2 = str2nr(get(b, idx, 0))
|
||||
if v1 < v2 | return -1
|
||||
elseif v1 > v2 | return 1
|
||||
endif
|
||||
endfor
|
||||
return 1
|
||||
return 0
|
||||
endfunction
|
||||
|
||||
function! s:compare_binary_versions(a, b)
|
||||
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||
endfunction
|
||||
|
||||
let s:checked = {}
|
||||
function! fzf#exec(...)
|
||||
if !exists('s:exec')
|
||||
let binaries = []
|
||||
if executable('fzf')
|
||||
call add(binaries, 'fzf')
|
||||
endif
|
||||
if executable(s:fzf_go)
|
||||
let s:exec = s:fzf_go
|
||||
elseif executable('fzf')
|
||||
let s:exec = 'fzf'
|
||||
elseif input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
||||
call add(binaries, s:fzf_go)
|
||||
endif
|
||||
|
||||
if empty(binaries)
|
||||
if input('fzf executable not found. Download binary? (y/n) ') =~? '^y'
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec()
|
||||
@@ -181,19 +206,27 @@ function! fzf#exec(...)
|
||||
redraw
|
||||
throw 'fzf executable not found'
|
||||
endif
|
||||
elseif len(binaries) > 1
|
||||
call sort(binaries, 's:compare_binary_versions')
|
||||
endif
|
||||
|
||||
let s:exec = binaries[-1]
|
||||
endif
|
||||
|
||||
if a:0 && !has_key(s:checked, a:1)
|
||||
let command = s:exec . ' --version'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
throw printf('Failed to run "%s": %s', command, output)
|
||||
endif
|
||||
let fzf_version = matchstr(output[-1], '[0-9.]\+')
|
||||
if s:version_requirement(fzf_version, a:1)
|
||||
let fzf_version = s:get_version(s:exec)
|
||||
if empty(fzf_version)
|
||||
let message = printf('Failed to run "%s --version"', s:exec)
|
||||
unlet s:exec
|
||||
throw message
|
||||
end
|
||||
|
||||
if s:compare_versions(fzf_version, a:1) >= 0
|
||||
let s:checked[a:1] = 1
|
||||
return s:exec
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
||||
let s:versions = {}
|
||||
unlet s:exec
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec(a:1, 1)
|
||||
@@ -764,6 +797,13 @@ function! s:split(dict)
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
nnoremap <silent> <Plug>(fzf-insert) i
|
||||
nnoremap <silent> <Plug>(fzf-normal) <Nop>
|
||||
if exists(':tnoremap')
|
||||
tnoremap <silent> <Plug>(fzf-insert) <C-\><C-n>i
|
||||
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
|
||||
endif
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
@@ -776,7 +816,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
function! fzf.switch_back(inplace)
|
||||
if a:inplace && bufnr('') == self.buf
|
||||
if bufexists(self.pbuf)
|
||||
execute 'keepalt b' self.pbuf
|
||||
execute 'keepalt keepjumps b' self.pbuf
|
||||
endif
|
||||
" No other listed buffer
|
||||
if bufnr('') == self.buf
|
||||
@@ -792,8 +832,6 @@ function! s:execute_term(dict, command, temps) abort
|
||||
call self.switch_back(1)
|
||||
else
|
||||
if bufnr('') == self.buf
|
||||
" Exit terminal mode first (see neovim#13769)
|
||||
call feedkeys("\<C-\>\<C-n>", 'n')
|
||||
" We use close instead of bd! since Vim does not close the split when
|
||||
" there's no other listed buffer (nvim +'set nobuflisted')
|
||||
close
|
||||
@@ -818,6 +856,10 @@ function! s:execute_term(dict, command, temps) abort
|
||||
call s:pushd(self.dict)
|
||||
call s:callback(self.dict, lines)
|
||||
call self.switch_back(s:getpos() == self.ppos)
|
||||
|
||||
if &buftype == 'terminal'
|
||||
call feedkeys(&filetype == 'fzf' ? "\<Plug>(fzf-insert)" : "\<Plug>(fzf-normal)")
|
||||
endif
|
||||
endfunction
|
||||
|
||||
try
|
||||
@@ -833,16 +875,16 @@ function! s:execute_term(dict, command, temps) abort
|
||||
if has('nvim')
|
||||
call termopen(command, fzf)
|
||||
else
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit), 'term_kill': 'term'}
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||
if v:version >= 802
|
||||
let term_opts.term_kill = 'term'
|
||||
endif
|
||||
if is_popup
|
||||
let term_opts.hidden = 1
|
||||
else
|
||||
let term_opts.curwin = 1
|
||||
endif
|
||||
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
||||
if exists('&termwinkey')
|
||||
call setbufvar(fzf.buf, '&termwinkey', '<c-z>')
|
||||
endif
|
||||
if is_popup && exists('#TerminalWinOpen')
|
||||
doautocmd <nomodeline> TerminalWinOpen
|
||||
endif
|
||||
@@ -851,6 +893,9 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
endif
|
||||
tnoremap <buffer> <c-z> <nop>
|
||||
if exists('&termwinkey') && (empty(&termwinkey) || &termwinkey =~? '<c-w>')
|
||||
tnoremap <buffer> <c-w> <c-w>.
|
||||
endif
|
||||
finally
|
||||
call s:dopopd()
|
||||
endtry
|
||||
@@ -906,13 +951,9 @@ if has('nvim')
|
||||
function s:create_popup(hl, opts) abort
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||
let border = has_key(opts, 'border') ? remove(opts, 'border') : []
|
||||
let win = nvim_open_win(buf, v:true, opts)
|
||||
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
|
||||
call setwinvar(win, '&colorcolumn', '')
|
||||
if !empty(border)
|
||||
call nvim_buf_set_lines(buf, 0, -1, v:true, border)
|
||||
endif
|
||||
return buf
|
||||
endfunction
|
||||
else
|
||||
|
||||
@@ -20,6 +20,7 @@ function fzf_key_bindings
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
@@ -42,6 +43,7 @@ function fzf_key_bindings
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
@@ -74,6 +76,7 @@ function fzf_key_bindings
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
@@ -88,6 +91,7 @@ function fzf_key_bindings
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
commandline -it -- $prefix
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,9 +120,15 @@ function fzf_key_bindings
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath and rest of token'
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l commandline (commandline -t)
|
||||
|
||||
# strip -option= from token if present
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
set -l commandline (eval "printf '%s' "(commandline -t))
|
||||
eval set commandline $commandline
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
@@ -138,6 +148,7 @@ function fzf_key_bindings
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
echo $prefix
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
|
||||
253
src/ansi.go
253
src/ansi.go
@@ -1,8 +1,6 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -82,73 +80,154 @@ func toAnsiString(color tui.Color, offset int) string {
|
||||
return ret + ";"
|
||||
}
|
||||
|
||||
var ansiRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
/*
|
||||
References:
|
||||
- https://github.com/gnachman/iTerm2
|
||||
- http://ascii-table.com/ansi-escape-sequences.php
|
||||
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
- https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
*/
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences
|
||||
ansiRegex = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
func isPrint(c uint8) bool {
|
||||
return '\x20' <= c && c <= '\x7e'
|
||||
}
|
||||
|
||||
func findAnsiStart(str string) int {
|
||||
idx := 0
|
||||
for ; idx < len(str); idx++ {
|
||||
b := str[idx]
|
||||
if b == 0x1b || b == 0x0e || b == 0x0f {
|
||||
return idx
|
||||
func matchOperatingSystemCommand(s string) int {
|
||||
// `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ match starting here
|
||||
//
|
||||
i := 5 // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||
}
|
||||
if b == 0x08 && idx > 0 {
|
||||
return idx - 1
|
||||
if i < len(s) {
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
return idx
|
||||
return -1
|
||||
}
|
||||
|
||||
func matchControlSequence(s string) int {
|
||||
// `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
|
||||
// ^ match starting here
|
||||
//
|
||||
i := 2 // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';' || s[i] == '?'); i++ {
|
||||
}
|
||||
if i < len(s) {
|
||||
c := s[i]
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func isCtrlSeqStart(c uint8) bool {
|
||||
return c == '\\' || c == '[' || c == '(' || c == ')'
|
||||
}
|
||||
|
||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||
// calling FindStringIndex() on the below regex (which was originally used):
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;?]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
|
||||
//
|
||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
// fast check for ANSI escape sequences
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x0e', '\x0f', '\x1b', '\x08':
|
||||
// We ignore the fact that '\x08' cannot be the first char
|
||||
// in the string and be an escape sequence for the sake of
|
||||
// speed and simplicity.
|
||||
goto Loop
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
|
||||
Loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x08':
|
||||
// backtrack to match: `.\x08`
|
||||
if i > 0 && s[i-1] != '\n' {
|
||||
if s[i-1] < utf8.RuneSelf {
|
||||
return i - 1, i + 1
|
||||
}
|
||||
_, n := utf8.DecodeLastRuneInString(s[:i])
|
||||
return i - n, i + 1
|
||||
}
|
||||
case '\x1b':
|
||||
// match: `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
|
||||
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
|
||||
if j := matchControlSequence(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
|
||||
s[i+3] == ';' && isPrint(s[i+4]) {
|
||||
|
||||
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b.`
|
||||
if i+1 < len(s) && s[i+1] != '\n' {
|
||||
if s[i+1] < utf8.RuneSelf {
|
||||
return i, i + 2
|
||||
}
|
||||
_, n := utf8.DecodeRuneInString(s[i+1:])
|
||||
return i, i + n + 1
|
||||
}
|
||||
case '\x0e', '\x0f':
|
||||
// match: `[\x0e\x0f]`
|
||||
return i, i + 1
|
||||
}
|
||||
}
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||
var offsets []ansiOffset
|
||||
var output bytes.Buffer
|
||||
// We append to a stack allocated variable that we'll
|
||||
// later copy and return, to save on allocations.
|
||||
offsets := make([]ansiOffset, 0, 32)
|
||||
|
||||
if state != nil {
|
||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||
}
|
||||
|
||||
prevIdx := 0
|
||||
runeCount := 0
|
||||
var (
|
||||
pstate *ansiState // lazily allocated
|
||||
output strings.Builder
|
||||
prevIdx int
|
||||
runeCount int
|
||||
)
|
||||
for idx := 0; idx < len(str); {
|
||||
idx += findAnsiStart(str[idx:])
|
||||
if idx == len(str) {
|
||||
// Make sure that we found an ANSI code
|
||||
start, end := nextAnsiEscapeSequence(str[idx:])
|
||||
if start == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Make sure that we found an ANSI code
|
||||
offset := ansiRegex.FindStringIndex(str[idx:])
|
||||
if len(offset) < 2 {
|
||||
idx++
|
||||
continue
|
||||
}
|
||||
offset[0] += idx
|
||||
offset[1] += idx
|
||||
idx = offset[1]
|
||||
start += idx
|
||||
idx += end
|
||||
|
||||
// Check if we should continue
|
||||
prev := str[prevIdx:offset[0]]
|
||||
prev := str[prevIdx:start]
|
||||
if proc != nil && !proc(prev, state) {
|
||||
return "", nil, nil
|
||||
}
|
||||
prevIdx = idx
|
||||
|
||||
prevIdx = offset[1]
|
||||
if len(prev) != 0 {
|
||||
runeCount += utf8.RuneCountInString(prev)
|
||||
// Grow the buffer size to the maximum possible length (string length
|
||||
// containing ansi codes) to avoid repetitive allocation
|
||||
if output.Cap() == 0 {
|
||||
output.Grow(len(str))
|
||||
}
|
||||
output.WriteString(prev)
|
||||
}
|
||||
|
||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
||||
newState := interpretCode(str[start:idx], state)
|
||||
if !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
@@ -157,8 +236,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
state = newState
|
||||
offsets = append(offsets, ansiOffset{[2]int32{int32(runeCount), int32(runeCount)}, *state})
|
||||
if pstate == nil {
|
||||
pstate = &ansiState{}
|
||||
}
|
||||
*pstate = newState
|
||||
state = pstate
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
newState,
|
||||
})
|
||||
} else {
|
||||
// Discard state
|
||||
state = nil
|
||||
@@ -168,7 +254,6 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
|
||||
var rest string
|
||||
var trimmed string
|
||||
|
||||
if prevIdx == 0 {
|
||||
// No ANSI code found
|
||||
rest = str
|
||||
@@ -178,51 +263,75 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
output.WriteString(rest)
|
||||
trimmed = output.String()
|
||||
}
|
||||
if proc != nil {
|
||||
proc(rest, state)
|
||||
}
|
||||
if len(offsets) > 0 {
|
||||
if len(rest) > 0 && state != nil {
|
||||
// Update last offset
|
||||
runeCount += utf8.RuneCountInString(rest)
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
if proc != nil {
|
||||
proc(rest, state)
|
||||
// Return a copy of the offsets slice
|
||||
a := make([]ansiOffset, len(offsets))
|
||||
copy(a, offsets)
|
||||
return trimmed, &a, state
|
||||
}
|
||||
if len(offsets) == 0 {
|
||||
return trimmed, nil, state
|
||||
}
|
||||
return trimmed, &offsets, state
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
// State
|
||||
var state *ansiState
|
||||
func parseAnsiCode(s string) (int, string) {
|
||||
var remaining string
|
||||
if i := strings.IndexByte(s, ';'); i >= 0 {
|
||||
remaining = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
// Inlined version of strconv.Atoi() that only handles positive
|
||||
// integers and does not allocate on error.
|
||||
code := 0
|
||||
for _, ch := range []byte(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, remaining
|
||||
}
|
||||
code = code*10 + int(ch)
|
||||
}
|
||||
return code, remaining
|
||||
}
|
||||
|
||||
return -1, remaining
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = &ansiState{-1, -1, 0, -1}
|
||||
state = ansiState{-1, -1, 0, -1}
|
||||
} else {
|
||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if strings.HasSuffix(ansiCode, "0K") && prevState != nil {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
state.lbg = prevState.bg
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
ptr := &state.fg
|
||||
state256 := 0
|
||||
|
||||
init := func() {
|
||||
if len(ansiCode) <= 3 {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
state256 = 0
|
||||
return state
|
||||
}
|
||||
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
if len(ansiCode) == 0 {
|
||||
init()
|
||||
}
|
||||
for _, code := range strings.Split(ansiCode, ";") {
|
||||
if num, err := strconv.Atoi(code); err == nil {
|
||||
|
||||
state256 := 0
|
||||
ptr := &state.fg
|
||||
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
@@ -253,7 +362,10 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
case 24: // tput rmul
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
case 0:
|
||||
init()
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
state256 = 0
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
state.fg = tui.Color(num - 30)
|
||||
@@ -289,6 +401,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
|
||||
239
src/ansi_test.go
239
src/ansi_test.go
@@ -2,12 +2,190 @@ package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
// The following regular expression will include not all but most of the
|
||||
// frequently used ANSI sequences. This regex is used as a reference for
|
||||
// testing nextAnsiEscapeSequence().
|
||||
//
|
||||
// References:
|
||||
// - https://github.com/gnachman/iTerm2
|
||||
// - http://ascii-table.com/ansi-escape-sequences.php
|
||||
// - http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexRefence = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
|
||||
toSlice := func(start, end int) []int {
|
||||
if start == -1 {
|
||||
return nil
|
||||
}
|
||||
return []int{start, end}
|
||||
}
|
||||
|
||||
s := str
|
||||
for i := 0; ; i++ {
|
||||
got := toSlice(nextAnsiEscapeSequence(s))
|
||||
exp := ansiRegexRefence.FindStringIndex(s)
|
||||
|
||||
equal := len(got) == len(exp)
|
||||
if equal {
|
||||
for i := 0; i < len(got); i++ {
|
||||
if got[i] != exp[i] {
|
||||
equal = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !equal {
|
||||
var exps, gots []rune
|
||||
if len(got) == 2 {
|
||||
gots = []rune(s[got[0]:got[1]])
|
||||
}
|
||||
if len(exp) == 2 {
|
||||
exps = []rune(s[exp[0]:exp[1]])
|
||||
}
|
||||
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
|
||||
return
|
||||
}
|
||||
if len(exp) == 0 {
|
||||
return
|
||||
}
|
||||
s = s[exp[1]:]
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence(t *testing.T) {
|
||||
testStrs := []string{
|
||||
"\x1b[0mhello world",
|
||||
"\x1b[1mhello world",
|
||||
"椙\x1b[1m椙",
|
||||
"椙\x1b[1椙m椙",
|
||||
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||
"\x1b[1mhello \x1b[Kworld",
|
||||
"hello \x1b[34;45;1mworld",
|
||||
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||
"hello \x1b[32;1mworld",
|
||||
"hello world",
|
||||
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||
"\x1b椙",
|
||||
"椙\x08",
|
||||
"\n\x08",
|
||||
"X\x08",
|
||||
"",
|
||||
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
|
||||
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
|
||||
ansiBenchmarkString,
|
||||
}
|
||||
|
||||
for _, s := range testStrs {
|
||||
testParserReference(t, s)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
testStrs := []string{
|
||||
"\x1b[0mhello world",
|
||||
"\x1b[1mhello world",
|
||||
"椙\x1b[1m椙",
|
||||
"椙\x1b[1椙m椙",
|
||||
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||
"\x1b[1mhello \x1b[Kworld",
|
||||
"hello \x1b[34;45;1mworld",
|
||||
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||
"hello \x1b[32;1mworld",
|
||||
"hello world",
|
||||
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||
ansiBenchmarkString,
|
||||
}
|
||||
|
||||
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
|
||||
|
||||
modifyString := func(s string, rr *rand.Rand) string {
|
||||
n := rr.Intn(len(s))
|
||||
b := []rune(s)
|
||||
for ; n >= 0 && len(b) != 0; n-- {
|
||||
i := rr.Intn(len(b))
|
||||
switch x := rr.Intn(4); x {
|
||||
case 0:
|
||||
b = append(b[:i], b[i+1:]...)
|
||||
case 1:
|
||||
j := rr.Intn(len(replacementBytes) - 1)
|
||||
b[i] = replacementBytes[j]
|
||||
case 2:
|
||||
x := rune(rr.Intn(utf8.MaxRune))
|
||||
for !utf8.ValidRune(x) {
|
||||
x = rune(rr.Intn(utf8.MaxRune))
|
||||
}
|
||||
b[i] = x
|
||||
case 3:
|
||||
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
|
||||
default:
|
||||
t.Fatalf("unsupported value: %d", x)
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for _, s := range testStrs {
|
||||
for i := 1_000; i >= 0; i-- {
|
||||
testParserReference(t, modifyString(s, rr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
randomString := func(rr *rand.Rand) string {
|
||||
numChars := rand.Intn(50)
|
||||
codePoints := make([]rune, numChars)
|
||||
for i := 0; i < len(codePoints); i++ {
|
||||
var r rune
|
||||
for n := 0; n < 1000; n++ {
|
||||
r = rune(rr.Intn(utf8.MaxRune))
|
||||
// Allow 10% of runes to be invalid
|
||||
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||
break
|
||||
}
|
||||
}
|
||||
codePoints[i] = r
|
||||
}
|
||||
return string(codePoints)
|
||||
}
|
||||
|
||||
rr := rand.New(rand.NewSource(1))
|
||||
for i := 0; i < 100_000; i++ {
|
||||
testParserReference(t, randomString(rr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractColor(t *testing.T) {
|
||||
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
||||
var attr tui.Attr
|
||||
@@ -185,3 +363,64 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
||||
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||
}
|
||||
|
||||
func TestParseAnsiCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
In, Exp string
|
||||
N int
|
||||
}{
|
||||
{"123", "", 123},
|
||||
{"1a", "", -1},
|
||||
{"1a;12", "12", -1},
|
||||
{"12;a", "a", 12},
|
||||
{"-2", "", -1},
|
||||
}
|
||||
for _, x := range tests {
|
||||
n, s := parseAnsiCode(x.In)
|
||||
if n != x.N || s != x.Exp {
|
||||
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// kernel/bpf/preload/iterators/README
|
||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
|
||||
"\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||
"\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
|
||||
|
||||
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
_, o := nextAnsiEscapeSequence(s)
|
||||
if o == -1 {
|
||||
break
|
||||
}
|
||||
s = s[o:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
|
||||
// previously used regex based implementation.
|
||||
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
s := ansiBenchmarkString
|
||||
for {
|
||||
a := ansiRegexRefence.FindStringIndex(s)
|
||||
if len(a) == 0 {
|
||||
break
|
||||
}
|
||||
s = s[a[1]:]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkExtractColor(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
for i := 0; i < b.N; i++ {
|
||||
extractColor(ansiBenchmarkString, nil, nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ func TestChunkCache(t *testing.T) {
|
||||
cache := NewChunkCache()
|
||||
chunk1p := &Chunk{}
|
||||
chunk2p := &Chunk{count: chunkSize}
|
||||
items1 := []Result{Result{}}
|
||||
items2 := []Result{Result{}, Result{}}
|
||||
items1 := []Result{{}}
|
||||
items2 := []Result{{}, {}}
|
||||
cache.Add(chunk1p, "foo", items1)
|
||||
cache.Add(chunk2p, "foo", items1)
|
||||
cache.Add(chunk2p, "bar", items2)
|
||||
|
||||
@@ -73,6 +73,7 @@ const (
|
||||
EvtSearchFin
|
||||
EvtHeader
|
||||
EvtReady
|
||||
EvtQuit
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -254,7 +254,11 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
for evt, value := range *events {
|
||||
switch evt {
|
||||
|
||||
case EvtQuit:
|
||||
if reading {
|
||||
reader.terminate()
|
||||
}
|
||||
os.Exit(value.(int))
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand)
|
||||
|
||||
@@ -106,7 +106,6 @@ func (mg *Merger) mergedGet(idx int) Result {
|
||||
minIdx = listIdx
|
||||
}
|
||||
}
|
||||
mg.cursors[listIdx] = cursor
|
||||
}
|
||||
|
||||
if minIdx >= 0 {
|
||||
|
||||
@@ -58,7 +58,7 @@ const usage = `usage: fzf [options]
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
--border[=STYLE] Draw border around the finder
|
||||
[rounded|sharp|horizontal|vertical|
|
||||
top|bottom|left|right] (default: rounded)
|
||||
top|bottom|left|right|none] (default: rounded)
|
||||
--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|inline|hidden]
|
||||
@@ -81,11 +81,11 @@ const usage = `usage: fzf [options]
|
||||
Preview
|
||||
--preview=COMMAND Command to preview highlighted line ({})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][:SIZE[%]]
|
||||
[:[no]wrap][:[no]cycle][:[no]follow][:[no]hidden]
|
||||
[:rounded|sharp|noborder]
|
||||
[:+SCROLL[-OFFSET]]
|
||||
[:default]
|
||||
[up|down|left|right][,SIZE[%]]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]hidden]
|
||||
[,border-BORDER_OPT]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default]
|
||||
|
||||
Scripting
|
||||
-q, --query=STR Start the finder with the given query
|
||||
@@ -170,6 +170,7 @@ type previewOpts struct {
|
||||
cycle bool
|
||||
follow bool
|
||||
border tui.BorderShape
|
||||
headerLines int
|
||||
}
|
||||
|
||||
// Options stores the values of command-line options
|
||||
@@ -231,7 +232,7 @@ type Options struct {
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded}
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.BorderRounded, 0}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -435,11 +436,13 @@ func parseBorder(str string, optional bool) tui.BorderShape {
|
||||
return tui.BorderLeft
|
||||
case "right":
|
||||
return tui.BorderRight
|
||||
case "none":
|
||||
return tui.BorderNone
|
||||
default:
|
||||
if optional && str == "" {
|
||||
return tui.BorderRounded
|
||||
}
|
||||
errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right)")
|
||||
errorExit("invalid border style (expected: rounded|sharp|horizontal|vertical|top|bottom|left|right|none)")
|
||||
}
|
||||
return tui.BorderNone
|
||||
}
|
||||
@@ -839,6 +842,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
||||
appendAction(actDeleteChar)
|
||||
case "delete-char/eof":
|
||||
appendAction(actDeleteCharEOF)
|
||||
case "deselect":
|
||||
appendAction(actDeselect)
|
||||
case "end-of-line":
|
||||
appendAction(actEndOfLine)
|
||||
case "cancel":
|
||||
@@ -879,6 +884,8 @@ func parseKeymap(keymap map[tui.Event][]action, str string) {
|
||||
appendAction(actToggleAll)
|
||||
case "toggle-search":
|
||||
appendAction(actToggleSearch)
|
||||
case "select":
|
||||
appendAction(actSelect)
|
||||
case "select-all":
|
||||
appendAction(actSelectAll)
|
||||
case "deselect-all":
|
||||
@@ -1071,9 +1078,11 @@ func parseInfoStyle(str string) infoStyle {
|
||||
}
|
||||
|
||||
func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
tokens := strings.Split(input, ":")
|
||||
delimRegex := regexp.MustCompile("[:,]") // : for backward compatibility
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
offsetRegex := regexp.MustCompile("^\\+([0-9]+|{-?[0-9]+})(-[0-9]+|-/[1-9][0-9]*)?$")
|
||||
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||
tokens := delimRegex.Split(input, -1)
|
||||
for _, token := range tokens {
|
||||
switch token {
|
||||
case "":
|
||||
@@ -1099,21 +1108,35 @@ func parsePreviewWindow(opts *previewOpts, input string) {
|
||||
opts.position = posLeft
|
||||
case "right":
|
||||
opts.position = posRight
|
||||
case "rounded", "border":
|
||||
case "rounded", "border", "border-rounded":
|
||||
opts.border = tui.BorderRounded
|
||||
case "sharp":
|
||||
case "sharp", "border-sharp":
|
||||
opts.border = tui.BorderSharp
|
||||
case "noborder":
|
||||
case "noborder", "border-none":
|
||||
opts.border = tui.BorderNone
|
||||
case "border-horizontal":
|
||||
opts.border = tui.BorderHorizontal
|
||||
case "border-vertical":
|
||||
opts.border = tui.BorderVertical
|
||||
case "border-top":
|
||||
opts.border = tui.BorderTop
|
||||
case "border-bottom":
|
||||
opts.border = tui.BorderBottom
|
||||
case "border-left":
|
||||
opts.border = tui.BorderLeft
|
||||
case "border-right":
|
||||
opts.border = tui.BorderRight
|
||||
case "follow":
|
||||
opts.follow = true
|
||||
case "nofollow":
|
||||
opts.follow = false
|
||||
default:
|
||||
if sizeRegex.MatchString(token) {
|
||||
if headerRegex.MatchString(token) {
|
||||
opts.headerLines = atoi(token[1:])
|
||||
} else if sizeRegex.MatchString(token) {
|
||||
opts.size = parseSize(token, 99, "window size")
|
||||
} else if offsetRegex.MatchString(token) {
|
||||
opts.scroll = token[1:]
|
||||
opts.scroll = token
|
||||
} else {
|
||||
errorExit("invalid preview window option: " + token)
|
||||
}
|
||||
@@ -1360,7 +1383,7 @@ func parseOptions(opts *Options, allArgs []string) {
|
||||
opts.Preview.command = ""
|
||||
case "--preview-window":
|
||||
parsePreviewWindow(&opts.Preview,
|
||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][:SIZE[%]][:rounded|sharp|noborder][:wrap][:cycle][:hidden][:+SCROLL[-OFFSET]][:default]"))
|
||||
nextString(allArgs, &i, "preview window layout required: [up|down|left|right][,SIZE[%]][,border-BORDER_OPT][,wrap][,cycle][,hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default]"))
|
||||
case "--height":
|
||||
opts.Height = parseHeight(nextString(allArgs, &i, "height required: HEIGHT[%]"))
|
||||
case "--min-height":
|
||||
|
||||
@@ -102,7 +102,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
}
|
||||
}
|
||||
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
|
||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
@@ -384,23 +384,23 @@ func TestPreviewOpts(t *testing.T) {
|
||||
opts.Preview.size.size == 50) {
|
||||
t.Error()
|
||||
}
|
||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap:+{1}-/2")
|
||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
|
||||
if !(opts.Preview.command == "cat {}" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.position == posLeft &&
|
||||
opts.Preview.scroll == "{1}-/2" &&
|
||||
opts.Preview.scroll == "+{1}-/2" &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview)
|
||||
}
|
||||
opts = optsFor("--preview-window=up:15:wrap:hidden:+{1}-/2", "--preview-window=down", "--preview-window=cycle")
|
||||
opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
|
||||
if !(opts.Preview.command == "" &&
|
||||
opts.Preview.hidden == true &&
|
||||
opts.Preview.wrap == true &&
|
||||
opts.Preview.cycle == true &&
|
||||
opts.Preview.position == posDown &&
|
||||
opts.Preview.scroll == "{1}-/2" &&
|
||||
opts.Preview.scroll == "+{1}+3-1-2/2" &&
|
||||
opts.Preview.size.percent == false &&
|
||||
opts.Preview.size.size == 15) {
|
||||
t.Error(opts.Preview.size.size)
|
||||
|
||||
@@ -337,7 +337,7 @@ func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result,
|
||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||
var input []Token
|
||||
if len(p.nth) == 0 {
|
||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
|
||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
||||
var input []Token
|
||||
if len(p.nth) == 0 {
|
||||
input = []Token{Token{text: &item.text, prefixLength: 0}}
|
||||
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||
} else {
|
||||
input = p.transformInput(item)
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
||||
trans := Transform(tokens, []Range{{1, 1}})
|
||||
|
||||
origBytes := []byte("junegunn.choi")
|
||||
for _, extended := range []bool{false, true} {
|
||||
|
||||
@@ -18,8 +18,8 @@ func withIndex(i *Item, index int) *Item {
|
||||
|
||||
func TestOffsetSort(t *testing.T) {
|
||||
offsets := []Offset{
|
||||
Offset{3, 5}, Offset{2, 7},
|
||||
Offset{1, 3}, Offset{2, 9}}
|
||||
{3, 5}, {2, 7},
|
||||
{1, 3}, {2, 9}}
|
||||
sort.Sort(ByOrder(offsets))
|
||||
|
||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||
@@ -84,13 +84,13 @@ func TestResultRank(t *testing.T) {
|
||||
|
||||
// Sort by relevance
|
||||
item3 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
|
||||
item4 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
|
||||
item5 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
|
||||
item6 := buildResult(
|
||||
withIndex(&Item{}, 2), []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
||||
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
|
||||
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||
sort.Sort(ByRelevance(items))
|
||||
if !(items[0] == item6 && items[1] == item5 &&
|
||||
|
||||
224
src/terminal.go
224
src/terminal.go
@@ -22,8 +22,9 @@ import (
|
||||
// import "github.com/pkg/profile"
|
||||
|
||||
var placeholder *regexp.Regexp
|
||||
var numericPrefix *regexp.Regexp
|
||||
var whiteSuffix *regexp.Regexp
|
||||
var offsetComponentRegex *regexp.Regexp
|
||||
var offsetTrimCharsRegex *regexp.Regexp
|
||||
var activeTempFiles []string
|
||||
|
||||
const ellipsis string = ".."
|
||||
@@ -31,8 +32,9 @@ const clearCode string = "\x1b[2J"
|
||||
|
||||
func init() {
|
||||
placeholder = regexp.MustCompile(`\\?(?:{[+sf]*[0-9,-.]*}|{q}|{\+?f?nf?})`)
|
||||
numericPrefix = regexp.MustCompile(`^[[:punct:]]*([0-9]+)`)
|
||||
whiteSuffix = regexp.MustCompile(`\s*$`)
|
||||
offsetComponentRegex = regexp.MustCompile(`([+-][0-9]+)|(-?/[1-9][0-9]*)`)
|
||||
offsetTrimCharsRegex = regexp.MustCompile(`[^0-9/+-]`)
|
||||
activeTempFiles = []string{}
|
||||
}
|
||||
|
||||
@@ -133,6 +135,7 @@ type Terminal struct {
|
||||
count int
|
||||
progress int
|
||||
reading bool
|
||||
running bool
|
||||
failed *string
|
||||
jumping jumpMode
|
||||
jumpLabels string
|
||||
@@ -157,6 +160,7 @@ type Terminal struct {
|
||||
slab *util.Slab
|
||||
theme *tui.ColorTheme
|
||||
tui tui.Renderer
|
||||
executing *util.AtomicBool
|
||||
}
|
||||
|
||||
type selectedItem struct {
|
||||
@@ -276,6 +280,8 @@ const (
|
||||
actReload
|
||||
actDisableSearch
|
||||
actEnableSearch
|
||||
actSelect
|
||||
actDeselect
|
||||
)
|
||||
|
||||
type placeholderFlags struct {
|
||||
@@ -502,6 +508,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
ansi: opts.Ansi,
|
||||
tabstop: opts.Tabstop,
|
||||
reading: true,
|
||||
running: true,
|
||||
failed: nil,
|
||||
jumping: jumpDisabled,
|
||||
jumpLabels: opts.JumpLabels,
|
||||
@@ -523,7 +530,8 @@ func NewTerminal(opts *Options, eventBox *util.EventBox) *Terminal {
|
||||
startChan: make(chan bool, 1),
|
||||
killChan: make(chan int),
|
||||
tui: renderer,
|
||||
initFunc: func() { renderer.Init() }}
|
||||
initFunc: func() { renderer.Init() },
|
||||
executing: util.NewAtomicBool(false)}
|
||||
t.prompt, t.promptLen = t.parsePrompt(opts.Prompt)
|
||||
t.pointer, t.pointerLen = t.processTabs([]rune(opts.Pointer), 0)
|
||||
t.marker, t.markerLen = t.processTabs([]rune(opts.Marker), 0)
|
||||
@@ -823,16 +831,33 @@ func (t *Terminal) resizeWindows() {
|
||||
createPreviewWindow := func(y int, x int, w int, h int) {
|
||||
pwidth := w
|
||||
pheight := h
|
||||
if t.previewOpts.border != tui.BorderNone {
|
||||
previewBorder := tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
|
||||
var previewBorder tui.BorderStyle
|
||||
if t.previewOpts.border == tui.BorderNone {
|
||||
previewBorder = tui.MakeTransparentBorder()
|
||||
} else {
|
||||
previewBorder = tui.MakeBorderStyle(t.previewOpts.border, t.unicode)
|
||||
}
|
||||
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
||||
switch t.previewOpts.border {
|
||||
case tui.BorderSharp, tui.BorderRounded:
|
||||
pwidth -= 4
|
||||
pheight -= 2
|
||||
x += 2
|
||||
y += 1
|
||||
} else {
|
||||
previewBorder := tui.MakeTransparentBorder()
|
||||
t.pborder = t.tui.NewWindow(y, x, w, h, true, previewBorder)
|
||||
case tui.BorderLeft:
|
||||
pwidth -= 2
|
||||
x += 2
|
||||
case tui.BorderRight:
|
||||
pwidth -= 2
|
||||
case tui.BorderTop:
|
||||
pheight -= 1
|
||||
y += 1
|
||||
case tui.BorderBottom:
|
||||
pheight -= 1
|
||||
case tui.BorderHorizontal:
|
||||
pheight -= 2
|
||||
y += 1
|
||||
case tui.BorderVertical:
|
||||
pwidth -= 4
|
||||
x += 2
|
||||
}
|
||||
@@ -840,9 +865,13 @@ func (t *Terminal) resizeWindows() {
|
||||
}
|
||||
verticalPad := 2
|
||||
minPreviewHeight := 3
|
||||
if t.previewOpts.border == tui.BorderNone {
|
||||
switch t.previewOpts.border {
|
||||
case tui.BorderNone, tui.BorderVertical, tui.BorderLeft, tui.BorderRight:
|
||||
verticalPad = 0
|
||||
minPreviewHeight = 1
|
||||
case tui.BorderTop, tui.BorderBottom:
|
||||
verticalPad = 1
|
||||
minPreviewHeight = 2
|
||||
}
|
||||
switch t.previewOpts.position {
|
||||
case posUp:
|
||||
@@ -1140,8 +1169,8 @@ func (t *Terminal) trimLeft(runes []rune, width int) ([]rune, int32) {
|
||||
width = util.Max(0, width)
|
||||
var trimmed int32
|
||||
// Assume that each rune takes at least one column on screen
|
||||
if len(runes) > width {
|
||||
diff := len(runes) - width
|
||||
if len(runes) > width+2 {
|
||||
diff := len(runes) - width - 2
|
||||
trimmed = int32(diff)
|
||||
runes = runes[diff:]
|
||||
}
|
||||
@@ -1289,18 +1318,37 @@ func (t *Terminal) renderPreviewSpinner() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
maxWidth := t.pwindow.Width()
|
||||
lineNo := -t.previewer.offset
|
||||
height := t.pwindow.Height()
|
||||
func (t *Terminal) renderPreviewArea(unchanged bool) {
|
||||
if unchanged {
|
||||
t.pwindow.MoveAndClear(0, 0)
|
||||
t.pwindow.MoveAndClear(0, 0) // Clear scroll offset display
|
||||
} else {
|
||||
t.previewed.filled = false
|
||||
t.pwindow.Erase()
|
||||
}
|
||||
|
||||
height := t.pwindow.Height()
|
||||
header := []string{}
|
||||
body := t.previewer.lines
|
||||
headerLines := t.previewOpts.headerLines
|
||||
// Do not enable preview header lines if it's value is too large
|
||||
if headerLines > 0 && headerLines < util.Min(len(body), height) {
|
||||
header = t.previewer.lines[0:headerLines]
|
||||
body = t.previewer.lines[headerLines:]
|
||||
// Always redraw header
|
||||
t.renderPreviewText(height, header, 0, false)
|
||||
t.pwindow.MoveAndClear(t.pwindow.Y(), 0)
|
||||
}
|
||||
t.renderPreviewText(height, body, -t.previewer.offset+headerLines, unchanged)
|
||||
|
||||
if !unchanged {
|
||||
t.pwindow.FinishFill()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) renderPreviewText(height int, lines []string, lineNo int, unchanged bool) {
|
||||
maxWidth := t.pwindow.Width()
|
||||
var ansi *ansiState
|
||||
for _, line := range t.previewer.lines {
|
||||
for _, line := range lines {
|
||||
var lbg tui.Color = -1
|
||||
if ansi != nil {
|
||||
ansi.lbg = -1
|
||||
@@ -1314,8 +1362,9 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
prefixWidth := 0
|
||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
||||
trimmed := []rune(str)
|
||||
trimmedLen := 0
|
||||
if !t.previewOpts.wrap {
|
||||
trimmed, _ = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
trimmed, trimmedLen = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
}
|
||||
str, width := t.processTabs(trimmed, prefixWidth)
|
||||
prefixWidth += width
|
||||
@@ -1325,7 +1374,8 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
} else {
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||
}
|
||||
return fillRet == tui.FillContinue
|
||||
return trimmedLen == 0 &&
|
||||
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
||||
})
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||
if fillRet == tui.FillNextLine {
|
||||
@@ -1346,9 +1396,6 @@ func (t *Terminal) renderPreviewText(unchanged bool) {
|
||||
}
|
||||
lineNo++
|
||||
}
|
||||
if !unchanged {
|
||||
t.pwindow.FinishFill()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printPreview() {
|
||||
@@ -1361,7 +1408,7 @@ func (t *Terminal) printPreview() {
|
||||
t.previewer.version == t.previewed.version &&
|
||||
t.previewer.offset == t.previewed.offset
|
||||
t.previewer.scrollable = t.previewer.offset > 0 || numLines > height
|
||||
t.renderPreviewText(unchanged)
|
||||
t.renderPreviewArea(unchanged)
|
||||
t.renderPreviewSpinner()
|
||||
t.previewed.numLines = numLines
|
||||
t.previewed.version = t.previewer.version
|
||||
@@ -1374,7 +1421,7 @@ func (t *Terminal) printPreviewDelayed() {
|
||||
}
|
||||
|
||||
t.previewer.scrollable = false
|
||||
t.renderPreviewText(true)
|
||||
t.renderPreviewArea(true)
|
||||
|
||||
message := t.trimMessage("Loading ..", t.pwindow.Width())
|
||||
pos := t.pwindow.Width() - len(message)
|
||||
@@ -1567,43 +1614,33 @@ func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input str
|
||||
template, t.ansi, t.delimiter, t.printsep, forcePlus, input, list)
|
||||
}
|
||||
|
||||
// Ascii to positive integer
|
||||
func atopi(s string) int {
|
||||
matches := numericPrefix.FindStringSubmatch(s)
|
||||
if len(matches) < 2 {
|
||||
return 0
|
||||
}
|
||||
n, e := strconv.Atoi(matches[1])
|
||||
if e != nil || n < 1 {
|
||||
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
|
||||
offsetExpr := offsetTrimCharsRegex.ReplaceAllString(
|
||||
t.replacePlaceholder(t.previewOpts.scroll, false, "", list), "")
|
||||
|
||||
atoi := func(s string) int {
|
||||
n, e := strconv.Atoi(s)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (t *Terminal) evaluateScrollOffset(list []*Item, height int) int {
|
||||
offsetExpr := t.replacePlaceholder(t.previewOpts.scroll, false, "", list)
|
||||
nums := strings.Split(offsetExpr, "-")
|
||||
switch len(nums) {
|
||||
case 0:
|
||||
return 0
|
||||
case 1, 2:
|
||||
base := atopi(nums[0])
|
||||
if base == 0 {
|
||||
return 0
|
||||
} else if len(nums) == 1 {
|
||||
return base - 1
|
||||
}
|
||||
if nums[1][0] == '/' {
|
||||
denom := atopi(nums[1][1:])
|
||||
|
||||
base := -1
|
||||
for _, component := range offsetComponentRegex.FindAllString(offsetExpr, -1) {
|
||||
if strings.HasPrefix(component, "-/") {
|
||||
component = component[1:]
|
||||
}
|
||||
if component[0] == '/' {
|
||||
denom := atoi(component[1:])
|
||||
if denom == 0 {
|
||||
return base
|
||||
}
|
||||
return base - height/denom
|
||||
}
|
||||
return base - atopi(nums[1]) - 1
|
||||
default:
|
||||
return 0
|
||||
base += atoi(component)
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func replacePlaceholder(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
@@ -1706,8 +1743,9 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
}
|
||||
command := t.replacePlaceholder(template, forcePlus, string(t.input), list)
|
||||
cmd := util.ExecCommand(command, false)
|
||||
t.executing.Set(true)
|
||||
if !background {
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdin = tui.TtyIn()
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
t.tui.Pause(true)
|
||||
@@ -1720,6 +1758,7 @@ func (t *Terminal) executeCommand(template string, forcePlus bool, background bo
|
||||
cmd.Run()
|
||||
t.tui.Resume(false, false)
|
||||
}
|
||||
t.executing.Set(false)
|
||||
cleanTemporaryFiles()
|
||||
}
|
||||
|
||||
@@ -1785,11 +1824,26 @@ func (t *Terminal) selectItem(item *Item) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Terminal) selectItemChanged(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; found {
|
||||
return false
|
||||
}
|
||||
return t.selectItem(item)
|
||||
}
|
||||
|
||||
func (t *Terminal) deselectItem(item *Item) {
|
||||
delete(t.selected, item.Index())
|
||||
t.version++
|
||||
}
|
||||
|
||||
func (t *Terminal) deselectItemChanged(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; found {
|
||||
t.deselectItem(item)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Terminal) toggleItem(item *Item) bool {
|
||||
if _, found := t.selected[item.Index()]; !found {
|
||||
return t.selectItem(item)
|
||||
@@ -1803,7 +1857,7 @@ func (t *Terminal) killPreview(code int) {
|
||||
case t.killChan <- code:
|
||||
default:
|
||||
if code != exitCancel {
|
||||
os.Exit(code)
|
||||
t.eventBox.Set(EvtQuit, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1820,8 +1874,12 @@ func (t *Terminal) Loop() {
|
||||
intChan := make(chan os.Signal, 1)
|
||||
signal.Notify(intChan, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-intChan
|
||||
for s := range intChan {
|
||||
// Don't quit by SIGINT while executing because it should be for the executing command and not for fzf itself
|
||||
if !(s == os.Interrupt && t.executing.Get()) {
|
||||
t.reqBox.Set(reqQuit, nil)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
contChan := make(chan os.Signal, 1)
|
||||
@@ -1898,7 +1956,7 @@ func (t *Terminal) Loop() {
|
||||
cmd := util.ExecCommand(command, true)
|
||||
if pwindow != nil {
|
||||
height := pwindow.Height()
|
||||
initialOffset = util.Max(0, t.evaluateScrollOffset(items, height))
|
||||
initialOffset = util.Max(0, t.evaluateScrollOffset(items, util.Max(0, height-t.previewOpts.headerLines)))
|
||||
env := os.Environ()
|
||||
lines := fmt.Sprintf("LINES=%d", height)
|
||||
columns := fmt.Sprintf("COLUMNS=%d", pwindow.Width())
|
||||
@@ -1931,6 +1989,7 @@ func (t *Terminal) Loop() {
|
||||
}()
|
||||
|
||||
// Goroutine 2 periodically requests rendering
|
||||
rendered := util.NewAtomicBool(false)
|
||||
go func(version int64) {
|
||||
lines := []string{}
|
||||
spinner := makeSpinner(t.unicode)
|
||||
@@ -1945,6 +2004,7 @@ func (t *Terminal) Loop() {
|
||||
if spinnerIndex >= 0 {
|
||||
spin := spinner[spinnerIndex%len(spinner)]
|
||||
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, spin})
|
||||
rendered.Set(true)
|
||||
offset = -1
|
||||
}
|
||||
spinnerIndex++
|
||||
@@ -1964,6 +2024,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
if err != nil {
|
||||
t.reqBox.Set(reqPreviewDisplay, previewResult{version, lines, offset, ""})
|
||||
rendered.Set(true)
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
@@ -1983,9 +2044,15 @@ func (t *Terminal) Loop() {
|
||||
case code := <-t.killChan:
|
||||
if code != exitCancel {
|
||||
util.KillCommand(cmd)
|
||||
os.Exit(code)
|
||||
t.eventBox.Set(EvtQuit, code)
|
||||
} else {
|
||||
timer := time.NewTimer(previewCancelWait)
|
||||
// We can immediately kill a long-running preview program
|
||||
// once we started rendering its partial output
|
||||
delay := previewCancelWait
|
||||
if rendered.Get() {
|
||||
delay = 0
|
||||
}
|
||||
timer := time.NewTimer(delay)
|
||||
select {
|
||||
case <-timer.C:
|
||||
util.KillCommand(cmd)
|
||||
@@ -2020,16 +2087,6 @@ func (t *Terminal) Loop() {
|
||||
}()
|
||||
}
|
||||
|
||||
exit := func(getCode func() int) {
|
||||
t.tui.Close()
|
||||
code := getCode()
|
||||
if code <= exitNoMatch && t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
// prof.Stop()
|
||||
t.killPreview(code)
|
||||
}
|
||||
|
||||
refreshPreview := func(command string) {
|
||||
if len(command) > 0 && t.isPreviewEnabled() {
|
||||
_, list := t.buildPlusList(command, false)
|
||||
@@ -2041,7 +2098,19 @@ func (t *Terminal) Loop() {
|
||||
go func() {
|
||||
var focusedIndex int32 = minItem.Index()
|
||||
var version int64 = -1
|
||||
for {
|
||||
running := true
|
||||
code := exitError
|
||||
exit := func(getCode func() int) {
|
||||
t.tui.Close()
|
||||
code = getCode()
|
||||
if code <= exitNoMatch && t.history != nil {
|
||||
t.history.append(string(t.input))
|
||||
}
|
||||
running = false
|
||||
t.mutex.Unlock()
|
||||
}
|
||||
|
||||
for running {
|
||||
t.reqBox.Wait(func(events *util.Events) {
|
||||
defer events.Clear()
|
||||
t.mutex.Lock()
|
||||
@@ -2087,6 +2156,7 @@ func (t *Terminal) Loop() {
|
||||
}
|
||||
return exitNoMatch
|
||||
})
|
||||
return
|
||||
case reqPreviewDisplay:
|
||||
result := value.(previewResult)
|
||||
if t.previewer.version != result.version {
|
||||
@@ -2098,7 +2168,7 @@ func (t *Terminal) Loop() {
|
||||
if t.previewer.following {
|
||||
t.previewer.offset = len(t.previewer.lines) - t.pwindow.Height()
|
||||
} else if result.offset >= 0 {
|
||||
t.previewer.offset = util.Constrain(result.offset, 0, len(t.previewer.lines)-1)
|
||||
t.previewer.offset = util.Constrain(result.offset, t.previewOpts.headerLines, len(t.previewer.lines)-1)
|
||||
}
|
||||
t.printPreview()
|
||||
case reqPreviewRefresh:
|
||||
@@ -2111,14 +2181,18 @@ func (t *Terminal) Loop() {
|
||||
t.printer(string(t.input))
|
||||
return exitOk
|
||||
})
|
||||
return
|
||||
case reqQuit:
|
||||
exit(func() int { return exitInterrupt })
|
||||
return
|
||||
}
|
||||
}
|
||||
t.refresh()
|
||||
t.mutex.Unlock()
|
||||
})
|
||||
}
|
||||
// prof.Stop()
|
||||
t.killPreview(code)
|
||||
}()
|
||||
|
||||
looping := true
|
||||
@@ -2167,7 +2241,7 @@ func (t *Terminal) Loop() {
|
||||
if t.previewOpts.cycle {
|
||||
newOffset = (newOffset + numLines) % numLines
|
||||
}
|
||||
newOffset = util.Constrain(newOffset, 0, numLines-1)
|
||||
newOffset = util.Constrain(newOffset, t.previewOpts.headerLines, numLines-1)
|
||||
if t.previewer.offset != newOffset {
|
||||
t.previewer.offset = newOffset
|
||||
req(reqPreviewRefresh)
|
||||
@@ -2341,6 +2415,16 @@ func (t *Terminal) Loop() {
|
||||
} else {
|
||||
req(reqQuit)
|
||||
}
|
||||
case actSelect:
|
||||
current := t.currentItem()
|
||||
if t.multi > 0 && current != nil && t.selectItemChanged(current) {
|
||||
req(reqList, reqInfo)
|
||||
}
|
||||
case actDeselect:
|
||||
current := t.currentItem()
|
||||
if t.multi > 0 && current != nil && t.deselectItemChanged(current) {
|
||||
req(reqList, reqInfo)
|
||||
}
|
||||
case actToggle:
|
||||
if t.multi > 0 && t.merger.Length() > 0 && toggle() {
|
||||
req(reqList)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -74,7 +74,7 @@ type LightRenderer struct {
|
||||
clickY []int
|
||||
ttyin *os.File
|
||||
buffer []byte
|
||||
origState *terminal.State
|
||||
origState *term.State
|
||||
width int
|
||||
height int
|
||||
yoffset int
|
||||
@@ -693,13 +693,17 @@ func (w *LightWindow) drawBorder() {
|
||||
}
|
||||
|
||||
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
color := ColBorder
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
if top {
|
||||
w.Move(0, 0)
|
||||
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width))
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
||||
}
|
||||
if bottom {
|
||||
w.Move(w.height-1, 0)
|
||||
w.CPrint(ColBorder, repeat(w.border.horizontal, w.width))
|
||||
w.CPrint(color, repeat(w.border.horizontal, w.width))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,14 +712,18 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
if !left || !right {
|
||||
width++
|
||||
}
|
||||
color := ColBorder
|
||||
if w.preview {
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
for y := 0; y < w.height; y++ {
|
||||
w.Move(y, 0)
|
||||
if left {
|
||||
w.CPrint(ColBorder, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
}
|
||||
w.CPrint(ColBorder, repeat(' ', width))
|
||||
w.CPrint(color, repeat(' ', width))
|
||||
if right {
|
||||
w.CPrint(ColBorder, string(w.border.vertical))
|
||||
w.CPrint(color, string(w.border.vertical))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -906,12 +914,6 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
for j, wl := range lines {
|
||||
if w.posx >= w.Width()-1 && wl.displayWidth == 0 {
|
||||
if w.posy < w.height-1 {
|
||||
w.Move(w.posy+1, 0)
|
||||
}
|
||||
return FillNextLine
|
||||
}
|
||||
w.stderrInternal(wl.text, false)
|
||||
w.posx += wl.displayWidth
|
||||
|
||||
@@ -926,6 +928,14 @@ func (w *LightWindow) fill(str string, onMove func()) FillReturn {
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.posx+1 >= w.Width() {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.Move(w.posy+1, 0)
|
||||
onMove()
|
||||
return FillNextLine
|
||||
}
|
||||
return FillContinue
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
func IsLightRendererSupported() bool {
|
||||
@@ -34,12 +34,12 @@ func (r *LightRenderer) fd() int {
|
||||
|
||||
func (r *LightRenderer) initPlatform() error {
|
||||
fd := r.fd()
|
||||
origState, err := terminal.GetState(fd)
|
||||
origState, err := term.GetState(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.origState = origState
|
||||
terminal.MakeRaw(fd)
|
||||
term.MakeRaw(fd)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -63,15 +63,15 @@ func openTtyIn() *os.File {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
terminal.MakeRaw(r.fd())
|
||||
term.MakeRaw(r.fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() {
|
||||
terminal.Restore(r.fd(), r.origState)
|
||||
term.Restore(r.fd(), r.origState)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() {
|
||||
width, height, err := terminal.GetSize(r.fd())
|
||||
width, height, err := term.GetSize(r.fd())
|
||||
|
||||
if err == nil {
|
||||
r.width = width
|
||||
|
||||
@@ -5,11 +5,16 @@ package tui
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
timeoutInterval = 10
|
||||
)
|
||||
|
||||
var (
|
||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
@@ -60,7 +65,7 @@ func (r *LightRenderer) initPlatform() error {
|
||||
|
||||
// channel for non-blocking reads. Buffer to make sure
|
||||
// we get the ESC sets:
|
||||
r.ttyinChannel = make(chan byte, 12)
|
||||
r.ttyinChannel = make(chan byte, 1024)
|
||||
|
||||
// the following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
@@ -68,9 +73,6 @@ func (r *LightRenderer) initPlatform() error {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
@@ -130,7 +132,7 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
select {
|
||||
case bc := <-r.ttyinChannel:
|
||||
return int(bc), true
|
||||
default:
|
||||
case <-time.After(timeoutInterval * time.Millisecond):
|
||||
return 0, false
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ package tui
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -29,3 +30,18 @@ func ttyname() string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
return in
|
||||
}
|
||||
}
|
||||
return os.Stdin
|
||||
}
|
||||
return in
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
package tui
|
||||
|
||||
import "os"
|
||||
|
||||
func ttyname() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
return os.Stdin
|
||||
}
|
||||
|
||||
154
test/test_go.rb
154
test/test_go.rb
@@ -1890,6 +1890,158 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
end
|
||||
|
||||
def test_select_deselect
|
||||
tmux.send_keys "seq 3 | #{FZF} --multi --bind up:deselect+up,down:select+down", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal 0, lines.select_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
end
|
||||
|
||||
def test_interrupt_execute
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 'C-l'
|
||||
tmux.until { |lines| assert lines.any_include?('executing 1') }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.send_keys 99
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_abort
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_accept
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{script}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', readonce.chomp
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_abort
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_accept
|
||||
script = tempname + '.sh'
|
||||
writelines(script,
|
||||
['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{script}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{script})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.item_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.item_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', readonce.chomp
|
||||
wait { refute system("pgrep -f #{script}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{script}")
|
||||
begin
|
||||
File.unlink(script)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_header
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '4/1000'
|
||||
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||
end
|
||||
tmux.send_keys '55'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-J'
|
||||
tmux.until do |lines|
|
||||
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 19, lines.match_count
|
||||
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-K'
|
||||
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
@@ -2060,7 +2212,7 @@ module CompletionTest
|
||||
end
|
||||
|
||||
# ~USERNAME**<TAB>
|
||||
user = ENV['USER']
|
||||
user = `whoami`.chomp
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~#{user}**", :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
|
||||
Reference in New Issue
Block a user