mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-09 11:53:47 -05:00
Compare commits
334 Commits
v0.58.0
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76dcfe85f2 | ||
|
|
8cdfb23df6 | ||
|
|
4ffde48e2f | ||
|
|
f2b33f038a | ||
|
|
d5913bf86e | ||
|
|
0e9026b817 | ||
|
|
ab407c4645 | ||
|
|
91c4bef35f | ||
|
|
bf77206221 | ||
|
|
0cb1be3f04 | ||
|
|
01cb38a5fb | ||
|
|
c38c6cad79 | ||
|
|
ba6fc40cfd | ||
|
|
dd46a256c0 | ||
|
|
d19ce0ad8d | ||
|
|
ed7becfb47 | ||
|
|
9ace1351ff | ||
|
|
e1de29bc40 | ||
|
|
0df7d10550 | ||
|
|
91e119a77e | ||
|
|
3984161f6c | ||
|
|
91beacf0f4 | ||
|
|
e6ad01fb90 | ||
|
|
ce2200e908 | ||
|
|
548061dbde | ||
|
|
8f0c91545d | ||
|
|
0eefcf348e | ||
|
|
c1f8d18a0c | ||
|
|
8585969d6d | ||
|
|
8a943a9b1a | ||
|
|
c87a8eccd4 | ||
|
|
65df0abf0e | ||
|
|
b51bc6b50e | ||
|
|
febaadbee5 | ||
|
|
0e67c5aa7a | ||
|
|
760d1b7c58 | ||
|
|
9bdacc8df2 | ||
|
|
8e936ecfa7 | ||
|
|
db2e95b1f2 | ||
|
|
687074e772 | ||
|
|
3401c2e0c7 | ||
|
|
e8cb315419 | ||
|
|
f0c4ee4047 | ||
|
|
de0df2422a | ||
|
|
148b0a94cd | ||
|
|
ca294109c3 | ||
|
|
9cad2686e9 | ||
|
|
9a45172232 | ||
|
|
2a92c7d792 | ||
|
|
f5975cf870 | ||
|
|
a67aa85820 | ||
|
|
c5cabe1691 | ||
|
|
cbed41cd82 | ||
|
|
6684771cbf | ||
|
|
f5f894ea47 | ||
|
|
a0a334fc8d | ||
|
|
ae12e94b1f | ||
|
|
9ed971cc90 | ||
|
|
129cb23078 | ||
|
|
d22812e917 | ||
|
|
10d712824a | ||
|
|
de4059c8fa | ||
|
|
416aff86e9 | ||
|
|
59dc7f178f | ||
|
|
a3c9f8bfee | ||
|
|
5546c65491 | ||
|
|
f2179f015c | ||
|
|
9a53d84b9c | ||
|
|
0a8ff7899c | ||
|
|
f9d7877d8b | ||
|
|
9fe9976591 | ||
|
|
de1824f71d | ||
|
|
19a9296c47 | ||
|
|
49967f3d45 | ||
|
|
978b6254c7 | ||
|
|
1afd143810 | ||
|
|
e5cd7f0a3a | ||
|
|
51d3940c63 | ||
|
|
179aec1578 | ||
|
|
af0014aba8 | ||
|
|
da3d995709 | ||
|
|
04c4269db3 | ||
|
|
78f238294f | ||
|
|
354d0468c1 | ||
|
|
4efcc344c3 | ||
|
|
5818b58350 | ||
|
|
7941129cc4 | ||
|
|
069d71a840 | ||
|
|
08027e7a79 | ||
|
|
ead302981c | ||
|
|
fe0ffa14ff | ||
|
|
821b8e70a8 | ||
|
|
8ceda54c7d | ||
|
|
84e515bd6e | ||
|
|
dea1df6878 | ||
|
|
0076ec2e8d | ||
|
|
82c9671f79 | ||
|
|
d364a1122e | ||
|
|
fb570e94e7 | ||
|
|
6e3c830cd2 | ||
|
|
d7db7fc132 | ||
|
|
ff1550bb38 | ||
|
|
976001e474 | ||
|
|
531dd6fb4f | ||
|
|
ba035f2a76 | ||
|
|
d34675d3c9 | ||
|
|
ce95adc66c | ||
|
|
397fe8e395 | ||
|
|
111266d832 | ||
|
|
19d858f9b6 | ||
|
|
79690724d8 | ||
|
|
5ed87ffcb9 | ||
|
|
b99cb6323f | ||
|
|
debf3d8a8a | ||
|
|
4811e52af3 | ||
|
|
8d81730ec2 | ||
|
|
330a85c25c | ||
|
|
3a21116307 | ||
|
|
247d168af6 | ||
|
|
b2a8a283c7 | ||
|
|
c36ddce36f | ||
|
|
c35d9cff7d | ||
|
|
549ce3cf6c | ||
|
|
575bc0768c | ||
|
|
89334e881e | ||
|
|
dcec6354f5 | ||
|
|
16d338da84 | ||
|
|
27258f7207 | ||
|
|
4d2d6a5ced | ||
|
|
0c00b203e6 | ||
|
|
3b68dcdd81 | ||
|
|
39db026161 | ||
|
|
f6c589c606 | ||
|
|
2bd29c3172 | ||
|
|
4a61f53b85 | ||
|
|
adc9ad28da | ||
|
|
585cfaef8b | ||
|
|
b5cd8880b1 | ||
|
|
44ddab881e | ||
|
|
bfa287b66d | ||
|
|
243e52fa11 | ||
|
|
c166eaba6d | ||
|
|
09194c24f2 | ||
|
|
ec521e47aa | ||
|
|
e3f4a51c18 | ||
|
|
0a06fd6f63 | ||
|
|
70eace5290 | ||
|
|
40f9f254a9 | ||
|
|
15d6c17390 | ||
|
|
a9d1d42436 | ||
|
|
1ecfa38eee | ||
|
|
54fd92b7dd | ||
|
|
835906d392 | ||
|
|
1721e6a1ed | ||
|
|
c7ee3b833f | ||
|
|
ffb6e28ca7 | ||
|
|
a4c6846851 | ||
|
|
d18c0bf694 | ||
|
|
4e3f9854e6 | ||
|
|
b27943423e | ||
|
|
894a1016bc | ||
|
|
efe6cddd34 | ||
|
|
f1c6bdf3e8 | ||
|
|
710659bcf5 | ||
|
|
be67775da4 | ||
|
|
2c6381499c | ||
|
|
4df842e78c | ||
|
|
b81696fb64 | ||
|
|
d226d841a1 | ||
|
|
c6d83047e5 | ||
|
|
46dabccdf1 | ||
|
|
cd9517b679 | ||
|
|
cd6677ba1d | ||
|
|
9c1a47acf7 | ||
|
|
0c280a3ce1 | ||
|
|
53e8b6e705 | ||
|
|
ad33165fa7 | ||
|
|
2055db61c8 | ||
|
|
d2c662e54f | ||
|
|
d24b58ef3f | ||
|
|
06ae9b0f3b | ||
|
|
2a9c1c06a4 | ||
|
|
90ad1b7f22 | ||
|
|
f22fbcd1af | ||
|
|
1d761684c5 | ||
|
|
e491770f1c | ||
|
|
a41be61506 | ||
|
|
1a8f633611 | ||
|
|
af8fe918d8 | ||
|
|
8ef9dfd9a2 | ||
|
|
66df24040f | ||
|
|
ed4442d9ea | ||
|
|
0edb5d5ebb | ||
|
|
9ffc2c7ca3 | ||
|
|
93cb3758b5 | ||
|
|
d22e75dcdd | ||
|
|
a1b2a6fe2c | ||
|
|
e15cba0c8c | ||
|
|
31fd207ba2 | ||
|
|
ba6d1b8772 | ||
|
|
0dce561ec9 | ||
|
|
376142eb0d | ||
|
|
664ee1f483 | ||
|
|
dac5b6fde1 | ||
|
|
998c57442b | ||
|
|
4a0ab6c926 | ||
|
|
f43e82f17f | ||
|
|
62238620a5 | ||
|
|
200745011a | ||
|
|
82fd88339b | ||
|
|
de0f2efbfb | ||
|
|
29cf28d845 | ||
|
|
7e4dbb5f3b | ||
|
|
923c3a814d | ||
|
|
779e3cc5b5 | ||
|
|
3f3d1ef8f5 | ||
|
|
f92f9f137a | ||
|
|
87f7f436e8 | ||
|
|
4298c0b1eb | ||
|
|
6c104d771e | ||
|
|
aefb9a5bc4 | ||
|
|
8868d7cbb8 | ||
|
|
10cbac20f9 | ||
|
|
26bcd0c90d | ||
|
|
fbece2bb67 | ||
|
|
0012183ede | ||
|
|
8916cbc6ab | ||
|
|
21ce70054f | ||
|
|
3ba82b6d87 | ||
|
|
e771c5d057 | ||
|
|
4e5e925e39 | ||
|
|
b7248d4115 | ||
|
|
639253840f | ||
|
|
710ebdf9c1 | ||
|
|
bb64d84ce4 | ||
|
|
cd1da27ff2 | ||
|
|
c1accc2e5b | ||
|
|
e4489dcbc1 | ||
|
|
c0d407f7ce | ||
|
|
461115afde | ||
|
|
bae1965231 | ||
|
|
b89c77ec9a | ||
|
|
1ca5f09d7b | ||
|
|
d79902ae59 | ||
|
|
77568e114f | ||
|
|
a24d274a3c | ||
|
|
dac81432d6 | ||
|
|
309b5081ef | ||
|
|
91bc4f2671 | ||
|
|
4c9d37d919 | ||
|
|
7e9566f66a | ||
|
|
3f7e8a475d | ||
|
|
1cf7c0f334 | ||
|
|
ff8ee9ee4e | ||
|
|
cbbd939a94 | ||
|
|
f232df2887 | ||
|
|
16bfb2c80c | ||
|
|
0ba066123e | ||
|
|
81c51c26cc | ||
|
|
6fa8295ac5 | ||
|
|
f975b40236 | ||
|
|
01d9d9c8c8 | ||
|
|
1eafc4e5d9 | ||
|
|
38e4020aa8 | ||
|
|
ac32fbb3b2 | ||
|
|
7d26eca5cc | ||
|
|
3347d61591 | ||
|
|
9abf2c8c9c | ||
|
|
84e2262ad6 | ||
|
|
378137d34a | ||
|
|
66ca16f836 | ||
|
|
282884ad83 | ||
|
|
7877ac42f0 | ||
|
|
19ef8891e3 | ||
|
|
bfea9e53a6 | ||
|
|
a2420026ab | ||
|
|
1be1991299 | ||
|
|
67dd7e1923 | ||
|
|
2b584586ed | ||
|
|
a1994ff0ab | ||
|
|
ca0e858871 | ||
|
|
06c6615507 | ||
|
|
818d0be436 | ||
|
|
fcd2baa945 | ||
|
|
62e0a2824a | ||
|
|
bbe1721a18 | ||
|
|
c1470a51b8 | ||
|
|
6ee31d5dc5 | ||
|
|
65d74387e7 | ||
|
|
7d0ea599c4 | ||
|
|
b7795a3dea | ||
|
|
323f6f6202 | ||
|
|
0c61223884 | ||
|
|
32234be7a2 | ||
|
|
178b49832e | ||
|
|
18cbb4a84d | ||
|
|
e84afe196a | ||
|
|
e1e171a3c4 | ||
|
|
d075c00015 | ||
|
|
6c0ca4a64a | ||
|
|
6b5d461411 | ||
|
|
7419e0dde1 | ||
|
|
cf2bb5e40e | ||
|
|
f466e94d65 | ||
|
|
eb0257d48f | ||
|
|
b83dd6c6b4 | ||
|
|
51c207448d | ||
|
|
a6a558da30 | ||
|
|
2bf5fa27be | ||
|
|
af7940746f | ||
|
|
a2aa1a156c | ||
|
|
2f8a72a42a | ||
|
|
8179ca5eaa | ||
|
|
4b74f882c7 | ||
|
|
7cf45af502 | ||
|
|
46c21158d8 | ||
|
|
80da0776f8 | ||
|
|
e91f10ab16 | ||
|
|
2c15cd7923 | ||
|
|
d6584543e9 | ||
|
|
c13228f346 | ||
|
|
7220d8233e | ||
|
|
0237bf09bf | ||
|
|
04017c25bb | ||
|
|
02199cd609 | ||
|
|
26b9f5831a | ||
|
|
243a76002c | ||
|
|
c71e4ddee4 | ||
|
|
32eb8c1be9 | ||
|
|
c587017830 | ||
|
|
fb885652cc | ||
|
|
afc2f05e5e | ||
|
|
06547d0cbe | ||
|
|
578108280e |
20
.editorconfig
Normal file
20
.editorconfig
Normal file
@@ -0,0 +1,20 @@
|
||||
root = true
|
||||
|
||||
[*.{sh,bash}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
binary_next_line = false
|
||||
switch_case_indent = true
|
||||
space_redirects = true
|
||||
function_next_line = false
|
||||
|
||||
# also bash scripts.
|
||||
[{install,uninstall,bin/fzf-preview.sh,bin/fzf-tmux}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
simplify = true
|
||||
binary_next_line = false
|
||||
switch_case_indent = true
|
||||
space_redirects = true
|
||||
function_next_line = false
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -27,18 +27,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@v4
|
||||
|
||||
2
.github/workflows/depsreview.yaml
vendored
2
.github/workflows/depsreview.yaml
vendored
@@ -9,6 +9,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
|
||||
20
.github/workflows/linux.yml
vendored
20
.github/workflows/linux.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Test fzf on Linux
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -16,33 +16,33 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.20"
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.0
|
||||
ruby-version: 3.4.6
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
run: sudo apt-get install --yes zsh fish tmux shfmt
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.25.1 rubocop:1.65.0 rubocop-minitest:0.35.1 rubocop-performance:1.21.1
|
||||
run: bundle install
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
run: make lint
|
||||
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||
|
||||
8
.github/workflows/macos.yml
vendored
8
.github/workflows/macos.yml
vendored
@@ -15,14 +15,14 @@ jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: "1.20"
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
- name: Install packages
|
||||
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
|
||||
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux shfmt
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
|
||||
2
.github/workflows/sponsors.yml
vendored
2
.github/workflows/sponsors.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
|
||||
4
.github/workflows/typos.yml
vendored
4
.github/workflows/typos.yml
vendored
@@ -6,5 +6,5 @@ jobs:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@v1.28.4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: crate-ci/typos@v1.29.4
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,6 @@ bin/fzf.exe
|
||||
dist
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
@@ -12,3 +11,4 @@ gopath
|
||||
fzf
|
||||
tmp
|
||||
*.patch
|
||||
.idea
|
||||
|
||||
@@ -14,6 +14,7 @@ builds:
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
- android
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
@@ -22,9 +23,9 @@ builds:
|
||||
- ppc64le
|
||||
- s390x
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
@@ -38,6 +39,10 @@ builds:
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
- goos: android
|
||||
goarch: arm
|
||||
|
||||
# .goreleaser.yaml
|
||||
notarize:
|
||||
|
||||
10
.rubocop.yml
10
.rubocop.yml
@@ -1,9 +1,13 @@
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Metrics:
|
||||
Enabled: false
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
AllowedMethods:
|
||||
@@ -28,5 +32,11 @@ Style/WordArray:
|
||||
MinSize: 1
|
||||
Minitest/AssertEqual:
|
||||
Enabled: false
|
||||
Minitest/EmptyLineBeforeAssertionMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Enabled: false
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
golang 1.20.13
|
||||
golang 1.23
|
||||
ruby 3.4
|
||||
shfmt 3.12
|
||||
|
||||
48
ADVANCED.md
48
ADVANCED.md
@@ -1,8 +1,8 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
* *Last update: 2024/06/24*
|
||||
* *Requires fzf 0.54.0 or later*
|
||||
* *Last update: 2025/02/02*
|
||||
* *Requires fzf 0.59.0 or later*
|
||||
|
||||
---
|
||||
|
||||
@@ -22,6 +22,7 @@ Advanced fzf examples
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
@@ -92,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
||||
|
||||

|
||||
|
||||
*(See `Layout` section of the man page to see the full list of options)*
|
||||
*(See man page to see the full list of options)*
|
||||
|
||||
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||
@@ -500,6 +501,44 @@ fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
### Controlling Ripgrep search and fzf search simultaneously
|
||||
|
||||
`search` and `transform-search` action allow you to trigger an fzf search with
|
||||
an arbitrary query string. This frees fzf from strictly following the prompt
|
||||
input, enabling custom search syntax.
|
||||
|
||||
In the example below, `transform` action is used to conditionally trigger
|
||||
`reload` for ripgrep, followed by `search` for fzf. The first word of the
|
||||
query initiates the Ripgrep process to generate the initial results, while the
|
||||
remainder of the query is passed to fzf for secondary filtering.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
INITIAL_QUERY="${*:-}"
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-line,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
Log tailing
|
||||
-----------
|
||||
|
||||
@@ -529,8 +568,7 @@ pods() {
|
||||
--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 'start,ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
|
||||
16
BUILD.md
16
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.20 or above
|
||||
- Go 1.23 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
@@ -41,6 +41,20 @@ make release
|
||||
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
|
||||
> ```
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
```sh
|
||||
# Run go unit tests
|
||||
make test
|
||||
|
||||
# Run integration tests (requires to be on tmux)
|
||||
make itest
|
||||
|
||||
# Run a single test case
|
||||
ruby test/runner.rb --name test_something
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
|
||||
653
CHANGELOG.md
653
CHANGELOG.md
@@ -1,6 +1,659 @@
|
||||
CHANGELOG
|
||||
=========
|
||||
|
||||
0.66.0
|
||||
------
|
||||
|
||||
### Quick summary
|
||||
|
||||
This version introduces many new features centered around the new "raw" mode.
|
||||
|
||||
| Type | Class | Name | Description |
|
||||
| :-- | :-- | :-- | :-- |
|
||||
| New | Option | `--raw` | Enable raw mode by default |
|
||||
| New | Option | `--gutter CHAR` | Set the gutter column character |
|
||||
| New | Option | `--gutter-raw CHAR` | Set the gutter column character in raw mode |
|
||||
| Enhancement | Option | `--listen SOCKET` | Added support for Unix domain sockets |
|
||||
| New | Action | `toggle-raw` | Toggle raw mode |
|
||||
| New | Action | `enable-raw` | Enable raw mode |
|
||||
| New | Action | `disable-raw` | Disable raw mode |
|
||||
| New | Action | `up-match` | Move up to the matching item |
|
||||
| New | Action | `down-match` | Move down to the matching item |
|
||||
| New | Action | `best` | Move to the matching item with the best score |
|
||||
| New | Color | `nomatch` | Color for non-matching items in raw mode |
|
||||
| New | Env Var | `FZF_RAW` | Matching status in raw mode (0, 1, or undefined) |
|
||||
| New | Env Var | `FZF_DIRECTION` | `up` or `down` depending on the layout |
|
||||
| New | Env Var | `FZF_SOCK` | Path to the Unix domain socket fzf is listening on |
|
||||
| Enhancement | Key | `CTRL-N` | `down` -> `down-match` |
|
||||
| Enhancement | Key | `CTRL-P` | `up` -> `up-match` |
|
||||
| Enhancement | Shell | `CTRL-R` binding | Toggle raw mode with `ALT-R` |
|
||||
| Enhancement | Shell | `CTRL-R` binding | Opt-out with an empty `FZF_CTRL_R_COMMAND` |
|
||||
|
||||
### 1. Introducing "raw" mode
|
||||
|
||||

|
||||
|
||||
This version introduces a new "raw" mode (named so because it shows the list
|
||||
"unfiltered"). In raw mode, non-matching items stay in their original positions,
|
||||
but appear dimmed. This allows you see surrounding items of a match and better
|
||||
understand the context of it. You can enable raw mode by default with `--raw`,
|
||||
but it's often more useful when toggled dynamically with the `toggle-raw`
|
||||
action.
|
||||
|
||||
```sh
|
||||
tree | fzf --reverse --bind alt-r:toggle-raw
|
||||
```
|
||||
|
||||
While non-matching items are displayed in a dimmed color, they are treated just
|
||||
like matching items, so you place the cursor on them and perform any action. If
|
||||
you prefer to navigate only through matching items, use the `down-match` and
|
||||
`up-match` actions, which are from now on bound to `CTRL-N` and `CTRL-P`
|
||||
respectively, and also to `ALT-DOWN` and `ALT-UP`.
|
||||
|
||||
| Key | Action | With `--history` |
|
||||
| :-- | :-- | :-- |
|
||||
| `down` | `down` | |
|
||||
| `up` | `up` | |
|
||||
| `ctrl-j` | `down` | |
|
||||
| `ctrl-k` | `up` | |
|
||||
| `ctrl-n` | `down-match` | `next-history` |
|
||||
| `ctrl-p` | `up-match` | `prev-history` |
|
||||
| `alt-down` | `down-match` | |
|
||||
| `alt-up` | `up-match` | |
|
||||
|
||||
> [!NOTE]
|
||||
> `CTRL-N` and `CTRL-P` are bound to `next-history` and `prev-history` when
|
||||
> `--history` option is enabled, so in that case, you'll need to manually bind
|
||||
> them, or use `ALT-DOWN` and `ALT-UP` instead.
|
||||
|
||||
> [!TIP]
|
||||
> `up-match` and `down-match` are equivalent to `up` and `down` when not in
|
||||
> raw mode, so you can safely bind them to `up` and `arrow` keys if you prefer.
|
||||
> ```sh
|
||||
> fzf --bind up:up-match,down:down-match
|
||||
> ```
|
||||
|
||||
#### Customizing the behavior
|
||||
|
||||
In raw mode, the input list is presented in its original order, unfiltered, and
|
||||
your cursor will not move to the matching item automatically. Here are ways to
|
||||
customize the behavior.
|
||||
|
||||
```sh
|
||||
# When the result list is updated, move the cursor to the item with the best score
|
||||
# (assuming sorting is not disabled)
|
||||
fzf --raw --bind result:best
|
||||
|
||||
# Move to the first matching item in the original list
|
||||
# - $FZF_RAW is set to 0 when raw mode is enabled and the current item is a non-match
|
||||
# - $FZF_DIRECTION is set to either 'up' or 'down' depending on the layout direction
|
||||
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
|
||||
```
|
||||
|
||||
#### Customizing the look
|
||||
|
||||
##### Gutter
|
||||
|
||||
To make the mode visually distinct, the gutter column is rendered in a dashed
|
||||
line using `▖` character. But you can customize it with the `--gutter-raw CHAR`
|
||||
option.
|
||||
|
||||
```sh
|
||||
# Use a thinner gutter instead of the default dashed line
|
||||
fzf --bind alt-r:toggle-raw --gutter-raw ▎
|
||||
```
|
||||
|
||||
##### Color and style of non-matching items
|
||||
|
||||
Non-matching items are displayed in a dimmed color by default, but you can
|
||||
change it with the `--color nomatch:...` option.
|
||||
|
||||
```sh
|
||||
fzf --raw --color nomatch:red
|
||||
fzf --raw --color nomatch:red:dim
|
||||
fzf --raw --color nomatch:red:dim:strikethrough
|
||||
fzf --raw --color nomatch:red:dim:strikethrough:italic
|
||||
```
|
||||
|
||||
For colored input, dimming alone may not be enough, and you may prefer to remove
|
||||
colors entirely. For that case, a new special style attribute `strip` has been
|
||||
added.
|
||||
|
||||
```sh
|
||||
fd --color always | fzf --ansi --raw --color nomatch:dim:strip:strikethrough
|
||||
```
|
||||
|
||||
#### Conditional actions for raw mode
|
||||
|
||||
You may want to perform different actions depending on whether the current item
|
||||
is a match or not. For that, fzf now exports `$FZF_RAW` environment variable.
|
||||
|
||||
It's:
|
||||
|
||||
- Undefined if raw mode is disabled
|
||||
- `1` if the current item is a match
|
||||
- `0` otherwise
|
||||
|
||||
```sh
|
||||
# Do not allow selecting non-matching items
|
||||
fzf --raw --bind 'enter:transform:[[ ${FZF_RAW-1} = 1 ]] && echo accept || echo bell'
|
||||
```
|
||||
|
||||
#### Leveraging raw mode in shell integration
|
||||
|
||||
The `CTRL-R` binding (command history) now lets you toggle raw mode with `ALT-R`.
|
||||
|
||||
### 2. Style changes
|
||||
|
||||
The screenshot on the right shows the updated gutter style:
|
||||
|
||||

|
||||
|
||||
This version includes a few minor updates to fzf's classic visual style:
|
||||
|
||||
- The gutter column is now narrower, rendered with the left-half block character (`▌`).
|
||||
- Markers no longer use background colors.
|
||||
- The `--color base16` theme (alias: `16`) has been updated for better compatibility with both dark and light themes.
|
||||
|
||||
### 3. `--listen` now supports Unix domain sockets
|
||||
|
||||
If an argument to `--listen` ends with `.sock`, fzf will listen on a Unix
|
||||
domain socket at the specified path.
|
||||
|
||||
```sh
|
||||
fzf --listen /tmp/fzf.sock --no-tmux
|
||||
|
||||
# GET
|
||||
curl --unix-socket /tmp/fzf.sock http
|
||||
|
||||
# POST
|
||||
curl --unix-socket /tmp/fzf.sock http -d up
|
||||
```
|
||||
|
||||
Note that any existing file at the given path will be removed before creating
|
||||
the socket, so avoid using an important file path.
|
||||
|
||||
### 4. Added options
|
||||
|
||||
#### `--gutter CHAR`
|
||||
|
||||
The gutter column can now be customized using `--gutter CHAR` and styled with
|
||||
`--color gutter:...`. Examples:
|
||||
|
||||
```sh
|
||||
# Right-aligned gutter
|
||||
fzf --gutter '▐'
|
||||
|
||||
# Even thinner gutter
|
||||
fzf --gutter '▎'
|
||||
|
||||
# Yellow checker pattern
|
||||
fzf --gutter '▚' --color gutter:yellow
|
||||
|
||||
# Classic style
|
||||
fzf --gutter ' ' --color gutter:reverse
|
||||
```
|
||||
|
||||
#### `--gutter-raw CHAR`
|
||||
|
||||
As noted above, the `--gutter-raw CHAR` option was also added for customizing the gutter column in raw mode.
|
||||
|
||||
### 5. Added actions
|
||||
|
||||
The following actions were introduced to support working with raw mode:
|
||||
|
||||
| Action | Description |
|
||||
| :-- | :-- |
|
||||
| `toggle-raw` | Toggle raw mode |
|
||||
| `enable-raw` | Enable raw mode |
|
||||
| `disable-raw` | Disable raw mode |
|
||||
| `up-match` | Move up to the matching item; identical to `up` if raw mode is disabled |
|
||||
| `down-match` | Move down to the matching item; identical to `down` if raw mode is disabled |
|
||||
| `best` | Move to the matching item with the best score; identical to `first` if raw mode is disabled |
|
||||
|
||||
### 6. Added environment variables
|
||||
|
||||
#### `$FZF_DIRECTION`
|
||||
|
||||
`$FZF_DIRECTION` is now exported to child processes, indicating the list direction of the current layout:
|
||||
|
||||
- `up` for the default layout
|
||||
- `down` for `reverse` or `reverse-list`
|
||||
|
||||
This simplifies writing transform actions involving layout-dependent actions
|
||||
like `{up,down}-match`, `{up,down}-selected`, and `toggle+{up,down}`.
|
||||
|
||||
```sh
|
||||
fzf --raw --bind 'result:first+transform:[[ $FZF_RAW = 0 ]] && echo $FZF_DIRECTION-match'
|
||||
```
|
||||
|
||||
#### `$FZF_SOCK`
|
||||
|
||||
When fzf is listening on a Unix domain socket using `--listen`, the path to the
|
||||
socket is exported as `$FZF_SOCK`, analogous to `$FZF_PORT` for TCP sockets.
|
||||
|
||||
#### `$FZF_RAW`
|
||||
|
||||
As described above, `$FZF_RAW` is now exported to child processes in raw mode,
|
||||
indicating whether the current item is a match (`1`) or not (`0`). It is not
|
||||
defined when not in raw mode.
|
||||
|
||||
#### `$FZF_CTRL_R_COMMAND`
|
||||
|
||||
You can opt-out `CTRL-R` binding from the shell integration by setting
|
||||
`FZF_CTRL_R_COMMAND` to an empty string. Setting it to any other value is not
|
||||
supported and will result in a warning.
|
||||
|
||||
```sh
|
||||
# Disable the CTRL-R binding from the shell integration
|
||||
FZF_CTRL_R_COMMAND= eval "$(fzf --bash)"
|
||||
```
|
||||
|
||||
### 7. Added key support for `--bind`
|
||||
|
||||
Pull request [#3996](https://github.com/junegunn/fzf/pull/3996) added support
|
||||
for many additional keys for `--bind` option, such as `ctrl-backspace`.
|
||||
|
||||
### 8. Breaking changes
|
||||
|
||||
#### Hiding the gutter column
|
||||
|
||||
In the previous versions, the recommended way to hide the gutter column was to
|
||||
set `--color gutter:-1`. That's because the gutter column was just a space
|
||||
character, reversed. But now that it's using a visible character (`▌`), applying
|
||||
the default color is no longer enough to hide it. Instead, you can set it to
|
||||
a space character.
|
||||
|
||||
```sh
|
||||
# Hide the gutter column
|
||||
fzf --gutter ' '
|
||||
|
||||
# Classic style
|
||||
fzf --gutter ' ' --color gutter:reverse
|
||||
```
|
||||
|
||||
#### `--color` option
|
||||
|
||||
In the previous versions, some elements had default style attributes applied and
|
||||
you would have to explicitly unset them with `regular` attribute if you wanted
|
||||
to reset them. This is no longer needed now, as the default style attributes
|
||||
are applied only when you do not specify any color or style for that element.
|
||||
|
||||
```sh
|
||||
# No 'dim', just red and italic.
|
||||
fzf --ghost 'Type to search' --color ghost:red:italic
|
||||
```
|
||||
|
||||
#### Compatibility changes
|
||||
|
||||
Starting with this release, fzf is built with Go 1.23. Support for some old OS versions has been dropped.
|
||||
|
||||
See https://go.dev/wiki/MinimumRequirements.
|
||||
|
||||
0.65.2
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- Fix incorrect truncation of `--info-command` with `--info=inline-right` (#4479)
|
||||
- [install] Support old uname in macOS (#4492)
|
||||
- [bash 3] Fix `CTRL-T` and `ALT-C` to preserve the last yank (#4496)
|
||||
- Do not unset `FZF_DEFAULT_*` variables when using winpty (#4497) (#4400)
|
||||
- Fix rendering of items with tabs when using a non-default ellipsis (#4505)
|
||||
- **This is the final release to support Windows 7.**
|
||||
- Future versions will be built with the latest Go toolchain, which has dropped support for Windows 7.
|
||||
|
||||
0.65.1
|
||||
------
|
||||
- Fixed incorrect `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_FOOTER_WORD` when the header or footer contains ANSI escape sequences and tab characters.
|
||||
- Fixed a bug where you cannot unset the default `--nth` using `change-nth` action.
|
||||
- Fixed a highlighting bug when using `--color fg:dim,nth:regular` pattern over ANSI-colored items.
|
||||
|
||||
0.65.0
|
||||
------
|
||||
- Added `click-footer` event that is triggered when the footer section is clicked. When the event is triggered, the following environment variables are set:
|
||||
- `$FZF_CLICK_FOOTER_COLUMN` - clicked column (1-based)
|
||||
- `$FZF_CLICK_FOOTER_LINE` - clicked line (1-based)
|
||||
- `$FZF_CLICK_FOOTER_WORD` - the word under the cursor
|
||||
```sh
|
||||
fzf --footer $'[Edit] [View]\n[Copy to clipboard]' \
|
||||
--with-shell 'bash -c' \
|
||||
--bind 'click-footer:transform:
|
||||
[[ $FZF_CLICK_FOOTER_WORD =~ Edit ]] && echo "execute:vim \{}"
|
||||
[[ $FZF_CLICK_FOOTER_WORD =~ View ]] && echo "execute:view \{}"
|
||||
(( FZF_CLICK_FOOTER_LINE == 2 )) && (( FZF_CLICK_FOOTER_COLUMN < 20 )) &&
|
||||
echo "execute-silent(echo -n \{} | pbcopy)+bell"
|
||||
'
|
||||
```
|
||||
- Added `trigger(...)` action that triggers events bound to another key or event.
|
||||
```sh
|
||||
# You can click on each key name to trigger the actions bound to that key
|
||||
fzf --footer 'Ctrl-E: Edit / Ctrl-V: View / Ctrl-Y: Copy to clipboard' \
|
||||
--with-shell 'bash -c' \
|
||||
--bind 'ctrl-e:execute:vim {}' \
|
||||
--bind 'ctrl-v:execute:view {}' \
|
||||
--bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell' \
|
||||
--bind 'click-footer:transform:
|
||||
[[ $FZF_CLICK_FOOTER_WORD =~ Ctrl ]] && echo "trigger(${FZF_CLICK_FOOTER_WORD%:})"
|
||||
'
|
||||
```
|
||||
- You can specify a series of keys and events
|
||||
```sh
|
||||
fzf --bind 'a:up,b:trigger(a,a,a)'
|
||||
```
|
||||
- Added support for `{*n}` and `{*nf}` placeholder.
|
||||
- `{*n}` evaluates to the zero-based ordinal index of all matched items.
|
||||
- `{*nf}` evaluates to the temporary file containing that.
|
||||
- Bug fixes and improvements
|
||||
- [neovim] Fixed margin background color when `&winborder` is used (#4453)
|
||||
- Fixed rendering error when hiding a preview window without border (#4465)
|
||||
- fix(shell): check for mawk existence before version check (#4468)
|
||||
- Thanks to @LangLangBart and @akinomyoga
|
||||
- Fixed `--no-header-lines-border` behavior (08027e7a)
|
||||
|
||||
0.64.0
|
||||
------
|
||||
- Added `multi` event that is triggered when the multi-selection has changed.
|
||||
```sh
|
||||
fzf --multi \
|
||||
--bind 'ctrl-a:select-all,ctrl-d:deselect-all' \
|
||||
--bind 'multi:transform-footer:(( FZF_SELECT_COUNT )) && echo "Selected $FZF_SELECT_COUNT item(s)"'
|
||||
```
|
||||
- [Halfwidth and fullwidth alphanumeric and punctuation characters](https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)) are now internally normalized to their ASCII equivalents to allow matching with ASCII queries.
|
||||
```sh
|
||||
echo ABC| fzf -q abc
|
||||
```
|
||||
- Renamed `clear-selection` action to `clear-multi` for consistency.
|
||||
- `clear-selection` remains supported as an alias for backward compatibility.
|
||||
- Bug fixes
|
||||
- Fixed a bug that could cause fzf to abort due to incorrect update ordering.
|
||||
- Fixed a bug where some multi-selections were lost when using `exclude` or `change-nth`.
|
||||
|
||||
0.63.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.63.0/_
|
||||
|
||||
- Added footer. The default border style for footer is `line`, which draws a single separator line.
|
||||
```sh
|
||||
fzf --reverse --footer "fzf: friend zone forever"
|
||||
```
|
||||
- Options
|
||||
- `--footer[=STRING]`
|
||||
- `--footer-border[=STYLE]`
|
||||
- `--footer-label=LABEL`
|
||||
- `--footer-label-pos=COL[:bottom]`
|
||||
- Colors
|
||||
- `footer`
|
||||
- `footer-bg`
|
||||
- `footer-border`
|
||||
- `footer-label`
|
||||
- Actions
|
||||
- `change-footer`
|
||||
- `transform-footer`
|
||||
- `bg-transform-footer`
|
||||
- `change-footer-label`
|
||||
- `transform-footer-label`
|
||||
- `bg-transform-footer-label`
|
||||
- `line` border style is now allowed for all types of border except for `--list-border`.
|
||||
```sh
|
||||
fzf --height 50% --style full:line --preview 'cat {}' \
|
||||
--bind 'focus:bg-transform-header(file {})+bg-transform-footer(wc {})'
|
||||
```
|
||||
- Added `{*}` placeholder flag that evaluates to all matched items.
|
||||
```bash
|
||||
seq 10000 | fzf --preview "awk '{sum += \$1} END {print sum}' {*f}"
|
||||
```
|
||||
- Use this with caution, as it can make fzf sluggish for large lists.
|
||||
- Added asynchronous transform actions with `bg-` prefix that run asynchronously in the background, along with `bg-cancel` action to cancel currently running `bg-transform` actions.
|
||||
```sh
|
||||
# Implement popup that disappears after 1 second
|
||||
# * Use footer as the popup
|
||||
# * Use `bell` to ring the terminal bell
|
||||
# * Use `bg-transform-footer` to clear the footer after 1 second
|
||||
# * Use `bg-cancel` to cancel currently running background transform actions
|
||||
fzf --multi --list-border \
|
||||
--bind 'enter:execute-silent(echo -n {+} | pbcopy)+bell' \
|
||||
--bind 'enter:+transform-footer(echo Copied {} to clipboard)' \
|
||||
--bind 'enter:+bg-cancel+bg-transform-footer(sleep 1)'
|
||||
|
||||
# It's okay for the commands to take a little while because they run in the background
|
||||
GETTER='curl -s http://metaphorpsum.com/sentences/1'
|
||||
fzf --style full --border --preview : \
|
||||
--bind "focus:bg-transform-header:$GETTER" \
|
||||
--bind "focus:+bg-transform-footer:$GETTER" \
|
||||
--bind "focus:+bg-transform-border-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-preview-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-input-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-list-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-header-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-footer-label:$GETTER" \
|
||||
--bind "focus:+bg-transform-ghost:$GETTER" \
|
||||
--bind "focus:+bg-transform-prompt:$GETTER"
|
||||
```
|
||||
- Added support for full-line background color in the list section
|
||||
```sh
|
||||
for i in $(seq 16 255); do
|
||||
echo -e "\x1b[48;5;${i}m\x1b[0Khello"
|
||||
done | fzf --ansi
|
||||
```
|
||||
- SSH completion enhancements by @akinomyoga
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.62.0
|
||||
------
|
||||
- Relaxed the `--color` option syntax to allow whitespace-separated entries (in addition to commas), making multi-line definitions easier to write and read
|
||||
```sh
|
||||
# seoul256-light
|
||||
fzf --style full --color='
|
||||
fg:#616161 fg+:#616161
|
||||
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
|
||||
hl:#719872 hl+:#719899
|
||||
pointer:#e12672 marker:#e17899
|
||||
header:#719872
|
||||
spinner:#719899 info:#727100
|
||||
prompt:#0099bd query:#616161
|
||||
border:#e1e1e1
|
||||
'
|
||||
```
|
||||
- Added `alt-bg` color to create striped lines to visually separate rows
|
||||
```sh
|
||||
fzf --color bg:237,alt-bg:238,current-bg:236 --highlight-line
|
||||
|
||||
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||
bat --plain --language bash --color always |
|
||||
fzf --read0 --ansi --reverse --multi \
|
||||
--color bg:237,alt-bg:238,current-bg:236 --highlight-line
|
||||
```
|
||||
- [fish] Improvements in CTRL-R binding (@bitraid)
|
||||
- You can trigger CTRL-R in the middle of a command to insert the selected item
|
||||
- You can delete history items with SHIFT-DEL
|
||||
- Bug fixes and improvements
|
||||
- Fixed unnecessary 100ms delay after `reload` (#4364)
|
||||
- Fixed `selected-bg` not applied to colored items (#4372)
|
||||
|
||||
0.61.3
|
||||
------
|
||||
- Reverted #4351 as it caused `tmux run-shell 'fzf --tmux'` to fail (#4559 #4560)
|
||||
- More environment variables for child processes (#4356)
|
||||
|
||||
0.61.2
|
||||
------
|
||||
- Fixed panic when using header border without pointer/marker (@phanen)
|
||||
- Fixed `--tmux` option when already inside a tmux popup (@peikk0)
|
||||
- Bug fixes and improvements in CTRL-T binding of fish (#4334) (@bitraid)
|
||||
- Added `--no-tty-default` option to make fzf search for the current TTY device instead of defaulting to `/dev/tty` (#4242)
|
||||
|
||||
0.61.1
|
||||
------
|
||||
- Disable bracketed-paste mode on exit. This fixes issue where pasting breaks after running fzf on old bash versions that don't support the mode.
|
||||
|
||||
0.61.0
|
||||
------
|
||||
- Added `--ghost=TEXT` to display a ghost text when the input is empty
|
||||
```sh
|
||||
# Display "Type to search" when the input is empty
|
||||
fzf --ghost "Type to search"
|
||||
```
|
||||
- Added `change-ghost` and `transform-ghost` actions for dynamically changing the ghost text
|
||||
- Added `change-pointer` and `transform-pointer` actions for dynamically changing the pointer sign
|
||||
- Added `r` flag for placeholder expression (raw mode) for unquoted output
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.60.3
|
||||
------
|
||||
- Bug fixes and improvements
|
||||
- [fish] Enable multiple history commands insertion (#4280) (@bitraid)
|
||||
- [walker] Append '/' to directory entries on MSYS2 (#4281)
|
||||
- Trim trailing whitespaces after processing ANSI sequences (#4282)
|
||||
- Remove temp files before `become` when using `--tmux` option (#4283)
|
||||
- Fix condition for using item numlines cache (#4285) (@alex-huff)
|
||||
- Make `--accept-nth` compatible with `--select-1` (#4287)
|
||||
- Increase the query length limit from 300 to 1000 (#4292)
|
||||
- [windows] Prevent fzf from consuming user input while paused (#4260)
|
||||
|
||||
0.60.2
|
||||
------
|
||||
- Template for `--with-nth` and `--accept-nth` now supports `{n}` which evaluates to the zero-based ordinal index of the item
|
||||
- Fixed a regression that caused the last field in the "nth" expression to be trimmed when a regular expression delimiter is used
|
||||
- Thanks to @phanen for the fix
|
||||
- Fixed 'jump' action when the pointer is an empty string
|
||||
|
||||
0.60.1
|
||||
------
|
||||
- Bug fixes and minor improvements
|
||||
- Built-in walker now prints directory entries with a trailing slash
|
||||
- Fixed a bug causing unexpected behavior with [fzf-tab](https://github.com/Aloxaf/fzf-tab). Please upgrade if you use it.
|
||||
- Thanks to @alexeisersun, @bitraid, @Lompik, and @fsc0 for the contributions
|
||||
|
||||
0.60.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.60.0/_
|
||||
|
||||
- Added `--accept-nth` for choosing output fields
|
||||
```sh
|
||||
ps -ef | fzf --multi --header-lines 1 | awk '{print $2}'
|
||||
# Becomes
|
||||
ps -ef | fzf --multi --header-lines 1 --accept-nth 2
|
||||
|
||||
git branch | fzf | cut -c3-
|
||||
# Can be rewritten as
|
||||
git branch | fzf --accept-nth -1
|
||||
```
|
||||
- `--accept-nth` and `--with-nth` now support a template that includes multiple field index expressions in curly braces
|
||||
```sh
|
||||
echo foo,bar,baz | fzf --delimiter , --accept-nth '{1}, {3}, {2}'
|
||||
# foo, baz, bar
|
||||
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{1},{3},{2},{1..2}'
|
||||
# foo,baz,bar,foo,bar
|
||||
```
|
||||
- Added `exclude` and `exclude-multi` actions for dynamically excluding items
|
||||
```sh
|
||||
seq 100 | fzf --bind 'ctrl-x:exclude'
|
||||
|
||||
# 'exclude-multi' will exclude the selected items or the current item
|
||||
seq 100 | fzf --multi --bind 'ctrl-x:exclude-multi'
|
||||
```
|
||||
- Preview window now prints wrap indicator when wrapping is enabled
|
||||
```sh
|
||||
seq 100 | xargs | fzf --wrap --preview 'echo {}' --preview-window wrap
|
||||
```
|
||||
- Bug fixes and improvements
|
||||
|
||||
0.59.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.59.0/_
|
||||
|
||||
- Prioritizing file name matches (#4192)
|
||||
- Added a new tiebreak option `pathname` for prioritizing file name matches
|
||||
- `--scheme=path` now sets `--tiebreak=pathname,length`
|
||||
- fzf will automatically choose `path` scheme
|
||||
* when the input is a TTY device, where fzf would start its built-in walker or run `$FZF_DEFAULT_COMMAND` which is usually a command for listing files,
|
||||
* but not when `reload` or `transform` action is bound to `start` event, because in that case, fzf can't be sure of the input type.
|
||||
- Added `--header-lines-border` to display header from `--header-lines` with a separate border
|
||||
```sh
|
||||
# Use --header-lines-border to separate two headers
|
||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||
--bind 'ctrl-r:reload(ps -ef)' --header 'Press CTRL-R to reload' \
|
||||
--header-lines-border bottom --no-list-border
|
||||
```
|
||||
- `click-header` event now sets `$FZF_CLICK_HEADER_WORD` and `$FZF_CLICK_HEADER_NTH`. You can use them to implement a clickable header for changing the search scope using the new `transform-nth` action.
|
||||
```sh
|
||||
# Click on the header line to limit search scope
|
||||
ps -ef | fzf --style full --layout reverse --header-lines 1 \
|
||||
--header-lines-border bottom --no-list-border \
|
||||
--color fg:dim,nth:regular \
|
||||
--bind 'click-header:transform-nth(
|
||||
echo $FZF_CLICK_HEADER_NTH
|
||||
)+transform-prompt(
|
||||
echo "$FZF_CLICK_HEADER_WORD> "
|
||||
)'
|
||||
```
|
||||
- `$FZF_KEY` was updated to expose the type of the click. e.g. `click`, `ctrl-click`, etc. You can use it to implement a more sophisticated behavior.
|
||||
- `kill` completion for bash and zsh were updated to use this feature
|
||||
- Added `--no-input` option to completely disable and hide the input section
|
||||
```sh
|
||||
# Click header to trigger search
|
||||
fzf --header '[src] [test]' --no-input --layout reverse \
|
||||
--header-border bottom --input-border \
|
||||
--bind 'click-header:transform-search:echo ${FZF_CLICK_HEADER_WORD:1:-1}'
|
||||
|
||||
# Vim-like mode switch
|
||||
fzf --layout reverse-list --no-input \
|
||||
--bind 'j:down,k:up,/:show-input+unbind(j,k,/)' \
|
||||
--bind 'enter,esc,ctrl-c:transform:
|
||||
if [[ $FZF_INPUT_STATE = enabled ]]; then
|
||||
echo "rebind(j,k,/)+hide-input"
|
||||
elif [[ $FZF_KEY = enter ]]; then
|
||||
echo accept
|
||||
else
|
||||
echo abort
|
||||
fi
|
||||
'
|
||||
```
|
||||
- You can later show the input section using `show-input` or `toggle-input` action, and hide it again using `hide-input`, or `toggle-input`.
|
||||
- Extended `{q}` placeholder to support ranges. e.g. `{q:1}`, `{q:2..}`, etc.
|
||||
- Added `search(...)` and `transform-search(...)` action to trigger an fzf search with an arbitrary query string. This can be used to extend the search syntax of fzf. In the following example, fzf will use the first word of the query to trigger ripgrep search, and use the rest of the query to perform fzf search within the result.
|
||||
```sh
|
||||
export TEMP=$(mktemp -u)
|
||||
trap 'rm -f "$TEMP"' EXIT
|
||||
|
||||
TRANSFORMER='
|
||||
rg_pat={q:1} # The first word is passed to ripgrep
|
||||
fzf_pat={q:2..} # The rest are passed to fzf
|
||||
|
||||
if ! [[ -r "$TEMP" ]] || [[ $rg_pat != $(cat "$TEMP") ]]; then
|
||||
echo "$rg_pat" > "$TEMP"
|
||||
printf "reload:sleep 0.1; rg --column --line-number --no-heading --color=always --smart-case %q || true" "$rg_pat"
|
||||
fi
|
||||
echo "+search:$fzf_pat"
|
||||
'
|
||||
fzf --ansi --disabled \
|
||||
--with-shell 'bash -c' \
|
||||
--bind "start,change:transform:$TRANSFORMER"
|
||||
```
|
||||
- You can now bind actions to multiple keys and events at once by writing a comma-separated list of keys and events before the colon
|
||||
```sh
|
||||
# Load 'ps -ef' output on start and reload it on CTRL-R
|
||||
fzf --bind 'start,ctrl-r:reload:ps -ef'
|
||||
```
|
||||
- `--min-height` option now takes a number followed by `+`, which tells fzf to show at least that many items in the list section. The default value is now changed to `10+`.
|
||||
```sh
|
||||
# You will only see the input section which takes 3 lines
|
||||
fzf --style=full --height 1% --min-height 3
|
||||
|
||||
# You will see 3 items in the list section
|
||||
fzf --style full --height 1% --min-height 3+
|
||||
```
|
||||
- Shell integration scripts were updated to use `--min-height 20+` by default
|
||||
- `--header-lines` will be displayed at the top in `reverse-list` layout
|
||||
- Added `bell` action to ring the terminal bell
|
||||
```sh
|
||||
# Press CTRL-Y to copy the current line to the clipboard and ring the bell
|
||||
fzf --bind 'ctrl-y:execute-silent(echo -n {} | pbcopy)+bell'
|
||||
```
|
||||
- Added `toggle-bind` action
|
||||
- Bug fixes and improvements
|
||||
- Fixed fish script to support fish 3.1.2 or later (@bitraid)
|
||||
|
||||
0.58.0
|
||||
------
|
||||
_Release highlights: https://junegunn.github.io/fzf/releases/0.58.0/_
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
FROM ubuntu:24.04
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
|
||||
FROM rubylang/ruby:3.4.1-noble
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
|
||||
RUN gem install --no-document -v 5.22.3 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
@@ -9,4 +9,4 @@ RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
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 ]"]
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
||||
8
Gemfile
Normal file
8
Gemfile
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'minitest', '5.25.4'
|
||||
gem 'rubocop', '1.71.0'
|
||||
gem 'rubocop-minitest', '0.36.0'
|
||||
gem 'rubocop-performance', '1.23.1'
|
||||
47
Gemfile.lock
Normal file
47
Gemfile.lock
Normal file
@@ -0,0 +1,47 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.9.1)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.25.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.71.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.36.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-performance (1.23.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-23
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (= 5.25.4)
|
||||
rubocop (= 1.71.0)
|
||||
rubocop-minitest (= 0.36.0)
|
||||
rubocop-performance (= 1.23.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
23
Makefile
23
Makefile
@@ -5,6 +5,15 @@ MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||
|
||||
BASH_SCRIPTS := $(ROOT_DIR)/bin/fzf-preview.sh \
|
||||
$(ROOT_DIR)/bin/fzf-tmux \
|
||||
$(ROOT_DIR)/install \
|
||||
$(ROOT_DIR)/uninstall \
|
||||
$(ROOT_DIR)/shell/common.sh \
|
||||
$(ROOT_DIR)/shell/update.sh \
|
||||
$(ROOT_DIR)/shell/completion.bash \
|
||||
$(ROOT_DIR)/shell/key-bindings.bash
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
else
|
||||
@@ -82,12 +91,20 @@ test: $(SOURCES)
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
itest:
|
||||
ruby test/runner.rb
|
||||
|
||||
bench:
|
||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||
|
||||
lint: $(SOURCES) test/test_go.rb
|
||||
lint: $(SOURCES) test/*.rb test/lib/*.rb ${BASH_SCRIPTS}
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
rubocop --require rubocop-minitest --require rubocop-performance
|
||||
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||
shell/update.sh --check ${BASH_SCRIPTS}
|
||||
|
||||
fmt: $(SOURCES) $(BASH_SCRIPTS)
|
||||
gofmt -s -w src
|
||||
shell/update.sh ${BASH_SCRIPTS}
|
||||
|
||||
install: bin/fzf
|
||||
|
||||
@@ -186,4 +203,4 @@ update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all generate build release test bench lint install clean docker docker-test update
|
||||
.PHONY: all generate build release test itest bench lint install clean docker docker-test update fmt
|
||||
|
||||
@@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ 'query': ['fg', 'Normal'],
|
||||
\ 'hl': ['fg', 'Comment'],
|
||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||
@@ -492,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
33
SECURITY.md
Normal file
33
SECURITY.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Security Reporting
|
||||
|
||||
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
|
||||
|
||||
## Reporting
|
||||
|
||||
To report a security vulnerability, please provide the following information:
|
||||
|
||||
1. **PROJECT**
|
||||
- https://github.com/junegunn/fzf
|
||||
|
||||
2. **PUBLIC**
|
||||
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
|
||||
- If so, provide relevant links.
|
||||
|
||||
3. **DESCRIPTION**
|
||||
- Provide a detailed description of the security vulnerability.
|
||||
- Include as much information as possible to help us understand and address the issue.
|
||||
|
||||
Send this information, along with any additional relevant details, to <junegunn.c AT gmail DOT com>.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
We kindly ask you to keep the report confidential until a public announcement is made.
|
||||
|
||||
## Notes
|
||||
|
||||
- Vulnerabilities will be handled on a best-effort basis.
|
||||
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
|
||||
- You will be notified via email simultaneously with the public announcement.
|
||||
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
|
||||
|
||||
Thank you for helping to improve the security of our project!
|
||||
@@ -49,23 +49,23 @@ if [[ ! $type =~ image/ ]]; then
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
if [[ $dim == x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && ((FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}'))); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use kitty icat on kitty terminal
|
||||
if [[ $KITTY_WINDOW_ID ]]; then
|
||||
# 1. Use icat (from Kitty) if kitten is installed
|
||||
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitty icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
|
||||
83
bin/fzf-tmux
83
bin/fzf-tmux
@@ -8,7 +8,7 @@ fail() {
|
||||
}
|
||||
|
||||
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
|
||||
[[ -x "$fzf" ]] || fail 'fzf executable not found'
|
||||
[[ -x $fzf ]] || fail 'fzf executable not found'
|
||||
|
||||
args=()
|
||||
opt=""
|
||||
@@ -16,8 +16,8 @@ skip=""
|
||||
swap=""
|
||||
close=""
|
||||
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}")
|
||||
[[ -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")
|
||||
@@ -47,7 +47,7 @@ help() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
arg="$1"
|
||||
shift
|
||||
[[ -z "$skip" ]] && case "$arg" in
|
||||
[[ -z $skip ]] && case "$arg" in
|
||||
-)
|
||||
term=1
|
||||
;;
|
||||
@@ -58,19 +58,19 @@ while [[ $# -gt 0 ]]; do
|
||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||
exit
|
||||
;;
|
||||
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
||||
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
||||
[[ "$opt" =~ "-E" ]] || opt="-E"
|
||||
elif [[ "$arg" =~ ^.[lr] ]]; then
|
||||
-p* | -w* | -h* | -x* | -y* | -d* | -u* | -r* | -l*)
|
||||
if [[ $arg =~ ^-[pwhxy] ]]; then
|
||||
[[ $opt =~ "-E" ]] || opt="-E"
|
||||
elif [[ $arg =~ ^.[lr] ]]; then
|
||||
opt="-h"
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
if [[ $arg =~ ^.l ]]; then
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -L"
|
||||
close="; tmux swap-pane -D"
|
||||
fi
|
||||
else
|
||||
opt=""
|
||||
if [[ "$arg" =~ ^.u ]]; then
|
||||
if [[ $arg =~ ^.u ]]; then
|
||||
opt="$opt -d"
|
||||
swap="; swap-pane -D ; select-pane -U"
|
||||
close="; tmux swap-pane -D"
|
||||
@@ -79,7 +79,7 @@ while [[ $# -gt 0 ]]; do
|
||||
if [[ ${#arg} -gt 2 ]]; then
|
||||
size="${arg:2}"
|
||||
else
|
||||
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then
|
||||
if [[ $1 =~ ^[0-9%,]+$ ]] || [[ $1 =~ ^[A-Z]$ ]]; then
|
||||
size="$1"
|
||||
shift
|
||||
else
|
||||
@@ -87,37 +87,37 @@ while [[ $# -gt 0 ]]; do
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$arg" =~ ^-p ]]; then
|
||||
if [[ -n "$size" ]]; then
|
||||
if [[ $arg =~ ^-p ]]; then
|
||||
if [[ -n $size ]]; then
|
||||
w=${size%%,*}
|
||||
h=${size##*,}
|
||||
opt="$opt -w$w -h$h"
|
||||
fi
|
||||
elif [[ "$arg" =~ ^-[whxy] ]]; then
|
||||
elif [[ $arg =~ ^-[whxy] ]]; then
|
||||
opt="$opt ${arg:0:2}$size"
|
||||
elif [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ $tmux_32 = 1 ]]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
elif [[ $size =~ %$ ]]; then
|
||||
size=${size:0:${#size}-1}
|
||||
if [[ $tmux_32 == 1 ]]; then
|
||||
if [[ -n $swap ]]; then
|
||||
opt="$opt -l $((100 - size))%"
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
if [[ -n $swap ]]; then
|
||||
opt="$opt -p $((100 - size))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
if [[ -n $swap ]]; then
|
||||
if [[ $arg =~ ^.l ]]; then
|
||||
max=$columns
|
||||
else
|
||||
max=$lines
|
||||
fi
|
||||
size=$(( max - size ))
|
||||
size=$((max - size))
|
||||
[[ $size -lt 0 ]] && size=0
|
||||
opt="$opt -l $size"
|
||||
else
|
||||
@@ -135,10 +135,10 @@ while [[ $# -gt 0 ]]; do
|
||||
args+=("$arg")
|
||||
;;
|
||||
esac
|
||||
[[ -n "$skip" ]] && args+=("$arg")
|
||||
[[ -n $skip ]] && args+=("$arg")
|
||||
done
|
||||
|
||||
if [[ -z "$TMUX" ]]; then
|
||||
if [[ -z $TMUX ]]; then
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
fi
|
||||
@@ -149,7 +149,7 @@ fi
|
||||
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
|
||||
if [[ ! $opt =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed_without_popup=1
|
||||
original_window=$(tmux display-message -p "#{window_id}")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||
@@ -165,22 +165,22 @@ fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
|
||||
tmux_win_opts=($(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts"))
|
||||
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
|
||||
else
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
tmux_win_opts=($(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/'))
|
||||
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
|
||||
fi
|
||||
cleanup() {
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
# Restore tmux window options
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
|
||||
if [[ ${#tmux_win_opts[@]} -gt 1 ]]; then
|
||||
eval "tmux ${tmux_win_opts[*]}"
|
||||
fi
|
||||
|
||||
# Remove temp window if we were zoomed without popup options
|
||||
if [[ -n "$zoomed_without_popup" ]]; then
|
||||
if [[ -n $zoomed_without_popup ]]; then
|
||||
tmux display-message -p "#{window_id}" > /dev/null
|
||||
tmux swap-pane -t $original_window \; \
|
||||
select-window -t $original_window \; \
|
||||
@@ -197,10 +197,10 @@ trap 'cleanup 1' SIGUSR1
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
if [[ $tmux_version = 3.2 ]]; then
|
||||
if [[ $opt =~ "-E" ]]; then
|
||||
if [[ $tmux_version == 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
elif [[ $tmux_32 = 1 ]]; then
|
||||
elif [[ $tmux_32 == 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
else
|
||||
@@ -211,8 +211,8 @@ fi
|
||||
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
||||
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
[[ -n $RUNEWIDTH_EASTASIAN ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n $BAT_THEME ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
||||
# Build arguments to fzf
|
||||
@@ -224,9 +224,9 @@ close="; trap - EXIT SIGINT SIGTERM $close"
|
||||
|
||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||
mkfifo -m o+w $fifo2
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
if [[ $opt =~ "-E" ]]; then
|
||||
cat $fifo2 &
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
else
|
||||
mkfifo $fifo1
|
||||
@@ -239,7 +239,7 @@ if [[ "$opt" =~ "-E" ]]; then
|
||||
fi
|
||||
|
||||
mkfifo -m o+w $fifo3
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
if [[ -n $term ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
else
|
||||
mkfifo $fifo1
|
||||
@@ -249,6 +249,9 @@ fi
|
||||
tmux \
|
||||
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
$tmux_off_opts \
|
||||
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
||||
> /dev/null 2>&1 || {
|
||||
"$fzf" "${args[@]}"
|
||||
exit $?
|
||||
}
|
||||
cat $fifo2
|
||||
exit "$(cat $fifo3)"
|
||||
|
||||
@@ -503,7 +503,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
||||
14
go.mod
14
go.mod
@@ -1,20 +1,20 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/charlievieth/fastwalk v1.0.9
|
||||
github.com/gdamore/tcell/v2 v2.8.1
|
||||
github.com/junegunn/go-shellwords v0.0.0-20240813092932-a62c48c52e97
|
||||
github.com/charlievieth/fastwalk v1.0.14
|
||||
github.com/gdamore/tcell/v2 v2.9.0
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.29.0
|
||||
golang.org/x/term v0.28.0
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/term v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
go 1.20
|
||||
go 1.23.0
|
||||
|
||||
54
go.sum
54
go.sum
@@ -1,12 +1,11 @@
|
||||
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/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
|
||||
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
|
||||
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/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=
|
||||
@@ -14,35 +13,20 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -50,36 +34,22 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
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=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
53
install
53
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.58.0
|
||||
version=0.66.0
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -104,7 +104,7 @@ check_binary() {
|
||||
|
||||
link_fzf_in_path() {
|
||||
if which_fzf="$(command -v fzf)"; then
|
||||
echo " - Found in \$PATH"
|
||||
echo ' - Found in $PATH'
|
||||
echo " - Creating symlink: bin/fzf -> $which_fzf"
|
||||
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
||||
check_binary && return
|
||||
@@ -164,27 +164,28 @@ download() {
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
archi=$(uname -sm)
|
||||
archi=$(uname -smo 2> /dev/null || uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
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 ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
esac
|
||||
|
||||
@@ -214,7 +215,7 @@ if [ -n "$binary_error" ]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
[[ "$*" =~ "--bin" ]] && exit 0
|
||||
[[ $* =~ "--bin" ]] && exit 0
|
||||
|
||||
for s in $shells; do
|
||||
if ! command -v "$s" > /dev/null; then
|
||||
@@ -241,7 +242,7 @@ fi
|
||||
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && continue
|
||||
[[ $shell == fish ]] && continue
|
||||
src=${prefix_expand}.${shell}
|
||||
echo -n "Generate $src ... "
|
||||
|
||||
@@ -265,7 +266,7 @@ fi
|
||||
EOF
|
||||
|
||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||
if [[ "$shell" = zsh ]]; then
|
||||
if [[ $shell == zsh ]]; then
|
||||
echo "source <(fzf --$shell)" >> "$src"
|
||||
else
|
||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||
@@ -285,7 +286,7 @@ EOF
|
||||
done
|
||||
|
||||
# fish
|
||||
if [[ "$shells" =~ fish ]]; then
|
||||
if [[ $shells =~ fish ]]; then
|
||||
echo -n "Update fish_user_paths ... "
|
||||
fish << EOF
|
||||
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
|
||||
@@ -317,7 +318,7 @@ append_line() {
|
||||
sed 's/^/ Line /' <<< "$lines"
|
||||
|
||||
update=0
|
||||
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then
|
||||
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines"; then
|
||||
echo " - But they all seem to be commented"
|
||||
ask " - Continue modifying $file?"
|
||||
update=$?
|
||||
@@ -355,12 +356,12 @@ if [ $update_config -eq 2 ]; then
|
||||
fi
|
||||
echo
|
||||
for shell in $shells; do
|
||||
[[ "$shell" = fish ]] && continue
|
||||
[[ $shell == fish ]] && continue
|
||||
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
||||
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
|
||||
done
|
||||
|
||||
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
||||
if [ $key_bindings -eq 1 ] && [[ $shells =~ fish ]]; then
|
||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||
if [ ! -e "$bind_file" ]; then
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
@@ -385,13 +386,13 @@ fi
|
||||
|
||||
if [ $update_config -eq 1 ]; then
|
||||
echo 'Finished. Restart your shell or reload config file.'
|
||||
if [[ "$shells" =~ bash ]]; then
|
||||
if [[ $shells =~ bash ]]; then
|
||||
echo -n ' source ~/.bashrc # bash'
|
||||
[[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||
[[ $archi =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||
echo
|
||||
fi
|
||||
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
||||
[[ $shells =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||
[[ $shells =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
||||
echo
|
||||
echo 'Use uninstall script to remove fzf.'
|
||||
echo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.58.0"
|
||||
$version="0.66.0"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
|
||||
2
main.go
2
main.go
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version = "0.58"
|
||||
var version = "0.66"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf\-tmux 1 "Jan 2025" "fzf 0.58.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Oct 2025" "fzf 0.66.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
486
man/man1/fzf.1
486
man/man1/fzf.1
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf 1 "Jan 2025" "fzf 0.58.0" "fzf - a command-line fuzzy finder"
|
||||
.TH fzf 1 "Oct 2025" "fzf 0.66.0" "fzf - a command-line fuzzy finder"
|
||||
|
||||
.SH NAME
|
||||
fzf - a command-line fuzzy finder
|
||||
@@ -56,7 +56,9 @@ Case-insensitive match (default: smart-case match)
|
||||
Case-sensitive match
|
||||
.TP
|
||||
.B "\-\-smart\-case"
|
||||
Smart-case match (default)
|
||||
Smart-case match (default). In this mode, the search is case-insensitive by
|
||||
default, but it becomes case-sensitive if the query contains any uppercase
|
||||
letters.
|
||||
.TP
|
||||
.B "\-\-literal"
|
||||
Do not normalize latin script letters for matching.
|
||||
@@ -76,7 +78,8 @@ Generic scoring scheme designed to work well with any type of input.
|
||||
.RS
|
||||
Additional bonus point is only given to the characters after path separator.
|
||||
You might want to choose this scheme over \fBdefault\fR if you have many files
|
||||
with spaces in their paths.
|
||||
with spaces in their paths. This also sets \fB\-\-tiebreak=pathname,length\fR,
|
||||
to prioritize matches occurring in the tail element of a file path.
|
||||
.RE
|
||||
.RE
|
||||
|
||||
@@ -90,6 +93,13 @@ more weight to the chronological ordering. This also sets
|
||||
.RE
|
||||
.RE
|
||||
|
||||
.RS
|
||||
fzf chooses \fBpath\fR scheme when the input is a TTY device, where fzf would
|
||||
start its built-in walker or run \fB$FZF_DEFAULT_COMMAND\fR, and there is no
|
||||
\fBreload\fR or \fBtransform\fR action bound to \fBstart\fR event. Otherwise,
|
||||
it chooses \fBdefault\fR scheme.
|
||||
.RE
|
||||
|
||||
.TP
|
||||
.BI "\-\-algo=" TYPE
|
||||
Fuzzy matching algorithm (default: v2)
|
||||
@@ -109,8 +119,38 @@ transformed lines (unlike in \fB\-\-preview\fR where fields are extracted from
|
||||
the original lines) because fzf doesn't allow searching against the hidden
|
||||
fields.
|
||||
.TP
|
||||
.BI "\-\-with\-nth=" "N[,..]"
|
||||
Transform the presentation of each line using field index expressions
|
||||
.BI "\-\-with\-nth=" "N[,..] or TEMPLATE"
|
||||
Transform the presentation of each line using the field index expressions.
|
||||
For advanced transformation, you can provide a template containing field index
|
||||
expressions in curly braces. When you use a template, the trailing delimiter is
|
||||
stripped from each expression, giving you more control over the output.
|
||||
\fB{n}\fR in template evaluates to the zero-based ordinal index of the line.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
# Single expression: drop the first field
|
||||
echo foo bar baz | fzf --with-nth 2..
|
||||
|
||||
# Use template to rearrange fields
|
||||
echo foo,bar,baz | fzf --delimiter , --with-nth '{n},{1},{3},{2},{1..2}'
|
||||
.RE
|
||||
.TP
|
||||
.BI "\-\-accept\-nth=" "N[,..] or TEMPLATE"
|
||||
Define which fields to print on accept. The last delimiter is stripped from the
|
||||
output. For advanced transformation, you can provide a template containing
|
||||
field index expressions in curly braces. When you use a template, the trailing
|
||||
delimiter is stripped from each expression, giving you more control over the
|
||||
output. \fB{n}\fR in template evaluates to the zero-based ordinal index of the
|
||||
line.
|
||||
|
||||
.RS
|
||||
e.g.
|
||||
# Single expression
|
||||
echo foo bar baz | fzf --accept-nth 2
|
||||
|
||||
# Template
|
||||
echo foo bar baz | fzf --accept-nth 'Index: {n}, 1st: {1}, 2nd: {2}, 3rd: {3}'
|
||||
.RE
|
||||
.TP
|
||||
.B "+s, \-\-no\-sort"
|
||||
Do not sort the result
|
||||
@@ -144,6 +184,8 @@ Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
.br
|
||||
.BR chunk " Prefers line with shorter matched chunk (delimited by whitespaces)"
|
||||
.br
|
||||
.BR pathname " Prefers line with matched substring in the file name of the path"
|
||||
.br
|
||||
.BR begin " Prefers line with matched substring closer to the beginning"
|
||||
.br
|
||||
.BR end " Prefers line with matched substring closer to the end"
|
||||
@@ -186,6 +228,13 @@ e.g. \fB# Avoid rendering both fzf instances at the same time
|
||||
(sleep 1; seq 1000000; sleep 1) |
|
||||
fzf \-\-sync \-\-query 5 \-\-listen \-\-bind start:up,load:up,result:up,focus:change\-header:Ready\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-no\-tty\-default"
|
||||
Make fzf search for the current TTY device via standard error instead of
|
||||
defaulting to \fB/dev/tty\fR. This option avoids issues when launching
|
||||
emacsclient from within fzf. Alternatively, you can change the default TTY
|
||||
device by setting \fB--tty-default=DEVICE_NAME\fR.
|
||||
|
||||
.SS GLOBAL STYLE
|
||||
.TP
|
||||
.BI "\-\-style=" "PRESET"
|
||||
@@ -193,15 +242,15 @@ Apply a style preset [default|minimal|full[:BORDER_STYLE]]
|
||||
.TP
|
||||
.BI "\-\-color=" "[BASE_SCHEME][,COLOR_NAME[:ANSI_COLOR][:ANSI_ATTRIBUTES]]..."
|
||||
Color configuration. The name of the base color scheme is followed by custom
|
||||
color mappings.
|
||||
color mappings. Each entry is separated by a comma and/or whitespaces.
|
||||
|
||||
.RS
|
||||
.B BASE SCHEME:
|
||||
(default: \fBdark\fR on 256-color terminal, otherwise \fB16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
|
||||
(default: \fBdark\fR on 256-color terminal, otherwise \fBbase16\fR; If \fBNO_COLOR\fR is set, \fBbw\fR)
|
||||
|
||||
\fBdark \fRColor scheme for dark 256-color terminal
|
||||
\fBlight \fRColor scheme for light 256-color terminal
|
||||
\fB16 \fRColor scheme for 16-color terminal
|
||||
\fBdark \fRColor scheme for dark terminal
|
||||
\fBlight \fRColor scheme for light terminal
|
||||
\fBbase16 \fRColor scheme using base 16 colors (alias: \fB16\fR)
|
||||
\fBbw \fRNo colors (equivalent to \fB\-\-no\-color\fR)
|
||||
|
||||
.B COLOR NAMES:
|
||||
@@ -213,15 +262,18 @@ color mappings.
|
||||
\fBlist\-bg \fRList section background
|
||||
\fBselected\-bg \fRSelected line background
|
||||
\fBpreview\-bg \fRPreview window background
|
||||
\fBinput\-bg \fRInput window background (\fB\-\-input\-border\fR)
|
||||
\fBheader\-bg \fRHeader window background (\fB\-\-header\-border\fR)
|
||||
\fBinput\-bg \fRInput window background
|
||||
\fBheader\-bg \fRHeader window background
|
||||
\fBfooter\-bg \fRFooter window background
|
||||
\fBhl \fRHighlighted substrings
|
||||
\fBselected\-hl \fRHighlighted substrings in the selected line
|
||||
\fBcurrent\-fg (fg+) \fRText (current line)
|
||||
\fBcurrent\-bg (bg+) \fRBackground (current line)
|
||||
\fBgutter \fRGutter on the left
|
||||
\fBcurrent\-hl (hl+) \fRHighlighted substrings (current line)
|
||||
\fBalt\-bg \fRAlternate background color to create striped lines
|
||||
\fBquery (input\-fg) \fRQuery string
|
||||
\fBghost \fRGhost text (\fB\-\-ghost\fR, \fBdim\fR applied by default)
|
||||
\fBdisabled \fRQuery string when search is disabled (\fB\-\-disabled\fR)
|
||||
\fBinfo \fRInfo line (match counters)
|
||||
\fBborder \fRBorder around the window (\fB\-\-border\fR and \fB\-\-preview\fR)
|
||||
@@ -233,17 +285,21 @@ color mappings.
|
||||
\fBpreview\-scrollbar \fRScrollbar
|
||||
\fBinput\-border \fRBorder around the input window (\fB\-\-input\-border\fR)
|
||||
\fBheader\-border \fRBorder around the header window (\fB\-\-header\-border\fR)
|
||||
\fBfooter\-border \fRBorder around the footer window (\fB\-\-footer\-border\fR)
|
||||
\fBlabel \fRBorder label (\fB\-\-border\-label\fR, \fB\-\-list\-label\fR, \fB\-\-input\-label\fR, and \fB\-\-preview\-label\fR)
|
||||
\fBlist\-label \fRBorder label of the list section (\fB\-\-list\-label\fR)
|
||||
\fBpreview\-label \fRBorder label of the preview window (\fB\-\-preview\-label\fR)
|
||||
\fBinput\-label \fRBorder label of the input window (\fB\-\-input\-label\fR)
|
||||
\fBheader\-label \fRBorder label of the header window (\fB\-\-header\-label\fR)
|
||||
\fBfooter\-label \fRBorder label of the footer window (\fB\-\-footer\-label\fR)
|
||||
\fBprompt \fRPrompt
|
||||
\fBpointer \fRPointer to the current line
|
||||
\fBmarker \fRMulti\-select marker
|
||||
\fBspinner \fRStreaming input indicator
|
||||
\fBheader (header\-fg) \fRHeader
|
||||
\fBfooter (footer\-fg) \fRFooter
|
||||
\fBnth \fRParts of the line specified by \fB\-\-nth\fR (only supports attributes)
|
||||
\fBnomatch \fRNon-matching items in raw mode (default: \fBdim\fR)
|
||||
|
||||
.B ANSI COLORS:
|
||||
\fB\-1 \fRDefault terminal foreground/background color
|
||||
@@ -269,7 +325,8 @@ color mappings.
|
||||
\fB#rrggbb \fR24-bit colors
|
||||
|
||||
.B ANSI ATTRIBUTES: (Only applies to foreground colors)
|
||||
\fBregular \fRClears previously set attributes; should precede the other ones
|
||||
\fBregular \fRClear previously set attributes; should precede the other ones
|
||||
\fBstrip \fRRemove colors
|
||||
\fBbold\fR
|
||||
\fBunderline\fR
|
||||
\fBreverse\fR
|
||||
@@ -288,7 +345,19 @@ color mappings.
|
||||
# Seoul256 theme with 24-bit colors
|
||||
fzf \-\-color='bg:#4B4B4B,bg+:#3F3F3F,info:#BDBB72,border:#6B6B6B,spinner:#98BC99' \\
|
||||
\-\-color='hl:#719872,fg:#D9D9D9,header:#719872,fg+:#D9D9D9' \\
|
||||
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'\fR
|
||||
\-\-color='pointer:#E12672,marker:#E17899,prompt:#98BEDE,hl+:#98BC99'
|
||||
|
||||
# Seoul256 light theme with 24-bit colors, each entry separated by whitespaces
|
||||
fzf \-\-style full \-\-color='
|
||||
fg:#616161 fg+:#616161
|
||||
bg:#ffffff bg+:#e9e9e9 alt-bg:#f1f1f1
|
||||
hl:#719872 hl+:#719899
|
||||
pointer:#e12672 marker:#e17899
|
||||
header:#719872
|
||||
spinner:#719899 info:#727100
|
||||
prompt:#0099bd query:#616161
|
||||
border:#e1e1e1
|
||||
'\fR
|
||||
.RE
|
||||
.TP
|
||||
.B "\-\-no\-color"
|
||||
@@ -326,9 +395,12 @@ Adaptive height has the following limitations:
|
||||
* It will not find the right size when there are multi-line items
|
||||
|
||||
.TP
|
||||
.BI "\-\-min\-height=" "HEIGHT"
|
||||
Minimum height when \fB\-\-height\fR is given in percent (default: 10).
|
||||
Ignored when \fB\-\-height\fR is not specified.
|
||||
.BI "\-\-min\-height=" "HEIGHT[+]"
|
||||
Minimum height when \fB\-\-height\fR is given as a percentage.
|
||||
Add \fB+\fR to automatically increase the value according to the other
|
||||
layout options so that the specified number of items are visible in the list
|
||||
section (default: \fB10+\fR).
|
||||
Ignored when \fB\-\-height\fR is not specified or set as an absolute value.
|
||||
.TP
|
||||
.BI "\-\-tmux" "[=[center|top|bottom|left|right][,SIZE[%]][,SIZE[%]][,border-native]]"
|
||||
Start fzf in a tmux popup (default \fBcenter,50%\fR). Requires tmux 3.3 or
|
||||
@@ -437,6 +509,8 @@ Draw border around the finder
|
||||
.br
|
||||
.BR vertical " Vertical lines on each side of the finder"
|
||||
.br
|
||||
.BR line " Single line border (position automatically determined)"
|
||||
.br
|
||||
.BR top " (up)"
|
||||
.br
|
||||
.BR bottom " (down)"
|
||||
@@ -452,6 +526,9 @@ If you use a terminal emulator where each box-drawing character takes
|
||||
2 columns, try setting \fB\-\-ambidouble\fR. If the border is still not properly
|
||||
rendered, set \fB\-\-no\-unicode\fR.
|
||||
|
||||
\fBline\fR style draws a single separator line at the top when \fB\-\-height\fR
|
||||
is used.
|
||||
|
||||
.TP
|
||||
.BI "\-\-border\-label" [=LABEL]
|
||||
Label to print on the horizontal border line. Should be used with one of the
|
||||
@@ -521,6 +598,9 @@ Indicator for wrapped lines. The default is '↳ ' or '> ' depending on
|
||||
.B "\-\-no\-multi\-line"
|
||||
Disable multi-line display of items when using \fB\-\-read0\fR
|
||||
.TP
|
||||
.B "\-\-raw"
|
||||
Enable raw mode where non-matching items are also displayed in a dimmed color.
|
||||
.TP
|
||||
.B "\-\-track"
|
||||
Make fzf track the current selection when the result list is updated.
|
||||
This can be useful when browsing logs using fzf with sorting disabled. It is
|
||||
@@ -568,6 +648,9 @@ on the center of the screen.
|
||||
.BI "\-\-jump\-labels=" "CHARS"
|
||||
Label characters for \fBjump\fR mode.
|
||||
.TP
|
||||
.BI "\-\-gutter=" "CHAR"
|
||||
Character used for the gutter column (default: '▌' unless \fB\-\-no\-unicode\fR is given)
|
||||
.TP
|
||||
.BI "\-\-pointer=" "STR"
|
||||
Pointer to the current line (default: '▌' or '>' depending on \fB\-\-no\-unicode\fR)
|
||||
.TP
|
||||
@@ -595,7 +678,8 @@ Do not display scrollbar. A synonym for \fB\-\-scrollbar=''\fB
|
||||
|
||||
.TP
|
||||
.BI "\-\-list\-border" [=STYLE]
|
||||
Draw border around the list section
|
||||
Draw border around the list section. \fBline\fR style is not supported for
|
||||
this border.
|
||||
|
||||
.TP
|
||||
.BI "\-\-list\-label" [=LABEL]
|
||||
@@ -607,6 +691,13 @@ Position of the list label
|
||||
|
||||
.SS INPUT SECTION
|
||||
|
||||
.TP
|
||||
.B "\-\-no\-input"
|
||||
Disable and hide the input section. You can no longer type in queries. To
|
||||
trigger a search, use \fBsearch\fR action. You can later show the input section
|
||||
using \fBshow\-input\fR or \fBtoggle\-input\fR action, and hide it again using
|
||||
\fBhide\-input\fR, or \fBtoggle\-input\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-\-prompt=" "STR"
|
||||
Input prompt (default: '> ')
|
||||
@@ -657,6 +748,10 @@ ANSI color codes are supported.
|
||||
Do not display horizontal separator on the info line. A synonym for
|
||||
\fB\-\-separator=''\fB
|
||||
|
||||
.TP
|
||||
.BI "\-\-ghost=" "TEXT"
|
||||
Ghost text to display when the input is empty
|
||||
|
||||
.TP
|
||||
.B "\-\-filepath\-word"
|
||||
Make word-wise movements and actions respect path separators. The following
|
||||
@@ -671,7 +766,8 @@ actions are affected:
|
||||
\fBkill\-word\fR
|
||||
.TP
|
||||
.BI "\-\-input\-border" [=STYLE]
|
||||
Draw border around the input section
|
||||
Draw border around the input section. \fBline\fR style draws a single separator
|
||||
line between the input section and the list section.
|
||||
|
||||
.TP
|
||||
.BI "\-\-input\-label" [=LABEL]
|
||||
@@ -705,31 +801,41 @@ fzf also exports \fB$FZF_PREVIEW_TOP\fR and \fB$FZF_PREVIEW_LEFT\fR so that
|
||||
the preview command can determine the position of the preview window.
|
||||
|
||||
A placeholder expression starting with \fB+\fR flag will be replaced to the
|
||||
space-separated list of the selected lines (or the current line if no selection
|
||||
space-separated list of the selected items (or the current item if no selection
|
||||
was made) individually quoted.
|
||||
|
||||
e.g.
|
||||
\fBfzf \-\-multi \-\-preview='head \-10 {+}'
|
||||
git log \-\-oneline | fzf \-\-multi \-\-preview 'git show {+1}'\fR
|
||||
|
||||
Similarly, a placeholder expression starting with \fB*\fR flag will be replaced
|
||||
to the space-separated list of all matched items individually quoted.
|
||||
|
||||
Each expression expands to a quoted string, so that it's safe to pass it as an
|
||||
argument to an external command. So you should not manually add quotes around
|
||||
the curly braces. But if you don't want this behavior, you can put
|
||||
\fBr\fR flag (raw) in the expression (e.g. \fB{r}\fR, \fB{r1}\fR, etc).
|
||||
Use it with caution as unquoted output can lead to broken commands.
|
||||
|
||||
When using a field index expression, leading and trailing whitespace is stripped
|
||||
from the replacement string. To preserve the whitespace, use the \fBs\fR flag.
|
||||
|
||||
A placeholder expression with \fBf\fR flag is replaced to the path of
|
||||
a temporary file that holds the evaluated list. This is useful when you
|
||||
multi-select a large number of items and the length of the evaluated string may
|
||||
pass a large number of items and the length of the evaluated string may
|
||||
exceed \fBARG_MAX\fR.
|
||||
|
||||
e.g.
|
||||
\fB# Press CTRL\-A to select 100K items and see the sum of all the numbers.
|
||||
\fB# See the sum of all the matched numbers
|
||||
# This won't work properly without 'f' flag due to ARG_MAX limit.
|
||||
seq 100000 | fzf \-\-multi \-\-bind ctrl\-a:select\-all \\
|
||||
\-\-preview "awk '{sum+=\\$1} END {print sum}' {+f}"\fR
|
||||
seq 100000 | fzf \-\-preview "awk '{sum+=\\$1} END {print sum}' {*f}"\fR
|
||||
|
||||
Also,
|
||||
|
||||
* \fB{q}\fR is replaced to the current query string
|
||||
.br
|
||||
* \fB{q}\fR can contain field index expressions. e.g. \fB{q:1}\fR, \fB{q:2..}\fR, etc.
|
||||
.br
|
||||
* \fB{n}\fR is replaced to the zero-based ordinal index of the current item.
|
||||
Use \fB{+n}\fR if you want all index numbers when multiple lines are selected.
|
||||
.br
|
||||
@@ -763,8 +869,7 @@ e.g.
|
||||
|
||||
.TP
|
||||
.BI "\-\-preview\-border" [=STYLE]
|
||||
Short for \fB\-\-preview\-window=border\-STYLE\fR. In addition to the other
|
||||
styles, \fBline\fR style is also supported for preview border, which draws
|
||||
Short for \fB\-\-preview\-window=border\-STYLE\fR. \fBline\fR style draws
|
||||
a single separator line between the preview window and the rest of the
|
||||
interface.
|
||||
|
||||
@@ -916,10 +1021,12 @@ The first N lines of the input are treated as the sticky header. When
|
||||
lines that follow.
|
||||
.TP
|
||||
.B "\-\-header\-first"
|
||||
Print header before the prompt line
|
||||
Print header before the prompt line. When both normal header and header lines
|
||||
(\fB\-\-header\-lines\fR) are present, this applies only to the normal header.
|
||||
.TP
|
||||
.BI "\-\-header\-border" [=STYLE]
|
||||
Draw border around the header section
|
||||
Draw border around the header section. \fBline\fR style draws a single
|
||||
separator line between the header window and the list section.
|
||||
|
||||
.TP
|
||||
.BI "\-\-header\-label" [=LABEL]
|
||||
@@ -929,6 +1036,35 @@ Label to print on the header border
|
||||
.BI "\-\-header\-label\-pos" [=N[:top|bottom]]
|
||||
Position of the header label
|
||||
|
||||
.TP
|
||||
.BI "\-\-header\-lines\-border" [=STYLE]
|
||||
Display header from \fB--header\-lines\fR with a separate border. Pass
|
||||
\fBnone\fR to still separate the header lines but without a border. To combine
|
||||
two headers, use \fB\-\-no\-header\-lines\-border\fR. \fBline\fR style draws
|
||||
a single separator line between the header lines and the list section.
|
||||
|
||||
.SS FOOTER
|
||||
|
||||
.TP
|
||||
.BI "\-\-footer=" "STR"
|
||||
The given string will be printed as the sticky footer. The lines are displayed
|
||||
in the given order from top to bottom regardless of \fB\-\-layout\fR option, and
|
||||
are not affected by \fB\-\-with\-nth\fR. ANSI color codes are processed even when
|
||||
\fB\-\-ansi\fR is not set.
|
||||
|
||||
.TP
|
||||
.BI "\-\-footer\-border" [=STYLE]
|
||||
Draw border around the footer section. \fBline\fR style draws a single
|
||||
separator line between the footer and the list section.
|
||||
|
||||
.TP
|
||||
.BI "\-\-footer\-label" [=LABEL]
|
||||
Label to print on the footer border
|
||||
|
||||
.TP
|
||||
.BI "\-\-footer\-label\-pos" [=N[:top|bottom]]
|
||||
Position of the footer label
|
||||
|
||||
.SS SCRIPTING
|
||||
.TP
|
||||
.BI "\-q, \-\-query=" "STR"
|
||||
@@ -997,19 +1133,25 @@ On Windows, the default value is \fBcmd /s/c\fR when \fB$SHELL\fR is not
|
||||
set.
|
||||
|
||||
.TP
|
||||
.B "\-\-listen[=[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]"
|
||||
Start HTTP server and listen on the given address. It allows external processes
|
||||
to send actions to perform via POST method.
|
||||
.B "\-\-listen[=SOCKET_PATH|[ADDR:]PORT]" "\-\-listen\-unsafe[=[ADDR:]PORT]"
|
||||
Start HTTP server and listen on the given address or Unix socket. It allows
|
||||
external processes to send actions to perform via POST method and query the
|
||||
program state via GET method. For the argument to be recognized as a socket
|
||||
path, it must have \fB.sock\fR extension.
|
||||
|
||||
- If the port number is omitted or given as 0, fzf will automatically choose
|
||||
a port and export it as \fBFZF_PORT\fR environment variable to the child processes
|
||||
a port and export it as \fBFZF_PORT\fR environment variable to the child processes.
|
||||
|
||||
- If a Unix socket path is given, fzf will create a Unix domain socket at the
|
||||
given path. The existing file will be removed. The path to the socket file
|
||||
is exported as \fBFZF_SOCK\fR environment variable.
|
||||
|
||||
- If \fBFZF_API_KEY\fR environment variable is set, the server would require
|
||||
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header
|
||||
sending an API key with the same value in the \fBx\-api\-key\fR HTTP header.
|
||||
|
||||
- \fBFZF_API_KEY\fR is required for a non-localhost listen address
|
||||
- \fBFZF_API_KEY\fR is required for a non-localhost listen address.
|
||||
|
||||
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR
|
||||
- To allow remote process execution, use \fB\-\-listen\-unsafe\fR.
|
||||
|
||||
e.g.
|
||||
\fB# Start HTTP server on port 6266
|
||||
@@ -1018,11 +1160,6 @@ e.g.
|
||||
# Send action to the server
|
||||
curl \-XPOST localhost:6266 \-d 'reload(seq 100)+change\-prompt(hundred> )'
|
||||
|
||||
# Get program state in JSON format (experimental)
|
||||
# * Make sure NOT to access this endpoint from execute/transform actions
|
||||
# as it will result in a timeout
|
||||
curl localhost:6266
|
||||
|
||||
# Start HTTP server on port 6266 with remote connections allowed
|
||||
# * Listening on non-localhost address requires using an API key
|
||||
export FZF_API_KEY="$(head \-c 32 /dev/urandom | base64)"
|
||||
@@ -1033,6 +1170,36 @@ e.g.
|
||||
|
||||
# Choose port automatically and export it as $FZF_PORT to the child process
|
||||
fzf \-\-listen \-\-bind 'start:execute\-silent:echo $FZF_PORT > /tmp/fzf\-port'
|
||||
|
||||
# Get program state in JSON format (experimental)
|
||||
# - GET Parameters:
|
||||
# - limit: number of items to return (default: 100)
|
||||
# - offset: number of items to skip (default: 0)
|
||||
curl localhost:6266
|
||||
|
||||
# Automatically select items with .txt extension
|
||||
fzf \-\-multi \-\-sync \-\-listen \-\-bind 'load:transform:
|
||||
pos=1
|
||||
curl \-s localhost:$FZF_PORT?limit=1000 | jq \-r .matches[].text | while read \-r text; do
|
||||
if [[ $text =~ \\.txt$ ]]; then
|
||||
echo \-n "+pos($pos)+select"
|
||||
fi
|
||||
pos=$((pos + 1))
|
||||
done
|
||||
echo +first
|
||||
'
|
||||
\fR
|
||||
|
||||
Here is an example script that uses a Unix socket instead of a TCP port.
|
||||
|
||||
\fB
|
||||
fzf --listen=/tmp/fzf.sock
|
||||
|
||||
# GET
|
||||
curl --unix-socket /tmp/fzf.sock http
|
||||
|
||||
# POST
|
||||
curl --unix-socket /tmp/fzf.sock http -d up
|
||||
\fR
|
||||
|
||||
.SS DIRECTORY TRAVERSAL
|
||||
@@ -1122,9 +1289,9 @@ Show man page
|
||||
.SH ENVIRONMENT VARIABLES
|
||||
.TP
|
||||
.B FZF_DEFAULT_COMMAND
|
||||
Default command to use when input is tty. On *nix systems, fzf runs the command
|
||||
with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh \-c\fR, so in
|
||||
this case make sure that the command is POSIX-compliant.
|
||||
Default command to use when input is a TTY device. On *nix systems, fzf runs
|
||||
the command with \fB$SHELL \-c\fR if \fBSHELL\fR is set, otherwise with \fBsh
|
||||
\-c\fR, so in this case make sure that the command is POSIX-compliant.
|
||||
.TP
|
||||
.B FZF_DEFAULT_OPTS
|
||||
Default options.
|
||||
@@ -1186,6 +1353,8 @@ fzf exports the following environment variables to its child processes.
|
||||
.br
|
||||
.BR FZF_COLUMNS " Number of columns fzf takes up excluding padding and margin"
|
||||
.br
|
||||
.BR FZF_DIRECTION " Direction of the list (\fBup\fR or \fBdown\fR)"
|
||||
.br
|
||||
.BR FZF_TOTAL_COUNT " Total number of items"
|
||||
.br
|
||||
.BR FZF_MATCH_COUNT " Number of matched items"
|
||||
@@ -1196,20 +1365,34 @@ fzf exports the following environment variables to its child processes.
|
||||
.br
|
||||
.BR FZF_QUERY " Current query string"
|
||||
.br
|
||||
.BR FZF_INPUT_STATE " Current input state (enabled, disabled, hidden)"
|
||||
.br
|
||||
.BR FZF_NTH " Current \-\-nth option"
|
||||
.br
|
||||
.BR FZF_PROMPT " Prompt string"
|
||||
.br
|
||||
.BR FZF_GHOST " Ghost string"
|
||||
.br
|
||||
.BR FZF_POINTER " Pointer string"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LABEL " Preview label string"
|
||||
.br
|
||||
.BR FZF_BORDER_LABEL " Border label string"
|
||||
.br
|
||||
.BR FZF_LIST_LABEL " List label string"
|
||||
.br
|
||||
.BR FZF_INPUT_LABEL " Input label string"
|
||||
.br
|
||||
.BR FZF_HEADER_LABEL " Header label string"
|
||||
.br
|
||||
.BR FZF_ACTION " The name of the last action performed"
|
||||
.br
|
||||
.BR FZF_KEY " The name of the last key pressed"
|
||||
.br
|
||||
.BR FZF_PORT " Port number when \-\-listen option is used"
|
||||
.br
|
||||
.BR FZF_SOCK " Unix socket path when \-\-listen option is used"
|
||||
.br
|
||||
.BR FZF_PREVIEW_TOP " Top position of the preview window"
|
||||
.br
|
||||
.BR FZF_PREVIEW_LEFT " Left position of the preview window"
|
||||
@@ -1217,6 +1400,8 @@ fzf exports the following environment variables to its child processes.
|
||||
.BR FZF_PREVIEW_LINES " Number of lines in the preview window"
|
||||
.br
|
||||
.BR FZF_PREVIEW_COLUMNS " Number of columns in the preview window"
|
||||
.br
|
||||
.BR FZF_RAW " Only in raw mode. 1 if the current item matches, 0 otherwise"
|
||||
|
||||
.SH EXTENDED SEARCH MODE
|
||||
|
||||
@@ -1275,10 +1460,15 @@ more \fBactions\fR. You can use it to customize key bindings or implement
|
||||
dynamic behaviors.
|
||||
|
||||
\fB\-\-bind\fR takes a comma-separated list of binding expressions. Each binding
|
||||
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR.
|
||||
expression is \fBKEY:ACTION\fR or \fBEVENT:ACTION\fR. You can bind actions to
|
||||
multiple keys and events by writing comma-separated list of keys and events
|
||||
before the colon. e.g. \fBKEY1,KEY2,EVENT1,EVENT2:ACTION\fR.
|
||||
|
||||
e.g.
|
||||
\fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line\fR
|
||||
\fBfzf \-\-bind=ctrl\-j:accept,ctrl\-k:kill\-line
|
||||
|
||||
# Load 'ps \-ef' output on start and reload it on CTRL\-R
|
||||
fzf \-\-bind 'start,ctrl\-r:reload:ps \-ef'\fR
|
||||
|
||||
.SS AVAILABLE KEYS: (SYNONYMS)
|
||||
\fIctrl\-[a\-z]\fR
|
||||
@@ -1315,12 +1505,22 @@ e.g.
|
||||
.br
|
||||
\fIalt\-right\fR
|
||||
.br
|
||||
\fIalt\-home\fR
|
||||
.br
|
||||
\fIalt\-end\fR
|
||||
.br
|
||||
\fIalt\-backspace\fR (\fIalt\-bspace\fR \fIalt\-bs\fR)
|
||||
.br
|
||||
\fIalt\-delete\fR
|
||||
.br
|
||||
\fIalt\-page\-up\fR
|
||||
.br
|
||||
\fIalt\-page\-down\fR
|
||||
.br
|
||||
\fIalt\-enter\fR
|
||||
.br
|
||||
\fIalt\-space\fR
|
||||
.br
|
||||
\fIalt\-backspace\fR (\fIalt\-bspace\fR \fIalt\-bs\fR)
|
||||
.br
|
||||
\fItab\fR
|
||||
.br
|
||||
\fIshift\-tab\fR (\fIbtab\fR)
|
||||
@@ -1347,6 +1547,26 @@ e.g.
|
||||
.br
|
||||
\fIpage\-down\fR (\fIpgdn\fR)
|
||||
.br
|
||||
\fIctrl\-up\fR
|
||||
.br
|
||||
\fIctrl\-down\fR
|
||||
.br
|
||||
\fIctrl\-left\fR
|
||||
.br
|
||||
\fIctrl\-right\fR
|
||||
.br
|
||||
\fIctrl\-home\fR
|
||||
.br
|
||||
\fIctrl\-end\fR
|
||||
.br
|
||||
\fIctrl\-backspace\fR (\fIctrl\-bspace\fR \fIctrl\-bs\fR)
|
||||
.br
|
||||
\fIctrl\-delete\fR
|
||||
.br
|
||||
\fIctrl\-page\-up\fR
|
||||
.br
|
||||
\fIctrl\-page\-down\fR
|
||||
.br
|
||||
\fIshift\-up\fR
|
||||
.br
|
||||
\fIshift\-down\fR
|
||||
@@ -1355,8 +1575,16 @@ e.g.
|
||||
.br
|
||||
\fIshift\-right\fR
|
||||
.br
|
||||
\fIshift\-home\fR
|
||||
.br
|
||||
\fIshift\-end\fR
|
||||
.br
|
||||
\fIshift\-delete\fR
|
||||
.br
|
||||
\fIshift\-page\-up\fR
|
||||
.br
|
||||
\fIshift\-page\-down\fR
|
||||
.br
|
||||
\fIalt\-shift\-up\fR
|
||||
.br
|
||||
\fIalt\-shift\-down\fR
|
||||
@@ -1365,6 +1593,72 @@ e.g.
|
||||
.br
|
||||
\fIalt\-shift\-right\fR
|
||||
.br
|
||||
\fIalt\-shift\-home\fR
|
||||
.br
|
||||
\fIalt\-shift\-end\fR
|
||||
.br
|
||||
\fIalt\-shift\-delete\fR
|
||||
.br
|
||||
\fIalt\-shift\-page\-up\fR
|
||||
.br
|
||||
\fIalt\-shift\-page\-down\fR
|
||||
.br
|
||||
\fIctrl\-alt\-up\fR
|
||||
.br
|
||||
\fIctrl\-alt\-down\fR
|
||||
.br
|
||||
\fIctrl\-alt\-left\fR
|
||||
.br
|
||||
\fIctrl\-alt\-right\fR
|
||||
.br
|
||||
\fIctrl\-alt\-home\fR
|
||||
.br
|
||||
\fIctrl\-alt\-end\fR
|
||||
.br
|
||||
\fIctrl\-alt\-backspace\fR (\fIctrl\-alt\-bspace\fR \fIctrl\-alt\-bs\fR)
|
||||
.br
|
||||
\fIctrl\-alt\-delete\fR
|
||||
.br
|
||||
\fIctrl\-alt\-page\-up\fR
|
||||
.br
|
||||
\fIctrl\-alt\-page\-down\fR
|
||||
.br
|
||||
\fIctrl\-shift\-up\fR
|
||||
.br
|
||||
\fIctrl\-shift\-down\fR
|
||||
.br
|
||||
\fIctrl\-shift\-left\fR
|
||||
.br
|
||||
\fIctrl\-shift\-right\fR
|
||||
.br
|
||||
\fIctrl\-shift\-home\fR
|
||||
.br
|
||||
\fIctrl\-shift\-end\fR
|
||||
.br
|
||||
\fIctrl\-shift\-delete\fR
|
||||
.br
|
||||
\fIctrl\-shift\-page\-up\fR
|
||||
.br
|
||||
\fIctrl\-shift\-page\-down\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-up\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-down\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-left\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-right\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-home\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-end\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-delete\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-page\-up\fR
|
||||
.br
|
||||
\fIctrl\-alt\-shift\-page\-down\fR
|
||||
.br
|
||||
\fIleft\-click\fR
|
||||
.br
|
||||
\fIright\-click\fR
|
||||
@@ -1389,6 +1683,8 @@ e.g.
|
||||
.br
|
||||
or any single character
|
||||
|
||||
Note that some terminal emulators may not support \fIctrl-*\fR bindings.
|
||||
|
||||
.SS AVAILABLE EVENTS:
|
||||
\fIstart\fR
|
||||
.RS
|
||||
@@ -1451,6 +1747,10 @@ e.g.
|
||||
# Beware not to introduce an infinite loop
|
||||
seq 10 | fzf \-\-bind 'focus:up' \-\-cycle\fR
|
||||
.RE
|
||||
\fImulti\fR
|
||||
.RS
|
||||
Triggered when the multi\-selection has changed.
|
||||
.RE
|
||||
|
||||
\fIone\fR
|
||||
.RS
|
||||
@@ -1503,11 +1803,29 @@ e.g.
|
||||
|
||||
\fIclick\-header\fR
|
||||
.RS
|
||||
Triggered when a mouse click occurs within the header. Sets \fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment variables starting from 1.
|
||||
Triggered when a mouse click occurs within the header. Sets
|
||||
\fBFZF_CLICK_HEADER_LINE\fR and \fBFZF_CLICK_HEADER_COLUMN\fR environment
|
||||
variables starting from 1. It optionally sets \fBFZF_CLICK_HEADER_WORD\fR and
|
||||
\fBFZF_CLICK_HEADER_NTH\fR if clicked on a word.
|
||||
|
||||
e.g.
|
||||
\fBprintf "head1\\nhead2" | fzf \-\-header\-lines=2 \-\-bind 'click\-header:transform\-prompt:printf ${FZF_CLICK_HEADER_LINE}x${FZF_CLICK_HEADER_COLUMN}'\fR
|
||||
\fB# Click on the header line to limit search scope
|
||||
ps \-ef | fzf \-\-style full \-\-layout reverse \-\-header\-lines 1 \\
|
||||
\-\-header\-lines\-border bottom \-\-no\-list\-border \\
|
||||
\-\-color fg:dim,nth:regular \\
|
||||
\-\-bind 'click\-header:transform\-nth(
|
||||
echo $FZF_CLICK_HEADER_NTH
|
||||
)+transform\-prompt(
|
||||
echo "$FZF_CLICK_HEADER_WORD> "
|
||||
)'\fR
|
||||
.RE
|
||||
|
||||
\fIclick\-footer\fR
|
||||
.RS
|
||||
Triggered when a mouse click occurs within the footer. Sets
|
||||
\fBFZF_CLICK_FOOTER_LINE\fR and \fBFZF_CLICK_FOOTER_COLUMN\fR environment
|
||||
variables starting from 1. It optionally sets \fBFZF_CLICK_FOOTER_WORD\fR
|
||||
if clicked on a word.
|
||||
.RE
|
||||
|
||||
.SS AVAILABLE ACTIONS:
|
||||
@@ -1519,14 +1837,20 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBaccept\-non\-empty\fR (same as \fBaccept\fR except that it prevents fzf from exiting without selection)
|
||||
\fBaccept\-or\-print\-query\fR (same as \fBaccept\fR except that it prints the query when there's no match)
|
||||
\fBbackward\-char\fR \fIctrl\-b left\fR
|
||||
\fBbackward\-delete\-char\fR \fIctrl\-h bspace\fR
|
||||
\fBbackward\-delete\-char\fR \fIctrl\-h ctrl\-bspace bspace\fR
|
||||
\fBbackward\-delete\-char/eof\fR (same as \fBbackward\-delete\-char\fR except aborts fzf if query is empty)
|
||||
\fBbackward\-kill\-subword\fR
|
||||
\fBbackward\-kill\-word\fR \fIalt\-bs\fR
|
||||
\fBbackward\-subword\fR
|
||||
\fBbackward\-word\fR \fIalt\-b shift\-left\fR
|
||||
\fBbecome(...)\fR (replace fzf process with the specified command; see below for the details)
|
||||
\fBbeginning\-of\-line\fR \fIctrl\-a home\fR
|
||||
\fBbell\fR (ring the terminal bell)
|
||||
\fBbest\fR (move to the best match; same as \fBfirst\fR if raw mode is disabled)
|
||||
\fBbg\-cancel\fR (cancel background transform processes)
|
||||
\fBcancel\fR (clear query string if not empty, abort fzf otherwise)
|
||||
\fBchange\-border\-label(...)\fR (change \fB\-\-border\-label\fR to the given string)
|
||||
\fBchange\-ghost(...)\fR (change ghost text to the given string)
|
||||
\fBchange\-header(...)\fR (change header to the given string; doesn't affect \fB\-\-header\-lines\fR)
|
||||
\fBchange\-header\-label(...)\fR (change \fB\-\-header\-label\fR to the given string)
|
||||
\fBchange\-input\-label(...)\fR (change \fB\-\-input\-label\fR to the given string)
|
||||
@@ -1534,47 +1858,57 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBchange\-multi\fR (enable multi-select mode with no limit)
|
||||
\fBchange\-multi(...)\fR (enable multi-select mode with a limit or disable it with 0)
|
||||
\fBchange\-nth(...)\fR (change \fB\-\-nth\fR option; rotate through the multiple options separated by '|')
|
||||
\fBchange\-pointer(...)\fR (change \fB\-\-pointer\fR option)
|
||||
\fBchange\-preview(...)\fR (change \fB\-\-preview\fR option)
|
||||
\fBchange\-preview\-label(...)\fR (change \fB\-\-preview\-label\fR to the given string)
|
||||
\fBchange\-preview\-window(...)\fR (change \fB\-\-preview\-window\fR option; rotate through the multiple option sets separated by '|')
|
||||
\fBchange\-prompt(...)\fR (change prompt to the given string)
|
||||
\fBchange\-query(...)\fR (change query string to the given string)
|
||||
\fBclear\-screen\fR \fIctrl\-l\fR
|
||||
\fBclear\-selection\fR (clear multi\-selection)
|
||||
\fBclear\-multi\fR (clear multi\-selection)
|
||||
\fBclose\fR (close preview window if open, abort fzf otherwise)
|
||||
\fBclear\-query\fR (clear query string)
|
||||
\fBdelete\-char\fR \fIdel\fR
|
||||
\fBdelete\-char/eof\fR \fIctrl\-d\fR (same as \fBdelete\-char\fR except aborts fzf if query is empty)
|
||||
\fBdeselect\fR
|
||||
\fBdeselect\-all\fR (deselect all matches)
|
||||
\fBdeselect\-all\fR (deselect all matches; to also clear non-matching selections, use \fBclear\-multi\fR)
|
||||
\fBdisable\-raw\fR (disable raw mode)
|
||||
\fBdisable\-search\fR (disable search functionality)
|
||||
\fBdown\fR \fIctrl\-j ctrl\-n down\fR
|
||||
\fBdown\fR \fIctrl\-j down\fR
|
||||
\fBdown\-match\fR \fIctrl\-n\fR \fIalt\-down\fR (move to the match below the cursor)
|
||||
\fBdown\-selected\fR (move to the selected item below the cursor)
|
||||
\fBenable\-raw\fR (enable raw mode)
|
||||
\fBenable\-search\fR (enable search functionality)
|
||||
\fBend\-of\-line\fR \fIctrl\-e end\fR
|
||||
\fBexclude\fR (exclude the current item from the result)
|
||||
\fBexclude\-multi\fR (exclude the selected items or the current item from the result)
|
||||
\fBexecute(...)\fR (see below for the details)
|
||||
\fBexecute\-silent(...)\fR (see below for the details)
|
||||
\fBfirst\fR (move to the first match; same as \fBpos(1)\fR)
|
||||
\fBforward\-char\fR \fIctrl\-f right\fR
|
||||
\fBforward\-subword\fR
|
||||
\fBforward\-word\fR \fIalt\-f shift\-right\fR
|
||||
\fBignore\fR
|
||||
\fBjump\fR (EasyMotion-like 2-keystroke movement)
|
||||
\fBkill\-line\fR
|
||||
\fBkill\-subword\fR
|
||||
\fBkill\-word\fR \fIalt\-d\fR
|
||||
\fBlast\fR (move to the last match; same as \fBpos(\-1)\fR)
|
||||
\fBnext\-history\fR (\fIctrl\-n\fR on \fB\-\-history\fR)
|
||||
\fBnext\-selected\fR (move to the next selected item)
|
||||
\fBnext\-selected\fR (synonym to \fBdown\-selected\fR)
|
||||
\fBpage\-down\fR \fIpgdn\fR
|
||||
\fBpage\-up\fR \fIpgup\fR
|
||||
\fBhalf\-page\-down\fR
|
||||
\fBhalf\-page\-up\fR
|
||||
\fBhide\-header\fR
|
||||
\fBhide\-input\fR
|
||||
\fBhide\-preview\fR
|
||||
\fBoffset\-down\fR (similar to CTRL\-E of Vim)
|
||||
\fBoffset\-up\fR (similar to CTRL\-Y of Vim)
|
||||
\fBoffset\-middle\fR (place the current item is in the middle of the screen)
|
||||
\fBpos(...)\fR (move cursor to the numeric position; negative number to count from the end)
|
||||
\fBprev\-history\fR (\fIctrl\-p\fR on \fB\-\-history\fR)
|
||||
\fBprev\-selected\fR (move to the previous selected item)
|
||||
\fBprev\-selected\fR (synonym to \fBup\-selected\fR)
|
||||
\fBpreview(...)\fR (see below for the details)
|
||||
\fBpreview\-down\fR \fIshift\-down\fR
|
||||
\fBpreview\-up\fR \fIshift\-up\fR
|
||||
@@ -1592,19 +1926,24 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBreload(...)\fR (see below for the details)
|
||||
\fBreload\-sync(...)\fR (see below for the details)
|
||||
\fBreplace\-query\fR (replace query string with the current selection)
|
||||
\fBsearch(...)\fR (trigger fzf search with the given string)
|
||||
\fBselect\fR
|
||||
\fBselect\-all\fR (select all matches)
|
||||
\fBshow\-header\fR
|
||||
\fBshow\-input\fR
|
||||
\fBshow\-preview\fR
|
||||
\fBtoggle\fR (\fIright\-click\fR)
|
||||
\fBtoggle\-all\fR (toggle all matches)
|
||||
\fBtoggle\-in\fR (\fB\-\-layout=reverse*\fR ? \fBtoggle+up\fR : \fBtoggle+down\fR)
|
||||
\fBtoggle\-out\fR (\fB\-\-layout=reverse*\fR ? \fBtoggle+down\fR : \fBtoggle+up\fR)
|
||||
\fBtoggle\-bind\fR
|
||||
\fBtoggle\-header\fR
|
||||
\fBtoggle\-hscroll\fR
|
||||
\fBtoggle\-input\fR
|
||||
\fBtoggle\-multi\-line\fR
|
||||
\fBtoggle\-preview\fR
|
||||
\fBtoggle\-preview\-wrap\fR
|
||||
\fBtoggle\-raw\fR (toggle raw mode for displaying non-matching items)
|
||||
\fBtoggle\-search\fR (toggle search functionality)
|
||||
\fBtoggle\-sort\fR
|
||||
\fBtoggle\-track\fR (toggle global tracking option (\fB\-\-track\fR))
|
||||
@@ -1615,20 +1954,30 @@ A key or an event can be bound to one or more of the following actions.
|
||||
\fBtrack\-current\fR (track the current item; automatically disabled if focus changes)
|
||||
\fBtransform(...)\fR (transform states using the output of an external command)
|
||||
\fBtransform\-border\-label(...)\fR (transform border label using an external command)
|
||||
\fBtransform\-ghost(...)\fR (transform ghost text using an external command)
|
||||
\fBtransform\-header(...)\fR (transform header using an external command)
|
||||
\fBtransform\-header\-label(...)\fR (transform header label using an external command)
|
||||
\fBtransform\-input\-label(...)\fR (transform input label using an external command)
|
||||
\fBtransform\-list\-label(...)\fR (transform list label using an external command)
|
||||
\fBtransform\-nth(...)\fR (transform nth using an external command)
|
||||
\fBtransform\-pointer(...)\fR (transform pointer using an external command)
|
||||
\fBtransform\-preview\-label(...)\fR (transform preview label using an external command)
|
||||
\fBtransform\-prompt(...)\fR (transform prompt string using an external command)
|
||||
\fBtransform\-query(...)\fR (transform query string using an external command)
|
||||
\fBtransform\-search(...)\fR (trigger fzf search with the output of an external command)
|
||||
\fBtrigger(...)\fR (trigger actions bound to a comma-separated list of keys and events)
|
||||
\fBunbind(...)\fR (unbind bindings)
|
||||
\fBunix\-line\-discard\fR \fIctrl\-u\fR
|
||||
\fBunix\-word\-rubout\fR \fIctrl\-w\fR
|
||||
\fBuntrack\-current\fR (stop tracking the current item; no-op if global tracking is enabled)
|
||||
\fBup\fR \fIctrl\-k ctrl\-p up\fR
|
||||
\fBup\fR \fIctrl\-k up\fR
|
||||
\fBup\-match\fR \fIctrl\-p\fR \fIalt\-up\fR (move to the match above the cursor)
|
||||
\fBup\-selected\fR (move to the selected item above the cursor)
|
||||
\fByank\fR \fIctrl\-y\fR
|
||||
|
||||
Each \fBtransform*\fR action has a corresponding \fBbg\-transform*\fR
|
||||
variant that runs the command in the background.
|
||||
|
||||
.SS ACTION COMPOSITION
|
||||
|
||||
Multiple actions can be chained using \fB+\fR separator.
|
||||
@@ -1637,6 +1986,9 @@ e.g.
|
||||
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all+accept'\fR
|
||||
\fBfzf \-\-multi \-\-bind 'ctrl\-a:select\-all' \-\-bind 'ctrl\-a:+accept'\fR
|
||||
|
||||
Any action after a terminal action that exits fzf, such as \fBaccept\fR or
|
||||
\fBabort\fR, is ignored.
|
||||
|
||||
.SS ACTION ARGUMENT
|
||||
|
||||
An action denoted with \fB(...)\fR suffix takes an argument.
|
||||
@@ -1750,6 +2102,26 @@ e.g.
|
||||
echo "change\-header:Invalid selection"'
|
||||
\fR
|
||||
|
||||
A common mistake when writing a \fBtransform\fR action is not escaping
|
||||
placeholder expressions when passing them back to fzf. In the following
|
||||
example, if you don't escape \fB{}\fR, fzf will immediately replace it with the
|
||||
single-quoted string of the current item. This causes single quotes to appear
|
||||
in the header and footer, and the script will break if any item contains
|
||||
double-quote characters.
|
||||
|
||||
\fBfzf \-\-bind 'focus:transform:[[ $FZF_ACTION =~ up ]] &&
|
||||
echo "change\-header()+transform\-footer:echo \\{}" ||
|
||||
echo "change\-footer()+transform\-header:echo \\{}"'\fR
|
||||
|
||||
.SS TRANSFORM IN THE BACKGROUND
|
||||
|
||||
Transform actions are synchronous, meaning fzf becomes unresponsive while the
|
||||
command runs. To avoid this, each \fBtransform*\fR action has a corresponding
|
||||
\fBbg\-transform*\fR variant that runs in the background. Unless you need to
|
||||
chain multiple transform actions where later ones depend on earlier results,
|
||||
prefer using the \fBbg\fR variant. To cancel currently running background
|
||||
transform processes, use \fBbg\-cancel\fR action.
|
||||
|
||||
.SS PREVIEW BINDING
|
||||
|
||||
With \fBpreview(...)\fR action, you can specify multiple different preview
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2013-2024 Junegunn Choi
|
||||
" Copyright (c) 2013-2025 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -358,7 +358,7 @@ endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors)
|
||||
let gui = get(g:, 'fzf_force_termguicolors', 0) || (!s:is_win && !has('win32unix') && (has('gui_running') || has('termguicolors') && &termguicolors))
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
@@ -553,8 +553,15 @@ try
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --no-tmux --height='.height
|
||||
endif
|
||||
|
||||
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
|
||||
" Add 1-column horizontal margin
|
||||
let optstr = join(['--margin 0,1', optstr])
|
||||
else
|
||||
" 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])
|
||||
endif
|
||||
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if use_term
|
||||
@@ -1020,8 +1027,23 @@ if has('nvim')
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||
let win = nvim_open_win(buf, v:true, opts)
|
||||
silent! call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||
call setwinvar(win, '&colorcolumn', '')
|
||||
|
||||
" Colors
|
||||
try
|
||||
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||
let rules = get(g:, 'fzf_colors', {})
|
||||
if has_key(rules, 'bg')
|
||||
let color = call('s:get_color', rules.bg)
|
||||
if len(color)
|
||||
let ns = nvim_create_namespace('fzf_popup')
|
||||
let hl = nvim_set_hl(ns, 'Normal',
|
||||
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
|
||||
call nvim_win_set_hl_ns(win, ns)
|
||||
endif
|
||||
endif
|
||||
catch
|
||||
endtry
|
||||
return buf
|
||||
endfunction
|
||||
else
|
||||
@@ -1081,7 +1103,7 @@ endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
|
||||
37
shell/common.sh
Normal file
37
shell/common.sh
Normal file
@@ -0,0 +1,37 @@
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
# This function performs `exec awk "$@"` safely by working around awk
|
||||
# compatibility issues.
|
||||
#
|
||||
# To reduce an extra fork, this function performs "exec" so is expected to be
|
||||
# run as the last command in a subshell.
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
# Note: Solaris awk at /usr/bin/awk is meant for backward compatibility
|
||||
# with an ancient implementation of 1977 awk in the original UNIX. It
|
||||
# lacks many features of POSIX awk, so it is essentially useless in the
|
||||
# modern point of view. To use a standard-conforming version in Solaris,
|
||||
# one needs to explicitly use /usr/xpg4/bin/awk.
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
# choose the faster mawk if: it's installed && build date >= 20230322 &&
|
||||
# version >= 1.3.4
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && ((d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004)) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
# Note: macOS awk has a quirk that it stops processing at all when it sees
|
||||
# any data not following UTF-8 in the input stream when the current LC_CTYPE
|
||||
# specifies the UTF-8 encoding. To work around this quirk, one needs to
|
||||
# specify LC_ALL=C to change the current encoding to the plain one.
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
@@ -31,21 +31,37 @@ if [[ $- =~ i ]]; then
|
||||
|
||||
###########################################################
|
||||
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
#----BEGIN shfmt
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && ((d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004)) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" == function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then
|
||||
elif [[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; }; then
|
||||
shift
|
||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||
else
|
||||
@@ -57,13 +73,13 @@ __fzf_comprun() {
|
||||
__fzf_orig_completion() {
|
||||
local l comp f cmd
|
||||
while read -r l; do
|
||||
if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||
if [[ $l =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||
comp="${BASH_REMATCH[1]}"
|
||||
f="${BASH_REMATCH[2]}"
|
||||
cmd="${BASH_REMATCH[3]}"
|
||||
[[ "$f" = _fzf_* ]] && continue
|
||||
[[ $f == _fzf_* ]] && continue
|
||||
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||
if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then
|
||||
if [[ $l == *" -o nospace "* ]] && [[ ${__fzf_nospace_commands-} != *" $cmd "* ]]; then
|
||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||
fi
|
||||
fi
|
||||
@@ -99,7 +115,7 @@ _fzf_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD - 1]}"
|
||||
opts="
|
||||
+c --no-color
|
||||
+i --no-ignore-case
|
||||
@@ -183,27 +199,27 @@ _fzf_opts_completion() {
|
||||
|
||||
case "${prev}" in
|
||||
--scheme)
|
||||
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
|
||||
COMPREPLY=($(compgen -W "default path history" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
|
||||
COMPREPLY=($(compgen -W "length chunk begin end index" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
|
||||
COMPREPLY=($(compgen -W "dark light 16 bw no" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--layout)
|
||||
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
|
||||
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--info)
|
||||
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
|
||||
COMPREPLY=($(compgen -W "default right hidden inline inline-right" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--preview-window)
|
||||
COMPREPLY=( $(compgen -W "
|
||||
COMPREPLY=($(compgen -W "
|
||||
default
|
||||
hidden
|
||||
nohidden
|
||||
@@ -229,21 +245,21 @@ _fzf_opts_completion() {
|
||||
border-left
|
||||
border-right
|
||||
follow
|
||||
nofollow" -- "$cur") )
|
||||
nofollow" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--border)
|
||||
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
|
||||
COMPREPLY=($(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
--border-label-pos|--preview-label-pos)
|
||||
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
|
||||
--border-label-pos | --preview-label-pos)
|
||||
COMPREPLY=($(compgen -W "center bottom top" -- "$cur"))
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cur" =~ ^-|\+ ]]; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
|
||||
if [[ $cur =~ ^-|\+ ]]; then
|
||||
COMPREPLY=($(compgen -W "${opts}" -- "$cur"))
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -257,7 +273,7 @@ _fzf_handle_dynamic_completion() {
|
||||
orig_cmd="$1"
|
||||
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
||||
"$REPLY" "$@"
|
||||
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
||||
elif [[ -n ${_fzf_completion_loader-} ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
$_fzf_completion_loader "$@"
|
||||
ret=$?
|
||||
@@ -271,7 +287,7 @@ _fzf_handle_dynamic_completion() {
|
||||
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
||||
orig_complete=$REPLY
|
||||
|
||||
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
|
||||
if [[ ${__fzf_nospace_commands-} == *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
@@ -291,18 +307,18 @@ __fzf_generic_path_completion() {
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
|
||||
dir=
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
[[ $base == *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[[ -z "$dir" ]] && dir='.'
|
||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||
if [[ -z $dir ]] || [[ -d $dir ]]; then
|
||||
leftover=${base/#"$dir"/}
|
||||
leftover=${leftover/#\//}
|
||||
[[ -z $dir ]] && dir='.'
|
||||
[[ $dir != "/" ]] && dir="${dir/%\//}"
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
@@ -311,28 +327,30 @@ __fzf_generic_path_completion() {
|
||||
else
|
||||
if [[ $1 =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||
fi
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" $rest
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||
fi | while read -r item; do
|
||||
printf "%q " "${item%$3}$3"
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n "$matches" ]]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
[[ -z $3 ]] && [[ ${__fzf_nospace_commands-} == *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n $matches ]]; then
|
||||
COMPREPLY=("$matches")
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(command dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
[[ $dir =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
shift
|
||||
@@ -348,15 +366,15 @@ _fzf_complete() {
|
||||
args=("$@")
|
||||
sep=
|
||||
for i in "${!args[@]}"; do
|
||||
if [[ "${args[$i]}" = -- ]]; then
|
||||
if [[ ${args[$i]} == -- ]]; then
|
||||
sep=$i
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ -n "$sep" ]]; then
|
||||
if [[ -n $sep ]]; then
|
||||
str_arg=
|
||||
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||
args=("${args[@]:0:$sep}")
|
||||
args=("${args[@]:0:sep}")
|
||||
else
|
||||
str_arg=$1
|
||||
args=()
|
||||
@@ -365,25 +383,27 @@ _fzf_complete() {
|
||||
fi
|
||||
|
||||
local cur selected trigger cmd post
|
||||
post="$(caller 0 | command awk '{print $2}')_post"
|
||||
post="$(caller 0 | __fzf_exec_awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
if [[ $cur == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$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
|
||||
if [[ -n $selected ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
@@ -409,7 +429,33 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
@@ -417,7 +463,7 @@ _fzf_proc_completion() {
|
||||
}
|
||||
|
||||
_fzf_proc_completion_post() {
|
||||
command awk '{print $2}'
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
# To use custom hostname lists, override __fzf_list_hosts.
|
||||
@@ -434,10 +480,54 @@ _fzf_proc_completion_post() {
|
||||
# }
|
||||
if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "set -f" and "GLOBIGNORE", which disable the pathname expansion
|
||||
# totally or partially. We need to unset "dotglob" and "nocaseglob" to
|
||||
# avoid matching unwanted files. We need to unset "failglob" to avoid
|
||||
# outputting the error messages to the terminal when no matching is
|
||||
# found. We need to set "nullglob" to avoid attempting to read the
|
||||
# literal filename '~/.ssh/config.d/*' when no matching is found.
|
||||
set +f
|
||||
GLOBIGNORE=
|
||||
shopt -u dotglob nocaseglob failglob
|
||||
shopt -s nullglob
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -452,13 +542,13 @@ _fzf_host_completion() {
|
||||
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
|
||||
_fzf_complete_ssh() {
|
||||
case $3 in
|
||||
-i|-F|-E)
|
||||
-i | -F | -E)
|
||||
_fzf_path_completion "$@"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}')
|
||||
[[ $2 =~ '@' ]] && user="${2%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -546,7 +636,7 @@ __fzf_defc() {
|
||||
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
|
||||
eval "$REPLY"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
eval "complete -F \"$func\" $opts \"$cmd\""
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -595,5 +685,6 @@ _fzf_setup_completion() {
|
||||
esac
|
||||
done
|
||||
}
|
||||
#----END shfmt
|
||||
|
||||
fi
|
||||
|
||||
@@ -96,14 +96,32 @@ if [[ -o interactive ]]; then
|
||||
|
||||
###########################################################
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && ((d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004)) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
@@ -242,11 +260,50 @@ _fzf_complete() {
|
||||
# desired sorting and with any duplicates removed, to standard output.
|
||||
if ! declare -f __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
setopt localoptions nonomatch
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "NO_GLOB" (or reset "GLOB"), which disable the pathname
|
||||
# expansion totally. We need to unset "DOT_GLOB" and set "CASE_GLOB"
|
||||
# to avoid matching unwanted files. We need to set "NULL_GLOB" to
|
||||
# avoid attempting to read the literal filename '~/.ssh/config.d/*'
|
||||
# when no matching is found.
|
||||
setopt GLOB NO_DOT_GLOB CASE_GLOB NO_NOMATCH NULL_GLOB
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
@@ -266,7 +323,7 @@ _fzf_complete_ssh() {
|
||||
*)
|
||||
local user
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -290,7 +347,33 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap -- "$@" < <(
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
@@ -298,7 +381,7 @@ _fzf_complete_kill() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill_post() {
|
||||
awk '{print $2}'
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
fzf-completion() {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
@@ -17,14 +18,33 @@ if [[ $- =~ i ]]; then
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
#----BEGIN shfmt
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && ((d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004)) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_select__() {
|
||||
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") \
|
||||
@@ -35,14 +55,14 @@ __fzf_select__() {
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
|
||||
[[ -n ${TMUX_PANE-} ]] && { [[ ${FZF_TMUX:-0} != 0 ]] || [[ -n ${FZF_TMUX_OPTS-} ]]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
local selected="$(__fzf_select__ "$@")"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||
READLINE_LINE="${READLINE_LINE:0:READLINE_POINT}$selected${READLINE_LINE:READLINE_POINT}"
|
||||
READLINE_POINT=$((READLINE_POINT + ${#selected}))
|
||||
}
|
||||
|
||||
__fzf_cd__() {
|
||||
@@ -62,11 +82,11 @@ if command -v perl > /dev/null; then
|
||||
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 --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
@@ -74,13 +94,7 @@ if command -v perl > /dev/null; then
|
||||
}
|
||||
else # awk - fallback for POSIX systems
|
||||
__fzf_history__() {
|
||||
local output script n x y z d
|
||||
if [[ -z $__fzf_awk ]]; then
|
||||
__fzf_awk=awk
|
||||
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
|
||||
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
local output script
|
||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
||||
NR==1 { b = substr($0, 2); next }
|
||||
@@ -90,12 +104,12 @@ else # awk - fallback for POSIX systems
|
||||
output=$(
|
||||
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 --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
if [[ -z $READLINE_POINT ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
@@ -110,37 +124,48 @@ bind -m vi-command '"\C-z": emacs-editing-mode'
|
||||
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||
|
||||
if (( BASH_VERSINFO[0] < 4 )); then
|
||||
if ((BASH_VERSINFO[0] < 4)); then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
fi
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
if [[ ${FZF_CTRL_T_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-command -x '"\C-r": __fzf_history__'
|
||||
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
||||
fi
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
if [[ ${FZF_ALT_C_COMMAND-x} != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
fi
|
||||
#----END shfmt
|
||||
|
||||
fi
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
@@ -14,15 +15,120 @@
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
|
||||
# command substitution syntax $(cmd) should never be used, even behind a version
|
||||
# check, otherwise the source command will fail on fish versions older than 3.4.0.
|
||||
function fzf_key_bindings
|
||||
|
||||
# Check fish version
|
||||
set -l fish_ver (string match -r '^(\d+).(\d+)' $version 2> /dev/null; or echo 0\n0\n0)
|
||||
if test \( "$fish_ver[2]" -lt 3 \) -o \( "$fish_ver[2]" -eq 3 -a "$fish_ver[3]" -lt 1 \)
|
||||
echo "This script requires fish version 3.1b1 or newer." >&2
|
||||
return 1
|
||||
else if not type -q fzf
|
||||
echo "fzf was not found in path." >&2
|
||||
return 1
|
||||
end
|
||||
|
||||
function __fzf_defaults
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
|
||||
test -r "$FZF_DEFAULT_OPTS_FILE"; and string collect -N -- <$FZF_DEFAULT_OPTS_FILE
|
||||
echo $FZF_DEFAULT_OPTS $argv[2]
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
# Set variables containing the major and minor fish version numbers, using
|
||||
# a method compatible with all supported fish versions.
|
||||
set -l -- fish_major (string match -r -- '^\d+' $version)
|
||||
set -l -- fish_minor (string match -r -- '^\d+\.(\d+)' $version)[2]
|
||||
|
||||
# fish v3.3.0 and newer: Don't use option prefix if " -- " is preceded.
|
||||
set -l -- match_regex '(?<fzf_query>[\s\S]*?(?=\n?$)$)'
|
||||
set -l -- prefix_regex '^-[^\s=]+=|^-(?!-)\S'
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -lt 3
|
||||
or string match -q -v -- '* -- *' (string sub -l (commandline -Cp) -- (commandline -p))
|
||||
set -- match_regex "(?<prefix>$prefix_regex)?$match_regex"
|
||||
end
|
||||
|
||||
# Set $prefix and expanded $fzf_query with preserved trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokens-expanded | string collect -N)
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- $match_regex (commandline --current-token --tokenize | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)' '')
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -l -- cl_token (commandline --current-token --tokenize | string collect -N)
|
||||
set -- prefix (string match -r -- $prefix_regex $cl_token)
|
||||
set -- fzf_query (string replace -- "$prefix" '' $cl_token | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^\\\(?=~)|\\\(?=\$\w)|\\\n\\\n$' '')
|
||||
end
|
||||
|
||||
if test -n "$fzf_query"
|
||||
# Normalize path in $fzf_query, set $dir to the longest existing directory.
|
||||
if test \( "$fish_major" -ge 4 \) -o \( "$fish_major" -eq 3 -a "$fish_minor" -ge 5 \)
|
||||
# fish v3.5.0 and newer
|
||||
set -- fzf_query (path normalize -- $fzf_query)
|
||||
set -- dir $fzf_query
|
||||
while not path is -d $dir
|
||||
set -- dir (path dirname $dir)
|
||||
end
|
||||
else
|
||||
# fish older than v3.5.0 (v3.1b1 - v3.4.1)
|
||||
if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.4.1
|
||||
string match -q -r -- '(?<fzf_query>^[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish v3.1b1 - v3.1.2
|
||||
set -- fzf_query (string replace -r -a -- '(?<=/)/|(?<!^)/+(?!\n)$' '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r '\\\n$' '')
|
||||
end
|
||||
set -- dir $fzf_query
|
||||
while not test -d "$dir"
|
||||
set -- dir (dirname -z -- "$dir" | string split0)
|
||||
end
|
||||
end
|
||||
|
||||
if not string match -q -- '.' $dir; or string match -q -r -- '^\./|^\.$' $fzf_query
|
||||
# Strip $dir from $fzf_query - preserve trailing newlines.
|
||||
if test "$fish_major" -ge 4
|
||||
# fish v4.0.0 and newer
|
||||
string match -q -r -- '^'(string escape --style=regex -- $dir)'/?(?<fzf_query>[\s\S]*)' $fzf_query
|
||||
else if test "$fish_major" -eq 3 -a "$fish_minor" -ge 2
|
||||
# fish v3.2.0 - v3.7.1 (last v3)
|
||||
string match -q -r -- '^/?(?<fzf_query>[\s\S]*?(?=\n?$)$)' \
|
||||
(string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
else
|
||||
# fish older than v3.2.0 (v3.1b1 - v3.1.2)
|
||||
set -- fzf_query (string replace -- "$dir" '' $fzf_query | string collect -N)
|
||||
eval set -- fzf_query (string escape -n -- $fzf_query | string replace -r -a '^/?|\\\n$' '')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
@@ -31,40 +137,35 @@ function fzf_key_bindings
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
set -l result
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_CTRL_T_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||
"$FZF_CTRL_T_OPTS --multi --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set result (eval (__fzfcmd) -m --query=$fzf_query)
|
||||
end
|
||||
if test -z "$result"
|
||||
commandline -f repaint
|
||||
return
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape -- $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
# merge history from other sessions before searching
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
set -l -- command_line (commandline)
|
||||
set -l -- current_line (commandline -L)
|
||||
set -l -- total_lines (count $command_line)
|
||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"\t"↳ ' --highlight-line +m $FZF_CTRL_R_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||
'--nth=2..,.. --scheme=history --multi --wrap-sign="\t↳ "' \
|
||||
'--bind=\'shift-delete:execute-silent(eval history delete --exact --case-sensitive -- (string escape -n -- {+} | string replace -r -a "^\d*\\\\\\t|(?<=\\\\\\n)\\\\\\t" ""))+reload(eval $FZF_DEFAULT_COMMAND)\'' \
|
||||
"--bind=ctrl-r:toggle-sort,alt-r:toggle-raw --highlight-line $FZF_CTRL_R_OPTS" \
|
||||
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND
|
||||
string match -q -r -- '/fish$' $SHELL; or set -lx SHELL (type -p fish)
|
||||
|
||||
if type -q perl
|
||||
set -a FZF_DEFAULT_OPTS '--tac'
|
||||
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
||||
@@ -75,9 +176,21 @@ function fzf_key_bindings
|
||||
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
||||
'end'
|
||||
end
|
||||
set -l result (eval "$FZF_DEFAULT_COMMAND | $(__fzfcmd) --read0 --print0 -q (commandline) --bind='enter:become:string replace -a -- \n\t \n {2..} | string collect'")
|
||||
and commandline -- $result
|
||||
|
||||
# Merge history from other sessions before searching
|
||||
test -z "$fish_private_mode"; and builtin history merge
|
||||
|
||||
if set -l result (eval $FZF_DEFAULT_COMMAND \| (__fzfcmd) --query=$fzf_query | string split0)
|
||||
if test "$total_lines" -eq 1
|
||||
commandline -- (string replace -a -- \n\t \n $result)
|
||||
else
|
||||
set -l a (math $current_line - 1)
|
||||
set -l b (math $current_line + 1)
|
||||
commandline -- $command_line[1..$a] (string replace -a -- \n\t \n $result)
|
||||
commandline -a -- '' $command_line[$b..-1]
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
@@ -87,113 +200,37 @@ function fzf_key_bindings
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root=$dir" "$FZF_ALT_C_OPTS")
|
||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=dir,follow,hidden --scheme=path" \
|
||||
"$FZF_ALT_C_OPTS --no-multi --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
set -l result (eval (__fzfcmd) +m --query=$fzf_query)
|
||||
|
||||
if test -n "$result"
|
||||
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
|
||||
cd -- $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
commandline -it -- $prefix
|
||||
end
|
||||
commandline -rt -- $prefix
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
if test -n "$FZF_TMUX_OPTS"
|
||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
if not set -q FZF_CTRL_R_COMMAND; or test -n "$FZF_CTRL_R_COMMAND"
|
||||
if test -n "$FZF_CTRL_R_COMMAND"
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
end
|
||||
bind \cr fzf-history-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
end
|
||||
|
||||
bind \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind \ct fzf-file-widget
|
||||
end
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
bind -M insert \cr fzf-history-widget
|
||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l commandline (commandline -t)
|
||||
|
||||
# strip -option= from token if present
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
|
||||
# Enable home directory expansion of leading ~/
|
||||
set commandline (string replace -r -- '^~/' '\$HOME/' $commandline)
|
||||
|
||||
# escape special characters, except for the $ sign of valid variable names,
|
||||
# so that after eval, the original string is returned, but with the
|
||||
# variable names replaced by their values.
|
||||
set commandline (string escape -n -- $commandline)
|
||||
set commandline (string replace -r -a -- '\x5c\$(?=[\w])' '\$' $commandline)
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
eval set commandline $commandline
|
||||
|
||||
# Combine multiple consecutive slashes into one
|
||||
set commandline (string replace -r -a -- '/+' '/' $commandline)
|
||||
|
||||
if test -z "$commandline"
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
else
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
|
||||
# BUG: on combined expressions, if a left argument is a single `!`, the
|
||||
# builtin test command of fish will treat it as the ! operator. To
|
||||
# overcome this, have the variable parts on the right.
|
||||
if test "." = "$dir" -a "./" != (string sub -l 2 -- $commandline)
|
||||
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||
set fzf_query $commandline
|
||||
else
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r -- "^$dir/?" '' $commandline)
|
||||
end
|
||||
end
|
||||
|
||||
echo (string escape -- $dir)
|
||||
echo (string escape -- $fzf_query)
|
||||
echo $prefix
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
|
||||
# Strip trailing slash, unless $dir is root dir (/)
|
||||
set dir (string replace -r -- '(?<!^)/$' '' $dir)
|
||||
|
||||
# Iteratively check if dir exists and strip tail end of path
|
||||
while test ! -d "$dir"
|
||||
# If path is absolute, this can keep going until ends up at /
|
||||
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||
set dir (dirname -- "$dir")
|
||||
end
|
||||
|
||||
echo $dir
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
# - $FZF_TMUX_OPTS
|
||||
# - $FZF_CTRL_T_COMMAND
|
||||
# - $FZF_CTRL_T_OPTS
|
||||
# - $FZF_CTRL_R_COMMAND
|
||||
# - $FZF_CTRL_R_OPTS
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
@@ -38,14 +39,32 @@ fi
|
||||
{
|
||||
if [[ -o interactive ]]; then
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
echo "--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore $1"
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk > /dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && ((d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004)) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fzf_select() {
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
@@ -114,16 +133,16 @@ fzf-history-widget() {
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,alt-r:toggle-raw --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") \
|
||||
selected="$(fc -rl 1 | __fzf_exec_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,alt-r:toggle-raw --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
|
||||
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||
if [[ $(__fzf_exec_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"
|
||||
@@ -132,10 +151,15 @@ fzf-history-widget() {
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
if [[ ${FZF_CTRL_R_COMMAND-x} != "" ]]; then
|
||||
if [[ -n ${FZF_CTRL_R_COMMAND-} ]]; then
|
||||
echo "warning: FZF_CTRL_R_COMMAND is set to a custom command, but custom commands are not yet supported for CTRL-R" >&2
|
||||
fi
|
||||
zle -N fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
fi
|
||||
fi
|
||||
|
||||
} always {
|
||||
|
||||
68
shell/update.sh
Executable file
68
shell/update.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script applies the contents of "common.sh" to the other files.
|
||||
|
||||
set -e
|
||||
|
||||
dir=${0%"${0##*/}"}
|
||||
|
||||
update() {
|
||||
{
|
||||
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
|
||||
cat << EOF
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
EOF
|
||||
echo
|
||||
grep -v '^[[:blank:]]*#' "$dir/common.sh" # remove code comments in common.sh
|
||||
sed -n '/^#----END INCLUDE/,$p' "$1"
|
||||
} > "$1.part"
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
}
|
||||
|
||||
update "$dir/completion.bash"
|
||||
update "$dir/completion.zsh"
|
||||
update "$dir/key-bindings.bash"
|
||||
update "$dir/key-bindings.zsh"
|
||||
|
||||
# Check if --check is in ARGV
|
||||
check=0
|
||||
rest=()
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
--check) check=1 ;;
|
||||
*) rest+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
fmt() {
|
||||
if ! grep -q "^#----BEGIN shfmt" "$1"; then
|
||||
if [[ $check == 1 ]]; then
|
||||
shfmt -d "$1"
|
||||
return $?
|
||||
else
|
||||
shfmt -w "$1"
|
||||
fi
|
||||
else
|
||||
{
|
||||
sed -n '1,/^#----BEGIN shfmt/p' "$1" | sed '$d'
|
||||
sed -n '/^#----BEGIN shfmt/,/^#----END shfmt/p' "$1" | shfmt --filename "$1"
|
||||
sed -n '/^#----END shfmt/,$p' "$1" | sed '1d'
|
||||
} > "$1.part"
|
||||
|
||||
if [[ $check == 1 ]]; then
|
||||
diff -q "$1" "$1.part"
|
||||
ret=$?
|
||||
rm -f "$1.part"
|
||||
return $ret
|
||||
fi
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
for file in "${rest[@]}"; do
|
||||
fmt "$file" || exit $?
|
||||
done
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2024 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -12,129 +12,177 @@ func _() {
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actChar-4]
|
||||
_ = x[actMouse-5]
|
||||
_ = x[actBeginningOfLine-6]
|
||||
_ = x[actAbort-7]
|
||||
_ = x[actAccept-8]
|
||||
_ = x[actAcceptNonEmpty-9]
|
||||
_ = x[actAcceptOrPrintQuery-10]
|
||||
_ = x[actBackwardChar-11]
|
||||
_ = x[actBackwardDeleteChar-12]
|
||||
_ = x[actBackwardDeleteCharEof-13]
|
||||
_ = x[actBackwardWord-14]
|
||||
_ = x[actCancel-15]
|
||||
_ = x[actChangeBorderLabel-16]
|
||||
_ = x[actChangeListLabel-17]
|
||||
_ = x[actChangeInputLabel-18]
|
||||
_ = x[actChangeHeader-19]
|
||||
_ = x[actChangeHeaderLabel-20]
|
||||
_ = x[actChangeMulti-21]
|
||||
_ = x[actChangePreviewLabel-22]
|
||||
_ = x[actChangePrompt-23]
|
||||
_ = x[actChangeQuery-24]
|
||||
_ = x[actChangeNth-25]
|
||||
_ = x[actClearScreen-26]
|
||||
_ = x[actClearQuery-27]
|
||||
_ = x[actClearSelection-28]
|
||||
_ = x[actClose-29]
|
||||
_ = x[actDeleteChar-30]
|
||||
_ = x[actDeleteCharEof-31]
|
||||
_ = x[actEndOfLine-32]
|
||||
_ = x[actFatal-33]
|
||||
_ = x[actForwardChar-34]
|
||||
_ = x[actForwardWord-35]
|
||||
_ = x[actKillLine-36]
|
||||
_ = x[actKillWord-37]
|
||||
_ = x[actUnixLineDiscard-38]
|
||||
_ = x[actUnixWordRubout-39]
|
||||
_ = x[actYank-40]
|
||||
_ = x[actBackwardKillWord-41]
|
||||
_ = x[actSelectAll-42]
|
||||
_ = x[actDeselectAll-43]
|
||||
_ = x[actToggle-44]
|
||||
_ = x[actToggleSearch-45]
|
||||
_ = x[actToggleAll-46]
|
||||
_ = x[actToggleDown-47]
|
||||
_ = x[actToggleUp-48]
|
||||
_ = x[actToggleIn-49]
|
||||
_ = x[actToggleOut-50]
|
||||
_ = x[actToggleTrack-51]
|
||||
_ = x[actToggleTrackCurrent-52]
|
||||
_ = x[actToggleHeader-53]
|
||||
_ = x[actToggleWrap-54]
|
||||
_ = x[actToggleMultiLine-55]
|
||||
_ = x[actToggleHscroll-56]
|
||||
_ = x[actTrackCurrent-57]
|
||||
_ = x[actUntrackCurrent-58]
|
||||
_ = x[actDown-59]
|
||||
_ = x[actUp-60]
|
||||
_ = x[actPageUp-61]
|
||||
_ = x[actPageDown-62]
|
||||
_ = x[actPosition-63]
|
||||
_ = x[actHalfPageUp-64]
|
||||
_ = x[actHalfPageDown-65]
|
||||
_ = x[actOffsetUp-66]
|
||||
_ = x[actOffsetDown-67]
|
||||
_ = x[actOffsetMiddle-68]
|
||||
_ = x[actJump-69]
|
||||
_ = x[actJumpAccept-70]
|
||||
_ = x[actPrintQuery-71]
|
||||
_ = x[actRefreshPreview-72]
|
||||
_ = x[actReplaceQuery-73]
|
||||
_ = x[actToggleSort-74]
|
||||
_ = x[actShowPreview-75]
|
||||
_ = x[actHidePreview-76]
|
||||
_ = x[actTogglePreview-77]
|
||||
_ = x[actTogglePreviewWrap-78]
|
||||
_ = x[actTransform-79]
|
||||
_ = x[actTransformBorderLabel-80]
|
||||
_ = x[actTransformListLabel-81]
|
||||
_ = x[actTransformInputLabel-82]
|
||||
_ = x[actTransformHeader-83]
|
||||
_ = x[actTransformHeaderLabel-84]
|
||||
_ = x[actTransformPreviewLabel-85]
|
||||
_ = x[actTransformPrompt-86]
|
||||
_ = x[actTransformQuery-87]
|
||||
_ = x[actPreview-88]
|
||||
_ = x[actChangePreview-89]
|
||||
_ = x[actChangePreviewWindow-90]
|
||||
_ = x[actPreviewTop-91]
|
||||
_ = x[actPreviewBottom-92]
|
||||
_ = x[actPreviewUp-93]
|
||||
_ = x[actPreviewDown-94]
|
||||
_ = x[actPreviewPageUp-95]
|
||||
_ = x[actPreviewPageDown-96]
|
||||
_ = x[actPreviewHalfPageUp-97]
|
||||
_ = x[actPreviewHalfPageDown-98]
|
||||
_ = x[actPrevHistory-99]
|
||||
_ = x[actPrevSelected-100]
|
||||
_ = x[actPrint-101]
|
||||
_ = x[actPut-102]
|
||||
_ = x[actNextHistory-103]
|
||||
_ = x[actNextSelected-104]
|
||||
_ = x[actExecute-105]
|
||||
_ = x[actExecuteSilent-106]
|
||||
_ = x[actExecuteMulti-107]
|
||||
_ = x[actSigStop-108]
|
||||
_ = x[actFirst-109]
|
||||
_ = x[actLast-110]
|
||||
_ = x[actReload-111]
|
||||
_ = x[actReloadSync-112]
|
||||
_ = x[actDisableSearch-113]
|
||||
_ = x[actEnableSearch-114]
|
||||
_ = x[actSelect-115]
|
||||
_ = x[actDeselect-116]
|
||||
_ = x[actUnbind-117]
|
||||
_ = x[actRebind-118]
|
||||
_ = x[actBecome-119]
|
||||
_ = x[actShowHeader-120]
|
||||
_ = x[actHideHeader-121]
|
||||
_ = x[actBracketedPasteBegin-4]
|
||||
_ = x[actBracketedPasteEnd-5]
|
||||
_ = x[actChar-6]
|
||||
_ = x[actMouse-7]
|
||||
_ = x[actBeginningOfLine-8]
|
||||
_ = x[actAbort-9]
|
||||
_ = x[actAccept-10]
|
||||
_ = x[actAcceptNonEmpty-11]
|
||||
_ = x[actAcceptOrPrintQuery-12]
|
||||
_ = x[actBackwardChar-13]
|
||||
_ = x[actBackwardDeleteChar-14]
|
||||
_ = x[actBackwardDeleteCharEof-15]
|
||||
_ = x[actBackwardWord-16]
|
||||
_ = x[actBackwardSubWord-17]
|
||||
_ = x[actCancel-18]
|
||||
_ = x[actChangeBorderLabel-19]
|
||||
_ = x[actChangeGhost-20]
|
||||
_ = x[actChangeHeader-21]
|
||||
_ = x[actChangeFooter-22]
|
||||
_ = x[actChangeHeaderLabel-23]
|
||||
_ = x[actChangeFooterLabel-24]
|
||||
_ = x[actChangeInputLabel-25]
|
||||
_ = x[actChangeListLabel-26]
|
||||
_ = x[actChangeMulti-27]
|
||||
_ = x[actChangeNth-28]
|
||||
_ = x[actChangePointer-29]
|
||||
_ = x[actChangePreview-30]
|
||||
_ = x[actChangePreviewLabel-31]
|
||||
_ = x[actChangePreviewWindow-32]
|
||||
_ = x[actChangePrompt-33]
|
||||
_ = x[actChangeQuery-34]
|
||||
_ = x[actClearScreen-35]
|
||||
_ = x[actClearQuery-36]
|
||||
_ = x[actClearSelection-37]
|
||||
_ = x[actClose-38]
|
||||
_ = x[actDeleteChar-39]
|
||||
_ = x[actDeleteCharEof-40]
|
||||
_ = x[actEndOfLine-41]
|
||||
_ = x[actFatal-42]
|
||||
_ = x[actForwardChar-43]
|
||||
_ = x[actForwardWord-44]
|
||||
_ = x[actForwardSubWord-45]
|
||||
_ = x[actKillLine-46]
|
||||
_ = x[actKillWord-47]
|
||||
_ = x[actKillSubWord-48]
|
||||
_ = x[actUnixLineDiscard-49]
|
||||
_ = x[actUnixWordRubout-50]
|
||||
_ = x[actYank-51]
|
||||
_ = x[actBackwardKillWord-52]
|
||||
_ = x[actBackwardKillSubWord-53]
|
||||
_ = x[actSelectAll-54]
|
||||
_ = x[actDeselectAll-55]
|
||||
_ = x[actToggle-56]
|
||||
_ = x[actToggleSearch-57]
|
||||
_ = x[actToggleAll-58]
|
||||
_ = x[actToggleDown-59]
|
||||
_ = x[actToggleUp-60]
|
||||
_ = x[actToggleIn-61]
|
||||
_ = x[actToggleOut-62]
|
||||
_ = x[actToggleTrack-63]
|
||||
_ = x[actToggleTrackCurrent-64]
|
||||
_ = x[actToggleHeader-65]
|
||||
_ = x[actToggleWrap-66]
|
||||
_ = x[actToggleMultiLine-67]
|
||||
_ = x[actToggleHscroll-68]
|
||||
_ = x[actToggleRaw-69]
|
||||
_ = x[actEnableRaw-70]
|
||||
_ = x[actDisableRaw-71]
|
||||
_ = x[actTrackCurrent-72]
|
||||
_ = x[actToggleInput-73]
|
||||
_ = x[actHideInput-74]
|
||||
_ = x[actShowInput-75]
|
||||
_ = x[actUntrackCurrent-76]
|
||||
_ = x[actDown-77]
|
||||
_ = x[actDownMatch-78]
|
||||
_ = x[actUp-79]
|
||||
_ = x[actUpMatch-80]
|
||||
_ = x[actPageUp-81]
|
||||
_ = x[actPageDown-82]
|
||||
_ = x[actPosition-83]
|
||||
_ = x[actHalfPageUp-84]
|
||||
_ = x[actHalfPageDown-85]
|
||||
_ = x[actOffsetUp-86]
|
||||
_ = x[actOffsetDown-87]
|
||||
_ = x[actOffsetMiddle-88]
|
||||
_ = x[actJump-89]
|
||||
_ = x[actJumpAccept-90]
|
||||
_ = x[actPrintQuery-91]
|
||||
_ = x[actRefreshPreview-92]
|
||||
_ = x[actReplaceQuery-93]
|
||||
_ = x[actToggleSort-94]
|
||||
_ = x[actShowPreview-95]
|
||||
_ = x[actHidePreview-96]
|
||||
_ = x[actTogglePreview-97]
|
||||
_ = x[actTogglePreviewWrap-98]
|
||||
_ = x[actTransform-99]
|
||||
_ = x[actTransformBorderLabel-100]
|
||||
_ = x[actTransformGhost-101]
|
||||
_ = x[actTransformHeader-102]
|
||||
_ = x[actTransformFooter-103]
|
||||
_ = x[actTransformHeaderLabel-104]
|
||||
_ = x[actTransformFooterLabel-105]
|
||||
_ = x[actTransformInputLabel-106]
|
||||
_ = x[actTransformListLabel-107]
|
||||
_ = x[actTransformNth-108]
|
||||
_ = x[actTransformPointer-109]
|
||||
_ = x[actTransformPreviewLabel-110]
|
||||
_ = x[actTransformPrompt-111]
|
||||
_ = x[actTransformQuery-112]
|
||||
_ = x[actTransformSearch-113]
|
||||
_ = x[actTrigger-114]
|
||||
_ = x[actBgTransform-115]
|
||||
_ = x[actBgTransformBorderLabel-116]
|
||||
_ = x[actBgTransformGhost-117]
|
||||
_ = x[actBgTransformHeader-118]
|
||||
_ = x[actBgTransformFooter-119]
|
||||
_ = x[actBgTransformHeaderLabel-120]
|
||||
_ = x[actBgTransformFooterLabel-121]
|
||||
_ = x[actBgTransformInputLabel-122]
|
||||
_ = x[actBgTransformListLabel-123]
|
||||
_ = x[actBgTransformNth-124]
|
||||
_ = x[actBgTransformPointer-125]
|
||||
_ = x[actBgTransformPreviewLabel-126]
|
||||
_ = x[actBgTransformPrompt-127]
|
||||
_ = x[actBgTransformQuery-128]
|
||||
_ = x[actBgTransformSearch-129]
|
||||
_ = x[actBgCancel-130]
|
||||
_ = x[actSearch-131]
|
||||
_ = x[actPreview-132]
|
||||
_ = x[actPreviewTop-133]
|
||||
_ = x[actPreviewBottom-134]
|
||||
_ = x[actPreviewUp-135]
|
||||
_ = x[actPreviewDown-136]
|
||||
_ = x[actPreviewPageUp-137]
|
||||
_ = x[actPreviewPageDown-138]
|
||||
_ = x[actPreviewHalfPageUp-139]
|
||||
_ = x[actPreviewHalfPageDown-140]
|
||||
_ = x[actPrevHistory-141]
|
||||
_ = x[actPrevSelected-142]
|
||||
_ = x[actPrint-143]
|
||||
_ = x[actPut-144]
|
||||
_ = x[actNextHistory-145]
|
||||
_ = x[actNextSelected-146]
|
||||
_ = x[actExecute-147]
|
||||
_ = x[actExecuteSilent-148]
|
||||
_ = x[actExecuteMulti-149]
|
||||
_ = x[actSigStop-150]
|
||||
_ = x[actBest-151]
|
||||
_ = x[actFirst-152]
|
||||
_ = x[actLast-153]
|
||||
_ = x[actReload-154]
|
||||
_ = x[actReloadSync-155]
|
||||
_ = x[actDisableSearch-156]
|
||||
_ = x[actEnableSearch-157]
|
||||
_ = x[actSelect-158]
|
||||
_ = x[actDeselect-159]
|
||||
_ = x[actUnbind-160]
|
||||
_ = x[actRebind-161]
|
||||
_ = x[actToggleBind-162]
|
||||
_ = x[actBecome-163]
|
||||
_ = x[actShowHeader-164]
|
||||
_ = x[actHideHeader-165]
|
||||
_ = x[actBell-166]
|
||||
_ = x[actExclude-167]
|
||||
_ = x[actExcludeMulti-168]
|
||||
_ = x[actAsync-169]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeListLabelactChangeInputLabelactChangeHeaderactChangeHeaderLabelactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactChangeNthactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformListLabelactTransformInputLabelactTransformHeaderactTransformHeaderLabelactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactShowHeaderactHideHeader"
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactToggleRawactEnableRawactDisableRawactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactDownMatchactUpactUpMatchactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactBestactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 245, 264, 279, 299, 313, 334, 349, 363, 375, 389, 402, 419, 427, 440, 456, 468, 476, 490, 504, 515, 526, 544, 561, 568, 587, 599, 613, 622, 637, 649, 662, 673, 684, 696, 710, 731, 746, 759, 777, 793, 808, 825, 832, 837, 846, 857, 868, 881, 896, 907, 920, 935, 942, 955, 968, 985, 1000, 1013, 1027, 1041, 1057, 1077, 1089, 1112, 1133, 1155, 1173, 1196, 1220, 1238, 1255, 1265, 1281, 1303, 1316, 1332, 1344, 1358, 1374, 1392, 1412, 1434, 1448, 1463, 1471, 1477, 1491, 1506, 1516, 1532, 1547, 1557, 1565, 1572, 1581, 1594, 1610, 1625, 1634, 1645, 1654, 1663, 1672, 1685, 1698}
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1021, 1033, 1046, 1061, 1075, 1087, 1099, 1116, 1123, 1135, 1140, 1150, 1159, 1170, 1181, 1194, 1209, 1220, 1233, 1248, 1255, 1268, 1281, 1298, 1313, 1326, 1340, 1354, 1370, 1390, 1402, 1425, 1442, 1460, 1478, 1501, 1524, 1546, 1567, 1582, 1601, 1625, 1643, 1660, 1678, 1688, 1702, 1727, 1746, 1766, 1786, 1811, 1836, 1860, 1883, 1900, 1921, 1947, 1967, 1986, 2006, 2017, 2026, 2036, 2049, 2065, 2077, 2091, 2107, 2125, 2145, 2167, 2181, 2196, 2204, 2210, 2224, 2239, 2249, 2265, 2280, 2290, 2297, 2305, 2312, 2321, 2334, 2350, 2365, 2374, 2385, 2394, 2403, 2416, 2425, 2438, 2451, 2458, 2468, 2483, 2491}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
|
||||
@@ -303,7 +303,7 @@ func bonusAt(input *util.Chars, idx int) int16 {
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
@@ -824,7 +827,7 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
|
||||
|
||||
// For simplicity, only look at the bonus at the first character position
|
||||
pidx := 0
|
||||
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
||||
bestPos, bonus, bbonus, bestBonus := -1, int16(0), int16(0), int16(-1)
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
index_ := indexAt(index, lenRunes, forward)
|
||||
char := text.Get(index_)
|
||||
@@ -846,7 +849,16 @@ func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryC
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
if boundaryCheck {
|
||||
ok = bonus >= bonusBoundary
|
||||
if forward && pidx_ == 0 {
|
||||
bbonus = bonus
|
||||
} else if !forward && pidx_ == lenPattern-1 {
|
||||
if index_ < lenRunes-1 {
|
||||
bbonus = bonusAt(text, index_+1)
|
||||
} else {
|
||||
bbonus = bonusBoundaryWhite
|
||||
}
|
||||
}
|
||||
ok = bbonus >= bonusBoundary
|
||||
if ok && pidx_ == 0 {
|
||||
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||
}
|
||||
|
||||
@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
|
||||
bytes[math.MaxUint16] = 'z'
|
||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
|
||||
func TestLongStringWithNormalize(t *testing.T) {
|
||||
bytes := make([]byte, 30000)
|
||||
for i := range bytes {
|
||||
bytes[i] = 'x'
|
||||
}
|
||||
unicodeString := string(bytes) + " Minímal example"
|
||||
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
|
||||
}
|
||||
|
||||
@@ -473,6 +473,103 @@ var normalized = map[rune]rune{
|
||||
'ử': 'u',
|
||||
'ữ': 'u',
|
||||
'ự': 'u',
|
||||
|
||||
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
|
||||
0xFF01: '!', // Fullwidth exclamation
|
||||
0xFF02: '"', // Fullwidth quotation mark
|
||||
0xFF03: '#', // Fullwidth number sign
|
||||
0xFF04: '$', // Fullwidth dollar sign
|
||||
0xFF05: '%', // Fullwidth percent
|
||||
0xFF06: '&', // Fullwidth ampersand
|
||||
0xFF07: '\'', // Fullwidth apostrophe
|
||||
0xFF08: '(', // Fullwidth left parenthesis
|
||||
0xFF09: ')', // Fullwidth right parenthesis
|
||||
0xFF0A: '*', // Fullwidth asterisk
|
||||
0xFF0B: '+', // Fullwidth plus
|
||||
0xFF0C: ',', // Fullwidth comma
|
||||
0xFF0D: '-', // Fullwidth hyphen-minus
|
||||
0xFF0E: '.', // Fullwidth period
|
||||
0xFF0F: '/', // Fullwidth slash
|
||||
0xFF10: '0',
|
||||
0xFF11: '1',
|
||||
0xFF12: '2',
|
||||
0xFF13: '3',
|
||||
0xFF14: '4',
|
||||
0xFF15: '5',
|
||||
0xFF16: '6',
|
||||
0xFF17: '7',
|
||||
0xFF18: '8',
|
||||
0xFF19: '9',
|
||||
0xFF1A: ':', // Fullwidth colon
|
||||
0xFF1B: ';', // Fullwidth semicolon
|
||||
0xFF1C: '<', // Fullwidth less-than
|
||||
0xFF1D: '=', // Fullwidth equal
|
||||
0xFF1E: '>', // Fullwidth greater-than
|
||||
0xFF1F: '?', // Fullwidth question mark
|
||||
0xFF20: '@', // Fullwidth at sign
|
||||
0xFF21: 'A',
|
||||
0xFF22: 'B',
|
||||
0xFF23: 'C',
|
||||
0xFF24: 'D',
|
||||
0xFF25: 'E',
|
||||
0xFF26: 'F',
|
||||
0xFF27: 'G',
|
||||
0xFF28: 'H',
|
||||
0xFF29: 'I',
|
||||
0xFF2A: 'J',
|
||||
0xFF2B: 'K',
|
||||
0xFF2C: 'L',
|
||||
0xFF2D: 'M',
|
||||
0xFF2E: 'N',
|
||||
0xFF2F: 'O',
|
||||
0xFF30: 'P',
|
||||
0xFF31: 'Q',
|
||||
0xFF32: 'R',
|
||||
0xFF33: 'S',
|
||||
0xFF34: 'T',
|
||||
0xFF35: 'U',
|
||||
0xFF36: 'V',
|
||||
0xFF37: 'W',
|
||||
0xFF38: 'X',
|
||||
0xFF39: 'Y',
|
||||
0xFF3A: 'Z',
|
||||
0xFF3B: '[', // Fullwidth left bracket
|
||||
0xFF3C: '\\', // Fullwidth backslash
|
||||
0xFF3D: ']', // Fullwidth right bracket
|
||||
0xFF3E: '^', // Fullwidth circumflex
|
||||
0xFF3F: '_', // Fullwidth underscore
|
||||
0xFF40: '`', // Fullwidth grave accent
|
||||
0xFF41: 'a',
|
||||
0xFF42: 'b',
|
||||
0xFF43: 'c',
|
||||
0xFF44: 'd',
|
||||
0xFF45: 'e',
|
||||
0xFF46: 'f',
|
||||
0xFF47: 'g',
|
||||
0xFF48: 'h',
|
||||
0xFF49: 'i',
|
||||
0xFF4A: 'j',
|
||||
0xFF4B: 'k',
|
||||
0xFF4C: 'l',
|
||||
0xFF4D: 'm',
|
||||
0xFF4E: 'n',
|
||||
0xFF4F: 'o',
|
||||
0xFF50: 'p',
|
||||
0xFF51: 'q',
|
||||
0xFF52: 'r',
|
||||
0xFF53: 's',
|
||||
0xFF54: 't',
|
||||
0xFF55: 'u',
|
||||
0xFF56: 'v',
|
||||
0xFF57: 'w',
|
||||
0xFF58: 'x',
|
||||
0xFF59: 'y',
|
||||
0xFF5A: 'z',
|
||||
0xFF5B: '{', // Fullwidth left brace
|
||||
0xFF5C: '|', // Fullwidth vertical bar
|
||||
0xFF5D: '}', // Fullwidth right brace
|
||||
0xFF5E: '~', // Fullwidth tilde
|
||||
0xFF61: '.', // Halfwidth ideographic full stop
|
||||
}
|
||||
|
||||
// NormalizeRunes normalizes latin script letters
|
||||
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
|
||||
64
src/ansi.go
64
src/ansi.go
@@ -156,13 +156,13 @@ func isCtrlSeqStart(c uint8) bool {
|
||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||
// calling FindStringIndex() on the below regex (which was originally used):
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
|
||||
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
|
||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
// fast check for ANSI escape sequences
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x0e', '\x0f', '\x1b', '\x08':
|
||||
case '\x0e', '\x0f', '\x1b', '\x08', '\n':
|
||||
// We ignore the fact that '\x08' cannot be the first char
|
||||
// in the string and be an escape sequence for the sake of
|
||||
// speed and simplicity.
|
||||
@@ -174,6 +174,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
Loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\n':
|
||||
// match: `\n`
|
||||
return i, i + 1
|
||||
case '\x08':
|
||||
// backtrack to match: `.\x08`
|
||||
if i > 0 && s[i-1] != '\n' {
|
||||
@@ -265,13 +268,30 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
output.WriteString(prev)
|
||||
}
|
||||
|
||||
newState := interpretCode(str[start:idx], state)
|
||||
if !newState.equals(state) {
|
||||
code := str[start:idx]
|
||||
newState := interpretCode(code, state)
|
||||
if code == "\n" || !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if code == "\n" {
|
||||
output.WriteRune('\n')
|
||||
runeCount++
|
||||
// Full-background marker
|
||||
if newState.lbg >= 0 {
|
||||
marker := newState
|
||||
marker.attr |= tui.FullBg
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
marker,
|
||||
})
|
||||
// Reset the full-line background color
|
||||
newState.lbg = -1
|
||||
}
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
if pstate == nil {
|
||||
@@ -349,6 +369,13 @@ func parseAnsiCode(s string) (int, string) {
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
if ansiCode == "\n" {
|
||||
if prevState != nil {
|
||||
return *prevState
|
||||
}
|
||||
return ansiState{-1, -1, 0, -1, nil}
|
||||
}
|
||||
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = ansiState{-1, -1, 0, -1, nil}
|
||||
@@ -356,24 +383,33 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
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") {
|
||||
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
|
||||
state.lbg = prevState.bg
|
||||
} else if ansiCode == "\x1b]8;;\x1b\\" { // End of a hyperlink
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && (strings.HasSuffix(ansiCode, "\x1b\\") || strings.HasSuffix(ansiCode, "\a")) {
|
||||
stLen := 2
|
||||
if strings.HasSuffix(ansiCode, "\a") {
|
||||
stLen = 1
|
||||
}
|
||||
// "\x1b]8;;\x1b\\" or "\x1b]8;;\a"
|
||||
if len(ansiCode) == 5+stLen && ansiCode[4] == ';' {
|
||||
state.url = nil
|
||||
} else if strings.HasPrefix(ansiCode, "\x1b]8;") && strings.HasSuffix(ansiCode, "\x1b\\") {
|
||||
if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
} else if paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||
params := ansiCode[4 : 4+paramsEnd]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-2]
|
||||
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
|
||||
state.url = &url{uri: uri, params: params}
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
if len(ansiCode) <= 3 {
|
||||
reset := func() {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
}
|
||||
|
||||
if len(ansiCode) <= 3 {
|
||||
reset()
|
||||
return state
|
||||
}
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
@@ -427,9 +463,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
case 29:
|
||||
state.attr = state.attr &^ tui.StrikeThrough
|
||||
case 0:
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
reset()
|
||||
state256 = 0
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
@@ -469,9 +503,7 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
|
||||
// Empty sequence: reset
|
||||
if count == 0 {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
reset()
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
|
||||
@@ -41,6 +41,13 @@ func (c *Chunk) IsFull() bool {
|
||||
return c.count == chunkSize
|
||||
}
|
||||
|
||||
func (c *Chunk) lastIndex(minValue int32) int32 {
|
||||
if c.count == 0 {
|
||||
return minValue
|
||||
}
|
||||
return c.items[c.count-1].Index() + 1 // Exclusive
|
||||
}
|
||||
|
||||
func (cl *ChunkList) lastChunk() *Chunk {
|
||||
return cl.chunks[len(cl.chunks)-1]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestChunkList(t *testing.T) {
|
||||
|
||||
// Add more data
|
||||
for i := 0; i < chunkSize*2; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
// Previous snapshot should remain the same
|
||||
@@ -86,7 +86,7 @@ func TestChunkListTail(t *testing.T) {
|
||||
})
|
||||
total := chunkSize*2 + chunkSize/2
|
||||
for i := 0; i < total; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
snapshot, count, changed := cl.Snapshot(0)
|
||||
|
||||
@@ -26,9 +26,13 @@ const (
|
||||
previewCancelWait = 500 * time.Millisecond
|
||||
previewChunkDelay = 100 * time.Millisecond
|
||||
previewDelayed = 500 * time.Millisecond
|
||||
maxPatternLength = 300
|
||||
maxPatternLength = 1000
|
||||
maxMulti = math.MaxInt32
|
||||
|
||||
// Background processes
|
||||
maxBgProcesses = 30
|
||||
maxBgProcessesPerAction = 3
|
||||
|
||||
// Matcher
|
||||
numPartitionsMultiplier = 8
|
||||
maxPartitions = 32
|
||||
|
||||
126
src/core.go
126
src/core.go
@@ -2,10 +2,12 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"maps"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -39,7 +41,7 @@ func (r revision) compatible(other revision) bool {
|
||||
// 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 {
|
||||
if opts.useTmux() {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
|
||||
@@ -74,21 +76,25 @@ func Run(opts *Options) (int, error) {
|
||||
|
||||
var lineAnsiState, prevLineAnsiState *ansiState
|
||||
if opts.Ansi {
|
||||
if opts.Theme.Colored {
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
|
||||
// Full line background is found. Add a special marker.
|
||||
if offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
|
||||
marker := (*offsets)[len(*offsets)-1]
|
||||
marker.offset[0] = marker.offset[1]
|
||||
marker.color.bg = newState.lbg
|
||||
marker.color.attr = marker.color.attr | tui.FullBg
|
||||
newOffsets := append(*offsets, marker)
|
||||
offsets = &newOffsets
|
||||
|
||||
// Reset the full-line background color
|
||||
lineAnsiState.lbg = -1
|
||||
}
|
||||
return util.ToChars(stringBytes(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, _, _ := extractColor(byteString(data), nil, nil)
|
||||
return util.ToChars(stringBytes(trimmed)), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
@@ -96,7 +102,7 @@ func Run(opts *Options) (int, error) {
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
if opts.WithNth == nil {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, byteString(data))
|
||||
@@ -109,9 +115,10 @@ func Run(opts *Options) (int, error) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
if opts.Ansi && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
if prevLineAnsiState != nil {
|
||||
ansiStateDup := *prevLineAnsiState
|
||||
@@ -127,15 +134,24 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
transformed := nthTransformer(tokens, itemIndex)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||
item.text.TrimTrailingWhitespaces()
|
||||
|
||||
// We should not trim trailing whitespaces with background colors
|
||||
var maxColorOffset int32
|
||||
if item.colors != nil {
|
||||
for _, ansi := range *item.colors {
|
||||
if ansi.color.bg >= 0 {
|
||||
maxColorOffset = util.Max32(maxColorOffset, ansi.offset[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
|
||||
item.text.Index = itemIndex
|
||||
item.origText = &data
|
||||
itemIndex++
|
||||
@@ -188,19 +204,34 @@ func Run(opts *Options) (int, error) {
|
||||
forward = false
|
||||
case byBegin:
|
||||
forward = true
|
||||
case byPathname:
|
||||
withPos = true
|
||||
forward = false
|
||||
}
|
||||
}
|
||||
|
||||
nth := opts.Nth
|
||||
nthRevision := 0
|
||||
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, nth, opts.Delimiter, nthRevision, runes)
|
||||
}
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
denyMutex.Lock()
|
||||
denylistCopy := maps.Clone(denylist)
|
||||
denyMutex.Unlock()
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
||||
}
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
@@ -236,11 +267,11 @@ func Run(opts *Options) (int, error) {
|
||||
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
result := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
for i := 0; i < merger.Length(); i++ {
|
||||
opts.Printer(merger.Get(i).item.AsString(opts.Ansi))
|
||||
for i := 0; i < result.merger.Length(); i++ {
|
||||
opts.Printer(result.merger.Get(i).item.AsString(opts.Ansi))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
@@ -277,6 +308,7 @@ func Run(opts *Options) (int, error) {
|
||||
// Event coordination
|
||||
reading := true
|
||||
ticks := 0
|
||||
startTick := 0
|
||||
var nextCommand *commandSpec
|
||||
var nextEnviron []string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
@@ -299,7 +331,11 @@ func Run(opts *Options) (int, error) {
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
startTick = ticks
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
@@ -345,7 +381,8 @@ func Run(opts *Options) (int, error) {
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
if useSnapshot && evt == EvtReadFin {
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
if !useSnapshot {
|
||||
@@ -376,10 +413,21 @@ func Run(opts *Options) (int, error) {
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
nthRevision++
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
@@ -431,12 +479,13 @@ func Run(opts *Options) (int, error) {
|
||||
|
||||
case EvtSearchFin:
|
||||
switch val := value.(type) {
|
||||
case *Merger:
|
||||
case MatchResult:
|
||||
merger := val.merger
|
||||
if deferred {
|
||||
count := val.Length()
|
||||
count := merger.Length()
|
||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||
determine(val.final)
|
||||
} else if val.final {
|
||||
determine(merger.final)
|
||||
} else if merger.final {
|
||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||
if opts.PrintQuery {
|
||||
opts.Printer(opts.Query)
|
||||
@@ -444,8 +493,17 @@ func Run(opts *Options) (int, error) {
|
||||
if len(opts.Expect) > 0 {
|
||||
opts.Printer("")
|
||||
}
|
||||
transformer := func(item *Item) string {
|
||||
return item.AsString(opts.Ansi)
|
||||
}
|
||||
if opts.AcceptNth != nil {
|
||||
fn := opts.AcceptNth(opts.Delimiter)
|
||||
transformer = func(item *Item) string {
|
||||
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
||||
}
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
opts.Printer(transformer(merger.Get(i).item))
|
||||
}
|
||||
if count == 0 {
|
||||
exitCode = ExitNoMatch
|
||||
@@ -453,7 +511,7 @@ func Run(opts *Options) (int, error) {
|
||||
stop = true
|
||||
return
|
||||
}
|
||||
determine(val.final)
|
||||
determine(merger.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val)
|
||||
@@ -467,7 +525,7 @@ func Run(opts *Options) (int, error) {
|
||||
}
|
||||
if delay && reading {
|
||||
dur := util.DurWithin(
|
||||
time.Duration(ticks)*coordinatorDelayStep,
|
||||
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||
0, coordinatorDelayMax)
|
||||
time.Sleep(dur)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
type transformed struct {
|
||||
// Because nth can be changed dynamically by change-nth action, we need to
|
||||
// keep the revision number at the time of transformation.
|
||||
revision int
|
||||
revision revision
|
||||
tokens []Token
|
||||
}
|
||||
|
||||
@@ -51,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
|
||||
}
|
||||
return item.text.ToString()
|
||||
}
|
||||
|
||||
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
transformed := transformer(tokens, item.Index())
|
||||
return StripLastDelimiter(transformed, delimiter)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,20 @@ type MatchRequest struct {
|
||||
revision revision
|
||||
}
|
||||
|
||||
type MatchResult struct {
|
||||
merger *Merger
|
||||
passMerger *Merger
|
||||
cancelled bool
|
||||
}
|
||||
|
||||
func (mr MatchResult) cacheable() bool {
|
||||
return mr.merger != nil && mr.merger.cacheable()
|
||||
}
|
||||
|
||||
func (mr MatchResult) final() bool {
|
||||
return mr.merger != nil && mr.merger.final
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
type Matcher struct {
|
||||
cache *ChunkCache
|
||||
@@ -29,7 +43,7 @@ type Matcher struct {
|
||||
reqBox *util.EventBox
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
mergerCache map[string]MatchResult
|
||||
revision revision
|
||||
}
|
||||
|
||||
@@ -51,7 +65,7 @@ func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger),
|
||||
mergerCache: make(map[string]MatchResult),
|
||||
revision: revision}
|
||||
}
|
||||
|
||||
@@ -85,43 +99,42 @@ func (m *Matcher) Loop() {
|
||||
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.mergerCache = make(map[string]MatchResult)
|
||||
if !request.revision.compatible(m.revision) {
|
||||
m.cache.Clear()
|
||||
}
|
||||
m.revision = request.revision
|
||||
cacheCleared = true
|
||||
}
|
||||
|
||||
// Restart search
|
||||
patternString := request.pattern.AsString()
|
||||
var merger *Merger
|
||||
cancelled := false
|
||||
var result MatchResult
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||
merger = cached
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final() == request.final {
|
||||
result = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
m.mergerCache = make(map[string]MatchResult)
|
||||
}
|
||||
}
|
||||
|
||||
if merger == nil {
|
||||
merger, cancelled = m.scan(request)
|
||||
if result.merger == nil {
|
||||
result = m.scan(request)
|
||||
}
|
||||
|
||||
if !cancelled {
|
||||
if merger.cacheable() {
|
||||
m.mergerCache[patternString] = merger
|
||||
if !result.cancelled {
|
||||
if result.cacheable() {
|
||||
m.mergerCache[patternString] = result
|
||||
}
|
||||
merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, merger)
|
||||
result.merger.final = request.final
|
||||
m.eventBox.Set(EvtSearchFin, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,19 +165,22 @@ type partialResult struct {
|
||||
matches []Result
|
||||
}
|
||||
|
||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
func (m *Matcher) scan(request MatchRequest) MatchResult {
|
||||
startedAt := time.Now()
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger(request.revision), false
|
||||
m := EmptyMerger(request.revision)
|
||||
return MatchResult{m, m, false}
|
||||
}
|
||||
pattern := request.pattern
|
||||
passMerger := PassMerger(&request.chunks, m.tac, request.revision)
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
return MatchResult{passMerger, passMerger, false}
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
@@ -223,7 +239,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
}
|
||||
|
||||
if m.reqBox.Peek(reqReset) {
|
||||
return nil, wait()
|
||||
return MatchResult{nil, nil, wait()}
|
||||
}
|
||||
|
||||
if time.Since(startedAt) > progressMinDuration {
|
||||
@@ -236,7 +252,8 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
|
||||
merger := NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex)
|
||||
return MatchResult{merger, passMerger, false}
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
|
||||
@@ -4,7 +4,7 @@ import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
func EmptyMerger(revision revision) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0, 0)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
@@ -22,14 +22,16 @@ type Merger struct {
|
||||
pass bool
|
||||
revision revision
|
||||
minIndex int32
|
||||
maxIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
var minIndex int32
|
||||
var minIndex, maxIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||
}
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
@@ -38,7 +40,8 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -47,7 +50,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
@@ -59,7 +62,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision,
|
||||
minIndex: minIndex}
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -137,6 +141,15 @@ func (mg *Merger) Get(idx int) Result {
|
||||
panic(fmt.Sprintf("Index out of bounds (unsorted, %d/%d)", idx, mg.count))
|
||||
}
|
||||
|
||||
func (mg *Merger) ToMap() map[int32]Result {
|
||||
ret := make(map[int32]Result, mg.count)
|
||||
for i := 0; i < mg.count; i++ {
|
||||
result := mg.Get(i)
|
||||
ret[result.Index()] = result
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (mg *Merger) cacheable() bool {
|
||||
return mg.count < mergerCacheMax
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0)
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -70,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -80,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
||||
767
src/options.go
767
src/options.go
File diff suppressed because it is too large
Load Diff
@@ -142,7 +142,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseKeys(t *testing.T) {
|
||||
pairs, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
checkEvent := func(e tui.Event, s string) {
|
||||
if pairs[e] != s {
|
||||
t.Errorf("%s != %s", pairs[e], s)
|
||||
@@ -168,11 +168,11 @@ func TestParseKeys(t *testing.T) {
|
||||
checkEvent(tui.AltKey(' '), "alt-SPACE")
|
||||
|
||||
// Synonyms
|
||||
pairs, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
if len(pairs) != 9 {
|
||||
t.Error(9)
|
||||
}
|
||||
check(tui.CtrlM, "Return")
|
||||
check(tui.Enter, "Return")
|
||||
checkEvent(tui.Key(' '), "space")
|
||||
check(tui.Tab, "tab")
|
||||
check(tui.ShiftTab, "btab")
|
||||
@@ -182,7 +182,7 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.Left, "left")
|
||||
check(tui.Right, "right")
|
||||
|
||||
pairs, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
if len(pairs) != 11 {
|
||||
t.Error(11)
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func TestParseKeys(t *testing.T) {
|
||||
check(tui.ShiftLeft, "shift-left")
|
||||
check(tui.ShiftRight, "shift-right")
|
||||
check(tui.ShiftTab, "shift-tab")
|
||||
check(tui.CtrlM, "Enter")
|
||||
check(tui.Enter, "Enter")
|
||||
check(tui.Backspace, "bspace")
|
||||
}
|
||||
|
||||
@@ -211,40 +211,40 @@ func TestParseKeysWithComma(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pairs, _ := parseKeyChords(",", "")
|
||||
pairs, _, _ := parseKeyChords(",", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords(",,a,b", "")
|
||||
pairs, _, _ = parseKeyChords(",,a,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords("a,b,,", "")
|
||||
pairs, _, _ = parseKeyChords("a,b,,", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords("a,,,b", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords("a,,,b,c", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b,c", "")
|
||||
checkN(len(pairs), 4)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key('c'), "c")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords(",,,", "")
|
||||
pairs, _, _ = parseKeyChords(",,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs, _ = parseKeyChords(",ALT-,,", "")
|
||||
pairs, _, _ = parseKeyChords(",ALT-,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltKey(','), "ALT-,")
|
||||
}
|
||||
@@ -300,8 +300,12 @@ func TestBind(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
var base *tui.ColorTheme
|
||||
theme := tui.Dark256
|
||||
dark, _ := parseTheme(theme, "dark")
|
||||
base, dark, _ := parseTheme(theme, "dark")
|
||||
if *dark != *base {
|
||||
t.Errorf("incorrect base theme returned")
|
||||
}
|
||||
if *dark != *theme {
|
||||
t.Errorf("colors should be equivalent")
|
||||
}
|
||||
@@ -309,7 +313,10 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
light, _ := parseTheme(theme, "dark,light")
|
||||
base, light, _ := parseTheme(theme, "dark,light")
|
||||
if *light != *base {
|
||||
t.Errorf("incorrect base theme returned")
|
||||
}
|
||||
if *light == *theme {
|
||||
t.Errorf("should not be equivalent")
|
||||
}
|
||||
@@ -320,7 +327,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||
_, customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
@@ -333,7 +340,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||
}
|
||||
|
||||
customized, _ = parseTheme(theme, "fg:231,dark,bg:232")
|
||||
_, customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
||||
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
@@ -350,8 +357,8 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
t.Error()
|
||||
}
|
||||
}
|
||||
check([]string{}, tui.CtrlN, actDown)
|
||||
check([]string{}, tui.CtrlP, actUp)
|
||||
check([]string{}, tui.CtrlN, actDownMatch)
|
||||
check([]string{}, tui.CtrlP, actUpMatch)
|
||||
|
||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
@@ -462,7 +469,7 @@ func TestValidateSign(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := validateSign(testCase.inputSign, "")
|
||||
err := validateSign(testCase.inputSign, "", 2)
|
||||
if testCase.isValid && err != nil {
|
||||
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
||||
}
|
||||
|
||||
@@ -60,9 +60,10 @@ type Pattern struct {
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
revision int
|
||||
revision revision
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
}
|
||||
|
||||
var _splitRegex *regexp.Regexp
|
||||
@@ -73,7 +74,7 @@ func init() {
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision int, runes []rune) *Pattern {
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -144,6 +145,7 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
@@ -243,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -296,6 +301,8 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
// Huge code duplication for minimizing unnecessary map lookups
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
@@ -310,6 +317,28 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if _, prs := p.denylist[chunk.items[idx].Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
// MatchItem returns true if the Item is a match
|
||||
@@ -403,6 +432,13 @@ func (p *Pattern) transformInput(item *Item) []Token {
|
||||
|
||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||
ret := Transform(tokens, p.nth)
|
||||
// Strip the last delimiter to allow suffix match
|
||||
if len(ret) > 0 && !p.delimiter.IsAwk() {
|
||||
chars := ret[len(ret)-1].text
|
||||
stripped := StripLastDelimiter(chars.ToString(), p.delimiter)
|
||||
newChars := util.ToChars(stringBytes(stripped))
|
||||
ret[len(ret)-1].text = &newChars
|
||||
}
|
||||
item.transformed = &transformed{p.revision, ret}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||
withPos, cacheable, nth, delimiter, 0, runes)
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
|
||||
14
src/proxy.go
14
src/proxy.go
@@ -59,12 +59,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
})
|
||||
}()
|
||||
|
||||
var command string
|
||||
var command, input 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")
|
||||
input, err = fifo("proxy-input")
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
@@ -90,11 +90,12 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Write the command to a temporary file and run it with sh to ensure POSIX compliance.
|
||||
var exports []string
|
||||
needBash := false
|
||||
if withExports {
|
||||
// Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
|
||||
exports = []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
@@ -144,10 +145,13 @@ func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool)
|
||||
env = elems[1:]
|
||||
}
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
ttyin, err := tui.TtyIn()
|
||||
ttyin, err := tui.TtyIn(opts.TtyDefault)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
os.Remove(temp)
|
||||
os.Remove(input)
|
||||
os.Remove(output)
|
||||
executor.Become(ttyin, env, command)
|
||||
}
|
||||
return code, err
|
||||
|
||||
@@ -277,12 +277,15 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
ignoresFull := []string{}
|
||||
ignoresSuffix := []string{}
|
||||
sep := string(os.PathSeparator)
|
||||
if _, ok := os.LookupEnv("MSYSTEM"); ok {
|
||||
sep = "/"
|
||||
}
|
||||
for _, ignore := range ignores {
|
||||
if strings.ContainsRune(ignore, os.PathSeparator) {
|
||||
if strings.HasPrefix(ignore, sep) {
|
||||
ignoresSuffix = append(ignoresSuffix, ignore)
|
||||
} else {
|
||||
// 'foo/bar' should match match
|
||||
// 'foo/bar' should match
|
||||
// * 'foo/bar'
|
||||
// * 'baz/foo/bar'
|
||||
// * but NOT 'bazfoo/bar'
|
||||
@@ -320,6 +323,9 @@ func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bo
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
if path != sep {
|
||||
path += sep
|
||||
}
|
||||
}
|
||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher(stringBytes(path)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
|
||||
@@ -19,6 +19,10 @@ type colorOffset struct {
|
||||
url *url
|
||||
}
|
||||
|
||||
func (co colorOffset) IsFullBgMarker(at int32) bool {
|
||||
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
item *Item
|
||||
points [4]uint16
|
||||
@@ -69,6 +73,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
}
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byPathname:
|
||||
if validOffsetFound {
|
||||
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
|
||||
lastDelim := -1
|
||||
s := item.text.ToString()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '/' || s[i] == '\\' {
|
||||
lastDelim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastDelim <= minBegin {
|
||||
val = util.AsUint16(minBegin - lastDelim)
|
||||
}
|
||||
}
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
@@ -104,14 +123,14 @@ func minRank() Result {
|
||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||
}
|
||||
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, current bool) []colorOffset {
|
||||
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr, hidden bool) []colorOffset {
|
||||
itemColors := result.item.Colors()
|
||||
|
||||
// No ANSI codes
|
||||
if len(itemColors) == 0 && len(nthOffsets) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
|
||||
offsets := make([]colorOffset, len(matchOffsets))
|
||||
for i, off := range matchOffsets {
|
||||
offsets[i] = colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
@@ -134,12 +153,20 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
color bool
|
||||
match bool
|
||||
nth bool
|
||||
fbg tui.Color
|
||||
}
|
||||
|
||||
cols := make([]cellInfo, maxCol)
|
||||
cols := make([]cellInfo, maxCol+1)
|
||||
for idx := range cols {
|
||||
cols[idx].fbg = -1
|
||||
}
|
||||
for colorIndex, ansi := range itemColors {
|
||||
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
|
||||
cols[ansi.offset[0]].fbg = ansi.color.lbg
|
||||
} else {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = cellInfo{colorIndex, true, false, false}
|
||||
cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,29 +188,35 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
var curr cellInfo = cellInfo{0, false, false, false}
|
||||
curr := cellInfo{0, false, false, false, -1}
|
||||
start := 0
|
||||
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||
if !theme.Colored {
|
||||
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
// fd --color always | fzf --ansi --delimiter / --nth -1 --color fg:dim:strip,nth:regular
|
||||
if base.ShouldStripColors() {
|
||||
return base
|
||||
}
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current.Color
|
||||
} else {
|
||||
fg = theme.Fg.Color
|
||||
}
|
||||
fg = colBase.Fg()
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg.Color
|
||||
} else {
|
||||
bg = theme.Bg.Color
|
||||
}
|
||||
bg = colBase.Bg()
|
||||
}
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
var colors []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr.fbg >= 0 {
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(start)},
|
||||
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
if (curr.color || curr.nth || curr.match) && idx > start {
|
||||
if curr.match {
|
||||
var color tui.ColorPair
|
||||
@@ -193,7 +226,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
color = colBase.Merge(colMatch)
|
||||
}
|
||||
var url *url
|
||||
if curr.color && theme.Colored {
|
||||
if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
url = ansi.color.url
|
||||
origColor := ansiToColorPair(ansi, colMatch)
|
||||
@@ -208,7 +241,7 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||
color = origColor
|
||||
if curr.nth {
|
||||
color = color.WithAttr(attrNth)
|
||||
color = color.WithAttr(attrNth &^ tui.AttrRegular)
|
||||
}
|
||||
} else {
|
||||
color = origColor.MergeNonDefault(color)
|
||||
@@ -218,19 +251,27 @@ func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, t
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||
} else if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
color := ansiToColorPair(ansi, colBase)
|
||||
base := colBase
|
||||
if curr.nth {
|
||||
color = color.WithAttr(attrNth)
|
||||
base = base.WithAttr(attrNth)
|
||||
}
|
||||
if hidden {
|
||||
base = base.WithFg(theme.Nomatch)
|
||||
}
|
||||
color := ansiToColorPair(ansi, base)
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: color,
|
||||
match: false,
|
||||
url: ansi.color.url})
|
||||
} else {
|
||||
color := colBase.WithAttr(attrNth)
|
||||
if hidden {
|
||||
color = color.WithFg(theme.Nomatch)
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)},
|
||||
color: colBase.WithAttr(attrNth),
|
||||
color: color,
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, true)
|
||||
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined, false)
|
||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||
@@ -158,7 +158,7 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
nthOffsets := []Offset{{37, 39}, {42, 45}}
|
||||
for _, attr := range []tui.Attr{tui.AttrRegular, tui.StrikeThrough} {
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, true)
|
||||
colors = item.colorOffsets(offsets, nthOffsets, tui.Dark256, colRegular, colUnderline, attr, false)
|
||||
|
||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
||||
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||
@@ -176,7 +176,7 @@ func TestColorOffset(t *testing.T) {
|
||||
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||
expected := tui.Bold | attr
|
||||
if attr == tui.AttrRegular {
|
||||
expected = tui.AttrRegular
|
||||
expected = tui.Bold
|
||||
}
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
|
||||
@@ -46,15 +46,20 @@ type httpServer struct {
|
||||
type listenAddress struct {
|
||||
host string
|
||||
port int
|
||||
sock string
|
||||
}
|
||||
|
||||
func (addr listenAddress) IsLocal() bool {
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1"
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1" || len(addr.sock) > 0
|
||||
}
|
||||
|
||||
var defaultListenAddr = listenAddress{"localhost", 0}
|
||||
var defaultListenAddr = listenAddress{"localhost", 0, ""}
|
||||
|
||||
func parseListenAddress(address string) (listenAddress, error) {
|
||||
if strings.HasSuffix(address, ".sock") {
|
||||
return listenAddress{"", 0, address}, nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(address, ":", 3)
|
||||
if len(parts) == 1 {
|
||||
parts = []string{"localhost", parts[0]}
|
||||
@@ -70,7 +75,7 @@ func parseListenAddress(address string) (listenAddress, error) {
|
||||
if len(parts[0]) == 0 {
|
||||
parts[0] = "localhost"
|
||||
}
|
||||
return listenAddress{parts[0], port}, nil
|
||||
return listenAddress{parts[0], port, ""}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||
@@ -80,8 +85,26 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
|
||||
if !address.IsLocal() && len(apiKey) == 0 {
|
||||
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
||||
}
|
||||
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if len(address.sock) > 0 {
|
||||
if _, err := os.Stat(address.sock); err == nil {
|
||||
// Check if the socket is already in use
|
||||
if conn, err := net.Dial("unix", address.sock); err == nil {
|
||||
conn.Close()
|
||||
return nil, 0, fmt.Errorf("socket already in use: %s", address.sock)
|
||||
}
|
||||
os.Remove(address.sock)
|
||||
}
|
||||
listener, err = net.Listen("unix", address.sock)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("failed to listen on %s", address.sock)
|
||||
}
|
||||
os.Chmod(address.sock, 0600)
|
||||
} else {
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err := net.Listen("tcp", addrStr)
|
||||
listener, err = net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||
}
|
||||
@@ -97,6 +120,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, getHan
|
||||
return nil, port, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
|
||||
2161
src/terminal.go
2161
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems []*Item) string {
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
|
||||
replaced, _ := replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: stripAnsi,
|
||||
@@ -30,11 +30,11 @@ func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
items2 := []*Item{
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||
items1 := [3][]*Item{{item1}, {item1}, nil}
|
||||
items2 := [3][]*Item{
|
||||
{newItem("foo'bar \x1b[31mbaz\x1b[m")},
|
||||
{newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}
|
||||
|
||||
delim := "'"
|
||||
var regex *regexp.Regexp
|
||||
@@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {r}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {r..}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
@@ -137,11 +145,11 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
|
||||
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// String delimiter
|
||||
@@ -158,17 +166,18 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
|
||||
see: TestParsePlaceholder
|
||||
*/
|
||||
items3 := []*Item{
|
||||
items3 := [3][]*Item{
|
||||
// single line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
{newItem("1a 1b 1c 1d 1e 1f")},
|
||||
// multi line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
{newItem("1a 1b 1c 1d 1e 1f"),
|
||||
newItem("2a 2b 2c 2d 2e 2f"),
|
||||
newItem("3a 3b 3c 3d 3e 3f"),
|
||||
newItem("4a 4b 4c 4d 4e 4f"),
|
||||
newItem("5a 5b 5c 5d 5e 5f"),
|
||||
newItem("6a 6b 6c 6d 6e 6f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f")},
|
||||
nil,
|
||||
}
|
||||
stripAnsi := false
|
||||
forcePlus := false
|
||||
@@ -485,6 +494,11 @@ func TestParsePlaceholder(t *testing.T) {
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
`{q}`: `{qq}`,
|
||||
`{q:1}`: `{qq:1}`,
|
||||
`{q:2..}`: `{qq:2..}`,
|
||||
`{q:..}`: `{qq:..}`,
|
||||
`{q:2..-1}`: `{qq:2..-1}`,
|
||||
`{q:s2..-1}`: `{sqq:2..-1}`, // FIXME
|
||||
|
||||
// IV. escaping placeholder
|
||||
`\{}`: `{}`,
|
||||
@@ -544,14 +558,14 @@ func newItem(str string) *Item {
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
// Functions tested in this file require array of items (allItems). The array needs
|
||||
// to consist of at least two nils. This is helper function.
|
||||
func newItems(str ...string) []*Item {
|
||||
result := make([]*Item, util.Max(len(str), 2))
|
||||
// Functions tested in this file require array of items (allItems).
|
||||
// This is helper function.
|
||||
func newItems(str ...string) [3][]*Item {
|
||||
result := make([]*Item, len(str))
|
||||
for i, s := range str {
|
||||
result[i] = newItem(s)
|
||||
}
|
||||
return result
|
||||
return [3][]*Item{result, nil, nil}
|
||||
}
|
||||
|
||||
// (for logging purposes)
|
||||
@@ -560,7 +574,7 @@ func (item *Item) String() string {
|
||||
}
|
||||
|
||||
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||
func templateToString(format string, data interface{}) string {
|
||||
func templateToString(format string, data any) string {
|
||||
bb := &bytes.Buffer{}
|
||||
|
||||
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||
@@ -575,7 +589,7 @@ func templateToString(format string, data interface{}) string {
|
||||
type give struct {
|
||||
template string
|
||||
query string
|
||||
allItems []*Item
|
||||
allItems [3][]*Item
|
||||
}
|
||||
type want struct {
|
||||
/*
|
||||
@@ -613,25 +627,25 @@ func testCommands(t *testing.T, tests []testCase) {
|
||||
// evaluate the test cases
|
||||
for idx, test := range tests {
|
||||
gotOutput := replacePlaceholderTest(
|
||||
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.give.query,
|
||||
test.give.allItems)
|
||||
test.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.query,
|
||||
test.allItems)
|
||||
switch {
|
||||
case test.want.output != "":
|
||||
if gotOutput != test.want.output {
|
||||
case test.output != "":
|
||||
if gotOutput != test.output {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.output)
|
||||
test.template, test.query, test.allItems,
|
||||
gotOutput, test.output)
|
||||
}
|
||||
case test.want.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
|
||||
case test.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.match, `\`, `\\`)
|
||||
wantRegex := regexp.MustCompile(wantMatch)
|
||||
if !wantRegex.MatchString(gotOutput) {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.match)
|
||||
test.template, test.query, test.allItems,
|
||||
gotOutput, test.match)
|
||||
}
|
||||
default:
|
||||
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
|
||||
|
||||
@@ -11,10 +11,14 @@ func runTmux(args []string, opts *Options) (int, error) {
|
||||
// Prepare arguments
|
||||
fzf, rest := args[0], args[1:]
|
||||
args = []string{"--bind=ctrl-z:ignore"}
|
||||
if !opts.Tmux.border && opts.BorderShape == tui.BorderUndefined {
|
||||
if !opts.Tmux.border && (opts.BorderShape == tui.BorderUndefined || opts.BorderShape == tui.BorderLine) {
|
||||
// We append --border option at the end, because `--style=full:STYLE`
|
||||
// may have changed the default border style.
|
||||
rest = append(rest, "--border")
|
||||
if tui.DefaultBorderShape == tui.BorderRounded {
|
||||
rest = append(rest, "--border=rounded")
|
||||
} else {
|
||||
rest = append(rest, "--border=sharp")
|
||||
}
|
||||
}
|
||||
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||
args = append(args, "--margin=0,1")
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
@@ -22,6 +23,18 @@ func (r Range) IsFull() bool {
|
||||
return r.begin == rangeEllipsis && r.end == rangeEllipsis
|
||||
}
|
||||
|
||||
func compareRanges(r1 []Range, r2 []Range) bool {
|
||||
if len(r1) != len(r2) {
|
||||
return false
|
||||
}
|
||||
for idx := range r1 {
|
||||
if r1[idx] != r2[idx] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func RangesToString(ranges []Range) string {
|
||||
strs := []string{}
|
||||
for _, r := range ranges {
|
||||
@@ -65,6 +78,11 @@ type Delimiter struct {
|
||||
str *string
|
||||
}
|
||||
|
||||
// IsAwk returns true if the delimiter is an AWK-style delimiter
|
||||
func (d Delimiter) IsAwk() bool {
|
||||
return d.regex == nil && d.str == nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Delimiter.
|
||||
func (d Delimiter) String() string {
|
||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||
@@ -199,7 +217,24 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) string {
|
||||
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
if lastLoc[1] == len(str) {
|
||||
str = str[:lastLoc[0]]
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
|
||||
// JoinTokens concatenates the tokens into a single string
|
||||
func JoinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
output.WriteString(token.text.ToString())
|
||||
@@ -217,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
||||
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
|
||||
@@ -85,14 +85,14 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != "abc: def: ghi: " {
|
||||
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
@@ -107,7 +107,7 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
|
||||
@@ -2,29 +2,7 @@
|
||||
|
||||
package tui
|
||||
|
||||
type Attr int32
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var DefaultBorderShape = BorderRounded
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
if b&AttrRegular > 0 {
|
||||
// Only keep bold attribute set by the system
|
||||
return b | (a & BoldForce)
|
||||
}
|
||||
|
||||
return a | b
|
||||
}
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 8)
|
||||
AttrClear = Attr(1 << 9)
|
||||
BoldForce = Attr(1 << 10)
|
||||
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
@@ -35,6 +13,12 @@ const (
|
||||
StrikeThrough = Attr(1 << 7)
|
||||
)
|
||||
|
||||
func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var DefaultBorderShape = BorderRounded
|
||||
|
||||
func (r *FullscreenRenderer) Init() error { return nil }
|
||||
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
|
||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||
@@ -44,6 +28,9 @@ func (r *FullscreenRenderer) PassThrough(string) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
||||
func (r *FullscreenRenderer) Bell() {}
|
||||
func (r *FullscreenRenderer) HideCursor() {}
|
||||
func (r *FullscreenRenderer) ShowCursor() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||
|
||||
@@ -21,7 +21,7 @@ func _() {
|
||||
_ = x[CtrlJ-10]
|
||||
_ = x[CtrlK-11]
|
||||
_ = x[CtrlL-12]
|
||||
_ = x[CtrlM-13]
|
||||
_ = x[Enter-13]
|
||||
_ = x[CtrlN-14]
|
||||
_ = x[CtrlO-15]
|
||||
_ = x[CtrlP-16]
|
||||
@@ -37,82 +37,137 @@ func _() {
|
||||
_ = x[CtrlZ-26]
|
||||
_ = x[Esc-27]
|
||||
_ = x[CtrlSpace-28]
|
||||
_ = x[CtrlDelete-29]
|
||||
_ = x[CtrlBackSlash-30]
|
||||
_ = x[CtrlRightBracket-31]
|
||||
_ = x[CtrlCaret-32]
|
||||
_ = x[CtrlSlash-33]
|
||||
_ = x[ShiftTab-34]
|
||||
_ = x[Backspace-35]
|
||||
_ = x[Delete-36]
|
||||
_ = x[PageUp-37]
|
||||
_ = x[PageDown-38]
|
||||
_ = x[Up-39]
|
||||
_ = x[Down-40]
|
||||
_ = x[Left-41]
|
||||
_ = x[Right-42]
|
||||
_ = x[Home-43]
|
||||
_ = x[End-44]
|
||||
_ = x[Insert-45]
|
||||
_ = x[ShiftUp-46]
|
||||
_ = x[ShiftDown-47]
|
||||
_ = x[ShiftLeft-48]
|
||||
_ = x[ShiftRight-49]
|
||||
_ = x[ShiftDelete-50]
|
||||
_ = x[F1-51]
|
||||
_ = x[F2-52]
|
||||
_ = x[F3-53]
|
||||
_ = x[F4-54]
|
||||
_ = x[F5-55]
|
||||
_ = x[F6-56]
|
||||
_ = x[F7-57]
|
||||
_ = x[F8-58]
|
||||
_ = x[F9-59]
|
||||
_ = x[F10-60]
|
||||
_ = x[F11-61]
|
||||
_ = x[F12-62]
|
||||
_ = x[AltBackspace-63]
|
||||
_ = x[AltUp-64]
|
||||
_ = x[AltDown-65]
|
||||
_ = x[AltLeft-66]
|
||||
_ = x[AltRight-67]
|
||||
_ = x[AltShiftUp-68]
|
||||
_ = x[AltShiftDown-69]
|
||||
_ = x[AltShiftLeft-70]
|
||||
_ = x[AltShiftRight-71]
|
||||
_ = x[Alt-72]
|
||||
_ = x[CtrlAlt-73]
|
||||
_ = x[Invalid-74]
|
||||
_ = x[Fatal-75]
|
||||
_ = x[Mouse-76]
|
||||
_ = x[DoubleClick-77]
|
||||
_ = x[LeftClick-78]
|
||||
_ = x[RightClick-79]
|
||||
_ = x[SLeftClick-80]
|
||||
_ = x[SRightClick-81]
|
||||
_ = x[ScrollUp-82]
|
||||
_ = x[ScrollDown-83]
|
||||
_ = x[SScrollUp-84]
|
||||
_ = x[SScrollDown-85]
|
||||
_ = x[PreviewScrollUp-86]
|
||||
_ = x[PreviewScrollDown-87]
|
||||
_ = x[Resize-88]
|
||||
_ = x[Change-89]
|
||||
_ = x[BackwardEOF-90]
|
||||
_ = x[Start-91]
|
||||
_ = x[Load-92]
|
||||
_ = x[Focus-93]
|
||||
_ = x[One-94]
|
||||
_ = x[Zero-95]
|
||||
_ = x[Result-96]
|
||||
_ = x[Jump-97]
|
||||
_ = x[JumpCancel-98]
|
||||
_ = x[ClickHeader-99]
|
||||
_ = x[CtrlBackSlash-29]
|
||||
_ = x[CtrlRightBracket-30]
|
||||
_ = x[CtrlCaret-31]
|
||||
_ = x[CtrlSlash-32]
|
||||
_ = x[ShiftTab-33]
|
||||
_ = x[Backspace-34]
|
||||
_ = x[Delete-35]
|
||||
_ = x[PageUp-36]
|
||||
_ = x[PageDown-37]
|
||||
_ = x[Up-38]
|
||||
_ = x[Down-39]
|
||||
_ = x[Left-40]
|
||||
_ = x[Right-41]
|
||||
_ = x[Home-42]
|
||||
_ = x[End-43]
|
||||
_ = x[Insert-44]
|
||||
_ = x[ShiftUp-45]
|
||||
_ = x[ShiftDown-46]
|
||||
_ = x[ShiftLeft-47]
|
||||
_ = x[ShiftRight-48]
|
||||
_ = x[ShiftDelete-49]
|
||||
_ = x[ShiftHome-50]
|
||||
_ = x[ShiftEnd-51]
|
||||
_ = x[ShiftPageUp-52]
|
||||
_ = x[ShiftPageDown-53]
|
||||
_ = x[F1-54]
|
||||
_ = x[F2-55]
|
||||
_ = x[F3-56]
|
||||
_ = x[F4-57]
|
||||
_ = x[F5-58]
|
||||
_ = x[F6-59]
|
||||
_ = x[F7-60]
|
||||
_ = x[F8-61]
|
||||
_ = x[F9-62]
|
||||
_ = x[F10-63]
|
||||
_ = x[F11-64]
|
||||
_ = x[F12-65]
|
||||
_ = x[AltBackspace-66]
|
||||
_ = x[AltUp-67]
|
||||
_ = x[AltDown-68]
|
||||
_ = x[AltLeft-69]
|
||||
_ = x[AltRight-70]
|
||||
_ = x[AltDelete-71]
|
||||
_ = x[AltHome-72]
|
||||
_ = x[AltEnd-73]
|
||||
_ = x[AltPageUp-74]
|
||||
_ = x[AltPageDown-75]
|
||||
_ = x[AltShiftUp-76]
|
||||
_ = x[AltShiftDown-77]
|
||||
_ = x[AltShiftLeft-78]
|
||||
_ = x[AltShiftRight-79]
|
||||
_ = x[AltShiftDelete-80]
|
||||
_ = x[AltShiftHome-81]
|
||||
_ = x[AltShiftEnd-82]
|
||||
_ = x[AltShiftPageUp-83]
|
||||
_ = x[AltShiftPageDown-84]
|
||||
_ = x[CtrlUp-85]
|
||||
_ = x[CtrlDown-86]
|
||||
_ = x[CtrlLeft-87]
|
||||
_ = x[CtrlRight-88]
|
||||
_ = x[CtrlHome-89]
|
||||
_ = x[CtrlEnd-90]
|
||||
_ = x[CtrlBackspace-91]
|
||||
_ = x[CtrlDelete-92]
|
||||
_ = x[CtrlPageUp-93]
|
||||
_ = x[CtrlPageDown-94]
|
||||
_ = x[Alt-95]
|
||||
_ = x[CtrlAlt-96]
|
||||
_ = x[CtrlAltUp-97]
|
||||
_ = x[CtrlAltDown-98]
|
||||
_ = x[CtrlAltLeft-99]
|
||||
_ = x[CtrlAltRight-100]
|
||||
_ = x[CtrlAltHome-101]
|
||||
_ = x[CtrlAltEnd-102]
|
||||
_ = x[CtrlAltBackspace-103]
|
||||
_ = x[CtrlAltDelete-104]
|
||||
_ = x[CtrlAltPageUp-105]
|
||||
_ = x[CtrlAltPageDown-106]
|
||||
_ = x[CtrlShiftUp-107]
|
||||
_ = x[CtrlShiftDown-108]
|
||||
_ = x[CtrlShiftLeft-109]
|
||||
_ = x[CtrlShiftRight-110]
|
||||
_ = x[CtrlShiftHome-111]
|
||||
_ = x[CtrlShiftEnd-112]
|
||||
_ = x[CtrlShiftDelete-113]
|
||||
_ = x[CtrlShiftPageUp-114]
|
||||
_ = x[CtrlShiftPageDown-115]
|
||||
_ = x[CtrlAltShiftUp-116]
|
||||
_ = x[CtrlAltShiftDown-117]
|
||||
_ = x[CtrlAltShiftLeft-118]
|
||||
_ = x[CtrlAltShiftRight-119]
|
||||
_ = x[CtrlAltShiftHome-120]
|
||||
_ = x[CtrlAltShiftEnd-121]
|
||||
_ = x[CtrlAltShiftDelete-122]
|
||||
_ = x[CtrlAltShiftPageUp-123]
|
||||
_ = x[CtrlAltShiftPageDown-124]
|
||||
_ = x[Invalid-125]
|
||||
_ = x[Fatal-126]
|
||||
_ = x[BracketedPasteBegin-127]
|
||||
_ = x[BracketedPasteEnd-128]
|
||||
_ = x[Mouse-129]
|
||||
_ = x[DoubleClick-130]
|
||||
_ = x[LeftClick-131]
|
||||
_ = x[RightClick-132]
|
||||
_ = x[SLeftClick-133]
|
||||
_ = x[SRightClick-134]
|
||||
_ = x[ScrollUp-135]
|
||||
_ = x[ScrollDown-136]
|
||||
_ = x[SScrollUp-137]
|
||||
_ = x[SScrollDown-138]
|
||||
_ = x[PreviewScrollUp-139]
|
||||
_ = x[PreviewScrollDown-140]
|
||||
_ = x[Resize-141]
|
||||
_ = x[Change-142]
|
||||
_ = x[BackwardEOF-143]
|
||||
_ = x[Start-144]
|
||||
_ = x[Load-145]
|
||||
_ = x[Focus-146]
|
||||
_ = x[One-147]
|
||||
_ = x[Zero-148]
|
||||
_ = x[Result-149]
|
||||
_ = x[Jump-150]
|
||||
_ = x[JumpCancel-151]
|
||||
_ = x[ClickHeader-152]
|
||||
_ = x[ClickFooter-153]
|
||||
_ = x[Multi-154]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
|
||||
|
||||
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}
|
||||
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, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1067, 1072, 1091, 1108, 1113, 1124, 1133, 1143, 1153, 1164, 1172, 1182, 1191, 1202, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
|
||||
405
src/tui/light.go
405
src/tui/light.go
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -27,11 +28,15 @@ const (
|
||||
maxInputBuffer = 1024 * 1024
|
||||
)
|
||||
|
||||
const consoleDevice string = "/dev/tty"
|
||||
const DefaultTtyDevice string = "/dev/tty"
|
||||
|
||||
var offsetRegexp = regexp.MustCompile("(.*)\x1b\\[([0-9]+);([0-9]+)R")
|
||||
var offsetRegexp = regexp.MustCompile("(.*?)\x00?\x1b\\[([0-9]+);([0-9]+)R")
|
||||
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||
|
||||
func (r *LightRenderer) Bell() {
|
||||
r.flushRaw("\a")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) PassThrough(str string) {
|
||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||
}
|
||||
@@ -40,8 +45,9 @@ func (r *LightRenderer) stderr(str string) {
|
||||
r.stderrInternal(str, true, "")
|
||||
}
|
||||
|
||||
const CR string = "\x1b[2m␍"
|
||||
const LF string = "\x1b[2m␊"
|
||||
const DIM string = "\x1b[2m"
|
||||
const CR string = DIM + "␍"
|
||||
const LF string = DIM + "␊"
|
||||
|
||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||
bytes := []byte(str)
|
||||
@@ -73,7 +79,13 @@ func (r *LightRenderer) csi(code string) string {
|
||||
|
||||
func (r *LightRenderer) flush() {
|
||||
if r.queued.Len() > 0 {
|
||||
r.flushRaw("\x1b[?7l\x1b[?25l" + r.queued.String() + "\x1b[?25h\x1b[?7h")
|
||||
raw := "\x1b[?7l\x1b[?25l" + r.queued.String()
|
||||
if r.showCursor {
|
||||
raw += "\x1b[?25h\x1b[?7h"
|
||||
} else {
|
||||
raw += "\x1b[?7h"
|
||||
}
|
||||
r.flushRaw(raw)
|
||||
r.queued.Reset()
|
||||
}
|
||||
}
|
||||
@@ -84,7 +96,6 @@ func (r *LightRenderer) flushRaw(sequence string) {
|
||||
|
||||
// Light renderer
|
||||
type LightRenderer struct {
|
||||
closed *util.AtomicBool
|
||||
theme *ColorTheme
|
||||
mouse bool
|
||||
forceBlack bool
|
||||
@@ -106,8 +117,10 @@ type LightRenderer struct {
|
||||
y int
|
||||
x int
|
||||
maxHeightFunc func(int) int
|
||||
showCursor bool
|
||||
|
||||
// Windows only
|
||||
mutex sync.Mutex
|
||||
ttyinChannel chan byte
|
||||
inHandle uintptr
|
||||
outHandle uintptr
|
||||
@@ -129,15 +142,16 @@ type LightWindow struct {
|
||||
tabstop int
|
||||
fg Color
|
||||
bg Color
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
}
|
||||
|
||||
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()
|
||||
func NewLightRenderer(ttyDefault string, ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
||||
out, err := openTtyOut(ttyDefault)
|
||||
if err != nil {
|
||||
out = os.Stderr
|
||||
}
|
||||
r := LightRenderer{
|
||||
closed: util.NewAtomicBool(false),
|
||||
theme: theme,
|
||||
forceBlack: forceBlack,
|
||||
mouse: mouse,
|
||||
@@ -148,7 +162,8 @@ func NewLightRenderer(ttyin *os.File, theme *ColorTheme, forceBlack bool, mouse
|
||||
tabstop: tabstop,
|
||||
fullscreen: fullscreen,
|
||||
upOneLine: false,
|
||||
maxHeightFunc: maxHeightFunc}
|
||||
maxHeightFunc: maxHeightFunc,
|
||||
showCursor: true}
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
@@ -198,7 +213,7 @@ func (r *LightRenderer) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
r.enableMouse()
|
||||
r.enableModes()
|
||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||
r.csi("G")
|
||||
r.csi("K")
|
||||
@@ -256,7 +271,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
|
||||
c, ok := r.getch(nonblock)
|
||||
if !nonblock && !ok {
|
||||
r.Close()
|
||||
return nil, errors.New("failed to read " + consoleDevice)
|
||||
return nil, errors.New("failed to read " + DefaultTtyDevice)
|
||||
}
|
||||
|
||||
retries := 0
|
||||
@@ -320,6 +335,8 @@ func (r *LightRenderer) GetChar() Event {
|
||||
return Event{CtrlQ, 0, nil}
|
||||
case 127:
|
||||
return Event{Backspace, 0, nil}
|
||||
case 8:
|
||||
return Event{CtrlBackspace, 0, nil}
|
||||
case 0:
|
||||
return Event{CtrlSpace, 0, nil}
|
||||
case 28:
|
||||
@@ -366,6 +383,9 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
|
||||
*sz = 2
|
||||
if r.buffer[1] == 8 {
|
||||
return Event{CtrlAltBackspace, 0, nil}
|
||||
}
|
||||
if r.buffer[1] >= 1 && r.buffer[1] <= 'z'-'a'+1 {
|
||||
return CtrlAltKey(rune(r.buffer[1] + 'a' - 1))
|
||||
}
|
||||
@@ -447,32 +467,150 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
// Bracketed paste mode: \e[200~ ... \e[201~
|
||||
if len(r.buffer) > 5 && r.buffer[3] == '0' && (r.buffer[4] == '0' || r.buffer[4] == '1') && r.buffer[5] == '~' {
|
||||
// Immediately discard the sequence from the buffer and reread input
|
||||
r.buffer = r.buffer[6:]
|
||||
*sz = 0
|
||||
return r.GetChar()
|
||||
*sz = 6
|
||||
if r.buffer[4] == '0' {
|
||||
return Event{BracketedPasteBegin, 0, nil}
|
||||
}
|
||||
return Event{BracketedPasteEnd, 0, nil}
|
||||
}
|
||||
return Event{Invalid, 0, nil} // INS
|
||||
case '3':
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{Delete, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
*sz = 7
|
||||
switch r.buffer[5] {
|
||||
case '0':
|
||||
return Event{AltShiftDelete, 0, nil}
|
||||
case '1':
|
||||
return Event{AltDelete, 0, nil}
|
||||
case '2':
|
||||
return Event{AltShiftDelete, 0, nil}
|
||||
case '3':
|
||||
return Event{CtrlAltDelete, 0, nil}
|
||||
case '4':
|
||||
return Event{CtrlAltShiftDelete, 0, nil}
|
||||
case '5':
|
||||
return Event{CtrlAltDelete, 0, nil}
|
||||
case '6':
|
||||
return Event{CtrlAltShiftDelete, 0, nil}
|
||||
}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '5':
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
case '2':
|
||||
return Event{ShiftDelete, 0, nil}
|
||||
case '3':
|
||||
return Event{AltDelete, 0, nil}
|
||||
case '4':
|
||||
return Event{AltShiftDelete, 0, nil}
|
||||
case '5':
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
case '6':
|
||||
return Event{CtrlShiftDelete, 0, nil}
|
||||
case '7':
|
||||
return Event{CtrlAltDelete, 0, nil}
|
||||
case '8':
|
||||
return Event{CtrlAltShiftDelete, 0, nil}
|
||||
case '9':
|
||||
return Event{AltDelete, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '4':
|
||||
return Event{End, 0, nil}
|
||||
case '5':
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{PageUp, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
*sz = 7
|
||||
switch r.buffer[5] {
|
||||
case '0':
|
||||
return Event{AltShiftPageUp, 0, nil}
|
||||
case '1':
|
||||
return Event{AltPageUp, 0, nil}
|
||||
case '2':
|
||||
return Event{AltShiftPageUp, 0, nil}
|
||||
case '3':
|
||||
return Event{CtrlAltPageUp, 0, nil}
|
||||
case '4':
|
||||
return Event{CtrlAltShiftPageUp, 0, nil}
|
||||
case '5':
|
||||
return Event{CtrlAltPageUp, 0, nil}
|
||||
case '6':
|
||||
return Event{CtrlAltShiftPageUp, 0, nil}
|
||||
}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '2':
|
||||
return Event{ShiftPageUp, 0, nil}
|
||||
case '3':
|
||||
return Event{AltPageUp, 0, nil}
|
||||
case '4':
|
||||
return Event{AltShiftPageUp, 0, nil}
|
||||
case '5':
|
||||
return Event{CtrlPageUp, 0, nil}
|
||||
case '6':
|
||||
return Event{CtrlShiftPageUp, 0, nil}
|
||||
case '7':
|
||||
return Event{CtrlAltPageUp, 0, nil}
|
||||
case '8':
|
||||
return Event{CtrlAltShiftPageUp, 0, nil}
|
||||
case '9':
|
||||
return Event{AltPageUp, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '6':
|
||||
if r.buffer[3] == '~' {
|
||||
return Event{PageDown, 0, nil}
|
||||
}
|
||||
if len(r.buffer) == 7 && r.buffer[6] == '~' && r.buffer[4] == '1' {
|
||||
*sz = 7
|
||||
switch r.buffer[5] {
|
||||
case '0':
|
||||
return Event{AltShiftPageDown, 0, nil}
|
||||
case '1':
|
||||
return Event{AltPageDown, 0, nil}
|
||||
case '2':
|
||||
return Event{AltShiftPageDown, 0, nil}
|
||||
case '3':
|
||||
return Event{CtrlAltPageDown, 0, nil}
|
||||
case '4':
|
||||
return Event{CtrlAltShiftPageDown, 0, nil}
|
||||
case '5':
|
||||
return Event{CtrlAltPageDown, 0, nil}
|
||||
case '6':
|
||||
return Event{CtrlAltShiftPageDown, 0, nil}
|
||||
}
|
||||
}
|
||||
if len(r.buffer) == 6 && r.buffer[5] == '~' {
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '2':
|
||||
return Event{ShiftPageDown, 0, nil}
|
||||
case '3':
|
||||
return Event{AltPageDown, 0, nil}
|
||||
case '4':
|
||||
return Event{AltShiftPageDown, 0, nil}
|
||||
case '5':
|
||||
return Event{CtrlPageDown, 0, nil}
|
||||
case '6':
|
||||
return Event{CtrlShiftPageDown, 0, nil}
|
||||
case '7':
|
||||
return Event{CtrlAltPageDown, 0, nil}
|
||||
case '8':
|
||||
return Event{CtrlAltShiftPageDown, 0, nil}
|
||||
case '9':
|
||||
return Event{AltPageDown, 0, nil}
|
||||
}
|
||||
}
|
||||
return Event{Invalid, 0, nil}
|
||||
case '7':
|
||||
return Event{Home, 0, nil}
|
||||
case '8':
|
||||
@@ -510,64 +648,173 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
||||
}
|
||||
*sz = 6
|
||||
switch r.buffer[4] {
|
||||
case '1', '2', '3', '4', '5':
|
||||
case '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
// Kitty iTerm2 WezTerm
|
||||
// SHIFT-ARROW "\e[1;2D"
|
||||
// ALT-SHIFT-ARROW "\e[1;4D" "\e[1;10D" "\e[1;4D"
|
||||
// CTRL-SHIFT-ARROW "\e[1;6D" N/A
|
||||
// CMD-SHIFT-ARROW "\e[1;10D" N/A N/A ("\e[1;2D")
|
||||
alt := r.buffer[4] == '3'
|
||||
ctrl := bytes.IndexByte([]byte{'5', '6', '7', '8'}, r.buffer[4]) >= 0
|
||||
alt := bytes.IndexByte([]byte{'3', '4', '7', '8'}, r.buffer[4]) >= 0
|
||||
shift := bytes.IndexByte([]byte{'2', '4', '6', '8'}, r.buffer[4]) >= 0
|
||||
char := r.buffer[5]
|
||||
altShift := false
|
||||
if r.buffer[4] == '1' && r.buffer[5] == '0' {
|
||||
altShift = true
|
||||
if len(r.buffer) < 7 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 7
|
||||
char = r.buffer[6]
|
||||
} else if r.buffer[4] == '4' {
|
||||
altShift = true
|
||||
if r.buffer[4] == '9' {
|
||||
ctrl = false
|
||||
alt = true
|
||||
shift = false
|
||||
if len(r.buffer) < 6 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 6
|
||||
char = r.buffer[5]
|
||||
} else if r.buffer[4] == '1' && bytes.IndexByte([]byte{'0', '1', '2', '3', '4', '5', '6'}, r.buffer[5]) >= 0 {
|
||||
ctrl = bytes.IndexByte([]byte{'3', '4', '5', '6'}, r.buffer[5]) >= 0
|
||||
alt = true
|
||||
shift = bytes.IndexByte([]byte{'0', '2', '4', '6'}, r.buffer[5]) >= 0
|
||||
if len(r.buffer) < 7 {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
*sz = 7
|
||||
char = r.buffer[6]
|
||||
}
|
||||
ctrlShift := ctrl && shift
|
||||
ctrlAlt := ctrl && alt
|
||||
altShift := alt && shift
|
||||
ctrlAltShift := ctrl && alt && shift
|
||||
switch char {
|
||||
case 'A':
|
||||
if alt {
|
||||
return Event{AltUp, 0, nil}
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftUp, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltUp, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftUp, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftUp, 0, nil}
|
||||
}
|
||||
return Event{ShiftUp, 0, nil}
|
||||
case 'B':
|
||||
if ctrl {
|
||||
return Event{CtrlUp, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltDown, 0, nil}
|
||||
return Event{AltUp, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftUp, 0, nil}
|
||||
}
|
||||
case 'B':
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftDown, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltDown, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftDown, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftDown, 0, nil}
|
||||
}
|
||||
return Event{ShiftDown, 0, nil}
|
||||
case 'C':
|
||||
if ctrl {
|
||||
return Event{CtrlDown, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltRight, 0, nil}
|
||||
return Event{AltDown, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftDown, 0, nil}
|
||||
}
|
||||
case 'C':
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftRight, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltRight, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftRight, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftRight, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlRight, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftRight, 0, nil}
|
||||
case 'D':
|
||||
}
|
||||
if alt {
|
||||
return Event{AltLeft, 0, nil}
|
||||
return Event{AltRight, 0, nil}
|
||||
}
|
||||
case 'D':
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftLeft, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltLeft, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftLeft, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftLeft, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlLeft, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltLeft, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftLeft, 0, nil}
|
||||
}
|
||||
case 'H':
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftHome, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltHome, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftHome, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftHome, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlHome, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltHome, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftHome, 0, nil}
|
||||
}
|
||||
case 'F':
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftEnd, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltEnd, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftEnd, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftEnd, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlEnd, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltEnd, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftEnd, 0, nil}
|
||||
}
|
||||
}
|
||||
} // r.buffer[4]
|
||||
} // r.buffer[3]
|
||||
} // r.buffer[2]
|
||||
@@ -622,15 +869,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
|
||||
// middle := t & 0b1
|
||||
left := t&0b11 == 0
|
||||
|
||||
// shift := t & 0b100
|
||||
// ctrl := t & 0b1000
|
||||
mod := t&0b1100 > 0
|
||||
|
||||
drag := t&0b100000 > 0
|
||||
ctrl := t&0b10000 > 0
|
||||
alt := t&0b01000 > 0
|
||||
shift := t&0b00100 > 0
|
||||
drag := t&0b100000 > 0 // 32
|
||||
|
||||
if scroll != 0 {
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, scroll, false, false, false, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
double := false
|
||||
@@ -654,7 +899,7 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
||||
}
|
||||
}
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) smcup() {
|
||||
@@ -668,7 +913,7 @@ func (r *LightRenderer) rmcup() {
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Pause(clear bool) {
|
||||
r.disableMouse()
|
||||
r.disableModes()
|
||||
r.restoreTerminal()
|
||||
if clear {
|
||||
if r.fullscreen {
|
||||
@@ -681,12 +926,13 @@ func (r *LightRenderer) Pause(clear bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) enableMouse() {
|
||||
func (r *LightRenderer) enableModes() {
|
||||
if r.mouse {
|
||||
r.csi("?1000h")
|
||||
r.csi("?1002h")
|
||||
r.csi("?1006h")
|
||||
}
|
||||
r.csi("?2004h") // Enable bracketed paste mode
|
||||
}
|
||||
|
||||
func (r *LightRenderer) disableMouse() {
|
||||
@@ -697,6 +943,11 @@ func (r *LightRenderer) disableMouse() {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) disableModes() {
|
||||
r.disableMouse()
|
||||
r.csi("?2004l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
r.setupTerminal()
|
||||
if clear {
|
||||
@@ -705,7 +956,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||
} else {
|
||||
r.rmcup()
|
||||
}
|
||||
r.enableMouse()
|
||||
r.enableModes()
|
||||
r.flush()
|
||||
} else if sigcont && !r.fullscreen && r.mouse {
|
||||
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
||||
@@ -757,11 +1008,13 @@ func (r *LightRenderer) Close() {
|
||||
} else if !r.fullscreen {
|
||||
r.csi("u")
|
||||
}
|
||||
r.disableMouse()
|
||||
if !r.showCursor {
|
||||
r.csi("?25h")
|
||||
}
|
||||
r.disableModes()
|
||||
r.flush()
|
||||
r.closePlatform()
|
||||
r.restoreTerminal()
|
||||
r.closed.Set(true)
|
||||
r.closePlatform()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Top() int {
|
||||
@@ -807,11 +1060,14 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, wind
|
||||
case WindowHeader:
|
||||
w.fg = r.theme.Header.Color
|
||||
w.bg = r.theme.HeaderBg.Color
|
||||
case WindowFooter:
|
||||
w.fg = r.theme.Footer.Color
|
||||
w.bg = r.theme.FooterBg.Color
|
||||
case WindowPreview:
|
||||
w.fg = r.theme.PreviewFg.Color
|
||||
w.bg = r.theme.PreviewBg.Color
|
||||
}
|
||||
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone {
|
||||
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone && w.height > 0 {
|
||||
// fzf --color bg:blue --border --padding 1,2
|
||||
w.Erase()
|
||||
}
|
||||
@@ -867,6 +1123,8 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||
color = ColInputBorder
|
||||
case WindowHeader:
|
||||
color = ColHeaderBorder
|
||||
case WindowFooter:
|
||||
color = ColFooterBorder
|
||||
case WindowPreview:
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
@@ -892,6 +1150,8 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||
color = ColInputBorder
|
||||
case WindowHeader:
|
||||
color = ColHeaderBorder
|
||||
case WindowFooter:
|
||||
color = ColFooterBorder
|
||||
case WindowPreview:
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
@@ -919,6 +1179,8 @@ func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||
color = ColInputBorder
|
||||
case WindowHeader:
|
||||
color = ColHeaderBorder
|
||||
case WindowFooter:
|
||||
color = ColFooterBorder
|
||||
case WindowPreview:
|
||||
color = ColPreviewBorder
|
||||
}
|
||||
@@ -1092,11 +1354,12 @@ type wrappedLine struct {
|
||||
displayWidth int
|
||||
}
|
||||
|
||||
func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLine {
|
||||
func wrapLine(input string, prefixLength int, initialMax int, tabstop int, wrapSignWidth int) []wrappedLine {
|
||||
lines := []wrappedLine{}
|
||||
width := 0
|
||||
line := ""
|
||||
gr := uniseg.NewGraphemes(input)
|
||||
max := initialMax
|
||||
for gr.Next() {
|
||||
rs := gr.Runes()
|
||||
str := string(rs)
|
||||
@@ -1118,6 +1381,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
line = str
|
||||
prefixLength = 0
|
||||
width = w
|
||||
max = initialMax - wrapSignWidth
|
||||
}
|
||||
}
|
||||
lines = append(lines, wrappedLine{string(line), width})
|
||||
@@ -1127,7 +1391,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
allLines := strings.Split(str, "\n")
|
||||
for i, line := range allLines {
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop)
|
||||
lines := wrapLine(line, w.posx, w.width, w.tabstop, w.wrapSignWidth)
|
||||
for j, wl := range lines {
|
||||
w.stderrInternal(wl.text, false, resetCode)
|
||||
w.posx += wl.displayWidth
|
||||
@@ -1140,6 +1404,18 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||
w.MoveAndClear(w.posy, w.posx)
|
||||
w.Move(w.posy+1, 0)
|
||||
w.renderer.stderr(resetCode)
|
||||
if len(lines) > 1 {
|
||||
sign := w.wrapSign
|
||||
width := w.wrapSignWidth
|
||||
if width > w.width {
|
||||
runes, truncatedWidth := util.Truncate(w.wrapSign, w.width)
|
||||
sign = string(runes)
|
||||
width = truncatedWidth
|
||||
}
|
||||
w.stderrInternal(DIM+sign, false, resetCode)
|
||||
w.renderer.stderr(resetCode)
|
||||
w.Move(w.posy, width)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1212,3 +1488,18 @@ func (w *LightWindow) Erase() {
|
||||
func (w *LightWindow) EraseMaybe() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w *LightWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (r *LightRenderer) HideCursor() {
|
||||
r.showCursor = false
|
||||
r.csi("?25l")
|
||||
}
|
||||
|
||||
func (r *LightRenderer) ShowCursor() {
|
||||
r.showCursor = true
|
||||
r.csi("?25h")
|
||||
}
|
||||
|
||||
335
src/tui/light_test.go
Normal file
335
src/tui/light_test.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestLightRenderer(t *testing.T) {
|
||||
tty_file, _ := os.Open("")
|
||||
renderer, _ := NewLightRenderer(
|
||||
"", tty_file, &ColorTheme{}, true, false, 0, false, true,
|
||||
func(h int) int { return h })
|
||||
|
||||
light_renderer := renderer.(*LightRenderer)
|
||||
|
||||
assertCharSequence := func(sequence string, name string) {
|
||||
bytes := []byte(sequence)
|
||||
light_renderer.buffer = bytes
|
||||
event := light_renderer.GetChar()
|
||||
if event.KeyName() != name {
|
||||
t.Errorf(
|
||||
"sequence: %q | %v | '%s' (%s) != %s",
|
||||
string(bytes), bytes,
|
||||
event.KeyName(), event.Type.String(), name)
|
||||
}
|
||||
}
|
||||
|
||||
assertEscSequence := func(sequence string, name string) {
|
||||
bytes := []byte(sequence)
|
||||
light_renderer.buffer = bytes
|
||||
|
||||
sz := 1
|
||||
event := light_renderer.escSequence(&sz)
|
||||
if fmt.Sprintf("!%s", event.Type.String()) == name {
|
||||
// this is fine
|
||||
} else if event.KeyName() != name {
|
||||
t.Errorf(
|
||||
"sequence: %q | %v | '%s' (%s) != %s",
|
||||
string(bytes), bytes,
|
||||
event.KeyName(), event.Type.String(), name)
|
||||
}
|
||||
}
|
||||
|
||||
// invalid
|
||||
assertEscSequence("\x1b[<", "!Invalid")
|
||||
assertEscSequence("\x1b[1;1R", "!Invalid")
|
||||
assertEscSequence("\x1b[", "!Invalid")
|
||||
assertEscSequence("\x1b[1", "!Invalid")
|
||||
assertEscSequence("\x1b[3;3~1", "!Invalid")
|
||||
assertEscSequence("\x1b[13", "!Invalid")
|
||||
assertEscSequence("\x1b[1;3", "!Invalid")
|
||||
assertEscSequence("\x1b[1;10", "!Invalid")
|
||||
assertEscSequence("\x1b[220~", "!Invalid")
|
||||
assertEscSequence("\x1b[5;30~", "!Invalid")
|
||||
assertEscSequence("\x1b[6;30~", "!Invalid")
|
||||
|
||||
// general
|
||||
for r := 'a'; r < 'z'; r++ {
|
||||
lower_r := fmt.Sprintf("%c", r)
|
||||
upper_r := fmt.Sprintf("%c", unicode.ToUpper(r))
|
||||
assertCharSequence(lower_r, lower_r)
|
||||
assertCharSequence(upper_r, upper_r)
|
||||
}
|
||||
|
||||
assertCharSequence("\x01", "ctrl-a")
|
||||
assertCharSequence("\x02", "ctrl-b")
|
||||
assertCharSequence("\x03", "ctrl-c")
|
||||
assertCharSequence("\x04", "ctrl-d")
|
||||
assertCharSequence("\x05", "ctrl-e")
|
||||
assertCharSequence("\x06", "ctrl-f")
|
||||
assertCharSequence("\x07", "ctrl-g")
|
||||
// ctrl-h is the same as ctrl-backspace
|
||||
// ctrl-i is the same as tab
|
||||
assertCharSequence("\n", "ctrl-j")
|
||||
assertCharSequence("\x0b", "ctrl-k")
|
||||
assertCharSequence("\x0c", "ctrl-l")
|
||||
assertCharSequence("\r", "enter") // enter
|
||||
assertCharSequence("\x0e", "ctrl-n")
|
||||
assertCharSequence("\x0f", "ctrl-o")
|
||||
assertCharSequence("\x10", "ctrl-p")
|
||||
assertCharSequence("\x11", "ctrl-q")
|
||||
assertCharSequence("\x12", "ctrl-r")
|
||||
assertCharSequence("\x13", "ctrl-s")
|
||||
assertCharSequence("\x14", "ctrl-t")
|
||||
assertCharSequence("\x15", "ctrl-u")
|
||||
assertCharSequence("\x16", "ctrl-v")
|
||||
assertCharSequence("\x17", "ctrl-w")
|
||||
assertCharSequence("\x18", "ctrl-x")
|
||||
assertCharSequence("\x19", "ctrl-y")
|
||||
assertCharSequence("\x1a", "ctrl-z")
|
||||
|
||||
assertCharSequence("\x00", "ctrl-space")
|
||||
assertCharSequence("\x1c", "ctrl-\\")
|
||||
assertCharSequence("\x1d", "ctrl-]")
|
||||
assertCharSequence("\x1e", "ctrl-^")
|
||||
assertCharSequence("\x1f", "ctrl-/")
|
||||
|
||||
assertEscSequence("\x1ba", "alt-a")
|
||||
assertEscSequence("\x1bb", "alt-b")
|
||||
assertEscSequence("\x1bc", "alt-c")
|
||||
assertEscSequence("\x1bd", "alt-d")
|
||||
assertEscSequence("\x1be", "alt-e")
|
||||
assertEscSequence("\x1bf", "alt-f")
|
||||
assertEscSequence("\x1bg", "alt-g")
|
||||
assertEscSequence("\x1bh", "alt-h")
|
||||
assertEscSequence("\x1bi", "alt-i")
|
||||
assertEscSequence("\x1bj", "alt-j")
|
||||
assertEscSequence("\x1bk", "alt-k")
|
||||
assertEscSequence("\x1bl", "alt-l")
|
||||
assertEscSequence("\x1bm", "alt-m")
|
||||
assertEscSequence("\x1bn", "alt-n")
|
||||
assertEscSequence("\x1bo", "alt-o")
|
||||
assertEscSequence("\x1bp", "alt-p")
|
||||
assertEscSequence("\x1bq", "alt-q")
|
||||
assertEscSequence("\x1br", "alt-r")
|
||||
assertEscSequence("\x1bs", "alt-s")
|
||||
assertEscSequence("\x1bt", "alt-t")
|
||||
assertEscSequence("\x1bu", "alt-u")
|
||||
assertEscSequence("\x1bv", "alt-v")
|
||||
assertEscSequence("\x1bw", "alt-w")
|
||||
assertEscSequence("\x1bx", "alt-x")
|
||||
assertEscSequence("\x1by", "alt-y")
|
||||
assertEscSequence("\x1bz", "alt-z")
|
||||
|
||||
assertEscSequence("\x1bOP", "f1")
|
||||
assertEscSequence("\x1bOQ", "f2")
|
||||
assertEscSequence("\x1bOR", "f3")
|
||||
assertEscSequence("\x1bOS", "f4")
|
||||
assertEscSequence("\x1b[15~", "f5")
|
||||
assertEscSequence("\x1b[17~", "f6")
|
||||
assertEscSequence("\x1b[18~", "f7")
|
||||
assertEscSequence("\x1b[19~", "f8")
|
||||
assertEscSequence("\x1b[20~", "f9")
|
||||
assertEscSequence("\x1b[21~", "f10")
|
||||
assertEscSequence("\x1b[23~", "f11")
|
||||
assertEscSequence("\x1b[24~", "f12")
|
||||
|
||||
assertEscSequence("\x1b", "esc")
|
||||
assertCharSequence("\t", "tab")
|
||||
assertEscSequence("\x1b[Z", "shift-tab")
|
||||
|
||||
assertCharSequence("\x7f", "backspace")
|
||||
assertEscSequence("\x1b\x7f", "alt-backspace")
|
||||
assertCharSequence("\b", "ctrl-backspace")
|
||||
assertEscSequence("\x1b\b", "ctrl-alt-backspace")
|
||||
|
||||
assertEscSequence("\x1b[A", "up")
|
||||
assertEscSequence("\x1b[B", "down")
|
||||
assertEscSequence("\x1b[C", "right")
|
||||
assertEscSequence("\x1b[D", "left")
|
||||
assertEscSequence("\x1b[H", "home")
|
||||
assertEscSequence("\x1b[F", "end")
|
||||
assertEscSequence("\x1b[2~", "insert")
|
||||
assertEscSequence("\x1b[3~", "delete")
|
||||
assertEscSequence("\x1b[5~", "page-up")
|
||||
assertEscSequence("\x1b[6~", "page-down")
|
||||
assertEscSequence("\x1b[7~", "home")
|
||||
assertEscSequence("\x1b[8~", "end")
|
||||
|
||||
assertEscSequence("\x1b[1;2A", "shift-up")
|
||||
assertEscSequence("\x1b[1;2B", "shift-down")
|
||||
assertEscSequence("\x1b[1;2C", "shift-right")
|
||||
assertEscSequence("\x1b[1;2D", "shift-left")
|
||||
assertEscSequence("\x1b[1;2H", "shift-home")
|
||||
assertEscSequence("\x1b[1;2F", "shift-end")
|
||||
assertEscSequence("\x1b[3;2~", "shift-delete")
|
||||
assertEscSequence("\x1b[5;2~", "shift-page-up")
|
||||
assertEscSequence("\x1b[6;2~", "shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b\x1b", "esc")
|
||||
assertEscSequence("\x1b\x1b[A", "alt-up")
|
||||
assertEscSequence("\x1b\x1b[B", "alt-down")
|
||||
assertEscSequence("\x1b\x1b[C", "alt-right")
|
||||
assertEscSequence("\x1b\x1b[D", "alt-left")
|
||||
|
||||
assertEscSequence("\x1b[1;3A", "alt-up")
|
||||
assertEscSequence("\x1b[1;3B", "alt-down")
|
||||
assertEscSequence("\x1b[1;3C", "alt-right")
|
||||
assertEscSequence("\x1b[1;3D", "alt-left")
|
||||
assertEscSequence("\x1b[1;3H", "alt-home")
|
||||
assertEscSequence("\x1b[1;3F", "alt-end")
|
||||
assertEscSequence("\x1b[3;3~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;3~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;3~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;4A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;4B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;4C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;4D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;4H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;4F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;4~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;4~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;4~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;5A", "ctrl-up")
|
||||
assertEscSequence("\x1b[1;5B", "ctrl-down")
|
||||
assertEscSequence("\x1b[1;5C", "ctrl-right")
|
||||
assertEscSequence("\x1b[1;5D", "ctrl-left")
|
||||
assertEscSequence("\x1b[1;5H", "ctrl-home")
|
||||
assertEscSequence("\x1b[1;5F", "ctrl-end")
|
||||
assertEscSequence("\x1b[3;5~", "ctrl-delete")
|
||||
assertEscSequence("\x1b[5;5~", "ctrl-page-up")
|
||||
assertEscSequence("\x1b[6;5~", "ctrl-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;7A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;7B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;7C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;7D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;7H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;7F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;7~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;7~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;7~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;6A", "ctrl-shift-up")
|
||||
assertEscSequence("\x1b[1;6B", "ctrl-shift-down")
|
||||
assertEscSequence("\x1b[1;6C", "ctrl-shift-right")
|
||||
assertEscSequence("\x1b[1;6D", "ctrl-shift-left")
|
||||
assertEscSequence("\x1b[1;6H", "ctrl-shift-home")
|
||||
assertEscSequence("\x1b[1;6F", "ctrl-shift-end")
|
||||
assertEscSequence("\x1b[3;6~", "ctrl-shift-delete")
|
||||
assertEscSequence("\x1b[5;6~", "ctrl-shift-page-up")
|
||||
assertEscSequence("\x1b[6;6~", "ctrl-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;8A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;8B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;8C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;8D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;8H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;8F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;8~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;8~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;8~", "ctrl-alt-shift-page-down")
|
||||
|
||||
// xterm meta & mac
|
||||
assertEscSequence("\x1b[1;9A", "alt-up")
|
||||
assertEscSequence("\x1b[1;9B", "alt-down")
|
||||
assertEscSequence("\x1b[1;9C", "alt-right")
|
||||
assertEscSequence("\x1b[1;9D", "alt-left")
|
||||
assertEscSequence("\x1b[1;9H", "alt-home")
|
||||
assertEscSequence("\x1b[1;9F", "alt-end")
|
||||
assertEscSequence("\x1b[3;9~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;9~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;9~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;10A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;10B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;10C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;10D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;10H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;10F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;10~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;10~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;10~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;11A", "alt-up")
|
||||
assertEscSequence("\x1b[1;11B", "alt-down")
|
||||
assertEscSequence("\x1b[1;11C", "alt-right")
|
||||
assertEscSequence("\x1b[1;11D", "alt-left")
|
||||
assertEscSequence("\x1b[1;11H", "alt-home")
|
||||
assertEscSequence("\x1b[1;11F", "alt-end")
|
||||
assertEscSequence("\x1b[3;11~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;11~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;11~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;12A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;12B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;12C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;12D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;12H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;12F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;12~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;12~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;12~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;13A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;13B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;13C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;13D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;13H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;13F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;13~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;13~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;13~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;14A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;14B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;14C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;14D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;14H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;14F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;14~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;14~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;14~", "ctrl-alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;15A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;15B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;15C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;15D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;15H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;15F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;15~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;15~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;15~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;16A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;16B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;16C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;16D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;16H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;16F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;16~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;16~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;16~", "ctrl-alt-shift-page-down")
|
||||
|
||||
// tmux & emacs
|
||||
assertEscSequence("\x1bOA", "up")
|
||||
assertEscSequence("\x1bOB", "down")
|
||||
assertEscSequence("\x1bOC", "right")
|
||||
assertEscSequence("\x1bOD", "left")
|
||||
assertEscSequence("\x1bOH", "home")
|
||||
assertEscSequence("\x1bOF", "end")
|
||||
|
||||
// rrvt
|
||||
assertEscSequence("\x1b[1~", "home")
|
||||
assertEscSequence("\x1b[4~", "end")
|
||||
assertEscSequence("\x1b[11~", "f1")
|
||||
assertEscSequence("\x1b[12~", "f2")
|
||||
assertEscSequence("\x1b[13~", "f3")
|
||||
assertEscSequence("\x1b[14~", "f4")
|
||||
|
||||
}
|
||||
@@ -42,26 +42,35 @@ func (r *LightRenderer) closePlatform() {
|
||||
r.ttyout.Close()
|
||||
}
|
||||
|
||||
func openTty(mode int) (*os.File, error) {
|
||||
in, err := os.OpenFile(consoleDevice, mode, 0)
|
||||
if err != nil {
|
||||
func openTty(ttyDefault string, mode int) (*os.File, error) {
|
||||
var in *os.File
|
||||
var err error
|
||||
if len(ttyDefault) > 0 {
|
||||
in, err = os.OpenFile(ttyDefault, mode, 0)
|
||||
}
|
||||
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to open " + consoleDevice)
|
||||
if ttyDefault != DefaultTtyDevice {
|
||||
if in, err = os.OpenFile(DefaultTtyDevice, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("failed to open " + DefaultTtyDevice)
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
return openTty(syscall.O_RDONLY)
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTty(ttyDefault, syscall.O_RDONLY)
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
return openTty(syscall.O_WRONLY)
|
||||
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTty(ttyDefault, syscall.O_WRONLY)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
var (
|
||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
counter = uint64(0)
|
||||
)
|
||||
|
||||
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
|
||||
}
|
||||
r.inHandle = uintptr(inHandle)
|
||||
|
||||
r.setupTerminal()
|
||||
|
||||
// channel for non-blocking reads. Buffer to make sure
|
||||
// we get the ESC sets:
|
||||
r.ttyinChannel = make(chan byte, 1024)
|
||||
|
||||
// the following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for !r.closed.Get() {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
r.setupTerminal()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,27 +76,51 @@ func (r *LightRenderer) closePlatform() {
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||
}
|
||||
|
||||
func openTtyIn() (*os.File, error) {
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
// not used
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func openTtyOut() (*os.File, error) {
|
||||
func openTtyOut(ttyDefault string) (*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
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
// The following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
current := counter
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
if _, err := util.Read(fd, b); err == nil {
|
||||
r.mutex.Lock()
|
||||
// This condition prevents the goroutine from running after the renderer
|
||||
// has been closed or paused.
|
||||
if current != counter {
|
||||
r.mutex.Unlock()
|
||||
break
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
r.ttyinChannel <- b[0]
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
func (r *LightRenderer) restoreTerminal() {
|
||||
r.mutex.Lock()
|
||||
counter++
|
||||
// We're setting ENABLE_VIRTUAL_TERMINAL_INPUT to allow escape sequences to be read during 'execute'.
|
||||
// e.g. fzf --bind 'enter:execute:less {}'
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput|windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
|
||||
253
src/tui/tcell.go
253
src/tui/tcell.go
@@ -36,8 +36,6 @@ func (p ColorPair) style() tcell.Style {
|
||||
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
|
||||
}
|
||||
|
||||
type Attr int32
|
||||
|
||||
type TcellWindow struct {
|
||||
color bool
|
||||
windowType WindowType
|
||||
@@ -52,6 +50,9 @@ type TcellWindow struct {
|
||||
borderStyle BorderStyle
|
||||
uri *string
|
||||
params *string
|
||||
showCursor bool
|
||||
wrapSign string
|
||||
wrapSignWidth int
|
||||
}
|
||||
|
||||
func (w *TcellWindow) Top() int {
|
||||
@@ -72,7 +73,9 @@ func (w *TcellWindow) Height() int {
|
||||
|
||||
func (w *TcellWindow) Refresh() {
|
||||
if w.moveCursor {
|
||||
if w.showCursor {
|
||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||
}
|
||||
w.moveCursor = false
|
||||
}
|
||||
w.lastX = 0
|
||||
@@ -93,12 +96,17 @@ const (
|
||||
Italic = Attr(tcell.AttrItalic)
|
||||
)
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 7)
|
||||
AttrClear = Attr(1 << 8)
|
||||
BoldForce = Attr(1 << 10)
|
||||
)
|
||||
func (r *FullscreenRenderer) Bell() {
|
||||
_screen.Beep()
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) HideCursor() {
|
||||
r.showCursor = false
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) ShowCursor() {
|
||||
r.showCursor = true
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||
// No-op
|
||||
@@ -141,15 +149,6 @@ func (c Color) Style() tcell.Color {
|
||||
}
|
||||
}
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
if b&AttrRegular > 0 {
|
||||
// Only keep bold attribute set by the system
|
||||
return b | (a & BoldForce)
|
||||
}
|
||||
|
||||
return a | b
|
||||
}
|
||||
|
||||
// handle the following as private members of FullscreenRenderer instance
|
||||
// they are declared here to prevent introducing tcell library in non-windows builds
|
||||
var (
|
||||
@@ -164,6 +163,9 @@ func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
if !r.showCursor {
|
||||
s.HideCursor()
|
||||
}
|
||||
_screen = s
|
||||
}
|
||||
return _screen, nil
|
||||
@@ -177,6 +179,7 @@ func (r *FullscreenRenderer) initScreen() error {
|
||||
if e = s.Init(); e != nil {
|
||||
return e
|
||||
}
|
||||
s.EnablePaste()
|
||||
if r.mouse {
|
||||
s.EnableMouse()
|
||||
} else {
|
||||
@@ -246,6 +249,11 @@ func (r *FullscreenRenderer) Size() TermSize {
|
||||
func (r *FullscreenRenderer) GetChar() Event {
|
||||
ev := _screen.PollEvent()
|
||||
switch ev := ev.(type) {
|
||||
case *tcell.EventPaste:
|
||||
if ev.Start() {
|
||||
return Event{BracketedPasteBegin, 0, nil}
|
||||
}
|
||||
return Event{BracketedPasteEnd, 0, nil}
|
||||
case *tcell.EventResize:
|
||||
// Ignore the first resize event
|
||||
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
|
||||
@@ -262,7 +270,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||
x, y := ev.Position()
|
||||
mod := ev.Modifiers() != 0
|
||||
|
||||
mod := ev.Modifiers()
|
||||
ctrl := (mod & tcell.ModCtrl) > 0
|
||||
alt := (mod & tcell.ModAlt) > 0
|
||||
shift := (mod & tcell.ModShift) > 0
|
||||
|
||||
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||
@@ -271,9 +283,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
switch {
|
||||
case button&tcell.WheelDown != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, ctrl, alt, shift}}
|
||||
case button&tcell.WheelUp != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, ctrl, alt, shift}}
|
||||
case button&tcell.Button1 != 0:
|
||||
double := false
|
||||
if !drag {
|
||||
@@ -296,9 +308,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
}
|
||||
// fire single or double click event
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, ctrl, alt, shift}}
|
||||
case button&tcell.Button2 != 0:
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, ctrl, alt, shift}}
|
||||
default:
|
||||
// double and single taps on Windows don't quite work due to
|
||||
// the console acting on the events and not allowing us
|
||||
@@ -307,7 +319,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
down := left || button&tcell.Button3 != 0
|
||||
double := false
|
||||
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||
// No need to report mouse movement events when no button is pressed
|
||||
if drag {
|
||||
return Event{Invalid, 0, nil}
|
||||
}
|
||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, ctrl, alt, shift}}
|
||||
}
|
||||
|
||||
// process keyboard:
|
||||
@@ -319,6 +335,8 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
shift := (mods & tcell.ModShift) > 0
|
||||
ctrlAlt := ctrl && alt
|
||||
altShift := alt && shift
|
||||
ctrlShift := ctrl && shift
|
||||
ctrlAltShift := ctrl && alt && shift
|
||||
|
||||
keyfn := func(r rune) Event {
|
||||
if alt {
|
||||
@@ -345,8 +363,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyCtrlH:
|
||||
switch ev.Rune() {
|
||||
case 0:
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltBackspace, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{Backspace, 0, nil}
|
||||
return Event{CtrlBackspace, 0, nil}
|
||||
}
|
||||
case rune(tcell.KeyCtrlH):
|
||||
switch {
|
||||
@@ -407,6 +428,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
return Event{CtrlSlash, 0, nil}
|
||||
// section 3: (Alt)+Backspace2
|
||||
case tcell.KeyBackspace2:
|
||||
if ctrl {
|
||||
return Event{CtrlBackspace, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltBackspace, 0, nil}
|
||||
}
|
||||
@@ -414,9 +438,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
case tcell.KeyUp:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftUp, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltUp, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftUp, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftUp, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlUp, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftUp, 0, nil}
|
||||
}
|
||||
@@ -425,9 +461,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Up, 0, nil}
|
||||
case tcell.KeyDown:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftDown, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltDown, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftDown, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftDown, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlDown, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftDown, 0, nil}
|
||||
}
|
||||
@@ -436,9 +484,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Down, 0, nil}
|
||||
case tcell.KeyLeft:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftLeft, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltLeft, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftLeft, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftLeft, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlLeft, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftLeft, 0, nil}
|
||||
}
|
||||
@@ -447,9 +507,21 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
}
|
||||
return Event{Left, 0, nil}
|
||||
case tcell.KeyRight:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftRight, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltRight, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftRight, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftRight, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlRight, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftRight, 0, nil}
|
||||
}
|
||||
@@ -462,20 +534,119 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
case tcell.KeyInsert:
|
||||
return Event{Insert, 0, nil}
|
||||
case tcell.KeyHome:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftHome, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltHome, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftHome, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftHome, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlHome, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftHome, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltHome, 0, nil}
|
||||
}
|
||||
return Event{Home, 0, nil}
|
||||
case tcell.KeyDelete:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftDelete, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltDelete, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftDelete, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftDelete, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlDelete, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltDelete, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftDelete, 0, nil}
|
||||
}
|
||||
return Event{Delete, 0, nil}
|
||||
case tcell.KeyEnd:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftEnd, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltEnd, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftEnd, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftEnd, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlEnd, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftEnd, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltEnd, 0, nil}
|
||||
}
|
||||
return Event{End, 0, nil}
|
||||
case tcell.KeyPgUp:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftPageUp, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltPageUp, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftPageUp, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftPageUp, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlPageUp, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftPageUp, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltPageUp, 0, nil}
|
||||
}
|
||||
return Event{PageUp, 0, nil}
|
||||
case tcell.KeyPgDn:
|
||||
if ctrlAltShift {
|
||||
return Event{CtrlAltShiftPageDown, 0, nil}
|
||||
}
|
||||
if ctrlAlt {
|
||||
return Event{CtrlAltPageDown, 0, nil}
|
||||
}
|
||||
if ctrlShift {
|
||||
return Event{CtrlShiftPageDown, 0, nil}
|
||||
}
|
||||
if altShift {
|
||||
return Event{AltShiftPageDown, 0, nil}
|
||||
}
|
||||
if ctrl {
|
||||
return Event{CtrlPageDown, 0, nil}
|
||||
}
|
||||
if shift {
|
||||
return Event{ShiftPageDown, 0, nil}
|
||||
}
|
||||
if alt {
|
||||
return Event{AltPageDown, 0, nil}
|
||||
}
|
||||
return Event{PageDown, 0, nil}
|
||||
case tcell.KeyBacktab:
|
||||
return Event{ShiftTab, 0, nil}
|
||||
@@ -534,18 +705,19 @@ func (r *FullscreenRenderer) GetChar() Event {
|
||||
|
||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||
if clear {
|
||||
_screen.Fini()
|
||||
_screen.Suspend()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
||||
if clear {
|
||||
r.initScreen()
|
||||
_screen.Resume()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) Close() {
|
||||
_screen.Fini()
|
||||
_screen = nil
|
||||
}
|
||||
|
||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||
@@ -565,6 +737,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
normal = ColNormal
|
||||
case WindowHeader:
|
||||
normal = ColHeader
|
||||
case WindowFooter:
|
||||
normal = ColFooter
|
||||
case WindowInput:
|
||||
normal = ColInput
|
||||
case WindowPreview:
|
||||
@@ -578,7 +752,8 @@ func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int,
|
||||
width: width,
|
||||
height: height,
|
||||
normal: normal,
|
||||
borderStyle: borderStyle}
|
||||
borderStyle: borderStyle,
|
||||
showCursor: r.showCursor}
|
||||
w.Erase()
|
||||
return w
|
||||
}
|
||||
@@ -601,6 +776,11 @@ func (w *TcellWindow) EraseMaybe() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *TcellWindow) SetWrapSign(sign string, width int) {
|
||||
w.wrapSign = sign
|
||||
w.wrapSignWidth = width
|
||||
}
|
||||
|
||||
func (w *TcellWindow) EncloseX(x int) bool {
|
||||
return x >= w.left && x < (w.left+w.width)
|
||||
}
|
||||
@@ -729,11 +909,26 @@ Loop:
|
||||
|
||||
// word wrap:
|
||||
xPos := w.left + w.lastX + lx
|
||||
if xPos >= (w.left + w.width) {
|
||||
if xPos >= w.left+w.width {
|
||||
w.lastY++
|
||||
if w.lastY >= w.height {
|
||||
return FillSuspend
|
||||
}
|
||||
w.lastX = 0
|
||||
lx = 0
|
||||
xPos = w.left
|
||||
sign := w.wrapSign
|
||||
if w.wrapSignWidth > w.width {
|
||||
runes, _ := util.Truncate(sign, w.width)
|
||||
sign = string(runes)
|
||||
}
|
||||
wgr := uniseg.NewGraphemes(sign)
|
||||
for wgr.Next() {
|
||||
rs := wgr.Runes()
|
||||
_screen.SetContent(w.left+lx, w.top+w.lastY, rs[0], rs[1:], style.Dim(true))
|
||||
lx += uniseg.StringWidth(string(rs))
|
||||
}
|
||||
xPos = w.left + lx
|
||||
}
|
||||
|
||||
yPos := w.top + w.lastY
|
||||
@@ -809,6 +1004,8 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||
style = ColListBorder.style()
|
||||
case WindowHeader:
|
||||
style = ColHeaderBorder.style()
|
||||
case WindowFooter:
|
||||
style = ColFooterBorder.style()
|
||||
case WindowInput:
|
||||
style = ColInputBorder.style()
|
||||
case WindowPreview:
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||
func assert(t *testing.T, context string, got any, want any) bool {
|
||||
if got == want {
|
||||
return true
|
||||
} else {
|
||||
@@ -82,9 +82,9 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyTab, rune(tcell.KeyTab), tcell.ModNone}, wantKey{Tab, 0, nil}}, // unhandled, actual "Tab" keystroke
|
||||
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||
// KeyEnter is alias for KeyCR
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
// Ctrl+Alt keys
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
@@ -107,18 +107,20 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
|
||||
@@ -126,9 +128,41 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl}, wantKey{CtrlUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl}, wantKey{CtrlDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl}, wantKey{CtrlRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl}, wantKey{CtrlLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModShift}, wantKey{ShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModShift}, wantKey{ShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift}, wantKey{ShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModAlt}, wantKey{AltUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModAlt}, wantKey{AltRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModAlt}, wantKey{AltLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
@@ -137,6 +171,46 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
|
||||
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModNone}, wantKey{Home, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModNone}, wantKey{End, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModNone}, wantKey{PageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModNone}, wantKey{PageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl}, wantKey{CtrlHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl}, wantKey{CtrlEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl}, wantKey{CtrlDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl}, wantKey{CtrlPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl}, wantKey{CtrlPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModShift}, wantKey{ShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModShift}, wantKey{ShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModShift}, wantKey{ShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift}, wantKey{ShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift}, wantKey{ShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModAlt}, wantKey{AltHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModAlt}, wantKey{AltEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModAlt}, wantKey{AltPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModAlt}, wantKey{AltPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageDown, 0, nil}},
|
||||
// section 6: (Ctrl+Alt)+'rune'
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
@@ -198,6 +272,10 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
initialResizeAsInvalid = false
|
||||
gotEvent = r.GetChar()
|
||||
}
|
||||
if gotEvent.Type == Resize {
|
||||
t.Logf("Resize swallowed")
|
||||
gotEvent = r.GetChar()
|
||||
}
|
||||
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||
|
||||
@@ -233,7 +311,7 @@ Quick reference
|
||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
|
||||
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||
|
||||
@@ -44,11 +44,11 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to read user input
|
||||
func TtyIn() (*os.File, error) {
|
||||
return openTtyIn()
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTtyIn(ttyDefault)
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to write to
|
||||
func TtyOut() (*os.File, error) {
|
||||
return openTtyOut()
|
||||
// TtyOut returns terminal device to write to
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTtyOut(ttyDefault)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ func ttyname() string {
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() (*os.File, error) {
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return os.Stdin, nil
|
||||
}
|
||||
|
||||
// TtyOut on Windows returns nil
|
||||
func TtyOut() (*os.File, error) {
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
660
src/tui/tui.go
660
src/tui/tui.go
@@ -8,6 +8,26 @@ import (
|
||||
"github.com/rivo/uniseg"
|
||||
)
|
||||
|
||||
type Attr int32
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 8)
|
||||
AttrClear = Attr(1 << 9)
|
||||
BoldForce = Attr(1 << 10)
|
||||
FullBg = Attr(1 << 11)
|
||||
Strip = Attr(1 << 12)
|
||||
)
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
if b&AttrRegular > 0 {
|
||||
// Only keep bold attribute set by the system
|
||||
return (b &^ AttrRegular) | (a & BoldForce)
|
||||
}
|
||||
|
||||
return (a &^ AttrRegular) | b
|
||||
}
|
||||
|
||||
// Types of user action
|
||||
//
|
||||
//go:generate stringer -type=EventType
|
||||
@@ -28,7 +48,7 @@ const (
|
||||
CtrlJ
|
||||
CtrlK
|
||||
CtrlL
|
||||
CtrlM
|
||||
Enter
|
||||
CtrlN
|
||||
CtrlO
|
||||
CtrlP
|
||||
@@ -44,7 +64,6 @@ const (
|
||||
CtrlZ
|
||||
Esc
|
||||
CtrlSpace
|
||||
CtrlDelete
|
||||
|
||||
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||
CtrlBackSlash
|
||||
@@ -72,6 +91,10 @@ const (
|
||||
ShiftLeft
|
||||
ShiftRight
|
||||
ShiftDelete
|
||||
ShiftHome
|
||||
ShiftEnd
|
||||
ShiftPageUp
|
||||
ShiftPageDown
|
||||
|
||||
F1
|
||||
F2
|
||||
@@ -92,17 +115,71 @@ const (
|
||||
AltDown
|
||||
AltLeft
|
||||
AltRight
|
||||
AltDelete
|
||||
AltHome
|
||||
AltEnd
|
||||
AltPageUp
|
||||
AltPageDown
|
||||
|
||||
AltShiftUp
|
||||
AltShiftDown
|
||||
AltShiftLeft
|
||||
AltShiftRight
|
||||
AltShiftDelete
|
||||
AltShiftHome
|
||||
AltShiftEnd
|
||||
AltShiftPageUp
|
||||
AltShiftPageDown
|
||||
|
||||
CtrlUp
|
||||
CtrlDown
|
||||
CtrlLeft
|
||||
CtrlRight
|
||||
CtrlHome
|
||||
CtrlEnd
|
||||
CtrlBackspace
|
||||
CtrlDelete
|
||||
CtrlPageUp
|
||||
CtrlPageDown
|
||||
|
||||
Alt
|
||||
CtrlAlt
|
||||
|
||||
CtrlAltUp
|
||||
CtrlAltDown
|
||||
CtrlAltLeft
|
||||
CtrlAltRight
|
||||
CtrlAltHome
|
||||
CtrlAltEnd
|
||||
CtrlAltBackspace
|
||||
CtrlAltDelete
|
||||
CtrlAltPageUp
|
||||
CtrlAltPageDown
|
||||
|
||||
CtrlShiftUp
|
||||
CtrlShiftDown
|
||||
CtrlShiftLeft
|
||||
CtrlShiftRight
|
||||
CtrlShiftHome
|
||||
CtrlShiftEnd
|
||||
CtrlShiftDelete
|
||||
CtrlShiftPageUp
|
||||
CtrlShiftPageDown
|
||||
|
||||
CtrlAltShiftUp
|
||||
CtrlAltShiftDown
|
||||
CtrlAltShiftLeft
|
||||
CtrlAltShiftRight
|
||||
CtrlAltShiftHome
|
||||
CtrlAltShiftEnd
|
||||
CtrlAltShiftDelete
|
||||
CtrlAltShiftPageUp
|
||||
CtrlAltShiftPageDown
|
||||
|
||||
Invalid
|
||||
Fatal
|
||||
BracketedPasteBegin
|
||||
BracketedPasteEnd
|
||||
|
||||
Mouse
|
||||
DoubleClick
|
||||
@@ -130,6 +207,8 @@ const (
|
||||
Jump
|
||||
JumpCancel
|
||||
ClickHeader
|
||||
ClickFooter
|
||||
Multi
|
||||
)
|
||||
|
||||
func (t EventType) AsEvent() Event {
|
||||
@@ -150,12 +229,19 @@ func (e Event) Comparable() Event {
|
||||
}
|
||||
|
||||
func (e Event) KeyName() string {
|
||||
if me := e.MouseEvent; me != nil {
|
||||
return me.Name()
|
||||
}
|
||||
|
||||
if e.Type >= Invalid {
|
||||
return ""
|
||||
}
|
||||
|
||||
switch e.Type {
|
||||
case Rune:
|
||||
if e.Char == ' ' {
|
||||
return "space"
|
||||
}
|
||||
return string(e.Char)
|
||||
case Alt:
|
||||
return "alt-" + string(e.Char)
|
||||
@@ -209,6 +295,13 @@ func (a ColorAttr) IsColorDefined() bool {
|
||||
return a.Color != colUndefined
|
||||
}
|
||||
|
||||
func (a ColorAttr) IsAttrDefined() bool {
|
||||
return a.Attr != AttrUndefined
|
||||
}
|
||||
func (a ColorAttr) IsUndefined() bool {
|
||||
return !a.IsColorDefined() && !a.IsAttrDefined()
|
||||
}
|
||||
|
||||
func NewColorAttr() ColorAttr {
|
||||
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
||||
}
|
||||
@@ -237,6 +330,14 @@ const (
|
||||
colMagenta
|
||||
colCyan
|
||||
colWhite
|
||||
colGrey
|
||||
colBrightRed
|
||||
colBrightGreen
|
||||
colBrightYellow
|
||||
colBrightBlue
|
||||
colBrightMagenta
|
||||
colBrightCyan
|
||||
colBrightWhite
|
||||
)
|
||||
|
||||
type FillReturn int
|
||||
@@ -264,6 +365,10 @@ func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
|
||||
return ColorPair{fg, bg, attr}
|
||||
}
|
||||
|
||||
func NoColorPair() ColorPair {
|
||||
return ColorPair{-1, -1, 0}
|
||||
}
|
||||
|
||||
func (p ColorPair) Fg() Color {
|
||||
return p.fg
|
||||
}
|
||||
@@ -276,6 +381,14 @@ func (p ColorPair) Attr() Attr {
|
||||
return p.attr
|
||||
}
|
||||
|
||||
func (p ColorPair) IsFullBgMarker() bool {
|
||||
return p.attr&FullBg > 0
|
||||
}
|
||||
|
||||
func (p ColorPair) ShouldStripColors() bool {
|
||||
return p.attr&Strip > 0
|
||||
}
|
||||
|
||||
func (p ColorPair) HasBg() bool {
|
||||
return p.attr&Reverse == 0 && p.bg != colDefault ||
|
||||
p.attr&Reverse > 0 && p.fg != colDefault
|
||||
@@ -299,6 +412,18 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
||||
return dup
|
||||
}
|
||||
|
||||
func (p ColorPair) WithFg(fg ColorAttr) ColorPair {
|
||||
dup := p
|
||||
fgPair := ColorPair{fg.Color, colUndefined, fg.Attr}
|
||||
return dup.Merge(fgPair)
|
||||
}
|
||||
|
||||
func (p ColorPair) WithBg(bg ColorAttr) ColorPair {
|
||||
dup := p
|
||||
bgPair := ColorPair{colUndefined, bg.Color, bg.Attr}
|
||||
return dup.Merge(bgPair)
|
||||
}
|
||||
|
||||
func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
|
||||
return p.WithAttr(other.attr)
|
||||
}
|
||||
@@ -314,12 +439,15 @@ func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
|
||||
type ColorTheme struct {
|
||||
Colored bool
|
||||
Input ColorAttr
|
||||
Ghost ColorAttr
|
||||
Disabled ColorAttr
|
||||
Fg ColorAttr
|
||||
Bg ColorAttr
|
||||
ListFg ColorAttr
|
||||
ListBg ColorAttr
|
||||
AltBg ColorAttr
|
||||
Nth ColorAttr
|
||||
Nomatch ColorAttr
|
||||
SelectedFg ColorAttr
|
||||
SelectedBg ColorAttr
|
||||
SelectedMatch ColorAttr
|
||||
@@ -342,6 +470,10 @@ type ColorTheme struct {
|
||||
HeaderBg ColorAttr
|
||||
HeaderBorder ColorAttr
|
||||
HeaderLabel ColorAttr
|
||||
Footer ColorAttr
|
||||
FooterBg ColorAttr
|
||||
FooterBorder ColorAttr
|
||||
FooterLabel ColorAttr
|
||||
Separator ColorAttr
|
||||
Scrollbar ColorAttr
|
||||
Border ColorAttr
|
||||
@@ -367,7 +499,37 @@ type MouseEvent struct {
|
||||
Left bool
|
||||
Down bool
|
||||
Double bool
|
||||
Mod bool
|
||||
Ctrl bool
|
||||
Alt bool
|
||||
Shift bool
|
||||
}
|
||||
|
||||
func (e MouseEvent) Mod() bool {
|
||||
return e.Ctrl || e.Alt || e.Shift
|
||||
}
|
||||
|
||||
func (e MouseEvent) Name() string {
|
||||
name := ""
|
||||
if e.Down {
|
||||
return name
|
||||
}
|
||||
|
||||
if e.Ctrl {
|
||||
name += "ctrl-"
|
||||
}
|
||||
if e.Alt {
|
||||
name += "alt-"
|
||||
}
|
||||
if e.Shift {
|
||||
name += "shift-"
|
||||
}
|
||||
if e.Double {
|
||||
name += "double-"
|
||||
}
|
||||
if !e.Left {
|
||||
name += "right-"
|
||||
}
|
||||
return name + "click"
|
||||
}
|
||||
|
||||
type BorderShape int
|
||||
@@ -376,6 +538,7 @@ const (
|
||||
BorderUndefined BorderShape = iota
|
||||
BorderLine
|
||||
BorderNone
|
||||
BorderPhantom
|
||||
BorderRounded
|
||||
BorderSharp
|
||||
BorderBold
|
||||
@@ -392,7 +555,7 @@ const (
|
||||
|
||||
func (s BorderShape) HasLeft() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -400,7 +563,7 @@ func (s BorderShape) HasLeft() bool {
|
||||
|
||||
func (s BorderShape) HasRight() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -408,7 +571,7 @@ func (s BorderShape) HasRight() bool {
|
||||
|
||||
func (s BorderShape) HasTop() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -416,7 +579,7 @@ func (s BorderShape) HasTop() bool {
|
||||
|
||||
func (s BorderShape) HasBottom() bool {
|
||||
switch s {
|
||||
case BorderNone, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -441,9 +604,9 @@ type BorderStyle struct {
|
||||
type BorderCharacter int
|
||||
|
||||
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||
if shape == BorderNone {
|
||||
if shape == BorderNone || shape == BorderPhantom {
|
||||
return BorderStyle{
|
||||
shape: shape,
|
||||
shape: BorderNone,
|
||||
top: ' ',
|
||||
bottom: ' ',
|
||||
left: ' ',
|
||||
@@ -564,6 +727,7 @@ const (
|
||||
WindowPreview
|
||||
WindowInput
|
||||
WindowHeader
|
||||
WindowFooter
|
||||
)
|
||||
|
||||
type Renderer interface {
|
||||
@@ -579,6 +743,9 @@ type Renderer interface {
|
||||
PassThrough(string)
|
||||
NeedScrollbarRedraw() bool
|
||||
ShouldEmitResizeEvent() bool
|
||||
Bell()
|
||||
HideCursor()
|
||||
ShowCursor()
|
||||
|
||||
GetChar() Event
|
||||
|
||||
@@ -618,6 +785,8 @@ type Window interface {
|
||||
LinkEnd()
|
||||
Erase()
|
||||
EraseMaybe() bool
|
||||
|
||||
SetWrapSign(string, int)
|
||||
}
|
||||
|
||||
type FullscreenRenderer struct {
|
||||
@@ -626,6 +795,7 @@ type FullscreenRenderer struct {
|
||||
forceBlack bool
|
||||
prevDownTime time.Time
|
||||
clicks [][2]int
|
||||
showCursor bool
|
||||
}
|
||||
|
||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||
@@ -634,11 +804,14 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
||||
mouse: mouse,
|
||||
forceBlack: forceBlack,
|
||||
prevDownTime: time.Unix(0, 0),
|
||||
clicks: [][2]int{}}
|
||||
clicks: [][2]int{},
|
||||
showCursor: true}
|
||||
return r
|
||||
}
|
||||
|
||||
var (
|
||||
NoColorTheme *ColorTheme
|
||||
EmptyTheme *ColorTheme
|
||||
Default16 *ColorTheme
|
||||
Dark256 *ColorTheme
|
||||
Light256 *ColorTheme
|
||||
@@ -647,9 +820,11 @@ var (
|
||||
ColNormal ColorPair
|
||||
ColInput ColorPair
|
||||
ColDisabled ColorPair
|
||||
ColGhost ColorPair
|
||||
ColMatch ColorPair
|
||||
ColCursor ColorPair
|
||||
ColCursorEmpty ColorPair
|
||||
ColCursorEmptyChar ColorPair
|
||||
ColMarker ColorPair
|
||||
ColSelected ColorPair
|
||||
ColSelectedMatch ColorPair
|
||||
@@ -664,6 +839,9 @@ var (
|
||||
ColHeader ColorPair
|
||||
ColHeaderBorder ColorPair
|
||||
ColHeaderLabel ColorPair
|
||||
ColFooter ColorPair
|
||||
ColFooterBorder ColorPair
|
||||
ColFooterLabel ColorPair
|
||||
ColSeparator ColorPair
|
||||
ColScrollbar ColorPair
|
||||
ColGapLine ColorPair
|
||||
@@ -680,146 +858,170 @@ var (
|
||||
ColInputLabel ColorPair
|
||||
)
|
||||
|
||||
func EmptyTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||
Marker: ColorAttr{colUndefined, AttrUndefined},
|
||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
func NoColorTheme() *ColorTheme {
|
||||
return &ColorTheme{
|
||||
Colored: false,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||
Match: ColorAttr{colDefault, Underline},
|
||||
Current: ColorAttr{colDefault, Reverse},
|
||||
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||
Info: ColorAttr{colDefault, AttrUndefined},
|
||||
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||
Marker: ColorAttr{colDefault, AttrUndefined},
|
||||
Header: ColorAttr{colDefault, AttrUndefined},
|
||||
Border: ColorAttr{colDefault, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
ListLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
ListBorder: ColorAttr{colDefault, AttrUndefined},
|
||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||
InputBg: ColorAttr{colDefault, AttrUndefined},
|
||||
InputBorder: ColorAttr{colDefault, AttrUndefined},
|
||||
InputLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
HeaderBg: ColorAttr{colDefault, AttrUndefined},
|
||||
HeaderBorder: ColorAttr{colDefault, AttrUndefined},
|
||||
HeaderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||
GapLine: ColorAttr{colDefault, AttrUndefined},
|
||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
defaultColor := ColorAttr{colDefault, AttrUndefined}
|
||||
undefined := ColorAttr{colUndefined, AttrUndefined}
|
||||
|
||||
NoColorTheme = &ColorTheme{
|
||||
Colored: false,
|
||||
Input: defaultColor,
|
||||
Fg: defaultColor,
|
||||
Bg: defaultColor,
|
||||
ListFg: defaultColor,
|
||||
ListBg: defaultColor,
|
||||
AltBg: undefined,
|
||||
SelectedFg: defaultColor,
|
||||
SelectedBg: defaultColor,
|
||||
SelectedMatch: defaultColor,
|
||||
DarkBg: defaultColor,
|
||||
Prompt: defaultColor,
|
||||
Match: defaultColor,
|
||||
Current: undefined,
|
||||
CurrentMatch: undefined,
|
||||
Spinner: defaultColor,
|
||||
Info: defaultColor,
|
||||
Cursor: defaultColor,
|
||||
Marker: defaultColor,
|
||||
Header: defaultColor,
|
||||
Border: undefined,
|
||||
BorderLabel: defaultColor,
|
||||
Ghost: undefined,
|
||||
Disabled: defaultColor,
|
||||
PreviewFg: defaultColor,
|
||||
PreviewBg: defaultColor,
|
||||
Gutter: undefined,
|
||||
PreviewBorder: defaultColor,
|
||||
PreviewScrollbar: defaultColor,
|
||||
PreviewLabel: defaultColor,
|
||||
ListLabel: defaultColor,
|
||||
ListBorder: defaultColor,
|
||||
Separator: defaultColor,
|
||||
Scrollbar: defaultColor,
|
||||
InputBg: defaultColor,
|
||||
InputBorder: defaultColor,
|
||||
InputLabel: defaultColor,
|
||||
HeaderBg: defaultColor,
|
||||
HeaderBorder: defaultColor,
|
||||
HeaderLabel: defaultColor,
|
||||
FooterBg: defaultColor,
|
||||
FooterBorder: defaultColor,
|
||||
FooterLabel: defaultColor,
|
||||
GapLine: defaultColor,
|
||||
Nth: undefined,
|
||||
Nomatch: undefined,
|
||||
}
|
||||
|
||||
EmptyTheme = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: undefined,
|
||||
Fg: undefined,
|
||||
Bg: undefined,
|
||||
ListFg: undefined,
|
||||
ListBg: undefined,
|
||||
AltBg: undefined,
|
||||
SelectedFg: undefined,
|
||||
SelectedBg: undefined,
|
||||
SelectedMatch: undefined,
|
||||
DarkBg: undefined,
|
||||
Prompt: undefined,
|
||||
Match: undefined,
|
||||
Current: undefined,
|
||||
CurrentMatch: undefined,
|
||||
Spinner: undefined,
|
||||
Info: undefined,
|
||||
Cursor: undefined,
|
||||
Marker: undefined,
|
||||
Header: undefined,
|
||||
Footer: undefined,
|
||||
Border: undefined,
|
||||
BorderLabel: undefined,
|
||||
ListLabel: undefined,
|
||||
ListBorder: undefined,
|
||||
Ghost: undefined,
|
||||
Disabled: undefined,
|
||||
PreviewFg: undefined,
|
||||
PreviewBg: undefined,
|
||||
Gutter: undefined,
|
||||
PreviewBorder: undefined,
|
||||
PreviewScrollbar: undefined,
|
||||
PreviewLabel: undefined,
|
||||
Separator: undefined,
|
||||
Scrollbar: undefined,
|
||||
InputBg: undefined,
|
||||
InputBorder: undefined,
|
||||
InputLabel: undefined,
|
||||
HeaderBg: undefined,
|
||||
HeaderBorder: undefined,
|
||||
HeaderLabel: undefined,
|
||||
FooterBg: undefined,
|
||||
FooterBorder: undefined,
|
||||
FooterLabel: undefined,
|
||||
GapLine: undefined,
|
||||
Nth: undefined,
|
||||
Nomatch: undefined,
|
||||
}
|
||||
|
||||
Default16 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||
Input: defaultColor,
|
||||
Fg: defaultColor,
|
||||
Bg: defaultColor,
|
||||
ListFg: undefined,
|
||||
ListBg: undefined,
|
||||
AltBg: undefined,
|
||||
SelectedFg: undefined,
|
||||
SelectedBg: undefined,
|
||||
SelectedMatch: undefined,
|
||||
DarkBg: ColorAttr{colGrey, AttrUndefined},
|
||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||
Match: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colYellow, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||
Current: ColorAttr{colBrightWhite, AttrUndefined},
|
||||
CurrentMatch: ColorAttr{colBrightGreen, AttrUndefined},
|
||||
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||
Info: ColorAttr{colWhite, AttrUndefined},
|
||||
Info: ColorAttr{colYellow, AttrUndefined},
|
||||
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||
Marker: ColorAttr{colMagenta, AttrUndefined},
|
||||
Header: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: ColorAttr{colBlack, AttrUndefined},
|
||||
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
||||
Footer: ColorAttr{colCyan, AttrUndefined},
|
||||
Border: undefined,
|
||||
BorderLabel: defaultColor,
|
||||
Ghost: undefined,
|
||||
Disabled: undefined,
|
||||
PreviewFg: undefined,
|
||||
PreviewBg: undefined,
|
||||
Gutter: undefined,
|
||||
PreviewBorder: undefined,
|
||||
PreviewScrollbar: undefined,
|
||||
PreviewLabel: undefined,
|
||||
ListLabel: undefined,
|
||||
ListBorder: undefined,
|
||||
Separator: undefined,
|
||||
Scrollbar: undefined,
|
||||
InputBg: undefined,
|
||||
InputBorder: undefined,
|
||||
InputLabel: undefined,
|
||||
HeaderBg: undefined,
|
||||
HeaderBorder: undefined,
|
||||
HeaderLabel: undefined,
|
||||
FooterBg: undefined,
|
||||
FooterBorder: undefined,
|
||||
FooterLabel: undefined,
|
||||
GapLine: undefined,
|
||||
Nth: undefined,
|
||||
Nomatch: undefined,
|
||||
}
|
||||
|
||||
Dark256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Input: defaultColor,
|
||||
Fg: defaultColor,
|
||||
Bg: defaultColor,
|
||||
ListFg: undefined,
|
||||
ListBg: undefined,
|
||||
AltBg: undefined,
|
||||
SelectedFg: undefined,
|
||||
SelectedBg: undefined,
|
||||
SelectedMatch: undefined,
|
||||
DarkBg: ColorAttr{236, AttrUndefined},
|
||||
Prompt: ColorAttr{110, AttrUndefined},
|
||||
Match: ColorAttr{108, AttrUndefined},
|
||||
@@ -830,35 +1032,46 @@ func init() {
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Marker: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{109, AttrUndefined},
|
||||
Footer: ColorAttr{109, AttrUndefined},
|
||||
Border: ColorAttr{59, AttrUndefined},
|
||||
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
||||
Ghost: undefined,
|
||||
Disabled: undefined,
|
||||
PreviewFg: undefined,
|
||||
PreviewBg: undefined,
|
||||
Gutter: undefined,
|
||||
PreviewBorder: undefined,
|
||||
PreviewScrollbar: undefined,
|
||||
PreviewLabel: undefined,
|
||||
ListLabel: undefined,
|
||||
ListBorder: undefined,
|
||||
Separator: undefined,
|
||||
Scrollbar: undefined,
|
||||
InputBg: undefined,
|
||||
InputBorder: undefined,
|
||||
InputLabel: undefined,
|
||||
HeaderBg: undefined,
|
||||
HeaderBorder: undefined,
|
||||
HeaderLabel: undefined,
|
||||
FooterBg: undefined,
|
||||
FooterBorder: undefined,
|
||||
FooterLabel: undefined,
|
||||
GapLine: undefined,
|
||||
Nth: undefined,
|
||||
Nomatch: undefined,
|
||||
}
|
||||
|
||||
Light256 = &ColorTheme{
|
||||
Colored: true,
|
||||
Input: ColorAttr{colDefault, AttrUndefined},
|
||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||
Input: defaultColor,
|
||||
Fg: defaultColor,
|
||||
Bg: defaultColor,
|
||||
ListFg: undefined,
|
||||
ListBg: undefined,
|
||||
AltBg: undefined,
|
||||
SelectedFg: undefined,
|
||||
SelectedBg: undefined,
|
||||
SelectedMatch: undefined,
|
||||
DarkBg: ColorAttr{251, AttrUndefined},
|
||||
Prompt: ColorAttr{25, AttrUndefined},
|
||||
Match: ColorAttr{66, AttrUndefined},
|
||||
@@ -869,35 +1082,57 @@ func init() {
|
||||
Cursor: ColorAttr{161, AttrUndefined},
|
||||
Marker: ColorAttr{168, AttrUndefined},
|
||||
Header: ColorAttr{31, AttrUndefined},
|
||||
Footer: ColorAttr{31, AttrUndefined},
|
||||
Border: ColorAttr{145, AttrUndefined},
|
||||
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
InputLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
HeaderBg: ColorAttr{colUndefined, AttrUndefined},
|
||||
HeaderBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||
HeaderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||
GapLine: ColorAttr{colUndefined, AttrUndefined},
|
||||
Nth: ColorAttr{colUndefined, AttrUndefined},
|
||||
Ghost: undefined,
|
||||
Disabled: undefined,
|
||||
PreviewFg: undefined,
|
||||
PreviewBg: undefined,
|
||||
Gutter: undefined,
|
||||
PreviewBorder: undefined,
|
||||
PreviewScrollbar: undefined,
|
||||
PreviewLabel: undefined,
|
||||
ListLabel: undefined,
|
||||
ListBorder: undefined,
|
||||
Separator: undefined,
|
||||
Scrollbar: undefined,
|
||||
InputBg: undefined,
|
||||
InputBorder: undefined,
|
||||
InputLabel: undefined,
|
||||
HeaderBg: undefined,
|
||||
HeaderBorder: undefined,
|
||||
HeaderLabel: undefined,
|
||||
FooterBg: undefined,
|
||||
FooterBorder: undefined,
|
||||
FooterLabel: undefined,
|
||||
GapLine: undefined,
|
||||
Nth: undefined,
|
||||
Nomatch: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
|
||||
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, boldify bool, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
|
||||
if forceBlack {
|
||||
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
||||
}
|
||||
|
||||
if boldify {
|
||||
boldify := func(c ColorAttr) ColorAttr {
|
||||
dup := c
|
||||
if (c.Attr & AttrRegular) == 0 {
|
||||
dup.Attr |= BoldForce
|
||||
}
|
||||
return dup
|
||||
}
|
||||
theme.Current = boldify(theme.Current)
|
||||
theme.CurrentMatch = boldify(theme.CurrentMatch)
|
||||
theme.Prompt = boldify(theme.Prompt)
|
||||
theme.Input = boldify(theme.Input)
|
||||
theme.Cursor = boldify(theme.Cursor)
|
||||
theme.Spinner = boldify(theme.Spinner)
|
||||
}
|
||||
|
||||
o := func(a ColorAttr, b ColorAttr) ColorAttr {
|
||||
c := a
|
||||
if b.Color != colUndefined {
|
||||
@@ -913,17 +1148,36 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
|
||||
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
||||
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
||||
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
||||
theme.Match = o(baseTheme.Match, theme.Match)
|
||||
match := theme.Match
|
||||
if !baseTheme.Colored && match.IsUndefined() {
|
||||
match.Attr = Underline
|
||||
}
|
||||
theme.Match = o(baseTheme.Match, match)
|
||||
// Inherit from 'fg', so that we don't have to write 'current-fg:dim'
|
||||
// e.g. fzf --delimiter / --nth -1 --color fg:dim,nth:regular
|
||||
theme.Current = theme.Fg.Merge(o(baseTheme.Current, theme.Current))
|
||||
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
|
||||
current := theme.Current
|
||||
if !baseTheme.Colored && current.IsUndefined() {
|
||||
current.Attr = Reverse
|
||||
}
|
||||
theme.Current = theme.Fg.Merge(o(baseTheme.Current, current))
|
||||
currentMatch := theme.CurrentMatch
|
||||
if !baseTheme.Colored && currentMatch.IsUndefined() {
|
||||
currentMatch.Attr = Reverse | Underline
|
||||
}
|
||||
theme.CurrentMatch = o(baseTheme.CurrentMatch, currentMatch)
|
||||
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
|
||||
theme.Info = o(baseTheme.Info, theme.Info)
|
||||
theme.Cursor = o(baseTheme.Cursor, theme.Cursor)
|
||||
theme.Marker = o(baseTheme.Marker, theme.Marker)
|
||||
theme.Header = o(baseTheme.Header, theme.Header)
|
||||
theme.Border = o(baseTheme.Border, theme.Border)
|
||||
theme.Footer = o(baseTheme.Footer, theme.Footer)
|
||||
|
||||
// If border color is undefined, set it to default color with dim attribute.
|
||||
border := theme.Border
|
||||
if baseTheme.Border.IsUndefined() && border.IsUndefined() {
|
||||
border.Attr = Dim
|
||||
}
|
||||
theme.Border = o(baseTheme.Border, border)
|
||||
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
||||
|
||||
undefined := NewColorAttr()
|
||||
@@ -936,8 +1190,23 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
|
||||
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
|
||||
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
||||
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||
|
||||
ghost := theme.Ghost
|
||||
if ghost.IsUndefined() {
|
||||
ghost.Attr = Dim
|
||||
} else if ghost.IsColorDefined() && !ghost.IsAttrDefined() {
|
||||
// Don't want to inherit 'bold' from 'input'
|
||||
ghost.Attr = AttrRegular
|
||||
}
|
||||
theme.Ghost = o(theme.Input, ghost)
|
||||
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
||||
|
||||
// Use dim gutter on non-colored themes if undefined
|
||||
gutter := theme.Gutter
|
||||
if !baseTheme.Colored && gutter.IsUndefined() {
|
||||
gutter.Attr = Dim
|
||||
}
|
||||
theme.Gutter = o(theme.DarkBg, gutter)
|
||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
||||
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
||||
@@ -975,6 +1244,14 @@ func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInp
|
||||
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
|
||||
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
|
||||
|
||||
theme.FooterBg = o(theme.Bg, theme.FooterBg)
|
||||
theme.FooterBorder = o(theme.Border, theme.FooterBorder)
|
||||
theme.FooterLabel = o(theme.BorderLabel, theme.FooterLabel)
|
||||
|
||||
if theme.Nomatch.IsUndefined() {
|
||||
theme.Nomatch.Attr = Dim
|
||||
}
|
||||
|
||||
initPalette(theme)
|
||||
}
|
||||
|
||||
@@ -992,15 +1269,17 @@ func initPalette(theme *ColorTheme) {
|
||||
ColNormal = pair(theme.ListFg, theme.ListBg)
|
||||
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
|
||||
ColInput = pair(theme.Input, theme.InputBg)
|
||||
ColDisabled = pair(theme.Disabled, theme.ListBg)
|
||||
ColGhost = pair(theme.Ghost, theme.InputBg)
|
||||
ColDisabled = pair(theme.Disabled, theme.InputBg)
|
||||
ColMatch = pair(theme.Match, theme.ListBg)
|
||||
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
|
||||
ColCursor = pair(theme.Cursor, theme.Gutter)
|
||||
ColCursorEmpty = pair(blank, theme.Gutter)
|
||||
ColCursorEmptyChar = pair(theme.Gutter, theme.ListBg)
|
||||
if theme.SelectedBg.Color != theme.ListBg.Color {
|
||||
ColMarker = pair(theme.Marker, theme.SelectedBg)
|
||||
} else {
|
||||
ColMarker = pair(theme.Marker, theme.Gutter)
|
||||
ColMarker = pair(theme.Marker, theme.ListBg)
|
||||
}
|
||||
ColCurrent = pair(theme.Current, theme.DarkBg)
|
||||
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
|
||||
@@ -1027,6 +1306,9 @@ func initPalette(theme *ColorTheme) {
|
||||
ColHeader = pair(theme.Header, theme.HeaderBg)
|
||||
ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg)
|
||||
ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg)
|
||||
ColFooter = pair(theme.Footer, theme.FooterBg)
|
||||
ColFooterBorder = pair(theme.FooterBorder, theme.FooterBg)
|
||||
ColFooterLabel = pair(theme.FooterLabel, theme.FooterBg)
|
||||
}
|
||||
|
||||
func runeWidth(r rune) int {
|
||||
|
||||
@@ -184,9 +184,31 @@ func (chars *Chars) TrailingWhitespaces() int {
|
||||
return whitespaces
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimTrailingWhitespaces() {
|
||||
func (chars *Chars) TrimTrailingWhitespaces(maxIndex int) {
|
||||
whitespaces := chars.TrailingWhitespaces()
|
||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||
end := len(chars.slice) - whitespaces
|
||||
chars.slice = chars.slice[0:Max(end, maxIndex)]
|
||||
}
|
||||
|
||||
func (chars *Chars) TrimSuffix(runes []rune) {
|
||||
lastIdx := len(chars.slice)
|
||||
firstIdx := lastIdx - len(runes)
|
||||
if firstIdx < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := firstIdx; i < lastIdx; i++ {
|
||||
char := chars.Get(i)
|
||||
if char != runes[i-firstIdx] {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
chars.slice = chars.slice[0:firstIdx]
|
||||
}
|
||||
|
||||
func (chars *Chars) SliceRight(last int) {
|
||||
chars.slice = chars.slice[:last]
|
||||
}
|
||||
|
||||
func (chars *Chars) ToString() string {
|
||||
@@ -273,9 +295,10 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
line = line[:len(line)-1]
|
||||
}
|
||||
|
||||
hasWrapSign := false
|
||||
for {
|
||||
cols := wrapCols
|
||||
if len(wrapped) > 0 {
|
||||
if hasWrapSign {
|
||||
cols -= wrapSignWidth
|
||||
}
|
||||
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
|
||||
@@ -288,9 +311,11 @@ func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWi
|
||||
return wrapped, true
|
||||
}
|
||||
wrapped = append(wrapped, line[:overflowIdx])
|
||||
hasWrapSign = true
|
||||
line = line[overflowIdx:]
|
||||
continue
|
||||
}
|
||||
hasWrapSign = false
|
||||
|
||||
// Restore trailing '\n'
|
||||
if newline {
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestCharsLines(t *testing.T) {
|
||||
check(true, 100, 3, 1, 1, 8, false)
|
||||
|
||||
// With wrap sign (3 + 2)
|
||||
check(true, 100, 3, 2, 1, 12, false)
|
||||
check(true, 100, 3, 2, 1, 10, false)
|
||||
|
||||
// With wrap sign (3 + 2) and no multi-line
|
||||
check(false, 100, 3, 2, 1, 13, false)
|
||||
|
||||
39
src/util/concurrent_set.go
Normal file
39
src/util/concurrent_set.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package util
|
||||
|
||||
import "sync"
|
||||
|
||||
// ConcurrentSet is a thread-safe set implementation.
|
||||
type ConcurrentSet[T comparable] struct {
|
||||
lock sync.RWMutex
|
||||
items map[T]struct{}
|
||||
}
|
||||
|
||||
// NewConcurrentSet creates a new ConcurrentSet.
|
||||
func NewConcurrentSet[T comparable]() *ConcurrentSet[T] {
|
||||
return &ConcurrentSet[T]{
|
||||
items: make(map[T]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Add adds an item to the set.
|
||||
func (s *ConcurrentSet[T]) Add(item T) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
s.items[item] = struct{}{}
|
||||
}
|
||||
|
||||
// Remove removes an item from the set.
|
||||
func (s *ConcurrentSet[T]) Remove(item T) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
delete(s.items, item)
|
||||
}
|
||||
|
||||
// ForEach iterates over each item in the set and applies the provided function.
|
||||
func (s *ConcurrentSet[T]) ForEach(fn func(item T)) {
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
for item := range s.items {
|
||||
fn(item)
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import "sync"
|
||||
type EventType int
|
||||
|
||||
// Events is a type that associates EventType to any data
|
||||
type Events map[EventType]interface{}
|
||||
type Events map[EventType]any
|
||||
|
||||
// EventBox is used for coordinating events
|
||||
type EventBox struct {
|
||||
@@ -36,7 +36,7 @@ func (b *EventBox) Wait(callback func(*Events)) {
|
||||
}
|
||||
|
||||
// Set turns on the event type on the box
|
||||
func (b *EventBox) Set(event EventType, value interface{}) {
|
||||
func (b *EventBox) Set(event EventType, value any) {
|
||||
b.cond.L.Lock()
|
||||
b.events[event] = value
|
||||
if _, found := b.ignore[event]; !found {
|
||||
|
||||
@@ -97,24 +97,12 @@ func Min32(first int32, second int32) int32 {
|
||||
|
||||
// Constrain32 limits the given 32-bit integer with the upper and lower bounds
|
||||
func Constrain32(val int32, min int32, max int32) int32 {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
return Max32(Min32(val, max), min)
|
||||
}
|
||||
|
||||
// Constrain limits the given integer with the upper and lower bounds
|
||||
func Constrain(val int, min int, max int) int {
|
||||
if val < min {
|
||||
return min
|
||||
}
|
||||
if val > max {
|
||||
return max
|
||||
}
|
||||
return val
|
||||
return Max(Min(val, max), min)
|
||||
}
|
||||
|
||||
func AsUint16(val int) uint16 {
|
||||
|
||||
240
test/lib/common.rb
Normal file
240
test/lib/common.rb
Normal file
@@ -0,0 +1,240 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bundler/setup'
|
||||
require 'minitest/autorun'
|
||||
require 'fileutils'
|
||||
require 'English'
|
||||
require 'shellwords'
|
||||
require 'erb'
|
||||
require 'tempfile'
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
TEMPLATE = File.read(File.expand_path('common.sh', __dir__))
|
||||
UNSETS = %w[
|
||||
FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS
|
||||
FZF_TMUX FZF_TMUX_OPTS
|
||||
FZF_CTRL_T_COMMAND FZF_CTRL_T_OPTS
|
||||
FZF_ALT_C_COMMAND
|
||||
FZF_ALT_C_OPTS FZF_CTRL_R_OPTS
|
||||
FZF_API_KEY
|
||||
].freeze
|
||||
DEFAULT_TIMEOUT = 10
|
||||
|
||||
FILE = File.expand_path(__FILE__)
|
||||
BASE = File.expand_path('../..', __dir__)
|
||||
Dir.chdir(BASE)
|
||||
FZF = %(FZF_DEFAULT_OPTS="--no-scrollbar --gutter ' ' --pointer '>' --marker '>'" FZF_DEFAULT_COMMAND= #{BASE}/bin/fzf).freeze
|
||||
|
||||
def wait(timeout = DEFAULT_TIMEOUT)
|
||||
since = Time.now
|
||||
begin
|
||||
yield or raise Minitest::Assertion, 'Assertion failure'
|
||||
rescue Minitest::Assertion
|
||||
raise if Time.now - since > timeout
|
||||
|
||||
sleep(0.05)
|
||||
retry
|
||||
end
|
||||
end
|
||||
|
||||
class Shell
|
||||
class << self
|
||||
def bash
|
||||
@bash ||=
|
||||
begin
|
||||
bashrc = '/tmp/fzf.bash'
|
||||
File.open(bashrc, 'w') do |f|
|
||||
f.puts ERB.new(TEMPLATE).result(binding)
|
||||
end
|
||||
|
||||
"bash --rcfile #{bashrc}"
|
||||
end
|
||||
end
|
||||
|
||||
def zsh
|
||||
@zsh ||=
|
||||
begin
|
||||
zdotdir = '/tmp/fzf-zsh'
|
||||
FileUtils.rm_rf(zdotdir)
|
||||
FileUtils.mkdir_p(zdotdir)
|
||||
File.open("#{zdotdir}/.zshrc", 'w') do |f|
|
||||
f.puts ERB.new(TEMPLATE).result(binding)
|
||||
end
|
||||
"ZDOTDIR=#{zdotdir} zsh"
|
||||
end
|
||||
end
|
||||
|
||||
def fish
|
||||
"unset #{UNSETS.join(' ')}; rm -f ~/.local/share/fish/fzf_test_history; FZF_DEFAULT_OPTS=\"--no-scrollbar --pointer '>' --marker '>'\" fish_history=fzf_test fish"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Tmux
|
||||
attr_reader :win
|
||||
|
||||
def initialize(shell = :bash)
|
||||
@win = go(%W[new-window -d -P -F #I #{Shell.send(shell)}]).first
|
||||
go(%W[set-window-option -t #{@win} pane-base-index 0])
|
||||
return unless shell == :fish
|
||||
|
||||
send_keys 'function fish_prompt; end; clear', :Enter
|
||||
self.until(&:empty?)
|
||||
end
|
||||
|
||||
def kill
|
||||
go(%W[kill-window -t #{win}])
|
||||
end
|
||||
|
||||
def focus
|
||||
go(%W[select-window -t #{win}])
|
||||
end
|
||||
|
||||
def send_keys(*args)
|
||||
go(%W[send-keys -t #{win}] + args.map(&:to_s))
|
||||
end
|
||||
|
||||
def paste(str)
|
||||
system('tmux', 'setb', str, ';', 'pasteb', '-t', win, ';', 'send-keys', '-t', win, 'Enter')
|
||||
end
|
||||
|
||||
def capture
|
||||
go(%W[capture-pane -p -J -t #{win}]).map(&:rstrip).reverse.drop_while(&:empty?).reverse
|
||||
end
|
||||
|
||||
def until(refresh = false, timeout: DEFAULT_TIMEOUT)
|
||||
lines = nil
|
||||
begin
|
||||
wait(timeout) do
|
||||
lines = capture
|
||||
class << lines
|
||||
def counts
|
||||
lazy
|
||||
.map { |l| l.scan(%r{^. ([0-9]+)/([0-9]+)( \(([0-9]+)\))?}) }
|
||||
.reject(&:empty?)
|
||||
.first&.first&.map(&:to_i)&.values_at(0, 1, 3) || [0, 0, 0]
|
||||
end
|
||||
|
||||
def match_count
|
||||
counts[0]
|
||||
end
|
||||
|
||||
def item_count
|
||||
counts[1]
|
||||
end
|
||||
|
||||
def select_count
|
||||
counts[2]
|
||||
end
|
||||
|
||||
def any_include?(val)
|
||||
method = val.is_a?(Regexp) ? :match : :include?
|
||||
find { |line| line.send(method, val) }
|
||||
end
|
||||
end
|
||||
yield(lines).tap do |ok|
|
||||
send_keys 'C-l' if refresh && !ok
|
||||
end
|
||||
end
|
||||
rescue Minitest::Assertion
|
||||
puts $ERROR_INFO.backtrace
|
||||
puts '>' * 80
|
||||
puts lines
|
||||
puts '<' * 80
|
||||
raise
|
||||
end
|
||||
lines
|
||||
end
|
||||
|
||||
def prepare
|
||||
tries = 0
|
||||
begin
|
||||
self.until(true) do |lines|
|
||||
message = "Prepare[#{tries}]"
|
||||
send_keys ' ', 'C-u', :Enter, message, :Left, :Right
|
||||
lines[-1] == message
|
||||
end
|
||||
rescue Minitest::Assertion
|
||||
(tries += 1) < 5 ? retry : raise
|
||||
end
|
||||
send_keys 'C-u', 'C-l'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def go(args)
|
||||
IO.popen(%w[tmux] + args) { |io| io.readlines(chomp: true) }
|
||||
end
|
||||
end
|
||||
|
||||
class TestBase < Minitest::Test
|
||||
TEMPNAME = Dir::Tmpname.create(%w[fzf]) {}
|
||||
FIFONAME = Dir::Tmpname.create(%w[fzf-fifo]) {}
|
||||
|
||||
def writelines(lines)
|
||||
File.write(TEMPNAME, lines.join("\n"))
|
||||
end
|
||||
|
||||
def tempname
|
||||
TEMPNAME
|
||||
end
|
||||
|
||||
def fzf_output
|
||||
@thread.join.value.chomp.tap { @thread = nil }
|
||||
end
|
||||
|
||||
def fzf_output_lines
|
||||
fzf_output.lines(chomp: true)
|
||||
end
|
||||
|
||||
def setup
|
||||
File.mkfifo(FIFONAME)
|
||||
end
|
||||
|
||||
def teardown
|
||||
FileUtils.rm_f([TEMPNAME, FIFONAME])
|
||||
end
|
||||
|
||||
alias assert_equal_org assert_equal
|
||||
def assert_equal(expected, actual)
|
||||
# Ignore info separator
|
||||
actual = actual&.sub(/\s*─+$/, '') if actual.is_a?(String) && actual&.match?(%r{\d+/\d+})
|
||||
assert_equal_org(expected, actual)
|
||||
end
|
||||
|
||||
# Run fzf with its output piped to a fifo
|
||||
def fzf(*opts)
|
||||
raise 'fzf_output not taken' if @thread
|
||||
|
||||
@thread = Thread.new { File.read(FIFONAME) }
|
||||
fzf!(*opts) + " > #{FIFONAME.shellescape}"
|
||||
end
|
||||
|
||||
def fzf!(*opts)
|
||||
opts = opts.filter_map do |o|
|
||||
case o
|
||||
when Symbol
|
||||
o = o.to_s
|
||||
o.length > 1 ? "--#{o.tr('_', '-')}" : "-#{o}"
|
||||
when String, Numeric
|
||||
o.to_s
|
||||
end
|
||||
end
|
||||
"#{FZF} #{opts.join(' ')}"
|
||||
end
|
||||
end
|
||||
|
||||
class TestInteractive < TestBase
|
||||
attr_reader :tmux
|
||||
|
||||
def setup
|
||||
super
|
||||
@tmux = Tmux.new
|
||||
end
|
||||
|
||||
def teardown
|
||||
super
|
||||
@tmux.kill
|
||||
end
|
||||
end
|
||||
59
test/lib/common.sh
Normal file
59
test/lib/common.sh
Normal file
@@ -0,0 +1,59 @@
|
||||
set -u
|
||||
PS1= PROMPT_COMMAND= HISTFILE= HISTSIZE=100
|
||||
unset <%= UNSETS.join(' ') %>
|
||||
unset $(env | sed -n /^_fzf_orig/s/=.*//p)
|
||||
unset $(declare -F | sed -n "/_fzf/s/.*-f //p")
|
||||
|
||||
export FZF_DEFAULT_OPTS="--no-scrollbar --pointer '>' --marker '>'"
|
||||
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "$PATH" == *<%= BASE %>/bin* ]]; then
|
||||
export PATH="${PATH:+${PATH}:}<%= BASE %>/bin"
|
||||
fi
|
||||
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
[[ $- == *i* ]] && source "<%= BASE %>/shell/completion.<%= __method__ %>" 2> /dev/null
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
source "<%= BASE %>/shell/key-bindings.<%= __method__ %>"
|
||||
|
||||
# Old API
|
||||
_fzf_complete_f() {
|
||||
_fzf_complete "+m --multi --prompt \"prompt-f> \"" "$@" < <(
|
||||
echo foo
|
||||
echo bar
|
||||
)
|
||||
}
|
||||
|
||||
# New API
|
||||
_fzf_complete_g() {
|
||||
_fzf_complete +m --multi --prompt "prompt-g> " -- "$@" < <(
|
||||
echo foo
|
||||
echo bar
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_f_post() {
|
||||
awk '{print "f" $0 $0}'
|
||||
}
|
||||
|
||||
_fzf_complete_g_post() {
|
||||
awk '{print "g" $0 $0}'
|
||||
}
|
||||
|
||||
[ -n "${BASH-}" ] && complete -F _fzf_complete_f -o default -o bashdefault f
|
||||
[ -n "${BASH-}" ] && complete -F _fzf_complete_g -o default -o bashdefault g
|
||||
|
||||
_comprun() {
|
||||
local command=$1
|
||||
shift
|
||||
|
||||
case "$command" in
|
||||
f) fzf "$@" --preview 'echo preview-f-{}' ;;
|
||||
g) fzf "$@" --preview 'echo preview-g-{}' ;;
|
||||
*) fzf "$@" ;;
|
||||
esac
|
||||
}
|
||||
5
test/runner.rb
Normal file
5
test/runner.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
Dir[File.join(__dir__, 'test_*.rb')].each { require it }
|
||||
|
||||
require 'minitest/autorun'
|
||||
2103
test/test_core.rb
Normal file
2103
test/test_core.rb
Normal file
File diff suppressed because it is too large
Load Diff
417
test/test_exec.rb
Normal file
417
test/test_exec.rb
Normal file
@@ -0,0 +1,417 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Process execution: execute, become, reload
|
||||
class TestExec < TestInteractive
|
||||
def test_execute
|
||||
output = '/tmp/fzf-test-execute'
|
||||
opts = %[--bind "alt-a:execute(echo /{}/ >> #{output})+change-header(alt-a),alt-b:execute[echo /{}{}/ >> #{output}]+change-header(alt-b),C:execute(echo /{}{}{}/ >> #{output})+change-header(C)"]
|
||||
writelines(%w[foo'bar foo"bar foo$bar])
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
|
||||
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :C
|
||||
ready.call('C')
|
||||
|
||||
tmux.send_keys 'barfoo'
|
||||
tmux.until { |lines| assert_equal ' 0/3', lines[-2] }
|
||||
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal %w[
|
||||
/foo'bar/ /foo'barfoo'bar/
|
||||
/foo"bar/ /foo"barfoo"bar/
|
||||
/foo$barfoo$barfoo$bar/
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_multi
|
||||
output = '/tmp/fzf-test-execute-multi'
|
||||
opts = %[--multi --bind "alt-a:execute-multi(echo {}/{+} >> #{output})+change-header(alt-a),alt-b:change-header(alt-b)"]
|
||||
writelines(%w[foo'bar foo"bar foo$bar foobar])
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} #{opts}", :Enter
|
||||
ready = ->(s) { tmux.until { |lines| assert_includes lines[-3], s } }
|
||||
|
||||
tmux.until { |lines| assert_equal ' 4/4 (0)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :BTab, :BTab, :BTab
|
||||
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
tmux.send_keys :Escape, :b
|
||||
ready.call('alt-b')
|
||||
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal ' 4/4 (3)', lines[-2] }
|
||||
tmux.send_keys :Escape, :a
|
||||
ready.call('alt-a')
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal [
|
||||
%(foo'bar/foo'bar),
|
||||
%(foo'bar foo"bar foo$bar/foo'bar foo"bar foo$bar),
|
||||
%(foo'bar foo"bar foobar/foo'bar foo"bar foobar)
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_plus_flag
|
||||
output = tempname + '.tmp'
|
||||
FileUtils.rm_f(output)
|
||||
writelines(['foo bar', '123 456'])
|
||||
|
||||
tmux.send_keys "cat #{tempname} | #{FZF} --multi --bind 'x:execute-silent(echo {+}/{}/{+2}/{2} >> #{output})'", :Enter
|
||||
|
||||
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
|
||||
tmux.send_keys 'xy'
|
||||
tmux.until { |lines| assert_equal ' 0/2 (0)', lines[-2] }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal ' 2/2 (0)', lines[-2] }
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.send_keys :Tab
|
||||
tmux.send_keys 'xy'
|
||||
tmux.until { |lines| assert_equal ' 0/2 (1)', lines[-2] }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal ' 2/2 (1)', lines[-2] }
|
||||
|
||||
tmux.send_keys :Tab
|
||||
tmux.send_keys 'xy'
|
||||
tmux.until { |lines| assert_equal ' 0/2 (2)', lines[-2] }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal ' 2/2 (2)', lines[-2] }
|
||||
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal [
|
||||
%(foo bar/foo bar/bar/bar),
|
||||
%(123 456/foo bar/456/bar),
|
||||
%(123 456 foo bar/foo bar/456 bar/bar)
|
||||
], File.readlines(output, chomp: true)
|
||||
end
|
||||
rescue StandardError
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_execute_shell
|
||||
# Custom script to use as $SHELL
|
||||
output = tempname + '.out'
|
||||
FileUtils.rm_f(output)
|
||||
writelines(['#!/usr/bin/env bash', "echo $1 / $2 > #{output}"])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys "echo foo | SHELL=#{tempname} fzf --bind 'enter:execute:{}bar'", :Enter
|
||||
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal ' 1/1', lines[-2] }
|
||||
wait do
|
||||
assert_path_exists output
|
||||
assert_equal ["-c / 'foo'bar"], File.readlines(output, chomp: true)
|
||||
end
|
||||
ensure
|
||||
FileUtils.rm_f(output)
|
||||
end
|
||||
|
||||
def test_interrupt_execute
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'ctrl-l:execute:echo executing {}; sleep 100'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys 'C-l'
|
||||
tmux.until { |lines| assert lines.any_include?('executing 1') }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys 99
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_abort
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys FZF.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_kill_default_command_on_accept
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys fzf.sub('FZF_DEFAULT_COMMAND=', "FZF_DEFAULT_COMMAND=#{tempname}"), :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', fzf_output
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_abort
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{FZF} --bind 'ctrl-r:reload(#{tempname})'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_kill_reload_command_on_accept
|
||||
writelines(['#!/usr/bin/env bash',
|
||||
"echo 'Started'",
|
||||
'while :; do sleep 1; done'])
|
||||
system("chmod +x #{tempname}")
|
||||
|
||||
tmux.send_keys "seq 1 3 | #{fzf("--bind 'ctrl-r:reload(#{tempname})'")}", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
assert_equal 'Started', fzf_output
|
||||
wait { refute system("pgrep -f #{tempname}") }
|
||||
ensure
|
||||
system("pkill -9 -f #{tempname}")
|
||||
end
|
||||
|
||||
def test_reload
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --bind 'change:reload(seq $FZF_QUERY),a:reload(seq 100),b:reload:seq 200' --header-lines 2 --multi 2), :Enter
|
||||
tmux.until { |lines| assert_equal 998, lines.match_count }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until do |lines|
|
||||
assert_equal 98, lines.item_count
|
||||
assert_equal 98, lines.match_count
|
||||
end
|
||||
tmux.send_keys 'b'
|
||||
tmux.until do |lines|
|
||||
assert_equal 198, lines.item_count
|
||||
assert_equal 198, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal ' 198/198 (1/2)', lines[-2] }
|
||||
tmux.send_keys '555'
|
||||
tmux.until { |lines| assert_equal ' 1/553 (0/2)', lines[-2] }
|
||||
end
|
||||
|
||||
def test_reload_even_when_theres_no_match
|
||||
tmux.send_keys %(: | #{FZF} --bind 'space:reload(seq 10)'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.item_count }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 10, lines.item_count }
|
||||
end
|
||||
|
||||
def test_reload_should_terminate_standard_input_stream
|
||||
tmux.send_keys %(ruby -e "STDOUT.sync = true; loop { puts 1; sleep 0.1 }" | fzf --bind 'start:reload(seq 100)'), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
end
|
||||
|
||||
def test_clear_list_when_header_lines_changed_due_to_reload
|
||||
tmux.send_keys %(seq 10 | #{FZF} --header 0 --header-lines 3 --bind 'space:reload(seq 1)'), :Enter
|
||||
tmux.until { |lines| assert_includes lines, ' 9' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| refute_includes lines, ' 9' }
|
||||
end
|
||||
|
||||
def test_item_index_reset_on_reload
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [[{n}]]' --bind 'up:last,down:first,space:reload:seq 100'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[[9]]' }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines[1], '[[0]]' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_includes lines[1], '[[0]]'
|
||||
end
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[[99]]' }
|
||||
end
|
||||
|
||||
def test_reload_should_update_preview
|
||||
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload:echo 4' --preview 'echo {}' --preview-window 'nohidden'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], '1' }
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||
end
|
||||
|
||||
def test_reload_and_change_preview_should_update_preview
|
||||
tmux.send_keys "seq 3 | #{FZF} --bind 'ctrl-t:reload(echo 4)+change-preview(echo {})'", :Enter
|
||||
tmux.until { |lines| assert_equal 3, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], '4' }
|
||||
end
|
||||
|
||||
def test_reload_sync
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind 'load:reload-sync(sleep 1; seq 1000)+unbind(load)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys '00'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
# After 1 second
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case1
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case2
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case3
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case4
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload-sync(sleep 2; seq 1000)+backward-delete-char'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case5
|
||||
tmux.send_keys "seq 100 | #{FZF} --query 99 --bind 'space:disable-search+reload(echo xx; sleep 2; seq 1000)'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :Space
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1001, lines.match_count }
|
||||
end
|
||||
|
||||
def test_reload_disabled_case6
|
||||
tmux.send_keys "seq 1000 | #{FZF} --disabled --bind 'change:reload:sleep 0.5; seq {q}'", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_equal 9, lines.match_count }
|
||||
tmux.send_keys '9'
|
||||
tmux.until { |lines| assert_equal 99, lines.match_count }
|
||||
|
||||
# TODO: How do we verify if an intermediate empty list is not shown?
|
||||
end
|
||||
|
||||
def test_reload_and_change
|
||||
tmux.send_keys "(echo foo; echo bar) | #{FZF} --bind 'load:reload-sync(sleep 60)+change-query(bar)'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
end
|
||||
|
||||
def test_become_tty
|
||||
tmux.send_keys "sleep 0.5 | #{FZF} --bind 'start:reload:ls' --bind 'load:become:tty'", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '/dev/tty' }
|
||||
end
|
||||
|
||||
def test_disabled_preview_update
|
||||
tmux.send_keys "echo bar | #{FZF} --disabled --bind 'change:reload:echo foo' --preview 'echo [{q}-{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[-bar]') }) }
|
||||
tmux.send_keys :x
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?('[x-foo]') }) }
|
||||
end
|
||||
|
||||
def test_start_on_reload
|
||||
tmux.send_keys %(echo foo | #{FZF} --header Loading --header-lines 1 --bind 'start:reload:sleep 2; echo bar' --bind 'load:change-header:Loaded' --bind space:change-header:), :Enter
|
||||
tmux.until(timeout: 1) { |lines| assert_includes lines[-3], 'Loading' }
|
||||
tmux.until(timeout: 1) { |lines| refute_includes lines[-4], 'foo' }
|
||||
tmux.until { |lines| assert_includes lines[-3], 'Loaded' }
|
||||
tmux.until { |lines| assert_includes lines[-4], 'bar' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-3], 'bar' }
|
||||
end
|
||||
|
||||
def test_become
|
||||
tmux.send_keys "seq 100 | fzf --bind 'enter:become:seq {} | fzf'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys 999
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 99, lines.item_count }
|
||||
end
|
||||
end
|
||||
315
test/test_filter.rb
Normal file
315
test/test_filter.rb
Normal file
@@ -0,0 +1,315 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Non-interactive tests
|
||||
class TestFilter < TestBase
|
||||
def test_default_extended
|
||||
assert_equal '100', `seq 100 | #{FZF} -f "1 00$"`.chomp
|
||||
assert_equal '', `seq 100 | #{FZF} -f "1 00$" +x`.chomp
|
||||
end
|
||||
|
||||
def test_exact
|
||||
assert_equal 4, `seq 123 | #{FZF} -f 13`.lines.length
|
||||
assert_equal 2, `seq 123 | #{FZF} -f 13 -e`.lines.length
|
||||
assert_equal 4, `seq 123 | #{FZF} -f 13 +e`.lines.length
|
||||
end
|
||||
|
||||
def test_or_operator
|
||||
assert_equal %w[1 5 10], `seq 10 | #{FZF} -f "1 | 5"`.lines(chomp: true)
|
||||
assert_equal %w[1 10 2 3 4 5 6 7 8 9],
|
||||
`seq 10 | #{FZF} -f '1 | !1'`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_smart_case_for_each_term
|
||||
assert_equal 1, `echo Foo bar | #{FZF} -x -f "foo Fbar" | wc -l`.to_i
|
||||
end
|
||||
|
||||
def test_filter_exitstatus
|
||||
# filter / streaming filter
|
||||
['', '--no-sort'].each do |opts|
|
||||
assert_includes `echo foo | #{FZF} -f foo #{opts}`, 'foo'
|
||||
assert_equal 0, $CHILD_STATUS.exitstatus
|
||||
|
||||
assert_empty `echo foo | #{FZF} -f bar #{opts}`
|
||||
assert_equal 1, $CHILD_STATUS.exitstatus
|
||||
end
|
||||
end
|
||||
|
||||
def test_long_line
|
||||
data = '.' * 256 * 1024
|
||||
File.open(tempname, 'w') do |f|
|
||||
f << data
|
||||
end
|
||||
assert_equal data, `#{FZF} -f . < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_read0
|
||||
lines = `find .`.lines(chomp: true)
|
||||
assert_equal lines.last, `find . | #{FZF} -e -f "^#{lines.last}$"`.chomp
|
||||
assert_equal \
|
||||
lines.last,
|
||||
`find . -print0 | #{FZF} --read0 -e -f "^#{lines.last}$"`.chomp
|
||||
end
|
||||
|
||||
def test_nth_suffix_match
|
||||
assert_equal \
|
||||
'foo,bar,baz',
|
||||
`echo foo,bar,baz | #{FZF} -d, -f'bar$' -n2`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_basic
|
||||
writelines(['hello world ', 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_template
|
||||
writelines(['hello world ', 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he he.he." -x -n 2.. --with-nth '{2} {1}. {1}.' < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_ansi
|
||||
writelines(["\x1b[33mhello \x1b[34;1mworld\x1b[m ", 'byebye'])
|
||||
assert_equal \
|
||||
'hello world ',
|
||||
`#{FZF} -f"^he hehe" -x -n 2.. --with-nth 2,1,1 --ansi < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_with_nth_no_ansi
|
||||
src = "\x1b[33mhello \x1b[34;1mworld\x1b[m "
|
||||
writelines([src, 'byebye'])
|
||||
assert_equal \
|
||||
src,
|
||||
`#{FZF} -fhehe -x -n 2.. --with-nth 2,1,1 --no-ansi < #{tempname}`.chomp
|
||||
end
|
||||
|
||||
def test_escaped_meta_characters
|
||||
input = [
|
||||
'foo^bar',
|
||||
'foo$bar',
|
||||
'foo!bar',
|
||||
"foo'bar",
|
||||
'foo bar',
|
||||
'bar foo'
|
||||
]
|
||||
writelines(input)
|
||||
|
||||
assert_equal input.length, `#{FZF} -f'foo bar' < #{tempname}`.lines.length
|
||||
assert_equal input.length - 1, `#{FZF} -f'^foo bar$' < #{tempname}`.lines.length
|
||||
assert_equal ['foo bar'], `#{FZF} -f'foo\\ bar' < #{tempname}`.lines(chomp: true)
|
||||
assert_equal ['foo bar'], `#{FZF} -f'^foo\\ bar$' < #{tempname}`.lines(chomp: true)
|
||||
assert_equal input.length - 1, `#{FZF} -f'!^foo\\ bar$' < #{tempname}`.lines.length
|
||||
end
|
||||
|
||||
def test_normalized_match
|
||||
echoes = '(echo a; echo á; echo A; echo Á;)'
|
||||
assert_equal %w[a á A Á], `#{echoes} | #{FZF} -f a`.lines.map(&:chomp)
|
||||
assert_equal %w[á Á], `#{echoes} | #{FZF} -f á`.lines.map(&:chomp)
|
||||
assert_equal %w[A Á], `#{echoes} | #{FZF} -f A`.lines.map(&:chomp)
|
||||
assert_equal %w[Á], `#{echoes} | #{FZF} -f Á`.lines.map(&:chomp)
|
||||
end
|
||||
|
||||
def test_unicode_case
|
||||
writelines(%w[строКА1 СТРОКА2 строка3 Строка4])
|
||||
assert_equal %w[СТРОКА2 Строка4], `#{FZF} -fС < #{tempname}`.lines(chomp: true)
|
||||
assert_equal %w[строКА1 СТРОКА2 строка3 Строка4], `#{FZF} -fс < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak
|
||||
input = %w[
|
||||
--foobar--------
|
||||
-----foobar---
|
||||
----foobar--
|
||||
-------foobar-
|
||||
]
|
||||
writelines(input)
|
||||
|
||||
assert_equal input, `#{FZF} -ffoobar --tiebreak=index < #{tempname}`.lines(chomp: true)
|
||||
|
||||
by_length = %w[
|
||||
----foobar--
|
||||
-----foobar---
|
||||
-------foobar-
|
||||
--foobar--------
|
||||
]
|
||||
assert_equal by_length, `#{FZF} -ffoobar < #{tempname}`.lines(chomp: true)
|
||||
assert_equal by_length, `#{FZF} -ffoobar --tiebreak=length < #{tempname}`.lines(chomp: true)
|
||||
|
||||
by_begin = %w[
|
||||
--foobar--------
|
||||
----foobar--
|
||||
-----foobar---
|
||||
-------foobar-
|
||||
]
|
||||
assert_equal by_begin, `#{FZF} -ffoobar --tiebreak=begin < #{tempname}`.lines(chomp: true)
|
||||
assert_equal by_begin, `#{FZF} -f"!z foobar" -x --tiebreak begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal %w[
|
||||
-------foobar-
|
||||
----foobar--
|
||||
-----foobar---
|
||||
--foobar--------
|
||||
], `#{FZF} -ffoobar --tiebreak end < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal input, `#{FZF} -f"!z" -x --tiebreak end < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_index_begin
|
||||
writelines([
|
||||
'xoxxxxxoxx',
|
||||
'xoxxxxxox',
|
||||
'xxoxxxoxx',
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
' xxoxoxxx'
|
||||
])
|
||||
|
||||
assert_equal [
|
||||
'xxxxoxox',
|
||||
' xxoxoxxx',
|
||||
'xxxoxoxxx',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxox',
|
||||
'xoxxxxxoxx'
|
||||
], `#{FZF} -foo < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
' xxoxoxxx',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxoxx',
|
||||
'xoxxxxxox'
|
||||
], `#{FZF} -foo --tiebreak=index < #{tempname}`.lines(chomp: true)
|
||||
|
||||
# Note that --tiebreak=begin is now based on the first occurrence of the
|
||||
# first character on the pattern
|
||||
assert_equal [
|
||||
' xxoxoxxx',
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxoxx',
|
||||
'xoxxxxxox'
|
||||
], `#{FZF} -foo --tiebreak=begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
' xxoxoxxx',
|
||||
'xxxoxoxxx',
|
||||
'xxxxoxox',
|
||||
'xxoxxxoxx',
|
||||
'xoxxxxxox',
|
||||
'xoxxxxxoxx'
|
||||
], `#{FZF} -foo --tiebreak=begin,length < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_begin_algo_v2
|
||||
writelines(['baz foo bar',
|
||||
'foo bar baz'])
|
||||
assert_equal [
|
||||
'foo bar baz',
|
||||
'baz foo bar'
|
||||
], `#{FZF} -fbar --tiebreak=begin --algo=v2 < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_end
|
||||
writelines(['xoxxxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xxxoxxxxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxxxoxxx',
|
||||
' xxxxoxxx'])
|
||||
|
||||
assert_equal [
|
||||
' xxxxoxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxxxoxxx',
|
||||
'xoxxxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xxxoxxxxxx'
|
||||
], `#{FZF} -fo < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'xxxxxoxxx',
|
||||
' xxxxoxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxoxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xoxxxxxxxx'
|
||||
], `#{FZF} -fo --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'xxxxxoxxx',
|
||||
' xxxxoxxx',
|
||||
'xxxxoxxxx',
|
||||
'xxxoxxxxxx',
|
||||
'xxoxxxxxxx',
|
||||
'xoxxxxxxxx'
|
||||
], `#{FZF} -fo --tiebreak=end,length,begin < #{tempname}`.lines(chomp: true)
|
||||
|
||||
writelines(['/bar/baz', '/foo/bar/baz'])
|
||||
assert_equal [
|
||||
'/foo/bar/baz',
|
||||
'/bar/baz'
|
||||
], `#{FZF} -fbaz --tiebreak=end < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_length_with_nth
|
||||
input = %w[
|
||||
1:hell
|
||||
123:hello
|
||||
12345:he
|
||||
1234567:h
|
||||
]
|
||||
writelines(input)
|
||||
|
||||
output = %w[
|
||||
1:hell
|
||||
12345:he
|
||||
123:hello
|
||||
1234567:h
|
||||
]
|
||||
assert_equal output, `#{FZF} -fh < #{tempname}`.lines(chomp: true)
|
||||
|
||||
# Since 0.16.8, --nth doesn't affect --tiebreak
|
||||
assert_equal output, `#{FZF} -fh -n2 -d: < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_tiebreak_chunk
|
||||
writelines(['1 foobarbaz ba',
|
||||
'2 foobar baz',
|
||||
'3 foo barbaz'])
|
||||
|
||||
assert_equal [
|
||||
'3 foo barbaz',
|
||||
'2 foobar baz',
|
||||
'1 foobarbaz ba'
|
||||
], `#{FZF} -fo --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'1 foobarbaz ba',
|
||||
'2 foobar baz',
|
||||
'3 foo barbaz'
|
||||
], `#{FZF} -fba --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
|
||||
assert_equal [
|
||||
'3 foo barbaz'
|
||||
], `#{FZF} -f'!foobar' --tiebreak=chunk < #{tempname}`.lines(chomp: true)
|
||||
end
|
||||
|
||||
def test_boundary_match
|
||||
# Underscore boundaries should be ranked lower
|
||||
{
|
||||
default: [' xyz '] + %w[/xyz/ [xyz] -xyz- -xyz_ _xyz- _xyz_],
|
||||
path: ['/xyz/', ' xyz '] + %w[[xyz] -xyz- -xyz_ _xyz- _xyz_],
|
||||
history: ['[xyz]', '-xyz-', ' xyz '] + %w[/xyz/ -xyz_ _xyz- _xyz_]
|
||||
}.each do |scheme, expected|
|
||||
result = `printf -- 'xxyzx\n-xxyz\nxyzx-\n_xyz_\n_xyz-\n-xyz_\n[xyz]\n-xyz-\n xyz \n/xyz/\n' | #{FZF} -f"'xyz'" --scheme=#{scheme}`.lines(chomp: true)
|
||||
assert_equal expected, result
|
||||
end
|
||||
end
|
||||
end
|
||||
4323
test/test_go.rb
4323
test/test_go.rb
File diff suppressed because it is too large
Load Diff
1292
test/test_layout.rb
Normal file
1292
test/test_layout.rb
Normal file
File diff suppressed because it is too large
Load Diff
575
test/test_preview.rb
Normal file
575
test/test_preview.rb
Normal file
@@ -0,0 +1,575 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Test cases for preview
|
||||
class TestPreview < TestInteractive
|
||||
def test_preview
|
||||
tmux.send_keys %(seq 1000 | sed s/^2$// | #{FZF} -m --preview 'sleep 0.2; echo {{}-{+}}' --bind ?:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], ' {-} ' }
|
||||
tmux.send_keys '555'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[1], ' {555-555} ' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {555-555} ' }
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert lines[-2]&.start_with?(' 28/1000 ') }
|
||||
tmux.send_keys 'foobar'
|
||||
tmux.until { |lines| refute_includes lines[1], ' {55-55} ' }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1-1} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {-1} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {3-1 } ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {4-1 3} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {5-1 3 4} ' }
|
||||
end
|
||||
|
||||
def test_toggle_preview_without_default_preview_command
|
||||
tmux.send_keys %(seq 100 | #{FZF} --bind 'space:preview(echo [{}]),enter:toggle-preview' --preview-window up,border-double), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
refute_includes lines[1], '║ [1]'
|
||||
end
|
||||
|
||||
# toggle-preview should do nothing
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| refute_includes lines[1], '║ [1]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
refute_includes lines[1], '║ [1]'
|
||||
refute_includes lines[1], '║ [2]'
|
||||
end
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 3'
|
||||
refute_includes lines[1], '║ [3]'
|
||||
end
|
||||
|
||||
# One-off preview action
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[1], '║ [3]' }
|
||||
|
||||
# toggle-preview to hide it
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| refute_includes lines[1], '║ [3]' }
|
||||
|
||||
# toggle-preview again does nothing
|
||||
tmux.send_keys :Enter, :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 4'
|
||||
refute_includes lines[1], '║ [4]'
|
||||
end
|
||||
end
|
||||
|
||||
def test_show_and_hide_preview
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview-window hidden,border-bold --preview 'echo [{}]' --bind 'a:show-preview,b:hide-preview'), :Enter
|
||||
|
||||
# Hidden by default
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
refute_includes lines[1], '┃ [1]'
|
||||
end
|
||||
|
||||
# Show
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [1]' }
|
||||
|
||||
# Already shown
|
||||
tmux.send_keys :a
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [2]' }
|
||||
|
||||
# Hide
|
||||
tmux.send_keys :b
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 3'
|
||||
refute_includes lines[1], '┃ [3]'
|
||||
end
|
||||
|
||||
# Already hidden
|
||||
tmux.send_keys :b
|
||||
tmux.send_keys :Up
|
||||
tmux.until do |lines|
|
||||
assert_includes lines, '> 4'
|
||||
refute_includes lines[1], '┃ [4]'
|
||||
end
|
||||
|
||||
# Show it again
|
||||
tmux.send_keys :a
|
||||
tmux.until { |lines| assert_includes lines[1], '┃ [4]' }
|
||||
end
|
||||
|
||||
def test_preview_hidden
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --preview 'echo {{}-{}-$FZF_PREVIEW_LINES-$FZF_PREVIEW_COLUMNS}' --preview-window down:1:hidden --bind ?:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_match(/ {1-1-1-[0-9]+}/, lines[-2]) }
|
||||
tmux.send_keys '555'
|
||||
tmux.until { |lines| assert_match(/ {555-555-1-[0-9]+}/, lines[-2]) }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_equal '> 555', lines[-1] }
|
||||
end
|
||||
|
||||
def test_preview_size_0
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0 --bind space:toggle-preview), :Enter
|
||||
tmux.until do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert_equal ' 100/100', lines[1]
|
||||
assert_equal '> 1', lines[2]
|
||||
end
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down, :Down
|
||||
tmux.until { |lines| assert_equal '> 3', lines[4] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down
|
||||
tmux.until { |lines| assert_equal '> 4', lines[5] }
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[1 3 4], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_size_0_hidden
|
||||
tmux.send_keys %(seq 100 | #{FZF} --reverse --preview 'echo {} >> #{tempname}; echo ' --preview-window 0,hidden --bind space:toggle-preview), :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys :Down, :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 3' }
|
||||
wait { refute_path_exists tempname }
|
||||
tmux.send_keys :Space
|
||||
wait do
|
||||
assert_path_exists tempname
|
||||
assert_equal %w[3], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Down
|
||||
wait do
|
||||
assert_equal %w[3 4], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
tmux.send_keys :Space, :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 5' }
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines, '> 6' }
|
||||
tmux.send_keys :Space
|
||||
wait do
|
||||
assert_equal %w[3 4 6], File.readlines(tempname, chomp: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_flags
|
||||
tmux.send_keys %(seq 10 | sed 's/^/:: /; s/$/ /' |
|
||||
#{FZF} --multi --preview 'echo {{2}/{s2}/{+2}/{+s2}/{q}/{n}/{+n}}'), :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 //0/0} ' }
|
||||
tmux.send_keys '123'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {////123//} ' }
|
||||
tmux.send_keys 'C-u', '1'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' {1/1 /1/1 /1/0/0} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1/1 /1/9/0} ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' {10/10 /1 10/1 10 /1/9/0 9} ' }
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /12//0 9} ' }
|
||||
tmux.send_keys '3'
|
||||
tmux.until { |lines| assert_includes lines[1], ' {//1 10/1 10 /123//0 9} ' }
|
||||
end
|
||||
|
||||
def test_preview_asterisk
|
||||
tmux.send_keys %(seq 5 | #{FZF} --multi --preview 'echo [{}/{+}/{*}/{*n}]' --preview-window '+{1}'), :Enter
|
||||
tmux.until { |lines| assert_equal 5, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' [1/1/1 2 3 4 5/0 1 2 3 4] ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' [2/1/1 2 3 4 5/0 1 2 3 4] ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' [3/1 2/1 2 3 4 5/0 1 2 3 4] ' }
|
||||
tmux.send_keys '5'
|
||||
tmux.until { |lines| assert_includes lines[1], ' [5/1 2/5/4] ' }
|
||||
tmux.send_keys '5'
|
||||
tmux.until { |lines| assert_includes lines[1], ' [/1 2//] ' }
|
||||
end
|
||||
|
||||
def test_preview_file
|
||||
tmux.send_keys %[(echo foo bar; echo bar foo) | #{FZF} --multi --preview 'cat {+f} {+f2} {+nf} {+fn}' --print0], :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar00 ' }
|
||||
tmux.send_keys :BTab
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo barbar foobarfoo0101 ' }
|
||||
end
|
||||
|
||||
def test_preview_q_no_match
|
||||
tmux.send_keys %(: | #{FZF} --preview 'echo foo {q} foo'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||
tmux.send_keys 'bar'
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo bar foo' }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.until { |lines| assert_includes lines[1], ' foo foo' }
|
||||
end
|
||||
|
||||
def test_preview_q_no_match_with_initial_query
|
||||
tmux.send_keys %(: | #{FZF} --preview 'echo 1. /{q}/{q:1}/; echo 2. /{q:..}/{q:2}/{q:-1}/; echo 3. /{q:s-2}/{q:-2}/{q:x}/' --query 'foo bar'), :Enter
|
||||
tmux.until { |lines| assert_equal 0, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], '1. /foo bar/foo/' }
|
||||
tmux.until { |lines| assert_includes lines[2], '2. /foo bar/bar/bar/' }
|
||||
tmux.until { |lines| assert_includes lines[3], '3. /foo /foo/{q:x}/' }
|
||||
end
|
||||
|
||||
def test_preview_update_on_select
|
||||
tmux.send_keys %(seq 10 | fzf -m --preview 'echo {+}' --bind a:toggle-all),
|
||||
:Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert(lines.any? { |line| line.include?(' 1 2 3 4 5 ') }) }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| lines.each { |line| refute_includes line, ' 1 2 3 4 5 ' } }
|
||||
end
|
||||
|
||||
def test_preview_correct_tab_width_after_ansi_reset_code
|
||||
writelines(["\x1b[31m+\x1b[m\t\x1b[32mgreen"])
|
||||
tmux.send_keys "#{FZF} --preview 'cat #{tempname}'", :Enter
|
||||
tmux.until { |lines| assert_includes lines[1], ' + green ' }
|
||||
end
|
||||
|
||||
def test_preview_bindings_with_default_preview
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview 'echo [{}]' --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
|
||||
tmux.until { |lines| lines.match_count == 10 }
|
||||
tmux.until { |lines| assert_includes lines[1], '[1]' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||
tmux.send_keys 'c'
|
||||
tmux.until { |lines| assert_includes lines[1], '[1]' }
|
||||
tmux.send_keys 'b'
|
||||
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[1], '[2]' }
|
||||
end
|
||||
|
||||
def test_preview_bindings_without_default_preview
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind 'a:preview(echo [{}{}]),b:preview(echo [{}{}{}]),c:refresh-preview'", :Enter
|
||||
tmux.until { |lines| lines.match_count == 10 }
|
||||
tmux.until { |lines| refute_includes lines[1], '1' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||
tmux.send_keys 'c' # does nothing
|
||||
tmux.until { |lines| assert_includes lines[1], '[11]' }
|
||||
tmux.send_keys 'b'
|
||||
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||
tmux.send_keys 9
|
||||
tmux.until { |lines| lines.match_count == 1 }
|
||||
tmux.until { |lines| refute_includes lines[1], '2' }
|
||||
tmux.until { |lines| assert_includes lines[1], '[111]' }
|
||||
end
|
||||
|
||||
def test_preview_scroll_begin_constant
|
||||
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+123", :Enter
|
||||
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||
tmux.until { |lines| assert_match %r{123.*123/1000}, lines[1] }
|
||||
end
|
||||
|
||||
def test_preview_scroll_begin_expr
|
||||
tmux.send_keys "echo foo 123 321 | #{FZF} --preview 'seq 1000' --preview-window left:+{3}", :Enter
|
||||
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||
tmux.until { |lines| assert_match %r{321.*321/1000}, lines[1] }
|
||||
end
|
||||
|
||||
def test_preview_scroll_begin_and_offset
|
||||
['echo foo 123 321', 'echo foo :123: 321'].each do |input|
|
||||
tmux.send_keys "#{input} | #{FZF} --preview 'seq 1000' --preview-window left:+{2}-2", :Enter
|
||||
tmux.until { |lines| assert_match %r{1/1}, lines[-2] }
|
||||
tmux.until { |lines| assert_match %r{121.*121/1000}, lines[1] }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_clear_screen
|
||||
tmux.send_keys %{seq 100 | #{FZF} --preview 'for i in $(seq 300); do (( i % 200 == 0 )) && printf "\\033[2J"; echo "[$i]"; sleep 0.001; done'}, :Enter
|
||||
tmux.until { |lines| lines.match_count == 100 }
|
||||
tmux.until { |lines| lines[1]&.include?('[200]') }
|
||||
end
|
||||
|
||||
def test_preview_window_follow
|
||||
file = Tempfile.new('fzf-follow')
|
||||
file.sync = true
|
||||
|
||||
tmux.send_keys %(seq 100 | #{FZF} --preview 'echo start; tail -f "#{file.path}"' --preview-window follow --bind 'up:preview-up,down:preview-down,space:change-preview-window:follow|nofollow' --preview-window '~4'), :Enter
|
||||
tmux.until { |lines| lines.match_count == 100 }
|
||||
|
||||
# Write to the temporary file, and check if the preview window is showing
|
||||
# the last line of the file
|
||||
tmux.until { |lines| assert_includes lines[1], 'start' }
|
||||
3.times { file.puts _1 } # header lines
|
||||
1000.times { file.puts _1 }
|
||||
tmux.until { |lines| assert_includes lines[1], '/1004' }
|
||||
tmux.until { |lines| assert_includes lines[-2], '999' }
|
||||
|
||||
# Scroll the preview window and fzf should stop following the file content
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[-2], '998' }
|
||||
file.puts 'foo', 'bar'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1006'
|
||||
assert_includes lines[-2], '998'
|
||||
end
|
||||
|
||||
# Scroll back to the bottom and fzf should start following the file again
|
||||
%w[999 foo bar].each do |item|
|
||||
wait do
|
||||
tmux.send_keys :Down
|
||||
tmux.until { |lines| assert_includes lines[-2], item }
|
||||
end
|
||||
end
|
||||
file.puts 'baz'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1007'
|
||||
assert_includes lines[-2], 'baz'
|
||||
end
|
||||
|
||||
# Scroll upwards to stop following
|
||||
tmux.send_keys :Up
|
||||
wait { assert_includes lines[-2], 'bar' }
|
||||
file.puts 'aaa'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1008'
|
||||
assert_includes lines[-2], 'bar'
|
||||
end
|
||||
|
||||
# Manually enable following
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines[-2], 'aaa' }
|
||||
file.puts 'bbb'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1009'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
|
||||
# Disable following
|
||||
tmux.send_keys :Space
|
||||
file.puts 'ccc', 'ddd'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '/1011'
|
||||
assert_includes lines[-2], 'bbb'
|
||||
end
|
||||
rescue StandardError
|
||||
file.close
|
||||
file.unlink
|
||||
end
|
||||
|
||||
def test_toggle_preview_wrap
|
||||
tmux.send_keys "#{FZF} --preview 'for i in $(seq $FZF_PREVIEW_COLUMNS); do echo -n .; done; echo wrapped; echo 2nd line' --bind ctrl-w:toggle-preview-wrap", :Enter
|
||||
2.times do
|
||||
tmux.until { |lines| assert_includes lines[2], '2nd line' }
|
||||
tmux.send_keys 'C-w'
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[2], 'wrapped'
|
||||
assert_includes lines[3], '2nd line'
|
||||
end
|
||||
tmux.send_keys 'C-w'
|
||||
end
|
||||
end
|
||||
|
||||
def test_close
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview 'echo foo' --bind ctrl-c:close", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[1], 'foo' }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.until { |lines| refute_includes lines[1], 'foo' }
|
||||
tmux.send_keys '10'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.send_keys 'C-c'
|
||||
tmux.send_keys 'C-l', 'closed'
|
||||
tmux.until { |lines| assert_includes lines[0], 'closed' }
|
||||
end
|
||||
|
||||
def test_preview_header
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind ctrl-k:preview-up+preview-up,ctrl-j:preview-down+preview-down+preview-down --preview 'seq 1000' --preview-window 'top:+{1}:~3'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
top5 = ->(lines) { lines.drop(1).take(5).map { |s| s[/[0-9]+/] } }
|
||||
tmux.until do |lines|
|
||||
assert_includes lines[1], '4/1000'
|
||||
assert_equal(%w[1 2 3 4 5], top5[lines])
|
||||
end
|
||||
tmux.send_keys '55'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_equal(%w[1 2 3 55 56], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-J'
|
||||
tmux.until do |lines|
|
||||
assert_equal(%w[1 2 3 58 59], top5[lines])
|
||||
end
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until do |lines|
|
||||
assert_equal 19, lines.match_count
|
||||
assert_equal(%w[1 2 3 5 6], top5[lines])
|
||||
end
|
||||
tmux.send_keys 'C-K'
|
||||
tmux.until { |lines| assert_equal(%w[1 2 3 4 5], top5[lines]) }
|
||||
end
|
||||
|
||||
def test_change_preview_window
|
||||
tmux.send_keys "seq 1000 | #{FZF} --preview 'echo [[{}]]' --no-preview-border --bind '" \
|
||||
'a:change-preview(echo __{}__),' \
|
||||
'b:change-preview-window(down)+change-preview(echo =={}==)+change-preview-window(up),' \
|
||||
'c:change-preview(),d:change-preview-window(hidden),' \
|
||||
"e:preview(printf ::%${FZF_PREVIEW_COLUMNS}s{})+change-preview-window(up),f:change-preview-window(up,wrap)'", :Enter
|
||||
tmux.until { |lines| assert_equal 1000, lines.match_count }
|
||||
tmux.until { |lines| assert_includes lines[0], '[[1]]' }
|
||||
|
||||
# change-preview action permanently changes the preview command set by --preview
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[0], '__1__' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines[0], '__2__' }
|
||||
|
||||
# When multiple change-preview-window actions are bound to a single key,
|
||||
# the last one wins and the updated options are immediately applied to the new preview
|
||||
tmux.send_keys 'b'
|
||||
tmux.until { |lines| assert_equal '==2==', lines[0] }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_equal '==3==', lines[0] }
|
||||
|
||||
# change-preview with an empty preview command closes the preview window
|
||||
tmux.send_keys 'c'
|
||||
tmux.until { |lines| refute_includes lines[0], '==' }
|
||||
|
||||
# change-preview again to re-open the preview window
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_equal '__3__', lines[0] }
|
||||
|
||||
# Hide the preview window with hidden flag
|
||||
tmux.send_keys 'd'
|
||||
tmux.until { |lines| refute_includes lines[0], '__3__' }
|
||||
|
||||
# One-off preview
|
||||
tmux.send_keys 'e'
|
||||
tmux.until do |lines|
|
||||
assert_equal '::', lines[0]
|
||||
refute_includes lines[1], '3'
|
||||
end
|
||||
|
||||
# Wrapped
|
||||
tmux.send_keys 'f'
|
||||
tmux.until do |lines|
|
||||
assert_equal '::', lines[0]
|
||||
assert_equal '↳ 3', lines[1]
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_preview_window_should_not_reset_change_preview
|
||||
tmux.send_keys "#{FZF} --preview-window up,border-none --bind 'start:change-preview(echo hello)' --bind 'enter:change-preview-window(border-left)'", :Enter
|
||||
tmux.until { |lines| assert_includes lines, 'hello' }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_includes lines, '│ hello' }
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview-window left,border-none --preview 'echo hello' --bind '" \
|
||||
"a:change-preview-window(right|down|up|hidden|)'", :Enter
|
||||
tmux.until { |lines| assert(lines.any? { _1.include?('100/100') }) }
|
||||
3.times do
|
||||
tmux.until { |lines| lines[0].start_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| lines[0].end_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| lines[-1].start_with?('hello') }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_equal 'hello', lines[0] }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[0], 'hello' }
|
||||
tmux.send_keys 'a'
|
||||
end
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate_hidden
|
||||
tmux.send_keys "seq 100 | #{FZF} --preview-window hidden --preview 'echo =={}==' --bind '" \
|
||||
"a:change-preview-window(nohidden||down,1|)'", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||
tmux.send_keys 'a'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
end
|
||||
|
||||
def test_change_preview_window_rotate_hidden_down
|
||||
tmux.send_keys "seq 100 | #{FZF} --bind '?:change-preview-window:up||down|' --preview 'echo =={}==' --preview-window hidden,down,1", :Enter
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[1], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[-2], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| refute_includes lines[-2], '==1==' }
|
||||
tmux.send_keys '?'
|
||||
tmux.until { |lines| assert_includes lines[1], '==1==' }
|
||||
end
|
||||
|
||||
def test_toggle_alternative_preview_window
|
||||
tmux.send_keys "seq 10 | #{FZF} --bind space:toggle-preview --preview-window '<100000(hidden,up,border-none)' --preview 'echo /{}/{}/'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.until { |lines| refute_includes lines, '/1/1/' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert_includes lines, '/1/1/' }
|
||||
end
|
||||
|
||||
def test_alternative_preview_window_opts
|
||||
tmux.send_keys "seq 10 | #{FZF} --preview-border rounded --preview-window '~5,2,+0,<100000(~0,+100,wrap,noinfo)' --preview 'seq 1000'", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
tmux.until do |lines|
|
||||
assert_equal ['╭────╮', '│ 10 │', '│ ↳ 0│', '│ 10 │', '│ ↳ 1│'], lines.take(5).map(&:strip)
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_width_exception
|
||||
tmux.send_keys "seq 10 | #{FZF} --scrollbar --preview-window border-left --border --preview 'seq 1000'", :Enter
|
||||
tmux.until do |lines|
|
||||
assert lines[1]&.end_with?(' 1/1000││')
|
||||
end
|
||||
end
|
||||
|
||||
def test_preview_window_hidden_on_focus
|
||||
tmux.send_keys "seq 3 | #{FZF} --preview 'echo {}' --bind focus:hide-preview", :Enter
|
||||
tmux.until { |lines| assert_includes lines, '> 1' }
|
||||
tmux.send_keys :Up
|
||||
tmux.until { |lines| assert_includes lines, '> 2' }
|
||||
end
|
||||
|
||||
def test_preview_query_should_not_be_affected_by_search
|
||||
tmux.send_keys "seq 1 | #{FZF} --bind 'change:transform-search(echo {q:1})' --preview 'echo [{q}/{}]'", :Enter
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| assert lines.any_include?('[1/1]') }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { |lines| assert lines.any_include?('[1 /1]') }
|
||||
tmux.send_keys '2'
|
||||
tmux.until do |lines|
|
||||
assert lines.any_include?('[1 2/1]')
|
||||
assert_equal 1, lines.match_count
|
||||
end
|
||||
end
|
||||
end
|
||||
113
test/test_raw.rb
Normal file
113
test/test_raw.rb
Normal file
@@ -0,0 +1,113 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Testing raw mode
|
||||
class TestRaw < TestInteractive
|
||||
def test_raw_mode
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind ctrl-x:toggle-raw,a:enable-raw,b:disable-raw --gutter '▌' --multi --bind 'space:transform-prompt:echo "[[$FZF_RAW]] "'), :Enter
|
||||
tmux.until { assert_equal 1000, it.match_count }
|
||||
|
||||
tmux.send_keys 1
|
||||
tmux.until { assert_equal 272, it.match_count }
|
||||
|
||||
tmux.send_keys :Up
|
||||
tmux.until { assert_includes it, '> 2' }
|
||||
|
||||
tmux.send_keys 'C-p'
|
||||
tmux.until do
|
||||
assert_includes it, '> 10'
|
||||
assert_includes it, '▖ 9'
|
||||
end
|
||||
|
||||
tmux.send_keys 'C-x'
|
||||
tmux.until do
|
||||
assert_includes it, '> 10'
|
||||
assert_includes it, '▌ 1'
|
||||
end
|
||||
|
||||
tmux.send_keys :Up, 'C-x'
|
||||
tmux.until do
|
||||
assert_includes it, '> 11'
|
||||
assert_includes it, '▖ 10'
|
||||
end
|
||||
|
||||
tmux.send_keys 1
|
||||
tmux.until { assert_equal 28, it.match_count }
|
||||
|
||||
tmux.send_keys 'C-p'
|
||||
tmux.until do
|
||||
assert_includes it, '> 101'
|
||||
assert_includes it, '▖ 100'
|
||||
end
|
||||
|
||||
tmux.send_keys 'C-n'
|
||||
tmux.until do
|
||||
assert_includes it, '> 11'
|
||||
assert_includes it, '▖ 10'
|
||||
end
|
||||
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { assert_equal 3, it.select_count }
|
||||
|
||||
tmux.send_keys 'C-x'
|
||||
tmux.until do
|
||||
assert_equal 1, it.select_count
|
||||
assert_includes it, '▌ 110'
|
||||
assert_includes it, '>>11'
|
||||
end
|
||||
|
||||
tmux.send_keys 'a'
|
||||
tmux.until do
|
||||
assert_equal 1, it.select_count
|
||||
assert_includes it, '>>11'
|
||||
assert_includes it, '▖ 10'
|
||||
end
|
||||
|
||||
tmux.send_keys :Down, :Space
|
||||
tmux.until { assert_includes it, '[[0]] 11' }
|
||||
|
||||
tmux.send_keys :Up, :Space
|
||||
tmux.until { assert_includes it, '[[1]] 11' }
|
||||
|
||||
tmux.send_keys 'b'
|
||||
tmux.until do
|
||||
assert_equal 1, it.select_count
|
||||
assert_includes it, '▌ 110'
|
||||
assert_includes it, '>>11'
|
||||
end
|
||||
|
||||
tmux.send_keys :Space
|
||||
tmux.until { assert_includes it, '[[]] 11' }
|
||||
|
||||
tmux.send_keys 'C-u', '5'
|
||||
tmux.until { assert_includes it, '> 5' }
|
||||
|
||||
tmux.send_keys 'C-x', 'C-p', 'C-p'
|
||||
tmux.until do
|
||||
assert_includes it, '> 25'
|
||||
assert_includes it, '▖ 24'
|
||||
end
|
||||
|
||||
tmux.send_keys 'C-x'
|
||||
tmux.until do
|
||||
assert_includes it, '> 25'
|
||||
assert_includes it, '▌ 15'
|
||||
end
|
||||
|
||||
# 35 is the closest match in raw mode
|
||||
tmux.send_keys 'C-x', :Up, :Up, :Up, :Up, :Up, :Up, 'C-x'
|
||||
tmux.until do
|
||||
assert_includes it, '> 35'
|
||||
assert_includes it, '▌ 25'
|
||||
end
|
||||
end
|
||||
|
||||
def test_raw_best
|
||||
tmux.send_keys %(seq 1000 | #{FZF} --raw --bind space:best), :Enter
|
||||
tmux.send_keys 999
|
||||
tmux.until { assert_includes it, '> 1' }
|
||||
tmux.send_keys :Space
|
||||
tmux.until { assert_includes it, '> 999' }
|
||||
end
|
||||
end
|
||||
52
test/test_server.rb
Normal file
52
test/test_server.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Test cases for API server
|
||||
class TestServer < TestInteractive
|
||||
def test_listen
|
||||
{ '--listen 6266' => -> { URI('http://localhost:6266') },
|
||||
"--listen --sync --bind 'start:execute-silent:echo $FZF_PORT > /tmp/fzf-port'" =>
|
||||
-> { URI("http://localhost:#{File.read('/tmp/fzf-port').chomp}") } }.each do |opts, fn|
|
||||
tmux.send_keys "seq 10 | fzf #{opts}", :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||
assert_equal 10, state[:totalCount]
|
||||
assert_equal 10, state[:matchCount]
|
||||
assert_empty state[:query]
|
||||
assert_equal({ index: 0, text: '1' }, state[:current])
|
||||
|
||||
Net::HTTP.post(fn.call, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ')
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
state = JSON.parse(Net::HTTP.get(fn.call), symbolize_names: true)
|
||||
assert_equal 100, state[:totalCount]
|
||||
assert_equal 0, state[:matchCount]
|
||||
assert_equal 'yo', state[:query]
|
||||
assert_nil state[:current]
|
||||
|
||||
teardown
|
||||
setup
|
||||
end
|
||||
end
|
||||
|
||||
def test_listen_with_api_key
|
||||
post_uri = URI('http://localhost:6266')
|
||||
tmux.send_keys 'seq 10 | FZF_API_KEY=123abc fzf --listen 6266', :Enter
|
||||
tmux.until { |lines| assert_equal 10, lines.match_count }
|
||||
# Incorrect API Key
|
||||
[nil, { 'x-api-key' => '' }, { 'x-api-key' => '124abc' }].each do |headers|
|
||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||
assert_equal '401', res.code
|
||||
assert_equal 'Unauthorized', res.message
|
||||
assert_equal "invalid api key\n", res.body
|
||||
end
|
||||
# Valid API Key
|
||||
[{ 'x-api-key' => '123abc' }, { 'X-API-Key' => '123abc' }].each do |headers|
|
||||
res = Net::HTTP.post(post_uri, 'change-query(yo)+reload(seq 100)+change-prompt:hundred> ', headers)
|
||||
assert_equal '200', res.code
|
||||
tmux.until { |lines| assert_equal 100, lines.item_count }
|
||||
tmux.until { |lines| assert_equal 'hundred> yo', lines[-1] }
|
||||
end
|
||||
end
|
||||
end
|
||||
517
test/test_shell_integration.rb
Normal file
517
test/test_shell_integration.rb
Normal file
@@ -0,0 +1,517 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'lib/common'
|
||||
|
||||
# Testing shell integration
|
||||
module TestShell
|
||||
attr_reader :tmux
|
||||
|
||||
def setup
|
||||
@tmux = Tmux.new(shell)
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def teardown
|
||||
@tmux.kill
|
||||
end
|
||||
|
||||
def set_var(name, val)
|
||||
tmux.prepare
|
||||
tmux.send_keys "export #{name}='#{val}'", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def unset_var(name)
|
||||
tmux.prepare
|
||||
tmux.send_keys "unset #{name}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_ctrl_t
|
||||
set_var('FZF_CTRL_T_COMMAND', 'seq 100')
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-t'
|
||||
tmux.until { |lines| assert_equal 100, lines.match_count }
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| assert lines.any_include?(' (3)') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('1 2 3') }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
|
||||
def test_ctrl_t_unicode
|
||||
writelines(['fzf-unicode 테스트1', 'fzf-unicode 테스트2'])
|
||||
set_var('FZF_CTRL_T_COMMAND', "cat #{tempname}")
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo ', 'C-t'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.send_keys 'fzf-unicode'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_match(/echo .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines.join) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'fzf-unicode 테스트1 fzf-unicode 테스트2', lines[-1] }
|
||||
end
|
||||
|
||||
def test_alt_c
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
lines = tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
expected = lines.reverse.find { |l| l.start_with?('> ') }[2..].chomp('/')
|
||||
tmux.send_keys :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.end_with?(expected) }
|
||||
end
|
||||
|
||||
def test_alt_c_command
|
||||
set_var('FZF_ALT_C_COMMAND', 'echo /tmp')
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /', :Enter
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys :Escape, :c
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys :pwd, :Enter
|
||||
tmux.until { |lines| assert_equal '/tmp', lines[-1] }
|
||||
end
|
||||
|
||||
def test_ctrl_r
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo 1st', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo 2nd', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo 3d', :Enter
|
||||
tmux.prepare
|
||||
3.times do
|
||||
tmux.send_keys 'echo 3rd', :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
tmux.send_keys 'echo 4th', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'e3d'
|
||||
# Duplicates removed: 3d (1) + 3rd (1) => 2 matches
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3d') }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert lines[-3]&.end_with?(' echo 3rd') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'echo 3rd', lines[-1] }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal '3rd', lines[-1] }
|
||||
end
|
||||
|
||||
def test_ctrl_r_multiline
|
||||
# NOTE: Current bash implementation shows an extra new line if there's
|
||||
# only entry in the history
|
||||
tmux.send_keys ':', :Enter
|
||||
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal '>', lines[-1] }
|
||||
tmux.send_keys 'foo bar'
|
||||
tmux.until { |lines| assert_includes lines[-4], '"foo' } unless shell == :zsh
|
||||
tmux.until { |lines| assert lines[-3]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines[-1]&.match?(/bar"␊?/) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[foo bar], lines[-2..] }
|
||||
end
|
||||
|
||||
def test_ctrl_r_abort
|
||||
skip("doesn't restore the original line when search is aborted pre Bash 4") if shell == :bash && `#{Shell.bash} --version`[/(?<= version )\d+/].to_i < 4
|
||||
%w[foo ' "].each do |query|
|
||||
tmux.prepare
|
||||
tmux.send_keys :Enter, query
|
||||
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
|
||||
tmux.send_keys 'C-r'
|
||||
tmux.until { |lines| assert_equal "> #{query}", lines[-1] }
|
||||
tmux.send_keys 'C-g'
|
||||
tmux.until { |lines| assert lines[-1]&.start_with?(query) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module CompletionTest
|
||||
def test_file_completion
|
||||
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||
FileUtils.mkdir_p('/tmp/fzf test')
|
||||
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/#{i}") }
|
||||
['no~such~user', '/tmp/fzf test/foobar'].each do |f|
|
||||
FileUtils.touch(File.expand_path(f))
|
||||
end
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cat /tmp/fzf-test/10**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys ' !d'
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
tmux.send_keys :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) do |lines|
|
||||
assert_equal 'cat /tmp/fzf-test/10 /tmp/fzf-test/100', lines[-1]
|
||||
end
|
||||
|
||||
# ~USERNAME**<TAB>
|
||||
user = `whoami`.chomp
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys "cat ~#{user}**", :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys "/#{user}"
|
||||
tmux.until { |lines| assert(lines.any? { |l| l.end_with?("/#{user}") }) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) do |lines|
|
||||
assert_match %r{cat .*/#{user}}, lines[-1]
|
||||
end
|
||||
|
||||
# ~INVALID_USERNAME**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat ~such**', :Tab
|
||||
tmux.until(true) { |lines| assert lines.any_include?('no~such~user') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cat no~such~user', lines[-1] }
|
||||
|
||||
# /tmp/fzf\ test**<TAB>
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf\ test/**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'foobar$'
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert lines.any_include?('> /tmp/fzf test/foobar')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cat /tmp/fzf\ test/foobar', lines[-1] }
|
||||
|
||||
# Should include hidden files
|
||||
(1..100).each { |i| FileUtils.touch("/tmp/fzf-test/.hidden-#{i}") }
|
||||
tmux.send_keys 'C-u'
|
||||
tmux.send_keys 'cat /tmp/fzf-test/hidden**', :Tab
|
||||
tmux.until(true) do |lines|
|
||||
assert_equal 100, lines.match_count
|
||||
assert lines.any_include?('/tmp/fzf-test/.hidden-')
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
ensure
|
||||
['/tmp/fzf-test', '/tmp/fzf test', '~/.fzf-home', 'no~such~user'].each do |f|
|
||||
FileUtils.rm_rf(File.expand_path(f))
|
||||
end
|
||||
end
|
||||
|
||||
def test_file_completion_root
|
||||
tmux.send_keys 'ls /**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys :Enter
|
||||
end
|
||||
|
||||
def test_dir_completion
|
||||
(1..100).each do |idx|
|
||||
FileUtils.mkdir_p("/tmp/fzf-test/d#{idx}")
|
||||
end
|
||||
FileUtils.touch('/tmp/fzf-test/d55/xxx')
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cd /tmp/fzf-test/**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys :Tab, :Tab # Tab does not work here
|
||||
tmux.send_keys 55
|
||||
tmux.until do |lines|
|
||||
assert_equal 1, lines.match_count
|
||||
assert_includes lines, '> 55'
|
||||
assert_includes lines, '> /tmp/fzf-test/d55/'
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
||||
tmux.send_keys :xx
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
||||
|
||||
# Should not match regular files (bash-only)
|
||||
if instance_of?(TestBash)
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/xx', lines[-1] }
|
||||
end
|
||||
|
||||
# Fail back to plusdirs
|
||||
tmux.send_keys :BSpace, :BSpace, :BSpace
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55', lines[-1] }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 'cd /tmp/fzf-test/d55/', lines[-1] }
|
||||
end
|
||||
|
||||
def test_process_completion
|
||||
tmux.send_keys 'sleep 12345 &', :Enter
|
||||
lines = tmux.until { |lines| assert lines[-1]&.start_with?('[1] ') }
|
||||
pid = lines[-1]&.split&.last
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-L'
|
||||
tmux.send_keys 'kill **', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'sleep12345'
|
||||
tmux.until { |lines| assert lines.any_include?('sleep 12345') }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal "kill #{pid}", lines[-1] }
|
||||
ensure
|
||||
if pid
|
||||
begin
|
||||
Process.kill('KILL', pid.to_i)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_completion
|
||||
tmux.send_keys '_fzf_compgen_path() { echo "$1"; seq 10; }', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'ls /tmp/**', :Tab
|
||||
tmux.until { |lines| assert_equal 11, lines.match_count }
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until { |lines| assert_equal 3, lines.select_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_equal 'ls /tmp 1 2', lines[-1] }
|
||||
end
|
||||
|
||||
def test_unset_completion
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
tmux.prepare
|
||||
|
||||
# Using tmux
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||
tmux.send_keys 'C-c'
|
||||
|
||||
# FZF_TMUX=1
|
||||
new_shell
|
||||
tmux.focus
|
||||
tmux.send_keys 'unset FZFFOOBR**', :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'unset FZFFOOBAR', lines[-1] }
|
||||
end
|
||||
|
||||
def test_completion_in_command_sequence
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
tmux.prepare
|
||||
|
||||
triggers = ['**', '~~', '++', 'ff', '/']
|
||||
triggers.push('&', '[', ';', '`') if instance_of?(TestZsh)
|
||||
|
||||
triggers.each do |trigger|
|
||||
set_var('FZF_COMPLETION_TRIGGER', trigger)
|
||||
command = "echo foo; QUX=THUD unset FZFFOOBR#{trigger}"
|
||||
tmux.send_keys command.sub(/(;|`)$/, '\\\\\1'), :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal 'echo foo; QUX=THUD unset FZFFOOBAR', lines[-1] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_file_completion_unicode
|
||||
FileUtils.mkdir_p('/tmp/fzf-test')
|
||||
tmux.paste "cd /tmp/fzf-test; echo test3 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2701'; echo test4 > $'fzf-unicode \\355\\205\\214\\354\\212\\244\\355\\212\\2702'"
|
||||
tmux.prepare
|
||||
tmux.send_keys 'cat fzf-unicode**', :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '1'
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
|
||||
tmux.send_keys :BSpace
|
||||
tmux.until { |lines| assert_equal 2, lines.match_count }
|
||||
|
||||
tmux.send_keys '2'
|
||||
tmux.until { |lines| assert_equal 1, lines.select_count }
|
||||
tmux.send_keys :Tab
|
||||
tmux.until { |lines| assert_equal 2, lines.select_count }
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until(true) { |lines| assert_match(/cat .*fzf-unicode.*1.* .*fzf-unicode.*2/, lines[-1]) }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal %w[test3 test4], lines[-2..] }
|
||||
end
|
||||
|
||||
def test_custom_completion_api
|
||||
tmux.send_keys 'eval "_fzf$(declare -f _comprun)"', :Enter
|
||||
%w[f g].each do |command|
|
||||
tmux.prepare
|
||||
tmux.send_keys "#{command} b**", :Tab
|
||||
tmux.until do |lines|
|
||||
assert_equal 2, lines.item_count
|
||||
assert_equal 1, lines.match_count
|
||||
assert lines.any_include?("prompt-#{command}")
|
||||
assert lines.any_include?("preview-#{command}-bar")
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal "#{command} #{command}barbar", lines[-1] }
|
||||
tmux.send_keys 'C-u'
|
||||
end
|
||||
ensure
|
||||
tmux.prepare
|
||||
tmux.send_keys 'unset -f _fzf_comprun', :Enter
|
||||
end
|
||||
|
||||
def test_ssh_completion
|
||||
(1..5).each { |i| FileUtils.touch("/tmp/fzf-test-ssh-#{i}") }
|
||||
|
||||
tmux.send_keys 'ssh jg@localhost**', :Tab
|
||||
tmux.until do |lines|
|
||||
assert_operator lines.match_count, :>=, 1
|
||||
end
|
||||
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost') }
|
||||
tmux.send_keys ' -i /tmp/fzf-test-ssh**', :Tab
|
||||
tmux.until do |lines|
|
||||
assert_operator lines.match_count, :>=, 5
|
||||
assert_equal 0, lines.select_count
|
||||
end
|
||||
tmux.send_keys :Tab, :Tab, :Tab
|
||||
tmux.until do |lines|
|
||||
assert_equal 3, lines.select_count
|
||||
end
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert lines.any_include?('ssh jg@localhost -i /tmp/fzf-test-ssh-') }
|
||||
|
||||
tmux.send_keys 'localhost**', :Tab
|
||||
tmux.until do |lines|
|
||||
assert_operator lines.match_count, :>=, 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestBash < TestBase
|
||||
include TestShell
|
||||
include CompletionTest
|
||||
|
||||
def shell
|
||||
:bash
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.prepare
|
||||
tmux.send_keys "FZF_TMUX=1 #{Shell.bash}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_dynamic_completion_loader
|
||||
tmux.paste 'touch /tmp/foo; _fzf_completion_loader=1'
|
||||
tmux.paste '_completion_loader() { complete -o default fake; }'
|
||||
tmux.paste 'complete -F _fzf_path_completion -o default -o bashdefault fake'
|
||||
tmux.send_keys 'fake /tmp/foo**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
tmux.send_keys 'C-c'
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'fake /tmp/foo'
|
||||
tmux.send_keys :Tab, 'C-u'
|
||||
|
||||
tmux.prepare
|
||||
tmux.send_keys 'fake /tmp/foo**', :Tab
|
||||
tmux.until { |lines| assert_operator lines.match_count, :>, 0 }
|
||||
end
|
||||
end
|
||||
|
||||
class TestZsh < TestBase
|
||||
include TestShell
|
||||
include CompletionTest
|
||||
|
||||
def shell
|
||||
:zsh
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys "FZF_TMUX=1 #{Shell.zsh}", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_complete_quoted_command
|
||||
tmux.send_keys 'export FZFFOOBAR=BAZ', :Enter
|
||||
['unset', '\unset', "'unset'"].each do |command|
|
||||
tmux.prepare
|
||||
tmux.send_keys "#{command} FZFFOOBR**", :Tab
|
||||
tmux.until { |lines| assert_equal 1, lines.match_count }
|
||||
tmux.send_keys :Enter
|
||||
tmux.until { |lines| assert_equal "#{command} FZFFOOBAR", lines[-1] }
|
||||
tmux.send_keys 'C-c'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class TestFish < TestBase
|
||||
include TestShell
|
||||
|
||||
def shell
|
||||
:fish
|
||||
end
|
||||
|
||||
def new_shell
|
||||
tmux.send_keys 'env FZF_TMUX=1 FZF_DEFAULT_OPTS=--no-scrollbar fish', :Enter
|
||||
tmux.send_keys 'function fish_prompt; end; clear', :Enter
|
||||
tmux.until { |lines| assert_empty lines }
|
||||
end
|
||||
|
||||
def set_var(name, val)
|
||||
tmux.prepare
|
||||
tmux.send_keys "set -g #{name} '#{val}'", :Enter
|
||||
tmux.prepare
|
||||
end
|
||||
|
||||
def test_ctrl_r_multi
|
||||
tmux.send_keys ':', :Enter
|
||||
tmux.send_keys 'echo "foo', :Enter, 'bar"', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'echo "bar', :Enter, 'foo"', :Enter
|
||||
tmux.prepare
|
||||
tmux.send_keys 'C-l', 'C-r'
|
||||
block = <<~BLOCK
|
||||
echo "foo
|
||||
bar"
|
||||
echo "bar
|
||||
foo"
|
||||
BLOCK
|
||||
tmux.until do |lines|
|
||||
block.lines.each_with_index do |line, idx|
|
||||
assert_includes lines[-6 + idx], line.chomp
|
||||
end
|
||||
end
|
||||
tmux.send_keys :BTab, :BTab
|
||||
tmux.until { |lines| assert_includes lines[-2], '(2)' }
|
||||
tmux.send_keys :Enter
|
||||
block = <<~BLOCK
|
||||
echo "bar
|
||||
foo"
|
||||
echo "foo
|
||||
bar"
|
||||
BLOCK
|
||||
tmux.until do |lines|
|
||||
assert_equal block.lines.map(&:chomp), lines
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -13,7 +13,7 @@ Execute (fzf#run with dir option):
|
||||
|
||||
execute 'lcd' fnameescape(cwd)
|
||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||
AssertEqual ['fzf.vader'], result
|
||||
AssertEqual 1, haslocaldir()
|
||||
AssertEqual getcwd(), cwd
|
||||
|
||||
@@ -23,8 +23,8 @@ Execute (fzf#run with Funcref command):
|
||||
call add(g:ret, a:e)
|
||||
endfunction
|
||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
||||
AssertEqual ['fzf.vader'], result
|
||||
AssertEqual ['fzf.vader'], sort(g:ret)
|
||||
|
||||
Execute (fzf#run with string source):
|
||||
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
||||
@@ -78,18 +78,18 @@ Execute (fzf#wrap):
|
||||
|
||||
let opts = fzf#wrap('foobar')
|
||||
Log opts
|
||||
AssertEqual '~40%', opts.down
|
||||
AssertEqual 0.9, opts.window.width
|
||||
Assert opts.options =~ '--expect='
|
||||
Assert !has_key(opts, 'sink')
|
||||
Assert has_key(opts, 'sink*')
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 0)
|
||||
Log opts
|
||||
AssertEqual '~40%', opts.down
|
||||
AssertEqual 0.9, opts.window.width
|
||||
|
||||
let opts = fzf#wrap('foobar', {}, 1)
|
||||
Log opts
|
||||
Assert !has_key(opts, 'down')
|
||||
Assert !has_key(opts, 'window')
|
||||
|
||||
let opts = fzf#wrap('foobar', {'down': '50%'})
|
||||
Log opts
|
||||
@@ -148,7 +148,7 @@ Execute (fzf#wrap):
|
||||
|
||||
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
||||
let opts = fzf#wrap({})
|
||||
Assert opts.options =~ '^--color=fg:'
|
||||
Assert opts.options =~ '--color=fg:'
|
||||
|
||||
Execute (fzf#shellescape with sh):
|
||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
||||
@@ -64,7 +64,7 @@ remove_line() {
|
||||
line_no=1
|
||||
continue
|
||||
fi
|
||||
line_no=$(( $(sed 's/:.*//' <<< "$line") + line_no - 1 ))
|
||||
line_no=$(($(sed 's/:.*//' <<< "$line") + line_no - 1))
|
||||
content=$(sed 's/^[0-9]*://' <<< "$line")
|
||||
match=1
|
||||
echo " - Line #$line_no: $content"
|
||||
@@ -76,7 +76,7 @@ remove_line() {
|
||||
echo " - Removed"
|
||||
else
|
||||
echo " - Skipped"
|
||||
line_no=$(( line_no + 1 ))
|
||||
line_no=$((line_no + 1))
|
||||
fi
|
||||
done
|
||||
[ $match -eq 0 ] && echo " - Nothing found"
|
||||
@@ -109,6 +109,6 @@ if [ -d "${fish_dir}/functions" ]; then
|
||||
fi
|
||||
|
||||
config_dir=$(dirname "$prefix_expand")
|
||||
if [[ "$xdg" = 1 ]] && [[ "$config_dir" = */fzf ]] && [[ -d "$config_dir" ]]; then
|
||||
if [[ $xdg == 1 ]] && [[ $config_dir == */fzf ]] && [[ -d $config_dir ]]; then
|
||||
rmdir "$config_dir"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user