mirror of
https://github.com/junegunn/fzf.git
synced 2025-08-15 20:23:50 -07:00
Compare commits
239 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 | ||
|
50fa90dfb8 | ||
|
a2c365e710 | ||
|
b4ddffdc61 | ||
|
8d4d184fc6 | ||
|
ea23539b59 | ||
|
9e92b6f11e | ||
|
6cbde812f6 | ||
|
3b2e932c13 | ||
|
8ff4e52641 | ||
|
2dbc874e3d | ||
|
039a2f1d04 | ||
|
4ef1cf5b35 | ||
|
b44ab9e33c | ||
|
8f4c23f1c4 | ||
|
23a391e715 | ||
|
035b0be29f | ||
|
e1fcdbc337 | ||
|
cfc149e994 | ||
|
2faffbd1b7 | ||
|
8db65704b9 | ||
|
797a01aed4 | ||
|
bf515a3d32 | ||
|
a06745826a | ||
|
0420ed4f2a | ||
|
3b944addd4 | ||
|
70bf8bc35d | ||
|
724f8a1d45 | ||
|
cc2b2146ee | ||
|
8689f5f230 | ||
|
e9e0011f1d | ||
|
5b52833785 | ||
|
1525768094 | ||
|
a70ea4654e | ||
|
b02bf9b6bb | ||
|
bee7bc5324 | ||
|
7c2ffd3fef | ||
|
db01e7dab6 | ||
|
2326c74eb2 | ||
|
b9d15569e8 | ||
|
c3cc378d89 | ||
|
27d1f5e0a8 | ||
|
540632bb9e | ||
|
d9c028c934 | ||
|
c54ad82e8d | ||
|
295b89631b | ||
|
6179faf778 | ||
|
16dc236a25 | ||
|
61ae9d75b6 | ||
|
e2401aca68 | ||
|
59943cbb48 | ||
|
02634d404d | ||
|
ed12925f7d | ||
|
e0ddb97ab4 | ||
|
b8c01af0fc | ||
|
6de0a7ddc1 | ||
|
79196c025d | ||
|
94c33ac020 | ||
|
b2ecb6352c | ||
|
9dc3ed638a | ||
|
0acace1ace | ||
|
1a2d37e1e6 | ||
|
22adb6494f | ||
|
e023736c30 | ||
|
dca2262fe6 | ||
|
0684a20ea3 | ||
|
a1a72bb8d1 | ||
|
144d55a5be | ||
|
7fc13c5cfd | ||
|
dfee7af57b | ||
|
9b0e2daf02 | ||
|
590060a16b | ||
|
368294edf6 | ||
|
c4a9ccd6af | ||
|
cbf91f2ed3 | ||
|
b1460d4787 | ||
|
7dc9e14874 | ||
|
1616ed543d | ||
|
dc73fba188 | ||
|
ef148dfd37 | ||
|
93bbb3032d | ||
|
4c83d8596d | ||
|
d453e6d7db | ||
|
c29533994f | ||
|
1afe13b5b5 | ||
|
36600eaaa9 | ||
|
3ee1fc2034 | ||
|
e2f93e5a2d | ||
|
cfdf2f1153 | ||
|
e042143e3f | ||
|
7c613d0d9b | ||
|
b00d46bc14 | ||
|
555b0d235b | ||
|
564daf9a7d | ||
|
41bcbe342f | ||
|
dbe8dc344e | ||
|
e33fb59da1 | ||
|
7aa88aa115 | ||
|
2b6d600879 | ||
|
05c765d442 | ||
|
49b496269c | ||
|
7405925952 | ||
|
3afd543a7e | ||
|
b4f2cde5ac | ||
|
ed53ef7cee | ||
|
12630b124d | ||
|
1d59ac09d2 | ||
|
a8f3a0dd59 | ||
|
124cd70710 | ||
|
782de139c8 | ||
|
32eb32ee5e | ||
|
2f51eb2b41 | ||
|
0ccbd79e10 | ||
|
99bd6de541 | ||
|
1fef36e4bc | ||
|
89375005b5 | ||
|
88e78c9193 | ||
|
29a19ad080 | ||
|
2a039ab746 | ||
|
7e9a0fcdbd | ||
|
7a97532547 | ||
|
996abb2831 | ||
|
da500a358f | ||
|
c36b846acc | ||
|
d9b5c9b2be | ||
|
3dee8778d0 | ||
|
d4216b0dcc | ||
|
bfe2bf4dce | ||
|
561f9291fd | ||
|
b5b0d6b3ea | ||
|
a90426b7ca | ||
|
303c3bae7f | ||
|
6b4358f641 | ||
|
552158f3ad | ||
|
7205203dc8 | ||
|
0cadf70072 | ||
|
076b3d0a9a | ||
|
7b0c9e04d3 | ||
|
573df524fe | ||
|
aee417c46a | ||
|
04db44067d | ||
|
5b204c54f9 | ||
|
daa602422d | ||
|
04dfb14e32 | ||
|
c24256cba3 | ||
|
685fb71d89 | ||
|
83b6033906 | ||
|
01e7668915 | ||
|
0994d9c881 | ||
|
030428ba43 | ||
|
8a110e02b9 | ||
|
86d92c17c4 | ||
|
c4cc7891b4 | ||
|
218843b9f1 | ||
|
d274d093af |
5
.github/workflows/linux.yml
vendored
5
.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
|
||||
@@ -46,6 +46,3 @@ jobs:
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
|
||||
- name: Integration test (tcell)
|
||||
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose
|
||||
|
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.21.0
|
||||
- uses: crate-ci/typos@v1.26.0
|
||||
|
1
.github/workflows/winget.yml
vendored
1
.github/workflows/winget.yml
vendored
@@ -10,6 +10,5 @@ jobs:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: junegunn.fzf
|
||||
version: ${{ github.event.release.tag_name }}
|
||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
|
100
.goreleaser.yml
100
.goreleaser.yml
@@ -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,21 +86,15 @@ archives:
|
||||
files:
|
||||
- non-existent*
|
||||
|
||||
checksum:
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
|
||||
release:
|
||||
github:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Tag }}'
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
name_template: '{{ .Version }}'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-devel"
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
@@ -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:
|
||||
|
100
ADVANCED.md
100
ADVANCED.md
@@ -1,18 +1,17 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2024/01/20*
|
||||
* *Requires fzf 0.46.0 or above*
|
||||
* *Last update: 2024/06/24*
|
||||
* *Requires fzf 0.54.0 or later*
|
||||
|
||||
---
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Screen Layout](#screen-layout)
|
||||
* [Display modes](#display-modes)
|
||||
* [`--height`](#--height)
|
||||
* [`fzf-tmux`](#fzf-tmux)
|
||||
* [Popup window support](#popup-window-support)
|
||||
* [`--tmux`](#--tmux)
|
||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
@@ -63,7 +62,7 @@ learn its wide variety of features.
|
||||
This document will guide you through some examples that will familiarize you
|
||||
with the advanced features of fzf.
|
||||
|
||||
Screen Layout
|
||||
Display modes
|
||||
-------------
|
||||
|
||||
### `--height`
|
||||
@@ -104,56 +103,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
|
||||
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||
```
|
||||
|
||||
### `fzf-tmux`
|
||||
### `--tmux`
|
||||
|
||||
Before fzf had `--height` option, we would open fzf in a tmux split pane not
|
||||
to take up the whole screen. This is done using `fzf-tmux` script.
|
||||
(Requires tmux 3.3 or later)
|
||||
|
||||
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
|
||||
|
||||
```sh
|
||||
# Open fzf on a tmux split pane below the current pane.
|
||||
# Takes the same set of options.
|
||||
fzf-tmux --layout=reverse
|
||||
# Open fzf in a tmux popup at the center of the screen with 70% width and height
|
||||
fzf --tmux 70%
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
The limitation of `fzf-tmux` is that it only works when you're on tmux unlike
|
||||
`--height` option. But the advantage of it is that it's more flexible.
|
||||
(See `man fzf-tmux` for available options.)
|
||||
`--tmux` option is silently ignored if you're not on tmux. So if you're trying
|
||||
to avoid opening fzf in fullscreen, try specifying both `--height` and `--tmux`.
|
||||
|
||||
```sh
|
||||
# On the right (50%)
|
||||
fzf-tmux -r
|
||||
|
||||
# On the left (30%)
|
||||
fzf-tmux -l30%
|
||||
|
||||
# Above the cursor
|
||||
fzf-tmux -u30%
|
||||
# --tmux is specified later so it takes precedence over --height when on tmux.
|
||||
# If you're not on tmux, --tmux is ignored and --height is used instead.
|
||||
fzf --height 70% --tmux 70%
|
||||
```
|
||||
|
||||

|
||||
You can also specify the position, width, and height of the popup window in
|
||||
the following format:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### Popup window support
|
||||
|
||||
But here's the really cool part; tmux 3.2 added support for popup windows. So
|
||||
you can open fzf in a popup window, which is quite useful if you frequently
|
||||
use split panes.
|
||||
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]`
|
||||
|
||||
```sh
|
||||
# Open tmux in a tmux popup window (default size: 50% of the screen)
|
||||
fzf-tmux -p
|
||||
|
||||
# 80% width, 60% height
|
||||
fzf-tmux -p 80%,60%
|
||||
# 100% width and 60% height
|
||||
fzf --tmux 100%,60% --border horizontal
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
```sh
|
||||
# On the right (50% width)
|
||||
fzf --tmux right
|
||||
```
|
||||
|
||||

|
||||
|
||||
```sh
|
||||
# On the left (40% width and 70% height)
|
||||
fzf --tmux left,40%,70%
|
||||
```
|
||||
|
||||

|
||||
|
||||
> [!TIP]
|
||||
> You might also want to check out my tmux plugins which support this popup
|
||||
> window layout.
|
||||
>
|
||||
@@ -363,7 +361,7 @@ projects, and it will free up memory as you narrow down the results.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
@@ -374,11 +372,9 @@ INITIAL_QUERY="${*:-}"
|
||||
|
||||

|
||||
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
|
||||
an empty input (`: | fzf`), then we make it start the initial Ripgrep
|
||||
process immediately via `start:reload` binding. This way, fzf owns the
|
||||
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
|
||||
the process will keep running in the background.
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
|
||||
the initial Ripgrep process immediately via `start:reload` binding for the
|
||||
consistency of the code.
|
||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
||||
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
||||
@@ -402,7 +398,7 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
@@ -446,7 +442,7 @@ CTRL-F.
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||
@@ -489,7 +485,7 @@ prevent immediate evaluation.
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
||||
@@ -529,15 +525,15 @@ Kubernetes pods.
|
||||
|
||||
```bash
|
||||
pods() {
|
||||
: | command='kubectl get pods --all-namespaces' fzf \
|
||||
command='kubectl get pods --all-namespaces' fzf \
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start:reload:$command' \
|
||||
--bind 'ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
}
|
||||
|
228
CHANGELOG.md
228
CHANGELOG.md
@@ -1,6 +1,234 @@
|
||||
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
|
||||
- [fastwalk: add optional sorting and improve documentation](https://github.com/charlievieth/fastwalk/pull/27)
|
||||
- [fastwalk: only check if MSYSTEM is set during MSYS/MSYS2](https://github.com/charlievieth/fastwalk/pull/28)
|
||||
- Thanks to @charlievieth
|
||||
- Reverted ALT-C binding of fish to use `cd` instead of `builtin cd`
|
||||
- `builtin cd` was introduced to work around a bug of `cd` coming from `zoxide init --cmd cd fish` where it cannot handle `--` argument.
|
||||
- However, the default `cd` of fish is actually a wrapper function for supporting `cd -`, so we want to use it instead.
|
||||
- See [#3928](https://github.com/junegunn/fzf/pull/3928) for more information and consider helping zoxide fix the bug.
|
||||
|
||||
0.54.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
|
||||
|
||||
- Implemented line wrap of long items
|
||||
- `--wrap` option enables line wrap
|
||||
- `--wrap-sign` customizes the sign for wrapped lines (default: `↳ `)
|
||||
- `toggle-wrap` action toggles line wrap
|
||||
```sh
|
||||
history | fzf --tac --wrap --bind 'ctrl-/:toggle-wrap' --wrap-sign $'\t↳ '
|
||||
```
|
||||
- fzf by default binds `CTRL-/` and `ALT-/` to `toggle-wrap`
|
||||
- Updated shell integration scripts to leverage line wrap
|
||||
- CTRL-R binding includes `--wrap-sign $'\t↳ '` to indent wrapped lines
|
||||
- `kill **` completion uses `--wrap` to show the whole line by default
|
||||
instead of showing it in the preview window
|
||||
- Added `--info-command` option for customizing the info line
|
||||
```sh
|
||||
# Prepend the current cursor position in yellow
|
||||
fzf --info-command='echo -e "\x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO 💛"'
|
||||
```
|
||||
- `$FZF_INFO` is set to the original info text
|
||||
- ANSI color codes are supported
|
||||
- Pointer and marker signs can be set to empty strings
|
||||
```sh
|
||||
# Minimal style
|
||||
fzf --pointer '' --marker '' --prompt '' --info hidden
|
||||
```
|
||||
- Better cache management and improved rendering for `--tail`
|
||||
- Improved `--sync` behavior
|
||||
- When `--sync` is provided, fzf will not render the interface until the initial filtering and the associated actions (bound to any of `start`, `load`, `result`, or `focus`) are complete.
|
||||
```sh
|
||||
# fzf will not render intermediate states
|
||||
(sleep 1; seq 1000000; sleep 1) |
|
||||
fzf --sync --query 5 --listen --bind start:up,load:up,result:up,focus:change-header:Ready
|
||||
```
|
||||
- GET endpoint is now available from `execute` and `transform` actions (it used to timeout due to lock conflict)
|
||||
```sh
|
||||
fzf --listen --sync --bind 'focus:transform-header:curl -s localhost:$FZF_PORT?limit=0 | jq .'
|
||||
```
|
||||
- Added `offset-middle` action to place the current item is in the middle of the screen
|
||||
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
|
||||
```sh
|
||||
# Now this will work as expected. Previously, this would print an invalid header line.
|
||||
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||
# `load` event would fire and the header would be prematurely updated.
|
||||
fzf --header 'Loading ...' --header-lines 1 \
|
||||
--bind 'start:reload:sleep 1; ps -ef' \
|
||||
--bind 'load:change-header:Loaded!'
|
||||
```
|
||||
- Fixed mouse support on Windows
|
||||
- Fixed crash when using `--tiebreak=end` with very long items
|
||||
- zsh 5.0 compatibility (thanks to @LangLangBart)
|
||||
- Fixed `--walker-skip` to also skip symlinks to directories
|
||||
- Fixed `result` event not fired when input stream is not complete
|
||||
- New tags will have `v` prefix so that they are available on https://proxy.golang.org/
|
||||
|
||||
0.53.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.53.0/_
|
||||
|
||||
- Multi-line display
|
||||
- See [Processing multi-line items](https://junegunn.github.io/fzf/tips/processing-multi-line-items/)
|
||||
- fzf can now display 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
|
||||
|
||||
# Ripgrep multi-line output
|
||||
rg --pretty bash | perl -0777 -pe 's/\n\n/\n\0/gm' |
|
||||
fzf --read0 --ansi --multi --highlight-line --reverse --tmux 70%
|
||||
```
|
||||
- To disable multi-line display, use `--no-multi-line`
|
||||
- CTRL-R bindings of bash, zsh, and fish have been updated to leverage multi-line display
|
||||
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
|
||||
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
|
||||
```
|
||||
╻First line
|
||||
┃...
|
||||
╹Last line
|
||||
```
|
||||
- Native tmux integration
|
||||
- Added `--tmux` option to replace fzf-tmux script and simplify distribution
|
||||
```sh
|
||||
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
# Center, 100% width and 70% height
|
||||
fzf --tmux 100%,70% --border horizontal --padding 1,2
|
||||
|
||||
# Left, 30% width
|
||||
fzf --tmux left,30%
|
||||
|
||||
# Bottom, 50% height
|
||||
fzf --tmux bottom,50%
|
||||
```
|
||||
- To keep the implementation simple, it only uses popups. You need tmux 3.3 or later.
|
||||
- To use `--tmux` in Vim plugin:
|
||||
```vim
|
||||
let g:fzf_layout = { 'tmux': '100%,70%' }
|
||||
```
|
||||
- Added support for endless input streams
|
||||
- See [Browsing log stream with fzf](https://junegunn.github.io/fzf/tips/browsing-log-streams/)
|
||||
- Added `--tail=NUM` option to limit the number of items to keep in memory. This is useful when you want to browse an endless stream of data (e.g. log stream) with fzf while limiting memory usage.
|
||||
```sh
|
||||
# Interactive filtering of a log stream
|
||||
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact
|
||||
```
|
||||
- Better Windows Support
|
||||
- fzf now works on Git bash (mintty) out of the box via winpty integration
|
||||
- Many fixes and improvements for Windows
|
||||
- man page is now embedded in the binary; `fzf --man` to see it
|
||||
- Changed the default `--scroll-off` to 3, as we think it's a better default
|
||||
- Process started by `execute` action now directly writes to and reads from `/dev/tty`. Manual `/dev/tty` redirection for interactive programs is no longer required.
|
||||
```sh
|
||||
# Vim will work fine without /dev/tty redirection
|
||||
ls | fzf --bind 'space:execute:vim {}' > selected
|
||||
```
|
||||
- Added `print(...)` action to queue an arbitrary string to be printed on exit. This was mainly added to work around the limitation of `--expect` where it's not compatible with `--bind` on the same key and it would ignore other actions bound to it.
|
||||
```sh
|
||||
# This doesn't work as expected because --expect is not compatible with --bind
|
||||
fzf --multi --expect ctrl-y --bind 'ctrl-y:select-all'
|
||||
|
||||
# This is something you can do instead
|
||||
fzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'
|
||||
```
|
||||
- We also considered making them compatible, but realized that some users may have been relying on the current behavior.
|
||||
- [`NO_COLOR`](https://no-color.org/) environment variable is now respected. If the variable is set, fzf defaults to `--no-color` unless otherwise specified.
|
||||
|
||||
0.52.1
|
||||
------
|
||||
- Fixed a critical bug in the Windows version
|
||||
|
@@ -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 ]"]
|
||||
|
13
Makefile
13
Makefile
@@ -4,17 +4,17 @@ GOOS ?= $(shell $(GO) env GOOS)
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
else
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null)
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
|
||||
endif
|
||||
ifeq ($(VERSION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||
endif
|
||||
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
|
||||
VERSION_TRIM := $(shell sed "s/^v//; s/-.*//" <<< $(VERSION))
|
||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
|
||||
ifdef FZF_REVISION
|
||||
@@ -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,12 +289,13 @@ 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%`) |
|
||||
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) |
|
||||
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
|
||||
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
|
||||
|
||||
@@ -457,12 +458,13 @@ let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
```
|
||||
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting fzf-tmux options in `tmux` key.
|
||||
or above) by putting `--tmux` option value in `tmux` key.
|
||||
|
||||
```vim
|
||||
" See `man fzf-tmux` for available options
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
|
@@ -57,7 +57,7 @@ if [[ $KITTY_WINDOW_ID ]]; then
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -f sixel -s "$dim" "$file"
|
||||
chafa -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
|
30
bin/fzf-tmux
30
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
|
||||
@@ -132,8 +143,10 @@ if [[ -z "$TMUX" ]]; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --height option is not allowed. CTRL-Z is also disabled.
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
|
||||
# * --height option is not allowed
|
||||
# * CTRL-Z is also disabled
|
||||
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
@@ -185,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
|
||||
|
12
doc/fzf.txt
12
doc/fzf.txt
@@ -306,12 +306,13 @@ 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%` )
|
||||
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
|
||||
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
@@ -469,11 +470,12 @@ in Neovim.
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
<
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting fzf-tmux options in `tmux` key.
|
||||
or above) by putting `--tmux` options in `tmux` key.
|
||||
>
|
||||
" See `man fzf-tmux` for available options
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
|
8
go.mod
8
go.mod
@@ -1,13 +1,13 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.3
|
||||
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.20.0
|
||||
golang.org/x/term v0.20.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.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
|
||||
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
|
||||
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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.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.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
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=
|
||||
|
14
install
14
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.52.1
|
||||
version=0.56.1
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -146,7 +146,7 @@ download() {
|
||||
fi
|
||||
|
||||
local url
|
||||
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
|
||||
url=https://github.com/junegunn/fzf/releases/download/v$version/${1}
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
@@ -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 ;;
|
||||
@@ -265,7 +265,11 @@ fi
|
||||
EOF
|
||||
|
||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
if [[ "$shell" = zsh ]]; then
|
||||
echo "source <(fzf --$shell)" >> "$src"
|
||||
else
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
fi
|
||||
else
|
||||
cat >> "$src" << EOF
|
||||
# Auto-completion
|
||||
|
@@ -1,4 +1,4 @@
|
||||
$version="0.52.1"
|
||||
$version="0.56.1"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
@@ -40,7 +40,7 @@ function download {
|
||||
return
|
||||
}
|
||||
cd "$fzf_base\bin"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/v$version/$file"
|
||||
$temp=$env:TMP + "\fzf.zip"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||
|
23
main.go
23
main.go
@@ -4,13 +4,14 @@ import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
fzf "github.com/junegunn/fzf/src"
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.52"
|
||||
var version = "0.56"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
@@ -28,6 +29,9 @@ var zshCompletion []byte
|
||||
//go:embed shell/key-bindings.fish
|
||||
var fishKeyBindings []byte
|
||||
|
||||
//go:embed man/man1/fzf.1
|
||||
var manPage []byte
|
||||
|
||||
func printScript(label string, content []byte) {
|
||||
fmt.Println("### " + label + " ###")
|
||||
fmt.Println(strings.TrimSpace(string(content)))
|
||||
@@ -35,7 +39,7 @@ func printScript(label string, content []byte) {
|
||||
}
|
||||
|
||||
func exit(code int, err error) {
|
||||
if err != nil {
|
||||
if code == fzf.ExitError && err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
}
|
||||
os.Exit(code)
|
||||
@@ -76,6 +80,21 @@ func main() {
|
||||
}
|
||||
return
|
||||
}
|
||||
if options.Man {
|
||||
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
|
||||
if len(file) == 0 {
|
||||
fmt.Print(string(manPage))
|
||||
return
|
||||
}
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command("man", file)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Print(string(manPage))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
code, err := fzf.Run(options)
|
||||
exit(code, err)
|
||||
|
@@ -21,48 +21,48 @@ 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 "May 2024" "fzf 0.52.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
|
||||
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
|
||||
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
a tmux popup window. It is designed to work just like fzf except that it does
|
||||
not take up the whole screen. You can safely use fzf-tmux instead of fzf in
|
||||
not take up the whole screen. You can safely use fzf\-tmux instead of fzf in
|
||||
your scripts as the extra options will be silently ignored if you're not on
|
||||
tmux.
|
||||
|
||||
.SH LAYOUT OPTIONS
|
||||
|
||||
(default layout: \fB-d 50%\fR)
|
||||
(default layout: \fB\-d 50%\fR)
|
||||
|
||||
.SS Popup window
|
||||
(requires tmux 3.2 or above)
|
||||
.TP
|
||||
.B "-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.B "\-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.TP
|
||||
.B "-w WIDTH[%]"
|
||||
.B "\-w WIDTH[%]"
|
||||
.TP
|
||||
.B "-h WIDTH[%]"
|
||||
.B "\-h WIDTH[%]"
|
||||
.TP
|
||||
.B "-x COL"
|
||||
.B "\-x COL"
|
||||
.TP
|
||||
.B "-y ROW"
|
||||
.B "\-y ROW"
|
||||
|
||||
.SS Split pane
|
||||
.TP
|
||||
.B "-u [height[%]]"
|
||||
.B "\-u [height[%]]"
|
||||
Split above (up)
|
||||
.TP
|
||||
.B "-d [height[%]]"
|
||||
.B "\-d [height[%]]"
|
||||
Split below (down)
|
||||
.TP
|
||||
.B "-l [width[%]]"
|
||||
.B "\-l [width[%]]"
|
||||
Split left
|
||||
.TP
|
||||
.B "-r [width[%]]"
|
||||
.B "\-r [width[%]]"
|
||||
Split right
|
||||
|
1066
man/man1/fzf.1
1066
man/man1/fzf.1
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -327,7 +332,10 @@ function! s:common_sink(action, lines) abort
|
||||
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
|
||||
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
|
||||
for item in a:lines
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
|
||||
if has('win32unix') && item !~ '/'
|
||||
let item = substitute(item, '\', '/', 'g')
|
||||
end
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
|
||||
let sep = s:is_win ? '\' : '/'
|
||||
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
|
||||
endif
|
||||
@@ -487,6 +495,8 @@ function! s:extract_option(opts, name)
|
||||
return opt
|
||||
endfunction
|
||||
|
||||
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||
@@ -529,18 +539,19 @@ try
|
||||
\ executable('tput') && filereadable('/dev/tty')
|
||||
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
||||
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
||||
let use_term = has_nvim_term ||
|
||||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_term = has_nvim_term || has_vim8_term
|
||||
\ && !s:need_cmd_window
|
||||
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
||||
if prefer_tmux && use_tmux
|
||||
let use_height = 0
|
||||
let use_term = 0
|
||||
endif
|
||||
if use_term
|
||||
let optstr .= ' --no-height'
|
||||
let optstr .= ' --no-height --no-tmux'
|
||||
elseif use_height
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
let optstr .= ' --no-tmux --height='.height
|
||||
endif
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||
@@ -573,19 +584,21 @@ function! s:fzf_tmux(dict)
|
||||
if empty(size)
|
||||
for o in ['up', 'down', 'left', 'right']
|
||||
if s:present(a:dict, o)
|
||||
let spec = a:dict[o]
|
||||
if (o == 'up' || o == 'down') && spec[0] == '~'
|
||||
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
|
||||
else
|
||||
" Legacy boolean option
|
||||
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
|
||||
endif
|
||||
let size = o . ',' . a:dict[o]
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
|
||||
" Legacy fzf-tmux options
|
||||
if size =~ '-'
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
end
|
||||
|
||||
" Using native --tmux option
|
||||
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
|
||||
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
|
||||
endfunction
|
||||
|
||||
function! s:splittable(dict)
|
||||
@@ -657,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
|
||||
@@ -708,10 +717,10 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
call jobstart(cmd, fzf)
|
||||
return []
|
||||
endif
|
||||
elseif has('win32unix') && $TERM !=# 'cygwin'
|
||||
elseif s:need_cmd_window
|
||||
let shellscript = s:fzf_tempname()
|
||||
call s:writefile([command], shellscript)
|
||||
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
|
||||
let command = 'start //WAIT sh -c '.shellscript
|
||||
let a:temps.shellscript = shellscript
|
||||
endif
|
||||
if a:use_height
|
||||
@@ -723,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
|
||||
@@ -738,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)
|
||||
@@ -904,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
|
||||
|
||||
|
@@ -101,75 +101,84 @@ _fzf_opts_completion() {
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="
|
||||
-h --help
|
||||
-e --exact
|
||||
+x --no-extended
|
||||
-q --query
|
||||
-f --filter
|
||||
--literal
|
||||
--scheme
|
||||
--expect
|
||||
--disabled
|
||||
--tiebreak
|
||||
--bind
|
||||
--color
|
||||
-d --delimiter
|
||||
-n --nth
|
||||
--with-nth
|
||||
+s --no-sort
|
||||
--track
|
||||
--tac
|
||||
-i --ignore-case
|
||||
+i --no-ignore-case
|
||||
-m --multi
|
||||
--ansi
|
||||
--no-mouse
|
||||
+c --no-color
|
||||
--no-bold
|
||||
--layout
|
||||
--reverse
|
||||
--cycle
|
||||
--keep-right
|
||||
--no-hscroll
|
||||
--hscroll-off
|
||||
--scroll-off
|
||||
--filepath-word
|
||||
--info
|
||||
--separator
|
||||
--no-separator
|
||||
--no-scrollbar
|
||||
--jump-labels
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
--read0
|
||||
--print0
|
||||
--print-query
|
||||
--prompt
|
||||
--pointer
|
||||
--marker
|
||||
--sync
|
||||
--history
|
||||
--history-size
|
||||
--header
|
||||
--header-lines
|
||||
--header-first
|
||||
--ellipsis
|
||||
--preview
|
||||
--preview-window
|
||||
--height
|
||||
--min-height
|
||||
+i --no-ignore-case
|
||||
+s --no-sort
|
||||
+x --no-extended
|
||||
--ansi
|
||||
--bash
|
||||
--bind
|
||||
--border
|
||||
--border-label
|
||||
--border-label-pos
|
||||
--color
|
||||
--cycle
|
||||
--disabled
|
||||
--ellipsis
|
||||
--expect
|
||||
--filepath-word
|
||||
--fish
|
||||
--header
|
||||
--header-first
|
||||
--header-lines
|
||||
--height
|
||||
--highlight-line
|
||||
--history
|
||||
--history-size
|
||||
--hscroll-off
|
||||
--info
|
||||
--jump-labels
|
||||
--keep-right
|
||||
--layout
|
||||
--listen
|
||||
--listen-unsafe
|
||||
--literal
|
||||
--man
|
||||
--margin
|
||||
--marker
|
||||
--min-height
|
||||
--no-bold
|
||||
--no-clear
|
||||
--no-hscroll
|
||||
--no-mouse
|
||||
--no-scrollbar
|
||||
--no-separator
|
||||
--no-unicode
|
||||
--padding
|
||||
--pointer
|
||||
--preview
|
||||
--preview-label
|
||||
--preview-label-pos
|
||||
--no-unicode
|
||||
--margin
|
||||
--padding
|
||||
--preview-window
|
||||
--print-query
|
||||
--print0
|
||||
--prompt
|
||||
--read0
|
||||
--reverse
|
||||
--scheme
|
||||
--scroll-off
|
||||
--separator
|
||||
--sync
|
||||
--tabstop
|
||||
--listen
|
||||
--no-clear
|
||||
--tac
|
||||
--tiebreak
|
||||
--tmux
|
||||
--track
|
||||
--version
|
||||
--with-nth
|
||||
--with-shell
|
||||
--wrap
|
||||
--zsh
|
||||
-0 --exit-0
|
||||
-1 --select-1
|
||||
-d --delimiter
|
||||
-e --exact
|
||||
-f --filter
|
||||
-h --help
|
||||
-i --ignore-case
|
||||
-m --multi
|
||||
-n --nth
|
||||
-q --query
|
||||
--"
|
||||
|
||||
case "${prev}" in
|
||||
@@ -255,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_[^ ]+)' ' ]] &&
|
||||
@@ -280,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
|
||||
@@ -367,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")
|
||||
@@ -399,9 +409,10 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
_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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -471,12 +482,38 @@ 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 diff diff3
|
||||
awk bat cat code diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
|
@@ -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
|
||||
@@ -157,10 +151,11 @@ __fzf_generic_path_completion() {
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
export FZF_DEFAULT_OPTS
|
||||
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
|
||||
@@ -169,10 +164,10 @@ __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
|
||||
fi | while read item; do
|
||||
__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 "${(q)item} "
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
@@ -197,11 +192,11 @@ _fzf_dir_completion() {
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_fzf_feed_fifo() (
|
||||
_fzf_feed_fifo() {
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
cat <&0 > "$1" &|
|
||||
}
|
||||
|
||||
_fzf_complete() {
|
||||
setopt localoptions ksh_arrays
|
||||
@@ -226,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
|
||||
|
||||
@@ -237,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
|
||||
@@ -264,13 +258,14 @@ _fzf_complete_telnet() {
|
||||
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
|
||||
# The current word without the trigger is in the $prefix variable passed from the caller.
|
||||
_fzf_complete_ssh() {
|
||||
local tokens=(${(z)1})
|
||||
local -a tokens
|
||||
tokens=(${(z)1})
|
||||
case ${tokens[-1]} in
|
||||
-i|-F|-E)
|
||||
_fzf_path_completion "$prefix" "$1"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
local user
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
@@ -296,9 +291,10 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
_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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -307,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})
|
||||
@@ -318,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+=("")
|
||||
@@ -337,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"
|
||||
@@ -363,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
|
||||
|
@@ -57,15 +57,15 @@ __fzf_cd__() {
|
||||
if command -v perl > /dev/null; then
|
||||
__fzf_history__() {
|
||||
local output script
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
@@ -91,7 +91,7 @@ else # awk - fallback for POSIX systems
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
|
@@ -59,20 +59,31 @@ function fzf_key_bindings
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
|
||||
# merge history from other sessions before searching
|
||||
if test -z "$fish_private_mode"
|
||||
builtin history merge
|
||||
end
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
if type -P perl > /dev/null 2>&1
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z --reverse | command perl -0 -pe 's/^/$.\t/g; s/\n/\n\t/gm' | eval (__fzfcmd) --tac --read0 --print0 -q '(commandline)' | command perl -pe 's/^\d*\t//' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line $FZF_CTRL_R_OPTS +m")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
builtin history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
end
|
||||
else
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
builtin history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
end
|
||||
end
|
||||
|
@@ -52,8 +52,8 @@ __fzf_select() {
|
||||
local item
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
echo
|
||||
@@ -106,16 +106,31 @@ fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
# 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") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
fi
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$(awk '{print $1}' <<< "$selected")
|
||||
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
|
||||
zle vi-fetch-history -n ${num%\*}
|
||||
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||
zle vi-fetch-history -n $MATCH
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
|
@@ -58,72 +58,74 @@ func _() {
|
||||
_ = x[actToggleTrack-47]
|
||||
_ = x[actToggleTrackCurrent-48]
|
||||
_ = x[actToggleHeader-49]
|
||||
_ = x[actTrackCurrent-50]
|
||||
_ = x[actUntrackCurrent-51]
|
||||
_ = x[actDown-52]
|
||||
_ = x[actUp-53]
|
||||
_ = x[actPageUp-54]
|
||||
_ = x[actPageDown-55]
|
||||
_ = x[actPosition-56]
|
||||
_ = x[actHalfPageUp-57]
|
||||
_ = x[actHalfPageDown-58]
|
||||
_ = x[actOffsetUp-59]
|
||||
_ = x[actOffsetDown-60]
|
||||
_ = x[actJump-61]
|
||||
_ = x[actJumpAccept-62]
|
||||
_ = x[actPrintQuery-63]
|
||||
_ = x[actRefreshPreview-64]
|
||||
_ = x[actReplaceQuery-65]
|
||||
_ = x[actToggleSort-66]
|
||||
_ = x[actShowPreview-67]
|
||||
_ = x[actHidePreview-68]
|
||||
_ = x[actTogglePreview-69]
|
||||
_ = x[actTogglePreviewWrap-70]
|
||||
_ = x[actTransform-71]
|
||||
_ = x[actTransformBorderLabel-72]
|
||||
_ = x[actTransformHeader-73]
|
||||
_ = x[actTransformPreviewLabel-74]
|
||||
_ = x[actTransformPrompt-75]
|
||||
_ = x[actTransformQuery-76]
|
||||
_ = x[actPreview-77]
|
||||
_ = x[actChangePreview-78]
|
||||
_ = x[actChangePreviewWindow-79]
|
||||
_ = x[actPreviewTop-80]
|
||||
_ = x[actPreviewBottom-81]
|
||||
_ = x[actPreviewUp-82]
|
||||
_ = x[actPreviewDown-83]
|
||||
_ = x[actPreviewPageUp-84]
|
||||
_ = x[actPreviewPageDown-85]
|
||||
_ = x[actPreviewHalfPageUp-86]
|
||||
_ = x[actPreviewHalfPageDown-87]
|
||||
_ = x[actPrevHistory-88]
|
||||
_ = x[actPrevSelected-89]
|
||||
_ = x[actPut-90]
|
||||
_ = x[actNextHistory-91]
|
||||
_ = x[actNextSelected-92]
|
||||
_ = x[actExecute-93]
|
||||
_ = x[actExecuteSilent-94]
|
||||
_ = x[actExecuteMulti-95]
|
||||
_ = x[actSigStop-96]
|
||||
_ = x[actFirst-97]
|
||||
_ = x[actLast-98]
|
||||
_ = x[actReload-99]
|
||||
_ = x[actReloadSync-100]
|
||||
_ = x[actDisableSearch-101]
|
||||
_ = x[actEnableSearch-102]
|
||||
_ = x[actSelect-103]
|
||||
_ = x[actDeselect-104]
|
||||
_ = x[actUnbind-105]
|
||||
_ = x[actRebind-106]
|
||||
_ = x[actBecome-107]
|
||||
_ = x[actResponse-108]
|
||||
_ = x[actShowHeader-109]
|
||||
_ = x[actHideHeader-110]
|
||||
_ = x[actToggleWrap-50]
|
||||
_ = x[actTrackCurrent-51]
|
||||
_ = x[actUntrackCurrent-52]
|
||||
_ = x[actDown-53]
|
||||
_ = x[actUp-54]
|
||||
_ = x[actPageUp-55]
|
||||
_ = x[actPageDown-56]
|
||||
_ = x[actPosition-57]
|
||||
_ = x[actHalfPageUp-58]
|
||||
_ = x[actHalfPageDown-59]
|
||||
_ = x[actOffsetUp-60]
|
||||
_ = x[actOffsetDown-61]
|
||||
_ = x[actOffsetMiddle-62]
|
||||
_ = x[actJump-63]
|
||||
_ = x[actJumpAccept-64]
|
||||
_ = x[actPrintQuery-65]
|
||||
_ = x[actRefreshPreview-66]
|
||||
_ = x[actReplaceQuery-67]
|
||||
_ = x[actToggleSort-68]
|
||||
_ = x[actShowPreview-69]
|
||||
_ = x[actHidePreview-70]
|
||||
_ = x[actTogglePreview-71]
|
||||
_ = x[actTogglePreviewWrap-72]
|
||||
_ = x[actTransform-73]
|
||||
_ = x[actTransformBorderLabel-74]
|
||||
_ = x[actTransformHeader-75]
|
||||
_ = x[actTransformPreviewLabel-76]
|
||||
_ = x[actTransformPrompt-77]
|
||||
_ = x[actTransformQuery-78]
|
||||
_ = x[actPreview-79]
|
||||
_ = x[actChangePreview-80]
|
||||
_ = x[actChangePreviewWindow-81]
|
||||
_ = x[actPreviewTop-82]
|
||||
_ = x[actPreviewBottom-83]
|
||||
_ = x[actPreviewUp-84]
|
||||
_ = x[actPreviewDown-85]
|
||||
_ = x[actPreviewPageUp-86]
|
||||
_ = x[actPreviewPageDown-87]
|
||||
_ = x[actPreviewHalfPageUp-88]
|
||||
_ = x[actPreviewHalfPageDown-89]
|
||||
_ = x[actPrevHistory-90]
|
||||
_ = x[actPrevSelected-91]
|
||||
_ = x[actPrint-92]
|
||||
_ = x[actPut-93]
|
||||
_ = x[actNextHistory-94]
|
||||
_ = x[actNextSelected-95]
|
||||
_ = x[actExecute-96]
|
||||
_ = x[actExecuteSilent-97]
|
||||
_ = x[actExecuteMulti-98]
|
||||
_ = x[actSigStop-99]
|
||||
_ = x[actFirst-100]
|
||||
_ = x[actLast-101]
|
||||
_ = x[actReload-102]
|
||||
_ = x[actReloadSync-103]
|
||||
_ = x[actDisableSearch-104]
|
||||
_ = x[actEnableSearch-105]
|
||||
_ = x[actSelect-106]
|
||||
_ = x[actDeselect-107]
|
||||
_ = x[actUnbind-108]
|
||||
_ = x[actRebind-109]
|
||||
_ = x[actBecome-110]
|
||||
_ = x[actShowHeader-111]
|
||||
_ = x[actHideHeader-112]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 690, 705, 722, 729, 734, 743, 754, 765, 778, 793, 804, 817, 832, 839, 852, 865, 882, 897, 910, 924, 938, 954, 974, 986, 1009, 1027, 1051, 1069, 1086, 1096, 1112, 1134, 1147, 1163, 1175, 1189, 1205, 1223, 1243, 1265, 1279, 1294, 1302, 1308, 1322, 1337, 1347, 1363, 1378, 1388, 1396, 1403, 1412, 1425, 1441, 1456, 1465, 1476, 1485, 1494, 1503, 1516, 1529}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -342,8 +342,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
state := interpretCode(code, prevState)
|
||||
if expected != state.ToString() {
|
||||
t.Errorf("expected: %s, actual: %s",
|
||||
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
|
||||
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
|
||||
strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
|
||||
strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
|
||||
}
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
|
@@ -22,6 +22,14 @@ func (cc *ChunkCache) Clear() {
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) retire(chunk ...*Chunk) {
|
||||
cc.mutex.Lock()
|
||||
for _, c := range chunk {
|
||||
delete(cc.cache, c)
|
||||
}
|
||||
cc.mutex.Unlock()
|
||||
}
|
||||
|
||||
// Add adds the list to the cache
|
||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||
|
@@ -16,14 +16,16 @@ type ChunkList struct {
|
||||
chunks []*Chunk
|
||||
mutex sync.Mutex
|
||||
trans ItemBuilder
|
||||
cache *ChunkCache
|
||||
}
|
||||
|
||||
// NewChunkList returns a new ChunkList
|
||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
|
||||
return &ChunkList{
|
||||
chunks: []*Chunk{},
|
||||
mutex: sync.Mutex{},
|
||||
trans: trans}
|
||||
trans: trans,
|
||||
cache: cache}
|
||||
}
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||
@@ -48,7 +50,12 @@ func CountItems(cs []*Chunk) int {
|
||||
if len(cs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
|
||||
if len(cs) == 1 {
|
||||
return cs[0].count
|
||||
}
|
||||
|
||||
// First chunk might not be full due to --tail=N
|
||||
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
|
||||
}
|
||||
|
||||
// Push adds the item to the list
|
||||
@@ -72,18 +79,56 @@ func (cl *ChunkList) Clear() {
|
||||
}
|
||||
|
||||
// Snapshot returns immutable snapshot of the ChunkList
|
||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||
cl.mutex.Lock()
|
||||
|
||||
changed := false
|
||||
if tail > 0 && CountItems(cl.chunks) > tail {
|
||||
changed = true
|
||||
// Find the number of chunks to keep
|
||||
numChunks := 0
|
||||
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
|
||||
numChunks++
|
||||
left -= cl.chunks[i].count
|
||||
}
|
||||
|
||||
// Copy the chunks to keep
|
||||
ret := make([]*Chunk, numChunks)
|
||||
minIndex := len(cl.chunks) - numChunks
|
||||
cl.cache.retire(cl.chunks[:minIndex]...)
|
||||
copy(ret, cl.chunks[minIndex:])
|
||||
|
||||
for left, i := tail, len(ret)-1; i >= 0; i-- {
|
||||
chunk := ret[i]
|
||||
if chunk.count > left {
|
||||
newChunk := *chunk
|
||||
newChunk.count = left
|
||||
oldCount := chunk.count
|
||||
for i := 0; i < left; i++ {
|
||||
newChunk.items[i] = chunk.items[oldCount-left+i]
|
||||
}
|
||||
ret[i] = &newChunk
|
||||
cl.cache.retire(chunk)
|
||||
break
|
||||
}
|
||||
left -= chunk.count
|
||||
}
|
||||
cl.chunks = ret
|
||||
}
|
||||
|
||||
ret := make([]*Chunk, len(cl.chunks))
|
||||
copy(ret, cl.chunks)
|
||||
|
||||
// Duplicate the last chunk
|
||||
// Duplicate the first and the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
if tail > 0 && cnt > 1 {
|
||||
newChunk := *ret[0]
|
||||
ret[0] = &newChunk
|
||||
}
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
|
||||
cl.mutex.Unlock()
|
||||
return ret, CountItems(ret)
|
||||
return ret, CountItems(ret), changed
|
||||
}
|
||||
|
@@ -11,13 +11,13 @@ func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(item *Item, s []byte) bool {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
snapshot, count := cl.Snapshot()
|
||||
snapshot, count, _ := cl.Snapshot(0)
|
||||
if len(snapshot) > 0 || count > 0 {
|
||||
t.Error("Snapshot should be empty now")
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// But the new snapshot should contain the added items
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 1 && count != 2 {
|
||||
t.Error("Snapshot should not be empty now")
|
||||
}
|
||||
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// New snapshot
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
|
||||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
||||
t.Error("Expected two full chunks and one more chunk")
|
||||
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
|
||||
t.Error("Unexpected number of items:", lastChunkCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkListTail(t *testing.T) {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
total := chunkSize*2 + chunkSize/2
|
||||
for i := 0; i < total; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
}
|
||||
|
||||
snapshot, count, changed := cl.Snapshot(0)
|
||||
assertCount := func(expected int, shouldChange bool) {
|
||||
if count != expected || CountItems(snapshot) != expected {
|
||||
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
|
||||
}
|
||||
if changed != shouldChange {
|
||||
t.Error("Unexpected change status")
|
||||
}
|
||||
}
|
||||
assertCount(total, false)
|
||||
|
||||
tail := chunkSize + chunkSize/2
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, true)
|
||||
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, false)
|
||||
|
||||
snapshot, count, changed = cl.Snapshot(0)
|
||||
assertCount(tail, false)
|
||||
|
||||
tail = chunkSize / 2
|
||||
snapshot, count, changed = cl.Snapshot(tail)
|
||||
assertCount(tail, true)
|
||||
}
|
||||
|
@@ -67,9 +67,9 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
ExitCancel = -1
|
||||
ExitOk = 0
|
||||
ExitNoMatch = 1
|
||||
ExitError = 2
|
||||
ExitBecome = 126
|
||||
ExitInterrupt = 130
|
||||
)
|
||||
|
99
src/core.go
99
src/core.go
@@ -2,6 +2,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,8 +18,36 @@ Matcher -> EvtSearchFin -> Terminal (update list)
|
||||
Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
type revision struct {
|
||||
major int
|
||||
minor int
|
||||
}
|
||||
|
||||
func (r *revision) bumpMajor() {
|
||||
r.major++
|
||||
r.minor = 0
|
||||
}
|
||||
|
||||
func (r *revision) bumpMinor() {
|
||||
r.minor++
|
||||
}
|
||||
|
||||
func (r revision) compatible(other revision) bool {
|
||||
return r.major == other.major
|
||||
}
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options) (int, error) {
|
||||
if opts.Filter == nil {
|
||||
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
|
||||
if needWinpty(opts) {
|
||||
return runWinpty(os.Args, opts)
|
||||
}
|
||||
}
|
||||
|
||||
if err := postProcessOptions(opts); err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
@@ -63,11 +92,12 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
cache := NewChunkCache()
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, byteString(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
@@ -79,7 +109,7 @@ func Run(opts *Options) (int, error) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
@@ -116,6 +146,24 @@ 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
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
var reader *Reader
|
||||
@@ -123,7 +171,10 @@ func Run(opts *Options) (int, error) {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
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
|
||||
@@ -139,15 +190,14 @@ func Run(opts *Options) (int, error) {
|
||||
forward = true
|
||||
}
|
||||
}
|
||||
cache := NewChunkCache()
|
||||
patternCache := make(map[string]*Pattern)
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
}
|
||||
inputRevision := 0
|
||||
snapshotRevision := 0
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
@@ -176,12 +226,13 @@ 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)
|
||||
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
@@ -206,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
|
||||
deferred := opts.Select1 || opts.Exit0 || opts.Sync
|
||||
go terminal.Loop()
|
||||
if !deferred && !heightUnknown {
|
||||
// Start right away
|
||||
@@ -252,9 +299,11 @@ func Run(opts *Options) (int, error) {
|
||||
reading = true
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision++
|
||||
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
|
||||
@@ -297,17 +346,18 @@ func Run(opts *Options) (int, error) {
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
if snapshotRevision != inputRevision {
|
||||
if !snapshotRevision.compatible(inputRevision) {
|
||||
query = []rune{}
|
||||
}
|
||||
snapshot, count = chunkList.Snapshot()
|
||||
var changed bool
|
||||
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
|
||||
}
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
@@ -340,7 +390,10 @@ func Run(opts *Options) (int, error) {
|
||||
break
|
||||
}
|
||||
if !useSnapshot {
|
||||
newSnapshot, newCount := chunkList.Snapshot()
|
||||
newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || newCount > 0 {
|
||||
@@ -392,7 +445,7 @@ func Run(opts *Options) (int, error) {
|
||||
determine(val.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, true)
|
||||
terminal.UpdateList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func writeTemporaryFile(data []string, printSep string) string {
|
||||
f, err := os.CreateTemp("", "fzf-preview-*")
|
||||
func WriteTemporaryFile(data []string, printSep string) string {
|
||||
f, err := os.CreateTemp("", "fzf-temp-*")
|
||||
if err != nil {
|
||||
// Unable to create temporary file
|
||||
// FIXME: Should we terminate the program?
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -17,7 +19,7 @@ func (item *Item) Index() int32 {
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
var minItem = Item{text: util.Chars{Index: -1}}
|
||||
var minItem = Item{text: util.Chars{Index: math.MinInt32}}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
|
@@ -16,7 +16,7 @@ type MatchRequest struct {
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
revision int
|
||||
revision revision
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
@@ -30,7 +30,7 @@ type Matcher struct {
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
revision int
|
||||
revision revision
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -40,7 +40,7 @@ const (
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
return &Matcher{
|
||||
cache: cache,
|
||||
@@ -82,11 +82,15 @@ func (m *Matcher) Loop() {
|
||||
break
|
||||
}
|
||||
|
||||
cacheCleared := false
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.revision = request.revision
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
m.cache.Clear()
|
||||
if !request.revision.compatible(m.revision) {
|
||||
m.cache.Clear()
|
||||
}
|
||||
cacheCleared = true
|
||||
}
|
||||
|
||||
// Restart search
|
||||
@@ -95,20 +99,20 @@ func (m *Matcher) Loop() {
|
||||
cancelled := false
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
foundCache := false
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
foundCache = true
|
||||
merger = cached
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||
merger = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
|
||||
if !foundCache {
|
||||
if merger == nil {
|
||||
merger, cancelled = m.scan(request)
|
||||
}
|
||||
|
||||
@@ -160,6 +164,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
@@ -190,7 +195,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort {
|
||||
if m.sort && request.pattern.sortable {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
} else {
|
||||
@@ -231,11 +236,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
|
||||
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
@@ -244,7 +249,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
||||
} else {
|
||||
event = reqRetry
|
||||
}
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
|
@@ -3,8 +3,8 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
func EmptyMerger(revision int) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision)
|
||||
func EmptyMerger(revision revision) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
@@ -20,19 +20,25 @@ type Merger struct {
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
revision int
|
||||
revision revision
|
||||
minIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
var minIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
}
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision}
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision}
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
}
|
||||
|
||||
// Revision returns revision number
|
||||
func (mg *Merger) Revision() int {
|
||||
func (mg *Merger) Revision() revision {
|
||||
return mg.revision
|
||||
}
|
||||
|
||||
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
index := -1
|
||||
if mg.pass {
|
||||
index = int(itemIndex)
|
||||
index = int(itemIndex - mg.minIndex)
|
||||
if mg.tac {
|
||||
index = mg.count - index - 1
|
||||
}
|
||||
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
firstChunk := (*mg.chunks)[0]
|
||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||
idx -= firstChunk.count
|
||||
|
||||
chunk := (*mg.chunks)[idx/chunkSize+1]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
|
@@ -23,10 +23,11 @@ func randResult() Result {
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, EmptyMerger(0).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(0).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
|
||||
r := revision{}
|
||||
assert(t, EmptyMerger(r).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(r).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(r).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(r).merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
@@ -57,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false, 0)
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -69,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false, 0)
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -79,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false, 0)
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
721
src/options.go
721
src/options.go
File diff suppressed because it is too large
Load Diff
@@ -106,10 +106,11 @@ func TestSplitNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIrrelevantNth(t *testing.T) {
|
||||
index := 0
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words := []string{"--nth", "..", "-x"}
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -118,7 +119,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -127,7 +128,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words = append(words, "-x")
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 2 {
|
||||
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||
@@ -335,10 +336,11 @@ func TestColorSpec(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDefaultCtrlNP(t *testing.T) {
|
||||
index := 0
|
||||
check := func(words []string, et tui.EventType, expected actionType) {
|
||||
e := et.AsEvent()
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[e][0].t != expected {
|
||||
t.Error()
|
||||
@@ -364,8 +366,9 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
}
|
||||
|
||||
func optsFor(words ...string) *Options {
|
||||
index := 0
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
@@ -451,7 +454,6 @@ func TestValidateSign(t *testing.T) {
|
||||
{"> ", true},
|
||||
{"아", true},
|
||||
{"😀", true},
|
||||
{"", false},
|
||||
{">>>", false},
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
||||
@@ -155,14 +157,14 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
}
|
||||
|
||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||
str = strings.ReplaceAll(str, "\\ ", "\t")
|
||||
tokens := _splitRegex.Split(str, -1)
|
||||
sets := []termSet{}
|
||||
set := termSet{}
|
||||
switchSet := false
|
||||
afterBar := false
|
||||
for _, token := range tokens {
|
||||
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
||||
typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
|
||||
lowerText := strings.ToLower(text)
|
||||
caseSensitive := caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && text != lowerText
|
||||
@@ -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
|
||||
|
@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
|
||||
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath")
|
||||
}
|
||||
|
158
src/proxy.go
Normal file
158
src/proxy.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
const becomeSuffix = ".become"
|
||||
|
||||
func escapeSingleQuote(str string) string {
|
||||
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func fifo(name string) (string, error) {
|
||||
ns := time.Now().UnixNano()
|
||||
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
|
||||
output, err := mkfifo(output, 0600)
|
||||
if err != nil {
|
||||
return output, err
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
defer os.Remove(output)
|
||||
|
||||
// Take the output
|
||||
go func() {
|
||||
withOutputPipe(output, func(outputFile io.ReadCloser) {
|
||||
if opts.Output == nil {
|
||||
io.Copy(os.Stdout, outputFile)
|
||||
} else {
|
||||
reader := bufio.NewReader(outputFile)
|
||||
sep := opts.PrintSep[0]
|
||||
for {
|
||||
item, err := reader.ReadString(sep)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
opts.Output <- item
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
var command string
|
||||
commandPrefix += ` --no-force-tty-in --proxy-script "$0"`
|
||||
if opts.Input == nil && (opts.ForceTtyIn || util.IsTty(os.Stdin)) {
|
||||
command = fmt.Sprintf(`%s > %q`, commandPrefix, output)
|
||||
} else {
|
||||
input, err := fifo("proxy-input")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
defer os.Remove(input)
|
||||
|
||||
go func() {
|
||||
withInputPipe(input, func(inputFile io.WriteCloser) {
|
||||
if opts.Input == nil {
|
||||
io.Copy(inputFile, os.Stdin)
|
||||
} else {
|
||||
for item := range opts.Input {
|
||||
fmt.Fprint(inputFile, item+opts.PrintSep)
|
||||
}
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
if withExports {
|
||||
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
|
||||
} else {
|
||||
// For mintty: cannot directly read named pipe from Go code
|
||||
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
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, err := cmdBuilder(temp, needBash)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
intChan := make(chan os.Signal, 1)
|
||||
defer close(intChan)
|
||||
go func() {
|
||||
if sig, valid := <-intChan; valid {
|
||||
cmd.Process.Signal(sig)
|
||||
}
|
||||
}()
|
||||
signal.Notify(intChan, os.Interrupt)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if exitError, ok := err.(*exec.ExitError); ok {
|
||||
code := exitError.ExitCode()
|
||||
if code == ExitBecome {
|
||||
becomeFile := temp + becomeSuffix
|
||||
data, err := os.ReadFile(becomeFile)
|
||||
os.Remove(becomeFile)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
elems := strings.Split(string(data), "\x00")
|
||||
if len(elems) < 1 {
|
||||
return ExitError, errors.New("invalid become command")
|
||||
}
|
||||
command := elems[0]
|
||||
env := []string{}
|
||||
if len(elems) > 1 {
|
||||
env = elems[1:]
|
||||
}
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
ttyin, err := tui.TtyIn()
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
executor.Become(ttyin, env, command)
|
||||
}
|
||||
return code, err
|
||||
}
|
||||
}
|
||||
|
||||
return ExitOk, nil
|
||||
}
|
41
src/proxy_unix.go
Normal file
41
src/proxy_unix.go
Normal file
@@ -0,0 +1,41 @@
|
||||
//go:build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func sh(bash bool) (string, error) {
|
||||
if bash {
|
||||
return "bash", nil
|
||||
}
|
||||
return "sh", nil
|
||||
}
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
return path, unix.Mkfifo(path, mode)
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task(outputFile)
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
task(inputFile)
|
||||
inputFile.Close()
|
||||
return nil
|
||||
}
|
85
src/proxy_windows.go
Normal file
85
src/proxy_windows.go
Normal file
@@ -0,0 +1,85 @@
|
||||
//go:build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var shPath atomic.Value
|
||||
|
||||
func sh(bash bool) (string, error) {
|
||||
if cached := shPath.Load(); cached != nil {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
name := "sh"
|
||||
if bash {
|
||||
name = "bash"
|
||||
}
|
||||
cmd := exec.Command("cygpath", "-w", "/usr/bin/"+name)
|
||||
bytes, err := cmd.Output()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sh := strings.TrimSpace(string(bytes))
|
||||
shPath.Store(sh)
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
func mkfifo(path string, mode uint32) (string, error) {
|
||||
m := strconv.FormatUint(uint64(mode), 8)
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return path, err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return path, err
|
||||
}
|
||||
return path + ".lnk", nil
|
||||
}
|
||||
|
||||
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
|
||||
outputFile, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
task(outputFile)
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||
sh, err := sh(false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
|
||||
inputFile, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
task(inputFile)
|
||||
inputFile.Close()
|
||||
cmd.Wait()
|
||||
return nil
|
||||
}
|
119
src/reader.go
119
src/reader.go
@@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -24,15 +24,26 @@ type Reader struct {
|
||||
event int32
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
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, 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() {
|
||||
@@ -78,18 +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 {
|
||||
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)
|
||||
}
|
||||
@@ -108,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 util.IsTty() {
|
||||
} 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)
|
||||
@@ -220,19 +241,47 @@ func (r *Reader) readFromStdin() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func isSymlinkToDir(path string, de os.DirEntry) bool {
|
||||
if de.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
if s, err := os.Stat(path); err == nil {
|
||||
return s.IsDir()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func trimPath(path string) string {
|
||||
bytes := stringBytes(path)
|
||||
|
||||
for len(bytes) > 1 && bytes[0] == '.' && (bytes[1] == '/' || bytes[1] == '\\') {
|
||||
bytes = bytes[2:]
|
||||
}
|
||||
|
||||
if len(bytes) == 0 {
|
||||
return "."
|
||||
}
|
||||
|
||||
return byteString(bytes)
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
||||
r.killed = false
|
||||
conf := fastwalk.Config{Follow: opts.follow}
|
||||
conf := fastwalk.Config{
|
||||
Follow: opts.follow,
|
||||
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||
ToSlash: fastwalk.DefaultToSlash(),
|
||||
Sort: fastwalk.SortFilesFirst,
|
||||
}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
path = trimPath(path)
|
||||
if path != "." {
|
||||
isDir := de.IsDir()
|
||||
if 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 {
|
||||
@@ -241,7 +290,7 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher([]byte(path)) {
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
@@ -255,24 +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
|
||||
}
|
||||
out, err := r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
execOut, err := exec.StdoutPipe()
|
||||
if err != nil || exec.Start() != nil {
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
err = r.exec.Start()
|
||||
r.mutex.Unlock()
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
// Function to call to terminate the running command
|
||||
r.termFunc = func() {
|
||||
execOut.Close()
|
||||
util.KillCommand(exec)
|
||||
}
|
||||
r.feed(out)
|
||||
return r.exec.Wait() == nil
|
||||
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@@ -15,6 +15,8 @@ type Offset [2]int32
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
match bool
|
||||
url *url
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -80,7 +82,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +111,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
@@ -176,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+].
|
||||
//
|
||||
@@ -193,12 +198,14 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
||||
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)})
|
||||
color: ansiToColorPair(ansi, colBase),
|
||||
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)
|
||||
|
@@ -38,9 +38,9 @@ const (
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
responseChannel chan string
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
getHandler func(getParams) string
|
||||
}
|
||||
|
||||
type listenAddress struct {
|
||||
@@ -73,7 +73,7 @@ func parseListenAddress(address string) (listenAddress, error) {
|
||||
return listenAddress{parts[0], port}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||
host := address.host
|
||||
port := address.port
|
||||
apiKey := os.Getenv("FZF_API_KEY")
|
||||
@@ -99,9 +99,9 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
responseChannel: responseChannel,
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
getHandler: getHandler,
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -165,17 +165,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
case 0:
|
||||
getMatch := getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) > 0 {
|
||||
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
|
||||
select {
|
||||
case response := <-server.responseChannel:
|
||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||
if len(response) > 0 {
|
||||
return good(response)
|
||||
case <-time.After(channelTimeout):
|
||||
go func() {
|
||||
// Drain the channel
|
||||
<-server.responseChannel
|
||||
}()
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
}
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
|
1460
src/terminal.go
1460
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
|
60
src/tmux.go
Normal file
60
src/tmux.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
func runTmux(args []string, opts *Options) (int, error) {
|
||||
// Prepare arguments
|
||||
fzf := args[0]
|
||||
args = append([]string{"--bind=ctrl-z:ignore"}, args[1:]...)
|
||||
if opts.BorderShape == tui.BorderUndefined {
|
||||
args = append(args, "--border")
|
||||
}
|
||||
argStr := escapeSingleQuote(fzf)
|
||||
for _, arg := range args {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-tmux --no-height`
|
||||
|
||||
// Get current directory
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
dir = "."
|
||||
}
|
||||
|
||||
// Set tmux options for popup placement
|
||||
// C Both The centre of the terminal
|
||||
// R -x The right side of the terminal
|
||||
// P Both The bottom left of the pane
|
||||
// M Both The mouse position
|
||||
// W Both The window position on the status line
|
||||
// S -y The line above or below the status line
|
||||
tmuxArgs := []string{"display-popup", "-E", "-B", "-d", dir}
|
||||
switch opts.Tmux.position {
|
||||
case posUp:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
|
||||
case posDown:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-y9999")
|
||||
case posLeft:
|
||||
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
|
||||
case posRight:
|
||||
tmuxArgs = append(tmuxArgs, "-xR", "-yC")
|
||||
case posCenter:
|
||||
tmuxArgs = append(tmuxArgs, "-xC", "-yC")
|
||||
}
|
||||
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
|
||||
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
|
||||
|
||||
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...), nil
|
||||
}, opts, true)
|
||||
}
|
@@ -107,11 +107,12 @@ func _() {
|
||||
_ = x[Result-96]
|
||||
_ = x[Jump-97]
|
||||
_ = x[JumpCancel-98]
|
||||
_ = x[ClickHeader-99]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637, 648}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
@@ -73,7 +73,7 @@ func (r *LightRenderer) csi(code string) string {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
fmt.Fprint(os.Stderr, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
|
||||
fmt.Fprint(r.ttyout, "\x1b[?7l\x1b[?25l"+r.queued.String()+"\x1b[?25h\x1b[?7h")
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
@@ -88,6 +88,7 @@ type LightRenderer struct {
|
||||
prevDownTime time.Time
|
||||
clicks [][2]int
|
||||
ttyin *os.File
|
||||
ttyout *os.File
|
||||
buffer []byte
|
||||
origState *term.State
|
||||
width int
|
||||
@@ -126,10 +127,10 @@ type LightWindow struct {
|
||||
bg Color
|
||||
}
|
||||
|
||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
in, err := openTtyIn()
|
||||
func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
out, err := openTtyOut()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
out = os.Stderr
|
||||
}
|
||||
r := LightRenderer{
|
||||
closed: util.NewAtomicBool(false),
|
||||
@@ -137,7 +138,8 @@ func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop in
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
clearOnExit: clearOnExit,
|
||||
ttyin: in,
|
||||
ttyin: ttyin,
|
||||
ttyout: out,
|
||||
yoffset: 0,
|
||||
tabstop: tabstop,
|
||||
fullscreen: fullscreen,
|
||||
@@ -792,6 +794,9 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
|
||||
w.fg = r.theme.Fg.Color
|
||||
w.bg = r.theme.Bg.Color
|
||||
}
|
||||
if !w.bg.IsDefault() && w.border.shape != BorderNone {
|
||||
w.Erase()
|
||||
}
|
||||
w.drawBorder(false)
|
||||
return w
|
||||
}
|
||||
@@ -1019,19 +1024,19 @@ func (w *LightWindow) Print(text string) {
|
||||
}
|
||||
|
||||
func cleanse(str string) string {
|
||||
return strings.Replace(str, "\x1b", "", -1)
|
||||
return strings.ReplaceAll(str, "\x1b", "")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -1092,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
|
||||
}
|
||||
@@ -1113,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()
|
||||
@@ -1128,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())
|
||||
|
@@ -33,27 +33,21 @@ func (r *LightRenderer) fd() int {
|
||||
return int(r.ttyin.Fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) initPlatform() error {
|
||||
fd := r.fd()
|
||||
origState, err := term.GetState(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.origState = origState
|
||||
term.MakeRaw(fd)
|
||||
return nil
|
||||
func (r *LightRenderer) initPlatform() (err error) {
|
||||
r.origState, err = term.MakeRaw(r.fd())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *LightRenderer) closePlatform() {
|
||||
// NOOP
|
||||
r.ttyout.Close()
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
func openTty(mode int) (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, mode, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
@@ -62,6 +56,14 @@ func openTtyIn() (*os.File, error) {
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
return openTty(syscall.O_RDONLY)
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return openTty(syscall.O_WRONLY)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
term.MakeRaw(r.fd())
|
||||
}
|
||||
|
@@ -96,6 +96,10 @@ func openTtyIn() (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return os.Stderr, nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
||||
return err
|
||||
@@ -135,7 +139,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
|
||||
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
|
@@ -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()
|
||||
|
@@ -3,6 +3,7 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
|
||||
|
||||
// Test the handling of the tcell keyboard events.
|
||||
func TestGetCharEventKey(t *testing.T) {
|
||||
if util.ToTty() {
|
||||
if util.IsTty(os.Stdout) {
|
||||
// This test is skipped when output goes to terminal, because it causes
|
||||
// some glitches:
|
||||
// - output lines may not start at the beginning of a row which makes
|
||||
|
@@ -4,12 +4,19 @@ package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
||||
|
||||
var tty atomic.Value
|
||||
|
||||
func ttyname() string {
|
||||
if cached := tty.Load(); cached != nil {
|
||||
return cached.(string)
|
||||
}
|
||||
|
||||
var stderr syscall.Stat_t
|
||||
if syscall.Fstat(2, &stderr) != nil {
|
||||
return ""
|
||||
@@ -27,24 +34,21 @@ func ttyname() string {
|
||||
continue
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
return prefix + file.Name()
|
||||
value := prefix + file.Name()
|
||||
tty.Store(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
return in
|
||||
}
|
||||
}
|
||||
return os.Stdin
|
||||
}
|
||||
return in
|
||||
// TtyIn returns terminal device to read user input
|
||||
func TtyIn() (*os.File, error) {
|
||||
return openTtyIn()
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to write to
|
||||
func TtyOut() (*os.File, error) {
|
||||
return openTtyOut()
|
||||
}
|
||||
|
@@ -2,13 +2,20 @@
|
||||
|
||||
package tui
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func ttyname() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
return os.Stdin
|
||||
func TtyIn() (*os.File, error) {
|
||||
return os.Stdin, nil
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns nil
|
||||
func TtyOut() (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
@@ -334,15 +334,6 @@ type Event struct {
|
||||
MouseEvent *MouseEvent
|
||||
}
|
||||
|
||||
func (e Event) Is(types ...EventType) bool {
|
||||
for _, t := range types {
|
||||
if e.Type == t {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type MouseEvent struct {
|
||||
Y int
|
||||
X int
|
||||
@@ -356,7 +347,8 @@ type MouseEvent struct {
|
||||
type BorderShape int
|
||||
|
||||
const (
|
||||
BorderNone BorderShape = iota
|
||||
BorderUndefined BorderShape = iota
|
||||
BorderNone
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderBold
|
||||
@@ -572,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
|
||||
}
|
||||
@@ -701,9 +695,9 @@ func init() {
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
@@ -731,9 +725,9 @@ func init() {
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
@@ -761,9 +755,9 @@ func init() {
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
@@ -822,7 +816,7 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||
// These colors are not defined in the base themes
|
||||
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
|
||||
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
|
||||
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch)
|
||||
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
|
||||
return chars.slice
|
||||
}
|
||||
|
||||
func (chars *Chars) NumLines(atMost int) (int, bool) {
|
||||
lines := 1
|
||||
if runes := chars.optionalRunes(); runes != nil {
|
||||
for _, r := range runes {
|
||||
if r == '\n' {
|
||||
lines++
|
||||
}
|
||||
if lines > atMost {
|
||||
return atMost, true
|
||||
}
|
||||
}
|
||||
return lines, false
|
||||
}
|
||||
|
||||
for idx := 0; idx < len(chars.slice); idx++ {
|
||||
found := bytes.IndexByte(chars.slice[idx:], '\n')
|
||||
if found < 0 {
|
||||
break
|
||||
}
|
||||
|
||||
idx += found
|
||||
lines++
|
||||
if lines > atMost {
|
||||
return atMost, true
|
||||
}
|
||||
}
|
||||
return lines, false
|
||||
}
|
||||
|
||||
func (chars *Chars) optionalRunes() []rune {
|
||||
if chars.inBytes {
|
||||
return nil
|
||||
@@ -196,3 +226,85 @@ func (chars *Chars) Prepend(prefix string) {
|
||||
chars.slice = append([]byte(prefix), chars.slice...)
|
||||
}
|
||||
}
|
||||
|
||||
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
|
||||
text := make([]rune, chars.Length())
|
||||
copy(text, chars.ToRunes())
|
||||
|
||||
lines := [][]rune{}
|
||||
overflow := false
|
||||
if !multiLine {
|
||||
lines = append(lines, text)
|
||||
} else {
|
||||
from := 0
|
||||
for off := 0; off < len(text); off++ {
|
||||
if text[off] == '\n' {
|
||||
lines = append(lines, text[from:off+1]) // Include '\n'
|
||||
from = off + 1
|
||||
if len(lines) >= maxLines {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var lastLine []rune
|
||||
if from < len(text) {
|
||||
lastLine = text[from:]
|
||||
}
|
||||
|
||||
overflow = false
|
||||
if len(lines) >= maxLines {
|
||||
overflow = true
|
||||
} else {
|
||||
lines = append(lines, lastLine)
|
||||
}
|
||||
}
|
||||
|
||||
// If wrapping is disabled, we're done
|
||||
if wrapCols == 0 {
|
||||
return lines, overflow
|
||||
}
|
||||
|
||||
wrapped := [][]rune{}
|
||||
for _, line := range lines {
|
||||
// Remove trailing '\n' and remember if it was there
|
||||
newline := len(line) > 0 && line[len(line)-1] == '\n'
|
||||
if newline {
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
for {
|
||||
cols := wrapCols
|
||||
if len(wrapped) > 0 {
|
||||
cols -= wrapSignWidth
|
||||
}
|
||||
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
|
||||
if overflowIdx >= 0 {
|
||||
// Might be a wide character
|
||||
if overflowIdx == 0 {
|
||||
overflowIdx = 1
|
||||
}
|
||||
if len(wrapped) >= maxLines {
|
||||
return wrapped, true
|
||||
}
|
||||
wrapped = append(wrapped, line[:overflowIdx])
|
||||
line = line[overflowIdx:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Restore trailing '\n'
|
||||
if newline {
|
||||
line = append(line, '\n')
|
||||
}
|
||||
|
||||
if len(wrapped) >= maxLines {
|
||||
return wrapped, true
|
||||
}
|
||||
|
||||
wrapped = append(wrapped, line)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return wrapped, false
|
||||
}
|
||||
|
@@ -1,6 +1,9 @@
|
||||
package util
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToCharsAscii(t *testing.T) {
|
||||
chars := ToChars([]byte("foobar"))
|
||||
@@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
|
||||
check(" h o ", 5)
|
||||
check(" ", 0)
|
||||
}
|
||||
|
||||
func TestCharsLines(t *testing.T) {
|
||||
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
|
||||
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
|
||||
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
|
||||
fmt.Println(lines, overflow)
|
||||
if len(lines) != expectedNumLines || overflow != expectedOverflow {
|
||||
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
|
||||
}
|
||||
}
|
||||
|
||||
// No wrap
|
||||
check(true, 1, 0, 0, 8, 1, true)
|
||||
check(true, 2, 0, 0, 8, 2, true)
|
||||
check(true, 3, 0, 0, 8, 3, false)
|
||||
|
||||
// Wrap (2)
|
||||
check(true, 4, 2, 0, 8, 4, true)
|
||||
check(true, 5, 2, 0, 8, 5, true)
|
||||
check(true, 6, 2, 0, 8, 6, true)
|
||||
check(true, 7, 2, 0, 8, 7, true)
|
||||
check(true, 8, 2, 0, 8, 8, true)
|
||||
check(true, 9, 2, 0, 8, 9, false)
|
||||
check(true, 9, 2, 0, 1, 8, false) // Smaller tab size
|
||||
|
||||
// With wrap sign (3 + 1)
|
||||
check(true, 100, 3, 1, 1, 8, false)
|
||||
|
||||
// With wrap sign (3 + 2)
|
||||
check(true, 100, 3, 2, 1, 12, false)
|
||||
|
||||
// With wrap sign (3 + 2) and no multi-line
|
||||
check(false, 100, 3, 2, 1, 13, false)
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package util
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -137,14 +138,20 @@ func DurWithin(
|
||||
return val
|
||||
}
|
||||
|
||||
// IsTty returns true if stdin is a terminal
|
||||
func IsTty() bool {
|
||||
return isatty.IsTerminal(os.Stdin.Fd())
|
||||
// IsTty returns true if the file is a terminal
|
||||
func IsTty(file *os.File) bool {
|
||||
fd := file.Fd()
|
||||
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
|
||||
}
|
||||
|
||||
// ToTty returns true if stdout is a terminal
|
||||
func ToTty() bool {
|
||||
return isatty.IsTerminal(os.Stdout.Fd())
|
||||
// RunOnce runs the given function only once
|
||||
func RunOnce(f func()) func() {
|
||||
once := Once(true)
|
||||
return func() {
|
||||
if once() {
|
||||
f()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Once returns a function that returns the specified boolean value only once
|
||||
@@ -152,7 +159,7 @@ func Once(nextResponse bool) func() bool {
|
||||
state := nextResponse
|
||||
return func() bool {
|
||||
prevState := state
|
||||
state = false
|
||||
state = !nextResponse
|
||||
return prevState
|
||||
}
|
||||
}
|
||||
@@ -188,3 +195,34 @@ func ToKebabCase(s string) string {
|
||||
}
|
||||
return strings.ToLower(name)
|
||||
}
|
||||
|
||||
// CompareVersions compares two version strings
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
parts1 := strings.Split(v1, ".")
|
||||
parts2 := strings.Split(v2, ".")
|
||||
|
||||
atoi := func(s string) int {
|
||||
n, e := strconv.Atoi(s)
|
||||
if e != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
|
||||
var p1, p2 int
|
||||
if i < len(parts1) {
|
||||
p1 = atoi(parts1[i])
|
||||
}
|
||||
if i < len(parts2) {
|
||||
p2 = atoi(parts2[i])
|
||||
}
|
||||
|
||||
if p1 > p2 {
|
||||
return 1
|
||||
} else if p1 < p2 {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
@@ -137,8 +137,11 @@ func TestOnce(t *testing.T) {
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
if !o() {
|
||||
t.Error("Expected: true")
|
||||
}
|
||||
if !o() {
|
||||
t.Error("Expected: true")
|
||||
}
|
||||
|
||||
o = Once(true)
|
||||
@@ -148,6 +151,9 @@ func TestOnce(t *testing.T) {
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
if o() {
|
||||
t.Error("Expected: false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunesWidth(t *testing.T) {
|
||||
@@ -203,3 +209,34 @@ func TestStringWidth(t *testing.T) {
|
||||
t.Errorf("Expected: %d, Actual: %d", 1, w)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
assert := func(a, b string, expected int) {
|
||||
if result := CompareVersions(a, b); result != expected {
|
||||
t.Errorf("Expected: %d, Actual: %d", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
assert("2", "1", 1)
|
||||
assert("2", "2", 0)
|
||||
assert("2", "10", -1)
|
||||
|
||||
assert("2.1", "2.2", -1)
|
||||
assert("2.1", "2.1.1", -1)
|
||||
|
||||
assert("1.2.3", "1.2.2", 1)
|
||||
assert("1.2.3", "1.2.3", 0)
|
||||
assert("1.2.3", "1.2.3.0", 0)
|
||||
assert("1.2.3", "1.2.4", -1)
|
||||
|
||||
// Different number of parts
|
||||
assert("1.0.0", "1", 0)
|
||||
assert("1.0.0", "1.0", 0)
|
||||
assert("1.0.0", "1.0.0", 0)
|
||||
assert("1.0", "1.0.0", 0)
|
||||
assert("1", "1.0.0", 0)
|
||||
assert("1.0.0", "1.0.0.1", -1)
|
||||
assert("1.0.0.1.0", "1.0.0.1", 0)
|
||||
|
||||
assert("", "3.4.5", -1)
|
||||
}
|
||||
|
@@ -157,10 +157,10 @@ func (x *Executor) QuoteEntry(entry string) string {
|
||||
*/
|
||||
return escapeArg(entry)
|
||||
case shellTypePowerShell:
|
||||
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
||||
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
||||
escaped := strings.ReplaceAll(entry, `"`, `\"`)
|
||||
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
|
||||
default:
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
|
||||
}
|
||||
}
|
||||
|
||||
|
13
src/winpty.go
Normal file
13
src/winpty.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import "errors"
|
||||
|
||||
func needWinpty(_ *Options) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func runWinpty(_ []string, _ *Options) (int, error) {
|
||||
return ExitError, errors.New("Not supported")
|
||||
}
|
80
src/winpty_windows.go
Normal file
80
src/winpty_windows.go
Normal file
@@ -0,0 +1,80 @@
|
||||
//go:build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func isMintty345() bool {
|
||||
return util.CompareVersions(os.Getenv("TERM_PROGRAM_VERSION"), "3.4.5") >= 0
|
||||
}
|
||||
|
||||
func needWinpty(opts *Options) bool {
|
||||
if os.Getenv("TERM_PROGRAM") != "mintty" {
|
||||
return false
|
||||
}
|
||||
if isMintty345() {
|
||||
/*
|
||||
See: https://github.com/junegunn/fzf/issues/3809
|
||||
|
||||
"MSYS=enable_pcon" allows fzf to run properly on mintty 3.4.5 or later.
|
||||
*/
|
||||
if strings.Contains(os.Getenv("MSYS"), "enable_pcon") {
|
||||
return false
|
||||
}
|
||||
|
||||
// Setting the environment variable here unfortunately doesn't help,
|
||||
// so we need to start a child process with "MSYS=enable_pcon"
|
||||
// os.Setenv("MSYS", "enable_pcon")
|
||||
return true
|
||||
}
|
||||
if opts.NoWinpty {
|
||||
return false
|
||||
}
|
||||
if _, err := exec.LookPath("winpty"); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func runWinpty(args []string, opts *Options) (int, error) {
|
||||
argStr := escapeSingleQuote(args[0])
|
||||
for _, arg := range args[1:] {
|
||||
argStr += " " + escapeSingleQuote(arg)
|
||||
}
|
||||
argStr += ` --no-winpty`
|
||||
|
||||
if isMintty345() {
|
||||
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, nil
|
||||
}, opts, false)
|
||||
}
|
||||
|
||||
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, nil
|
||||
}, opts, false)
|
||||
}
|
477
test/test_go.rb
477
test/test_go.rb
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user