mirror of
https://github.com/junegunn/fzf.git
synced 2025-07-26 09:42:02 -07:00
Compare commits
85 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
64c61603e9 | ||
|
57c08d925f | ||
|
51623a5f6a | ||
|
ca3f6181d7 | ||
|
9c94f9c3d0 | ||
|
4a85843bcf | ||
|
d4d9b99879 | ||
|
6816b7d95b | ||
|
acdf265d7a | ||
|
19495eb9bb | ||
|
bacc8609ee | ||
|
99163f5afa | ||
|
0607227730 | ||
|
d938fdc496 | ||
|
dcb4c3d84a | ||
|
82ebcd9209 | ||
|
ff1687744d | ||
|
782c870fb2 | ||
|
71fad63829 | ||
|
d65c6101a8 | ||
|
3c40b1bd51 | ||
|
90a8800bb5 | ||
|
97f1dae2d1 | ||
|
e54ec05709 | ||
|
a24eb99679 | ||
|
ad113d00b7 | ||
|
7bd5884d12 | ||
|
c3505858a6 | ||
|
e76aa37fd4 | ||
|
1a32220ca9 | ||
|
4161403a1d | ||
|
53bcdc4294 | ||
|
30a8ef28cd | ||
|
855f90727a | ||
|
2191a44e36 | ||
|
952276dc2d | ||
|
2286edb329 | ||
|
a0f28583e7 | ||
|
8af0af3400 | ||
|
769e5cbb2d | ||
|
fc69308057 | ||
|
c6d620c99e | ||
|
f510a4def6 | ||
|
4ae3069c6f | ||
|
c0f27751d3 | ||
|
efbcd5a683 | ||
|
6a67712944 | ||
|
e8a690928d | ||
|
0eee95af57 | ||
|
a09c6e991a | ||
|
d06c5ab990 | ||
|
e0924d27b8 | ||
|
2775b771f2 | ||
|
cf7a3c6a0e | ||
|
230cc6acc3 | ||
|
626a23a585 | ||
|
74f196eebb | ||
|
cf2242aea3 | ||
|
8cb59e6fca | ||
|
5cce17e80a | ||
|
ee5302fb2d | ||
|
387c6ef664 | ||
|
581734c369 | ||
|
d90a969c00 | ||
|
2c8a96bb27 | ||
|
5da168db30 | ||
|
e215e2daf3 | ||
|
e28f5aa45b | ||
|
a2d0e8f233 | ||
|
303d04106a | ||
|
c423c496a1 | ||
|
4e85f72f0e | ||
|
dd0737aac0 | ||
|
f90985845d | ||
|
af4917dbb6 | ||
|
d9404fcce4 | ||
|
5c01fee5a9 | ||
|
9b27d68a37 | ||
|
b99d884e57 | ||
|
587df594b8 | ||
|
b896e0d314 | ||
|
559fb7ee45 | ||
|
62545cd983 | ||
|
c50812518e | ||
|
4cc5609d8b |
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
|
||||
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
|
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.23.1
|
||||
- uses: crate-ci/typos@v1.26.0
|
||||
|
@@ -1,4 +1,5 @@
|
||||
---
|
||||
version: 2
|
||||
project_name: fzf
|
||||
|
||||
before:
|
||||
@@ -6,60 +7,9 @@ before:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- id: fzf-macos
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: |
|
||||
sh -c '
|
||||
cat > /tmp/fzf-gon-amd64.hcl << EOF
|
||||
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
|
||||
bundle_id = "junegunn.fzf"
|
||||
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
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
hooks:
|
||||
post: |
|
||||
sh -c '
|
||||
cat > /tmp/fzf-gon-arm64.hcl << EOF
|
||||
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
|
||||
bundle_id = "junegunn.fzf"
|
||||
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
|
||||
'
|
||||
|
||||
- id: fzf
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
@@ -89,6 +39,42 @@ builds:
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
|
||||
# .goreleaser.yaml
|
||||
notarize:
|
||||
macos:
|
||||
- # Whether this configuration is enabled or not.
|
||||
#
|
||||
# Default: false.
|
||||
# Templates: allowed.
|
||||
enabled: "{{ not .IsSnapshot }}"
|
||||
|
||||
# Before notarizing, we need to sign the binary.
|
||||
# This blocks defines the configuration for doing so.
|
||||
sign:
|
||||
# The .p12 certificate file path or its base64'd contents.
|
||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||
|
||||
# The password to be used to open the certificate.
|
||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||
|
||||
# Then, we notarize the binaries.
|
||||
notarize:
|
||||
# The issuer ID.
|
||||
# Its the UUID you see when creating the App Store Connect key.
|
||||
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
|
||||
|
||||
# Key ID.
|
||||
# You can see it in the list of App Store Connect Keys.
|
||||
# It will also be in the ApiKey filename.
|
||||
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
|
||||
|
||||
# The .p8 key file path or its base64'd contents.
|
||||
key: "{{.Env.MACOS_NOTARY_KEY}}"
|
||||
|
||||
# Whether to wait for the notarization to finish.
|
||||
# Not recommended, as it could take a really long time.
|
||||
wait: true
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
builds:
|
||||
@@ -100,18 +86,12 @@ archives:
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
checksum:
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Version }}'
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
@@ -6,7 +6,7 @@ Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
IgnoredMethods:
|
||||
AllowedMethods:
|
||||
- assert
|
||||
- exit
|
||||
- paste
|
||||
@@ -15,7 +15,7 @@ Style/MethodCallWithArgsParentheses:
|
||||
- refute
|
||||
- require
|
||||
- send_keys
|
||||
IgnoredPatterns:
|
||||
AllowedPatterns:
|
||||
- ^assert_
|
||||
- ^refute_
|
||||
Style/NumericPredicate:
|
||||
|
88
CHANGELOG.md
88
CHANGELOG.md
@@ -1,6 +1,94 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.56.1
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- Fixed a race condition which would cause fzf to present stale results after `reload` (#4070)
|
||||
- `page-up` and `page-down` actions now work correctly with multi-line items (#4069)
|
||||
- `{n}` is allowed in `SCROLL` expression in `--preview-window` (#4079)
|
||||
- [zsh] Fixed regression in history loading with shared option (#4071)
|
||||
- [zsh] Better command extraction in zsh completion (#4082)
|
||||
- Thanks to @LangLangBart, @jaydee-coder, @alex-huff, and @vejkse for the contributions
|
||||
|
||||
0.56.0
|
||||
------
|
||||
- Added `--gap[=N]` option to display empty lines between items.
|
||||
- This can be useful to visually separate adjacent multi-line items.
|
||||
```sh
|
||||
# All bash functions, highlighted
|
||||
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --reverse --multi --highlight-line --gap
|
||||
```
|
||||
- Or just to make the list easier to read. For single-line items, you probably want to set `--color gutter:-1` as well to hide the gutter.
|
||||
```sh
|
||||
fzf --info inline-right --gap --color gutter:-1
|
||||
```
|
||||
- Added `noinfo` option to `--preview-window` to hide the scroll indicator in the preview window
|
||||
- Bug fixes
|
||||
- Thanks to @LangLangBart, @akinomyoga, and @charlievieth for fixing the bugs
|
||||
|
||||
0.55.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_
|
||||
|
||||
- Added `exact-boundary-match` type to the search syntax. When a search term is single-quoted, fzf will search for the exact occurrences of the string with both ends at word boundaries.
|
||||
```sh
|
||||
fzf --query "'here'" << EOF
|
||||
come here
|
||||
not there
|
||||
EOF
|
||||
```
|
||||
- [bash] Fuzzy path completion is enabled for all commands
|
||||
- 1. If the default completion is not already set
|
||||
- 2. And if the current bash supports `complete -D` option
|
||||
- However, fuzzy completion for some commands can be "dynamically" disabled by the dynamic completion loader
|
||||
- See the comment in `__fzf_default_completion` function for more information
|
||||
- Comments are now allowed in `$FZF_DEFAULT_OPTS` and `$FZF_DEFAULT_OPTS_FILE`
|
||||
```sh
|
||||
export FZF_DEFAULT_OPTS='
|
||||
# Layout options
|
||||
--layout=reverse
|
||||
--info=inline-right # Show info on the right side of the prompt line
|
||||
# ...
|
||||
'
|
||||
```
|
||||
- Hyperlinks (OSC 8) are now supported in the preview window and in the main window
|
||||
```sh
|
||||
printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>' | fzf --ansi
|
||||
|
||||
fzf --preview "printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>'"
|
||||
```
|
||||
- The default `--ellipsis` is now `··` instead of `..`.
|
||||
- [vim] A spec can have `exit` callback that is called with the exit status of fzf
|
||||
- This can be used to clean up temporary resources or restore the original state when fzf is closed without a selection
|
||||
- Fixed `--tmux bottom` when the status line is not at the bottom
|
||||
- Fixed extra scroll offset in multi-line mode (`--read0` or `--wrap`)
|
||||
- Added fallback `ps` command for `kill` completion on Cygwin
|
||||
|
||||
0.54.3
|
||||
------
|
||||
- Fixed incompatibility of adaptive height specification and 'start:reload'
|
||||
```sh
|
||||
# A regression in 0.54.0 would cause this to fail
|
||||
fzf --height '~100%' --bind 'start:reload:seq 10'
|
||||
```
|
||||
- Environment variables are now available to `$FZF_DEFAULT_COMMAND`
|
||||
```sh
|
||||
FZF_DEFAULT_COMMAND='echo $FZF_QUERY' fzf --query foo
|
||||
```
|
||||
|
||||
0.54.2
|
||||
------
|
||||
- Fixed incorrect syntax highlighting of truncated multi-line entries
|
||||
- Updated GoReleaser to 2.1.0 to simplify notarization of macOS binaries
|
||||
- macOS archives will be in `tar.gz` format instead of `zip` format since we no longer notarize the zip files but binaries
|
||||
- (Windows) Reverted a mintty fix in 0.54.0
|
||||
- As a result, mouse may not work on mintty in fullscreen mode. However, fzf will correctly read non-ASCII input in fullscreen mode (`--no-height`).
|
||||
- fzf unfortunately cannot read non-ASCII input when not in fullscreen mode on Windows. So if you need to input non-ASCII characters, add `--no-height` to your `$FZF_DEFAULT_OPTS`.
|
||||
- Any help in fixing this issue will be appreciated (#3799, #3847).
|
||||
|
||||
0.54.1
|
||||
------
|
||||
- Updated [fastwalk](https://github.com/charlievieth/fastwalk) dependency for built-in directory walker
|
||||
|
@@ -8,5 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
ENV LANG C.UTF-8
|
||||
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
|
||||
ENV LANG=C.UTF-8
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
7
Makefile
7
Makefile
@@ -77,7 +77,6 @@ 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 \
|
||||
@@ -87,6 +86,10 @@ test: $(SOURCES)
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
lint: $(SOURCES) test/test_go.rb
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
rubocop --require rubocop-minitest --require rubocop-performance
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
generate:
|
||||
@@ -184,4 +187,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all generate build release test bench install clean docker docker-test update
|
||||
.PHONY: all generate build release test bench lint install clean docker docker-test update
|
||||
|
@@ -289,8 +289,9 @@ The following table summarizes the available options.
|
||||
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
||||
| `source` | list | Vim list as input to fzf |
|
||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||
| `sink` | funcref | Reference to function to process each selected item |
|
||||
| `sink` | funcref | Function to be called with each selected item |
|
||||
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130) |
|
||||
| `options` | string/list | Options to fzf |
|
||||
| `dir` | string | Working directory |
|
||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||
|
24
bin/fzf-tmux
24
bin/fzf-tmux
@@ -19,6 +19,9 @@ term=""
|
||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||
|
||||
help() {
|
||||
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
@@ -94,10 +97,18 @@ while [[ $# -gt 0 ]]; do
|
||||
opt="$opt ${arg:0:2}$size"
|
||||
elif [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
if [[ $tmux_32 = 1 ]]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
@@ -187,12 +198,11 @@ trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||
if [[ $tmux_version = 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
elif [[ $tmux_32 = 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
elif [[ $tmux_version = 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
else
|
||||
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||
exit 2
|
||||
|
@@ -306,8 +306,9 @@ The following table summarizes the available options.
|
||||
`source` | string | External command to generate input to fzf (e.g. `find .` )
|
||||
`source` | list | Vim list as input to fzf
|
||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||
`sink` | funcref | Reference to function to process each selected item
|
||||
`sink` | funcref | Function to be called with each selected item
|
||||
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130)
|
||||
`options` | string/list | Options to fzf
|
||||
`dir` | string | Working directory
|
||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||
|
8
go.mod
8
go.mod
@@ -1,13 +1,13 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.8
|
||||
github.com/charlievieth/fastwalk v1.0.9
|
||||
github.com/gdamore/tcell/v2 v2.7.4
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.22.0
|
||||
golang.org/x/term v0.22.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/term v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
|
16
go.sum
16
go.sum
@@ -1,17 +1,17 @@
|
||||
github.com/charlievieth/fastwalk v1.0.8 h1:uaoH6cAKSk73aK7aKXqs0+bL+J3Txzd3NGH8tRXgHko=
|
||||
github.com/charlievieth/fastwalk v1.0.8/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/charlievieth/fastwalk v1.0.9 h1:Odb92AfoReO3oFBfDGT5J+nwgzQPF/gWAw6E6/lkor0=
|
||||
github.com/charlievieth/fastwalk v1.0.9/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97 h1:rqzLixVo1c/GQW6px9j1xQmlvQIn+lf/V6M1UQ7IFzw=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
@@ -36,14 +36,14 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
6
install
6
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.54.1
|
||||
version=0.56.1
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -168,8 +168,8 @@ archi=$(uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
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.54.1"
|
||||
$version="0.56.1"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.54"
|
||||
var version = "0.56"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
@@ -21,13 +21,13 @@ 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 "Jul 2024" "fzf 0.54.1" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Nov 2024" "fzf 0.56.1" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B fzf\-tmux [LAYOUT OPTIONS] [\-\-] [FZF OPTIONS]
|
||||
.B fzf\-tmux [\fILAYOUT OPTIONS\fR] [\-\-] [\fIFZF OPTIONS\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
|
@@ -21,13 +21,13 @@ 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 "Jul 2024" "fzf 0.54.1" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Nov 2024" "fzf 0.56.1" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
|
||||
.SH SYNOPSIS
|
||||
fzf [options]
|
||||
fzf [\fIoptions\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf is an interactive filter program for any kind of list.
|
||||
@@ -208,6 +208,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
|
||||
.B "\-\-no\-multi\-line"
|
||||
Disable multi-line display of items when using \fB\-\-read0\fR
|
||||
.TP
|
||||
.BI "\-\-gap" "[=N]"
|
||||
Render empty lines between each item
|
||||
.TP
|
||||
.B "\-\-keep\-right"
|
||||
Keep the right end of the line visible when it's too long. Effective only when
|
||||
the query string is empty.
|
||||
@@ -526,7 +529,7 @@ lines that follow.
|
||||
Print header before the prompt line
|
||||
.TP
|
||||
.BI "\-\-ellipsis=" "STR"
|
||||
Ellipsis to show when line is truncated (default: '..')
|
||||
Ellipsis to show when line is truncated (default: '··')
|
||||
.SS Display
|
||||
.TP
|
||||
.B "\-\-ansi"
|
||||
@@ -756,7 +759,7 @@ default value 0 (or \fBcenter\fR) will put the label at the center of the
|
||||
border line.
|
||||
|
||||
.TP
|
||||
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
||||
.BI "\-\-preview\-window=" "[POSITION][,SIZE[%]][,border\-BORDER_OPT][,[no]wrap][,[no]follow][,[no]cycle][,[no]info][,[no]hidden][,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES][,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]"
|
||||
|
||||
.RS
|
||||
.B POSITION: (default: right)
|
||||
@@ -790,6 +793,9 @@ e.g.
|
||||
|
||||
* Cyclic scrolling is enabled with \fBcycle\fR flag.
|
||||
|
||||
* To hide the scroll offset information on the top right corner, specify
|
||||
\fBnoinfo\fR.
|
||||
|
||||
* To change the style of the border of the preview window, specify one of
|
||||
the options for \fB\-\-border\fR with \fBborder\-\fR prefix.
|
||||
e.g. \fBborder\-rounded\fR (border with rounded edges, default),
|
||||
@@ -799,7 +805,7 @@ e.g. \fBborder\-rounded\fR (border with rounded edges, default),
|
||||
* \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.
|
||||
- \fBSCROLL\fR can be either a numeric integer or a single-field index expression that refers to a numeric integer or {n} to refer to the zero-based ordinal index of the current item.
|
||||
|
||||
- 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).
|
||||
|
||||
@@ -1146,6 +1152,22 @@ A term can be prefixed by \fB^\fR, or suffixed by \fB$\fR to become an
|
||||
anchored-match term. Then fzf will search for the lines that start with or end
|
||||
with the given string. An anchored-match term is also an exact-match term.
|
||||
|
||||
.SS Exact\-boundary\-match (quoted both ends)
|
||||
A single-quoted term is interpreted as an "exact\-boundary\-match". fzf will
|
||||
search for the exact occurrences of the string with both ends at the word
|
||||
boundaries. Unlike in regular expressions, this also sees an underscore as
|
||||
a word boundary. But the words around underscores are ranked lower and appear
|
||||
later in the result than the other words around the other types of word
|
||||
boundaries.
|
||||
|
||||
1. xxx foo xxx (highest score)
|
||||
.br
|
||||
2. xxx foo_xxx
|
||||
.br
|
||||
3. xxx_foo xxx
|
||||
.br
|
||||
4. xxx_foo_xxx (lowest score)
|
||||
|
||||
.SS Negation
|
||||
If a term is prefixed by \fB!\fR, fzf will exclude the lines that satisfy the
|
||||
term from the result. In this case, fzf performs exact match by default.
|
||||
|
@@ -198,6 +198,7 @@ function! s:compare_binary_versions(a, b)
|
||||
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||
endfunction
|
||||
|
||||
let s:min_version = '0.53.0'
|
||||
let s:checked = {}
|
||||
function! fzf#exec(...)
|
||||
if !exists('s:exec')
|
||||
@@ -225,7 +226,11 @@ function! fzf#exec(...)
|
||||
let s:exec = binaries[-1]
|
||||
endif
|
||||
|
||||
if a:0 && !has_key(s:checked, a:1)
|
||||
let min_version = s:min_version
|
||||
if a:0 && s:compare_versions(a:1, min_version) > 0
|
||||
let min_version = a:1
|
||||
endif
|
||||
if !has_key(s:checked, min_version)
|
||||
let fzf_version = s:get_version(s:exec)
|
||||
if empty(fzf_version)
|
||||
let message = printf('Failed to run "%s --version"', s:exec)
|
||||
@@ -233,17 +238,17 @@ function! fzf#exec(...)
|
||||
throw message
|
||||
end
|
||||
|
||||
if s:compare_versions(fzf_version, a:1) >= 0
|
||||
let s:checked[a:1] = 1
|
||||
if s:compare_versions(fzf_version, min_version) >= 0
|
||||
let s:checked[min_version] = 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'
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', min_version, fzf_version)) =~? '^y'
|
||||
let s:versions = {}
|
||||
unlet s:exec
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec(a:1, 1)
|
||||
return fzf#exec(min_version, 1)
|
||||
else
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', a:1)
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', min_version)
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -665,21 +670,17 @@ else
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
endif
|
||||
|
||||
function! s:exit_handler(code, command, ...)
|
||||
if a:code == 130
|
||||
return 0
|
||||
elseif has('nvim') && a:code == 129
|
||||
" When deleting the terminal buffer while fzf is still running,
|
||||
" Nvim sends SIGHUP.
|
||||
return 0
|
||||
elseif a:code > 1
|
||||
function! s:exit_handler(dict, code, command, ...)
|
||||
if has_key(a:dict, 'exit')
|
||||
call a:dict.exit(a:code)
|
||||
endif
|
||||
if a:code == 2
|
||||
call s:error('Error running ' . a:command)
|
||||
if !empty(a:000)
|
||||
sleep
|
||||
endif
|
||||
return 0
|
||||
endif
|
||||
return 1
|
||||
return a:code
|
||||
endfunction
|
||||
|
||||
function! s:execute(dict, command, use_height, temps) abort
|
||||
@@ -731,7 +732,7 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(exit_status, command) ? lines : []
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:execute_tmux(dict, command, temps) abort
|
||||
@@ -746,7 +747,7 @@ function! s:execute_tmux(dict, command, temps) abort
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(exit_status, command) ? lines : []
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:calc_size(max, val, dict)
|
||||
@@ -912,7 +913,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
|
||||
let lines = s:collect(self.temps)
|
||||
if !s:exit_handler(a:code, self.command, 1)
|
||||
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
|
||||
return
|
||||
endif
|
||||
|
||||
|
@@ -264,6 +264,7 @@ _fzf_handle_dynamic_completion() {
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||
__fzf_orig_completion_get_orig_func "$cmd" || ret=1
|
||||
|
||||
# Update orig_complete by _fzf_orig_completion entry
|
||||
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
||||
@@ -289,7 +290,7 @@ __fzf_generic_path_completion() {
|
||||
fi
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
@@ -376,7 +377,7 @@ _fzf_complete() {
|
||||
selected=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [[ -n "$selected" ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
@@ -410,7 +411,8 @@ _fzf_complete_kill() {
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
@@ -480,10 +482,36 @@ complete -o default -F _fzf_opts_completion fzf
|
||||
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
||||
complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
# Default path completion
|
||||
__fzf_default_completion() {
|
||||
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
|
||||
|
||||
# Dynamic completion loader has updated the completion for the command
|
||||
if [[ $? -eq 124 ]]; then
|
||||
# We trigger _fzf_setup_completion so that fuzzy completion for the command
|
||||
# still works. However, loader can update the completion for multiple
|
||||
# commands at once, and fuzzy completion will no longer work for those
|
||||
# other commands. e.g. pytest -> py.test, pytest-2, pytest-3, etc
|
||||
_fzf_setup_completion path "$1"
|
||||
return 124
|
||||
fi
|
||||
}
|
||||
|
||||
# Set fuzzy path completion as the default completion for all commands.
|
||||
# We can't set up default completion,
|
||||
# 1. if it's already set up by another script
|
||||
# 2. or if the current version of bash doesn't support -D option
|
||||
complete | command grep -q __fzf_default_completion ||
|
||||
complete | command grep -- '-D$' | command grep -qv _comp_complete_load ||
|
||||
complete -D -F __fzf_default_completion -o default -o bashdefault 2> /dev/null
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
||||
|
||||
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
||||
# undocumented and subject to change in the future.
|
||||
#
|
||||
# NOTE: Although we have default completion, we still need to set up completion
|
||||
# for each command in case they already have completion set up by another script.
|
||||
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
||||
awk bat cat code diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
|
@@ -120,25 +120,19 @@ __fzf_comprun() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
||||
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
|
||||
__fzf_extract_command() {
|
||||
local token tokens
|
||||
tokens=(${(z)1})
|
||||
for token in $tokens; do
|
||||
token=${(Q)token}
|
||||
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
|
||||
echo "$token"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "${tokens[1]}"
|
||||
setopt localoptions noksh_arrays
|
||||
# Control completion with the "compstate" parameter, insert and list noting
|
||||
compstate[insert]=
|
||||
compstate[list]=
|
||||
cmd_word="${words[1]}"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
|
||||
local base lbuf compgen fzf_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
compgen=$3
|
||||
fzf_opts=$4
|
||||
suffix=$5
|
||||
@@ -161,7 +155,7 @@ __fzf_generic_path_completion() {
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
else
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
@@ -170,7 +164,7 @@ __fzf_generic_path_completion() {
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
fi | while read -r item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n -E "${(q)item} "
|
||||
@@ -227,10 +221,9 @@ _fzf_complete() {
|
||||
rest=("$@")
|
||||
fi
|
||||
|
||||
local fifo lbuf cmd matches post
|
||||
local fifo lbuf matches post
|
||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||
lbuf=${rest[0]}
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
@@ -238,7 +231,7 @@ _fzf_complete() {
|
||||
matches=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
@@ -300,7 +293,8 @@ _fzf_complete_unalias() {
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
@@ -309,9 +303,16 @@ _fzf_complete_kill_post() {
|
||||
}
|
||||
|
||||
fzf-completion() {
|
||||
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
||||
trap 'unset cmd_word' EXIT
|
||||
local tokens prefix trigger tail matches lbuf d_cmds
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# Check if at least one completion system (old or new) is active
|
||||
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( $+functions[compdef] )); then
|
||||
if ! zmodload -e zsh/compctl; then
|
||||
zmodload -i zsh/compctl
|
||||
fi
|
||||
fi
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
tokens=(${(z)LBUFFER})
|
||||
@@ -320,8 +321,6 @@ fzf-completion() {
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=$(__fzf_extract_command "$LBUFFER")
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||
@@ -339,16 +338,20 @@ fzf-completion() {
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
|
||||
|
||||
# Make the 'cmd_word' global
|
||||
zle __fzf_extract_command || :
|
||||
[[ -z "$cmd_word" ]] && return
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
||||
if eval "type _fzf_complete_${cmd_word} > /dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
|
||||
zle reset-prompt
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
|
||||
_fzf_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_fzf_path_completion "$prefix" "$lbuf"
|
||||
@@ -365,6 +368,9 @@ fzf-completion() {
|
||||
unset binding
|
||||
}
|
||||
|
||||
# Completion widget to gain access to the 'words' array (man zshcompwid)
|
||||
zle -C __fzf_extract_command .complete-word __fzf_extract_command
|
||||
# Normal widget
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
fi
|
||||
|
@@ -108,13 +108,20 @@ fi
|
||||
fzf-history-widget() {
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
# Ensure the associative history array, which maps event numbers to the full
|
||||
# history lines, is loaded, and that Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:history 2>/dev/null && (( ${#commands[perl]} )); then
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( $+commands[perl] )); then
|
||||
# Import commands from other shells if SHARE_HISTORY is enabled, as the
|
||||
# 'history' array only updates after executing a non-empty command.
|
||||
selected="$(
|
||||
if [[ -o sharehistory ]]; then
|
||||
fc -RI
|
||||
fi
|
||||
printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
else
|
||||
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
|
@@ -798,6 +798,14 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
// The solution is much cheaper since there is only one possible alignment of
|
||||
// the pattern.
|
||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, false, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func ExactMatchBoundary(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, true, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryCheck bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -832,10 +840,22 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
}
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if pchar == char {
|
||||
ok := pchar == char
|
||||
if ok {
|
||||
if pidx_ == 0 {
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
if boundaryCheck {
|
||||
ok = bonus >= bonusBoundary
|
||||
if ok && pidx_ == 0 {
|
||||
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||
}
|
||||
if ok && pidx_ == len(pattern)-1 {
|
||||
ok = index_ == lenRunes-1 || charClassOf(text.Get(index_+1)) <= charDelimiter
|
||||
}
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
pidx++
|
||||
if pidx == lenPattern {
|
||||
if bonus > bestBonus {
|
||||
@@ -861,7 +881,23 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
sidx = lenRunes - (bestPos + 1)
|
||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||
}
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
var score int
|
||||
if boundaryCheck {
|
||||
// Underscore boundaries should be ranked lower than the other types of boundaries
|
||||
score = int(bonus)
|
||||
deduct := int(bonus-bonusBoundary) + 1
|
||||
if sidx > 0 && text.Get(sidx-1) == '_' {
|
||||
score -= deduct + 1
|
||||
deduct = 1
|
||||
}
|
||||
if eidx < lenRunes && text.Get(eidx) == '_' {
|
||||
score -= deduct
|
||||
}
|
||||
// Add base score so that this can compete with other match types e.g. 'foo' | bar
|
||||
score += scoreMatch*lenPattern + int(bonusBoundaryWhite)*(lenPattern+1)
|
||||
} else {
|
||||
score, _ = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
}
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
|
38
src/ansi.go
38
src/ansi.go
@@ -1,6 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -13,22 +14,28 @@ type ansiOffset struct {
|
||||
color ansiState
|
||||
}
|
||||
|
||||
type url struct {
|
||||
uri string
|
||||
params string
|
||||
}
|
||||
|
||||
type ansiState struct {
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
attr tui.Attr
|
||||
lbg tui.Color
|
||||
url *url
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||
}
|
||||
|
||||
func (s *ansiState) equals(t *ansiState) bool {
|
||||
if t == nil {
|
||||
return !s.colored()
|
||||
}
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||
}
|
||||
|
||||
func (s *ansiState) ToString() string {
|
||||
@@ -60,7 +67,11 @@ func (s *ansiState) ToString() string {
|
||||
}
|
||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||
|
||||
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||
if s.url != nil {
|
||||
ret = fmt.Sprintf("\x1b]8;%s;%s\x1b\\%s\x1b]8;;\x1b", s.url.params, s.url.uri, ret)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func toAnsiString(color tui.Color, offset int) string {
|
||||
@@ -98,10 +109,19 @@ func matchOperatingSystemCommand(s string) int {
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------
|
||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------------
|
||||
if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -328,13 +348,21 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
||||
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, nil}
|
||||
} else {
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
state.lbg = prevState.bg
|
||||
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
|
||||
state.url = nil
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
|
||||
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
params := ansiCode[4 : 4+paramsEnd]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
|
||||
state.url = &url{uri: uri, params: params}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
@@ -58,7 +58,6 @@ const (
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
EvtReadFin
|
||||
EvtReadNone
|
||||
EvtSearchNew
|
||||
EvtSearchProgress
|
||||
EvtSearchFin
|
||||
|
48
src/core.go
48
src/core.go
@@ -146,8 +146,25 @@ func Run(opts *Options) (int, error) {
|
||||
// Process executor
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
|
||||
// Terminal I/O
|
||||
var terminal *Terminal
|
||||
var err error
|
||||
var initialEnv []string
|
||||
initialReload := opts.extractReloadOnStart()
|
||||
if opts.Filter == nil {
|
||||
terminal, err = NewTerminal(opts, eventBox, executor)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
if len(initialReload) > 0 {
|
||||
var temps []string
|
||||
initialReload, temps = terminal.replacePlaceholderInInitialCommand(initialReload)
|
||||
initialEnv = terminal.environ()
|
||||
defer removeFiles(temps)
|
||||
}
|
||||
}
|
||||
|
||||
// Reader
|
||||
reloadOnStart := opts.reloadOnStart()
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
var reader *Reader
|
||||
if !streamingFilter {
|
||||
@@ -155,12 +172,9 @@ func Run(opts *Options) (int, error) {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
|
||||
if reloadOnStart {
|
||||
// reload or reload-sync action is bound to 'start' event, no need to start the reader
|
||||
eventBox.Set(EvtReadNone, nil)
|
||||
} else {
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
}
|
||||
readyChan := make(chan bool)
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
// Matcher
|
||||
@@ -212,7 +226,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
return false
|
||||
}, eventBox, executor, opts.ReadZero, false)
|
||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
@@ -234,8 +248,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
sync := opts.Sync && !reloadOnStart
|
||||
if sync {
|
||||
if opts.Sync {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
}
|
||||
@@ -244,18 +257,14 @@ func Run(opts *Options) (int, error) {
|
||||
go matcher.Loop()
|
||||
defer matcher.Stop()
|
||||
|
||||
// Terminal I/O
|
||||
terminal, err := NewTerminal(opts, eventBox, executor)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
// Handling adaptive height
|
||||
maxFit := 0 // Maximum number of items that can fit on screen
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0 || sync
|
||||
deferred := opts.Select1 || opts.Exit0 || opts.Sync
|
||||
go terminal.Loop()
|
||||
if !deferred && !heightUnknown {
|
||||
// Start right away
|
||||
@@ -292,7 +301,9 @@ func Run(opts *Options) (int, error) {
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command, environ)
|
||||
readyChan := make(chan bool)
|
||||
go reader.restart(command, environ, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
exitCode := ExitOk
|
||||
@@ -322,9 +333,6 @@ func Run(opts *Options) (int, error) {
|
||||
err = quitSignal.err
|
||||
stop = true
|
||||
return
|
||||
case EvtReadNone:
|
||||
reading = false
|
||||
terminal.UpdateCount(0, false, nil)
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand, nextEnviron)
|
||||
|
@@ -102,7 +102,7 @@ func (m *Matcher) Loop() {
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||
merger = cached
|
||||
}
|
||||
} else {
|
||||
|
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/algo"
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
|
||||
"github.com/mattn/go-shellwords"
|
||||
"github.com/junegunn/go-shellwords"
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
@@ -56,6 +56,7 @@ Usage: fzf [options]
|
||||
--wrap Enable line wrap
|
||||
--wrap-sign=STR Indicator for wrapped lines
|
||||
--no-multi-line Disable multi-line display of items when using --read0
|
||||
--gap[=N] Render empty lines between each item
|
||||
--keep-right Keep the right end of the line visible on overflow
|
||||
--scroll-off=LINES Number of screen lines to keep above or below when
|
||||
scrolling to the top or to the bottom (default: 0)
|
||||
@@ -103,7 +104,7 @@ Usage: fzf [options]
|
||||
--header=STR String to print as header
|
||||
--header-lines=N The first N lines of the input are treated as header
|
||||
--header-first Print header before the prompt line
|
||||
--ellipsis=STR Ellipsis to show when line is truncated (default: '..')
|
||||
--ellipsis=STR Ellipsis to show when line is truncated (default: '··')
|
||||
|
||||
Display
|
||||
--ansi Enable processing of ANSI color codes
|
||||
@@ -120,8 +121,8 @@ Usage: fzf [options]
|
||||
--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]
|
||||
[,border-BORDER_OPT]
|
||||
[,[no]wrap][,[no]cycle][,[no]follow][,[no]info]
|
||||
[,[no]hidden][,border-BORDER_OPT]
|
||||
[,+SCROLL[OFFSETS][/DENOM]][,~HEADER_LINES]
|
||||
[,default][,<SIZE_THRESHOLD(ALTERNATIVE_LAYOUT)]
|
||||
--preview-label=LABEL
|
||||
@@ -271,6 +272,7 @@ type previewOpts struct {
|
||||
wrap bool
|
||||
cycle bool
|
||||
follow bool
|
||||
info bool
|
||||
border tui.BorderShape
|
||||
headerLines int
|
||||
threshold int
|
||||
@@ -386,7 +388,7 @@ func (a previewOpts) sameLayout(b previewOpts) bool {
|
||||
}
|
||||
|
||||
func (a previewOpts) sameContentLayout(b previewOpts) bool {
|
||||
return a.wrap == b.wrap && a.headerLines == b.headerLines
|
||||
return a.wrap == b.wrap && a.headerLines == b.headerLines && a.info == b.info
|
||||
}
|
||||
|
||||
func firstLine(s string) string {
|
||||
@@ -472,7 +474,8 @@ type Options struct {
|
||||
Header []string
|
||||
HeaderLines int
|
||||
HeaderFirst bool
|
||||
Ellipsis string
|
||||
Gap int
|
||||
Ellipsis *string
|
||||
Scrollbar *string
|
||||
Margin [4]sizeSpec
|
||||
Padding [4]sizeSpec
|
||||
@@ -508,7 +511,7 @@ func filterNonEmpty(input []string) []string {
|
||||
}
|
||||
|
||||
func defaultPreviewOpts(command string) previewOpts {
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, tui.DefaultBorderShape, 0, 0, nil}
|
||||
return previewOpts{command, posRight, sizeSpec{50, true}, "", false, false, false, false, true, tui.DefaultBorderShape, 0, 0, nil}
|
||||
}
|
||||
|
||||
func defaultOptions() *Options {
|
||||
@@ -578,7 +581,8 @@ func defaultOptions() *Options {
|
||||
Header: make([]string, 0),
|
||||
HeaderLines: 0,
|
||||
HeaderFirst: false,
|
||||
Ellipsis: "..",
|
||||
Gap: 0,
|
||||
Ellipsis: nil,
|
||||
Scrollbar: nil,
|
||||
Margin: defaultMargin(),
|
||||
Padding: defaultMargin(),
|
||||
@@ -1722,7 +1726,7 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
||||
var err error
|
||||
tokenRegex := regexp.MustCompile(`[:,]*(<([1-9][0-9]*)\(([^)<]+)\)|[^,:]+)`)
|
||||
sizeRegex := regexp.MustCompile("^[0-9]+%?$")
|
||||
offsetRegex := regexp.MustCompile(`^(\+{-?[0-9]+})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
offsetRegex := regexp.MustCompile(`^(\+{(-?[0-9]+|n)})?([+-][0-9]+)*(-?/[1-9][0-9]*)?$`)
|
||||
headerRegex := regexp.MustCompile("^~(0|[1-9][0-9]*)$")
|
||||
tokens := tokenRegex.FindAllStringSubmatch(input, -1)
|
||||
var alternative string
|
||||
@@ -1789,6 +1793,10 @@ func parsePreviewWindowImpl(opts *previewOpts, input string) error {
|
||||
opts.follow = true
|
||||
case "nofollow":
|
||||
opts.follow = false
|
||||
case "info":
|
||||
opts.info = true
|
||||
case "noinfo":
|
||||
opts.info = false
|
||||
default:
|
||||
if headerRegex.MatchString(token) {
|
||||
if opts.headerLines, err = atoi(token[1:]); err != nil {
|
||||
@@ -2338,10 +2346,19 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
opts.HeaderFirst = true
|
||||
case "--no-header-first":
|
||||
opts.HeaderFirst = false
|
||||
case "--ellipsis":
|
||||
if opts.Ellipsis, err = nextString(allArgs, &i, "ellipsis string required"); err != nil {
|
||||
case "--gap":
|
||||
if opts.Gap, err = optionalNumeric(allArgs, &i, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
case "--no-gap":
|
||||
opts.Gap = 0
|
||||
case "--ellipsis":
|
||||
str, err := nextString(allArgs, &i, "ellipsis string required")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
str = firstLine(str)
|
||||
opts.Ellipsis = &str
|
||||
case "--preview":
|
||||
if opts.Preview.command, err = nextString(allArgs, &i, "preview command required"); err != nil {
|
||||
return err
|
||||
@@ -2622,8 +2639,13 @@ func parseOptions(index *int, opts *Options, allArgs []string) error {
|
||||
if opts.HeaderLines, err = atoi(value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--gap="); match {
|
||||
if opts.Gap, err = atoi(value); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if match, value := optString(arg, "--ellipsis="); match {
|
||||
opts.Ellipsis = value
|
||||
str := firstLine(value)
|
||||
opts.Ellipsis = &str
|
||||
} else if match, value := optString(arg, "--preview="); match {
|
||||
opts.Preview.command = value
|
||||
} else if match, value := optString(arg, "--preview-window="); match {
|
||||
@@ -2915,6 +2937,12 @@ func postProcessOptions(opts *Options) error {
|
||||
return processScheme(opts)
|
||||
}
|
||||
|
||||
func parseShellWords(str string) ([]string, error) {
|
||||
parser := shellwords.NewParser()
|
||||
parser.ParseComment = true
|
||||
return parser.Parse(str)
|
||||
}
|
||||
|
||||
// ParseOptions parses command-line options
|
||||
func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
opts := defaultOptions()
|
||||
@@ -2928,7 +2956,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return nil, errors.New("$FZF_DEFAULT_OPTS_FILE: " + err.Error())
|
||||
}
|
||||
|
||||
words, parseErr := shellwords.Parse(string(bytes))
|
||||
words, parseErr := parseShellWords(string(bytes))
|
||||
if parseErr != nil {
|
||||
return nil, errors.New(path + ": " + parseErr.Error())
|
||||
}
|
||||
@@ -2940,7 +2968,7 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
}
|
||||
|
||||
// 2. Options from $FZF_DEFAULT_OPTS string
|
||||
words, parseErr := shellwords.Parse(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
words, parseErr := parseShellWords(os.Getenv("FZF_DEFAULT_OPTS"))
|
||||
if parseErr != nil {
|
||||
return nil, errors.New("$FZF_DEFAULT_OPTS: " + parseErr.Error())
|
||||
}
|
||||
@@ -2964,17 +2992,18 @@ func ParseOptions(useDefaults bool, args []string) (*Options, error) {
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func (opts *Options) reloadOnStart() bool {
|
||||
// Not compatible with --filter
|
||||
if opts.Filter != nil {
|
||||
return false
|
||||
}
|
||||
func (opts *Options) extractReloadOnStart() string {
|
||||
cmd := ""
|
||||
if actions, prs := opts.Keymap[tui.Start.AsEvent()]; prs {
|
||||
filtered := []*action{}
|
||||
for _, action := range actions {
|
||||
if action.t == actReload || action.t == actReloadSync {
|
||||
return true
|
||||
cmd = action.a
|
||||
} else {
|
||||
filtered = append(filtered, action)
|
||||
}
|
||||
}
|
||||
opts.Keymap[tui.Start.AsEvent()] = filtered
|
||||
}
|
||||
return false
|
||||
return cmd
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ type termType int
|
||||
const (
|
||||
termFuzzy termType = iota
|
||||
termExact
|
||||
termExactBoundary
|
||||
termPrefix
|
||||
termSuffix
|
||||
termEqual
|
||||
@@ -147,6 +148,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||
ptr.procFun[termEqual] = algo.EqualMatch
|
||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||
ptr.procFun[termExactBoundary] = algo.ExactMatchBoundary
|
||||
ptr.procFun[termPrefix] = algo.PrefixMatch
|
||||
ptr.procFun[termSuffix] = algo.SuffixMatch
|
||||
|
||||
@@ -193,7 +195,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(text, "'") {
|
||||
if len(text) > 2 && strings.HasPrefix(text, "'") && strings.HasSuffix(text, "'") {
|
||||
typ = termExactBoundary
|
||||
text = text[1 : len(text)-1]
|
||||
} else if strings.HasPrefix(text, "'") {
|
||||
// Flip exactness
|
||||
if fuzzy && !inv {
|
||||
typ = termExact
|
||||
|
22
src/proxy.go
22
src/proxy.go
@@ -9,6 +9,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -32,7 +33,7 @@ func fifo(name string) (string, error) {
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts *Options, withExports bool) (int, error) {
|
||||
func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool) (*exec.Cmd, error), opts *Options, withExports bool) (int, error) {
|
||||
output, err := fifo("proxy-output")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
@@ -92,17 +93,28 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string) *exec.Cmd, opts
|
||||
// To ensure that the options are processed by a POSIX-compliant shell,
|
||||
// we need to write the command to a temporary file and execute it with sh.
|
||||
var exports []string
|
||||
needBash := false
|
||||
if withExports {
|
||||
exports = os.Environ()
|
||||
for idx, pairStr := range exports {
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
exports[idx] = fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1]))
|
||||
if validIdentifier.MatchString(pair[0]) {
|
||||
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
|
||||
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
|
||||
name := pair[0][10 : len(pair[0])-2]
|
||||
exports = append(exports, name+pair[1])
|
||||
exports = append(exports, "export -f "+name)
|
||||
needBash = true
|
||||
}
|
||||
}
|
||||
}
|
||||
temp := WriteTemporaryFile(append(exports, command), "\n")
|
||||
defer os.Remove(temp)
|
||||
|
||||
cmd := cmdBuilder(temp)
|
||||
cmd, err := cmdBuilder(temp, needBash)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
intChan := make(chan os.Signal, 1)
|
||||
defer close(intChan)
|
||||
|
@@ -9,7 +9,10 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func sh() (string, error) {
|
||||
func sh(bash bool) (string, error) {
|
||||
if bash {
|
||||
return "bash", nil
|
||||
}
|
||||
return "sh", nil
|
||||
}
|
||||
|
||||
|
@@ -13,12 +13,16 @@ import (
|
||||
|
||||
var shPath atomic.Value
|
||||
|
||||
func sh() (string, error) {
|
||||
func sh(bash bool) (string, error) {
|
||||
if cached := shPath.Load(); cached != nil {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/sh")
|
||||
name := "sh"
|
||||
if bash {
|
||||
name = "bash"
|
||||
}
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/"+name)
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -31,7 +35,7 @@ func sh() (string, error) {
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
m := strconv.FormatUint(uint64(mode), 8)
|
||||
sh, err := sh()
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
@@ -43,7 +47,7 @@ func mkfifo(path string, mode uint32) (string, error) {
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
sh, err := sh()
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,7 +66,7 @@ func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
sh, err := sh()
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@@ -6,7 +6,6 @@ import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -25,16 +24,26 @@ type Reader struct {
|
||||
event int32
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
execOut io.ReadCloser
|
||||
command *string
|
||||
killed bool
|
||||
termFunc func()
|
||||
command *string
|
||||
wait bool
|
||||
}
|
||||
|
||||
// NewReader returns new Reader object
|
||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
||||
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, nil, false, wait}
|
||||
return &Reader{
|
||||
pusher,
|
||||
executor,
|
||||
eventBox,
|
||||
delimNil,
|
||||
int32(EvtReady),
|
||||
make(chan bool, 1),
|
||||
sync.Mutex{},
|
||||
false,
|
||||
func() { os.Stdin.Close() },
|
||||
nil,
|
||||
wait}
|
||||
}
|
||||
|
||||
func (r *Reader) startEventPoller() {
|
||||
@@ -80,19 +89,19 @@ func (r *Reader) fin(success bool) {
|
||||
func (r *Reader) terminate() {
|
||||
r.mutex.Lock()
|
||||
r.killed = true
|
||||
if r.exec != nil && r.exec.Process != nil {
|
||||
r.execOut.Close()
|
||||
util.KillCommand(r.exec)
|
||||
} else {
|
||||
os.Stdin.Close()
|
||||
if r.termFunc != nil {
|
||||
r.termFunc()
|
||||
r.termFunc = nil
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *Reader) restart(command commandSpec, environ []string) {
|
||||
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
|
||||
r.event = int32(EvtReady)
|
||||
r.startEventPoller()
|
||||
success := r.readFromCommand(command.command, environ)
|
||||
success := r.readFromCommand(command.command, environ, func() {
|
||||
readyChan <- true
|
||||
})
|
||||
r.fin(success)
|
||||
removeFiles(command.tempFiles)
|
||||
}
|
||||
@@ -111,20 +120,29 @@ func (r *Reader) readChannel(inputChan chan string) bool {
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
|
||||
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
signalReady := func() {
|
||||
if readyChan != nil {
|
||||
readyChan <- true
|
||||
}
|
||||
}
|
||||
if inputChan != nil {
|
||||
signalReady()
|
||||
success = r.readChannel(inputChan)
|
||||
} else if len(initCmd) > 0 {
|
||||
success = r.readFromCommand(initCmd, initEnv, signalReady)
|
||||
} else if util.IsTty(os.Stdin) {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
signalReady()
|
||||
success = r.readFiles(root, opts, ignores)
|
||||
} else {
|
||||
// We can't export FZF_* environment variables to the default command
|
||||
success = r.readFromCommand(cmd, nil)
|
||||
success = r.readFromCommand(cmd, initEnv, signalReady)
|
||||
}
|
||||
} else {
|
||||
signalReady()
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.fin(success)
|
||||
@@ -248,7 +266,6 @@ func trimPath(path string) string {
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
||||
r.killed = false
|
||||
conf := fastwalk.Config{
|
||||
Follow: opts.follow,
|
||||
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||
@@ -264,7 +281,7 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
isDir := de.IsDir()
|
||||
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' {
|
||||
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
@@ -287,31 +304,32 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
return fastwalk.Walk(&conf, root, fn) == nil
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(command string, environ []string) bool {
|
||||
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
|
||||
r.mutex.Lock()
|
||||
|
||||
r.killed = false
|
||||
r.termFunc = nil
|
||||
r.command = &command
|
||||
r.exec = r.executor.ExecCommand(command, true)
|
||||
exec := r.executor.ExecCommand(command, true)
|
||||
if environ != nil {
|
||||
r.exec.Env = environ
|
||||
exec.Env = environ
|
||||
}
|
||||
|
||||
var err error
|
||||
r.execOut, err = r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
r.exec = nil
|
||||
execOut, err := exec.StdoutPipe()
|
||||
if err != nil || exec.Start() != nil {
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
err = r.exec.Start()
|
||||
if err != nil {
|
||||
r.exec = nil
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
// Function to call to terminate the running command
|
||||
r.termFunc = func() {
|
||||
execOut.Close()
|
||||
util.KillCommand(exec)
|
||||
}
|
||||
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
r.feed(r.execOut)
|
||||
return r.exec.Wait() == nil
|
||||
|
||||
r.feed(execOut)
|
||||
return exec.Wait() == nil
|
||||
}
|
||||
|
@@ -23,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
counter := 0
|
||||
ready := func() {
|
||||
counter++
|
||||
}
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
@@ -48,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
|
||||
reader.startEventPoller()
|
||||
|
||||
// Failing command
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil))
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
|
||||
strs = []string{}
|
||||
if len(strs) > 0 {
|
||||
if len(strs) > 0 || counter != 2 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
|
@@ -16,6 +16,7 @@ type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
match bool
|
||||
url *url
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -177,8 +178,11 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
if curr != 0 && idx > start {
|
||||
if curr < 0 {
|
||||
color := colMatch
|
||||
var url *url
|
||||
if curr < -1 && theme.Colored {
|
||||
origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
|
||||
ansi := itemColors[-curr-2]
|
||||
url = ansi.color.url
|
||||
origColor := ansiToColorPair(ansi, colMatch)
|
||||
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||
// combination of either [hl and bg] or [hl+ and bg+].
|
||||
//
|
||||
@@ -194,13 +198,14 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true})
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: ansiToColorPair(ansi, colBase),
|
||||
match: false})
|
||||
match: false,
|
||||
url: ansi.color.url})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -124,10 +124,10 @@ func TestColorOffset(t *testing.T) {
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
|
255
src/terminal.go
255
src/terminal.go
@@ -245,6 +245,7 @@ type Terminal struct {
|
||||
hscroll bool
|
||||
hscrollOff int
|
||||
scrollOff int
|
||||
gap int
|
||||
wordRubout string
|
||||
wordNext string
|
||||
cx int
|
||||
@@ -382,6 +383,7 @@ const (
|
||||
reqRedrawPreviewLabel
|
||||
reqClose
|
||||
reqPrintQuery
|
||||
reqPreviewReady
|
||||
reqPreviewEnqueue
|
||||
reqPreviewDisplay
|
||||
reqPreviewRefresh
|
||||
@@ -728,7 +730,7 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
}
|
||||
}
|
||||
if fullscreen {
|
||||
if !tui.IsLightRendererSupported() {
|
||||
if tui.HasFullscreenRenderer() {
|
||||
renderer = tui.NewFullscreenRenderer(opts.Theme, opts.Black, opts.Mouse)
|
||||
} else {
|
||||
renderer, err = tui.NewLightRenderer(ttyin, opts.Theme, opts.Black, opts.Mouse, opts.Tabstop, opts.ClearOnExit,
|
||||
@@ -824,9 +826,9 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
headerVisible: true,
|
||||
headerFirst: opts.HeaderFirst,
|
||||
headerLines: opts.HeaderLines,
|
||||
gap: opts.Gap,
|
||||
header: []string{},
|
||||
header0: opts.Header,
|
||||
ellipsis: opts.Ellipsis,
|
||||
ansi: opts.Ansi,
|
||||
tabstop: opts.Tabstop,
|
||||
hasStartActions: false,
|
||||
@@ -854,7 +856,6 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
mutex: sync.Mutex{},
|
||||
uiMutex: sync.Mutex{},
|
||||
suppress: true,
|
||||
sigstop: false,
|
||||
slab: util.MakeSlab(slab16Size, slab32Size),
|
||||
theme: opts.Theme,
|
||||
startChan: make(chan fitpad, 1),
|
||||
@@ -883,6 +884,15 @@ func NewTerminal(opts *Options, eventBox *util.EventBox, executor *util.Executor
|
||||
}
|
||||
t.separator, t.separatorLen = t.ansiLabelPrinter(bar, &tui.ColSeparator, true)
|
||||
}
|
||||
|
||||
if opts.Ellipsis != nil {
|
||||
t.ellipsis = *opts.Ellipsis
|
||||
} else if t.unicode {
|
||||
t.ellipsis = "··"
|
||||
} else {
|
||||
t.ellipsis = ".."
|
||||
}
|
||||
|
||||
if t.unicode {
|
||||
t.wrapSign = "↳ "
|
||||
t.borderWidth = uniseg.StringWidth("│")
|
||||
@@ -1067,7 +1077,7 @@ func (t *Terminal) parsePrompt(prompt string) (func(), int) {
|
||||
// // unless the part has a non-default ANSI state
|
||||
loc := whiteSuffix.FindStringIndex(trimmed)
|
||||
if loc != nil {
|
||||
blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1}}
|
||||
blankState := ansiOffset{[2]int32{int32(loc[0]), int32(loc[1])}, ansiState{-1, -1, tui.AttrClear, -1, nil}}
|
||||
if item.colors != nil {
|
||||
lastColor := (*item.colors)[len(*item.colors)-1]
|
||||
if lastColor.offset[1] < int32(loc[1]) {
|
||||
@@ -1128,15 +1138,23 @@ func (t *Terminal) wrapCols() int {
|
||||
return util.Max(t.window.Width()-(t.pointerLen+t.markerLen+1), 1)
|
||||
}
|
||||
|
||||
// Number of lines the item takes including the gap
|
||||
func (t *Terminal) numItemLines(item *Item, atMost int) (int, bool) {
|
||||
var numLines int
|
||||
if !t.wrap && !t.multiLine {
|
||||
return 1, false
|
||||
numLines = 1 + t.gap
|
||||
return numLines, numLines > atMost
|
||||
}
|
||||
var overflow bool
|
||||
if !t.wrap && t.multiLine {
|
||||
return item.text.NumLines(atMost)
|
||||
numLines, overflow = item.text.NumLines(atMost)
|
||||
} else {
|
||||
var lines [][]rune
|
||||
lines, overflow = item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
|
||||
numLines = len(lines)
|
||||
}
|
||||
lines, overflow := item.text.Lines(t.multiLine, atMost, t.wrapCols(), t.wrapSignWidth, t.tabstop)
|
||||
return len(lines), overflow
|
||||
numLines += t.gap
|
||||
return numLines, overflow || numLines > atMost
|
||||
}
|
||||
|
||||
func (t *Terminal) itemLines(item *Item, atMost int) ([][]rune, bool) {
|
||||
@@ -2042,6 +2060,21 @@ func (t *Terminal) printHeader() {
|
||||
t.wrap = wrap
|
||||
}
|
||||
|
||||
func (t *Terminal) canSpanMultiLines() bool {
|
||||
return t.multiLine || t.wrap || t.gap > 0
|
||||
}
|
||||
|
||||
func (t *Terminal) renderEmptyLine(line int, barRange [2]int) {
|
||||
t.move(line, 0, true)
|
||||
t.markEmptyLine(line)
|
||||
// If the screen is not filled with the list in non-multi-line mode,
|
||||
// scrollbar is not visible at all. But in multi-line mode, we may need
|
||||
// to redraw the scrollbar character at the end.
|
||||
if t.canSpanMultiLines() {
|
||||
t.prevLines[line].hasBar = t.printBar(line, true, barRange)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) printList() {
|
||||
t.constrain()
|
||||
barLength, barStart := t.getScrollbar()
|
||||
@@ -2062,14 +2095,7 @@ func (t *Terminal) printList() {
|
||||
item := t.merger.Get(itemCount + t.offset)
|
||||
line = t.printItem(item, line, maxy, itemCount, itemCount == t.cy-t.offset, barRange)
|
||||
} else if !t.prevLines[line].empty {
|
||||
t.move(line, 0, true)
|
||||
t.markEmptyLine(line)
|
||||
// If the screen is not filled with the list in non-multi-line mode,
|
||||
// scrollbar is not visible at all. But in multi-line mode, we may need
|
||||
// to redraw the scrollbar character at the end.
|
||||
if t.multiLine || t.wrap {
|
||||
t.prevLines[line].hasBar = t.printBar(line, true, barRange)
|
||||
}
|
||||
t.renderEmptyLine(line, barRange)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2117,9 +2143,6 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
||||
prevLine.queryLen == newLine.queryLen &&
|
||||
prevLine.result == newLine.result {
|
||||
t.prevLines[line].hasBar = printBar(line, false)
|
||||
if !t.multiLine && !t.wrap {
|
||||
return line
|
||||
}
|
||||
return line + numLines - 1
|
||||
}
|
||||
|
||||
@@ -2206,6 +2229,10 @@ func (t *Terminal) printItem(result Result, line int, maxLine int, index int, cu
|
||||
}
|
||||
finalLineNum = t.printHighlighted(result, base, match, false, true, line, maxLine, forceRedraw, preTask, postTask)
|
||||
}
|
||||
for i := 0; i < t.gap && finalLineNum < maxLine; i++ {
|
||||
finalLineNum++
|
||||
t.renderEmptyLine(finalLineNum, barRange)
|
||||
}
|
||||
return finalLineNum
|
||||
}
|
||||
|
||||
@@ -2267,7 +2294,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
allOffsets := result.colorOffsets(charOffsets, t.theme, colBase, colMatch, current)
|
||||
|
||||
maxLines := 1
|
||||
if t.multiLine || t.wrap {
|
||||
if t.canSpanMultiLines() {
|
||||
maxLines = maxLineNum - lineNum + 1
|
||||
}
|
||||
lines, overflow := t.itemLines(item, maxLines)
|
||||
@@ -2275,8 +2302,9 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
|
||||
finalLineNum := lineNum
|
||||
topCutoff := false
|
||||
skipLines := 0
|
||||
wrapped := false
|
||||
if t.multiLine || t.wrap {
|
||||
if t.canSpanMultiLines() {
|
||||
// Cut off the upper lines in the 'default' layout
|
||||
if t.layout == layoutDefault && !current && maxLines == numItemLines && overflow {
|
||||
lines, _ = t.itemLines(item, math.MaxInt)
|
||||
@@ -2287,7 +2315,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
wrapped = true
|
||||
}
|
||||
|
||||
lines = lines[len(lines)-maxLines:]
|
||||
skipLines = len(lines) - maxLines
|
||||
topCutoff = true
|
||||
}
|
||||
}
|
||||
@@ -2323,6 +2351,10 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
}
|
||||
}
|
||||
from += len(line)
|
||||
if lineOffset < skipLines {
|
||||
continue
|
||||
}
|
||||
actualLineOffset := lineOffset - skipLines
|
||||
|
||||
var maxe int
|
||||
for _, offset := range offsets {
|
||||
@@ -2333,7 +2365,7 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
|
||||
actualLineNum := lineNum
|
||||
if t.layout == layoutDefault {
|
||||
actualLineNum = (lineNum - lineOffset) + (numItemLines - lineOffset) - 1
|
||||
actualLineNum = (lineNum - actualLineOffset) + (numItemLines - actualLineOffset) - 1
|
||||
}
|
||||
t.move(actualLineNum, 0, forceRedraw)
|
||||
|
||||
@@ -2348,13 +2380,13 @@ func (t *Terminal) printHighlighted(result Result, colBase tui.ColorPair, colMat
|
||||
marker = markerTop
|
||||
}
|
||||
} else {
|
||||
if lineOffset == 0 { // First line
|
||||
if actualLineOffset == 0 { // First line
|
||||
if topCutoff {
|
||||
marker = markerMiddle
|
||||
} else {
|
||||
marker = markerTop
|
||||
}
|
||||
} else if lineOffset == numItemLines-1 { // Last line
|
||||
} else if actualLineOffset == numItemLines-1 { // Last line
|
||||
if topCutoff || !overflow {
|
||||
marker = markerBottom
|
||||
} else {
|
||||
@@ -2454,15 +2486,24 @@ func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []
|
||||
var substr string
|
||||
var prefixWidth int
|
||||
maxOffset := int32(len(text))
|
||||
var url *url
|
||||
for _, offset := range offsets {
|
||||
b := util.Constrain32(offset.offset[0], index, maxOffset)
|
||||
e := util.Constrain32(offset.offset[1], index, maxOffset)
|
||||
if url != nil && offset.url == nil {
|
||||
url = nil
|
||||
window.LinkEnd()
|
||||
}
|
||||
|
||||
substr, prefixWidth = t.processTabs(text[index:b], prefixWidth)
|
||||
window.CPrint(colBase, substr)
|
||||
|
||||
if b < e {
|
||||
substr, prefixWidth = t.processTabs(text[b:e], prefixWidth)
|
||||
if url == nil && offset.url != nil {
|
||||
url = offset.url
|
||||
window.LinkBegin(url.uri, url.params)
|
||||
}
|
||||
window.CPrint(offset.color, substr)
|
||||
}
|
||||
|
||||
@@ -2471,6 +2512,9 @@ func (t *Terminal) printColoredString(window tui.Window, text []rune, offsets []
|
||||
break
|
||||
}
|
||||
}
|
||||
if url != nil {
|
||||
window.LinkEnd()
|
||||
}
|
||||
if index < maxOffset {
|
||||
substr, _ = t.processTabs(text[index:], prefixWidth)
|
||||
window.CPrint(colBase, substr)
|
||||
@@ -2482,7 +2526,7 @@ func (t *Terminal) renderPreviewSpinner() {
|
||||
spin := t.previewer.spinner
|
||||
if len(spin) > 0 || t.previewer.scrollable {
|
||||
maxWidth := t.pwindow.Width()
|
||||
if !t.previewer.scrollable {
|
||||
if !t.previewer.scrollable || !t.previewOpts.info {
|
||||
if maxWidth > 0 {
|
||||
t.pwindow.Move(0, maxWidth-1)
|
||||
t.pwindow.CPrint(tui.ColPreviewSpinner, spin)
|
||||
@@ -2662,17 +2706,30 @@ Loop:
|
||||
|
||||
var fillRet tui.FillReturn
|
||||
prefixWidth := 0
|
||||
var url *url
|
||||
_, _, ansi = extractColor(line, ansi, func(str string, ansi *ansiState) bool {
|
||||
trimmed := []rune(str)
|
||||
isTrimmed := false
|
||||
if !t.previewOpts.wrap {
|
||||
trimmed, isTrimmed = t.trimRight(trimmed, maxWidth-t.pwindow.X())
|
||||
}
|
||||
if url == nil && ansi != nil && ansi.url != nil {
|
||||
url = ansi.url
|
||||
t.pwindow.LinkBegin(url.uri, url.params)
|
||||
}
|
||||
if url != nil && (ansi == nil || ansi.url == nil) {
|
||||
url = nil
|
||||
t.pwindow.LinkEnd()
|
||||
}
|
||||
if ansi != nil {
|
||||
lbg = ansi.lbg
|
||||
} else {
|
||||
lbg = -1
|
||||
}
|
||||
str, width := t.processTabs(trimmed, prefixWidth)
|
||||
if width > prefixWidth {
|
||||
prefixWidth = width
|
||||
if t.theme.Colored && ansi != nil && ansi.colored() {
|
||||
lbg = ansi.lbg
|
||||
fillRet = t.pwindow.CFill(ansi.fg, ansi.bg, ansi.attr, str)
|
||||
} else {
|
||||
fillRet = t.pwindow.CFill(tui.ColPreview.Fg(), tui.ColPreview.Bg(), tui.AttrRegular, str)
|
||||
@@ -2681,6 +2738,9 @@ Loop:
|
||||
return !isTrimmed &&
|
||||
(fillRet == tui.FillContinue || t.previewOpts.wrap && fillRet == tui.FillNextLine)
|
||||
})
|
||||
if url != nil {
|
||||
t.pwindow.LinkEnd()
|
||||
}
|
||||
t.previewer.scrollable = t.previewer.scrollable || t.pwindow.Y() == height-1 && t.pwindow.X() == t.pwindow.Width()
|
||||
if fillRet == tui.FillNextLine {
|
||||
continue
|
||||
@@ -2691,11 +2751,15 @@ Loop:
|
||||
if unchanged && lineNo == 0 {
|
||||
break
|
||||
}
|
||||
if lbg >= 0 {
|
||||
t.pwindow.CFill(-1, lbg, tui.AttrRegular,
|
||||
if t.theme.Colored && lbg >= 0 {
|
||||
fillRet = t.pwindow.CFill(-1, lbg, tui.AttrRegular,
|
||||
strings.Repeat(" ", t.pwindow.Width()-t.pwindow.X())+"\n")
|
||||
} else {
|
||||
t.pwindow.Fill("\n")
|
||||
fillRet = t.pwindow.Fill("\n")
|
||||
}
|
||||
if fillRet == tui.FillSuspend {
|
||||
t.previewed.filled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
lineNo++
|
||||
@@ -2941,6 +3005,10 @@ type replacePlaceholderParams struct {
|
||||
executor *util.Executor
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholderInInitialCommand(template string) (string, []string) {
|
||||
return t.replacePlaceholder(template, false, string(t.input), []*Item{nil, nil})
|
||||
}
|
||||
|
||||
func (t *Terminal) replacePlaceholder(template string, forcePlus bool, input string, list []*Item) (string, []string) {
|
||||
return replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
@@ -3391,19 +3459,6 @@ func (t *Terminal) Loop() error {
|
||||
}
|
||||
}()
|
||||
|
||||
contChan := make(chan os.Signal, 1)
|
||||
notifyOnCont(contChan)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-contChan:
|
||||
t.reqBox.Set(reqReinit, nil)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if !t.tui.ShouldEmitResizeEvent() {
|
||||
resizeChan := make(chan os.Signal, 1)
|
||||
notifyOnResize(resizeChan) // Non-portable
|
||||
@@ -3460,6 +3515,7 @@ func (t *Terminal) Loop() error {
|
||||
go func() {
|
||||
var version int64
|
||||
stop := false
|
||||
t.previewBox.WaitFor(reqPreviewReady)
|
||||
for {
|
||||
var items []*Item
|
||||
var commandTemplate string
|
||||
@@ -3488,6 +3544,9 @@ func (t *Terminal) Loop() error {
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
if items == nil {
|
||||
continue
|
||||
}
|
||||
version++
|
||||
// We don't display preview window if no match
|
||||
if items[0] != nil {
|
||||
@@ -3729,12 +3788,15 @@ func (t *Terminal) Loop() error {
|
||||
t.printHeader()
|
||||
case reqActivate:
|
||||
t.suppress = false
|
||||
if t.hasPreviewer() {
|
||||
t.previewBox.Set(reqPreviewReady, nil)
|
||||
}
|
||||
case reqRedrawBorderLabel:
|
||||
t.printLabel(t.border, t.borderLabel, t.borderLabelOpts, t.borderLabelLen, t.borderShape, true)
|
||||
case reqRedrawPreviewLabel:
|
||||
t.printLabel(t.pborder, t.previewLabel, t.previewLabelOpts, t.previewLabelLen, t.previewOpts.border, true)
|
||||
case reqReinit:
|
||||
t.tui.Resume(t.fullscreen, t.sigstop)
|
||||
t.tui.Resume(t.fullscreen, true)
|
||||
t.fullRedraw()
|
||||
case reqResize, reqFullRedraw:
|
||||
if req == reqResize {
|
||||
@@ -4348,17 +4410,77 @@ func (t *Terminal) Loop() error {
|
||||
suffix := copySlice(t.input[t.cx:])
|
||||
t.input = append(append(t.input[:t.cx], t.yanked...), suffix...)
|
||||
t.cx += len(t.yanked)
|
||||
case actPageUp:
|
||||
t.vmove(t.maxItems()-1, false)
|
||||
req(reqList)
|
||||
case actPageDown:
|
||||
t.vmove(-(t.maxItems() - 1), false)
|
||||
req(reqList)
|
||||
case actHalfPageUp:
|
||||
t.vmove(t.maxItems()/2, false)
|
||||
req(reqList)
|
||||
case actHalfPageDown:
|
||||
t.vmove(-(t.maxItems() / 2), false)
|
||||
case actPageUp, actPageDown, actHalfPageUp, actHalfPageDown:
|
||||
// Calculate the number of lines to move
|
||||
maxItems := t.maxItems()
|
||||
linesToMove := maxItems - 1
|
||||
if a.t == actHalfPageUp || a.t == actHalfPageDown {
|
||||
linesToMove = maxItems / 2
|
||||
}
|
||||
// Move at least one line even in a very short window
|
||||
linesToMove = util.Max(1, linesToMove)
|
||||
|
||||
// Determine the direction of the movement
|
||||
direction := -1
|
||||
if a.t == actPageUp || a.t == actHalfPageUp {
|
||||
direction = 1
|
||||
}
|
||||
|
||||
// In non-default layout, items are listed from top to bottom
|
||||
if t.layout != layoutDefault {
|
||||
direction *= -1
|
||||
}
|
||||
|
||||
// We can simply add the number of lines to the current position in
|
||||
// single-line mode
|
||||
if !t.canSpanMultiLines() {
|
||||
t.vset(t.cy + direction*linesToMove)
|
||||
req(reqList)
|
||||
break
|
||||
}
|
||||
|
||||
// But in multi-line mode, we need to carefully limit the amount of
|
||||
// vertical movement so that items are not skipped. In order to do
|
||||
// this, we calculate the minimum or maximum offset based on the
|
||||
// direction of the movement and the number of lines of the items
|
||||
// around the current scroll offset.
|
||||
var minOffset, maxOffset, lineSum int
|
||||
if direction > 0 {
|
||||
maxOffset = t.offset
|
||||
for ; maxOffset < t.merger.Length(); maxOffset++ {
|
||||
itemLines, _ := t.numItemLines(t.merger.Get(maxOffset).item, maxItems)
|
||||
lineSum += itemLines
|
||||
if lineSum >= maxItems {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
minOffset = t.offset
|
||||
for ; minOffset >= 0 && minOffset < t.merger.Length(); minOffset-- {
|
||||
itemLines, _ := t.numItemLines(t.merger.Get(minOffset).item, maxItems)
|
||||
lineSum += itemLines
|
||||
if lineSum >= maxItems {
|
||||
if lineSum > maxItems {
|
||||
minOffset++
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < linesToMove; i++ {
|
||||
cy, offset := t.cy, t.offset
|
||||
t.vset(cy + direction)
|
||||
t.constrain()
|
||||
if cy == t.cy {
|
||||
break
|
||||
}
|
||||
if i > 0 && (direction > 0 && t.offset > maxOffset ||
|
||||
direction < 0 && t.offset < minOffset) {
|
||||
t.cy, t.offset = cy, offset
|
||||
break
|
||||
}
|
||||
}
|
||||
req(reqList)
|
||||
case actOffsetUp, actOffsetDown:
|
||||
diff := 1
|
||||
@@ -4474,11 +4596,11 @@ func (t *Terminal) Loop() error {
|
||||
case actSigStop:
|
||||
p, err := os.FindProcess(os.Getpid())
|
||||
if err == nil {
|
||||
t.sigstop = true
|
||||
t.tui.Clear()
|
||||
t.tui.Pause(t.fullscreen)
|
||||
notifyStop(p)
|
||||
t.mutex.Unlock()
|
||||
t.reqBox.Set(reqReinit, nil)
|
||||
return false
|
||||
}
|
||||
case actMouse:
|
||||
@@ -4836,16 +4958,23 @@ func (t *Terminal) constrain() {
|
||||
for tries := 0; tries < maxLines; tries++ {
|
||||
numItems := maxLines
|
||||
// How many items can be fit on screen including the current item?
|
||||
if (t.multiLine || t.wrap) && t.merger.Length() > 0 {
|
||||
if t.canSpanMultiLines() && t.merger.Length() > 0 {
|
||||
numItemsFound := 0
|
||||
linesSum := 0
|
||||
|
||||
add := func(i int) bool {
|
||||
lines, _ := t.numItemLines(t.merger.Get(i).item, numItems-linesSum)
|
||||
lines, overflow := t.numItemLines(t.merger.Get(i).item, numItems-linesSum)
|
||||
linesSum += lines
|
||||
if linesSum >= numItems {
|
||||
if numItemsFound == 0 {
|
||||
numItemsFound = 1
|
||||
/*
|
||||
# Should show all 3 items
|
||||
printf "file1\0file2\0file3\0" | fzf --height=5 --read0 --bind load:last --reverse
|
||||
|
||||
# Should not truncate the last item
|
||||
printf "file\n1\0file\n2\0file\n3\0" | fzf --height=5 --read0 --bind load:last --reverse
|
||||
*/
|
||||
if numItemsFound == 0 || !overflow {
|
||||
numItemsFound++
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -4884,12 +5013,12 @@ func (t *Terminal) constrain() {
|
||||
for {
|
||||
prevOffset := newOffset
|
||||
numItems := t.merger.Length()
|
||||
itemLines := 1
|
||||
if (t.multiLine || t.wrap) && t.cy < numItems {
|
||||
itemLines := 1 + t.gap
|
||||
if t.canSpanMultiLines() && t.cy < numItems {
|
||||
itemLines, _ = t.numItemLines(t.merger.Get(t.cy).item, maxLines)
|
||||
}
|
||||
linesBefore := t.cy - newOffset
|
||||
if t.multiLine || t.wrap {
|
||||
if t.canSpanMultiLines() {
|
||||
linesBefore = 0
|
||||
for i := newOffset; i < t.cy && i < numItems; i++ {
|
||||
lines, _ := t.numItemLines(t.merger.Get(i).item, maxLines-linesBefore-itemLines)
|
||||
|
@@ -20,9 +20,5 @@ func notifyStop(p *os.Process) {
|
||||
if err == nil {
|
||||
pid = pgid * -1
|
||||
}
|
||||
unix.Kill(pid, syscall.SIGSTOP)
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||
unix.Kill(pid, syscall.SIGTSTP)
|
||||
}
|
||||
|
@@ -13,7 +13,3 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
func notifyStop(p *os.Process) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
// NOOP
|
||||
}
|
||||
|
11
src/tmux.go
11
src/tmux.go
@@ -38,7 +38,7 @@ func runTmux(args []string, opts *Options) (int, error) {
|
||||
case posUp:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
|
||||
case posDown:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-yS")
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y9999")
|
||||
case posLeft:
|
||||
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
|
||||
case posRight:
|
||||
@@ -49,9 +49,12 @@ func runTmux(args []string, opts *Options) (int, error) {
|
||||
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
|
||||
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
|
||||
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
sh, _ := sh()
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tmuxArgs = append(tmuxArgs, sh, temp)
|
||||
return exec.Command("tmux", tmuxArgs...)
|
||||
return exec.Command("tmux", tmuxArgs...), nil
|
||||
}, opts, true)
|
||||
}
|
||||
|
@@ -1030,13 +1030,13 @@ func cleanse(str string) string {
|
||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
w.csi("m")
|
||||
w.csi("0m")
|
||||
}
|
||||
|
||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||
hasColors, code := w.csiColor(fg, bg, attr)
|
||||
if hasColors {
|
||||
defer w.csi("m")
|
||||
defer w.csi("0m")
|
||||
}
|
||||
w.stderrInternal(cleanse(text), false, code)
|
||||
}
|
||||
@@ -1097,7 +1097,7 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
}
|
||||
}
|
||||
}
|
||||
if w.posx+1 >= w.Width() {
|
||||
if w.posx >= w.Width() {
|
||||
if w.posy+1 >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
@@ -1118,6 +1118,14 @@ func (w *LightWindow) setBg() string {
|
||||
return "\x1b[m"
|
||||
}
|
||||
|
||||
func (w *LightWindow) LinkBegin(uri string, params string) {
|
||||
w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\")
|
||||
}
|
||||
|
||||
func (w *LightWindow) LinkEnd() {
|
||||
w.renderer.queued.WriteString("\x1b]8;;\x1b\\")
|
||||
}
|
||||
|
||||
func (w *LightWindow) Fill(text string) FillReturn {
|
||||
w.Move(w.posy, w.posx)
|
||||
code := w.setBg()
|
||||
@@ -1133,7 +1141,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
||||
bg = w.bg
|
||||
}
|
||||
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
||||
defer w.csi("m")
|
||||
defer w.csi("0m")
|
||||
return w.fill(text, resetCode)
|
||||
}
|
||||
return w.fill(text, w.setBg())
|
||||
|
@@ -4,6 +4,7 @@ package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -49,6 +50,8 @@ type TcellWindow struct {
|
||||
lastY int
|
||||
moveCursor bool
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -601,6 +604,16 @@ func (w *TcellWindow) Print(text string) {
|
||||
w.printString(text, w.normal)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
|
||||
if w.uri != nil {
|
||||
style = style.Url(*w.uri)
|
||||
if md := regexp.MustCompile(`id=([^:]+)`).FindStringSubmatch(*w.params); len(md) > 1 {
|
||||
style = style.UrlId(md[1])
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
lx := 0
|
||||
a := pair.Attr()
|
||||
@@ -615,6 +628,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||
}
|
||||
style = w.withUrl(style)
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
for gr.Next() {
|
||||
@@ -665,6 +679,7 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||
style = w.withUrl(style)
|
||||
|
||||
gr := uniseg.NewGraphemes(text)
|
||||
Loop:
|
||||
@@ -716,6 +731,16 @@ func (w *TcellWindow) Fill(str string) FillReturn {
|
||||
return w.fillString(str, w.normal)
|
||||
}
|
||||
|
||||
func (w *TcellWindow) LinkBegin(uri string, params string) {
|
||||
w.uri = &uri
|
||||
w.params = ¶ms
|
||||
}
|
||||
|
||||
func (w *TcellWindow) LinkEnd() {
|
||||
w.uri = nil
|
||||
w.params = nil
|
||||
}
|
||||
|
||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||
if fg == colDefault {
|
||||
fg = w.normal.Fg()
|
||||
|
@@ -564,6 +564,8 @@ type Window interface {
|
||||
CPrint(color ColorPair, text string)
|
||||
Fill(text string) FillReturn
|
||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||
LinkBegin(uri string, params string)
|
||||
LinkEnd()
|
||||
Erase()
|
||||
EraseMaybe() bool
|
||||
}
|
||||
|
@@ -44,11 +44,6 @@ func needWinpty(opts *Options) bool {
|
||||
}
|
||||
|
||||
func runWinpty(args []string, opts *Options) (int, error) {
|
||||
sh, err := sh()
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
|
||||
argStr := escapeSingleQuote(args[0])
|
||||
for _, arg := range args[1:] {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
@@ -56,20 +51,30 @@ func runWinpty(args []string, opts *Options) (int, error) {
|
||||
argStr += ` --no-winpty`
|
||||
|
||||
if isMintty345() {
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(sh, temp)
|
||||
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}, opts, false)
|
||||
}
|
||||
|
||||
return runProxy(argStr, func(temp string) *exec.Cmd {
|
||||
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||
sh, err := sh(needBash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd
|
||||
return cmd, nil
|
||||
}, opts, false)
|
||||
}
|
||||
|
@@ -1443,7 +1443,7 @@ class TestGoFZF < TestBase
|
||||
[0, 3, 6].each do |off|
|
||||
tmux.prepare
|
||||
tmux.send_keys "#{FZF} --hscroll-off=#{off} -q 0 < #{tempname}", :Enter
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?((0..off).to_a.join + '..') }
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?((0..off).to_a.join + '··') }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?('789') }
|
||||
tmux.send_keys :Enter
|
||||
@@ -3366,6 +3366,66 @@ class TestGoFZF < TestBase
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-3], 'bar' }
|
||||
end
|
||||
|
||||
def test_boundary_match
|
||||
# Underscore boundaries should be ranked lower
|
||||
{
|
||||
default: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
|
||||
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
|
||||
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
|
||||
}.each do |scheme, expected|
|
||||
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
|
||||
assert_equal expected, result
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_noinfo
|
||||
# │ 1 ││
|
||||
tmux.send_keys %(#{FZF} --preview 'seq 1000' --preview-window top,noinfo --scrollbar --bind space:change-preview-window:info), :Enter
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.start_with?('│ 1')
|
||||
assert lines[1]&.end_with?(' ││')
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.start_with?('│ 1')
|
||||
assert lines[1]&.end_with?('1000││')
|
||||
end
|
||||
end
|
||||
|
||||
def test_gap
|
||||
tmux.send_keys %(seq 100 | #{FZF} --gap --border --reverse), :Enter
|
||||
block = <<~BLOCK
|
||||
╭─────────────────
|
||||
│ >
|
||||
│ 100/100 ──────
|
||||
│ > 1
|
||||
│
|
||||
│ 2
|
||||
│
|
||||
│ 3
|
||||
│
|
||||
│ 4
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, _1) }
|
||||
end
|
||||
|
||||
def test_gap_2
|
||||
tmux.send_keys %(seq 100 | #{FZF} --gap=2 --border --reverse), :Enter
|
||||
block = <<~BLOCK
|
||||
╭─────────────────
|
||||
│ >
|
||||
│ 100/100 ──────
|
||||
│ > 1
|
||||
│
|
||||
│
|
||||
│ 2
|
||||
│
|
||||
│
|
||||
│ 3
|
||||
BLOCK
|
||||
tmux.until { assert_block(block, _1) }
|
||||
end
|
||||
end
|
||||
|
||||
module TestShell
|
||||
|
Reference in New Issue
Block a user