mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-11 12:53:48 -05:00
Compare commits
526 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
65db7352b7 | ||
|
|
a4db8bd7b5 | ||
|
|
f1c1b02d77 | ||
|
|
6580f32b43 | ||
|
|
b028cbd8bd | ||
|
|
a1a5418318 | ||
|
|
5a32634b74 | ||
|
|
c1875af70b | ||
|
|
0a10d14e19 | ||
|
|
ed8ceec66f | ||
|
|
03760011d7 | ||
|
|
0d5aebb806 | ||
|
|
1313510890 | ||
|
|
b712f2bb6a | ||
|
|
938c15ec63 | ||
|
|
3e7f032ec2 | ||
|
|
b42f5bfb19 | ||
|
|
717562b264 | ||
|
|
9d6637c1b3 | ||
|
|
56fef7c8df | ||
|
|
ba0935c71f | ||
|
|
d83eb2800a | ||
|
|
6f943112a9 | ||
|
|
f422893b8e | ||
|
|
22b498489c | ||
|
|
5460517bd2 | ||
|
|
9a6e557e52 | ||
|
|
4fdc07927f | ||
|
|
9030b67e4f | ||
|
|
43eafdf4b7 | ||
|
|
dfb88edb5e | ||
|
|
bd3e65df4d | ||
|
|
d7b13f3408 | ||
|
|
14ef8e8051 | ||
|
|
cc1d9f124e | ||
|
|
93c0299606 | ||
|
|
55e3c73221 | ||
|
|
6783417504 | ||
|
|
fa3f706e71 | ||
|
|
9c2f6cae88 | ||
|
|
a30181e240 | ||
|
|
b4ccf64e62 | ||
|
|
88d768bf6b | ||
|
|
6444cc7905 | ||
|
|
328af1f397 | ||
|
|
5ae60e2e80 | ||
|
|
0e0b868342 | ||
|
|
a5beb08ed7 | ||
|
|
45fc7b903d | ||
|
|
4f2c274942 | ||
|
|
93415493b4 | ||
|
|
8e4d338de9 | ||
|
|
8a71e091a8 | ||
|
|
120cd7f25a | ||
|
|
fb3bf6c984 | ||
|
|
d57e1f8baa | ||
|
|
15ca9ad8eb | ||
|
|
c2e1861747 | ||
|
|
543d41f3dd | ||
|
|
e5cfc988ec | ||
|
|
ee3916be17 | ||
|
|
fd513f8af8 | ||
|
|
9a2b7f559c | ||
|
|
b8d2b0df7e | ||
|
|
fe3a9c603e | ||
|
|
97030d4cb1 | ||
|
|
b2c3e567da | ||
|
|
ca5e633399 | ||
|
|
e60a9a628b | ||
|
|
0167691941 | ||
|
|
3b0f976380 | ||
|
|
7bd298b536 | ||
|
|
0476a65fca | ||
|
|
2cb2af115a | ||
|
|
789226ff6d | ||
|
|
805efc5bf1 | ||
|
|
cdcab26766 | ||
|
|
ec3acb1932 | ||
|
|
d30e37434e | ||
|
|
20d5b2e20e | ||
|
|
6c6be4ab1a | ||
|
|
d004eb1f7c | ||
|
|
3148b0f3e8 | ||
|
|
3fc0bd26a5 | ||
|
|
6c9025ff17 | ||
|
|
289997e373 | ||
|
|
db44cbdff0 | ||
|
|
da9179335c | ||
|
|
cdf641fa3e | ||
|
|
66dbee10f5 | ||
|
|
19e9b620ba | ||
|
|
e4e4700aff | ||
|
|
bb55045596 | ||
|
|
d7e51cdeb5 | ||
|
|
7f4964b366 | ||
|
|
a6957aba11 | ||
|
|
b5f94f961d | ||
|
|
e182d3db7a | ||
|
|
3e6e0528a6 | ||
|
|
ac508a1ce4 | ||
|
|
d7fc1e09b1 | ||
|
|
3b0c86e401 | ||
|
|
61d10d8ffa | ||
|
|
7d9548919e | ||
|
|
bee80a730f | ||
|
|
ac3e24c99c | ||
|
|
e7e852bdb3 | ||
|
|
2b7f168571 | ||
|
|
5b3da1d878 | ||
|
|
99f1bc0177 | ||
|
|
ed76f076dd | ||
|
|
4d357d1063 | ||
|
|
961ae1541c | ||
|
|
add1aec685 | ||
|
|
03d6ba7496 | ||
|
|
71e4d5cc51 | ||
|
|
215ab48222 | ||
|
|
0c64c68781 | ||
|
|
3ec035c68b | ||
|
|
20c7dcfbca | ||
|
|
c1b8780b9c | ||
|
|
64c61603e9 | ||
|
|
57c08d925f | ||
|
|
51623a5f6a | ||
|
|
ca3f6181d7 | ||
|
|
9c94f9c3d0 | ||
|
|
4a85843bcf | ||
|
|
d4d9b99879 | ||
|
|
6816b7d95b | ||
|
|
acdf265d7a | ||
|
|
19495eb9bb | ||
|
|
bacc8609ee | ||
|
|
99163f5afa | ||
|
|
0607227730 | ||
|
|
d938fdc496 | ||
|
|
dcb4c3d84a | ||
|
|
82ebcd9209 | ||
|
|
ff1687744d | ||
|
|
782c870fb2 | ||
|
|
71fad63829 | ||
|
|
d65c6101a8 | ||
|
|
3c40b1bd51 | ||
|
|
90a8800bb5 | ||
|
|
97f1dae2d1 | ||
|
|
e54ec05709 | ||
|
|
a24eb99679 | ||
|
|
ad113d00b7 | ||
|
|
7bd5884d12 | ||
|
|
c3505858a6 | ||
|
|
e76aa37fd4 | ||
|
|
1a32220ca9 | ||
|
|
4161403a1d | ||
|
|
53bcdc4294 | ||
|
|
30a8ef28cd | ||
|
|
855f90727a | ||
|
|
2191a44e36 | ||
|
|
952276dc2d | ||
|
|
2286edb329 | ||
|
|
a0f28583e7 | ||
|
|
8af0af3400 | ||
|
|
769e5cbb2d | ||
|
|
fc69308057 | ||
|
|
c6d620c99e | ||
|
|
f510a4def6 | ||
|
|
4ae3069c6f | ||
|
|
c0f27751d3 | ||
|
|
efbcd5a683 | ||
|
|
6a67712944 | ||
|
|
e8a690928d | ||
|
|
0eee95af57 | ||
|
|
a09c6e991a | ||
|
|
d06c5ab990 | ||
|
|
e0924d27b8 | ||
|
|
2775b771f2 | ||
|
|
cf7a3c6a0e | ||
|
|
230cc6acc3 | ||
|
|
626a23a585 | ||
|
|
74f196eebb | ||
|
|
cf2242aea3 | ||
|
|
8cb59e6fca | ||
|
|
5cce17e80a | ||
|
|
ee5302fb2d | ||
|
|
387c6ef664 | ||
|
|
581734c369 | ||
|
|
d90a969c00 | ||
|
|
2c8a96bb27 | ||
|
|
5da168db30 | ||
|
|
e215e2daf3 | ||
|
|
e28f5aa45b | ||
|
|
a2d0e8f233 | ||
|
|
303d04106a | ||
|
|
c423c496a1 | ||
|
|
4e85f72f0e | ||
|
|
dd0737aac0 | ||
|
|
f90985845d | ||
|
|
af4917dbb6 | ||
|
|
d9404fcce4 | ||
|
|
5c01fee5a9 | ||
|
|
9b27d68a37 | ||
|
|
b99d884e57 | ||
|
|
587df594b8 | ||
|
|
b896e0d314 | ||
|
|
559fb7ee45 | ||
|
|
62545cd983 | ||
|
|
c50812518e | ||
|
|
4cc5609d8b | ||
|
|
50fa90dfb8 | ||
|
|
a2c365e710 | ||
|
|
b4ddffdc61 | ||
|
|
8d4d184fc6 | ||
|
|
ea23539b59 | ||
|
|
9e92b6f11e | ||
|
|
6cbde812f6 | ||
|
|
3b2e932c13 | ||
|
|
8ff4e52641 | ||
|
|
2dbc874e3d | ||
|
|
039a2f1d04 | ||
|
|
4ef1cf5b35 | ||
|
|
b44ab9e33c | ||
|
|
8f4c23f1c4 | ||
|
|
23a391e715 | ||
|
|
035b0be29f | ||
|
|
e1fcdbc337 | ||
|
|
cfc149e994 | ||
|
|
2faffbd1b7 | ||
|
|
8db65704b9 | ||
|
|
797a01aed4 | ||
|
|
bf515a3d32 | ||
|
|
a06745826a | ||
|
|
0420ed4f2a | ||
|
|
3b944addd4 | ||
|
|
70bf8bc35d | ||
|
|
724f8a1d45 | ||
|
|
cc2b2146ee | ||
|
|
8689f5f230 | ||
|
|
e9e0011f1d | ||
|
|
5b52833785 | ||
|
|
1525768094 | ||
|
|
a70ea4654e | ||
|
|
b02bf9b6bb | ||
|
|
bee7bc5324 | ||
|
|
7c2ffd3fef | ||
|
|
db01e7dab6 | ||
|
|
2326c74eb2 | ||
|
|
b9d15569e8 | ||
|
|
c3cc378d89 | ||
|
|
27d1f5e0a8 | ||
|
|
540632bb9e | ||
|
|
d9c028c934 | ||
|
|
c54ad82e8d | ||
|
|
295b89631b | ||
|
|
6179faf778 | ||
|
|
16dc236a25 | ||
|
|
61ae9d75b6 | ||
|
|
e2401aca68 | ||
|
|
59943cbb48 | ||
|
|
02634d404d | ||
|
|
ed12925f7d | ||
|
|
e0ddb97ab4 | ||
|
|
b8c01af0fc | ||
|
|
6de0a7ddc1 | ||
|
|
79196c025d | ||
|
|
94c33ac020 | ||
|
|
b2ecb6352c | ||
|
|
9dc3ed638a | ||
|
|
0acace1ace | ||
|
|
1a2d37e1e6 | ||
|
|
22adb6494f | ||
|
|
e023736c30 | ||
|
|
dca2262fe6 | ||
|
|
0684a20ea3 | ||
|
|
a1a72bb8d1 | ||
|
|
144d55a5be | ||
|
|
7fc13c5cfd | ||
|
|
dfee7af57b | ||
|
|
9b0e2daf02 | ||
|
|
590060a16b | ||
|
|
368294edf6 | ||
|
|
c4a9ccd6af | ||
|
|
cbf91f2ed3 | ||
|
|
b1460d4787 | ||
|
|
7dc9e14874 | ||
|
|
1616ed543d | ||
|
|
dc73fba188 | ||
|
|
ef148dfd37 | ||
|
|
93bbb3032d | ||
|
|
4c83d8596d | ||
|
|
d453e6d7db | ||
|
|
c29533994f | ||
|
|
1afe13b5b5 | ||
|
|
36600eaaa9 | ||
|
|
3ee1fc2034 | ||
|
|
e2f93e5a2d | ||
|
|
cfdf2f1153 | ||
|
|
e042143e3f | ||
|
|
7c613d0d9b | ||
|
|
b00d46bc14 | ||
|
|
555b0d235b | ||
|
|
564daf9a7d | ||
|
|
41bcbe342f | ||
|
|
dbe8dc344e | ||
|
|
e33fb59da1 | ||
|
|
7aa88aa115 | ||
|
|
2b6d600879 | ||
|
|
05c765d442 | ||
|
|
49b496269c | ||
|
|
7405925952 | ||
|
|
3afd543a7e | ||
|
|
b4f2cde5ac | ||
|
|
ed53ef7cee | ||
|
|
12630b124d | ||
|
|
1d59ac09d2 | ||
|
|
a8f3a0dd59 | ||
|
|
124cd70710 | ||
|
|
782de139c8 | ||
|
|
32eb32ee5e | ||
|
|
2f51eb2b41 | ||
|
|
0ccbd79e10 | ||
|
|
99bd6de541 | ||
|
|
1fef36e4bc | ||
|
|
89375005b5 | ||
|
|
88e78c9193 | ||
|
|
29a19ad080 | ||
|
|
2a039ab746 | ||
|
|
7e9a0fcdbd | ||
|
|
7a97532547 | ||
|
|
996abb2831 | ||
|
|
da500a358f | ||
|
|
c36b846acc | ||
|
|
d9b5c9b2be | ||
|
|
3dee8778d0 | ||
|
|
d4216b0dcc | ||
|
|
bfe2bf4dce | ||
|
|
561f9291fd | ||
|
|
b5b0d6b3ea | ||
|
|
a90426b7ca | ||
|
|
303c3bae7f | ||
|
|
6b4358f641 | ||
|
|
552158f3ad | ||
|
|
7205203dc8 | ||
|
|
0cadf70072 | ||
|
|
076b3d0a9a | ||
|
|
7b0c9e04d3 | ||
|
|
573df524fe | ||
|
|
aee417c46a | ||
|
|
04db44067d | ||
|
|
5b204c54f9 | ||
|
|
daa602422d | ||
|
|
04dfb14e32 | ||
|
|
c24256cba3 | ||
|
|
685fb71d89 | ||
|
|
83b6033906 | ||
|
|
01e7668915 | ||
|
|
0994d9c881 | ||
|
|
030428ba43 | ||
|
|
8a110e02b9 | ||
|
|
86d92c17c4 | ||
|
|
c4cc7891b4 | ||
|
|
218843b9f1 | ||
|
|
d274d093af |
13
.github/workflows/linux.yml
vendored
13
.github/workflows/linux.yml
vendored
@@ -16,7 +16,7 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
@@ -30,22 +30,19 @@ jobs:
|
|||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
with:
|
with:
|
||||||
ruby-version: 3.1.0
|
ruby-version: 3.4.1
|
||||||
|
|
||||||
- name: Install packages
|
- name: Install packages
|
||||||
run: sudo apt-get install --yes zsh fish tmux
|
run: sudo apt-get install --yes zsh fish tmux
|
||||||
|
|
||||||
- name: Install Ruby gems
|
- name: Install Ruby gems
|
||||||
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
|
run: bundle install
|
||||||
|
|
||||||
- name: Rubocop
|
- name: Rubocop
|
||||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
run: make lint
|
||||||
|
|
||||||
- name: Unit test
|
- name: Unit test
|
||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
- name: Integration 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
|
||||||
|
|
||||||
- name: Integration test (tcell)
|
|
||||||
run: TAGS=tcell make clean install && ruby test/test_go.rb --verbose
|
|
||||||
|
|||||||
2
.github/workflows/typos.yml
vendored
2
.github/workflows/typos.yml
vendored
@@ -7,4 +7,4 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: crate-ci/typos@v1.21.0
|
- uses: crate-ci/typos@v1.29.4
|
||||||
|
|||||||
1
.github/workflows/winget.yml
vendored
1
.github/workflows/winget.yml
vendored
@@ -10,6 +10,5 @@ jobs:
|
|||||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
with:
|
with:
|
||||||
identifier: junegunn.fzf
|
identifier: junegunn.fzf
|
||||||
version: ${{ github.event.release.tag_name }}
|
|
||||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||||
token: ${{ secrets.WINGET_TOKEN }}
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,6 @@ bin/fzf.exe
|
|||||||
dist
|
dist
|
||||||
target
|
target
|
||||||
pkg
|
pkg
|
||||||
Gemfile.lock
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
doc/tags
|
doc/tags
|
||||||
vendor
|
vendor
|
||||||
@@ -12,3 +11,4 @@ gopath
|
|||||||
fzf
|
fzf
|
||||||
tmp
|
tmp
|
||||||
*.patch
|
*.patch
|
||||||
|
.idea
|
||||||
|
|||||||
100
.goreleaser.yml
100
.goreleaser.yml
@@ -1,4 +1,5 @@
|
|||||||
---
|
---
|
||||||
|
version: 2
|
||||||
project_name: fzf
|
project_name: fzf
|
||||||
|
|
||||||
before:
|
before:
|
||||||
@@ -6,60 +7,9 @@ before:
|
|||||||
- go mod download
|
- go mod download
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: fzf-macos
|
|
||||||
binary: fzf
|
|
||||||
goos:
|
|
||||||
- darwin
|
|
||||||
goarch:
|
|
||||||
- amd64
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
|
||||||
hooks:
|
|
||||||
post: |
|
|
||||||
sh -c '
|
|
||||||
cat > /tmp/fzf-gon-amd64.hcl << EOF
|
|
||||||
source = ["./dist/fzf-macos_darwin_amd64_v1/fzf"]
|
|
||||||
bundle_id = "junegunn.fzf"
|
|
||||||
sign {
|
|
||||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
|
||||||
}
|
|
||||||
zip {
|
|
||||||
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
gon /tmp/fzf-gon-amd64.hcl
|
|
||||||
'
|
|
||||||
|
|
||||||
- id: fzf-macos-arm
|
|
||||||
binary: fzf
|
|
||||||
goos:
|
|
||||||
- darwin
|
|
||||||
goarch:
|
|
||||||
- arm64
|
|
||||||
flags:
|
|
||||||
- -trimpath
|
|
||||||
ldflags:
|
|
||||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
|
||||||
hooks:
|
|
||||||
post: |
|
|
||||||
sh -c '
|
|
||||||
cat > /tmp/fzf-gon-arm64.hcl << EOF
|
|
||||||
source = ["./dist/fzf-macos-arm_darwin_arm64/fzf"]
|
|
||||||
bundle_id = "junegunn.fzf"
|
|
||||||
sign {
|
|
||||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
|
||||||
}
|
|
||||||
zip {
|
|
||||||
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
gon /tmp/fzf-gon-arm64.hcl
|
|
||||||
'
|
|
||||||
|
|
||||||
- id: fzf
|
- id: fzf
|
||||||
goos:
|
goos:
|
||||||
|
- darwin
|
||||||
- linux
|
- linux
|
||||||
- windows
|
- windows
|
||||||
- freebsd
|
- freebsd
|
||||||
@@ -89,6 +39,42 @@ builds:
|
|||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm64
|
goarch: arm64
|
||||||
|
|
||||||
|
# .goreleaser.yaml
|
||||||
|
notarize:
|
||||||
|
macos:
|
||||||
|
- # Whether this configuration is enabled or not.
|
||||||
|
#
|
||||||
|
# Default: false.
|
||||||
|
# Templates: allowed.
|
||||||
|
enabled: "{{ not .IsSnapshot }}"
|
||||||
|
|
||||||
|
# Before notarizing, we need to sign the binary.
|
||||||
|
# This blocks defines the configuration for doing so.
|
||||||
|
sign:
|
||||||
|
# The .p12 certificate file path or its base64'd contents.
|
||||||
|
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||||
|
|
||||||
|
# The password to be used to open the certificate.
|
||||||
|
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||||
|
|
||||||
|
# Then, we notarize the binaries.
|
||||||
|
notarize:
|
||||||
|
# The issuer ID.
|
||||||
|
# Its the UUID you see when creating the App Store Connect key.
|
||||||
|
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
|
||||||
|
|
||||||
|
# Key ID.
|
||||||
|
# You can see it in the list of App Store Connect Keys.
|
||||||
|
# It will also be in the ApiKey filename.
|
||||||
|
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
|
||||||
|
|
||||||
|
# The .p8 key file path or its base64'd contents.
|
||||||
|
key: "{{.Env.MACOS_NOTARY_KEY}}"
|
||||||
|
|
||||||
|
# Whether to wait for the notarization to finish.
|
||||||
|
# Not recommended, as it could take a really long time.
|
||||||
|
wait: true
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||||
builds:
|
builds:
|
||||||
@@ -100,21 +86,15 @@ archives:
|
|||||||
files:
|
files:
|
||||||
- non-existent*
|
- non-existent*
|
||||||
|
|
||||||
checksum:
|
|
||||||
extra_files:
|
|
||||||
- glob: ./dist/fzf-*darwin*.zip
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
github:
|
github:
|
||||||
owner: junegunn
|
owner: junegunn
|
||||||
name: fzf
|
name: fzf
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
name_template: '{{ .Tag }}'
|
name_template: '{{ .Version }}'
|
||||||
extra_files:
|
|
||||||
- glob: ./dist/fzf-*darwin*.zip
|
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ .Tag }}-devel"
|
name_template: "{{ .Version }}-devel"
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
|
|||||||
14
.rubocop.yml
14
.rubocop.yml
@@ -1,12 +1,16 @@
|
|||||||
|
AllCops:
|
||||||
|
NewCops: enable
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Metrics:
|
Metrics:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
Lint/ShadowingOuterLocalVariable:
|
Lint/ShadowingOuterLocalVariable:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Lint/NestedMethodDefinition:
|
||||||
|
Enabled: false
|
||||||
Style/MethodCallWithArgsParentheses:
|
Style/MethodCallWithArgsParentheses:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
IgnoredMethods:
|
AllowedMethods:
|
||||||
- assert
|
- assert
|
||||||
- exit
|
- exit
|
||||||
- paste
|
- paste
|
||||||
@@ -15,7 +19,7 @@ Style/MethodCallWithArgsParentheses:
|
|||||||
- refute
|
- refute
|
||||||
- require
|
- require
|
||||||
- send_keys
|
- send_keys
|
||||||
IgnoredPatterns:
|
AllowedPatterns:
|
||||||
- ^assert_
|
- ^assert_
|
||||||
- ^refute_
|
- ^refute_
|
||||||
Style/NumericPredicate:
|
Style/NumericPredicate:
|
||||||
@@ -28,5 +32,11 @@ Style/WordArray:
|
|||||||
MinSize: 1
|
MinSize: 1
|
||||||
Minitest/AssertEqual:
|
Minitest/AssertEqual:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Minitest/EmptyLineBeforeAssertionMethods:
|
||||||
|
Enabled: false
|
||||||
Naming/VariableNumber:
|
Naming/VariableNumber:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
Lint/EmptyBlock:
|
||||||
|
Enabled: false
|
||||||
|
Style/SafeNavigationChainLength:
|
||||||
|
Enabled: false
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
golang 1.20.13
|
golang 1.20.13
|
||||||
|
ruby 3.4.1
|
||||||
|
|||||||
144
ADVANCED.md
144
ADVANCED.md
@@ -1,18 +1,17 @@
|
|||||||
Advanced fzf examples
|
Advanced fzf examples
|
||||||
======================
|
======================
|
||||||
|
|
||||||
* *Last update: 2024/01/20*
|
* *Last update: 2025/02/02*
|
||||||
* *Requires fzf 0.46.0 or above*
|
* *Requires fzf 0.59.0 or later*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!-- vim-markdown-toc GFM -->
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
* [Introduction](#introduction)
|
* [Introduction](#introduction)
|
||||||
* [Screen Layout](#screen-layout)
|
* [Display modes](#display-modes)
|
||||||
* [`--height`](#--height)
|
* [`--height`](#--height)
|
||||||
* [`fzf-tmux`](#fzf-tmux)
|
* [`--tmux`](#--tmux)
|
||||||
* [Popup window support](#popup-window-support)
|
|
||||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||||
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||||
* [Toggling between data sources](#toggling-between-data-sources)
|
* [Toggling between data sources](#toggling-between-data-sources)
|
||||||
@@ -23,6 +22,7 @@ Advanced fzf examples
|
|||||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
* [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](#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)
|
* [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)
|
* [Log tailing](#log-tailing)
|
||||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||||
@@ -63,7 +63,7 @@ learn its wide variety of features.
|
|||||||
This document will guide you through some examples that will familiarize you
|
This document will guide you through some examples that will familiarize you
|
||||||
with the advanced features of fzf.
|
with the advanced features of fzf.
|
||||||
|
|
||||||
Screen Layout
|
Display modes
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
### `--height`
|
### `--height`
|
||||||
@@ -93,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
|
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
|
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||||
@@ -104,56 +104,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
|
|||||||
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||||
```
|
```
|
||||||
|
|
||||||
### `fzf-tmux`
|
### `--tmux`
|
||||||
|
|
||||||
Before fzf had `--height` option, we would open fzf in a tmux split pane not
|
(Requires tmux 3.3 or later)
|
||||||
to take up the whole screen. This is done using `fzf-tmux` script.
|
|
||||||
|
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Open fzf on a tmux split pane below the current pane.
|
# Open fzf in a tmux popup at the center of the screen with 70% width and height
|
||||||
# Takes the same set of options.
|
fzf --tmux 70%
|
||||||
fzf-tmux --layout=reverse
|
|
||||||
```
|
```
|
||||||
|
|
||||||

|

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

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

|
* `[center|top|bottom|left|right][,SIZE[%]][,SIZE[%][,border-native]]`
|
||||||
|
|
||||||

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

|

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

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

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

|

|
||||||
|
|
||||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we start fzf with
|
- Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
|
||||||
an empty input (`: | fzf`), then we make it start the initial Ripgrep
|
the initial Ripgrep process immediately via `start:reload` binding for the
|
||||||
process immediately via `start:reload` binding. This way, fzf owns the
|
consistency of the code.
|
||||||
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
|
|
||||||
the process will keep running in the background.
|
|
||||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||||
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
||||||
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
||||||
@@ -402,7 +399,7 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
|||||||
# 3. Open the file in Vim
|
# 3. Open the file in Vim
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
--bind "start:reload:$RG_PREFIX {q}" \
|
--bind "start:reload:$RG_PREFIX {q}" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||||
@@ -446,7 +443,7 @@ CTRL-F.
|
|||||||
rm -f /tmp/rg-fzf-{r,f}
|
rm -f /tmp/rg-fzf-{r,f}
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||||
@@ -489,7 +486,7 @@ prevent immediate evaluation.
|
|||||||
rm -f /tmp/rg-fzf-{r,f}
|
rm -f /tmp/rg-fzf-{r,f}
|
||||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
INITIAL_QUERY="${*:-}"
|
INITIAL_QUERY="${*:-}"
|
||||||
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
--bind "start:reload:$RG_PREFIX {q}" \
|
--bind "start:reload:$RG_PREFIX {q}" \
|
||||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
||||||
@@ -504,6 +501,44 @@ INITIAL_QUERY="${*:-}"
|
|||||||
--bind 'enter:become(vim {1} +{2})'
|
--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
|
Log tailing
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
@@ -529,15 +564,14 @@ Kubernetes pods.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pods() {
|
pods() {
|
||||||
: | command='kubectl get pods --all-namespaces' fzf \
|
command='kubectl get pods --all-namespaces' fzf \
|
||||||
--info=inline --layout=reverse --header-lines=1 \
|
--info=inline --layout=reverse --header-lines=1 \
|
||||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||||
--bind 'start:reload:$command' \
|
--bind 'start,ctrl-r:reload:$command' \
|
||||||
--bind 'ctrl-r:reload:$command' \
|
|
||||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||||
--preview-window up:follow \
|
--preview-window up:follow \
|
||||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||||
}
|
}
|
||||||
|
|||||||
570
CHANGELOG.md
570
CHANGELOG.md
@@ -1,6 +1,576 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
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/_
|
||||||
|
|
||||||
|
This version introduces three new border types, `--list-border`, `--input-border`, and `--header-border`, offering much greater flexibility for customizing the user interface.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-4-borders.png" />
|
||||||
|
|
||||||
|
Also, fzf now offers "style presets" for quick customization, which can be activated using the `--style` option.
|
||||||
|
|
||||||
|
| Preset | Screenshot |
|
||||||
|
| :--- | :--- |
|
||||||
|
| `default` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-default.png"/> |
|
||||||
|
| `full` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-full.png"/> |
|
||||||
|
| `minimal` | <img src="https://raw.githubusercontent.com/junegunn/i/master/fzf-style-minimal.png"/> |
|
||||||
|
|
||||||
|
- Style presets (#4160)
|
||||||
|
- `--style=full[:BORDER_STYLE]`
|
||||||
|
- `--style=default`
|
||||||
|
- `--style=minimal`
|
||||||
|
- Border and label for the list section (#4148)
|
||||||
|
- Options
|
||||||
|
- `--list-border[=STYLE]`
|
||||||
|
- `--list-label=LABEL`
|
||||||
|
- `--list-label-pos=COL[:bottom]`
|
||||||
|
- Colors
|
||||||
|
- `list-fg`
|
||||||
|
- `list-bg`
|
||||||
|
- `list-border`
|
||||||
|
- `list-label`
|
||||||
|
- Actions
|
||||||
|
- `change-list-label`
|
||||||
|
- `transform-list-label`
|
||||||
|
- Border and label for the input section (prompt line and info line) (#4154)
|
||||||
|
- Options
|
||||||
|
- `--input-border[=STYLE]`
|
||||||
|
- `--input-label=LABEL`
|
||||||
|
- `--input-label-pos=COL[:bottom]`
|
||||||
|
- Colors
|
||||||
|
- `input-fg` (`query`)
|
||||||
|
- `input-bg`
|
||||||
|
- `input-border`
|
||||||
|
- `input-label`
|
||||||
|
- Actions
|
||||||
|
- `change-input-label`
|
||||||
|
- `transform-input-label`
|
||||||
|
- Border and label for the header section (#4159)
|
||||||
|
- Options
|
||||||
|
- `--header-border[=STYLE]`
|
||||||
|
- `--header-label=LABEL`
|
||||||
|
- `--header-label-pos=COL[:bottom]`
|
||||||
|
- Colors
|
||||||
|
- `header-fg` (`header`)
|
||||||
|
- `header-bg`
|
||||||
|
- `header-border`
|
||||||
|
- `header-label`
|
||||||
|
- Actions
|
||||||
|
- `change-header-label`
|
||||||
|
- `transform-header-label`
|
||||||
|
- Added `--preview-border[=STYLE]` as short for `--preview-window=border[-STYLE]`
|
||||||
|
- Added new preview border style `line` which draws a single separator line between the preview window and the rest of the interface
|
||||||
|
- fzf will now render a dashed line (`┈┈`) in each `--gap` for better visual separation.
|
||||||
|
```sh
|
||||||
|
# All bash/zsh functions, highlighted
|
||||||
|
declare -f |
|
||||||
|
perl -0 -pe 's/^}\n/}\0/gm' |
|
||||||
|
bat --plain --language bash --color always |
|
||||||
|
fzf --read0 --ansi --layout reverse --multi --highlight-line --gap
|
||||||
|
```
|
||||||
|
* You can customize the line using `--gap-line[=STR]`.
|
||||||
|
- You can specify `border-native` to `--tmux` so that native tmux border is used instead of `--border`. This can be useful if you start a different program from inside the popup.
|
||||||
|
```sh
|
||||||
|
fzf --tmux border-native --bind 'enter:execute:less {}'
|
||||||
|
```
|
||||||
|
- Added `toggle-multi-line` action
|
||||||
|
- Added `toggle-hscroll` action
|
||||||
|
- Added `change-nth` action for dynamically changing the value of the `--nth` option
|
||||||
|
```sh
|
||||||
|
# Start with --nth 1, then 2, then 3, then back to the default, 1
|
||||||
|
echo 'foo foobar foobarbaz' | fzf --bind 'space:change-nth(2|3|)' --nth 1 -q foo
|
||||||
|
```
|
||||||
|
- `--nth` parts of each line can now be rendered in a different text style
|
||||||
|
```sh
|
||||||
|
# nth in a different style
|
||||||
|
ls -al | fzf --nth -1 --color nth:italic
|
||||||
|
ls -al | fzf --nth -1 --color nth:reverse
|
||||||
|
ls -al | fzf --nth -1 --color nth:reverse:bold
|
||||||
|
|
||||||
|
# Dim the other parts
|
||||||
|
ls -al | fzf --nth -1 --color nth:regular,fg:dim
|
||||||
|
|
||||||
|
# With 'change-nth'. The current nth option is exported as $FZF_NTH.
|
||||||
|
ps -ef | fzf --reverse --header-lines 1 --header-border bottom --input-border \
|
||||||
|
--color nth:regular,fg:dim \
|
||||||
|
--bind 'ctrl-n:change-nth(8..|1|2|3|4|5|6|7|)' \
|
||||||
|
--bind 'result:transform-prompt:echo "${FZF_NTH}> "'
|
||||||
|
```
|
||||||
|
- A single-character delimiter is now treated as a plain string delimiter rather than a regular expression delimiter, even if it's a regular expression meta-character.
|
||||||
|
- This means you can just write `--delimiter '|'` instead of escaping it as `--delimiter '\|'`
|
||||||
|
- Bug fixes
|
||||||
|
- Bug fixes and improvements in fish scripts (thanks to @bitraid)
|
||||||
|
|
||||||
|
0.57.0
|
||||||
|
------
|
||||||
|
- You can now resize the preview window by dragging the border
|
||||||
|
- Built-in walker improvements
|
||||||
|
- `--walker-root` can take multiple directory arguments. e.g. `--walker-root include src lib`
|
||||||
|
- `--walker-skip` can handle multi-component patterns. e.g. `--walker-skip target/build`
|
||||||
|
- Removed long processing delay when displaying images in the preview window
|
||||||
|
- `FZF_PREVIEW_*` environment variables are exported to all child processes (#4098)
|
||||||
|
- Bug fixes in fish scripts
|
||||||
|
|
||||||
|
0.56.3
|
||||||
|
------
|
||||||
|
- Bug fixes in zsh scripts
|
||||||
|
- fix(zsh): handle backtick trigger edge case (#4090)
|
||||||
|
- revert(zsh): remove 'fc -RI' call in the history widget (#4093)
|
||||||
|
- Thanks to @LangLangBart for the contributions
|
||||||
|
|
||||||
|
0.56.2
|
||||||
|
------
|
||||||
|
- Bug fixes
|
||||||
|
- Fixed abnormal scrolling behavior when `--wrap` is set (#4083)
|
||||||
|
- [zsh] Fixed warning message when `ksh_arrays` is set (#4084)
|
||||||
|
|
||||||
|
0.56.1
|
||||||
|
------
|
||||||
|
- Bug fixes and improvements
|
||||||
|
- Fixed a race condition which would cause fzf to present stale results after `reload` (#4070)
|
||||||
|
- `page-up` and `page-down` actions now work correctly with multi-line items (#4069)
|
||||||
|
- `{n}` is allowed in `SCROLL` expression in `--preview-window` (#4079)
|
||||||
|
- [zsh] Fixed regression in history loading with shared option (#4071)
|
||||||
|
- [zsh] Better command extraction in zsh completion (#4082)
|
||||||
|
- Thanks to @LangLangBart, @jaydee-coder, @alex-huff, and @vejkse for the contributions
|
||||||
|
|
||||||
|
0.56.0
|
||||||
|
------
|
||||||
|
- Added `--gap[=N]` option to display empty lines between items.
|
||||||
|
- This can be useful to visually separate adjacent multi-line items.
|
||||||
|
```sh
|
||||||
|
# All bash functions, highlighted
|
||||||
|
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||||
|
bat --plain --language bash --color always |
|
||||||
|
fzf --read0 --ansi --reverse --multi --highlight-line --gap
|
||||||
|
```
|
||||||
|
- Or just to make the list easier to read. For single-line items, you probably want to set `--color gutter:-1` as well to hide the gutter.
|
||||||
|
```sh
|
||||||
|
fzf --info inline-right --gap --color gutter:-1
|
||||||
|
```
|
||||||
|
- Added `noinfo` option to `--preview-window` to hide the scroll indicator in the preview window
|
||||||
|
- Bug fixes
|
||||||
|
- Thanks to @LangLangBart, @akinomyoga, and @charlievieth for fixing the bugs
|
||||||
|
|
||||||
|
0.55.0
|
||||||
|
------
|
||||||
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.55.0/_
|
||||||
|
|
||||||
|
- Added `exact-boundary-match` type to the search syntax. When a search term is single-quoted, fzf will search for the exact occurrences of the string with both ends at word boundaries.
|
||||||
|
```sh
|
||||||
|
fzf --query "'here'" << EOF
|
||||||
|
come here
|
||||||
|
not there
|
||||||
|
EOF
|
||||||
|
```
|
||||||
|
- [bash] Fuzzy path completion is enabled for all commands
|
||||||
|
- 1. If the default completion is not already set
|
||||||
|
- 2. And if the current bash supports `complete -D` option
|
||||||
|
- However, fuzzy completion for some commands can be "dynamically" disabled by the dynamic completion loader
|
||||||
|
- See the comment in `__fzf_default_completion` function for more information
|
||||||
|
- Comments are now allowed in `$FZF_DEFAULT_OPTS` and `$FZF_DEFAULT_OPTS_FILE`
|
||||||
|
```sh
|
||||||
|
export FZF_DEFAULT_OPTS='
|
||||||
|
# Layout options
|
||||||
|
--layout=reverse
|
||||||
|
--info=inline-right # Show info on the right side of the prompt line
|
||||||
|
# ...
|
||||||
|
'
|
||||||
|
```
|
||||||
|
- Hyperlinks (OSC 8) are now supported in the preview window and in the main window
|
||||||
|
```sh
|
||||||
|
printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>' | fzf --ansi
|
||||||
|
|
||||||
|
fzf --preview "printf '<< \e]8;;http://github.com/junegunn/fzf\e\\Link to \e[32mfz\e[0mf\e]8;;\e\\ >>'"
|
||||||
|
```
|
||||||
|
- The default `--ellipsis` is now `··` instead of `..`.
|
||||||
|
- [vim] A spec can have `exit` callback that is called with the exit status of fzf
|
||||||
|
- This can be used to clean up temporary resources or restore the original state when fzf is closed without a selection
|
||||||
|
- Fixed `--tmux bottom` when the status line is not at the bottom
|
||||||
|
- Fixed extra scroll offset in multi-line mode (`--read0` or `--wrap`)
|
||||||
|
- Added fallback `ps` command for `kill` completion on Cygwin
|
||||||
|
|
||||||
|
0.54.3
|
||||||
|
------
|
||||||
|
- Fixed incompatibility of adaptive height specification and 'start:reload'
|
||||||
|
```sh
|
||||||
|
# A regression in 0.54.0 would cause this to fail
|
||||||
|
fzf --height '~100%' --bind 'start:reload:seq 10'
|
||||||
|
```
|
||||||
|
- Environment variables are now available to `$FZF_DEFAULT_COMMAND`
|
||||||
|
```sh
|
||||||
|
FZF_DEFAULT_COMMAND='echo $FZF_QUERY' fzf --query foo
|
||||||
|
```
|
||||||
|
|
||||||
|
0.54.2
|
||||||
|
------
|
||||||
|
- Fixed incorrect syntax highlighting of truncated multi-line entries
|
||||||
|
- Updated GoReleaser to 2.1.0 to simplify notarization of macOS binaries
|
||||||
|
- macOS archives will be in `tar.gz` format instead of `zip` format since we no longer notarize the zip files but binaries
|
||||||
|
- (Windows) Reverted a mintty fix in 0.54.0
|
||||||
|
- As a result, mouse may not work on mintty in fullscreen mode. However, fzf will correctly read non-ASCII input in fullscreen mode (`--no-height`).
|
||||||
|
- fzf unfortunately cannot read non-ASCII input when not in fullscreen mode on Windows. So if you need to input non-ASCII characters, add `--no-height` to your `$FZF_DEFAULT_OPTS`.
|
||||||
|
- Any help in fixing this issue will be appreciated (#3799, #3847).
|
||||||
|
|
||||||
|
0.54.1
|
||||||
|
------
|
||||||
|
- Updated [fastwalk](https://github.com/charlievieth/fastwalk) dependency for built-in directory walker
|
||||||
|
- [fastwalk: add optional sorting and improve documentation](https://github.com/charlievieth/fastwalk/pull/27)
|
||||||
|
- [fastwalk: only check if MSYSTEM is set during MSYS/MSYS2](https://github.com/charlievieth/fastwalk/pull/28)
|
||||||
|
- Thanks to @charlievieth
|
||||||
|
- Reverted ALT-C binding of fish to use `cd` instead of `builtin cd`
|
||||||
|
- `builtin cd` was introduced to work around a bug of `cd` coming from `zoxide init --cmd cd fish` where it cannot handle `--` argument.
|
||||||
|
- However, the default `cd` of fish is actually a wrapper function for supporting `cd -`, so we want to use it instead.
|
||||||
|
- See [#3928](https://github.com/junegunn/fzf/pull/3928) for more information and consider helping zoxide fix the bug.
|
||||||
|
|
||||||
|
0.54.0
|
||||||
|
------
|
||||||
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.54.0/_
|
||||||
|
|
||||||
|
- Implemented line wrap of long items
|
||||||
|
- `--wrap` option enables line wrap
|
||||||
|
- `--wrap-sign` customizes the sign for wrapped lines (default: `↳ `)
|
||||||
|
- `toggle-wrap` action toggles line wrap
|
||||||
|
```sh
|
||||||
|
history | fzf --tac --wrap --bind 'ctrl-/:toggle-wrap' --wrap-sign $'\t↳ '
|
||||||
|
```
|
||||||
|
- fzf by default binds `CTRL-/` and `ALT-/` to `toggle-wrap`
|
||||||
|
- Updated shell integration scripts to leverage line wrap
|
||||||
|
- CTRL-R binding includes `--wrap-sign $'\t↳ '` to indent wrapped lines
|
||||||
|
- `kill **` completion uses `--wrap` to show the whole line by default
|
||||||
|
instead of showing it in the preview window
|
||||||
|
- Added `--info-command` option for customizing the info line
|
||||||
|
```sh
|
||||||
|
# Prepend the current cursor position in yellow
|
||||||
|
fzf --info-command='echo -e "\x1b[33;1m$FZF_POS\x1b[m/$FZF_INFO 💛"'
|
||||||
|
```
|
||||||
|
- `$FZF_INFO` is set to the original info text
|
||||||
|
- ANSI color codes are supported
|
||||||
|
- Pointer and marker signs can be set to empty strings
|
||||||
|
```sh
|
||||||
|
# Minimal style
|
||||||
|
fzf --pointer '' --marker '' --prompt '' --info hidden
|
||||||
|
```
|
||||||
|
- Better cache management and improved rendering for `--tail`
|
||||||
|
- Improved `--sync` behavior
|
||||||
|
- When `--sync` is provided, fzf will not render the interface until the initial filtering and the associated actions (bound to any of `start`, `load`, `result`, or `focus`) are complete.
|
||||||
|
```sh
|
||||||
|
# fzf will not render intermediate states
|
||||||
|
(sleep 1; seq 1000000; sleep 1) |
|
||||||
|
fzf --sync --query 5 --listen --bind start:up,load:up,result:up,focus:change-header:Ready
|
||||||
|
```
|
||||||
|
- GET endpoint is now available from `execute` and `transform` actions (it used to timeout due to lock conflict)
|
||||||
|
```sh
|
||||||
|
fzf --listen --sync --bind 'focus:transform-header:curl -s localhost:$FZF_PORT?limit=0 | jq .'
|
||||||
|
```
|
||||||
|
- Added `offset-middle` action to place the current item is in the middle of the screen
|
||||||
|
- fzf will not start the initial reader when `reload` or `reload-sync` is bound to `start` event. `fzf < /dev/null` or `: | fzf` are no longer required and extraneous `load` event will not fire due to the empty list.
|
||||||
|
```sh
|
||||||
|
# Now this will work as expected. Previously, this would print an invalid header line.
|
||||||
|
# `fzf < /dev/null` or `: | fzf` would fix the problem, but then an extraneous
|
||||||
|
# `load` event would fire and the header would be prematurely updated.
|
||||||
|
fzf --header 'Loading ...' --header-lines 1 \
|
||||||
|
--bind 'start:reload:sleep 1; ps -ef' \
|
||||||
|
--bind 'load:change-header:Loaded!'
|
||||||
|
```
|
||||||
|
- Fixed mouse support on Windows
|
||||||
|
- Fixed crash when using `--tiebreak=end` with very long items
|
||||||
|
- zsh 5.0 compatibility (thanks to @LangLangBart)
|
||||||
|
- Fixed `--walker-skip` to also skip symlinks to directories
|
||||||
|
- Fixed `result` event not fired when input stream is not complete
|
||||||
|
- New tags will have `v` prefix so that they are available on https://proxy.golang.org/
|
||||||
|
|
||||||
|
0.53.0
|
||||||
|
------
|
||||||
|
_Release highlights: https://junegunn.github.io/fzf/releases/0.53.0/_
|
||||||
|
|
||||||
|
- Multi-line display
|
||||||
|
- See [Processing multi-line items](https://junegunn.github.io/fzf/tips/processing-multi-line-items/)
|
||||||
|
- fzf can now display multi-line items
|
||||||
|
```sh
|
||||||
|
# All bash functions, highlighted
|
||||||
|
declare -f | perl -0777 -pe 's/^}\n/}\0/gm' |
|
||||||
|
bat --plain --language bash --color always |
|
||||||
|
fzf --read0 --ansi --reverse --multi --highlight-line
|
||||||
|
|
||||||
|
# Ripgrep multi-line output
|
||||||
|
rg --pretty bash | perl -0777 -pe 's/\n\n/\n\0/gm' |
|
||||||
|
fzf --read0 --ansi --multi --highlight-line --reverse --tmux 70%
|
||||||
|
```
|
||||||
|
- To disable multi-line display, use `--no-multi-line`
|
||||||
|
- CTRL-R bindings of bash, zsh, and fish have been updated to leverage multi-line display
|
||||||
|
- The default `--pointer` and `--marker` have been changed from `>` to Unicode bar characters as they look better with multi-line items
|
||||||
|
- Added `--marker-multi-line` to customize the select marker for multi-line entries with the default set to `╻┃╹`
|
||||||
|
```
|
||||||
|
╻First line
|
||||||
|
┃...
|
||||||
|
╹Last line
|
||||||
|
```
|
||||||
|
- Native tmux integration
|
||||||
|
- Added `--tmux` option to replace fzf-tmux script and simplify distribution
|
||||||
|
```sh
|
||||||
|
# --tmux [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
|
# Center, 100% width and 70% height
|
||||||
|
fzf --tmux 100%,70% --border horizontal --padding 1,2
|
||||||
|
|
||||||
|
# Left, 30% width
|
||||||
|
fzf --tmux left,30%
|
||||||
|
|
||||||
|
# Bottom, 50% height
|
||||||
|
fzf --tmux bottom,50%
|
||||||
|
```
|
||||||
|
- To keep the implementation simple, it only uses popups. You need tmux 3.3 or later.
|
||||||
|
- To use `--tmux` in Vim plugin:
|
||||||
|
```vim
|
||||||
|
let g:fzf_layout = { 'tmux': '100%,70%' }
|
||||||
|
```
|
||||||
|
- Added support for endless input streams
|
||||||
|
- See [Browsing log stream with fzf](https://junegunn.github.io/fzf/tips/browsing-log-streams/)
|
||||||
|
- Added `--tail=NUM` option to limit the number of items to keep in memory. This is useful when you want to browse an endless stream of data (e.g. log stream) with fzf while limiting memory usage.
|
||||||
|
```sh
|
||||||
|
# Interactive filtering of a log stream
|
||||||
|
tail -f *.log | fzf --tail 100000 --tac --no-sort --exact
|
||||||
|
```
|
||||||
|
- Better Windows Support
|
||||||
|
- fzf now works on Git bash (mintty) out of the box via winpty integration
|
||||||
|
- Many fixes and improvements for Windows
|
||||||
|
- man page is now embedded in the binary; `fzf --man` to see it
|
||||||
|
- Changed the default `--scroll-off` to 3, as we think it's a better default
|
||||||
|
- Process started by `execute` action now directly writes to and reads from `/dev/tty`. Manual `/dev/tty` redirection for interactive programs is no longer required.
|
||||||
|
```sh
|
||||||
|
# Vim will work fine without /dev/tty redirection
|
||||||
|
ls | fzf --bind 'space:execute:vim {}' > selected
|
||||||
|
```
|
||||||
|
- Added `print(...)` action to queue an arbitrary string to be printed on exit. This was mainly added to work around the limitation of `--expect` where it's not compatible with `--bind` on the same key and it would ignore other actions bound to it.
|
||||||
|
```sh
|
||||||
|
# This doesn't work as expected because --expect is not compatible with --bind
|
||||||
|
fzf --multi --expect ctrl-y --bind 'ctrl-y:select-all'
|
||||||
|
|
||||||
|
# This is something you can do instead
|
||||||
|
fzf --multi --bind 'enter:print()+accept,ctrl-y:select-all+print(ctrl-y)+accept'
|
||||||
|
```
|
||||||
|
- We also considered making them compatible, but realized that some users may have been relying on the current behavior.
|
||||||
|
- [`NO_COLOR`](https://no-color.org/) environment variable is now respected. If the variable is set, fzf defaults to `--no-color` unless otherwise specified.
|
||||||
|
|
||||||
0.52.1
|
0.52.1
|
||||||
------
|
------
|
||||||
- Fixed a critical bug in the Windows version
|
- Fixed a critical bug in the Windows version
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
FROM ubuntu:24.04
|
FROM rubylang/ruby:3.4.1-noble
|
||||||
RUN apt-get update -y && apt install -y git make golang zsh fish ruby tmux
|
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 gem install --no-document -v 5.22.3 minitest
|
||||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||||
@@ -8,5 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
|||||||
RUN rm -f /etc/bash.bashrc
|
RUN rm -f /etc/bash.bashrc
|
||||||
COPY . /fzf
|
COPY . /fzf
|
||||||
RUN cd /fzf && make install && ./install --all
|
RUN cd /fzf && make install && ./install --all
|
||||||
ENV LANG C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
|
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
|
||||||
17
Makefile
17
Makefile
@@ -1,20 +1,19 @@
|
|||||||
SHELL := bash
|
|
||||||
GO ?= go
|
GO ?= go
|
||||||
GOOS ?= $(shell $(GO) env GOOS)
|
GOOS ?= $(shell $(GO) env GOOS)
|
||||||
|
|
||||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh) $(MAKEFILE)
|
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||||
|
|
||||||
ifdef FZF_VERSION
|
ifdef FZF_VERSION
|
||||||
VERSION := $(FZF_VERSION)
|
VERSION := $(FZF_VERSION)
|
||||||
else
|
else
|
||||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null)
|
VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
|
||||||
endif
|
endif
|
||||||
ifeq ($(VERSION),)
|
ifeq ($(VERSION),)
|
||||||
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||||
endif
|
endif
|
||||||
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
|
VERSION_TRIM := $(shell echo $(VERSION) | sed "s/^v//; s/-.*//")
|
||||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||||
|
|
||||||
ifdef FZF_REVISION
|
ifdef FZF_REVISION
|
||||||
@@ -77,16 +76,22 @@ endif
|
|||||||
all: target/$(BINARY)
|
all: target/$(BINARY)
|
||||||
|
|
||||||
test: $(SOURCES)
|
test: $(SOURCES)
|
||||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
|
||||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||||
github.com/junegunn/fzf/src \
|
github.com/junegunn/fzf/src \
|
||||||
github.com/junegunn/fzf/src/algo \
|
github.com/junegunn/fzf/src/algo \
|
||||||
github.com/junegunn/fzf/src/tui \
|
github.com/junegunn/fzf/src/tui \
|
||||||
github.com/junegunn/fzf/src/util
|
github.com/junegunn/fzf/src/util
|
||||||
|
|
||||||
|
itest:
|
||||||
|
ruby test/runner.rb
|
||||||
|
|
||||||
bench:
|
bench:
|
||||||
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||||
|
|
||||||
|
lint: $(SOURCES) test/*.rb test/lib/*.rb
|
||||||
|
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||||
|
bundle exec rubocop -a --require rubocop-minitest --require rubocop-performance
|
||||||
|
|
||||||
install: bin/fzf
|
install: bin/fzf
|
||||||
|
|
||||||
generate:
|
generate:
|
||||||
@@ -184,4 +189,4 @@ update:
|
|||||||
$(GO) get -u
|
$(GO) get -u
|
||||||
$(GO) mod tidy
|
$(GO) mod tidy
|
||||||
|
|
||||||
.PHONY: all generate build release test bench install clean docker docker-test update
|
.PHONY: all generate build release test itest bench lint install clean docker docker-test update
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
|
|||||||
let g:fzf_colors =
|
let g:fzf_colors =
|
||||||
\ { 'fg': ['fg', 'Normal'],
|
\ { 'fg': ['fg', 'Normal'],
|
||||||
\ 'bg': ['bg', 'Normal'],
|
\ 'bg': ['bg', 'Normal'],
|
||||||
|
\ 'query': ['fg', 'Normal'],
|
||||||
\ 'hl': ['fg', 'Comment'],
|
\ 'hl': ['fg', 'Comment'],
|
||||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||||
@@ -289,12 +290,13 @@ The following table summarizes the available options.
|
|||||||
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
||||||
| `source` | list | Vim list as input to fzf |
|
| `source` | list | Vim list as input to fzf |
|
||||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||||
| `sink` | funcref | Reference to function to process each selected item |
|
| `sink` | funcref | Function to be called with each selected item |
|
||||||
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||||
|
| `exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130) |
|
||||||
| `options` | string/list | Options to fzf |
|
| `options` | string/list | Options to fzf |
|
||||||
| `dir` | string | Working directory |
|
| `dir` | string | Working directory |
|
||||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||||
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) |
|
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
|
||||||
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||||
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
|
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
|
||||||
|
|
||||||
@@ -457,12 +459,13 @@ let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
|||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||||
or above) by putting fzf-tmux options in `tmux` key.
|
or above) by putting `--tmux` option value in `tmux` key.
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
" See `man fzf-tmux` for available options
|
" See `--tmux` option in `man fzf` for available options
|
||||||
|
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
if exists('$TMUX')
|
if exists('$TMUX')
|
||||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||||
else
|
else
|
||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
endif
|
endif
|
||||||
|
|||||||
@@ -9,12 +9,24 @@
|
|||||||
# - https://iterm2.com/utilities/imgcat
|
# - https://iterm2.com/utilities/imgcat
|
||||||
|
|
||||||
if [[ $# -ne 1 ]]; then
|
if [[ $# -ne 1 ]]; then
|
||||||
>&2 echo "usage: $0 FILENAME"
|
>&2 echo "usage: $0 FILENAME[:LINENO][:IGNORED]"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
file=${1/#\~\//$HOME/}
|
file=${1/#\~\//$HOME/}
|
||||||
type=$(file --dereference --mime -- "$file")
|
|
||||||
|
center=0
|
||||||
|
if [[ ! -r $file ]]; then
|
||||||
|
if [[ $file =~ ^(.+):([0-9]+)\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
|
||||||
|
file=${BASH_REMATCH[1]}
|
||||||
|
center=${BASH_REMATCH[2]}
|
||||||
|
elif [[ $file =~ ^(.+):([0-9]+):[0-9]+\ *$ ]] && [[ -r ${BASH_REMATCH[1]} ]]; then
|
||||||
|
file=${BASH_REMATCH[1]}
|
||||||
|
center=${BASH_REMATCH[2]}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
type=$(file --brief --dereference --mime -- "$file")
|
||||||
|
|
||||||
if [[ ! $type =~ image/ ]]; then
|
if [[ ! $type =~ image/ ]]; then
|
||||||
if [[ $type =~ =binary ]]; then
|
if [[ $type =~ =binary ]]; then
|
||||||
@@ -32,7 +44,7 @@ if [[ ! $type =~ image/ ]]; then
|
|||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never -- "$file"
|
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never --highlight-line="${center:-0}" -- "$file"
|
||||||
exit
|
exit
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -45,19 +57,19 @@ elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stt
|
|||||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 1. Use kitty icat on kitty terminal
|
# 1. Use icat (from Kitty) if kitten is installed
|
||||||
if [[ $KITTY_WINDOW_ID ]]; then
|
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,
|
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||||
# you have to use 'stream'.
|
# you have to use 'stream'.
|
||||||
#
|
#
|
||||||
# 2. The last line of the output is the ANSI reset code without newline.
|
# 2. The last line of the output is the ANSI reset code without newline.
|
||||||
# This confuses fzf and makes it render scroll offset indicator.
|
# 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.
|
# 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
|
# 2. Use chafa with Sixel output
|
||||||
elif command -v chafa > /dev/null; then
|
elif command -v chafa > /dev/null; then
|
||||||
chafa -f sixel -s "$dim" "$file"
|
chafa -s "$dim" "$file"
|
||||||
# Add a new line character so that fzf can display multiple images in the preview window
|
# Add a new line character so that fzf can display multiple images in the preview window
|
||||||
echo
|
echo
|
||||||
|
|
||||||
|
|||||||
24
bin/fzf-tmux
24
bin/fzf-tmux
@@ -19,6 +19,9 @@ term=""
|
|||||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
[[ -n "$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 "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||||
|
|
||||||
|
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||||
|
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||||
|
|
||||||
@@ -94,11 +97,19 @@ while [[ $# -gt 0 ]]; do
|
|||||||
opt="$opt ${arg:0:2}$size"
|
opt="$opt ${arg:0:2}$size"
|
||||||
elif [[ "$size" =~ %$ ]]; then
|
elif [[ "$size" =~ %$ ]]; then
|
||||||
size=${size:0:((${#size}-1))}
|
size=${size:0:((${#size}-1))}
|
||||||
|
if [[ $tmux_32 = 1 ]]; then
|
||||||
if [[ -n "$swap" ]]; then
|
if [[ -n "$swap" ]]; then
|
||||||
opt="$opt -l $(( 100 - size ))%"
|
opt="$opt -l $(( 100 - size ))%"
|
||||||
else
|
else
|
||||||
opt="$opt -l $size%"
|
opt="$opt -l $size%"
|
||||||
fi
|
fi
|
||||||
|
else
|
||||||
|
if [[ -n "$swap" ]]; then
|
||||||
|
opt="$opt -p $(( 100 - size ))"
|
||||||
|
else
|
||||||
|
opt="$opt -p $size"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
if [[ -n "$swap" ]]; then
|
if [[ -n "$swap" ]]; then
|
||||||
if [[ "$arg" =~ ^.l ]]; then
|
if [[ "$arg" =~ ^.l ]]; then
|
||||||
@@ -132,8 +143,10 @@ if [[ -z "$TMUX" ]]; then
|
|||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --height option is not allowed. CTRL-Z is also disabled.
|
# * --height option is not allowed
|
||||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
|
# * CTRL-Z is also disabled
|
||||||
|
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
|
||||||
|
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||||
|
|
||||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
# 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
|
||||||
@@ -185,12 +198,11 @@ trap 'cleanup' EXIT
|
|||||||
|
|
||||||
envs="export TERM=$TERM "
|
envs="export TERM=$TERM "
|
||||||
if [[ "$opt" =~ "-E" ]]; then
|
if [[ "$opt" =~ "-E" ]]; then
|
||||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
if [[ $tmux_version = 3.2 ]]; then
|
||||||
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||||
|
elif [[ $tmux_32 = 1 ]]; then
|
||||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||||
opt="-B $opt"
|
opt="-B $opt"
|
||||||
elif [[ $tmux_version = 3.2 ]]; then
|
|
||||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
|
||||||
else
|
else
|
||||||
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
12
doc/fzf.txt
12
doc/fzf.txt
@@ -306,12 +306,13 @@ The following table summarizes the available options.
|
|||||||
`source` | string | External command to generate input to fzf (e.g. `find .` )
|
`source` | string | External command to generate input to fzf (e.g. `find .` )
|
||||||
`source` | list | Vim list as input to fzf
|
`source` | list | Vim list as input to fzf
|
||||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||||
`sink` | funcref | Reference to function to process each selected item
|
`sink` | funcref | Function to be called with each selected item
|
||||||
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||||
|
`exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130)
|
||||||
`options` | string/list | Options to fzf
|
`options` | string/list | Options to fzf
|
||||||
`dir` | string | Working directory
|
`dir` | string | Working directory
|
||||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||||
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
|
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
|
||||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||||
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
||||||
---------------------------+---------------+----------------------------------------------------------------------
|
---------------------------+---------------+----------------------------------------------------------------------
|
||||||
@@ -469,11 +470,12 @@ in Neovim.
|
|||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
<
|
<
|
||||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||||
or above) by putting fzf-tmux options in `tmux` key.
|
or above) by putting `--tmux` options in `tmux` key.
|
||||||
>
|
>
|
||||||
" See `man fzf-tmux` for available options
|
" See `--tmux` option in `man fzf` for available options
|
||||||
|
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||||
if exists('$TMUX')
|
if exists('$TMUX')
|
||||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||||
else
|
else
|
||||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
endif
|
endif
|
||||||
|
|||||||
16
go.mod
16
go.mod
@@ -1,20 +1,20 @@
|
|||||||
module github.com/junegunn/fzf
|
module github.com/junegunn/fzf
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/charlievieth/fastwalk v1.0.3
|
github.com/charlievieth/fastwalk v1.0.10
|
||||||
github.com/gdamore/tcell/v2 v2.7.4
|
github.com/gdamore/tcell/v2 v2.8.1
|
||||||
|
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
github.com/mattn/go-shellwords v1.0.12
|
|
||||||
github.com/rivo/uniseg v0.4.7
|
github.com/rivo/uniseg v0.4.7
|
||||||
golang.org/x/sys v0.20.0
|
golang.org/x/sys v0.30.0
|
||||||
golang.org/x/term v0.20.0
|
golang.org/x/term v0.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gdamore/encoding v1.0.0 // indirect
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.21.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.20
|
go 1.20
|
||||||
|
|||||||
56
go.sum
56
go.sum
@@ -1,17 +1,18 @@
|
|||||||
github.com/charlievieth/fastwalk v1.0.3 h1:eNWFaNPe5srPqQ5yyDbhAf11paeZaHWcihRhpuYFfSg=
|
github.com/charlievieth/fastwalk v1.0.10 h1:0qUbvA2O+K+X+IrTfZTC0UH2DK5MOA+KjVfStAHUnGg=
|
||||||
github.com/charlievieth/fastwalk v1.0.3/go.mod h1:JSfglY/gmL/rqsUS1NCsJTocB5n6sSl9ApAqif4CUbs=
|
github.com/charlievieth/fastwalk v1.0.10/go.mod h1:yGy1zbxog41ZVMcKA/i8ojXLFsuayX5VvwhQVoj9PBI=
|
||||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
|
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||||
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
|
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-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 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
|
||||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.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.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
@@ -19,15 +20,29 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
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-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.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.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.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-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-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.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.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-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.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.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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
@@ -35,23 +50,38 @@ 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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.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.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.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
|
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||||
|
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
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.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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
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.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.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.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=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
45
install
45
install
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.52.1
|
version=0.62.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
@@ -83,7 +83,7 @@ ask() {
|
|||||||
check_binary() {
|
check_binary() {
|
||||||
echo -n " - Checking fzf executable ... "
|
echo -n " - Checking fzf executable ... "
|
||||||
local output
|
local output
|
||||||
output=$("$fzf_base"/bin/fzf --version 2>&1)
|
output=$(FZF_DEFAULT_OPTS= "$fzf_base"/bin/fzf --version 2>&1)
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Error: $output"
|
echo "Error: $output"
|
||||||
binary_error="Invalid binary"
|
binary_error="Invalid binary"
|
||||||
@@ -146,7 +146,7 @@ download() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local url
|
local url
|
||||||
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
|
url=https://github.com/junegunn/fzf/releases/download/v$version/${1}
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
if ! (try_curl $url || try_wget $url); then
|
if ! (try_curl $url || try_wget $url); then
|
||||||
set +o pipefail
|
set +o pipefail
|
||||||
@@ -168,8 +168,8 @@ archi=$(uname -sm)
|
|||||||
binary_available=1
|
binary_available=1
|
||||||
binary_error=""
|
binary_error=""
|
||||||
case "$archi" in
|
case "$archi" in
|
||||||
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
Darwin\ arm64) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
Darwin\ x86_64) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||||
@@ -265,7 +265,11 @@ fi
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
if [[ $auto_completion -eq 1 ]] && [[ $key_bindings -eq 1 ]]; then
|
||||||
|
if [[ "$shell" = zsh ]]; then
|
||||||
|
echo "source <(fzf --$shell)" >> "$src"
|
||||||
|
else
|
||||||
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
echo "eval \"\$(fzf --$shell)\"" >> "$src"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
cat >> "$src" << EOF
|
cat >> "$src" << EOF
|
||||||
# Auto-completion
|
# Auto-completion
|
||||||
@@ -291,35 +295,44 @@ EOF
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
append_line() {
|
append_line() {
|
||||||
set -e
|
local update line file pat lines
|
||||||
|
|
||||||
local update line file pat lno
|
|
||||||
update="$1"
|
update="$1"
|
||||||
line="$2"
|
line="$2"
|
||||||
file="$3"
|
file="$3"
|
||||||
pat="${4:-}"
|
pat="${4:-}"
|
||||||
lno=""
|
lines=""
|
||||||
|
|
||||||
echo "Update $file:"
|
echo "Update $file:"
|
||||||
echo " - $line"
|
echo " - $line"
|
||||||
if [ -f "$file" ]; then
|
if [ -f "$file" ]; then
|
||||||
if [ $# -lt 4 ]; then
|
if [ $# -lt 4 ]; then
|
||||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
lines=$(\grep -nF "$line" "$file")
|
||||||
else
|
else
|
||||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
lines=$(\grep -nF "$pat" "$file")
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if [ -n "$lno" ]; then
|
|
||||||
echo " - Already exists: line #$lno"
|
if [ -n "$lines" ]; then
|
||||||
else
|
echo " - Already exists:"
|
||||||
if [ $update -eq 1 ]; then
|
sed 's/^/ Line /' <<< "$lines"
|
||||||
|
|
||||||
|
update=0
|
||||||
|
if ! \grep -qv "^[0-9]*:[[:space:]]*#" <<< "$lines" ; then
|
||||||
|
echo " - But they all seem to be commented"
|
||||||
|
ask " - Continue modifying $file?"
|
||||||
|
update=$?
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -e
|
||||||
|
if [ "$update" -eq 1 ]; then
|
||||||
[ -f "$file" ] && echo >> "$file"
|
[ -f "$file" ] && echo >> "$file"
|
||||||
echo "$line" >> "$file"
|
echo "$line" >> "$file"
|
||||||
echo " + Added"
|
echo " + Added"
|
||||||
else
|
else
|
||||||
echo " ~ Skipped"
|
echo " ~ Skipped"
|
||||||
fi
|
fi
|
||||||
fi
|
|
||||||
echo
|
echo
|
||||||
set +e
|
set +e
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
$version="0.52.1"
|
$version="0.62.0"
|
||||||
|
|
||||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ function download {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cd "$fzf_base\bin"
|
cd "$fzf_base\bin"
|
||||||
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
$url="https://github.com/junegunn/fzf/releases/download/v$version/$file"
|
||||||
$temp=$env:TMP + "\fzf.zip"
|
$temp=$env:TMP + "\fzf.zip"
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||||
|
|||||||
23
main.go
23
main.go
@@ -4,13 +4,14 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
fzf "github.com/junegunn/fzf/src"
|
fzf "github.com/junegunn/fzf/src"
|
||||||
"github.com/junegunn/fzf/src/protector"
|
"github.com/junegunn/fzf/src/protector"
|
||||||
)
|
)
|
||||||
|
|
||||||
var version = "0.52"
|
var version = "0.62"
|
||||||
var revision = "devel"
|
var revision = "devel"
|
||||||
|
|
||||||
//go:embed shell/key-bindings.bash
|
//go:embed shell/key-bindings.bash
|
||||||
@@ -28,6 +29,9 @@ var zshCompletion []byte
|
|||||||
//go:embed shell/key-bindings.fish
|
//go:embed shell/key-bindings.fish
|
||||||
var fishKeyBindings []byte
|
var fishKeyBindings []byte
|
||||||
|
|
||||||
|
//go:embed man/man1/fzf.1
|
||||||
|
var manPage []byte
|
||||||
|
|
||||||
func printScript(label string, content []byte) {
|
func printScript(label string, content []byte) {
|
||||||
fmt.Println("### " + label + " ###")
|
fmt.Println("### " + label + " ###")
|
||||||
fmt.Println(strings.TrimSpace(string(content)))
|
fmt.Println(strings.TrimSpace(string(content)))
|
||||||
@@ -35,7 +39,7 @@ func printScript(label string, content []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func exit(code int, err error) {
|
func exit(code int, err error) {
|
||||||
if err != nil {
|
if code == fzf.ExitError && err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err.Error())
|
fmt.Fprintln(os.Stderr, err.Error())
|
||||||
}
|
}
|
||||||
os.Exit(code)
|
os.Exit(code)
|
||||||
@@ -76,6 +80,21 @@ func main() {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if options.Man {
|
||||||
|
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
|
||||||
|
if len(file) == 0 {
|
||||||
|
fmt.Print(string(manPage))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(file)
|
||||||
|
cmd := exec.Command("man", file)
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
fmt.Print(string(manPage))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
code, err := fzf.Run(options)
|
code, err := fzf.Run(options)
|
||||||
exit(code, err)
|
exit(code, err)
|
||||||
|
|||||||
@@ -21,48 +21,48 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf-tmux 1 "May 2024" "fzf 0.52.1" "fzf-tmux - open fzf in tmux split pane"
|
.TH fzf\-tmux 1 "May 2025" "fzf 0.62.0" "fzf\-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf-tmux - open fzf in tmux split pane
|
fzf\-tmux - open fzf in tmux split pane
|
||||||
|
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
.B fzf\-tmux [\fILAYOUT OPTIONS\fR] [\-\-] [\fIFZF OPTIONS\fR]
|
||||||
|
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||||
a tmux popup window. It is designed to work just like fzf except that it does
|
a tmux popup window. It is designed to work just like fzf except that it does
|
||||||
not take up the whole screen. You can safely use fzf-tmux instead of fzf in
|
not take up the whole screen. You can safely use fzf\-tmux instead of fzf in
|
||||||
your scripts as the extra options will be silently ignored if you're not on
|
your scripts as the extra options will be silently ignored if you're not on
|
||||||
tmux.
|
tmux.
|
||||||
|
|
||||||
.SH LAYOUT OPTIONS
|
.SH LAYOUT OPTIONS
|
||||||
|
|
||||||
(default layout: \fB-d 50%\fR)
|
(default layout: \fB\-d 50%\fR)
|
||||||
|
|
||||||
.SS Popup window
|
.SS Popup window
|
||||||
(requires tmux 3.2 or above)
|
(requires tmux 3.2 or above)
|
||||||
.TP
|
.TP
|
||||||
.B "-p [WIDTH[%][,HEIGHT[%]]]"
|
.B "\-p [WIDTH[%][,HEIGHT[%]]]"
|
||||||
.TP
|
.TP
|
||||||
.B "-w WIDTH[%]"
|
.B "\-w WIDTH[%]"
|
||||||
.TP
|
.TP
|
||||||
.B "-h WIDTH[%]"
|
.B "\-h WIDTH[%]"
|
||||||
.TP
|
.TP
|
||||||
.B "-x COL"
|
.B "\-x COL"
|
||||||
.TP
|
.TP
|
||||||
.B "-y ROW"
|
.B "\-y ROW"
|
||||||
|
|
||||||
.SS Split pane
|
.SS Split pane
|
||||||
.TP
|
.TP
|
||||||
.B "-u [height[%]]"
|
.B "\-u [height[%]]"
|
||||||
Split above (up)
|
Split above (up)
|
||||||
.TP
|
.TP
|
||||||
.B "-d [height[%]]"
|
.B "\-d [height[%]]"
|
||||||
Split below (down)
|
Split below (down)
|
||||||
.TP
|
.TP
|
||||||
.B "-l [width[%]]"
|
.B "\-l [width[%]]"
|
||||||
Split left
|
Split left
|
||||||
.TP
|
.TP
|
||||||
.B "-r [width[%]]"
|
.B "\-r [width[%]]"
|
||||||
Split right
|
Split right
|
||||||
|
|||||||
1662
man/man1/fzf.1
1662
man/man1/fzf.1
File diff suppressed because it is too large
Load Diff
@@ -198,6 +198,7 @@ function! s:compare_binary_versions(a, b)
|
|||||||
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
let s:min_version = '0.53.0'
|
||||||
let s:checked = {}
|
let s:checked = {}
|
||||||
function! fzf#exec(...)
|
function! fzf#exec(...)
|
||||||
if !exists('s:exec')
|
if !exists('s:exec')
|
||||||
@@ -225,7 +226,11 @@ function! fzf#exec(...)
|
|||||||
let s:exec = binaries[-1]
|
let s:exec = binaries[-1]
|
||||||
endif
|
endif
|
||||||
|
|
||||||
if a:0 && !has_key(s:checked, a:1)
|
let min_version = s:min_version
|
||||||
|
if a:0 && s:compare_versions(a:1, min_version) > 0
|
||||||
|
let min_version = a:1
|
||||||
|
endif
|
||||||
|
if !has_key(s:checked, min_version)
|
||||||
let fzf_version = s:get_version(s:exec)
|
let fzf_version = s:get_version(s:exec)
|
||||||
if empty(fzf_version)
|
if empty(fzf_version)
|
||||||
let message = printf('Failed to run "%s --version"', s:exec)
|
let message = printf('Failed to run "%s --version"', s:exec)
|
||||||
@@ -233,17 +238,17 @@ function! fzf#exec(...)
|
|||||||
throw message
|
throw message
|
||||||
end
|
end
|
||||||
|
|
||||||
if s:compare_versions(fzf_version, a:1) >= 0
|
if s:compare_versions(fzf_version, min_version) >= 0
|
||||||
let s:checked[a:1] = 1
|
let s:checked[min_version] = 1
|
||||||
return s:exec
|
return s:exec
|
||||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', min_version, fzf_version)) =~? '^y'
|
||||||
let s:versions = {}
|
let s:versions = {}
|
||||||
unlet s:exec
|
unlet s:exec
|
||||||
redraw
|
redraw
|
||||||
call fzf#install()
|
call fzf#install()
|
||||||
return fzf#exec(a:1, 1)
|
return fzf#exec(min_version, 1)
|
||||||
else
|
else
|
||||||
throw printf('You need to upgrade fzf (required: %s or above)', a:1)
|
throw printf('You need to upgrade fzf (required: %s or above)', min_version)
|
||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -327,7 +332,10 @@ function! s:common_sink(action, lines) abort
|
|||||||
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
|
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
|
||||||
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
|
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
|
||||||
for item in a:lines
|
for item in a:lines
|
||||||
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
|
if has('win32unix') && item !~ '/'
|
||||||
|
let item = substitute(item, '\', '/', 'g')
|
||||||
|
end
|
||||||
|
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
|
||||||
let sep = s:is_win ? '\' : '/'
|
let sep = s:is_win ? '\' : '/'
|
||||||
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
|
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
|
||||||
endif
|
endif
|
||||||
@@ -350,7 +358,7 @@ endfunction
|
|||||||
|
|
||||||
function! s:get_color(attr, ...)
|
function! s:get_color(attr, ...)
|
||||||
" Force 24 bit colors: g:fzf_force_termguicolors (temporary workaround for https://github.com/junegunn/fzf.vim/issues/1152)
|
" 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 fam = gui ? 'gui' : 'cterm'
|
||||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||||
for group in a:000
|
for group in a:000
|
||||||
@@ -487,6 +495,8 @@ function! s:extract_option(opts, name)
|
|||||||
return opt
|
return opt
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
|
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
|
||||||
|
|
||||||
function! fzf#run(...) abort
|
function! fzf#run(...) abort
|
||||||
try
|
try
|
||||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||||
@@ -529,18 +539,19 @@ try
|
|||||||
\ executable('tput') && filereadable('/dev/tty')
|
\ executable('tput') && filereadable('/dev/tty')
|
||||||
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
||||||
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
||||||
let use_term = has_nvim_term ||
|
let use_term = has_nvim_term || has_vim8_term
|
||||||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
\ && !s:need_cmd_window
|
||||||
|
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||||
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
||||||
if prefer_tmux && use_tmux
|
if prefer_tmux && use_tmux
|
||||||
let use_height = 0
|
let use_height = 0
|
||||||
let use_term = 0
|
let use_term = 0
|
||||||
endif
|
endif
|
||||||
if use_term
|
if use_term
|
||||||
let optstr .= ' --no-height'
|
let optstr .= ' --no-height --no-tmux'
|
||||||
elseif use_height
|
elseif use_height
|
||||||
let height = s:calc_size(&lines, dict.down, dict)
|
let height = s:calc_size(&lines, dict.down, dict)
|
||||||
let optstr .= ' --height='.height
|
let optstr .= ' --no-tmux --height='.height
|
||||||
endif
|
endif
|
||||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
" 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])
|
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||||
@@ -573,19 +584,21 @@ function! s:fzf_tmux(dict)
|
|||||||
if empty(size)
|
if empty(size)
|
||||||
for o in ['up', 'down', 'left', 'right']
|
for o in ['up', 'down', 'left', 'right']
|
||||||
if s:present(a:dict, o)
|
if s:present(a:dict, o)
|
||||||
let spec = a:dict[o]
|
let size = o . ',' . a:dict[o]
|
||||||
if (o == 'up' || o == 'down') && spec[0] == '~'
|
|
||||||
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
|
|
||||||
else
|
|
||||||
" Legacy boolean option
|
|
||||||
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
|
|
||||||
endif
|
|
||||||
break
|
break
|
||||||
endif
|
endif
|
||||||
endfor
|
endfor
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
" Legacy fzf-tmux options
|
||||||
|
if size =~ '-'
|
||||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||||
|
end
|
||||||
|
|
||||||
|
" Using native --tmux option
|
||||||
|
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
|
||||||
|
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:splittable(dict)
|
function! s:splittable(dict)
|
||||||
@@ -657,21 +670,17 @@ else
|
|||||||
let s:launcher = function('s:xterm_launcher')
|
let s:launcher = function('s:xterm_launcher')
|
||||||
endif
|
endif
|
||||||
|
|
||||||
function! s:exit_handler(code, command, ...)
|
function! s:exit_handler(dict, code, command, ...)
|
||||||
if a:code == 130
|
if has_key(a:dict, 'exit')
|
||||||
return 0
|
call a:dict.exit(a:code)
|
||||||
elseif has('nvim') && a:code == 129
|
endif
|
||||||
" When deleting the terminal buffer while fzf is still running,
|
if a:code == 2
|
||||||
" Nvim sends SIGHUP.
|
|
||||||
return 0
|
|
||||||
elseif a:code > 1
|
|
||||||
call s:error('Error running ' . a:command)
|
call s:error('Error running ' . a:command)
|
||||||
if !empty(a:000)
|
if !empty(a:000)
|
||||||
sleep
|
sleep
|
||||||
endif
|
endif
|
||||||
return 0
|
|
||||||
endif
|
endif
|
||||||
return 1
|
return a:code
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:execute(dict, command, use_height, temps) abort
|
function! s:execute(dict, command, use_height, temps) abort
|
||||||
@@ -708,10 +717,10 @@ function! s:execute(dict, command, use_height, temps) abort
|
|||||||
call jobstart(cmd, fzf)
|
call jobstart(cmd, fzf)
|
||||||
return []
|
return []
|
||||||
endif
|
endif
|
||||||
elseif has('win32unix') && $TERM !=# 'cygwin'
|
elseif s:need_cmd_window
|
||||||
let shellscript = s:fzf_tempname()
|
let shellscript = s:fzf_tempname()
|
||||||
call s:writefile([command], shellscript)
|
call s:writefile([command], shellscript)
|
||||||
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
|
let command = 'start //WAIT sh -c '.shellscript
|
||||||
let a:temps.shellscript = shellscript
|
let a:temps.shellscript = shellscript
|
||||||
endif
|
endif
|
||||||
if a:use_height
|
if a:use_height
|
||||||
@@ -723,7 +732,7 @@ function! s:execute(dict, command, use_height, temps) abort
|
|||||||
let exit_status = v:shell_error
|
let exit_status = v:shell_error
|
||||||
redraw!
|
redraw!
|
||||||
let lines = s:collect(a:temps)
|
let lines = s:collect(a:temps)
|
||||||
return s:exit_handler(exit_status, command) ? lines : []
|
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:execute_tmux(dict, command, temps) abort
|
function! s:execute_tmux(dict, command, temps) abort
|
||||||
@@ -738,7 +747,7 @@ function! s:execute_tmux(dict, command, temps) abort
|
|||||||
let exit_status = v:shell_error
|
let exit_status = v:shell_error
|
||||||
redraw!
|
redraw!
|
||||||
let lines = s:collect(a:temps)
|
let lines = s:collect(a:temps)
|
||||||
return s:exit_handler(exit_status, command) ? lines : []
|
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||||
endfunction
|
endfunction
|
||||||
|
|
||||||
function! s:calc_size(max, val, dict)
|
function! s:calc_size(max, val, dict)
|
||||||
@@ -904,7 +913,7 @@ function! s:execute_term(dict, command, temps) abort
|
|||||||
endif
|
endif
|
||||||
|
|
||||||
let lines = s:collect(self.temps)
|
let lines = s:collect(self.temps)
|
||||||
if !s:exit_handler(a:code, self.command, 1)
|
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
|
||||||
return
|
return
|
||||||
endif
|
endif
|
||||||
|
|
||||||
@@ -1072,7 +1081,7 @@ endfunction
|
|||||||
|
|
||||||
function! s:cmd(bang, ...) abort
|
function! s:cmd(bang, ...) abort
|
||||||
let args = copy(a:000)
|
let args = copy(a:000)
|
||||||
let opts = { 'options': ['--multi'] }
|
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||||
if len(args) && isdirectory(expand(args[-1]))
|
if len(args) && isdirectory(expand(args[-1]))
|
||||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||||
if s:is_win && !&shellslash
|
if s:is_win && !&shellslash
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ bind '"\e[0n": redraw-current-line' 2> /dev/null
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append 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"
|
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
@@ -101,75 +101,84 @@ _fzf_opts_completion() {
|
|||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
opts="
|
opts="
|
||||||
-h --help
|
|
||||||
-e --exact
|
|
||||||
+x --no-extended
|
|
||||||
-q --query
|
|
||||||
-f --filter
|
|
||||||
--literal
|
|
||||||
--scheme
|
|
||||||
--expect
|
|
||||||
--disabled
|
|
||||||
--tiebreak
|
|
||||||
--bind
|
|
||||||
--color
|
|
||||||
-d --delimiter
|
|
||||||
-n --nth
|
|
||||||
--with-nth
|
|
||||||
+s --no-sort
|
|
||||||
--track
|
|
||||||
--tac
|
|
||||||
-i --ignore-case
|
|
||||||
+i --no-ignore-case
|
|
||||||
-m --multi
|
|
||||||
--ansi
|
|
||||||
--no-mouse
|
|
||||||
+c --no-color
|
+c --no-color
|
||||||
--no-bold
|
+i --no-ignore-case
|
||||||
--layout
|
+s --no-sort
|
||||||
--reverse
|
+x --no-extended
|
||||||
--cycle
|
--ansi
|
||||||
--keep-right
|
--bash
|
||||||
--no-hscroll
|
--bind
|
||||||
--hscroll-off
|
|
||||||
--scroll-off
|
|
||||||
--filepath-word
|
|
||||||
--info
|
|
||||||
--separator
|
|
||||||
--no-separator
|
|
||||||
--no-scrollbar
|
|
||||||
--jump-labels
|
|
||||||
-1 --select-1
|
|
||||||
-0 --exit-0
|
|
||||||
--read0
|
|
||||||
--print0
|
|
||||||
--print-query
|
|
||||||
--prompt
|
|
||||||
--pointer
|
|
||||||
--marker
|
|
||||||
--sync
|
|
||||||
--history
|
|
||||||
--history-size
|
|
||||||
--header
|
|
||||||
--header-lines
|
|
||||||
--header-first
|
|
||||||
--ellipsis
|
|
||||||
--preview
|
|
||||||
--preview-window
|
|
||||||
--height
|
|
||||||
--min-height
|
|
||||||
--border
|
--border
|
||||||
--border-label
|
--border-label
|
||||||
--border-label-pos
|
--border-label-pos
|
||||||
|
--color
|
||||||
|
--cycle
|
||||||
|
--disabled
|
||||||
|
--ellipsis
|
||||||
|
--expect
|
||||||
|
--filepath-word
|
||||||
|
--fish
|
||||||
|
--header
|
||||||
|
--header-first
|
||||||
|
--header-lines
|
||||||
|
--height
|
||||||
|
--highlight-line
|
||||||
|
--history
|
||||||
|
--history-size
|
||||||
|
--hscroll-off
|
||||||
|
--info
|
||||||
|
--jump-labels
|
||||||
|
--keep-right
|
||||||
|
--layout
|
||||||
|
--listen
|
||||||
|
--listen-unsafe
|
||||||
|
--literal
|
||||||
|
--man
|
||||||
|
--margin
|
||||||
|
--marker
|
||||||
|
--min-height
|
||||||
|
--no-bold
|
||||||
|
--no-clear
|
||||||
|
--no-hscroll
|
||||||
|
--no-mouse
|
||||||
|
--no-scrollbar
|
||||||
|
--no-separator
|
||||||
|
--no-unicode
|
||||||
|
--padding
|
||||||
|
--pointer
|
||||||
|
--preview
|
||||||
--preview-label
|
--preview-label
|
||||||
--preview-label-pos
|
--preview-label-pos
|
||||||
--no-unicode
|
--preview-window
|
||||||
--margin
|
--print-query
|
||||||
--padding
|
--print0
|
||||||
|
--prompt
|
||||||
|
--read0
|
||||||
|
--reverse
|
||||||
|
--scheme
|
||||||
|
--scroll-off
|
||||||
|
--separator
|
||||||
|
--sync
|
||||||
--tabstop
|
--tabstop
|
||||||
--listen
|
--tac
|
||||||
--no-clear
|
--tiebreak
|
||||||
|
--tmux
|
||||||
|
--track
|
||||||
--version
|
--version
|
||||||
|
--with-nth
|
||||||
|
--with-shell
|
||||||
|
--wrap
|
||||||
|
--zsh
|
||||||
|
-0 --exit-0
|
||||||
|
-1 --select-1
|
||||||
|
-d --delimiter
|
||||||
|
-e --exact
|
||||||
|
-f --filter
|
||||||
|
-h --help
|
||||||
|
-i --ignore-case
|
||||||
|
-m --multi
|
||||||
|
-n --nth
|
||||||
|
-q --query
|
||||||
--"
|
--"
|
||||||
|
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
@@ -255,6 +264,7 @@ _fzf_handle_dynamic_completion() {
|
|||||||
# _completion_loader may not have updated completion for the command
|
# _completion_loader may not have updated completion for the command
|
||||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||||
|
__fzf_orig_completion_get_orig_func "$cmd" || ret=1
|
||||||
|
|
||||||
# Update orig_complete by _fzf_orig_completion entry
|
# Update orig_complete by _fzf_orig_completion entry
|
||||||
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
||||||
@@ -280,7 +290,7 @@ __fzf_generic_path_completion() {
|
|||||||
fi
|
fi
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||||
base=${cur:0:${#cur}-${#trigger}}
|
base=${cur:0:${#cur}-${#trigger}}
|
||||||
eval "base=$base" 2> /dev/null || return
|
eval "base=$base" 2> /dev/null || return
|
||||||
@@ -301,12 +311,12 @@ __fzf_generic_path_completion() {
|
|||||||
else
|
else
|
||||||
if [[ $1 =~ dir ]]; then
|
if [[ $1 =~ dir ]]; then
|
||||||
walker=dir,follow
|
walker=dir,follow
|
||||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||||
else
|
else
|
||||||
walker=file,dir,follow,hidden
|
walker=file,dir,follow,hidden
|
||||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||||
fi
|
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
|
fi | while read -r item; do
|
||||||
printf "%q " "${item%$3}$3"
|
printf "%q " "${item%$3}$3"
|
||||||
done
|
done
|
||||||
@@ -367,7 +377,7 @@ _fzf_complete() {
|
|||||||
selected=$(
|
selected=$(
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' \
|
FZF_DEFAULT_OPTS_FILE='' \
|
||||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | command tr '\n' ' ')
|
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
|
||||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||||
if [[ -n "$selected" ]]; then
|
if [[ -n "$selected" ]]; then
|
||||||
COMPREPLY=("$selected")
|
COMPREPLY=("$selected")
|
||||||
@@ -399,9 +409,36 @@ _fzf_complete_kill() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_proc_completion() {
|
_fzf_proc_completion() {
|
||||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
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,start,time,command 2> /dev/null ||
|
||||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||||
|
command ps --everyone --full --windows # For cygwin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,12 +508,38 @@ complete -o default -F _fzf_opts_completion fzf
|
|||||||
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
||||||
complete -o default -F _fzf_opts_completion fzf-tmux
|
complete -o default -F _fzf_opts_completion fzf-tmux
|
||||||
|
|
||||||
|
# Default path completion
|
||||||
|
__fzf_default_completion() {
|
||||||
|
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
|
||||||
|
|
||||||
|
# Dynamic completion loader has updated the completion for the command
|
||||||
|
if [[ $? -eq 124 ]]; then
|
||||||
|
# We trigger _fzf_setup_completion so that fuzzy completion for the command
|
||||||
|
# still works. However, loader can update the completion for multiple
|
||||||
|
# commands at once, and fuzzy completion will no longer work for those
|
||||||
|
# other commands. e.g. pytest -> py.test, pytest-2, pytest-3, etc
|
||||||
|
_fzf_setup_completion path "$1"
|
||||||
|
return 124
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set fuzzy path completion as the default completion for all commands.
|
||||||
|
# We can't set up default completion,
|
||||||
|
# 1. if it's already set up by another script
|
||||||
|
# 2. or if the current version of bash doesn't support -D option
|
||||||
|
complete | command grep -q __fzf_default_completion ||
|
||||||
|
complete | command grep -- '-D$' | command grep -qv _comp_complete_load ||
|
||||||
|
complete -D -F __fzf_default_completion -o default -o bashdefault 2> /dev/null
|
||||||
|
|
||||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
||||||
|
|
||||||
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
||||||
# undocumented and subject to change in the future.
|
# undocumented and subject to change in the future.
|
||||||
|
#
|
||||||
|
# NOTE: Although we have default completion, we still need to set up completion
|
||||||
|
# for each command in case they already have completion set up by another script.
|
||||||
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
||||||
awk bat cat diff diff3
|
awk bat cat code diff diff3
|
||||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||||
javac ld less more mvim nvim patch perl python ruby
|
javac ld less more mvim nvim patch perl python ruby
|
||||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ if [[ -o interactive ]]; then
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append 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"
|
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo -E "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_comprun() {
|
__fzf_comprun() {
|
||||||
@@ -120,25 +120,18 @@ __fzf_comprun() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
|
||||||
__fzf_extract_command() {
|
__fzf_extract_command() {
|
||||||
local token tokens
|
# Control completion with the "compstate" parameter, insert and list nothing
|
||||||
tokens=(${(z)1})
|
compstate[insert]=
|
||||||
for token in $tokens; do
|
compstate[list]=
|
||||||
token=${(Q)token}
|
cmd_word="${(Q)words[1]}"
|
||||||
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
|
|
||||||
echo "$token"
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "${tokens[1]}"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_generic_path_completion() {
|
__fzf_generic_path_completion() {
|
||||||
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
|
local base lbuf compgen fzf_opts suffix tail dir leftover matches
|
||||||
base=$1
|
base=$1
|
||||||
lbuf=$2
|
lbuf=$2
|
||||||
cmd=$(__fzf_extract_command "$lbuf")
|
|
||||||
compgen=$3
|
compgen=$3
|
||||||
fzf_opts=$4
|
fzf_opts=$4
|
||||||
suffix=$5
|
suffix=$5
|
||||||
@@ -157,10 +150,11 @@ __fzf_generic_path_completion() {
|
|||||||
[ -z "$dir" ] && dir='.'
|
[ -z "$dir" ] && dir='.'
|
||||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||||
matches=$(
|
matches=$(
|
||||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
export FZF_DEFAULT_OPTS
|
||||||
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||||
if declare -f "$compgen" > /dev/null; then
|
if declare -f "$compgen" > /dev/null; then
|
||||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||||
else
|
else
|
||||||
if [[ $compgen =~ dir ]]; then
|
if [[ $compgen =~ dir ]]; then
|
||||||
walker=dir,follow
|
walker=dir,follow
|
||||||
@@ -169,10 +163,10 @@ __fzf_generic_path_completion() {
|
|||||||
walker=file,dir,follow,hidden
|
walker=file,dir,follow,hidden
|
||||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||||
fi
|
fi
|
||||||
__fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||||
fi | while read item; do
|
fi | while read -r item; do
|
||||||
item="${item%$suffix}$suffix"
|
item="${item%$suffix}$suffix"
|
||||||
echo -n "${(q)item} "
|
echo -n -E "${(q)item} "
|
||||||
done
|
done
|
||||||
)
|
)
|
||||||
matches=${matches% }
|
matches=${matches% }
|
||||||
@@ -197,11 +191,11 @@ _fzf_dir_completion() {
|
|||||||
"" "/" ""
|
"" "/" ""
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_feed_fifo() (
|
_fzf_feed_fifo() {
|
||||||
command rm -f "$1"
|
command rm -f "$1"
|
||||||
mkfifo "$1"
|
mkfifo "$1"
|
||||||
cat <&0 > "$1" &
|
cat <&0 > "$1" &|
|
||||||
)
|
}
|
||||||
|
|
||||||
_fzf_complete() {
|
_fzf_complete() {
|
||||||
setopt localoptions ksh_arrays
|
setopt localoptions ksh_arrays
|
||||||
@@ -226,10 +220,9 @@ _fzf_complete() {
|
|||||||
rest=("$@")
|
rest=("$@")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local fifo lbuf cmd matches post
|
local fifo lbuf matches post
|
||||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||||
lbuf=${rest[0]}
|
lbuf=${rest[0]}
|
||||||
cmd=$(__fzf_extract_command "$lbuf")
|
|
||||||
post="${funcstack[1]}_post"
|
post="${funcstack[1]}_post"
|
||||||
type $post > /dev/null 2>&1 || post=cat
|
type $post > /dev/null 2>&1 || post=cat
|
||||||
|
|
||||||
@@ -237,7 +230,7 @@ _fzf_complete() {
|
|||||||
matches=$(
|
matches=$(
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' \
|
FZF_DEFAULT_OPTS_FILE='' \
|
||||||
__fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||||
if [ -n "$matches" ]; then
|
if [ -n "$matches" ]; then
|
||||||
LBUFFER="$lbuf$matches"
|
LBUFFER="$lbuf$matches"
|
||||||
fi
|
fi
|
||||||
@@ -264,13 +257,14 @@ _fzf_complete_telnet() {
|
|||||||
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
|
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
|
||||||
# The current word without the trigger is in the $prefix variable passed from the caller.
|
# The current word without the trigger is in the $prefix variable passed from the caller.
|
||||||
_fzf_complete_ssh() {
|
_fzf_complete_ssh() {
|
||||||
local tokens=(${(z)1})
|
local -a tokens
|
||||||
|
tokens=(${(z)1})
|
||||||
case ${tokens[-1]} in
|
case ${tokens[-1]} in
|
||||||
-i|-F|-E)
|
-i|-F|-E)
|
||||||
_fzf_path_completion "$prefix" "$1"
|
_fzf_path_completion "$prefix" "$1"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
local user=
|
local user
|
||||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||||
;;
|
;;
|
||||||
@@ -296,9 +290,36 @@ _fzf_complete_unalias() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_kill() {
|
_fzf_complete_kill() {
|
||||||
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
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,start,time,command 2> /dev/null ||
|
||||||
command ps -eo user,pid,ppid,time,args # For BusyBox
|
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||||
|
command ps --everyone --full --windows # For cygwin
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,7 +328,7 @@ _fzf_complete_kill_post() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fzf-completion() {
|
fzf-completion() {
|
||||||
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
local tokens prefix trigger tail matches lbuf d_cmds cursor_pos cmd_word
|
||||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||||
|
|
||||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||||
@@ -318,11 +339,9 @@ fzf-completion() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd=$(__fzf_extract_command "$LBUFFER")
|
|
||||||
|
|
||||||
# Explicitly allow for empty trigger.
|
# Explicitly allow for empty trigger.
|
||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
[[ -z $trigger && ${LBUFFER[-1]} == ' ' ]] && tokens+=("")
|
||||||
|
|
||||||
# When the trigger starts with ';', it becomes a separate token
|
# When the trigger starts with ';', it becomes a separate token
|
||||||
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
||||||
@@ -337,16 +356,37 @@ fzf-completion() {
|
|||||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
|
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir})
|
||||||
|
|
||||||
|
{
|
||||||
|
cursor_pos=$CURSOR
|
||||||
|
# Move the cursor before the trigger to preserve word array elements when
|
||||||
|
# trigger chars like ';' or '`' would otherwise reset the 'words' array.
|
||||||
|
CURSOR=$((cursor_pos - ${#trigger} - 1))
|
||||||
|
# Check if at least one completion system (old or new) is active.
|
||||||
|
# If at least one user-defined completion widget is detected, nothing will
|
||||||
|
# be completed if neither the old nor the new completion system is enabled.
|
||||||
|
# In such cases, the 'zsh/compctl' module is loaded as a fallback.
|
||||||
|
if ! zmodload -F zsh/parameter p:functions 2>/dev/null || ! (( ${+functions[compdef]} )); then
|
||||||
|
zmodload -F zsh/compctl 2>/dev/null
|
||||||
|
fi
|
||||||
|
# Create a completion widget to access the 'words' array (man zshcompwid)
|
||||||
|
zle -C __fzf_extract_command .complete-word __fzf_extract_command
|
||||||
|
zle __fzf_extract_command
|
||||||
|
} always {
|
||||||
|
CURSOR=$cursor_pos
|
||||||
|
# Delete the completion widget
|
||||||
|
zle -D __fzf_extract_command 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||||
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||||
|
|
||||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
|
||||||
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
|
||||||
zle reset-prompt
|
zle reset-prompt
|
||||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
|
||||||
_fzf_dir_completion "$prefix" "$lbuf"
|
_fzf_dir_completion "$prefix" "$lbuf"
|
||||||
else
|
else
|
||||||
_fzf_path_completion "$prefix" "$lbuf"
|
_fzf_path_completion "$prefix" "$lbuf"
|
||||||
@@ -363,6 +403,7 @@ fzf-completion() {
|
|||||||
unset binding
|
unset binding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Normal widget
|
||||||
zle -N fzf-completion
|
zle -N fzf-completion
|
||||||
bindkey '^I' fzf-completion
|
bindkey '^I' fzf-completion
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ if [[ $- =~ i ]]; then
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append 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"
|
echo "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
@@ -57,15 +57,15 @@ __fzf_cd__() {
|
|||||||
if command -v perl > /dev/null; then
|
if command -v perl > /dev/null; then
|
||||||
__fzf_history__() {
|
__fzf_history__() {
|
||||||
local output script
|
local output script
|
||||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||||
output=$(
|
output=$(
|
||||||
set +o pipefail
|
set +o pipefail
|
||||||
builtin fc -lnr -2147483648 |
|
builtin fc -lnr -2147483648 |
|
||||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
) || return
|
) || return
|
||||||
READLINE_LINE=${output#*$'\t'}
|
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||||
if [[ -z "$READLINE_POINT" ]]; then
|
if [[ -z "$READLINE_POINT" ]]; then
|
||||||
echo "$READLINE_LINE"
|
echo "$READLINE_LINE"
|
||||||
else
|
else
|
||||||
@@ -91,7 +91,7 @@ else # awk - fallback for POSIX systems
|
|||||||
set +o pipefail
|
set +o pipefail
|
||||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||||
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
) || return
|
) || return
|
||||||
READLINE_LINE=${output#*$'\t'}
|
READLINE_LINE=${output#*$'\t'}
|
||||||
|
|||||||
@@ -11,20 +11,123 @@
|
|||||||
# - $FZF_ALT_C_COMMAND
|
# - $FZF_ALT_C_COMMAND
|
||||||
# - $FZF_ALT_C_OPTS
|
# - $FZF_ALT_C_OPTS
|
||||||
|
|
||||||
status is-interactive; or exit 0
|
|
||||||
|
|
||||||
|
|
||||||
# Key bindings
|
# 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
|
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
|
function __fzf_defaults
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append 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 FZF_TMUX_HEIGHT 40%
|
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||||
echo "--height $FZF_TMUX_HEIGHT --bind=ctrl-z:ignore" $argv[1]
|
string join ' ' -- \
|
||||||
command cat "$FZF_DEFAULT_OPTS_FILE" 2> /dev/null
|
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||||
echo $FZF_DEFAULT_OPTS $argv[2]
|
(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
|
end
|
||||||
|
|
||||||
# Store current token in $dir as root for the 'find' command
|
# Store current token in $dir as root for the 'find' command
|
||||||
@@ -34,48 +137,59 @@ function fzf_key_bindings
|
|||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
set -l prefix $commandline[3]
|
set -l prefix $commandline[3]
|
||||||
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
begin
|
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_CTRL_T_OPTS")
|
"$FZF_CTRL_T_OPTS --multi --print0")
|
||||||
|
|
||||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
eval (__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
|
||||||
end
|
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||||
if [ -z "$result" ]
|
and commandline -rt -- (string join -- ' ' $prefix(string escape -- $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
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
function fzf-history-widget -d "Show command history"
|
function fzf-history-widget -d "Show command history"
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
set -l -- command_line (commandline)
|
||||||
begin
|
set -l -- current_line (commandline -L)
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "" "--scheme=history --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m")
|
set -l -- total_lines (count $command_line)
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||||
|
|
||||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
'--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 --highlight-line $FZF_CTRL_R_OPTS" \
|
||||||
|
'--accept-nth=2.. --read0 --print0 --with-shell='(status fish-path)\\ -c)
|
||||||
|
|
||||||
# history's -z flag is needed for multi-line support.
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
set -lx FZF_DEFAULT_COMMAND
|
||||||
# before 2.4.0.
|
|
||||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
if type -q perl
|
||||||
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
set -a FZF_DEFAULT_OPTS '--tac'
|
||||||
and commandline -- $result
|
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
||||||
else
|
else
|
||||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
set FZF_DEFAULT_COMMAND \
|
||||||
and commandline -- $result
|
'set -l h (builtin history -z --reverse | string split0);' \
|
||||||
|
'for i in (seq (count $h) -1 1);' \
|
||||||
|
'string join0 -- $i\t(string replace -a -- \n \n\t $h[$i] | string collect);' \
|
||||||
|
'end'
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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
|
||||||
end
|
end
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -85,102 +199,32 @@ function fzf_key_bindings
|
|||||||
set -l fzf_query $commandline[2]
|
set -l fzf_query $commandline[2]
|
||||||
set -l prefix $commandline[3]
|
set -l prefix $commandline[3]
|
||||||
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||||
begin
|
"--reverse --walker=dir,follow,hidden --scheme=path" \
|
||||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path --walker-root='$dir'" "$FZF_ALT_C_OPTS")
|
"$FZF_ALT_C_OPTS --no-multi --print0")
|
||||||
set -lx FZF_DEFAULT_OPTS_FILE ''
|
|
||||||
|
set -lx FZF_DEFAULT_OPTS_FILE
|
||||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||||
eval (__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
|
||||||
|
|
||||||
if [ -n "$result" ]
|
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
|
||||||
cd -- $result
|
cd -- $result
|
||||||
|
commandline -rt -- $prefix
|
||||||
# Remove last token from commandline.
|
|
||||||
commandline -t ""
|
|
||||||
commandline -it -- $prefix
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzfcmd
|
|
||||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
|
||||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
|
||||||
if [ -n "$FZF_TMUX_OPTS" ]
|
|
||||||
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
|
||||||
else if [ $FZF_TMUX -eq 1 ]
|
|
||||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
|
||||||
else
|
|
||||||
echo "fzf"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
bind \cr fzf-history-widget
|
bind \cr fzf-history-widget
|
||||||
|
bind -M insert \cr fzf-history-widget
|
||||||
|
|
||||||
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
if not set -q FZF_CTRL_T_COMMAND; or test -n "$FZF_CTRL_T_COMMAND"
|
||||||
bind \ct fzf-file-widget
|
bind \ct fzf-file-widget
|
||||||
end
|
|
||||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
|
||||||
bind \ec fzf-cd-widget
|
|
||||||
end
|
|
||||||
|
|
||||||
if bind -M insert > /dev/null 2>&1
|
|
||||||
bind -M insert \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
|
bind -M insert \ct fzf-file-widget
|
||||||
end
|
end
|
||||||
|
|
||||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
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
|
bind -M insert \ec fzf-cd-widget
|
||||||
end
|
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 commandline (commandline -t)
|
|
||||||
|
|
||||||
# strip -option= from token if present
|
|
||||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
|
||||||
set commandline (string replace -- "$prefix" '' $commandline)
|
|
||||||
|
|
||||||
# eval is used to do shell expansion on paths
|
|
||||||
eval set commandline $commandline
|
|
||||||
|
|
||||||
if [ -z $commandline ]
|
|
||||||
# Default to current directory with no --query
|
|
||||||
set dir '.'
|
|
||||||
set fzf_query ''
|
|
||||||
else
|
|
||||||
set dir (__fzf_get_dir $commandline)
|
|
||||||
|
|
||||||
if [ "$dir" = "." -a (string sub -l 1 -- $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 $dir
|
|
||||||
echo $fzf_query
|
|
||||||
echo $prefix
|
|
||||||
end
|
|
||||||
|
|
||||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
|
||||||
set dir $argv
|
|
||||||
|
|
||||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
|
||||||
if [ (string length -- $dir) -gt 1 ]
|
|
||||||
set dir (string replace -r '/*$' -- '' $dir)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Iteratively check if dir exists and strip tail end of path
|
|
||||||
while [ ! -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
|
end
|
||||||
|
|||||||
@@ -41,9 +41,9 @@ if [[ -o interactive ]]; then
|
|||||||
__fzf_defaults() {
|
__fzf_defaults() {
|
||||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||||
# $2: Append 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"
|
echo -E "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||||
echo "${FZF_DEFAULT_OPTS-} $2"
|
echo -E "${FZF_DEFAULT_OPTS-} $2"
|
||||||
}
|
}
|
||||||
|
|
||||||
# CTRL-T - Paste the selected file path(s) into the command line
|
# CTRL-T - Paste the selected file path(s) into the command line
|
||||||
@@ -52,8 +52,8 @@ __fzf_select() {
|
|||||||
local item
|
local item
|
||||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read item; do
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
|
||||||
echo -n "${(q)item} "
|
echo -n -E "${(q)item} "
|
||||||
done
|
done
|
||||||
local ret=$?
|
local ret=$?
|
||||||
echo
|
echo
|
||||||
@@ -106,16 +106,25 @@ fi
|
|||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
fzf-history-widget() {
|
fzf-history-widget() {
|
||||||
local selected num
|
local selected
|
||||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||||
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
# Ensure the module is loaded if not already, and the required features, such
|
||||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
# as the associative 'history' array, which maps event numbers to full history
|
||||||
|
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||||
|
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||||
|
selected="$(printf '%s\t%s\000' "${(kv)history[@]}" |
|
||||||
|
perl -0 -ne 'if (!$seen{(/^\s*[0-9]+\**\t(.*)/s, $1)}++) { s/\n/\n\t/g; print; }' |
|
||||||
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m --read0") \
|
||||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||||
|
else
|
||||||
|
selected="$(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||||
|
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||||
|
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||||
|
fi
|
||||||
local ret=$?
|
local ret=$?
|
||||||
if [ -n "$selected" ]; then
|
if [ -n "$selected" ]; then
|
||||||
num=$(awk '{print $1}' <<< "$selected")
|
if [[ $(awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||||
if [[ "$num" =~ '^[1-9][0-9]*\*?$' ]]; then
|
zle vi-fetch-history -n $MATCH
|
||||||
zle vi-fetch-history -n ${num%\*}
|
|
||||||
else # selected is a custom query, not from history
|
else # selected is a custom query, not from history
|
||||||
LBUFFER="$selected"
|
LBUFFER="$selected"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -12,118 +12,145 @@ func _() {
|
|||||||
_ = x[actStart-1]
|
_ = x[actStart-1]
|
||||||
_ = x[actClick-2]
|
_ = x[actClick-2]
|
||||||
_ = x[actInvalid-3]
|
_ = x[actInvalid-3]
|
||||||
_ = x[actChar-4]
|
_ = x[actBracketedPasteBegin-4]
|
||||||
_ = x[actMouse-5]
|
_ = x[actBracketedPasteEnd-5]
|
||||||
_ = x[actBeginningOfLine-6]
|
_ = x[actChar-6]
|
||||||
_ = x[actAbort-7]
|
_ = x[actMouse-7]
|
||||||
_ = x[actAccept-8]
|
_ = x[actBeginningOfLine-8]
|
||||||
_ = x[actAcceptNonEmpty-9]
|
_ = x[actAbort-9]
|
||||||
_ = x[actAcceptOrPrintQuery-10]
|
_ = x[actAccept-10]
|
||||||
_ = x[actBackwardChar-11]
|
_ = x[actAcceptNonEmpty-11]
|
||||||
_ = x[actBackwardDeleteChar-12]
|
_ = x[actAcceptOrPrintQuery-12]
|
||||||
_ = x[actBackwardDeleteCharEof-13]
|
_ = x[actBackwardChar-13]
|
||||||
_ = x[actBackwardWord-14]
|
_ = x[actBackwardDeleteChar-14]
|
||||||
_ = x[actCancel-15]
|
_ = x[actBackwardDeleteCharEof-15]
|
||||||
_ = x[actChangeBorderLabel-16]
|
_ = x[actBackwardWord-16]
|
||||||
_ = x[actChangeHeader-17]
|
_ = x[actCancel-17]
|
||||||
_ = x[actChangeMulti-18]
|
_ = x[actChangeBorderLabel-18]
|
||||||
_ = x[actChangePreviewLabel-19]
|
_ = x[actChangeGhost-19]
|
||||||
_ = x[actChangePrompt-20]
|
_ = x[actChangeHeader-20]
|
||||||
_ = x[actChangeQuery-21]
|
_ = x[actChangeHeaderLabel-21]
|
||||||
_ = x[actClearScreen-22]
|
_ = x[actChangeInputLabel-22]
|
||||||
_ = x[actClearQuery-23]
|
_ = x[actChangeListLabel-23]
|
||||||
_ = x[actClearSelection-24]
|
_ = x[actChangeMulti-24]
|
||||||
_ = x[actClose-25]
|
_ = x[actChangeNth-25]
|
||||||
_ = x[actDeleteChar-26]
|
_ = x[actChangePointer-26]
|
||||||
_ = x[actDeleteCharEof-27]
|
_ = x[actChangePreview-27]
|
||||||
_ = x[actEndOfLine-28]
|
_ = x[actChangePreviewLabel-28]
|
||||||
_ = x[actFatal-29]
|
_ = x[actChangePreviewWindow-29]
|
||||||
_ = x[actForwardChar-30]
|
_ = x[actChangePrompt-30]
|
||||||
_ = x[actForwardWord-31]
|
_ = x[actChangeQuery-31]
|
||||||
_ = x[actKillLine-32]
|
_ = x[actClearScreen-32]
|
||||||
_ = x[actKillWord-33]
|
_ = x[actClearQuery-33]
|
||||||
_ = x[actUnixLineDiscard-34]
|
_ = x[actClearSelection-34]
|
||||||
_ = x[actUnixWordRubout-35]
|
_ = x[actClose-35]
|
||||||
_ = x[actYank-36]
|
_ = x[actDeleteChar-36]
|
||||||
_ = x[actBackwardKillWord-37]
|
_ = x[actDeleteCharEof-37]
|
||||||
_ = x[actSelectAll-38]
|
_ = x[actEndOfLine-38]
|
||||||
_ = x[actDeselectAll-39]
|
_ = x[actFatal-39]
|
||||||
_ = x[actToggle-40]
|
_ = x[actForwardChar-40]
|
||||||
_ = x[actToggleSearch-41]
|
_ = x[actForwardWord-41]
|
||||||
_ = x[actToggleAll-42]
|
_ = x[actKillLine-42]
|
||||||
_ = x[actToggleDown-43]
|
_ = x[actKillWord-43]
|
||||||
_ = x[actToggleUp-44]
|
_ = x[actUnixLineDiscard-44]
|
||||||
_ = x[actToggleIn-45]
|
_ = x[actUnixWordRubout-45]
|
||||||
_ = x[actToggleOut-46]
|
_ = x[actYank-46]
|
||||||
_ = x[actToggleTrack-47]
|
_ = x[actBackwardKillWord-47]
|
||||||
_ = x[actToggleTrackCurrent-48]
|
_ = x[actSelectAll-48]
|
||||||
_ = x[actToggleHeader-49]
|
_ = x[actDeselectAll-49]
|
||||||
_ = x[actTrackCurrent-50]
|
_ = x[actToggle-50]
|
||||||
_ = x[actUntrackCurrent-51]
|
_ = x[actToggleSearch-51]
|
||||||
_ = x[actDown-52]
|
_ = x[actToggleAll-52]
|
||||||
_ = x[actUp-53]
|
_ = x[actToggleDown-53]
|
||||||
_ = x[actPageUp-54]
|
_ = x[actToggleUp-54]
|
||||||
_ = x[actPageDown-55]
|
_ = x[actToggleIn-55]
|
||||||
_ = x[actPosition-56]
|
_ = x[actToggleOut-56]
|
||||||
_ = x[actHalfPageUp-57]
|
_ = x[actToggleTrack-57]
|
||||||
_ = x[actHalfPageDown-58]
|
_ = x[actToggleTrackCurrent-58]
|
||||||
_ = x[actOffsetUp-59]
|
_ = x[actToggleHeader-59]
|
||||||
_ = x[actOffsetDown-60]
|
_ = x[actToggleWrap-60]
|
||||||
_ = x[actJump-61]
|
_ = x[actToggleMultiLine-61]
|
||||||
_ = x[actJumpAccept-62]
|
_ = x[actToggleHscroll-62]
|
||||||
_ = x[actPrintQuery-63]
|
_ = x[actTrackCurrent-63]
|
||||||
_ = x[actRefreshPreview-64]
|
_ = x[actToggleInput-64]
|
||||||
_ = x[actReplaceQuery-65]
|
_ = x[actHideInput-65]
|
||||||
_ = x[actToggleSort-66]
|
_ = x[actShowInput-66]
|
||||||
_ = x[actShowPreview-67]
|
_ = x[actUntrackCurrent-67]
|
||||||
_ = x[actHidePreview-68]
|
_ = x[actDown-68]
|
||||||
_ = x[actTogglePreview-69]
|
_ = x[actUp-69]
|
||||||
_ = x[actTogglePreviewWrap-70]
|
_ = x[actPageUp-70]
|
||||||
_ = x[actTransform-71]
|
_ = x[actPageDown-71]
|
||||||
_ = x[actTransformBorderLabel-72]
|
_ = x[actPosition-72]
|
||||||
_ = x[actTransformHeader-73]
|
_ = x[actHalfPageUp-73]
|
||||||
_ = x[actTransformPreviewLabel-74]
|
_ = x[actHalfPageDown-74]
|
||||||
_ = x[actTransformPrompt-75]
|
_ = x[actOffsetUp-75]
|
||||||
_ = x[actTransformQuery-76]
|
_ = x[actOffsetDown-76]
|
||||||
_ = x[actPreview-77]
|
_ = x[actOffsetMiddle-77]
|
||||||
_ = x[actChangePreview-78]
|
_ = x[actJump-78]
|
||||||
_ = x[actChangePreviewWindow-79]
|
_ = x[actJumpAccept-79]
|
||||||
_ = x[actPreviewTop-80]
|
_ = x[actPrintQuery-80]
|
||||||
_ = x[actPreviewBottom-81]
|
_ = x[actRefreshPreview-81]
|
||||||
_ = x[actPreviewUp-82]
|
_ = x[actReplaceQuery-82]
|
||||||
_ = x[actPreviewDown-83]
|
_ = x[actToggleSort-83]
|
||||||
_ = x[actPreviewPageUp-84]
|
_ = x[actShowPreview-84]
|
||||||
_ = x[actPreviewPageDown-85]
|
_ = x[actHidePreview-85]
|
||||||
_ = x[actPreviewHalfPageUp-86]
|
_ = x[actTogglePreview-86]
|
||||||
_ = x[actPreviewHalfPageDown-87]
|
_ = x[actTogglePreviewWrap-87]
|
||||||
_ = x[actPrevHistory-88]
|
_ = x[actTransform-88]
|
||||||
_ = x[actPrevSelected-89]
|
_ = x[actTransformBorderLabel-89]
|
||||||
_ = x[actPut-90]
|
_ = x[actTransformGhost-90]
|
||||||
_ = x[actNextHistory-91]
|
_ = x[actTransformHeader-91]
|
||||||
_ = x[actNextSelected-92]
|
_ = x[actTransformHeaderLabel-92]
|
||||||
_ = x[actExecute-93]
|
_ = x[actTransformInputLabel-93]
|
||||||
_ = x[actExecuteSilent-94]
|
_ = x[actTransformListLabel-94]
|
||||||
_ = x[actExecuteMulti-95]
|
_ = x[actTransformNth-95]
|
||||||
_ = x[actSigStop-96]
|
_ = x[actTransformPointer-96]
|
||||||
_ = x[actFirst-97]
|
_ = x[actTransformPreviewLabel-97]
|
||||||
_ = x[actLast-98]
|
_ = x[actTransformPrompt-98]
|
||||||
_ = x[actReload-99]
|
_ = x[actTransformQuery-99]
|
||||||
_ = x[actReloadSync-100]
|
_ = x[actTransformSearch-100]
|
||||||
_ = x[actDisableSearch-101]
|
_ = x[actSearch-101]
|
||||||
_ = x[actEnableSearch-102]
|
_ = x[actPreview-102]
|
||||||
_ = x[actSelect-103]
|
_ = x[actPreviewTop-103]
|
||||||
_ = x[actDeselect-104]
|
_ = x[actPreviewBottom-104]
|
||||||
_ = x[actUnbind-105]
|
_ = x[actPreviewUp-105]
|
||||||
_ = x[actRebind-106]
|
_ = x[actPreviewDown-106]
|
||||||
_ = x[actBecome-107]
|
_ = x[actPreviewPageUp-107]
|
||||||
_ = x[actResponse-108]
|
_ = x[actPreviewPageDown-108]
|
||||||
_ = x[actShowHeader-109]
|
_ = x[actPreviewHalfPageUp-109]
|
||||||
_ = x[actHideHeader-110]
|
_ = x[actPreviewHalfPageDown-110]
|
||||||
|
_ = x[actPrevHistory-111]
|
||||||
|
_ = x[actPrevSelected-112]
|
||||||
|
_ = x[actPrint-113]
|
||||||
|
_ = x[actPut-114]
|
||||||
|
_ = x[actNextHistory-115]
|
||||||
|
_ = x[actNextSelected-116]
|
||||||
|
_ = x[actExecute-117]
|
||||||
|
_ = x[actExecuteSilent-118]
|
||||||
|
_ = x[actExecuteMulti-119]
|
||||||
|
_ = x[actSigStop-120]
|
||||||
|
_ = x[actFirst-121]
|
||||||
|
_ = x[actLast-122]
|
||||||
|
_ = x[actReload-123]
|
||||||
|
_ = x[actReloadSync-124]
|
||||||
|
_ = x[actDisableSearch-125]
|
||||||
|
_ = x[actEnableSearch-126]
|
||||||
|
_ = x[actSelect-127]
|
||||||
|
_ = x[actDeselect-128]
|
||||||
|
_ = x[actUnbind-129]
|
||||||
|
_ = x[actRebind-130]
|
||||||
|
_ = x[actToggleBind-131]
|
||||||
|
_ = x[actBecome-132]
|
||||||
|
_ = x[actShowHeader-133]
|
||||||
|
_ = x[actHideHeader-134]
|
||||||
|
_ = x[actBell-135]
|
||||||
|
_ = x[actExclude-136]
|
||||||
|
_ = x[actExcludeMulti-137]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeHeaderactChangeMultiactChangePreviewLabelactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactTrackCurrentactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformHeaderactTransformPreviewLabelactTransformPromptactTransformQueryactPreviewactChangePreviewactChangePreviewWindowactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactBecomeactResponseactShowHeaderactHideHeader"
|
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeHeaderLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactKillLineactKillWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformHeaderLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMulti"
|
||||||
|
|
||||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 42, 50, 68, 76, 85, 102, 123, 138, 159, 183, 198, 207, 227, 242, 256, 277, 292, 306, 320, 333, 350, 358, 371, 387, 399, 407, 421, 435, 446, 457, 475, 492, 499, 518, 530, 544, 553, 568, 580, 593, 604, 615, 627, 641, 662, 677, 692, 709, 716, 721, 730, 741, 752, 765, 780, 791, 804, 811, 824, 837, 854, 869, 882, 896, 910, 926, 946, 958, 981, 999, 1023, 1041, 1058, 1068, 1084, 1106, 1119, 1135, 1147, 1161, 1177, 1195, 1215, 1237, 1251, 1266, 1272, 1286, 1301, 1311, 1327, 1342, 1352, 1360, 1367, 1376, 1389, 1405, 1420, 1429, 1440, 1449, 1458, 1467, 1478, 1491, 1504}
|
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 249, 269, 283, 298, 318, 337, 355, 369, 381, 397, 413, 434, 456, 471, 485, 499, 512, 529, 537, 550, 566, 578, 586, 600, 614, 625, 636, 654, 671, 678, 697, 709, 723, 732, 747, 759, 772, 783, 794, 806, 820, 841, 856, 869, 887, 903, 918, 932, 944, 956, 973, 980, 985, 994, 1005, 1016, 1029, 1044, 1055, 1068, 1083, 1090, 1103, 1116, 1133, 1148, 1161, 1175, 1189, 1205, 1225, 1237, 1260, 1277, 1295, 1318, 1340, 1361, 1376, 1395, 1419, 1437, 1454, 1472, 1481, 1491, 1504, 1520, 1532, 1546, 1562, 1580, 1600, 1622, 1636, 1651, 1659, 1665, 1679, 1694, 1704, 1720, 1735, 1745, 1753, 1760, 1769, 1782, 1798, 1813, 1822, 1833, 1842, 1851, 1864, 1873, 1886, 1899, 1906, 1916, 1931}
|
||||||
|
|
||||||
func (i actionType) String() string {
|
func (i actionType) String() string {
|
||||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||||
|
|||||||
@@ -401,7 +401,7 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
|
|||||||
if i == 0 {
|
if i == 0 {
|
||||||
fmt.Print(" ")
|
fmt.Print(" ")
|
||||||
for j := int(f); j <= lastIdx; j++ {
|
for j := int(f); j <= lastIdx; j++ {
|
||||||
fmt.Printf(" " + string(T[j]) + " ")
|
fmt.Print(" " + string(T[j]) + " ")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
@@ -767,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
|||||||
char = unicode.To(unicode.LowerCase, char)
|
char = unicode.To(unicode.LowerCase, char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if normalize {
|
||||||
|
char = normalizeRune(char)
|
||||||
|
}
|
||||||
|
|
||||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||||
pchar := pattern[pidx_]
|
pchar := pattern[pidx_]
|
||||||
@@ -798,6 +801,14 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
|||||||
// The solution is much cheaper since there is only one possible alignment of
|
// The solution is much cheaper since there is only one possible alignment of
|
||||||
// the pattern.
|
// the pattern.
|
||||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
|
return exactMatchNaive(caseSensitive, normalize, forward, false, text, pattern, withPos, slab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExactMatchBoundary(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
|
return exactMatchNaive(caseSensitive, normalize, forward, true, text, pattern, withPos, slab)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryCheck bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
if len(pattern) == 0 {
|
if len(pattern) == 0 {
|
||||||
return Result{0, 0, 0}, nil
|
return Result{0, 0, 0}, nil
|
||||||
}
|
}
|
||||||
@@ -832,10 +843,22 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
|||||||
}
|
}
|
||||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||||
pchar := pattern[pidx_]
|
pchar := pattern[pidx_]
|
||||||
if pchar == char {
|
ok := pchar == char
|
||||||
|
if ok {
|
||||||
if pidx_ == 0 {
|
if pidx_ == 0 {
|
||||||
bonus = bonusAt(text, index_)
|
bonus = bonusAt(text, index_)
|
||||||
}
|
}
|
||||||
|
if boundaryCheck {
|
||||||
|
ok = bonus >= bonusBoundary
|
||||||
|
if ok && pidx_ == 0 {
|
||||||
|
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||||
|
}
|
||||||
|
if ok && pidx_ == len(pattern)-1 {
|
||||||
|
ok = index_ == lenRunes-1 || charClassOf(text.Get(index_+1)) <= charDelimiter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
pidx++
|
pidx++
|
||||||
if pidx == lenPattern {
|
if pidx == lenPattern {
|
||||||
if bonus > bestBonus {
|
if bonus > bestBonus {
|
||||||
@@ -861,7 +884,23 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
|||||||
sidx = lenRunes - (bestPos + 1)
|
sidx = lenRunes - (bestPos + 1)
|
||||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||||
}
|
}
|
||||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
var score int
|
||||||
|
if boundaryCheck {
|
||||||
|
// Underscore boundaries should be ranked lower than the other types of boundaries
|
||||||
|
score = int(bonus)
|
||||||
|
deduct := int(bonus-bonusBoundary) + 1
|
||||||
|
if sidx > 0 && text.Get(sidx-1) == '_' {
|
||||||
|
score -= deduct + 1
|
||||||
|
deduct = 1
|
||||||
|
}
|
||||||
|
if eidx < lenRunes && text.Get(eidx) == '_' {
|
||||||
|
score -= deduct
|
||||||
|
}
|
||||||
|
// Add base score so that this can compete with other match types e.g. 'foo' | bar
|
||||||
|
score += scoreMatch*lenPattern + int(bonusBoundaryWhite)*(lenPattern+1)
|
||||||
|
} else {
|
||||||
|
score, _ = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||||
|
}
|
||||||
return Result{sidx, eidx, score}, nil
|
return Result{sidx, eidx, score}, nil
|
||||||
}
|
}
|
||||||
return Result{-1, -1, 0}, nil
|
return Result{-1, -1, 0}, nil
|
||||||
|
|||||||
@@ -200,3 +200,12 @@ func TestLongString(t *testing.T) {
|
|||||||
bytes[math.MaxUint16] = 'z'
|
bytes[math.MaxUint16] = 'z'
|
||||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
87
src/ansi.go
87
src/ansi.go
@@ -1,6 +1,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -13,22 +14,28 @@ type ansiOffset struct {
|
|||||||
color ansiState
|
color ansiState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type url struct {
|
||||||
|
uri string
|
||||||
|
params string
|
||||||
|
}
|
||||||
|
|
||||||
type ansiState struct {
|
type ansiState struct {
|
||||||
fg tui.Color
|
fg tui.Color
|
||||||
bg tui.Color
|
bg tui.Color
|
||||||
attr tui.Attr
|
attr tui.Attr
|
||||||
lbg tui.Color
|
lbg tui.Color
|
||||||
|
url *url
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) colored() bool {
|
func (s *ansiState) colored() bool {
|
||||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
|
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) equals(t *ansiState) bool {
|
func (s *ansiState) equals(t *ansiState) bool {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return !s.colored()
|
return !s.colored()
|
||||||
}
|
}
|
||||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
|
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) ToString() string {
|
func (s *ansiState) ToString() string {
|
||||||
@@ -37,7 +44,7 @@ func (s *ansiState) ToString() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ret := ""
|
ret := ""
|
||||||
if s.attr&tui.Bold > 0 {
|
if s.attr&tui.Bold > 0 || s.attr&tui.BoldForce > 0 {
|
||||||
ret += "1;"
|
ret += "1;"
|
||||||
}
|
}
|
||||||
if s.attr&tui.Dim > 0 {
|
if s.attr&tui.Dim > 0 {
|
||||||
@@ -60,7 +67,11 @@ func (s *ansiState) ToString() string {
|
|||||||
}
|
}
|
||||||
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
ret += toAnsiString(s.fg, 30) + toAnsiString(s.bg, 40)
|
||||||
|
|
||||||
return "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
ret = "\x1b[" + strings.TrimSuffix(ret, ";") + "m"
|
||||||
|
if s.url != nil {
|
||||||
|
ret = fmt.Sprintf("\x1b]8;%s;%s\x1b\\%s\x1b]8;;\x1b", s.url.params, s.url.uri, ret)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func toAnsiString(color tui.Color, offset int) string {
|
func toAnsiString(color tui.Color, offset int) string {
|
||||||
@@ -87,21 +98,30 @@ func isPrint(c uint8) bool {
|
|||||||
return '\x20' <= c && c <= '\x7e'
|
return '\x20' <= c && c <= '\x7e'
|
||||||
}
|
}
|
||||||
|
|
||||||
func matchOperatingSystemCommand(s string) int {
|
func matchOperatingSystemCommand(s string, start int) int {
|
||||||
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||||
// ^ match starting here
|
// ^ match starting here after the first printable character
|
||||||
//
|
//
|
||||||
i := 5 // prefix matched in nextAnsiEscapeSequence()
|
i := start // prefix matched in nextAnsiEscapeSequence()
|
||||||
for ; i < len(s) && isPrint(s[i]); i++ {
|
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||||
}
|
}
|
||||||
if i < len(s) {
|
if i < len(s) {
|
||||||
if s[i] == '\x07' {
|
if s[i] == '\x07' {
|
||||||
return i + 1
|
return i + 1
|
||||||
}
|
}
|
||||||
|
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||||
|
// ------
|
||||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||||
return i + 2
|
return i + 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||||
|
// ------------
|
||||||
|
if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +156,7 @@ func isCtrlSeqStart(c uint8) bool {
|
|||||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||||
// calling FindStringIndex() on the below regex (which was originally used):
|
// 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)"
|
||||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||||
// fast check for ANSI escape sequences
|
// fast check for ANSI escape sequences
|
||||||
i := 0
|
i := 0
|
||||||
@@ -171,12 +191,20 @@ Loop:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// match: `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
// match: `\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||||
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
|
if i+5 < len(s) && s[i+1] == ']' {
|
||||||
(s[i+3] == ';' || s[i+3] == ':') && isPrint(s[i+4]) {
|
j := 2
|
||||||
|
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||||
|
// ------
|
||||||
|
for ; i+j < len(s) && isNumeric(s[i+j]); j++ {
|
||||||
|
}
|
||||||
|
|
||||||
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
|
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||||
return i, i + j
|
// ---------------
|
||||||
|
if j > 2 && i+j+1 < len(s) && (s[i+j] == ';' || s[i+j] == ':') && isPrint(s[i+j+1]) {
|
||||||
|
if k := matchOperatingSystemCommand(s[i:], j+2); k != -1 {
|
||||||
|
return i, i + k
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,20 +318,15 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
|||||||
return trimmed, nil, state
|
return trimmed, nil, state
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
func parseAnsiCode(s string) (int, string) {
|
||||||
var remaining string
|
var remaining string
|
||||||
var i int
|
var i int
|
||||||
if delimiter == 0 {
|
|
||||||
// Faster than strings.IndexAny(";:")
|
// Faster than strings.IndexAny(";:")
|
||||||
i = strings.IndexByte(s, ';')
|
i = strings.IndexByte(s, ';')
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
i = strings.IndexByte(s, ':')
|
i = strings.IndexByte(s, ':')
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
i = strings.IndexByte(s, delimiter)
|
|
||||||
}
|
|
||||||
if i >= 0 {
|
if i >= 0 {
|
||||||
delimiter = s[i]
|
|
||||||
remaining = s[i+1:]
|
remaining = s[i+1:]
|
||||||
s = s[:i]
|
s = s[:i]
|
||||||
}
|
}
|
||||||
@@ -315,26 +338,39 @@ func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
|||||||
for _, ch := range stringBytes(s) {
|
for _, ch := range stringBytes(s) {
|
||||||
ch -= '0'
|
ch -= '0'
|
||||||
if ch > 9 {
|
if ch > 9 {
|
||||||
return -1, delimiter, remaining
|
return -1, remaining
|
||||||
}
|
}
|
||||||
code = code*10 + int(ch)
|
code = code*10 + int(ch)
|
||||||
}
|
}
|
||||||
return code, delimiter, remaining
|
return code, remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
return -1, delimiter, remaining
|
return -1, remaining
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||||
var state ansiState
|
var state ansiState
|
||||||
if prevState == nil {
|
if prevState == nil {
|
||||||
state = ansiState{-1, -1, 0, -1}
|
state = ansiState{-1, -1, 0, -1, nil}
|
||||||
} else {
|
} else {
|
||||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
|
||||||
}
|
}
|
||||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||||
state.lbg = prevState.bg
|
state.lbg = prevState.bg
|
||||||
|
} 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 paramsEnd := strings.IndexRune(ansiCode[4:], ';'); paramsEnd >= 0 {
|
||||||
|
params := ansiCode[4 : 4+paramsEnd]
|
||||||
|
uri := ansiCode[5+paramsEnd : len(ansiCode)-stLen]
|
||||||
|
state.url = &url{uri: uri, params: params}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
@@ -350,11 +386,10 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
|||||||
state256 := 0
|
state256 := 0
|
||||||
ptr := &state.fg
|
ptr := &state.fg
|
||||||
|
|
||||||
var delimiter byte
|
|
||||||
count := 0
|
count := 0
|
||||||
for len(ansiCode) != 0 {
|
for len(ansiCode) != 0 {
|
||||||
var num int
|
var num int
|
||||||
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
|
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||||
count++
|
count++
|
||||||
switch state256 {
|
switch state256 {
|
||||||
case 0:
|
case 0:
|
||||||
|
|||||||
@@ -335,6 +335,28 @@ func TestExtractColor(t *testing.T) {
|
|||||||
assert((*offsets)[0], 0, 6, 2, -1, true)
|
assert((*offsets)[0], 0, 6, 2, -1, true)
|
||||||
assert((*offsets)[1], 6, 11, 200, 100, false)
|
assert((*offsets)[1], 6, 11, 200, 100, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
state = nil
|
||||||
|
var color24 tui.Color = (1 << 24) + (180 << 16) + (190 << 8) + 254
|
||||||
|
src = "\x1b[1mhello \x1b[22;1;38:2:180:190:254mworld"
|
||||||
|
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||||
|
if len(*offsets) != 2 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if state.fg != color24 || state.attr != 1 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
assert((*offsets)[0], 0, 6, -1, -1, true)
|
||||||
|
assert((*offsets)[1], 6, 11, color24, -1, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
src = "\x1b]133;A\x1b\\hello \x1b]133;C\x1b\\world"
|
||||||
|
check(func(offsets *[]ansiOffset, state *ansiState) {
|
||||||
|
if len(*offsets) != 1 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
assert((*offsets)[0], 0, 11, color24, -1, true)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAnsiCodeStringConversion(t *testing.T) {
|
func TestAnsiCodeStringConversion(t *testing.T) {
|
||||||
@@ -342,8 +364,8 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
|||||||
state := interpretCode(code, prevState)
|
state := interpretCode(code, prevState)
|
||||||
if expected != state.ToString() {
|
if expected != state.ToString() {
|
||||||
t.Errorf("expected: %s, actual: %s",
|
t.Errorf("expected: %s, actual: %s",
|
||||||
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
|
strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
|
||||||
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
|
strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert("\x1b[m", nil, "")
|
assert("\x1b[m", nil, "")
|
||||||
@@ -381,7 +403,7 @@ func TestParseAnsiCode(t *testing.T) {
|
|||||||
{"-2", "", -1},
|
{"-2", "", -1},
|
||||||
}
|
}
|
||||||
for _, x := range tests {
|
for _, x := range tests {
|
||||||
n, _, s := parseAnsiCode(x.In, 0)
|
n, s := parseAnsiCode(x.In)
|
||||||
if n != x.N || s != x.Exp {
|
if n != x.N || s != x.Exp {
|
||||||
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,14 @@ func (cc *ChunkCache) Clear() {
|
|||||||
cc.mutex.Unlock()
|
cc.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cc *ChunkCache) retire(chunk ...*Chunk) {
|
||||||
|
cc.mutex.Lock()
|
||||||
|
for _, c := range chunk {
|
||||||
|
delete(cc.cache, c)
|
||||||
|
}
|
||||||
|
cc.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// Add adds the list to the cache
|
// Add adds the list to the cache
|
||||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||||
|
|||||||
@@ -16,14 +16,16 @@ type ChunkList struct {
|
|||||||
chunks []*Chunk
|
chunks []*Chunk
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
trans ItemBuilder
|
trans ItemBuilder
|
||||||
|
cache *ChunkCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewChunkList returns a new ChunkList
|
// NewChunkList returns a new ChunkList
|
||||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
|
||||||
return &ChunkList{
|
return &ChunkList{
|
||||||
chunks: []*Chunk{},
|
chunks: []*Chunk{},
|
||||||
mutex: sync.Mutex{},
|
mutex: sync.Mutex{},
|
||||||
trans: trans}
|
trans: trans,
|
||||||
|
cache: cache}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||||
@@ -48,7 +50,12 @@ func CountItems(cs []*Chunk) int {
|
|||||||
if len(cs) == 0 {
|
if len(cs) == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
|
if len(cs) == 1 {
|
||||||
|
return cs[0].count
|
||||||
|
}
|
||||||
|
|
||||||
|
// First chunk might not be full due to --tail=N
|
||||||
|
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push adds the item to the list
|
// Push adds the item to the list
|
||||||
@@ -72,18 +79,56 @@ func (cl *ChunkList) Clear() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot returns immutable snapshot of the ChunkList
|
// Snapshot returns immutable snapshot of the ChunkList
|
||||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||||
cl.mutex.Lock()
|
cl.mutex.Lock()
|
||||||
|
|
||||||
|
changed := false
|
||||||
|
if tail > 0 && CountItems(cl.chunks) > tail {
|
||||||
|
changed = true
|
||||||
|
// Find the number of chunks to keep
|
||||||
|
numChunks := 0
|
||||||
|
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
|
||||||
|
numChunks++
|
||||||
|
left -= cl.chunks[i].count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the chunks to keep
|
||||||
|
ret := make([]*Chunk, numChunks)
|
||||||
|
minIndex := len(cl.chunks) - numChunks
|
||||||
|
cl.cache.retire(cl.chunks[:minIndex]...)
|
||||||
|
copy(ret, cl.chunks[minIndex:])
|
||||||
|
|
||||||
|
for left, i := tail, len(ret)-1; i >= 0; i-- {
|
||||||
|
chunk := ret[i]
|
||||||
|
if chunk.count > left {
|
||||||
|
newChunk := *chunk
|
||||||
|
newChunk.count = left
|
||||||
|
oldCount := chunk.count
|
||||||
|
for i := 0; i < left; i++ {
|
||||||
|
newChunk.items[i] = chunk.items[oldCount-left+i]
|
||||||
|
}
|
||||||
|
ret[i] = &newChunk
|
||||||
|
cl.cache.retire(chunk)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
left -= chunk.count
|
||||||
|
}
|
||||||
|
cl.chunks = ret
|
||||||
|
}
|
||||||
|
|
||||||
ret := make([]*Chunk, len(cl.chunks))
|
ret := make([]*Chunk, len(cl.chunks))
|
||||||
copy(ret, cl.chunks)
|
copy(ret, cl.chunks)
|
||||||
|
|
||||||
// Duplicate the last chunk
|
// Duplicate the first and the last chunk
|
||||||
if cnt := len(ret); cnt > 0 {
|
if cnt := len(ret); cnt > 0 {
|
||||||
|
if tail > 0 && cnt > 1 {
|
||||||
|
newChunk := *ret[0]
|
||||||
|
ret[0] = &newChunk
|
||||||
|
}
|
||||||
newChunk := *ret[cnt-1]
|
newChunk := *ret[cnt-1]
|
||||||
ret[cnt-1] = &newChunk
|
ret[cnt-1] = &newChunk
|
||||||
}
|
}
|
||||||
|
|
||||||
cl.mutex.Unlock()
|
cl.mutex.Unlock()
|
||||||
return ret, CountItems(ret)
|
return ret, CountItems(ret), changed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ func TestChunkList(t *testing.T) {
|
|||||||
// FIXME global
|
// FIXME global
|
||||||
sortCriteria = []criterion{byScore, byLength}
|
sortCriteria = []criterion{byScore, byLength}
|
||||||
|
|
||||||
cl := NewChunkList(func(item *Item, s []byte) bool {
|
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||||
item.text = util.ToChars(s)
|
item.text = util.ToChars(s)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Snapshot
|
// Snapshot
|
||||||
snapshot, count := cl.Snapshot()
|
snapshot, count, _ := cl.Snapshot(0)
|
||||||
if len(snapshot) > 0 || count > 0 {
|
if len(snapshot) > 0 || count > 0 {
|
||||||
t.Error("Snapshot should be empty now")
|
t.Error("Snapshot should be empty now")
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// But the new snapshot should contain the added items
|
// But the new snapshot should contain the added items
|
||||||
snapshot, count = cl.Snapshot()
|
snapshot, count, _ = cl.Snapshot(0)
|
||||||
if len(snapshot) != 1 && count != 2 {
|
if len(snapshot) != 1 && count != 2 {
|
||||||
t.Error("Snapshot should not be empty now")
|
t.Error("Snapshot should not be empty now")
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New snapshot
|
// New snapshot
|
||||||
snapshot, count = cl.Snapshot()
|
snapshot, count, _ = cl.Snapshot(0)
|
||||||
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
|
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
|
||||||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
||||||
t.Error("Expected two full chunks and one more chunk")
|
t.Error("Expected two full chunks and one more chunk")
|
||||||
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
|
|||||||
t.Error("Unexpected number of items:", lastChunkCount)
|
t.Error("Unexpected number of items:", lastChunkCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChunkListTail(t *testing.T) {
|
||||||
|
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||||
|
item.text = util.ToChars(s)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
total := chunkSize*2 + chunkSize/2
|
||||||
|
for i := 0; i < total; i++ {
|
||||||
|
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot, count, changed := cl.Snapshot(0)
|
||||||
|
assertCount := func(expected int, shouldChange bool) {
|
||||||
|
if count != expected || CountItems(snapshot) != expected {
|
||||||
|
t.Errorf("Unexpected count: %d (expected: %d)", count, expected)
|
||||||
|
}
|
||||||
|
if changed != shouldChange {
|
||||||
|
t.Error("Unexpected change status")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertCount(total, false)
|
||||||
|
|
||||||
|
tail := chunkSize + chunkSize/2
|
||||||
|
snapshot, count, changed = cl.Snapshot(tail)
|
||||||
|
assertCount(tail, true)
|
||||||
|
|
||||||
|
snapshot, count, changed = cl.Snapshot(tail)
|
||||||
|
assertCount(tail, false)
|
||||||
|
|
||||||
|
snapshot, count, changed = cl.Snapshot(0)
|
||||||
|
assertCount(tail, false)
|
||||||
|
|
||||||
|
tail = chunkSize / 2
|
||||||
|
snapshot, count, changed = cl.Snapshot(tail)
|
||||||
|
assertCount(tail, true)
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
previewCancelWait = 500 * time.Millisecond
|
previewCancelWait = 500 * time.Millisecond
|
||||||
previewChunkDelay = 100 * time.Millisecond
|
previewChunkDelay = 100 * time.Millisecond
|
||||||
previewDelayed = 500 * time.Millisecond
|
previewDelayed = 500 * time.Millisecond
|
||||||
maxPatternLength = 300
|
maxPatternLength = 1000
|
||||||
maxMulti = math.MaxInt32
|
maxMulti = math.MaxInt32
|
||||||
|
|
||||||
// Matcher
|
// Matcher
|
||||||
@@ -67,9 +67,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ExitCancel = -1
|
|
||||||
ExitOk = 0
|
ExitOk = 0
|
||||||
ExitNoMatch = 1
|
ExitNoMatch = 1
|
||||||
ExitError = 2
|
ExitError = 2
|
||||||
|
ExitBecome = 126
|
||||||
ExitInterrupt = 130
|
ExitInterrupt = 130
|
||||||
)
|
)
|
||||||
|
|||||||
168
src/core.go
168
src/core.go
@@ -2,6 +2,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,8 +18,36 @@ Matcher -> EvtSearchFin -> Terminal (update list)
|
|||||||
Matcher -> EvtHeader -> Terminal (update header)
|
Matcher -> EvtHeader -> Terminal (update header)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type revision struct {
|
||||||
|
major int
|
||||||
|
minor int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *revision) bumpMajor() {
|
||||||
|
r.major++
|
||||||
|
r.minor = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *revision) bumpMinor() {
|
||||||
|
r.minor++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r revision) compatible(other revision) bool {
|
||||||
|
return r.major == other.major
|
||||||
|
}
|
||||||
|
|
||||||
// Run starts fzf
|
// Run starts fzf
|
||||||
func Run(opts *Options) (int, error) {
|
func Run(opts *Options) (int, error) {
|
||||||
|
if opts.Filter == nil {
|
||||||
|
if opts.Tmux != nil && len(os.Getenv("TMUX")) > 0 && opts.Tmux.index >= opts.Height.index {
|
||||||
|
return runTmux(os.Args, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
if needWinpty(opts) {
|
||||||
|
return runWinpty(os.Args, opts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := postProcessOptions(opts); err != nil {
|
if err := postProcessOptions(opts); err != nil {
|
||||||
return ExitError, err
|
return ExitError, err
|
||||||
}
|
}
|
||||||
@@ -63,11 +92,12 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Chunk list
|
// Chunk list
|
||||||
|
cache := NewChunkCache()
|
||||||
var chunkList *ChunkList
|
var chunkList *ChunkList
|
||||||
var itemIndex int32
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
header := make([]string, 0, opts.HeaderLines)
|
||||||
if len(opts.WithNth) == 0 {
|
if opts.WithNth == nil {
|
||||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, byteString(data))
|
header = append(header, byteString(data))
|
||||||
eventBox.Set(EvtHeader, header)
|
eventBox.Set(EvtHeader, header)
|
||||||
@@ -79,7 +109,8 @@ func Run(opts *Options) (int, error) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||||
|
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||||
var ansiState *ansiState
|
var ansiState *ansiState
|
||||||
@@ -97,8 +128,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
trans := Transform(tokens, opts.WithNth)
|
transformed := nthTransformer(tokens, itemIndex)
|
||||||
transformed := joinTokens(trans)
|
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, transformed)
|
header = append(header, transformed)
|
||||||
eventBox.Set(EvtHeader, header)
|
eventBox.Set(EvtHeader, header)
|
||||||
@@ -116,6 +146,24 @@ func Run(opts *Options) (int, error) {
|
|||||||
// Process executor
|
// Process executor
|
||||||
executor := util.NewExecutor(opts.WithShell)
|
executor := util.NewExecutor(opts.WithShell)
|
||||||
|
|
||||||
|
// Terminal I/O
|
||||||
|
var terminal *Terminal
|
||||||
|
var err error
|
||||||
|
var initialEnv []string
|
||||||
|
initialReload := opts.extractReloadOnStart()
|
||||||
|
if opts.Filter == nil {
|
||||||
|
terminal, err = NewTerminal(opts, eventBox, executor)
|
||||||
|
if err != nil {
|
||||||
|
return ExitError, err
|
||||||
|
}
|
||||||
|
if len(initialReload) > 0 {
|
||||||
|
var temps []string
|
||||||
|
initialReload, temps = terminal.replacePlaceholderInInitialCommand(initialReload)
|
||||||
|
initialEnv = terminal.environ()
|
||||||
|
defer removeFiles(temps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reader
|
// Reader
|
||||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||||
var reader *Reader
|
var reader *Reader
|
||||||
@@ -123,7 +171,10 @@ func Run(opts *Options) (int, error) {
|
|||||||
reader = NewReader(func(data []byte) bool {
|
reader = NewReader(func(data []byte) bool {
|
||||||
return chunkList.Push(data)
|
return chunkList.Push(data)
|
||||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
|
||||||
|
readyChan := make(chan bool)
|
||||||
|
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
|
||||||
|
<-readyChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matcher
|
// Matcher
|
||||||
@@ -137,17 +188,37 @@ func Run(opts *Options) (int, error) {
|
|||||||
forward = false
|
forward = false
|
||||||
case byBegin:
|
case byBegin:
|
||||||
forward = true
|
forward = true
|
||||||
|
case byPathname:
|
||||||
|
withPos = true
|
||||||
|
forward = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cache := NewChunkCache()
|
|
||||||
|
nth := opts.Nth
|
||||||
|
inputRevision := revision{}
|
||||||
|
snapshotRevision := revision{}
|
||||||
patternCache := make(map[string]*Pattern)
|
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 {
|
patternBuilder := func(runes []rune) *Pattern {
|
||||||
|
denyMutex.Lock()
|
||||||
|
denylistCopy := make(map[int32]struct{})
|
||||||
|
for k, v := range denylist {
|
||||||
|
denylistCopy[k] = v
|
||||||
|
}
|
||||||
|
denyMutex.Unlock()
|
||||||
return BuildPattern(cache, patternCache,
|
return BuildPattern(cache, patternCache,
|
||||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
||||||
}
|
}
|
||||||
inputRevision := 0
|
|
||||||
snapshotRevision := 0
|
|
||||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||||
|
|
||||||
// Filtering mode
|
// Filtering mode
|
||||||
@@ -176,12 +247,13 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, eventBox, executor, opts.ReadZero, false)
|
}, eventBox, executor, opts.ReadZero, false)
|
||||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip)
|
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
|
||||||
} else {
|
} else {
|
||||||
eventBox.Unwatch(EvtReadNew)
|
eventBox.Unwatch(EvtReadNew)
|
||||||
eventBox.WaitFor(EvtReadFin)
|
eventBox.WaitFor(EvtReadFin)
|
||||||
|
|
||||||
snapshot, _ := chunkList.Snapshot()
|
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||||
|
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||||
merger, _ := matcher.scan(MatchRequest{
|
merger, _ := matcher.scan(MatchRequest{
|
||||||
chunks: snapshot,
|
chunks: snapshot,
|
||||||
pattern: pattern})
|
pattern: pattern})
|
||||||
@@ -206,18 +278,14 @@ func Run(opts *Options) (int, error) {
|
|||||||
go matcher.Loop()
|
go matcher.Loop()
|
||||||
defer matcher.Stop()
|
defer matcher.Stop()
|
||||||
|
|
||||||
// Terminal I/O
|
// Handling adaptive height
|
||||||
terminal, err := NewTerminal(opts, eventBox, executor)
|
|
||||||
if err != nil {
|
|
||||||
return ExitError, err
|
|
||||||
}
|
|
||||||
maxFit := 0 // Maximum number of items that can fit on screen
|
maxFit := 0 // Maximum number of items that can fit on screen
|
||||||
padHeight := 0
|
padHeight := 0
|
||||||
heightUnknown := opts.Height.auto
|
heightUnknown := opts.Height.auto
|
||||||
if heightUnknown {
|
if heightUnknown {
|
||||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||||
}
|
}
|
||||||
deferred := opts.Select1 || opts.Exit0
|
deferred := opts.Select1 || opts.Exit0 || opts.Sync
|
||||||
go terminal.Loop()
|
go terminal.Loop()
|
||||||
if !deferred && !heightUnknown {
|
if !deferred && !heightUnknown {
|
||||||
// Start right away
|
// Start right away
|
||||||
@@ -227,6 +295,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
// Event coordination
|
// Event coordination
|
||||||
reading := true
|
reading := true
|
||||||
ticks := 0
|
ticks := 0
|
||||||
|
startTick := 0
|
||||||
var nextCommand *commandSpec
|
var nextCommand *commandSpec
|
||||||
var nextEnviron []string
|
var nextEnviron []string
|
||||||
eventBox.Watch(EvtReadNew)
|
eventBox.Watch(EvtReadNew)
|
||||||
@@ -249,12 +318,18 @@ func Run(opts *Options) (int, error) {
|
|||||||
var snapshot []*Chunk
|
var snapshot []*Chunk
|
||||||
var count int
|
var count int
|
||||||
restart := func(command commandSpec, environ []string) {
|
restart := func(command commandSpec, environ []string) {
|
||||||
|
if !useSnapshot {
|
||||||
|
clearDenylist()
|
||||||
|
}
|
||||||
reading = true
|
reading = true
|
||||||
|
startTick = ticks
|
||||||
chunkList.Clear()
|
chunkList.Clear()
|
||||||
itemIndex = 0
|
itemIndex = 0
|
||||||
inputRevision++
|
inputRevision.bumpMajor()
|
||||||
header = make([]string, 0, opts.HeaderLines)
|
header = make([]string, 0, opts.HeaderLines)
|
||||||
go reader.restart(command, environ)
|
readyChan := make(chan bool)
|
||||||
|
go reader.restart(command, environ, readyChan)
|
||||||
|
<-readyChan
|
||||||
}
|
}
|
||||||
|
|
||||||
exitCode := ExitOk
|
exitCode := ExitOk
|
||||||
@@ -293,21 +368,23 @@ func Run(opts *Options) (int, error) {
|
|||||||
} else {
|
} else {
|
||||||
reading = reading && evt == EvtReadNew
|
reading = reading && evt == EvtReadNew
|
||||||
}
|
}
|
||||||
if useSnapshot && evt == EvtReadFin {
|
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||||
|
clearDenylist()
|
||||||
useSnapshot = false
|
useSnapshot = false
|
||||||
}
|
}
|
||||||
if !useSnapshot {
|
if !useSnapshot {
|
||||||
if snapshotRevision != inputRevision {
|
if !snapshotRevision.compatible(inputRevision) {
|
||||||
query = []rune{}
|
query = []rune{}
|
||||||
}
|
}
|
||||||
snapshot, count = chunkList.Snapshot()
|
var changed bool
|
||||||
|
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
|
||||||
|
if changed {
|
||||||
|
inputRevision.bumpMinor()
|
||||||
|
}
|
||||||
snapshotRevision = inputRevision
|
snapshotRevision = inputRevision
|
||||||
}
|
}
|
||||||
total = count
|
total = count
|
||||||
terminal.UpdateCount(total, !reading, value.(*string))
|
terminal.UpdateCount(total, !reading, value.(*string))
|
||||||
if opts.Sync {
|
|
||||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision), false)
|
|
||||||
}
|
|
||||||
if heightUnknown && !deferred {
|
if heightUnknown && !deferred {
|
||||||
determine(!reading)
|
determine(!reading)
|
||||||
}
|
}
|
||||||
@@ -323,6 +400,25 @@ func Run(opts *Options) (int, error) {
|
|||||||
command = val.command
|
command = val.command
|
||||||
environ = val.environ
|
environ = val.environ
|
||||||
changed = val.changed
|
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
|
||||||
|
bump = true
|
||||||
|
}
|
||||||
|
if bump {
|
||||||
|
patternCache = make(map[string]*Pattern)
|
||||||
|
cache.Clear()
|
||||||
|
inputRevision.bumpMinor()
|
||||||
|
}
|
||||||
if command != nil {
|
if command != nil {
|
||||||
useSnapshot = val.sync
|
useSnapshot = val.sync
|
||||||
}
|
}
|
||||||
@@ -340,7 +436,10 @@ func Run(opts *Options) (int, error) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !useSnapshot {
|
if !useSnapshot {
|
||||||
newSnapshot, newCount := chunkList.Snapshot()
|
newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
|
||||||
|
if changed {
|
||||||
|
inputRevision.bumpMinor()
|
||||||
|
}
|
||||||
// We want to avoid showing empty list when reload is triggered
|
// We want to avoid showing empty list when reload is triggered
|
||||||
// and the query string is changed at the same time i.e. command != nil && changed
|
// and the query string is changed at the same time i.e. command != nil && changed
|
||||||
if command == nil || newCount > 0 {
|
if command == nil || newCount > 0 {
|
||||||
@@ -380,8 +479,17 @@ func Run(opts *Options) (int, error) {
|
|||||||
if len(opts.Expect) > 0 {
|
if len(opts.Expect) > 0 {
|
||||||
opts.Printer("")
|
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++ {
|
for i := 0; i < count; i++ {
|
||||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
opts.Printer(transformer(val.Get(i).item))
|
||||||
}
|
}
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
exitCode = ExitNoMatch
|
exitCode = ExitNoMatch
|
||||||
@@ -392,7 +500,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
determine(val.final)
|
determine(val.final)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.UpdateList(val, true)
|
terminal.UpdateList(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -403,7 +511,7 @@ func Run(opts *Options) (int, error) {
|
|||||||
}
|
}
|
||||||
if delay && reading {
|
if delay && reading {
|
||||||
dur := util.DurWithin(
|
dur := util.DurWithin(
|
||||||
time.Duration(ticks)*coordinatorDelayStep,
|
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||||
0, coordinatorDelayMax)
|
0, coordinatorDelayMax)
|
||||||
time.Sleep(dur)
|
time.Sleep(dur)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
func writeTemporaryFile(data []string, printSep string) string {
|
func WriteTemporaryFile(data []string, printSep string) string {
|
||||||
f, err := os.CreateTemp("", "fzf-preview-*")
|
f, err := os.CreateTemp("", "fzf-temp-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Unable to create temporary file
|
// Unable to create temporary file
|
||||||
// FIXME: Should we terminate the program?
|
// FIXME: Should we terminate the program?
|
||||||
|
|||||||
19
src/item.go
19
src/item.go
@@ -1,13 +1,22 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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 revision
|
||||||
|
tokens []Token
|
||||||
|
}
|
||||||
|
|
||||||
// Item represents each input line. 56 bytes.
|
// Item represents each input line. 56 bytes.
|
||||||
type Item struct {
|
type Item struct {
|
||||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||||
transformed *[]Token // 8
|
transformed *transformed // 8
|
||||||
origText *[]byte // 8
|
origText *[]byte // 8
|
||||||
colors *[]ansiOffset // 8
|
colors *[]ansiOffset // 8
|
||||||
}
|
}
|
||||||
@@ -17,7 +26,7 @@ func (item *Item) Index() int32 {
|
|||||||
return item.text.Index
|
return item.text.Index
|
||||||
}
|
}
|
||||||
|
|
||||||
var minItem = Item{text: util.Chars{Index: -1}}
|
var minItem = Item{text: util.Chars{Index: math.MinInt32}}
|
||||||
|
|
||||||
func (item *Item) TrimLength() uint16 {
|
func (item *Item) TrimLength() uint16 {
|
||||||
return item.text.TrimLength()
|
return item.text.TrimLength()
|
||||||
@@ -42,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
|
|||||||
}
|
}
|
||||||
return item.text.ToString()
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type MatchRequest struct {
|
|||||||
pattern *Pattern
|
pattern *Pattern
|
||||||
final bool
|
final bool
|
||||||
sort bool
|
sort bool
|
||||||
revision int
|
revision revision
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matcher is responsible for performing search
|
// Matcher is responsible for performing search
|
||||||
@@ -30,7 +30,7 @@ type Matcher struct {
|
|||||||
partitions int
|
partitions int
|
||||||
slab []*util.Slab
|
slab []*util.Slab
|
||||||
mergerCache map[string]*Merger
|
mergerCache map[string]*Merger
|
||||||
revision int
|
revision revision
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -40,7 +40,7 @@ const (
|
|||||||
|
|
||||||
// NewMatcher returns a new Matcher
|
// NewMatcher returns a new Matcher
|
||||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||||
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
|
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
||||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||||
return &Matcher{
|
return &Matcher{
|
||||||
cache: cache,
|
cache: cache,
|
||||||
@@ -82,12 +82,16 @@ func (m *Matcher) Loop() {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cacheCleared := false
|
||||||
if request.sort != m.sort || request.revision != m.revision {
|
if request.sort != m.sort || request.revision != m.revision {
|
||||||
m.sort = request.sort
|
m.sort = request.sort
|
||||||
m.revision = request.revision
|
m.revision = request.revision
|
||||||
m.mergerCache = make(map[string]*Merger)
|
m.mergerCache = make(map[string]*Merger)
|
||||||
|
if !request.revision.compatible(m.revision) {
|
||||||
m.cache.Clear()
|
m.cache.Clear()
|
||||||
}
|
}
|
||||||
|
cacheCleared = true
|
||||||
|
}
|
||||||
|
|
||||||
// Restart search
|
// Restart search
|
||||||
patternString := request.pattern.AsString()
|
patternString := request.pattern.AsString()
|
||||||
@@ -95,11 +99,10 @@ func (m *Matcher) Loop() {
|
|||||||
cancelled := false
|
cancelled := false
|
||||||
count := CountItems(request.chunks)
|
count := CountItems(request.chunks)
|
||||||
|
|
||||||
foundCache := false
|
if !cacheCleared {
|
||||||
if count == prevCount {
|
if count == prevCount {
|
||||||
// Look up mergerCache
|
// Look up mergerCache
|
||||||
if cached, found := m.mergerCache[patternString]; found {
|
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||||
foundCache = true
|
|
||||||
merger = cached
|
merger = cached
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -107,8 +110,9 @@ func (m *Matcher) Loop() {
|
|||||||
prevCount = count
|
prevCount = count
|
||||||
m.mergerCache = make(map[string]*Merger)
|
m.mergerCache = make(map[string]*Merger)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if !foundCache {
|
if merger == nil {
|
||||||
merger, cancelled = m.scan(request)
|
merger, cancelled = m.scan(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +164,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
minIndex := request.chunks[0].items[0].Index()
|
||||||
cancelled := util.NewAtomicBool(false)
|
cancelled := util.NewAtomicBool(false)
|
||||||
|
|
||||||
slices := m.sliceChunks(request.chunks)
|
slices := m.sliceChunks(request.chunks)
|
||||||
@@ -190,7 +195,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
for _, matches := range allMatches {
|
for _, matches := range allMatches {
|
||||||
sliceMatches = append(sliceMatches, matches...)
|
sliceMatches = append(sliceMatches, matches...)
|
||||||
}
|
}
|
||||||
if m.sort {
|
if m.sort && request.pattern.sortable {
|
||||||
if m.tac {
|
if m.tac {
|
||||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||||
} else {
|
} else {
|
||||||
@@ -231,11 +236,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
partialResult := <-resultChan
|
partialResult := <-resultChan
|
||||||
partialResults[partialResult.index] = partialResult.matches
|
partialResults[partialResult.index] = partialResult.matches
|
||||||
}
|
}
|
||||||
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
|
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset is called to interrupt/signal the ongoing search
|
// Reset is called to interrupt/signal the ongoing search
|
||||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
|
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
|
||||||
pattern := m.patternBuilder(patternRunes)
|
pattern := m.patternBuilder(patternRunes)
|
||||||
|
|
||||||
var event util.EventType
|
var event util.EventType
|
||||||
@@ -244,7 +249,7 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
|||||||
} else {
|
} else {
|
||||||
event = reqRetry
|
event = reqRetry
|
||||||
}
|
}
|
||||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
|
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Matcher) Stop() {
|
func (m *Matcher) Stop() {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package fzf
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// EmptyMerger is a Merger with no data
|
// EmptyMerger is a Merger with no data
|
||||||
func EmptyMerger(revision int) *Merger {
|
func EmptyMerger(revision revision) *Merger {
|
||||||
return NewMerger(nil, [][]Result{}, false, false, revision)
|
return NewMerger(nil, [][]Result{}, false, false, revision, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||||
@@ -20,19 +20,25 @@ type Merger struct {
|
|||||||
final bool
|
final bool
|
||||||
count int
|
count int
|
||||||
pass bool
|
pass bool
|
||||||
revision int
|
revision revision
|
||||||
|
minIndex int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassMerger returns a new Merger that simply returns the items in the
|
// PassMerger returns a new Merger that simply returns the items in the
|
||||||
// original order
|
// original order
|
||||||
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||||
|
var minIndex int32
|
||||||
|
if len(*chunks) > 0 {
|
||||||
|
minIndex = (*chunks)[0].items[0].Index()
|
||||||
|
}
|
||||||
mg := Merger{
|
mg := Merger{
|
||||||
pattern: nil,
|
pattern: nil,
|
||||||
chunks: chunks,
|
chunks: chunks,
|
||||||
tac: tac,
|
tac: tac,
|
||||||
count: 0,
|
count: 0,
|
||||||
pass: true,
|
pass: true,
|
||||||
revision: revision}
|
revision: revision,
|
||||||
|
minIndex: minIndex}
|
||||||
|
|
||||||
for _, chunk := range *mg.chunks {
|
for _, chunk := range *mg.chunks {
|
||||||
mg.count += chunk.count
|
mg.count += chunk.count
|
||||||
@@ -41,7 +47,7 @@ func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMerger returns a new Merger
|
// NewMerger returns a new Merger
|
||||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision int) *Merger {
|
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32) *Merger {
|
||||||
mg := Merger{
|
mg := Merger{
|
||||||
pattern: pattern,
|
pattern: pattern,
|
||||||
lists: lists,
|
lists: lists,
|
||||||
@@ -52,7 +58,8 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
|||||||
tac: tac,
|
tac: tac,
|
||||||
final: false,
|
final: false,
|
||||||
count: 0,
|
count: 0,
|
||||||
revision: revision}
|
revision: revision,
|
||||||
|
minIndex: minIndex}
|
||||||
|
|
||||||
for _, list := range mg.lists {
|
for _, list := range mg.lists {
|
||||||
mg.count += len(list)
|
mg.count += len(list)
|
||||||
@@ -61,7 +68,7 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revisi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Revision returns revision number
|
// Revision returns revision number
|
||||||
func (mg *Merger) Revision() int {
|
func (mg *Merger) Revision() revision {
|
||||||
return mg.revision
|
return mg.revision
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +88,7 @@ func (mg *Merger) First() Result {
|
|||||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||||
index := -1
|
index := -1
|
||||||
if mg.pass {
|
if mg.pass {
|
||||||
index = int(itemIndex)
|
index = int(itemIndex - mg.minIndex)
|
||||||
if mg.tac {
|
if mg.tac {
|
||||||
index = mg.count - index - 1
|
index = mg.count - index - 1
|
||||||
}
|
}
|
||||||
@@ -102,6 +109,13 @@ func (mg *Merger) Get(idx int) Result {
|
|||||||
if mg.tac {
|
if mg.tac {
|
||||||
idx = mg.count - idx - 1
|
idx = mg.count - idx - 1
|
||||||
}
|
}
|
||||||
|
firstChunk := (*mg.chunks)[0]
|
||||||
|
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||||
|
idx -= firstChunk.count
|
||||||
|
|
||||||
|
chunk := (*mg.chunks)[idx/chunkSize+1]
|
||||||
|
return Result{item: &chunk.items[idx%chunkSize]}
|
||||||
|
}
|
||||||
chunk := (*mg.chunks)[idx/chunkSize]
|
chunk := (*mg.chunks)[idx/chunkSize]
|
||||||
return Result{item: &chunk.items[idx%chunkSize]}
|
return Result{item: &chunk.items[idx%chunkSize]}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,11 @@ func randResult() Result {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyMerger(t *testing.T) {
|
func TestEmptyMerger(t *testing.T) {
|
||||||
assert(t, EmptyMerger(0).Length() == 0, "Not empty")
|
r := revision{}
|
||||||
assert(t, EmptyMerger(0).count == 0, "Invalid count")
|
assert(t, EmptyMerger(r).Length() == 0, "Not empty")
|
||||||
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
|
assert(t, EmptyMerger(r).count == 0, "Invalid count")
|
||||||
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
|
assert(t, len(EmptyMerger(r).lists) == 0, "Invalid lists")
|
||||||
|
assert(t, len(EmptyMerger(r).merged) == 0, "Invalid merged list")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||||
@@ -57,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
|
|||||||
cnt := len(items)
|
cnt := len(items)
|
||||||
|
|
||||||
// Not sorted: same order
|
// Not sorted: same order
|
||||||
mg := NewMerger(nil, lists, false, false, 0)
|
mg := NewMerger(nil, lists, false, false, revision{}, 0)
|
||||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||||
for i := 0; i < cnt; i++ {
|
for i := 0; i < cnt; i++ {
|
||||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||||
@@ -69,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
|
|||||||
cnt := len(items)
|
cnt := len(items)
|
||||||
|
|
||||||
// Sorted sorted order
|
// Sorted sorted order
|
||||||
mg := NewMerger(nil, lists, true, false, 0)
|
mg := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
for i := 0; i < cnt; i++ {
|
for i := 0; i < cnt; i++ {
|
||||||
@@ -79,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inverse order
|
// Inverse order
|
||||||
mg2 := NewMerger(nil, lists, true, false, 0)
|
mg2 := NewMerger(nil, lists, true, false, revision{}, 0)
|
||||||
for i := cnt - 1; i >= 0; i-- {
|
for i := cnt - 1; i >= 0; i-- {
|
||||||
if items[i] != mg2.Get(i) {
|
if items[i] != mg2.Get(i) {
|
||||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||||
|
|||||||
1721
src/options.go
1721
src/options.go
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestDelimiterRegex(t *testing.T) {
|
func TestDelimiterRegex(t *testing.T) {
|
||||||
// Valid regex
|
// Valid regex, but a single character -> string
|
||||||
delim := delimiterRegexp(".")
|
delim := delimiterRegexp(".")
|
||||||
if delim.regex == nil || delim.str != nil {
|
if delim.regex != nil || *delim.str != "." {
|
||||||
|
t.Error(delim)
|
||||||
|
}
|
||||||
|
delim = delimiterRegexp("|")
|
||||||
|
if delim.regex != nil || *delim.str != "|" {
|
||||||
t.Error(delim)
|
t.Error(delim)
|
||||||
}
|
}
|
||||||
// Broken regex -> string
|
// Broken regex -> string
|
||||||
@@ -106,10 +110,11 @@ func TestSplitNth(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestIrrelevantNth(t *testing.T) {
|
func TestIrrelevantNth(t *testing.T) {
|
||||||
|
index := 0
|
||||||
{
|
{
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
words := []string{"--nth", "..", "-x"}
|
words := []string{"--nth", "..", "-x"}
|
||||||
parseOptions(opts, words)
|
parseOptions(&index, opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if len(opts.Nth) != 0 {
|
if len(opts.Nth) != 0 {
|
||||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||||
@@ -118,7 +123,7 @@ func TestIrrelevantNth(t *testing.T) {
|
|||||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||||
{
|
{
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
parseOptions(opts, words)
|
parseOptions(&index, opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if len(opts.Nth) != 0 {
|
if len(opts.Nth) != 0 {
|
||||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||||
@@ -127,7 +132,7 @@ func TestIrrelevantNth(t *testing.T) {
|
|||||||
{
|
{
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
words = append(words, "-x")
|
words = append(words, "-x")
|
||||||
parseOptions(opts, words)
|
parseOptions(&index, opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if len(opts.Nth) != 2 {
|
if len(opts.Nth) != 2 {
|
||||||
t.Errorf("nth should not be empty: %v", opts.Nth)
|
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||||
@@ -167,7 +172,7 @@ func TestParseKeys(t *testing.T) {
|
|||||||
if len(pairs) != 9 {
|
if len(pairs) != 9 {
|
||||||
t.Error(9)
|
t.Error(9)
|
||||||
}
|
}
|
||||||
check(tui.CtrlM, "Return")
|
check(tui.Enter, "Return")
|
||||||
checkEvent(tui.Key(' '), "space")
|
checkEvent(tui.Key(' '), "space")
|
||||||
check(tui.Tab, "tab")
|
check(tui.Tab, "tab")
|
||||||
check(tui.ShiftTab, "btab")
|
check(tui.ShiftTab, "btab")
|
||||||
@@ -190,7 +195,7 @@ func TestParseKeys(t *testing.T) {
|
|||||||
check(tui.ShiftLeft, "shift-left")
|
check(tui.ShiftLeft, "shift-left")
|
||||||
check(tui.ShiftRight, "shift-right")
|
check(tui.ShiftRight, "shift-right")
|
||||||
check(tui.ShiftTab, "shift-tab")
|
check(tui.ShiftTab, "shift-tab")
|
||||||
check(tui.CtrlM, "Enter")
|
check(tui.Enter, "Enter")
|
||||||
check(tui.Backspace, "bspace")
|
check(tui.Backspace, "bspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,17 +333,18 @@ func TestColorSpec(t *testing.T) {
|
|||||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
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 {
|
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||||
t.Errorf("color not customized")
|
t.Errorf("color not customized")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultCtrlNP(t *testing.T) {
|
func TestDefaultCtrlNP(t *testing.T) {
|
||||||
|
index := 0
|
||||||
check := func(words []string, et tui.EventType, expected actionType) {
|
check := func(words []string, et tui.EventType, expected actionType) {
|
||||||
e := et.AsEvent()
|
e := et.AsEvent()
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
parseOptions(opts, words)
|
parseOptions(&index, opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if opts.Keymap[e][0].t != expected {
|
if opts.Keymap[e][0].t != expected {
|
||||||
t.Error()
|
t.Error()
|
||||||
@@ -364,8 +370,9 @@ func TestDefaultCtrlNP(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func optsFor(words ...string) *Options {
|
func optsFor(words ...string) *Options {
|
||||||
|
index := 0
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
parseOptions(opts, words)
|
parseOptions(&index, opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
@@ -451,7 +458,6 @@ func TestValidateSign(t *testing.T) {
|
|||||||
{"> ", true},
|
{"> ", true},
|
||||||
{"아", true},
|
{"아", true},
|
||||||
{"😀", true},
|
{"😀", true},
|
||||||
{"", false},
|
|
||||||
{">>>", false},
|
{">>>", false},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type termType int
|
|||||||
const (
|
const (
|
||||||
termFuzzy termType = iota
|
termFuzzy termType = iota
|
||||||
termExact
|
termExact
|
||||||
|
termExactBoundary
|
||||||
termPrefix
|
termPrefix
|
||||||
termSuffix
|
termSuffix
|
||||||
termEqual
|
termEqual
|
||||||
@@ -59,8 +60,10 @@ type Pattern struct {
|
|||||||
cacheKey string
|
cacheKey string
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
nth []Range
|
nth []Range
|
||||||
|
revision revision
|
||||||
procFun map[termType]algo.Algo
|
procFun map[termType]algo.Algo
|
||||||
cache *ChunkCache
|
cache *ChunkCache
|
||||||
|
denylist map[int32]struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _splitRegex *regexp.Regexp
|
var _splitRegex *regexp.Regexp
|
||||||
@@ -71,7 +74,7 @@ func init() {
|
|||||||
|
|
||||||
// BuildPattern builds Pattern object from the given arguments
|
// 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,
|
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, runes []rune) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||||
|
|
||||||
var asString string
|
var asString string
|
||||||
if extended {
|
if extended {
|
||||||
@@ -139,14 +142,17 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
|||||||
sortable: sortable,
|
sortable: sortable,
|
||||||
cacheable: cacheable,
|
cacheable: cacheable,
|
||||||
nth: nth,
|
nth: nth,
|
||||||
|
revision: revision,
|
||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
|
denylist: denylist,
|
||||||
procFun: make(map[termType]algo.Algo)}
|
procFun: make(map[termType]algo.Algo)}
|
||||||
|
|
||||||
ptr.cacheKey = ptr.buildCacheKey()
|
ptr.cacheKey = ptr.buildCacheKey()
|
||||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||||
ptr.procFun[termEqual] = algo.EqualMatch
|
ptr.procFun[termEqual] = algo.EqualMatch
|
||||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||||
|
ptr.procFun[termExactBoundary] = algo.ExactMatchBoundary
|
||||||
ptr.procFun[termPrefix] = algo.PrefixMatch
|
ptr.procFun[termPrefix] = algo.PrefixMatch
|
||||||
ptr.procFun[termSuffix] = algo.SuffixMatch
|
ptr.procFun[termSuffix] = algo.SuffixMatch
|
||||||
|
|
||||||
@@ -155,14 +161,14 @@ func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||||
str = strings.Replace(str, "\\ ", "\t", -1)
|
str = strings.ReplaceAll(str, "\\ ", "\t")
|
||||||
tokens := _splitRegex.Split(str, -1)
|
tokens := _splitRegex.Split(str, -1)
|
||||||
sets := []termSet{}
|
sets := []termSet{}
|
||||||
set := termSet{}
|
set := termSet{}
|
||||||
switchSet := false
|
switchSet := false
|
||||||
afterBar := false
|
afterBar := false
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
|
||||||
lowerText := strings.ToLower(text)
|
lowerText := strings.ToLower(text)
|
||||||
caseSensitive := caseMode == CaseRespect ||
|
caseSensitive := caseMode == CaseRespect ||
|
||||||
caseMode == CaseSmart && text != lowerText
|
caseMode == CaseSmart && text != lowerText
|
||||||
@@ -193,7 +199,10 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
text = text[:len(text)-1]
|
text = text[:len(text)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(text, "'") {
|
if len(text) > 2 && strings.HasPrefix(text, "'") && strings.HasSuffix(text, "'") {
|
||||||
|
typ = termExactBoundary
|
||||||
|
text = text[1 : len(text)-1]
|
||||||
|
} else if strings.HasPrefix(text, "'") {
|
||||||
// Flip exactness
|
// Flip exactness
|
||||||
if fuzzy && !inv {
|
if fuzzy && !inv {
|
||||||
typ = termExact
|
typ = termExact
|
||||||
@@ -236,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
|
|
||||||
// IsEmpty returns true if the pattern is effectively empty
|
// IsEmpty returns true if the pattern is effectively empty
|
||||||
func (p *Pattern) IsEmpty() bool {
|
func (p *Pattern) IsEmpty() bool {
|
||||||
|
if len(p.denylist) > 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if !p.extended {
|
if !p.extended {
|
||||||
return len(p.text) == 0
|
return len(p.text) == 0
|
||||||
}
|
}
|
||||||
@@ -289,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 {
|
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||||
matches := []Result{}
|
matches := []Result{}
|
||||||
|
|
||||||
|
if len(p.denylist) == 0 {
|
||||||
|
// Huge code duplication for minimizing unnecessary map lookups
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for idx := 0; idx < chunk.count; idx++ {
|
for idx := 0; idx < chunk.count; idx++ {
|
||||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||||
@@ -303,6 +317,28 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Re
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return matches
|
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
|
// MatchItem returns true if the Item is a match
|
||||||
@@ -388,12 +424,22 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
|||||||
|
|
||||||
func (p *Pattern) transformInput(item *Item) []Token {
|
func (p *Pattern) transformInput(item *Item) []Token {
|
||||||
if item.transformed != nil {
|
if item.transformed != nil {
|
||||||
return *item.transformed
|
transformed := *item.transformed
|
||||||
|
if transformed.revision == p.revision {
|
||||||
|
return transformed.tokens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||||
ret := Transform(tokens, p.nth)
|
ret := Transform(tokens, p.nth)
|
||||||
item.transformed = &ret
|
// 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
|
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 {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||||
withPos, cacheable, nth, delimiter, runes)
|
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExact(t *testing.T) {
|
func TestExact(t *testing.T) {
|
||||||
@@ -135,12 +135,12 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
|||||||
chunk.items[0] = Item{
|
chunk.items[0] = Item{
|
||||||
text: util.ToChars([]byte("junegunn")),
|
text: util.ToChars([]byte("junegunn")),
|
||||||
origText: &origBytes,
|
origText: &origBytes,
|
||||||
transformed: &trans}
|
transformed: &transformed{pattern.revision, trans}}
|
||||||
pattern.extended = extended
|
pattern.extended = extended
|
||||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||||
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
|
reflect.DeepEqual((*matches[0].item.transformed).tokens, trans)) {
|
||||||
t.Error("Invalid match result", matches)
|
t.Error("Invalid match result", matches)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
|||||||
if !(match.item.text.ToString() == "junegunn" &&
|
if !(match.item.text.ToString() == "junegunn" &&
|
||||||
string(*match.item.origText) == "junegunn.choi" &&
|
string(*match.item.origText) == "junegunn.choi" &&
|
||||||
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
||||||
reflect.DeepEqual(*match.item.transformed, trans)) {
|
reflect.DeepEqual((*match.item.transformed).tokens, trans)) {
|
||||||
t.Error("Invalid match result", match, offsets, extended)
|
t.Error("Invalid match result", match, offsets, extended)
|
||||||
}
|
}
|
||||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
|
|||||||
|
|
||||||
// Protect calls OS specific protections like pledge on OpenBSD
|
// Protect calls OS specific protections like pledge on OpenBSD
|
||||||
func Protect() {
|
func Protect() {
|
||||||
unix.PledgePromises("stdio rpath tty proc exec inet tmppath")
|
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath")
|
||||||
}
|
}
|
||||||
|
|||||||
161
src/proxy.go
Normal file
161
src/proxy.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/junegunn/fzf/src/tui"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const becomeSuffix = ".become"
|
||||||
|
|
||||||
|
func escapeSingleQuote(str string) string {
|
||||||
|
return "'" + strings.ReplaceAll(str, "'", "'\\''") + "'"
|
||||||
|
}
|
||||||
|
|
||||||
|
func fifo(name string) (string, error) {
|
||||||
|
ns := time.Now().UnixNano()
|
||||||
|
output := filepath.Join(os.TempDir(), fmt.Sprintf("fzf-%s-%d", name, ns))
|
||||||
|
output, err := mkfifo(output, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return output, err
|
||||||
|
}
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runProxy(commandPrefix string, cmdBuilder func(temp string, needBash bool) (*exec.Cmd, error), opts *Options, withExports bool) (int, error) {
|
||||||
|
output, err := fifo("proxy-output")
|
||||||
|
if err != nil {
|
||||||
|
return ExitError, err
|
||||||
|
}
|
||||||
|
defer os.Remove(output)
|
||||||
|
|
||||||
|
// Take the output
|
||||||
|
go func() {
|
||||||
|
withOutputPipe(output, func(outputFile io.ReadCloser) {
|
||||||
|
if opts.Output == nil {
|
||||||
|
io.Copy(os.Stdout, outputFile)
|
||||||
|
} else {
|
||||||
|
reader := bufio.NewReader(outputFile)
|
||||||
|
sep := opts.PrintSep[0]
|
||||||
|
for {
|
||||||
|
item, err := reader.ReadString(sep)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
opts.Output <- item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
var command, 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")
|
||||||
|
if err != nil {
|
||||||
|
return ExitError, err
|
||||||
|
}
|
||||||
|
defer os.Remove(input)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
withInputPipe(input, func(inputFile io.WriteCloser) {
|
||||||
|
if opts.Input == nil {
|
||||||
|
io.Copy(inputFile, os.Stdin)
|
||||||
|
} else {
|
||||||
|
for item := range opts.Input {
|
||||||
|
fmt.Fprint(inputFile, item+opts.PrintSep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
|
||||||
|
if withExports {
|
||||||
|
command = fmt.Sprintf(`%s < %q > %q`, commandPrefix, input, output)
|
||||||
|
} else {
|
||||||
|
// For mintty: cannot directly read named pipe from Go code
|
||||||
|
command = fmt.Sprintf(`command cat %q | %s > %q`, input, commandPrefix, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// * Write the command to a temporary file and run it with sh to ensure POSIX compliance.
|
||||||
|
// * 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="}
|
||||||
|
needBash := false
|
||||||
|
if withExports {
|
||||||
|
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||||
|
for _, pairStr := range os.Environ() {
|
||||||
|
pair := strings.SplitN(pairStr, "=", 2)
|
||||||
|
if validIdentifier.MatchString(pair[0]) {
|
||||||
|
exports = append(exports, fmt.Sprintf("export %s=%s", pair[0], escapeSingleQuote(pair[1])))
|
||||||
|
} else if strings.HasPrefix(pair[0], "BASH_FUNC_") && strings.HasSuffix(pair[0], "%%") {
|
||||||
|
name := pair[0][10 : len(pair[0])-2]
|
||||||
|
exports = append(exports, name+pair[1])
|
||||||
|
exports = append(exports, "export -f "+name)
|
||||||
|
needBash = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
temp := WriteTemporaryFile(append(exports, command), "\n")
|
||||||
|
defer os.Remove(temp)
|
||||||
|
|
||||||
|
cmd, err := cmdBuilder(temp, needBash)
|
||||||
|
if err != nil {
|
||||||
|
return ExitError, err
|
||||||
|
}
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
intChan := make(chan os.Signal, 1)
|
||||||
|
defer close(intChan)
|
||||||
|
go func() {
|
||||||
|
if sig, valid := <-intChan; valid {
|
||||||
|
cmd.Process.Signal(sig)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
signal.Notify(intChan, os.Interrupt)
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
code := exitError.ExitCode()
|
||||||
|
if code == ExitBecome {
|
||||||
|
becomeFile := temp + becomeSuffix
|
||||||
|
data, err := os.ReadFile(becomeFile)
|
||||||
|
os.Remove(becomeFile)
|
||||||
|
if err != nil {
|
||||||
|
return ExitError, err
|
||||||
|
}
|
||||||
|
elems := strings.Split(string(data), "\x00")
|
||||||
|
if len(elems) < 1 {
|
||||||
|
return ExitError, errors.New("invalid become command")
|
||||||
|
}
|
||||||
|
command := elems[0]
|
||||||
|
env := []string{}
|
||||||
|
if len(elems) > 1 {
|
||||||
|
env = elems[1:]
|
||||||
|
}
|
||||||
|
executor := util.NewExecutor(opts.WithShell)
|
||||||
|
ttyin, err := tui.TtyIn(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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ExitOk, nil
|
||||||
|
}
|
||||||
41
src/proxy_unix.go
Normal file
41
src/proxy_unix.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sh(bash bool) (string, error) {
|
||||||
|
if bash {
|
||||||
|
return "bash", nil
|
||||||
|
}
|
||||||
|
return "sh", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkfifo(path string, mode uint32) (string, error) {
|
||||||
|
return path, unix.Mkfifo(path, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||||
|
outputFile, err := os.OpenFile(output, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task(outputFile)
|
||||||
|
outputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||||
|
inputFile, err := os.OpenFile(input, os.O_WRONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task(inputFile)
|
||||||
|
inputFile.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
85
src/proxy_windows.go
Normal file
85
src/proxy_windows.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
var shPath atomic.Value
|
||||||
|
|
||||||
|
func sh(bash bool) (string, error) {
|
||||||
|
if cached := shPath.Load(); cached != nil {
|
||||||
|
return cached.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "sh"
|
||||||
|
if bash {
|
||||||
|
name = "bash"
|
||||||
|
}
|
||||||
|
cmd := exec.Command("cygpath", "-w", "/usr/bin/"+name)
|
||||||
|
bytes, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sh := strings.TrimSpace(string(bytes))
|
||||||
|
shPath.Store(sh)
|
||||||
|
return sh, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkfifo(path string, mode uint32) (string, error) {
|
||||||
|
m := strconv.FormatUint(uint64(mode), 8)
|
||||||
|
sh, err := sh(false)
|
||||||
|
if err != nil {
|
||||||
|
return path, err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command mkfifo -m %s %q`, m, path))
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return path, err
|
||||||
|
}
|
||||||
|
return path + ".lnk", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func withOutputPipe(output string, task func(io.ReadCloser)) error {
|
||||||
|
sh, err := sh(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat %q`, output))
|
||||||
|
outputFile, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
task(outputFile)
|
||||||
|
cmd.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func withInputPipe(input string, task func(io.WriteCloser)) error {
|
||||||
|
sh, err := sh(false)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd := exec.Command(sh, "-c", fmt.Sprintf(`command cat - > %q`, input))
|
||||||
|
inputFile, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
task(inputFile)
|
||||||
|
inputFile.Close()
|
||||||
|
cmd.Wait()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
168
src/reader.go
168
src/reader.go
@@ -4,9 +4,10 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,15 +25,26 @@ type Reader struct {
|
|||||||
event int32
|
event int32
|
||||||
finChan chan bool
|
finChan chan bool
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
exec *exec.Cmd
|
|
||||||
command *string
|
|
||||||
killed bool
|
killed bool
|
||||||
|
termFunc func()
|
||||||
|
command *string
|
||||||
wait bool
|
wait bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewReader returns new Reader object
|
// NewReader returns new Reader object
|
||||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, executor *util.Executor, delimNil bool, wait bool) *Reader {
|
||||||
return &Reader{pusher, executor, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
|
return &Reader{
|
||||||
|
pusher,
|
||||||
|
executor,
|
||||||
|
eventBox,
|
||||||
|
delimNil,
|
||||||
|
int32(EvtReady),
|
||||||
|
make(chan bool, 1),
|
||||||
|
sync.Mutex{},
|
||||||
|
false,
|
||||||
|
func() { os.Stdin.Close() },
|
||||||
|
nil,
|
||||||
|
wait}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) startEventPoller() {
|
func (r *Reader) startEventPoller() {
|
||||||
@@ -78,18 +90,19 @@ func (r *Reader) fin(success bool) {
|
|||||||
func (r *Reader) terminate() {
|
func (r *Reader) terminate() {
|
||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
r.killed = true
|
r.killed = true
|
||||||
if r.exec != nil && r.exec.Process != nil {
|
if r.termFunc != nil {
|
||||||
util.KillCommand(r.exec)
|
r.termFunc()
|
||||||
} else {
|
r.termFunc = nil
|
||||||
os.Stdin.Close()
|
|
||||||
}
|
}
|
||||||
r.mutex.Unlock()
|
r.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) restart(command commandSpec, environ []string) {
|
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
|
||||||
r.event = int32(EvtReady)
|
r.event = int32(EvtReady)
|
||||||
r.startEventPoller()
|
r.startEventPoller()
|
||||||
success := r.readFromCommand(command.command, environ)
|
success := r.readFromCommand(command.command, environ, func() {
|
||||||
|
readyChan <- true
|
||||||
|
})
|
||||||
r.fin(success)
|
r.fin(success)
|
||||||
removeFiles(command.tempFiles)
|
removeFiles(command.tempFiles)
|
||||||
}
|
}
|
||||||
@@ -108,20 +121,29 @@ func (r *Reader) readChannel(inputChan chan string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReadSource reads data from the default command or from standard input
|
// ReadSource reads data from the default command or from standard input
|
||||||
func (r *Reader) ReadSource(inputChan chan string, root string, opts walkerOpts, ignores []string) {
|
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||||
r.startEventPoller()
|
r.startEventPoller()
|
||||||
var success bool
|
var success bool
|
||||||
|
signalReady := func() {
|
||||||
|
if readyChan != nil {
|
||||||
|
readyChan <- true
|
||||||
|
}
|
||||||
|
}
|
||||||
if inputChan != nil {
|
if inputChan != nil {
|
||||||
|
signalReady()
|
||||||
success = r.readChannel(inputChan)
|
success = r.readChannel(inputChan)
|
||||||
} else if util.IsTty() {
|
} else if len(initCmd) > 0 {
|
||||||
|
success = r.readFromCommand(initCmd, initEnv, signalReady)
|
||||||
|
} else if util.IsTty(os.Stdin) {
|
||||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||||
if len(cmd) == 0 {
|
if len(cmd) == 0 {
|
||||||
success = r.readFiles(root, opts, ignores)
|
signalReady()
|
||||||
|
success = r.readFiles(roots, opts, ignores)
|
||||||
} else {
|
} else {
|
||||||
// We can't export FZF_* environment variables to the default command
|
success = r.readFromCommand(cmd, initEnv, signalReady)
|
||||||
success = r.readFromCommand(cmd, nil)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
signalReady()
|
||||||
success = r.readFromStdin()
|
success = r.readFromStdin()
|
||||||
}
|
}
|
||||||
r.fin(success)
|
r.fin(success)
|
||||||
@@ -220,28 +242,92 @@ func (r *Reader) readFromStdin() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool {
|
func isSymlinkToDir(path string, de os.DirEntry) bool {
|
||||||
r.killed = false
|
if de.Type()&fs.ModeSymlink == 0 {
|
||||||
conf := fastwalk.Config{Follow: opts.follow}
|
return false
|
||||||
|
}
|
||||||
|
if s, err := os.Stat(path); err == nil {
|
||||||
|
return s.IsDir()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimPath(path string) string {
|
||||||
|
bytes := stringBytes(path)
|
||||||
|
|
||||||
|
for len(bytes) > 1 && bytes[0] == '.' && (bytes[1] == '/' || bytes[1] == '\\') {
|
||||||
|
bytes = bytes[2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(bytes) == 0 {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
|
||||||
|
return byteString(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) readFiles(roots []string, opts walkerOpts, ignores []string) bool {
|
||||||
|
conf := fastwalk.Config{
|
||||||
|
Follow: opts.follow,
|
||||||
|
// Use forward slashes when running a Windows binary under WSL or MSYS
|
||||||
|
ToSlash: fastwalk.DefaultToSlash(),
|
||||||
|
Sort: fastwalk.SortFilesFirst,
|
||||||
|
}
|
||||||
|
ignoresBase := []string{}
|
||||||
|
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'
|
||||||
|
// * 'baz/foo/bar'
|
||||||
|
// * but NOT 'bazfoo/bar'
|
||||||
|
ignoresFull = append(ignoresFull, ignore)
|
||||||
|
ignoresSuffix = append(ignoresSuffix, sep+ignore)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ignoresBase = append(ignoresBase, ignore)
|
||||||
|
}
|
||||||
|
}
|
||||||
fn := func(path string, de os.DirEntry, err error) error {
|
fn := func(path string, de os.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
path = filepath.Clean(path)
|
path = trimPath(path)
|
||||||
if path != "." {
|
if path != "." {
|
||||||
isDir := de.IsDir()
|
isDir := de.IsDir()
|
||||||
if isDir {
|
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
||||||
base := filepath.Base(path)
|
base := filepath.Base(path)
|
||||||
if !opts.hidden && base[0] == '.' {
|
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
for _, ignore := range ignores {
|
for _, ignore := range ignoresBase {
|
||||||
if ignore == base {
|
if ignore == base {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, ignore := range ignoresFull {
|
||||||
|
if ignore == path {
|
||||||
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
if ((opts.file && !isDir) || (opts.dir && isDir)) && r.pusher([]byte(path)) {
|
}
|
||||||
|
for _, ignore := range ignoresSuffix {
|
||||||
|
if strings.HasSuffix(path, ignore) {
|
||||||
|
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))
|
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -252,27 +338,39 @@ func (r *Reader) readFiles(root string, opts walkerOpts, ignores []string) bool
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return fastwalk.Walk(&conf, root, fn) == nil
|
noerr := true
|
||||||
|
for _, root := range roots {
|
||||||
|
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
|
||||||
|
}
|
||||||
|
return noerr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readFromCommand(command string, environ []string) bool {
|
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
|
||||||
r.mutex.Lock()
|
r.mutex.Lock()
|
||||||
|
|
||||||
r.killed = false
|
r.killed = false
|
||||||
|
r.termFunc = nil
|
||||||
r.command = &command
|
r.command = &command
|
||||||
r.exec = r.executor.ExecCommand(command, true)
|
exec := r.executor.ExecCommand(command, true)
|
||||||
if environ != nil {
|
if environ != nil {
|
||||||
r.exec.Env = environ
|
exec.Env = environ
|
||||||
}
|
}
|
||||||
out, err := r.exec.StdoutPipe()
|
execOut, err := exec.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil || exec.Start() != nil {
|
||||||
|
signalReady()
|
||||||
r.mutex.Unlock()
|
r.mutex.Unlock()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
err = r.exec.Start()
|
|
||||||
r.mutex.Unlock()
|
// Function to call to terminate the running command
|
||||||
if err != nil {
|
r.termFunc = func() {
|
||||||
return false
|
execOut.Close()
|
||||||
|
util.KillCommand(exec)
|
||||||
}
|
}
|
||||||
r.feed(out)
|
|
||||||
return r.exec.Wait() == nil
|
signalReady()
|
||||||
|
r.mutex.Unlock()
|
||||||
|
|
||||||
|
r.feed(execOut)
|
||||||
|
return exec.Wait() == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normal command
|
// Normal command
|
||||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil))
|
counter := 0
|
||||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
ready := func() {
|
||||||
|
counter++
|
||||||
|
}
|
||||||
|
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
|
||||||
|
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
|
||||||
t.Errorf("%s", strs)
|
t.Errorf("%s", strs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
|
|||||||
reader.startEventPoller()
|
reader.startEventPoller()
|
||||||
|
|
||||||
// Failing command
|
// Failing command
|
||||||
reader.fin(reader.readFromCommand(`no-such-command`, nil))
|
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
|
||||||
strs = []string{}
|
strs = []string{}
|
||||||
if len(strs) > 0 {
|
if len(strs) > 0 || counter != 2 {
|
||||||
t.Errorf("%s", strs)
|
t.Errorf("%s", strs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
103
src/result.go
103
src/result.go
@@ -15,6 +15,8 @@ type Offset [2]int32
|
|||||||
type colorOffset struct {
|
type colorOffset struct {
|
||||||
offset [2]int32
|
offset [2]int32
|
||||||
color tui.ColorPair
|
color tui.ColorPair
|
||||||
|
match bool
|
||||||
|
url *url
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -67,6 +69,21 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
}
|
}
|
||||||
case byLength:
|
case byLength:
|
||||||
val = item.TrimLength()
|
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:
|
case byBegin, byEnd:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
whitePrefixLen := 0
|
whitePrefixLen := 0
|
||||||
@@ -80,7 +97,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
|||||||
if criterion == byBegin {
|
if criterion == byBegin {
|
||||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||||
} else {
|
} else {
|
||||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()+1))
|
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,21 +119,21 @@ func minRank() Result {
|
|||||||
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset {
|
func (result *Result) colorOffsets(matchOffsets []Offset, nthOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, attrNth tui.Attr) []colorOffset {
|
||||||
itemColors := result.item.Colors()
|
itemColors := result.item.Colors()
|
||||||
|
|
||||||
// No ANSI codes
|
// No ANSI codes
|
||||||
if len(itemColors) == 0 {
|
if len(itemColors) == 0 && len(nthOffsets) == 0 {
|
||||||
var offsets []colorOffset
|
var offsets []colorOffset
|
||||||
for _, off := range matchOffsets {
|
for _, off := range matchOffsets {
|
||||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true})
|
||||||
}
|
}
|
||||||
return offsets
|
return offsets
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find max column
|
// Find max column
|
||||||
var maxCol int32
|
var maxCol int32
|
||||||
for _, off := range matchOffsets {
|
for _, off := range append(matchOffsets, nthOffsets...) {
|
||||||
if off[1] > maxCol {
|
if off[1] > maxCol {
|
||||||
maxCol = off[1]
|
maxCol = off[1]
|
||||||
}
|
}
|
||||||
@@ -127,21 +144,30 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cols := make([]int, maxCol)
|
type cellInfo struct {
|
||||||
|
index int
|
||||||
|
color bool
|
||||||
|
match bool
|
||||||
|
nth bool
|
||||||
|
}
|
||||||
|
|
||||||
|
cols := make([]cellInfo, maxCol)
|
||||||
for colorIndex, ansi := range itemColors {
|
for colorIndex, ansi := range itemColors {
|
||||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||||
cols[i] = colorIndex + 1 // 1-based index of itemColors
|
cols[i] = cellInfo{colorIndex, true, false, false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, off := range matchOffsets {
|
for _, off := range matchOffsets {
|
||||||
for i := off[0]; i < off[1]; i++ {
|
for i := off[0]; i < off[1]; i++ {
|
||||||
// Negative of 1-based index of itemColors
|
cols[i].match = true
|
||||||
// - The extra -1 means highlighted
|
|
||||||
if cols[i] >= 0 {
|
|
||||||
cols[i] = cols[i]*-1 - 1
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, off := range nthOffsets {
|
||||||
|
for i := off[0]; i < off[1]; i++ {
|
||||||
|
cols[i].nth = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sort.Sort(ByOrder(offsets))
|
// sort.Sort(ByOrder(offsets))
|
||||||
@@ -150,34 +176,34 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
|||||||
// ------------ ---- -- ----
|
// ------------ ---- -- ----
|
||||||
// ++++++++ ++++++++++
|
// ++++++++ ++++++++++
|
||||||
// --++++++++-- --++++++++++---
|
// --++++++++-- --++++++++++---
|
||||||
curr := 0
|
var curr cellInfo = cellInfo{0, false, false, false}
|
||||||
start := 0
|
start := 0
|
||||||
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||||
fg := ansi.color.fg
|
fg := ansi.color.fg
|
||||||
bg := ansi.color.bg
|
bg := ansi.color.bg
|
||||||
if fg == -1 {
|
if fg == -1 {
|
||||||
if current {
|
fg = colBase.Fg()
|
||||||
fg = theme.Current.Color
|
|
||||||
} else {
|
|
||||||
fg = theme.Fg.Color
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if bg == -1 {
|
if bg == -1 {
|
||||||
if current {
|
bg = colBase.Bg()
|
||||||
bg = theme.DarkBg.Color
|
|
||||||
} else {
|
|
||||||
bg = theme.Bg.Color
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
||||||
}
|
}
|
||||||
var colors []colorOffset
|
var colors []colorOffset
|
||||||
add := func(idx int) {
|
add := func(idx int) {
|
||||||
if curr != 0 && idx > start {
|
if (curr.color || curr.nth || curr.match) && idx > start {
|
||||||
if curr < 0 {
|
if curr.match {
|
||||||
color := colMatch
|
var color tui.ColorPair
|
||||||
if curr < -1 && theme.Colored {
|
if curr.nth {
|
||||||
origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
|
color = colBase.WithAttr(attrNth).Merge(colMatch)
|
||||||
|
} else {
|
||||||
|
color = colBase.Merge(colMatch)
|
||||||
|
}
|
||||||
|
var url *url
|
||||||
|
if curr.color && theme.Colored {
|
||||||
|
ansi := itemColors[curr.index]
|
||||||
|
url = ansi.color.url
|
||||||
|
origColor := ansiToColorPair(ansi, colMatch)
|
||||||
// hl or hl+ only sets the foreground color, so colMatch is the
|
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||||
// combination of either [hl and bg] or [hl+ and bg+].
|
// combination of either [hl and bg] or [hl+ and bg+].
|
||||||
//
|
//
|
||||||
@@ -188,17 +214,32 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
|||||||
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||||
color = origColor
|
color = origColor
|
||||||
|
if curr.nth {
|
||||||
|
color = color.WithAttr(attrNth)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
color = origColor.MergeNonDefault(color)
|
color = origColor.MergeNonDefault(color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
colors = append(colors, colorOffset{
|
colors = append(colors, colorOffset{
|
||||||
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||||
} else {
|
} else if curr.color {
|
||||||
ansi := itemColors[curr-1]
|
ansi := itemColors[curr.index]
|
||||||
|
color := ansiToColorPair(ansi, colBase)
|
||||||
|
if curr.nth {
|
||||||
|
color = color.WithAttr(attrNth)
|
||||||
|
}
|
||||||
colors = append(colors, colorOffset{
|
colors = append(colors, colorOffset{
|
||||||
offset: [2]int32{int32(start), int32(idx)},
|
offset: [2]int32{int32(start), int32(idx)},
|
||||||
color: ansiToColorPair(ansi, colBase)})
|
color: color,
|
||||||
|
match: false,
|
||||||
|
url: ansi.color.url})
|
||||||
|
} else {
|
||||||
|
colors = append(colors, colorOffset{
|
||||||
|
offset: [2]int32{int32(start), int32(idx)},
|
||||||
|
color: colBase.WithAttr(attrNth),
|
||||||
|
match: false,
|
||||||
|
url: nil})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,14 +124,14 @@ func TestColorOffset(t *testing.T) {
|
|||||||
item := Result{
|
item := Result{
|
||||||
item: &Item{
|
item: &Item{
|
||||||
colors: &[]ansiOffset{
|
colors: &[]ansiOffset{
|
||||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
|
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
|
||||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
|
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
|
||||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
|
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
|
||||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
|
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
|
||||||
|
|
||||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||||
colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true)
|
colors := item.colorOffsets(offsets, nil, tui.Dark256, colBase, colMatch, tui.AttrUndefined)
|
||||||
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||||
o := colors[idx]
|
o := colors[idx]
|
||||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||||
@@ -155,12 +155,15 @@ func TestColorOffset(t *testing.T) {
|
|||||||
|
|
||||||
colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
|
colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
|
||||||
colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
|
colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
|
||||||
colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true)
|
|
||||||
|
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)
|
||||||
|
|
||||||
// [{[0 5] {1 5 0}} {[5 15] {1 5 8}} {[15 20] {1 5 0}}
|
// [{[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}}
|
// {[22 25] {2 6 1}} {[25 27] {2 6 9}} {[27 30] {-1 -1 8}}
|
||||||
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
|
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
|
||||||
// {[35 40] {4 8 1}}]
|
// {[35 37] {4 8 1}} {[37 39] {4 8 x|1}} {[39 40] {4 8 x|1}}]
|
||||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
@@ -170,5 +173,12 @@ func TestColorOffset(t *testing.T) {
|
|||||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||||
assert(7, 32, 33, colUnderline)
|
assert(7, 32, 33, colUnderline)
|
||||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||||
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
|
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
|
expected := tui.Bold | attr
|
||||||
|
if attr == tui.AttrRegular {
|
||||||
|
expected = tui.AttrRegular
|
||||||
|
}
|
||||||
|
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||||
|
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ const (
|
|||||||
type httpServer struct {
|
type httpServer struct {
|
||||||
apiKey []byte
|
apiKey []byte
|
||||||
actionChannel chan []*action
|
actionChannel chan []*action
|
||||||
responseChannel chan string
|
getHandler func(getParams) string
|
||||||
}
|
}
|
||||||
|
|
||||||
type listenAddress struct {
|
type listenAddress struct {
|
||||||
@@ -73,7 +73,7 @@ func parseListenAddress(address string) (listenAddress, error) {
|
|||||||
return listenAddress{parts[0], port}, nil
|
return listenAddress{parts[0], port}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (net.Listener, int, error) {
|
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||||
host := address.host
|
host := address.host
|
||||||
port := address.port
|
port := address.port
|
||||||
apiKey := os.Getenv("FZF_API_KEY")
|
apiKey := os.Getenv("FZF_API_KEY")
|
||||||
@@ -101,7 +101,7 @@ func startHttpServer(address listenAddress, actionChannel chan []*action, respon
|
|||||||
server := httpServer{
|
server := httpServer{
|
||||||
apiKey: []byte(apiKey),
|
apiKey: []byte(apiKey),
|
||||||
actionChannel: actionChannel,
|
actionChannel: actionChannel,
|
||||||
responseChannel: responseChannel,
|
getHandler: getHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -165,17 +165,11 @@ func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
|||||||
case 0:
|
case 0:
|
||||||
getMatch := getRegex.FindStringSubmatch(text)
|
getMatch := getRegex.FindStringSubmatch(text)
|
||||||
if len(getMatch) > 0 {
|
if len(getMatch) > 0 {
|
||||||
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
|
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||||
select {
|
if len(response) > 0 {
|
||||||
case response := <-server.responseChannel:
|
|
||||||
return good(response)
|
return good(response)
|
||||||
case <-time.After(channelTimeout):
|
|
||||||
go func() {
|
|
||||||
// Drain the channel
|
|
||||||
<-server.responseChannel
|
|
||||||
}()
|
|
||||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
|
||||||
}
|
}
|
||||||
|
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||||
return bad("invalid request method")
|
return bad("invalid request method")
|
||||||
}
|
}
|
||||||
|
|||||||
3100
src/terminal.go
3100
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -75,6 +75,14 @@ func TestReplacePlaceholder(t *testing.T) {
|
|||||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
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
|
// {}, with multiple items
|
||||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||||
@@ -485,6 +493,11 @@ func TestParsePlaceholder(t *testing.T) {
|
|||||||
// query flag is not removed after parsing, so it gets doubled
|
// query flag is not removed after parsing, so it gets doubled
|
||||||
// while the double q is invalid, it is useful here for testing purposes
|
// while the double q is invalid, it is useful here for testing purposes
|
||||||
`{q}`: `{qq}`,
|
`{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
|
// IV. escaping placeholder
|
||||||
`\{}`: `{}`,
|
`\{}`: `{}`,
|
||||||
@@ -507,6 +520,34 @@ func TestParsePlaceholder(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractPassthroughs(t *testing.T) {
|
||||||
|
for _, middle := range []string{
|
||||||
|
"\x1bPtmux;\x1b\x1bbar\x1b\\",
|
||||||
|
"\x1bPtmux;\x1b\x1bbar\x1bbar\x1b\\",
|
||||||
|
"\x1b]1337;bar\x1b\\",
|
||||||
|
"\x1b]1337;bar\x1bbar\x1b\\",
|
||||||
|
"\x1b]1337;bar\a",
|
||||||
|
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\",
|
||||||
|
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1b\\\r",
|
||||||
|
"\x1b_Ga=T,f=32,s=1258,v=1295,c=74,r=35,m=1\x1bbar\x1b\\\r",
|
||||||
|
"\x1b_Gm=1;AAAAAAAAA=\x1b\\",
|
||||||
|
"\x1b_Gm=1;AAAAAAAAA=\x1b\\\r",
|
||||||
|
"\x1b_Gm=1;\x1bAAAAAAAAA=\x1b\\\r",
|
||||||
|
} {
|
||||||
|
line := "foo" + middle + "baz"
|
||||||
|
loc := findPassThrough(line)
|
||||||
|
if loc == nil || line[0:loc[0]] != "foo" || line[loc[1]:] != "baz" {
|
||||||
|
t.Error("failed to find passthrough")
|
||||||
|
}
|
||||||
|
garbage := "\x1bPtmux;\x1b]1337;\x1b_Ga=\x1b]1337;bar\x1b."
|
||||||
|
line = strings.Repeat("foo"+middle+middle+"baz", 3) + garbage
|
||||||
|
passthroughs, result := extractPassThroughs(line)
|
||||||
|
if result != "foobazfoobazfoobaz"+garbage || len(passthroughs) != 6 {
|
||||||
|
t.Error("failed to extract passthroughs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* utilities section */
|
/* utilities section */
|
||||||
|
|
||||||
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||||
@@ -532,7 +573,7 @@ func (item *Item) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
// 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{}
|
bb := &bytes.Buffer{}
|
||||||
|
|
||||||
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||||
|
|||||||
@@ -20,9 +20,5 @@ func notifyStop(p *os.Process) {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
pid = pgid * -1
|
pid = pgid * -1
|
||||||
}
|
}
|
||||||
unix.Kill(pid, syscall.SIGSTOP)
|
unix.Kill(pid, syscall.SIGTSTP)
|
||||||
}
|
|
||||||
|
|
||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
|
||||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,3 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
|
|||||||
func notifyStop(p *os.Process) {
|
func notifyStop(p *os.Process) {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
|
||||||
// NOOP
|
|
||||||
}
|
|
||||||
|
|||||||
68
src/tmux.go
Normal file
68
src/tmux.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
|
"github.com/junegunn/fzf/src/tui"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 {
|
||||||
|
// We append --border option at the end, because `--style=full:STYLE`
|
||||||
|
// may have changed the default border style.
|
||||||
|
rest = append(rest, "--border")
|
||||||
|
}
|
||||||
|
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||||
|
args = append(args, "--margin=0,1")
|
||||||
|
}
|
||||||
|
argStr := escapeSingleQuote(fzf)
|
||||||
|
for _, arg := range append(args, rest...) {
|
||||||
|
argStr += " " + escapeSingleQuote(arg)
|
||||||
|
}
|
||||||
|
argStr += ` --no-tmux --no-height`
|
||||||
|
|
||||||
|
// Get current directory
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set tmux options for popup placement
|
||||||
|
// C Both The centre of the terminal
|
||||||
|
// R -x The right side of the terminal
|
||||||
|
// P Both The bottom left of the pane
|
||||||
|
// M Both The mouse position
|
||||||
|
// W Both The window position on the status line
|
||||||
|
// S -y The line above or below the status line
|
||||||
|
tmuxArgs := []string{"display-popup", "-E", "-d", dir}
|
||||||
|
if !opts.Tmux.border {
|
||||||
|
tmuxArgs = append(tmuxArgs, "-B")
|
||||||
|
}
|
||||||
|
switch opts.Tmux.position {
|
||||||
|
case posUp:
|
||||||
|
tmuxArgs = append(tmuxArgs, "-xC", "-y0")
|
||||||
|
case posDown:
|
||||||
|
tmuxArgs = append(tmuxArgs, "-xC", "-y9999")
|
||||||
|
case posLeft:
|
||||||
|
tmuxArgs = append(tmuxArgs, "-x0", "-yC")
|
||||||
|
case posRight:
|
||||||
|
tmuxArgs = append(tmuxArgs, "-xR", "-yC")
|
||||||
|
case posCenter:
|
||||||
|
tmuxArgs = append(tmuxArgs, "-xC", "-yC")
|
||||||
|
}
|
||||||
|
tmuxArgs = append(tmuxArgs, "-w"+opts.Tmux.width.String())
|
||||||
|
tmuxArgs = append(tmuxArgs, "-h"+opts.Tmux.height.String())
|
||||||
|
|
||||||
|
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||||
|
sh, err := sh(needBash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tmuxArgs = append(tmuxArgs, sh, temp)
|
||||||
|
return exec.Command("tmux", tmuxArgs...), nil
|
||||||
|
}, opts, true)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
@@ -18,6 +19,48 @@ type Range struct {
|
|||||||
end int
|
end int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
s := ""
|
||||||
|
if r.begin == rangeEllipsis && r.end == rangeEllipsis {
|
||||||
|
s = ".."
|
||||||
|
} else if r.begin == r.end {
|
||||||
|
s = strconv.Itoa(r.begin)
|
||||||
|
} else {
|
||||||
|
if r.begin != rangeEllipsis {
|
||||||
|
s += strconv.Itoa(r.begin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.begin != -1 {
|
||||||
|
s += ".."
|
||||||
|
if r.end != rangeEllipsis {
|
||||||
|
s += strconv.Itoa(r.end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strs = append(strs, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(strs, ",")
|
||||||
|
}
|
||||||
|
|
||||||
// Token contains the tokenized part of the strings and its prefix length
|
// Token contains the tokenized part of the strings and its prefix length
|
||||||
type Token struct {
|
type Token struct {
|
||||||
text *util.Chars
|
text *util.Chars
|
||||||
@@ -35,13 +78,18 @@ type Delimiter struct {
|
|||||||
str *string
|
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.
|
// String returns the string representation of a Delimiter.
|
||||||
func (d Delimiter) String() string {
|
func (d Delimiter) String() string {
|
||||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRange(begin int, end int) Range {
|
func newRange(begin int, end int) Range {
|
||||||
if begin == 1 {
|
if begin == 1 && end != 1 {
|
||||||
begin = rangeEllipsis
|
begin = rangeEllipsis
|
||||||
}
|
}
|
||||||
if end == -1 {
|
if end == -1 {
|
||||||
@@ -73,7 +121,7 @@ func ParseRange(str *string) (Range, bool) {
|
|||||||
}
|
}
|
||||||
begin, err1 := strconv.Atoi(ns[0])
|
begin, err1 := strconv.Atoi(ns[0])
|
||||||
end, err2 := strconv.Atoi(ns[1])
|
end, err2 := strconv.Atoi(ns[1])
|
||||||
if err1 != nil || err2 != nil || begin == 0 || end == 0 {
|
if err1 != nil || err2 != nil || begin == 0 || end == 0 || begin < 0 && end > 0 {
|
||||||
return Range{}, false
|
return Range{}, false
|
||||||
}
|
}
|
||||||
return newRange(begin, end), true
|
return newRange(begin, end), true
|
||||||
@@ -169,7 +217,24 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
|||||||
return withPrefixLengths(tokens, 0)
|
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
|
var output bytes.Buffer
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
output.WriteString(token.text.ToString())
|
output.WriteString(token.text.ToString())
|
||||||
@@ -187,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
|||||||
if r.begin == r.end {
|
if r.begin == r.end {
|
||||||
idx := r.begin
|
idx := r.begin
|
||||||
if idx == rangeEllipsis {
|
if idx == rangeEllipsis {
|
||||||
chars := util.ToChars(stringBytes(joinTokens(tokens)))
|
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||||
parts = append(parts, &chars)
|
parts = append(parts, &chars)
|
||||||
} else {
|
} else {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
|
|||||||
@@ -40,6 +40,18 @@ func TestParseRange(t *testing.T) {
|
|||||||
t.Errorf("%v", r)
|
t.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
i := "1..3..5"
|
||||||
|
if r, ok := ParseRange(&i); ok {
|
||||||
|
t.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
i := "-3..3"
|
||||||
|
if r, ok := ParseRange(&i); ok {
|
||||||
|
t.Errorf("%v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTokenize(t *testing.T) {
|
func TestTokenize(t *testing.T) {
|
||||||
@@ -73,14 +85,14 @@ func TestTransform(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ranges, _ := splitNth("1,2,3")
|
ranges, _ := splitNth("1,2,3")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if joinTokens(tx) != "abc: def: ghi: " {
|
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||||
t.Errorf("%s", tx)
|
t.Errorf("%s", tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ranges, _ := splitNth("1..2,3,2..,1")
|
ranges, _ := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
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 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||||
@@ -95,7 +107,7 @@ func TestTransform(t *testing.T) {
|
|||||||
{
|
{
|
||||||
ranges, _ := splitNth("1..2,3,2..,1")
|
ranges, _ := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
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 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ func HasFullscreenRenderer() bool {
|
|||||||
var DefaultBorderShape = BorderRounded
|
var DefaultBorderShape = BorderRounded
|
||||||
|
|
||||||
func (a Attr) Merge(b Attr) Attr {
|
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
|
return a | b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,6 +23,7 @@ const (
|
|||||||
AttrUndefined = Attr(0)
|
AttrUndefined = Attr(0)
|
||||||
AttrRegular = Attr(1 << 8)
|
AttrRegular = Attr(1 << 8)
|
||||||
AttrClear = Attr(1 << 9)
|
AttrClear = Attr(1 << 9)
|
||||||
|
BoldForce = Attr(1 << 10)
|
||||||
|
|
||||||
Bold = Attr(1)
|
Bold = Attr(1)
|
||||||
Dim = Attr(1 << 1)
|
Dim = Attr(1 << 1)
|
||||||
@@ -30,6 +36,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Init() error { return nil }
|
func (r *FullscreenRenderer) Init() error { return nil }
|
||||||
|
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme { return nil }
|
||||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||||
func (r *FullscreenRenderer) Pause(bool) {}
|
func (r *FullscreenRenderer) Pause(bool) {}
|
||||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||||
@@ -37,6 +44,9 @@ func (r *FullscreenRenderer) PassThrough(string) {}
|
|||||||
func (r *FullscreenRenderer) Clear() {}
|
func (r *FullscreenRenderer) Clear() {}
|
||||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() 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) Refresh() {}
|
||||||
func (r *FullscreenRenderer) Close() {}
|
func (r *FullscreenRenderer) Close() {}
|
||||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||||
@@ -48,6 +58,6 @@ func (r *FullscreenRenderer) MaxY() int { return 0 }
|
|||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func _() {
|
|||||||
_ = x[CtrlJ-10]
|
_ = x[CtrlJ-10]
|
||||||
_ = x[CtrlK-11]
|
_ = x[CtrlK-11]
|
||||||
_ = x[CtrlL-12]
|
_ = x[CtrlL-12]
|
||||||
_ = x[CtrlM-13]
|
_ = x[Enter-13]
|
||||||
_ = x[CtrlN-14]
|
_ = x[CtrlN-14]
|
||||||
_ = x[CtrlO-15]
|
_ = x[CtrlO-15]
|
||||||
_ = x[CtrlP-16]
|
_ = x[CtrlP-16]
|
||||||
@@ -84,34 +84,37 @@ func _() {
|
|||||||
_ = x[CtrlAlt-73]
|
_ = x[CtrlAlt-73]
|
||||||
_ = x[Invalid-74]
|
_ = x[Invalid-74]
|
||||||
_ = x[Fatal-75]
|
_ = x[Fatal-75]
|
||||||
_ = x[Mouse-76]
|
_ = x[BracketedPasteBegin-76]
|
||||||
_ = x[DoubleClick-77]
|
_ = x[BracketedPasteEnd-77]
|
||||||
_ = x[LeftClick-78]
|
_ = x[Mouse-78]
|
||||||
_ = x[RightClick-79]
|
_ = x[DoubleClick-79]
|
||||||
_ = x[SLeftClick-80]
|
_ = x[LeftClick-80]
|
||||||
_ = x[SRightClick-81]
|
_ = x[RightClick-81]
|
||||||
_ = x[ScrollUp-82]
|
_ = x[SLeftClick-82]
|
||||||
_ = x[ScrollDown-83]
|
_ = x[SRightClick-83]
|
||||||
_ = x[SScrollUp-84]
|
_ = x[ScrollUp-84]
|
||||||
_ = x[SScrollDown-85]
|
_ = x[ScrollDown-85]
|
||||||
_ = x[PreviewScrollUp-86]
|
_ = x[SScrollUp-86]
|
||||||
_ = x[PreviewScrollDown-87]
|
_ = x[SScrollDown-87]
|
||||||
_ = x[Resize-88]
|
_ = x[PreviewScrollUp-88]
|
||||||
_ = x[Change-89]
|
_ = x[PreviewScrollDown-89]
|
||||||
_ = x[BackwardEOF-90]
|
_ = x[Resize-90]
|
||||||
_ = x[Start-91]
|
_ = x[Change-91]
|
||||||
_ = x[Load-92]
|
_ = x[BackwardEOF-92]
|
||||||
_ = x[Focus-93]
|
_ = x[Start-93]
|
||||||
_ = x[One-94]
|
_ = x[Load-94]
|
||||||
_ = x[Zero-95]
|
_ = x[Focus-95]
|
||||||
_ = x[Result-96]
|
_ = x[One-96]
|
||||||
_ = x[Jump-97]
|
_ = x[Zero-97]
|
||||||
_ = x[JumpCancel-98]
|
_ = x[Result-98]
|
||||||
|
_ = x[Jump-99]
|
||||||
|
_ = x[JumpCancel-100]
|
||||||
|
_ = x[ClickHeader-101]
|
||||||
}
|
}
|
||||||
|
|
||||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLCtrlMCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancel"
|
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlDeleteCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltCtrlAltInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeader"
|
||||||
|
|
||||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 452, 463, 472, 482, 492, 503, 511, 521, 530, 541, 556, 573, 579, 585, 596, 601, 605, 610, 613, 617, 623, 627, 637}
|
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 154, 167, 183, 192, 201, 209, 218, 224, 230, 238, 240, 244, 248, 253, 257, 260, 266, 273, 282, 291, 301, 312, 314, 316, 318, 320, 322, 324, 326, 328, 330, 333, 336, 339, 351, 356, 363, 370, 378, 388, 400, 412, 425, 428, 435, 442, 447, 466, 483, 488, 499, 508, 518, 528, 539, 547, 557, 566, 577, 592, 609, 615, 621, 632, 637, 641, 646, 649, 653, 659, 663, 673, 684}
|
||||||
|
|
||||||
func (i EventType) String() string {
|
func (i EventType) String() string {
|
||||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||||
|
|||||||
226
src/tui/light.go
226
src/tui/light.go
@@ -8,6 +8,7 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -27,11 +28,15 @@ const (
|
|||||||
maxInputBuffer = 1024 * 1024
|
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")
|
var offsetRegexpBegin = regexp.MustCompile("^\x1b\\[[0-9]+;[0-9]+R")
|
||||||
|
|
||||||
|
func (r *LightRenderer) Bell() {
|
||||||
|
r.flushRaw("\a")
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) PassThrough(str string) {
|
func (r *LightRenderer) PassThrough(str string) {
|
||||||
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
r.queued.WriteString("\x1b7" + str + "\x1b8")
|
||||||
}
|
}
|
||||||
@@ -40,8 +45,9 @@ func (r *LightRenderer) stderr(str string) {
|
|||||||
r.stderrInternal(str, true, "")
|
r.stderrInternal(str, true, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
const CR string = "\x1b[2m␍"
|
const DIM string = "\x1b[2m"
|
||||||
const LF string = "\x1b[2m␊"
|
const CR string = DIM + "␍"
|
||||||
|
const LF string = DIM + "␊"
|
||||||
|
|
||||||
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
func (r *LightRenderer) stderrInternal(str string, allowNLCR bool, resetCode string) {
|
||||||
bytes := []byte(str)
|
bytes := []byte(str)
|
||||||
@@ -73,14 +79,23 @@ func (r *LightRenderer) csi(code string) string {
|
|||||||
|
|
||||||
func (r *LightRenderer) flush() {
|
func (r *LightRenderer) flush() {
|
||||||
if r.queued.Len() > 0 {
|
if r.queued.Len() > 0 {
|
||||||
fmt.Fprint(os.Stderr, "\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()
|
r.queued.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) flushRaw(sequence string) {
|
||||||
|
fmt.Fprint(r.ttyout, sequence)
|
||||||
|
}
|
||||||
|
|
||||||
// Light renderer
|
// Light renderer
|
||||||
type LightRenderer struct {
|
type LightRenderer struct {
|
||||||
closed *util.AtomicBool
|
|
||||||
theme *ColorTheme
|
theme *ColorTheme
|
||||||
mouse bool
|
mouse bool
|
||||||
forceBlack bool
|
forceBlack bool
|
||||||
@@ -88,6 +103,7 @@ type LightRenderer struct {
|
|||||||
prevDownTime time.Time
|
prevDownTime time.Time
|
||||||
clicks [][2]int
|
clicks [][2]int
|
||||||
ttyin *os.File
|
ttyin *os.File
|
||||||
|
ttyout *os.File
|
||||||
buffer []byte
|
buffer []byte
|
||||||
origState *term.State
|
origState *term.State
|
||||||
width int
|
width int
|
||||||
@@ -101,8 +117,10 @@ type LightRenderer struct {
|
|||||||
y int
|
y int
|
||||||
x int
|
x int
|
||||||
maxHeightFunc func(int) int
|
maxHeightFunc func(int) int
|
||||||
|
showCursor bool
|
||||||
|
|
||||||
// Windows only
|
// Windows only
|
||||||
|
mutex sync.Mutex
|
||||||
ttyinChannel chan byte
|
ttyinChannel chan byte
|
||||||
inHandle uintptr
|
inHandle uintptr
|
||||||
outHandle uintptr
|
outHandle uintptr
|
||||||
@@ -113,7 +131,7 @@ type LightRenderer struct {
|
|||||||
type LightWindow struct {
|
type LightWindow struct {
|
||||||
renderer *LightRenderer
|
renderer *LightRenderer
|
||||||
colored bool
|
colored bool
|
||||||
preview bool
|
windowType WindowType
|
||||||
border BorderStyle
|
border BorderStyle
|
||||||
top int
|
top int
|
||||||
left int
|
left int
|
||||||
@@ -124,25 +142,28 @@ type LightWindow struct {
|
|||||||
tabstop int
|
tabstop int
|
||||||
fg Color
|
fg Color
|
||||||
bg Color
|
bg Color
|
||||||
|
wrapSign string
|
||||||
|
wrapSignWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewLightRenderer(theme *ColorTheme, forceBlack bool, mouse bool, tabstop int, clearOnExit bool, fullscreen bool, maxHeightFunc func(int) int) (Renderer, error) {
|
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) {
|
||||||
in, err := openTtyIn()
|
out, err := openTtyOut(ttyDefault)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
out = os.Stderr
|
||||||
}
|
}
|
||||||
r := LightRenderer{
|
r := LightRenderer{
|
||||||
closed: util.NewAtomicBool(false),
|
|
||||||
theme: theme,
|
theme: theme,
|
||||||
forceBlack: forceBlack,
|
forceBlack: forceBlack,
|
||||||
mouse: mouse,
|
mouse: mouse,
|
||||||
clearOnExit: clearOnExit,
|
clearOnExit: clearOnExit,
|
||||||
ttyin: in,
|
ttyin: ttyin,
|
||||||
|
ttyout: out,
|
||||||
yoffset: 0,
|
yoffset: 0,
|
||||||
tabstop: tabstop,
|
tabstop: tabstop,
|
||||||
fullscreen: fullscreen,
|
fullscreen: fullscreen,
|
||||||
upOneLine: false,
|
upOneLine: false,
|
||||||
maxHeightFunc: maxHeightFunc}
|
maxHeightFunc: maxHeightFunc,
|
||||||
|
showCursor: true}
|
||||||
return &r, nil
|
return &r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +189,6 @@ func (r *LightRenderer) Init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
r.updateTerminalSize()
|
r.updateTerminalSize()
|
||||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
|
||||||
|
|
||||||
if r.fullscreen {
|
if r.fullscreen {
|
||||||
r.smcup()
|
r.smcup()
|
||||||
@@ -193,7 +213,7 @@ func (r *LightRenderer) Init() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
r.enableMouse()
|
r.enableModes()
|
||||||
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
r.csi(fmt.Sprintf("%dA", r.MaxY()-1))
|
||||||
r.csi("G")
|
r.csi("G")
|
||||||
r.csi("K")
|
r.csi("K")
|
||||||
@@ -251,7 +271,7 @@ func (r *LightRenderer) getBytesInternal(buffer []byte, nonblock bool) ([]byte,
|
|||||||
c, ok := r.getch(nonblock)
|
c, ok := r.getch(nonblock)
|
||||||
if !nonblock && !ok {
|
if !nonblock && !ok {
|
||||||
r.Close()
|
r.Close()
|
||||||
return nil, errors.New("failed to read " + consoleDevice)
|
return nil, errors.New("failed to read " + DefaultTtyDevice)
|
||||||
}
|
}
|
||||||
|
|
||||||
retries := 0
|
retries := 0
|
||||||
@@ -442,10 +462,11 @@ func (r *LightRenderer) escSequence(sz *int) Event {
|
|||||||
}
|
}
|
||||||
// Bracketed paste mode: \e[200~ ... \e[201~
|
// 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] == '~' {
|
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
|
*sz = 6
|
||||||
r.buffer = r.buffer[6:]
|
if r.buffer[4] == '0' {
|
||||||
*sz = 0
|
return Event{BracketedPasteBegin, 0, nil}
|
||||||
return r.GetChar()
|
}
|
||||||
|
return Event{BracketedPasteEnd, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{Invalid, 0, nil} // INS
|
return Event{Invalid, 0, nil} // INS
|
||||||
case '3':
|
case '3':
|
||||||
@@ -617,15 +638,13 @@ func (r *LightRenderer) mouseSequence(sz *int) Event {
|
|||||||
|
|
||||||
// middle := t & 0b1
|
// middle := t & 0b1
|
||||||
left := t&0b11 == 0
|
left := t&0b11 == 0
|
||||||
|
ctrl := t&0b10000 > 0
|
||||||
// shift := t & 0b100
|
alt := t&0b01000 > 0
|
||||||
// ctrl := t & 0b1000
|
shift := t&0b00100 > 0
|
||||||
mod := t&0b1100 > 0
|
drag := t&0b100000 > 0 // 32
|
||||||
|
|
||||||
drag := t&0b100000 > 0
|
|
||||||
|
|
||||||
if scroll != 0 {
|
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
|
double := false
|
||||||
@@ -649,19 +668,21 @@ 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() {
|
func (r *LightRenderer) smcup() {
|
||||||
r.csi("?1049h")
|
r.flush()
|
||||||
|
r.flushRaw("\x1b[?1049h")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) rmcup() {
|
func (r *LightRenderer) rmcup() {
|
||||||
r.csi("?1049l")
|
r.flush()
|
||||||
|
r.flushRaw("\x1b[?1049l")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) Pause(clear bool) {
|
func (r *LightRenderer) Pause(clear bool) {
|
||||||
r.disableMouse()
|
r.disableModes()
|
||||||
r.restoreTerminal()
|
r.restoreTerminal()
|
||||||
if clear {
|
if clear {
|
||||||
if r.fullscreen {
|
if r.fullscreen {
|
||||||
@@ -674,12 +695,13 @@ func (r *LightRenderer) Pause(clear bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) enableMouse() {
|
func (r *LightRenderer) enableModes() {
|
||||||
if r.mouse {
|
if r.mouse {
|
||||||
r.csi("?1000h")
|
r.csi("?1000h")
|
||||||
r.csi("?1002h")
|
r.csi("?1002h")
|
||||||
r.csi("?1006h")
|
r.csi("?1006h")
|
||||||
}
|
}
|
||||||
|
r.csi("?2004h") // Enable bracketed paste mode
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) disableMouse() {
|
func (r *LightRenderer) disableMouse() {
|
||||||
@@ -690,6 +712,11 @@ func (r *LightRenderer) disableMouse() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) disableModes() {
|
||||||
|
r.disableMouse()
|
||||||
|
r.csi("?2004l")
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
||||||
r.setupTerminal()
|
r.setupTerminal()
|
||||||
if clear {
|
if clear {
|
||||||
@@ -698,7 +725,7 @@ func (r *LightRenderer) Resume(clear bool, sigcont bool) {
|
|||||||
} else {
|
} else {
|
||||||
r.rmcup()
|
r.rmcup()
|
||||||
}
|
}
|
||||||
r.enableMouse()
|
r.enableModes()
|
||||||
r.flush()
|
r.flush()
|
||||||
} else if sigcont && !r.fullscreen && r.mouse {
|
} else if sigcont && !r.fullscreen && r.mouse {
|
||||||
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
// NOTE: SIGCONT (Coming back from CTRL-Z):
|
||||||
@@ -750,11 +777,13 @@ func (r *LightRenderer) Close() {
|
|||||||
} else if !r.fullscreen {
|
} else if !r.fullscreen {
|
||||||
r.csi("u")
|
r.csi("u")
|
||||||
}
|
}
|
||||||
r.disableMouse()
|
if !r.showCursor {
|
||||||
|
r.csi("?25h")
|
||||||
|
}
|
||||||
|
r.disableModes()
|
||||||
r.flush()
|
r.flush()
|
||||||
r.closePlatform()
|
|
||||||
r.restoreTerminal()
|
r.restoreTerminal()
|
||||||
r.closed.Set(true)
|
r.closePlatform()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) Top() int {
|
func (r *LightRenderer) Top() int {
|
||||||
@@ -772,11 +801,13 @@ func (r *LightRenderer) MaxY() int {
|
|||||||
return r.height
|
return r.height
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
func (r *LightRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||||
|
width = util.Max(0, width)
|
||||||
|
height = util.Max(0, height)
|
||||||
w := &LightWindow{
|
w := &LightWindow{
|
||||||
renderer: r,
|
renderer: r,
|
||||||
colored: r.theme.Colored,
|
colored: r.theme.Colored,
|
||||||
preview: preview,
|
windowType: windowType,
|
||||||
border: borderStyle,
|
border: borderStyle,
|
||||||
top: top,
|
top: top,
|
||||||
left: left,
|
left: left,
|
||||||
@@ -785,12 +816,26 @@ func (r *LightRenderer) NewWindow(top int, left int, width int, height int, prev
|
|||||||
tabstop: r.tabstop,
|
tabstop: r.tabstop,
|
||||||
fg: colDefault,
|
fg: colDefault,
|
||||||
bg: colDefault}
|
bg: colDefault}
|
||||||
if preview {
|
switch windowType {
|
||||||
w.fg = r.theme.PreviewFg.Color
|
case WindowBase:
|
||||||
w.bg = r.theme.PreviewBg.Color
|
|
||||||
} else {
|
|
||||||
w.fg = r.theme.Fg.Color
|
w.fg = r.theme.Fg.Color
|
||||||
w.bg = r.theme.Bg.Color
|
w.bg = r.theme.Bg.Color
|
||||||
|
case WindowList:
|
||||||
|
w.fg = r.theme.ListFg.Color
|
||||||
|
w.bg = r.theme.ListBg.Color
|
||||||
|
case WindowInput:
|
||||||
|
w.fg = r.theme.Input.Color
|
||||||
|
w.bg = r.theme.InputBg.Color
|
||||||
|
case WindowHeader:
|
||||||
|
w.fg = r.theme.Header.Color
|
||||||
|
w.bg = r.theme.HeaderBg.Color
|
||||||
|
case WindowPreview:
|
||||||
|
w.fg = r.theme.PreviewFg.Color
|
||||||
|
w.bg = r.theme.PreviewBg.Color
|
||||||
|
}
|
||||||
|
if erase && !w.bg.IsDefault() && w.border.shape != BorderNone {
|
||||||
|
// fzf --color bg:blue --border --padding 1,2
|
||||||
|
w.Erase()
|
||||||
}
|
}
|
||||||
w.drawBorder(false)
|
w.drawBorder(false)
|
||||||
return w
|
return w
|
||||||
@@ -805,6 +850,9 @@ func (w *LightWindow) DrawHBorder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
||||||
|
if w.height == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
switch w.border.shape {
|
switch w.border.shape {
|
||||||
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||||
w.drawBorderAround(onlyHorizontal)
|
w.drawBorderAround(onlyHorizontal)
|
||||||
@@ -834,7 +882,14 @@ func (w *LightWindow) drawBorder(onlyHorizontal bool) {
|
|||||||
|
|
||||||
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
||||||
color := ColBorder
|
color := ColBorder
|
||||||
if w.preview {
|
switch w.windowType {
|
||||||
|
case WindowList:
|
||||||
|
color = ColListBorder
|
||||||
|
case WindowInput:
|
||||||
|
color = ColInputBorder
|
||||||
|
case WindowHeader:
|
||||||
|
color = ColHeaderBorder
|
||||||
|
case WindowPreview:
|
||||||
color = ColPreviewBorder
|
color = ColPreviewBorder
|
||||||
}
|
}
|
||||||
hw := runeWidth(w.border.top)
|
hw := runeWidth(w.border.top)
|
||||||
@@ -852,7 +907,14 @@ func (w *LightWindow) drawBorderHorizontal(top, bottom bool) {
|
|||||||
func (w *LightWindow) drawBorderVertical(left, right bool) {
|
func (w *LightWindow) drawBorderVertical(left, right bool) {
|
||||||
vw := runeWidth(w.border.left)
|
vw := runeWidth(w.border.left)
|
||||||
color := ColBorder
|
color := ColBorder
|
||||||
if w.preview {
|
switch w.windowType {
|
||||||
|
case WindowList:
|
||||||
|
color = ColListBorder
|
||||||
|
case WindowInput:
|
||||||
|
color = ColInputBorder
|
||||||
|
case WindowHeader:
|
||||||
|
color = ColHeaderBorder
|
||||||
|
case WindowPreview:
|
||||||
color = ColPreviewBorder
|
color = ColPreviewBorder
|
||||||
}
|
}
|
||||||
for y := 0; y < w.height; y++ {
|
for y := 0; y < w.height; y++ {
|
||||||
@@ -872,7 +934,14 @@ func (w *LightWindow) drawBorderVertical(left, right bool) {
|
|||||||
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
func (w *LightWindow) drawBorderAround(onlyHorizontal bool) {
|
||||||
w.Move(0, 0)
|
w.Move(0, 0)
|
||||||
color := ColBorder
|
color := ColBorder
|
||||||
if w.preview {
|
switch w.windowType {
|
||||||
|
case WindowList:
|
||||||
|
color = ColListBorder
|
||||||
|
case WindowInput:
|
||||||
|
color = ColInputBorder
|
||||||
|
case WindowHeader:
|
||||||
|
color = ColHeaderBorder
|
||||||
|
case WindowPreview:
|
||||||
color = ColPreviewBorder
|
color = ColPreviewBorder
|
||||||
}
|
}
|
||||||
hw := runeWidth(w.border.top)
|
hw := runeWidth(w.border.top)
|
||||||
@@ -924,9 +993,6 @@ func (w *LightWindow) Height() int {
|
|||||||
func (w *LightWindow) Refresh() {
|
func (w *LightWindow) Refresh() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Close() {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *LightWindow) X() int {
|
func (w *LightWindow) X() int {
|
||||||
return w.posx
|
return w.posx
|
||||||
}
|
}
|
||||||
@@ -935,9 +1001,16 @@ func (w *LightWindow) Y() int {
|
|||||||
return w.posy
|
return w.posy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) EncloseX(x int) bool {
|
||||||
|
return x >= w.left && x < (w.left+w.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) EncloseY(y int) bool {
|
||||||
|
return y >= w.top && y < (w.top+w.height)
|
||||||
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Enclose(y int, x int) bool {
|
func (w *LightWindow) Enclose(y int, x int) bool {
|
||||||
return x >= w.left && x < (w.left+w.width) &&
|
return w.EncloseX(x) && w.EncloseY(y)
|
||||||
y >= w.top && y < (w.top+w.height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Move(y int, x int) {
|
func (w *LightWindow) Move(y int, x int) {
|
||||||
@@ -960,7 +1033,7 @@ func attrCodes(attr Attr) []string {
|
|||||||
if (attr & AttrClear) > 0 {
|
if (attr & AttrClear) > 0 {
|
||||||
return codes
|
return codes
|
||||||
}
|
}
|
||||||
if (attr & Bold) > 0 {
|
if (attr&Bold) > 0 || (attr&BoldForce) > 0 {
|
||||||
codes = append(codes, "1")
|
codes = append(codes, "1")
|
||||||
}
|
}
|
||||||
if (attr & Dim) > 0 {
|
if (attr & Dim) > 0 {
|
||||||
@@ -1019,19 +1092,19 @@ func (w *LightWindow) Print(text string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func cleanse(str string) string {
|
func cleanse(str string) string {
|
||||||
return strings.Replace(str, "\x1b", "", -1)
|
return strings.ReplaceAll(str, "\x1b", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
func (w *LightWindow) CPrint(pair ColorPair, text string) {
|
||||||
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
_, code := w.csiColor(pair.Fg(), pair.Bg(), pair.Attr())
|
||||||
w.stderrInternal(cleanse(text), false, code)
|
w.stderrInternal(cleanse(text), false, code)
|
||||||
w.csi("m")
|
w.csi("0m")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
func (w *LightWindow) cprint2(fg Color, bg Color, attr Attr, text string) {
|
||||||
hasColors, code := w.csiColor(fg, bg, attr)
|
hasColors, code := w.csiColor(fg, bg, attr)
|
||||||
if hasColors {
|
if hasColors {
|
||||||
defer w.csi("m")
|
defer w.csi("0m")
|
||||||
}
|
}
|
||||||
w.stderrInternal(cleanse(text), false, code)
|
w.stderrInternal(cleanse(text), false, code)
|
||||||
}
|
}
|
||||||
@@ -1041,11 +1114,12 @@ type wrappedLine struct {
|
|||||||
displayWidth int
|
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{}
|
lines := []wrappedLine{}
|
||||||
width := 0
|
width := 0
|
||||||
line := ""
|
line := ""
|
||||||
gr := uniseg.NewGraphemes(input)
|
gr := uniseg.NewGraphemes(input)
|
||||||
|
max := initialMax
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
rs := gr.Runes()
|
rs := gr.Runes()
|
||||||
str := string(rs)
|
str := string(rs)
|
||||||
@@ -1067,6 +1141,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
line = str
|
line = str
|
||||||
prefixLength = 0
|
prefixLength = 0
|
||||||
width = w
|
width = w
|
||||||
|
max = initialMax - wrapSignWidth
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines = append(lines, wrappedLine{string(line), width})
|
lines = append(lines, wrappedLine{string(line), width})
|
||||||
@@ -1076,7 +1151,7 @@ func wrapLine(input string, prefixLength int, max int, tabstop int) []wrappedLin
|
|||||||
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
||||||
allLines := strings.Split(str, "\n")
|
allLines := strings.Split(str, "\n")
|
||||||
for i, line := range allLines {
|
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 {
|
for j, wl := range lines {
|
||||||
w.stderrInternal(wl.text, false, resetCode)
|
w.stderrInternal(wl.text, false, resetCode)
|
||||||
w.posx += wl.displayWidth
|
w.posx += wl.displayWidth
|
||||||
@@ -1089,10 +1164,22 @@ func (w *LightWindow) fill(str string, resetCode string) FillReturn {
|
|||||||
w.MoveAndClear(w.posy, w.posx)
|
w.MoveAndClear(w.posy, w.posx)
|
||||||
w.Move(w.posy+1, 0)
|
w.Move(w.posy+1, 0)
|
||||||
w.renderer.stderr(resetCode)
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if w.posx+1 >= w.Width() {
|
}
|
||||||
|
if w.posx >= w.Width() {
|
||||||
if w.posy+1 >= w.height {
|
if w.posy+1 >= w.height {
|
||||||
return FillSuspend
|
return FillSuspend
|
||||||
}
|
}
|
||||||
@@ -1113,6 +1200,14 @@ func (w *LightWindow) setBg() string {
|
|||||||
return "\x1b[m"
|
return "\x1b[m"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) LinkBegin(uri string, params string) {
|
||||||
|
w.renderer.queued.WriteString("\x1b]8;" + params + ";" + uri + "\x1b\\")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *LightWindow) LinkEnd() {
|
||||||
|
w.renderer.queued.WriteString("\x1b]8;;\x1b\\")
|
||||||
|
}
|
||||||
|
|
||||||
func (w *LightWindow) Fill(text string) FillReturn {
|
func (w *LightWindow) Fill(text string) FillReturn {
|
||||||
w.Move(w.posy, w.posx)
|
w.Move(w.posy, w.posx)
|
||||||
code := w.setBg()
|
code := w.setBg()
|
||||||
@@ -1128,7 +1223,7 @@ func (w *LightWindow) CFill(fg Color, bg Color, attr Attr, text string) FillRetu
|
|||||||
bg = w.bg
|
bg = w.bg
|
||||||
}
|
}
|
||||||
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
if hasColors, resetCode := w.csiColor(fg, bg, attr); hasColors {
|
||||||
defer w.csi("m")
|
defer w.csi("0m")
|
||||||
return w.fill(text, resetCode)
|
return w.fill(text, resetCode)
|
||||||
}
|
}
|
||||||
return w.fill(text, w.setBg())
|
return w.fill(text, w.setBg())
|
||||||
@@ -1153,3 +1248,18 @@ func (w *LightWindow) Erase() {
|
|||||||
func (w *LightWindow) EraseMaybe() bool {
|
func (w *LightWindow) EraseMaybe() bool {
|
||||||
return false
|
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")
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func IsLightRendererSupported() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
func (r *LightRenderer) DefaultTheme() *ColorTheme {
|
||||||
if strings.Contains(os.Getenv("TERM"), "256") {
|
if strings.Contains(os.Getenv("TERM"), "256") {
|
||||||
return Dark256
|
return Dark256
|
||||||
}
|
}
|
||||||
@@ -33,35 +33,46 @@ func (r *LightRenderer) fd() int {
|
|||||||
return int(r.ttyin.Fd())
|
return int(r.ttyin.Fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) initPlatform() error {
|
func (r *LightRenderer) initPlatform() (err error) {
|
||||||
fd := r.fd()
|
r.origState, err = term.MakeRaw(r.fd())
|
||||||
origState, err := term.GetState(fd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
|
||||||
r.origState = origState
|
|
||||||
term.MakeRaw(fd)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) closePlatform() {
|
func (r *LightRenderer) closePlatform() {
|
||||||
// NOOP
|
r.ttyout.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func openTtyIn() (*os.File, error) {
|
func openTty(ttyDefault string, mode int) (*os.File, error) {
|
||||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
var in *os.File
|
||||||
if err != nil {
|
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()
|
tty := ttyname()
|
||||||
if len(tty) > 0 {
|
if len(tty) > 0 {
|
||||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||||
return in, nil
|
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
|
return in, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||||
|
return openTty(ttyDefault, syscall.O_RDONLY)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||||
|
return openTty(ttyDefault, syscall.O_WRONLY)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) setupTerminal() {
|
func (r *LightRenderer) setupTerminal() {
|
||||||
term.MakeRaw(r.fd())
|
term.MakeRaw(r.fd())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const (
|
|||||||
var (
|
var (
|
||||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
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)
|
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
|
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||||
@@ -39,7 +40,7 @@ func IsLightRendererSupported() bool {
|
|||||||
return canSetVt100
|
return canSetVt100
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
func (r *LightRenderer) DefaultTheme() *ColorTheme {
|
||||||
// the getenv check is borrowed from here: https://github.com/gdamore/tcell/commit/0c473b86d82f68226a142e96cc5a34c5a29b3690#diff-b008fcd5e6934bf31bc3d33bf49f47d8R178:
|
// the getenv check is borrowed from here: https://github.com/gdamore/tcell/commit/0c473b86d82f68226a142e96cc5a34c5a29b3690#diff-b008fcd5e6934bf31bc3d33bf49f47d8R178:
|
||||||
if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" {
|
if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" {
|
||||||
return Default16
|
return Default16
|
||||||
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
|
|||||||
}
|
}
|
||||||
r.inHandle = uintptr(inHandle)
|
r.inHandle = uintptr(inHandle)
|
||||||
|
|
||||||
r.setupTerminal()
|
|
||||||
|
|
||||||
// channel for non-blocking reads. Buffer to make sure
|
// channel for non-blocking reads. Buffer to make sure
|
||||||
// we get the ESC sets:
|
// we get the ESC sets:
|
||||||
r.ttyinChannel = make(chan byte, 1024)
|
r.ttyinChannel = make(chan byte, 1024)
|
||||||
|
|
||||||
// the following allows for non-blocking IO.
|
r.setupTerminal()
|
||||||
// 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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -91,23 +76,51 @@ func (r *LightRenderer) closePlatform() {
|
|||||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||||
}
|
}
|
||||||
|
|
||||||
func openTtyIn() (*os.File, error) {
|
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||||
// not used
|
// not used
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) setupTerminal() error {
|
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
return os.Stderr, nil
|
||||||
return err
|
|
||||||
}
|
|
||||||
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) restoreTerminal() error {
|
func (r *LightRenderer) setupTerminal() {
|
||||||
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
|
||||||
return err
|
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.outHandle), r.origStateOutput)
|
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() {
|
||||||
|
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 {
|
func (r *LightRenderer) Size() TermSize {
|
||||||
@@ -135,7 +148,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
|||||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||||
return -1, -1
|
return -1, -1
|
||||||
}
|
}
|
||||||
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
|
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||||
|
|||||||
184
src/tui/tcell.go
184
src/tui/tcell.go
@@ -4,6 +4,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
@@ -39,7 +40,7 @@ type Attr int32
|
|||||||
|
|
||||||
type TcellWindow struct {
|
type TcellWindow struct {
|
||||||
color bool
|
color bool
|
||||||
preview bool
|
windowType WindowType
|
||||||
top int
|
top int
|
||||||
left int
|
left int
|
||||||
width int
|
width int
|
||||||
@@ -49,6 +50,11 @@ type TcellWindow struct {
|
|||||||
lastY int
|
lastY int
|
||||||
moveCursor bool
|
moveCursor bool
|
||||||
borderStyle BorderStyle
|
borderStyle BorderStyle
|
||||||
|
uri *string
|
||||||
|
params *string
|
||||||
|
showCursor bool
|
||||||
|
wrapSign string
|
||||||
|
wrapSignWidth int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Top() int {
|
func (w *TcellWindow) Top() int {
|
||||||
@@ -69,7 +75,9 @@ func (w *TcellWindow) Height() int {
|
|||||||
|
|
||||||
func (w *TcellWindow) Refresh() {
|
func (w *TcellWindow) Refresh() {
|
||||||
if w.moveCursor {
|
if w.moveCursor {
|
||||||
|
if w.showCursor {
|
||||||
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
_screen.ShowCursor(w.left+w.lastX, w.top+w.lastY)
|
||||||
|
}
|
||||||
w.moveCursor = false
|
w.moveCursor = false
|
||||||
}
|
}
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
@@ -94,8 +102,21 @@ const (
|
|||||||
AttrUndefined = Attr(0)
|
AttrUndefined = Attr(0)
|
||||||
AttrRegular = Attr(1 << 7)
|
AttrRegular = Attr(1 << 7)
|
||||||
AttrClear = Attr(1 << 8)
|
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) {
|
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||||
// No-op
|
// No-op
|
||||||
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
||||||
@@ -103,8 +124,12 @@ func (r *FullscreenRenderer) PassThrough(str string) {
|
|||||||
|
|
||||||
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
func (r *FullscreenRenderer) DefaultTheme() *ColorTheme {
|
||||||
if _screen.Colors() >= 256 {
|
s, e := r.getScreen()
|
||||||
|
if e != nil {
|
||||||
|
return Default16
|
||||||
|
}
|
||||||
|
if s.Colors() >= 256 {
|
||||||
return Dark256
|
return Dark256
|
||||||
}
|
}
|
||||||
return Default16
|
return Default16
|
||||||
@@ -134,6 +159,11 @@ func (c Color) Style() tcell.Color {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a Attr) Merge(b Attr) Attr {
|
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
|
return a | b
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,20 +175,34 @@ var (
|
|||||||
_initialResize bool = true
|
_initialResize bool = true
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *FullscreenRenderer) initScreen() error {
|
func (r *FullscreenRenderer) getScreen() (tcell.Screen, error) {
|
||||||
|
if _screen == nil {
|
||||||
s, e := tcell.NewScreen()
|
s, e := tcell.NewScreen()
|
||||||
|
if e != nil {
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
if !r.showCursor {
|
||||||
|
s.HideCursor()
|
||||||
|
}
|
||||||
|
_screen = s
|
||||||
|
}
|
||||||
|
return _screen, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) initScreen() error {
|
||||||
|
s, e := r.getScreen()
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
if e = s.Init(); e != nil {
|
if e = s.Init(); e != nil {
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
s.EnablePaste()
|
||||||
if r.mouse {
|
if r.mouse {
|
||||||
s.EnableMouse()
|
s.EnableMouse()
|
||||||
} else {
|
} else {
|
||||||
s.DisableMouse()
|
s.DisableMouse()
|
||||||
}
|
}
|
||||||
_screen = s
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -171,7 +215,6 @@ func (r *FullscreenRenderer) Init() error {
|
|||||||
if err := r.initScreen(); err != nil {
|
if err := r.initScreen(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -224,6 +267,11 @@ func (r *FullscreenRenderer) Size() TermSize {
|
|||||||
func (r *FullscreenRenderer) GetChar() Event {
|
func (r *FullscreenRenderer) GetChar() Event {
|
||||||
ev := _screen.PollEvent()
|
ev := _screen.PollEvent()
|
||||||
switch ev := ev.(type) {
|
switch ev := ev.(type) {
|
||||||
|
case *tcell.EventPaste:
|
||||||
|
if ev.Start() {
|
||||||
|
return Event{BracketedPasteBegin, 0, nil}
|
||||||
|
}
|
||||||
|
return Event{BracketedPasteEnd, 0, nil}
|
||||||
case *tcell.EventResize:
|
case *tcell.EventResize:
|
||||||
// Ignore the first resize event
|
// Ignore the first resize event
|
||||||
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
|
// https://github.com/gdamore/tcell/blob/v2.7.0/TUTORIAL.md?plain=1#L18
|
||||||
@@ -240,7 +288,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)
|
// 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
|
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||||
x, y := ev.Position()
|
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
|
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||||
prevButton, button := _prevMouseButton, ev.Buttons()
|
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||||
@@ -249,9 +301,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case button&tcell.WheelDown != 0:
|
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:
|
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:
|
case button&tcell.Button1 != 0:
|
||||||
double := false
|
double := false
|
||||||
if !drag {
|
if !drag {
|
||||||
@@ -274,9 +326,9 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fire single or double click 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:
|
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:
|
default:
|
||||||
// double and single taps on Windows don't quite work due to
|
// double and single taps on Windows don't quite work due to
|
||||||
// the console acting on the events and not allowing us
|
// the console acting on the events and not allowing us
|
||||||
@@ -285,7 +337,11 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
down := left || button&tcell.Button3 != 0
|
down := left || button&tcell.Button3 != 0
|
||||||
double := false
|
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:
|
// process keyboard:
|
||||||
@@ -512,7 +568,7 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
|
|
||||||
func (r *FullscreenRenderer) Pause(clear bool) {
|
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||||
if clear {
|
if clear {
|
||||||
_screen.Fini()
|
r.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +580,7 @@ func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
|||||||
|
|
||||||
func (r *FullscreenRenderer) Close() {
|
func (r *FullscreenRenderer) Close() {
|
||||||
_screen.Fini()
|
_screen.Fini()
|
||||||
|
_screen = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
||||||
@@ -534,28 +591,34 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
|||||||
_screen.Show()
|
_screen.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window {
|
||||||
normal := ColNormal
|
width = util.Max(0, width)
|
||||||
if preview {
|
height = util.Max(0, height)
|
||||||
|
normal := ColBorder
|
||||||
|
switch windowType {
|
||||||
|
case WindowList:
|
||||||
|
normal = ColNormal
|
||||||
|
case WindowHeader:
|
||||||
|
normal = ColHeader
|
||||||
|
case WindowInput:
|
||||||
|
normal = ColInput
|
||||||
|
case WindowPreview:
|
||||||
normal = ColPreview
|
normal = ColPreview
|
||||||
}
|
}
|
||||||
w := &TcellWindow{
|
w := &TcellWindow{
|
||||||
color: r.theme.Colored,
|
color: r.theme.Colored,
|
||||||
preview: preview,
|
windowType: windowType,
|
||||||
top: top,
|
top: top,
|
||||||
left: left,
|
left: left,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
normal: normal,
|
normal: normal,
|
||||||
borderStyle: borderStyle}
|
borderStyle: borderStyle,
|
||||||
|
showCursor: r.showCursor}
|
||||||
w.Erase()
|
w.Erase()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Close() {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
func fill(x, y, w, h int, n ColorPair, r rune) {
|
func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||||
for ly := 0; ly <= h; ly++ {
|
for ly := 0; ly <= h; ly++ {
|
||||||
for lx := 0; lx <= w; lx++ {
|
for lx := 0; lx <= w; lx++ {
|
||||||
@@ -565,11 +628,7 @@ func fill(x, y, w, h int, n ColorPair, r rune) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Erase() {
|
func (w *TcellWindow) Erase() {
|
||||||
if w.borderStyle.shape.HasLeft() {
|
|
||||||
fill(w.left-1, w.top, w.width, w.height-1, w.normal, ' ')
|
|
||||||
} else {
|
|
||||||
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
|
fill(w.left, w.top, w.width-1, w.height-1, w.normal, ' ')
|
||||||
}
|
|
||||||
w.drawBorder(false)
|
w.drawBorder(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,9 +637,21 @@ func (w *TcellWindow) EraseMaybe() bool {
|
|||||||
return true
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) EncloseY(y int) bool {
|
||||||
|
return y >= w.top && y < (w.top+w.height)
|
||||||
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||||
return x >= w.left && x < (w.left+w.width) &&
|
return w.EncloseX(x) && w.EncloseY(y)
|
||||||
y >= w.top && y < (w.top+w.height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Move(y int, x int) {
|
func (w *TcellWindow) Move(y int, x int) {
|
||||||
@@ -601,6 +672,16 @@ func (w *TcellWindow) Print(text string) {
|
|||||||
w.printString(text, w.normal)
|
w.printString(text, w.normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) withUrl(style tcell.Style) tcell.Style {
|
||||||
|
if w.uri != nil {
|
||||||
|
style = style.Url(*w.uri)
|
||||||
|
if md := regexp.MustCompile(`id=([^:]+)`).FindStringSubmatch(*w.params); len(md) > 1 {
|
||||||
|
style = style.UrlId(md[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||||
lx := 0
|
lx := 0
|
||||||
a := pair.Attr()
|
a := pair.Attr()
|
||||||
@@ -615,6 +696,7 @@ func (w *TcellWindow) printString(text string, pair ColorPair) {
|
|||||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||||
}
|
}
|
||||||
|
style = w.withUrl(style)
|
||||||
|
|
||||||
gr := uniseg.NewGraphemes(text)
|
gr := uniseg.NewGraphemes(text)
|
||||||
for gr.Next() {
|
for gr.Next() {
|
||||||
@@ -659,12 +741,13 @@ func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
|||||||
}
|
}
|
||||||
style = style.
|
style = style.
|
||||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||||
Bold(a&Attr(tcell.AttrBold) != 0).
|
Bold(a&Attr(tcell.AttrBold) != 0 || a&BoldForce != 0).
|
||||||
Dim(a&Attr(tcell.AttrDim) != 0).
|
Dim(a&Attr(tcell.AttrDim) != 0).
|
||||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||||
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||||
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||||
Italic(a&Attr(tcell.AttrItalic) != 0)
|
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||||
|
style = w.withUrl(style)
|
||||||
|
|
||||||
gr := uniseg.NewGraphemes(text)
|
gr := uniseg.NewGraphemes(text)
|
||||||
Loop:
|
Loop:
|
||||||
@@ -687,11 +770,26 @@ Loop:
|
|||||||
|
|
||||||
// word wrap:
|
// word wrap:
|
||||||
xPos := w.left + w.lastX + lx
|
xPos := w.left + w.lastX + lx
|
||||||
if xPos >= (w.left + w.width) {
|
if xPos >= w.left+w.width {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
|
if w.lastY >= w.height {
|
||||||
|
return FillSuspend
|
||||||
|
}
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
xPos = w.left
|
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
|
yPos := w.top + w.lastY
|
||||||
@@ -716,6 +814,16 @@ func (w *TcellWindow) Fill(str string) FillReturn {
|
|||||||
return w.fillString(str, w.normal)
|
return w.fillString(str, w.normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) LinkBegin(uri string, params string) {
|
||||||
|
w.uri = &uri
|
||||||
|
w.params = ¶ms
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) LinkEnd() {
|
||||||
|
w.uri = nil
|
||||||
|
w.params = nil
|
||||||
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||||
if fg == colDefault {
|
if fg == colDefault {
|
||||||
fg = w.normal.Fg()
|
fg = w.normal.Fg()
|
||||||
@@ -735,6 +843,9 @@ func (w *TcellWindow) DrawHBorder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||||
|
if w.height == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
shape := w.borderStyle.shape
|
shape := w.borderStyle.shape
|
||||||
if shape == BorderNone {
|
if shape == BorderNone {
|
||||||
return
|
return
|
||||||
@@ -747,10 +858,17 @@ func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
|||||||
|
|
||||||
var style tcell.Style
|
var style tcell.Style
|
||||||
if w.color {
|
if w.color {
|
||||||
if w.preview {
|
switch w.windowType {
|
||||||
style = ColPreviewBorder.style()
|
case WindowBase:
|
||||||
} else {
|
|
||||||
style = ColBorder.style()
|
style = ColBorder.style()
|
||||||
|
case WindowList:
|
||||||
|
style = ColListBorder.style()
|
||||||
|
case WindowHeader:
|
||||||
|
style = ColHeaderBorder.style()
|
||||||
|
case WindowInput:
|
||||||
|
style = ColInputBorder.style()
|
||||||
|
case WindowPreview:
|
||||||
|
style = ColPreviewBorder.style()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
style = w.normal.style()
|
style = w.normal.style()
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/gdamore/tcell/v2"
|
"github.com/gdamore/tcell/v2"
|
||||||
"github.com/junegunn/fzf/src/util"
|
"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 {
|
if got == want {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
|
|||||||
|
|
||||||
// Test the handling of the tcell keyboard events.
|
// Test the handling of the tcell keyboard events.
|
||||||
func TestGetCharEventKey(t *testing.T) {
|
func TestGetCharEventKey(t *testing.T) {
|
||||||
if util.ToTty() {
|
if util.IsTty(os.Stdout) {
|
||||||
// This test is skipped when output goes to terminal, because it causes
|
// This test is skipped when output goes to terminal, because it causes
|
||||||
// some glitches:
|
// some glitches:
|
||||||
// - output lines may not start at the beginning of a row which makes
|
// - output lines may not start at the beginning of a row which makes
|
||||||
@@ -81,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}}, // unhandled, actual "Tab" keystroke
|
||||||
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
{giveKey{tcell.KeyTAB, rune(tcell.KeyTAB), tcell.ModNone}, wantKey{Tab, 0, nil}}, // fabricated, unhandled
|
||||||
// KeyEnter is alias for KeyCR
|
// KeyEnter is alias for KeyCR
|
||||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
|
||||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||||
// Ctrl+Alt keys
|
// 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}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||||
@@ -232,7 +233,7 @@ Quick reference
|
|||||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
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
|
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||||
|
|||||||
@@ -4,12 +4,19 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
||||||
|
|
||||||
|
var tty atomic.Value
|
||||||
|
|
||||||
func ttyname() string {
|
func ttyname() string {
|
||||||
|
if cached := tty.Load(); cached != nil {
|
||||||
|
return cached.(string)
|
||||||
|
}
|
||||||
|
|
||||||
var stderr syscall.Stat_t
|
var stderr syscall.Stat_t
|
||||||
if syscall.Fstat(2, &stderr) != nil {
|
if syscall.Fstat(2, &stderr) != nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -27,24 +34,21 @@ func ttyname() string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||||
return prefix + file.Name()
|
value := prefix + file.Name()
|
||||||
|
tty.Store(value)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
|
// TtyIn returns terminal device to read user input
|
||||||
func TtyIn() *os.File {
|
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
return openTtyIn(ttyDefault)
|
||||||
if err != nil {
|
}
|
||||||
tty := ttyname()
|
|
||||||
if len(tty) > 0 {
|
// TtyIn returns terminal device to write to
|
||||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||||
return in
|
return openTtyOut(ttyDefault)
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.Stdin
|
|
||||||
}
|
|
||||||
return in
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,20 @@
|
|||||||
|
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
import "os"
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
func ttyname() string {
|
func ttyname() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// TtyIn on Windows returns os.Stdin
|
// TtyIn on Windows returns os.Stdin
|
||||||
func TtyIn() *os.File {
|
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||||
return os.Stdin
|
return os.Stdin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TtyOut on Windows returns nil
|
||||||
|
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
328
src/tui/tui.go
328
src/tui/tui.go
@@ -28,7 +28,7 @@ const (
|
|||||||
CtrlJ
|
CtrlJ
|
||||||
CtrlK
|
CtrlK
|
||||||
CtrlL
|
CtrlL
|
||||||
CtrlM
|
Enter
|
||||||
CtrlN
|
CtrlN
|
||||||
CtrlO
|
CtrlO
|
||||||
CtrlP
|
CtrlP
|
||||||
@@ -103,6 +103,8 @@ const (
|
|||||||
|
|
||||||
Invalid
|
Invalid
|
||||||
Fatal
|
Fatal
|
||||||
|
BracketedPasteBegin
|
||||||
|
BracketedPasteEnd
|
||||||
|
|
||||||
Mouse
|
Mouse
|
||||||
DoubleClick
|
DoubleClick
|
||||||
@@ -150,12 +152,19 @@ func (e Event) Comparable() Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e Event) KeyName() string {
|
func (e Event) KeyName() string {
|
||||||
|
if me := e.MouseEvent; me != nil {
|
||||||
|
return me.Name()
|
||||||
|
}
|
||||||
|
|
||||||
if e.Type >= Invalid {
|
if e.Type >= Invalid {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
switch e.Type {
|
switch e.Type {
|
||||||
case Rune:
|
case Rune:
|
||||||
|
if e.Char == ' ' {
|
||||||
|
return "space"
|
||||||
|
}
|
||||||
return string(e.Char)
|
return string(e.Char)
|
||||||
case Alt:
|
case Alt:
|
||||||
return "alt-" + string(e.Char)
|
return "alt-" + string(e.Char)
|
||||||
@@ -205,10 +214,24 @@ type ColorAttr struct {
|
|||||||
Attr Attr
|
Attr Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a ColorAttr) IsColorDefined() bool {
|
||||||
|
return a.Color != colUndefined
|
||||||
|
}
|
||||||
|
|
||||||
func NewColorAttr() ColorAttr {
|
func NewColorAttr() ColorAttr {
|
||||||
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a ColorAttr) Merge(other ColorAttr) ColorAttr {
|
||||||
|
if other.Color != colUndefined {
|
||||||
|
a.Color = other.Color
|
||||||
|
}
|
||||||
|
if other.Attr != AttrUndefined {
|
||||||
|
a.Attr = a.Attr.Merge(other.Attr)
|
||||||
|
}
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
colUndefined Color = -2
|
colUndefined Color = -2
|
||||||
colDefault Color = -1
|
colDefault Color = -1
|
||||||
@@ -285,6 +308,12 @@ func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
|||||||
return dup
|
return dup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
|
||||||
return p.WithAttr(other.attr)
|
return p.WithAttr(other.attr)
|
||||||
}
|
}
|
||||||
@@ -303,6 +332,10 @@ type ColorTheme struct {
|
|||||||
Disabled ColorAttr
|
Disabled ColorAttr
|
||||||
Fg ColorAttr
|
Fg ColorAttr
|
||||||
Bg ColorAttr
|
Bg ColorAttr
|
||||||
|
ListFg ColorAttr
|
||||||
|
ListBg ColorAttr
|
||||||
|
AltBg ColorAttr
|
||||||
|
Nth ColorAttr
|
||||||
SelectedFg ColorAttr
|
SelectedFg ColorAttr
|
||||||
SelectedBg ColorAttr
|
SelectedBg ColorAttr
|
||||||
SelectedMatch ColorAttr
|
SelectedMatch ColorAttr
|
||||||
@@ -311,6 +344,9 @@ type ColorTheme struct {
|
|||||||
DarkBg ColorAttr
|
DarkBg ColorAttr
|
||||||
Gutter ColorAttr
|
Gutter ColorAttr
|
||||||
Prompt ColorAttr
|
Prompt ColorAttr
|
||||||
|
InputBg ColorAttr
|
||||||
|
InputBorder ColorAttr
|
||||||
|
InputLabel ColorAttr
|
||||||
Match ColorAttr
|
Match ColorAttr
|
||||||
Current ColorAttr
|
Current ColorAttr
|
||||||
CurrentMatch ColorAttr
|
CurrentMatch ColorAttr
|
||||||
@@ -319,13 +355,19 @@ type ColorTheme struct {
|
|||||||
Cursor ColorAttr
|
Cursor ColorAttr
|
||||||
Marker ColorAttr
|
Marker ColorAttr
|
||||||
Header ColorAttr
|
Header ColorAttr
|
||||||
|
HeaderBg ColorAttr
|
||||||
|
HeaderBorder ColorAttr
|
||||||
|
HeaderLabel ColorAttr
|
||||||
Separator ColorAttr
|
Separator ColorAttr
|
||||||
Scrollbar ColorAttr
|
Scrollbar ColorAttr
|
||||||
Border ColorAttr
|
Border ColorAttr
|
||||||
PreviewBorder ColorAttr
|
PreviewBorder ColorAttr
|
||||||
|
PreviewLabel ColorAttr
|
||||||
PreviewScrollbar ColorAttr
|
PreviewScrollbar ColorAttr
|
||||||
BorderLabel ColorAttr
|
BorderLabel ColorAttr
|
||||||
PreviewLabel ColorAttr
|
ListLabel ColorAttr
|
||||||
|
ListBorder ColorAttr
|
||||||
|
GapLine ColorAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
@@ -334,15 +376,6 @@ type Event struct {
|
|||||||
MouseEvent *MouseEvent
|
MouseEvent *MouseEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Event) Is(types ...EventType) bool {
|
|
||||||
for _, t := range types {
|
|
||||||
if e.Type == t {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type MouseEvent struct {
|
type MouseEvent struct {
|
||||||
Y int
|
Y int
|
||||||
X int
|
X int
|
||||||
@@ -350,13 +383,46 @@ type MouseEvent struct {
|
|||||||
Left bool
|
Left bool
|
||||||
Down bool
|
Down bool
|
||||||
Double 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
|
type BorderShape int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BorderNone BorderShape = iota
|
BorderUndefined BorderShape = iota
|
||||||
|
BorderLine
|
||||||
|
BorderNone
|
||||||
|
BorderPhantom
|
||||||
BorderRounded
|
BorderRounded
|
||||||
BorderSharp
|
BorderSharp
|
||||||
BorderBold
|
BorderBold
|
||||||
@@ -373,7 +439,7 @@ const (
|
|||||||
|
|
||||||
func (s BorderShape) HasLeft() bool {
|
func (s BorderShape) HasLeft() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
case BorderNone, BorderPhantom, BorderLine, BorderRight, BorderTop, BorderBottom, BorderHorizontal: // No Left
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -381,7 +447,7 @@ func (s BorderShape) HasLeft() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasRight() bool {
|
func (s BorderShape) HasRight() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -389,12 +455,24 @@ func (s BorderShape) HasRight() bool {
|
|||||||
|
|
||||||
func (s BorderShape) HasTop() bool {
|
func (s BorderShape) HasTop() bool {
|
||||||
switch s {
|
switch s {
|
||||||
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s BorderShape) HasBottom() bool {
|
||||||
|
switch s {
|
||||||
|
case BorderNone, BorderPhantom, BorderLine, BorderLeft, BorderRight, BorderTop, BorderVertical: // No bottom
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s BorderShape) Visible() bool {
|
||||||
|
return s != BorderNone
|
||||||
|
}
|
||||||
|
|
||||||
type BorderStyle struct {
|
type BorderStyle struct {
|
||||||
shape BorderShape
|
shape BorderShape
|
||||||
top rune
|
top rune
|
||||||
@@ -410,6 +488,18 @@ type BorderStyle struct {
|
|||||||
type BorderCharacter int
|
type BorderCharacter int
|
||||||
|
|
||||||
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||||
|
if shape == BorderNone || shape == BorderPhantom {
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: ' ',
|
||||||
|
bottom: ' ',
|
||||||
|
left: ' ',
|
||||||
|
right: ' ',
|
||||||
|
topLeft: ' ',
|
||||||
|
topRight: ' ',
|
||||||
|
bottomLeft: ' ',
|
||||||
|
bottomRight: ' '}
|
||||||
|
}
|
||||||
if !unicode {
|
if !unicode {
|
||||||
return BorderStyle{
|
return BorderStyle{
|
||||||
shape: shape,
|
shape: shape,
|
||||||
@@ -506,19 +596,6 @@ func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeTransparentBorder() BorderStyle {
|
|
||||||
return BorderStyle{
|
|
||||||
shape: BorderRounded,
|
|
||||||
top: ' ',
|
|
||||||
bottom: ' ',
|
|
||||||
left: ' ',
|
|
||||||
right: ' ',
|
|
||||||
topLeft: ' ',
|
|
||||||
topRight: ' ',
|
|
||||||
bottomLeft: ' ',
|
|
||||||
bottomRight: ' '}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TermSize struct {
|
type TermSize struct {
|
||||||
Lines int
|
Lines int
|
||||||
Columns int
|
Columns int
|
||||||
@@ -526,7 +603,18 @@ type TermSize struct {
|
|||||||
PxHeight int
|
PxHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WindowType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
WindowBase WindowType = iota
|
||||||
|
WindowList
|
||||||
|
WindowPreview
|
||||||
|
WindowInput
|
||||||
|
WindowHeader
|
||||||
|
)
|
||||||
|
|
||||||
type Renderer interface {
|
type Renderer interface {
|
||||||
|
DefaultTheme() *ColorTheme
|
||||||
Init() error
|
Init() error
|
||||||
Resize(maxHeightFunc func(int) int)
|
Resize(maxHeightFunc func(int) int)
|
||||||
Pause(clear bool)
|
Pause(clear bool)
|
||||||
@@ -538,6 +626,9 @@ type Renderer interface {
|
|||||||
PassThrough(string)
|
PassThrough(string)
|
||||||
NeedScrollbarRedraw() bool
|
NeedScrollbarRedraw() bool
|
||||||
ShouldEmitResizeEvent() bool
|
ShouldEmitResizeEvent() bool
|
||||||
|
Bell()
|
||||||
|
HideCursor()
|
||||||
|
ShowCursor()
|
||||||
|
|
||||||
GetChar() Event
|
GetChar() Event
|
||||||
|
|
||||||
@@ -547,7 +638,7 @@ type Renderer interface {
|
|||||||
|
|
||||||
Size() TermSize
|
Size() TermSize
|
||||||
|
|
||||||
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
|
NewWindow(top int, left int, width int, height int, windowType WindowType, borderStyle BorderStyle, erase bool) Window
|
||||||
}
|
}
|
||||||
|
|
||||||
type Window interface {
|
type Window interface {
|
||||||
@@ -560,10 +651,11 @@ type Window interface {
|
|||||||
DrawHBorder()
|
DrawHBorder()
|
||||||
Refresh()
|
Refresh()
|
||||||
FinishFill()
|
FinishFill()
|
||||||
Close()
|
|
||||||
|
|
||||||
X() int
|
X() int
|
||||||
Y() int
|
Y() int
|
||||||
|
EncloseX(x int) bool
|
||||||
|
EncloseY(y int) bool
|
||||||
Enclose(y int, x int) bool
|
Enclose(y int, x int) bool
|
||||||
|
|
||||||
Move(y int, x int)
|
Move(y int, x int)
|
||||||
@@ -572,8 +664,12 @@ type Window interface {
|
|||||||
CPrint(color ColorPair, text string)
|
CPrint(color ColorPair, text string)
|
||||||
Fill(text string) FillReturn
|
Fill(text string) FillReturn
|
||||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||||
|
LinkBegin(uri string, params string)
|
||||||
|
LinkEnd()
|
||||||
Erase()
|
Erase()
|
||||||
EraseMaybe() bool
|
EraseMaybe() bool
|
||||||
|
|
||||||
|
SetWrapSign(string, int)
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullscreenRenderer struct {
|
type FullscreenRenderer struct {
|
||||||
@@ -582,6 +678,7 @@ type FullscreenRenderer struct {
|
|||||||
forceBlack bool
|
forceBlack bool
|
||||||
prevDownTime time.Time
|
prevDownTime time.Time
|
||||||
clicks [][2]int
|
clicks [][2]int
|
||||||
|
showCursor bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||||
@@ -590,7 +687,8 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
|||||||
mouse: mouse,
|
mouse: mouse,
|
||||||
forceBlack: forceBlack,
|
forceBlack: forceBlack,
|
||||||
prevDownTime: time.Unix(0, 0),
|
prevDownTime: time.Unix(0, 0),
|
||||||
clicks: [][2]int{}}
|
clicks: [][2]int{},
|
||||||
|
showCursor: true}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -618,8 +716,11 @@ var (
|
|||||||
ColSpinner ColorPair
|
ColSpinner ColorPair
|
||||||
ColInfo ColorPair
|
ColInfo ColorPair
|
||||||
ColHeader ColorPair
|
ColHeader ColorPair
|
||||||
|
ColHeaderBorder ColorPair
|
||||||
|
ColHeaderLabel ColorPair
|
||||||
ColSeparator ColorPair
|
ColSeparator ColorPair
|
||||||
ColScrollbar ColorPair
|
ColScrollbar ColorPair
|
||||||
|
ColGapLine ColorPair
|
||||||
ColBorder ColorPair
|
ColBorder ColorPair
|
||||||
ColPreview ColorPair
|
ColPreview ColorPair
|
||||||
ColPreviewBorder ColorPair
|
ColPreviewBorder ColorPair
|
||||||
@@ -627,6 +728,10 @@ var (
|
|||||||
ColPreviewLabel ColorPair
|
ColPreviewLabel ColorPair
|
||||||
ColPreviewScrollbar ColorPair
|
ColPreviewScrollbar ColorPair
|
||||||
ColPreviewSpinner ColorPair
|
ColPreviewSpinner ColorPair
|
||||||
|
ColListBorder ColorPair
|
||||||
|
ColListLabel ColorPair
|
||||||
|
ColInputBorder ColorPair
|
||||||
|
ColInputLabel ColorPair
|
||||||
)
|
)
|
||||||
|
|
||||||
func EmptyTheme() *ColorTheme {
|
func EmptyTheme() *ColorTheme {
|
||||||
@@ -635,6 +740,9 @@ func EmptyTheme() *ColorTheme {
|
|||||||
Input: ColorAttr{colUndefined, AttrUndefined},
|
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Fg: ColorAttr{colUndefined, AttrUndefined},
|
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Bg: ColorAttr{colUndefined, AttrUndefined},
|
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||||
@@ -650,6 +758,8 @@ func EmptyTheme() *ColorTheme {
|
|||||||
Header: ColorAttr{colUndefined, AttrUndefined},
|
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Border: ColorAttr{colUndefined, AttrUndefined},
|
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||||
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
@@ -659,6 +769,14 @@ func EmptyTheme() *ColorTheme {
|
|||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Scrollbar: 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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,6 +786,9 @@ func NoColorTheme() *ColorTheme {
|
|||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
ListFg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
ListBg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
||||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
||||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
||||||
@@ -690,8 +811,18 @@ func NoColorTheme() *ColorTheme {
|
|||||||
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
||||||
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||||
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
ListLabel: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
ListBorder: ColorAttr{colDefault, AttrUndefined},
|
||||||
Separator: ColorAttr{colDefault, AttrUndefined},
|
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||||
Scrollbar: 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},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -701,9 +832,12 @@ func init() {
|
|||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||||
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||||
Prompt: ColorAttr{colBlue, AttrUndefined},
|
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||||
Match: ColorAttr{colGreen, AttrUndefined},
|
Match: ColorAttr{colGreen, AttrUndefined},
|
||||||
@@ -723,17 +857,27 @@ func init() {
|
|||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Scrollbar: 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},
|
||||||
}
|
}
|
||||||
Dark256 = &ColorTheme{
|
Dark256 = &ColorTheme{
|
||||||
Colored: true,
|
Colored: true,
|
||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||||
DarkBg: ColorAttr{236, AttrUndefined},
|
DarkBg: ColorAttr{236, AttrUndefined},
|
||||||
Prompt: ColorAttr{110, AttrUndefined},
|
Prompt: ColorAttr{110, AttrUndefined},
|
||||||
Match: ColorAttr{108, AttrUndefined},
|
Match: ColorAttr{108, AttrUndefined},
|
||||||
@@ -753,17 +897,27 @@ func init() {
|
|||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Scrollbar: 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},
|
||||||
}
|
}
|
||||||
Light256 = &ColorTheme{
|
Light256 = &ColorTheme{
|
||||||
Colored: true,
|
Colored: true,
|
||||||
Input: ColorAttr{colDefault, AttrUndefined},
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
Fg: ColorAttr{colDefault, AttrUndefined},
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Bg: ColorAttr{colDefault, AttrUndefined},
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
SelectedFg: ColorAttr{colDefault, AttrUndefined},
|
ListFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedBg: ColorAttr{colDefault, AttrUndefined},
|
ListBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
SelectedMatch: ColorAttr{colDefault, AttrUndefined},
|
AltBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
SelectedMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||||
DarkBg: ColorAttr{251, AttrUndefined},
|
DarkBg: ColorAttr{251, AttrUndefined},
|
||||||
Prompt: ColorAttr{25, AttrUndefined},
|
Prompt: ColorAttr{25, AttrUndefined},
|
||||||
Match: ColorAttr{66, AttrUndefined},
|
Match: ColorAttr{66, AttrUndefined},
|
||||||
@@ -783,12 +937,22 @@ func init() {
|
|||||||
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
ListBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Separator: ColorAttr{colUndefined, AttrUndefined},
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Scrollbar: 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 initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
func InitTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool, hasInputWindow bool, hasHeaderWindow bool) {
|
||||||
if forceBlack {
|
if forceBlack {
|
||||||
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
||||||
}
|
}
|
||||||
@@ -809,7 +973,9 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
|||||||
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
||||||
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
theme.Prompt = o(baseTheme.Prompt, theme.Prompt)
|
||||||
theme.Match = o(baseTheme.Match, theme.Match)
|
theme.Match = o(baseTheme.Match, theme.Match)
|
||||||
theme.Current = o(baseTheme.Current, theme.Current)
|
// 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)
|
theme.CurrentMatch = o(baseTheme.CurrentMatch, theme.CurrentMatch)
|
||||||
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
|
theme.Spinner = o(baseTheme.Spinner, theme.Spinner)
|
||||||
theme.Info = o(baseTheme.Info, theme.Info)
|
theme.Info = o(baseTheme.Info, theme.Info)
|
||||||
@@ -819,19 +985,54 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
|||||||
theme.Border = o(baseTheme.Border, theme.Border)
|
theme.Border = o(baseTheme.Border, theme.Border)
|
||||||
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
||||||
|
|
||||||
|
undefined := NewColorAttr()
|
||||||
|
scrollbarDefined := theme.Scrollbar != undefined
|
||||||
|
previewBorderDefined := theme.PreviewBorder != undefined
|
||||||
|
|
||||||
// These colors are not defined in the base themes
|
// These colors are not defined in the base themes
|
||||||
theme.SelectedFg = o(theme.Fg, theme.SelectedFg)
|
theme.ListFg = o(theme.Fg, theme.ListFg)
|
||||||
theme.SelectedBg = o(theme.Bg, theme.SelectedBg)
|
theme.ListBg = o(theme.Bg, theme.ListBg)
|
||||||
theme.SelectedMatch = o(theme.CurrentMatch, theme.SelectedMatch)
|
theme.SelectedFg = o(theme.ListFg, theme.SelectedFg)
|
||||||
|
theme.SelectedBg = o(theme.ListBg, theme.SelectedBg)
|
||||||
|
theme.SelectedMatch = o(theme.Match, theme.SelectedMatch)
|
||||||
theme.Disabled = o(theme.Input, theme.Disabled)
|
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||||
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
||||||
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||||
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
||||||
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
||||||
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
|
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
|
||||||
theme.Separator = o(theme.Border, theme.Separator)
|
theme.ListLabel = o(theme.BorderLabel, theme.ListLabel)
|
||||||
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
|
theme.ListBorder = o(theme.Border, theme.ListBorder)
|
||||||
|
theme.Separator = o(theme.ListBorder, theme.Separator)
|
||||||
|
theme.Scrollbar = o(theme.ListBorder, theme.Scrollbar)
|
||||||
|
theme.GapLine = o(theme.ListBorder, theme.GapLine)
|
||||||
|
/*
|
||||||
|
--color list-border:green
|
||||||
|
--color scrollbar:red
|
||||||
|
--color scrollbar:red,list-border:green
|
||||||
|
--color scrollbar:red,preview-border:green
|
||||||
|
*/
|
||||||
|
if scrollbarDefined && !previewBorderDefined {
|
||||||
|
theme.PreviewScrollbar = o(theme.Scrollbar, theme.PreviewScrollbar)
|
||||||
|
} else {
|
||||||
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
|
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
|
||||||
|
}
|
||||||
|
if hasInputWindow {
|
||||||
|
theme.InputBg = o(theme.Bg, theme.InputBg)
|
||||||
|
} else {
|
||||||
|
// We shouldn't use input-bg if there's no separate input window
|
||||||
|
// e.g. fzf --color 'list-bg:green,input-bg:red' --no-input-border
|
||||||
|
theme.InputBg = o(theme.Bg, theme.ListBg)
|
||||||
|
}
|
||||||
|
theme.InputBorder = o(theme.Border, theme.InputBorder)
|
||||||
|
theme.InputLabel = o(theme.BorderLabel, theme.InputLabel)
|
||||||
|
if hasHeaderWindow {
|
||||||
|
theme.HeaderBg = o(theme.Bg, theme.HeaderBg)
|
||||||
|
} else {
|
||||||
|
theme.HeaderBg = o(theme.Bg, theme.ListBg)
|
||||||
|
}
|
||||||
|
theme.HeaderBorder = o(theme.Border, theme.HeaderBorder)
|
||||||
|
theme.HeaderLabel = o(theme.BorderLabel, theme.HeaderLabel)
|
||||||
|
|
||||||
initPalette(theme)
|
initPalette(theme)
|
||||||
}
|
}
|
||||||
@@ -843,19 +1044,19 @@ func initPalette(theme *ColorTheme) {
|
|||||||
}
|
}
|
||||||
return ColorPair{fg.Color, bg.Color, fg.Attr}
|
return ColorPair{fg.Color, bg.Color, fg.Attr}
|
||||||
}
|
}
|
||||||
blank := theme.Fg
|
blank := theme.ListFg
|
||||||
blank.Attr = AttrRegular
|
blank.Attr = AttrRegular
|
||||||
|
|
||||||
ColPrompt = pair(theme.Prompt, theme.Bg)
|
ColPrompt = pair(theme.Prompt, theme.InputBg)
|
||||||
ColNormal = pair(theme.Fg, theme.Bg)
|
ColNormal = pair(theme.ListFg, theme.ListBg)
|
||||||
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
|
ColSelected = pair(theme.SelectedFg, theme.SelectedBg)
|
||||||
ColInput = pair(theme.Input, theme.Bg)
|
ColInput = pair(theme.Input, theme.InputBg)
|
||||||
ColDisabled = pair(theme.Disabled, theme.Bg)
|
ColDisabled = pair(theme.Disabled, theme.ListBg)
|
||||||
ColMatch = pair(theme.Match, theme.Bg)
|
ColMatch = pair(theme.Match, theme.ListBg)
|
||||||
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
|
ColSelectedMatch = pair(theme.SelectedMatch, theme.SelectedBg)
|
||||||
ColCursor = pair(theme.Cursor, theme.Gutter)
|
ColCursor = pair(theme.Cursor, theme.Gutter)
|
||||||
ColCursorEmpty = pair(blank, theme.Gutter)
|
ColCursorEmpty = pair(blank, theme.Gutter)
|
||||||
if theme.SelectedBg.Color != theme.Bg.Color {
|
if theme.SelectedBg.Color != theme.ListBg.Color {
|
||||||
ColMarker = pair(theme.Marker, theme.SelectedBg)
|
ColMarker = pair(theme.Marker, theme.SelectedBg)
|
||||||
} else {
|
} else {
|
||||||
ColMarker = pair(theme.Marker, theme.Gutter)
|
ColMarker = pair(theme.Marker, theme.Gutter)
|
||||||
@@ -866,11 +1067,11 @@ func initPalette(theme *ColorTheme) {
|
|||||||
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
|
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
|
||||||
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
|
ColCurrentMarker = pair(theme.Marker, theme.DarkBg)
|
||||||
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
|
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
|
||||||
ColSpinner = pair(theme.Spinner, theme.Bg)
|
ColSpinner = pair(theme.Spinner, theme.InputBg)
|
||||||
ColInfo = pair(theme.Info, theme.Bg)
|
ColInfo = pair(theme.Info, theme.InputBg)
|
||||||
ColHeader = pair(theme.Header, theme.Bg)
|
ColSeparator = pair(theme.Separator, theme.InputBg)
|
||||||
ColSeparator = pair(theme.Separator, theme.Bg)
|
ColScrollbar = pair(theme.Scrollbar, theme.ListBg)
|
||||||
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
|
ColGapLine = pair(theme.GapLine, theme.ListBg)
|
||||||
ColBorder = pair(theme.Border, theme.Bg)
|
ColBorder = pair(theme.Border, theme.Bg)
|
||||||
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
|
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
|
||||||
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
|
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
|
||||||
@@ -878,6 +1079,13 @@ func initPalette(theme *ColorTheme) {
|
|||||||
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
|
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
|
||||||
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
|
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
|
||||||
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
|
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
|
||||||
|
ColListLabel = pair(theme.ListLabel, theme.ListBg)
|
||||||
|
ColListBorder = pair(theme.ListBorder, theme.ListBg)
|
||||||
|
ColInputBorder = pair(theme.InputBorder, theme.InputBg)
|
||||||
|
ColInputLabel = pair(theme.InputLabel, theme.InputBg)
|
||||||
|
ColHeader = pair(theme.Header, theme.HeaderBg)
|
||||||
|
ColHeaderBorder = pair(theme.HeaderBorder, theme.HeaderBg)
|
||||||
|
ColHeaderLabel = pair(theme.HeaderLabel, theme.HeaderBg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runeWidth(r rune) int {
|
func runeWidth(r rune) int {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -74,6 +75,35 @@ func (chars *Chars) Bytes() []byte {
|
|||||||
return chars.slice
|
return chars.slice
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) NumLines(atMost int) (int, bool) {
|
||||||
|
lines := 1
|
||||||
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
|
for _, r := range runes {
|
||||||
|
if r == '\n' {
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
if lines > atMost {
|
||||||
|
return atMost, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 0; idx < len(chars.slice); idx++ {
|
||||||
|
found := bytes.IndexByte(chars.slice[idx:], '\n')
|
||||||
|
if found < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
idx += found
|
||||||
|
lines++
|
||||||
|
if lines > atMost {
|
||||||
|
return atMost, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines, false
|
||||||
|
}
|
||||||
|
|
||||||
func (chars *Chars) optionalRunes() []rune {
|
func (chars *Chars) optionalRunes() []rune {
|
||||||
if chars.inBytes {
|
if chars.inBytes {
|
||||||
return nil
|
return nil
|
||||||
@@ -159,6 +189,27 @@ func (chars *Chars) TrimTrailingWhitespaces() {
|
|||||||
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (chars *Chars) ToString() string {
|
||||||
if runes := chars.optionalRunes(); runes != nil {
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
return string(runes)
|
return string(runes)
|
||||||
@@ -196,3 +247,88 @@ func (chars *Chars) Prepend(prefix string) {
|
|||||||
chars.slice = append([]byte(prefix), chars.slice...)
|
chars.slice = append([]byte(prefix), chars.slice...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) Lines(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int) ([][]rune, bool) {
|
||||||
|
text := make([]rune, chars.Length())
|
||||||
|
copy(text, chars.ToRunes())
|
||||||
|
|
||||||
|
lines := [][]rune{}
|
||||||
|
overflow := false
|
||||||
|
if !multiLine {
|
||||||
|
lines = append(lines, text)
|
||||||
|
} else {
|
||||||
|
from := 0
|
||||||
|
for off := 0; off < len(text); off++ {
|
||||||
|
if text[off] == '\n' {
|
||||||
|
lines = append(lines, text[from:off+1]) // Include '\n'
|
||||||
|
from = off + 1
|
||||||
|
if len(lines) >= maxLines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastLine []rune
|
||||||
|
if from < len(text) {
|
||||||
|
lastLine = text[from:]
|
||||||
|
}
|
||||||
|
|
||||||
|
overflow = false
|
||||||
|
if len(lines) >= maxLines {
|
||||||
|
overflow = true
|
||||||
|
} else {
|
||||||
|
lines = append(lines, lastLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If wrapping is disabled, we're done
|
||||||
|
if wrapCols == 0 {
|
||||||
|
return lines, overflow
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped := [][]rune{}
|
||||||
|
for _, line := range lines {
|
||||||
|
// Remove trailing '\n' and remember if it was there
|
||||||
|
newline := len(line) > 0 && line[len(line)-1] == '\n'
|
||||||
|
if newline {
|
||||||
|
line = line[:len(line)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
hasWrapSign := false
|
||||||
|
for {
|
||||||
|
cols := wrapCols
|
||||||
|
if hasWrapSign {
|
||||||
|
cols -= wrapSignWidth
|
||||||
|
}
|
||||||
|
_, overflowIdx := RunesWidth(line, 0, tabstop, cols)
|
||||||
|
if overflowIdx >= 0 {
|
||||||
|
// Might be a wide character
|
||||||
|
if overflowIdx == 0 {
|
||||||
|
overflowIdx = 1
|
||||||
|
}
|
||||||
|
if len(wrapped) >= maxLines {
|
||||||
|
return wrapped, true
|
||||||
|
}
|
||||||
|
wrapped = append(wrapped, line[:overflowIdx])
|
||||||
|
hasWrapSign = true
|
||||||
|
line = line[overflowIdx:]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasWrapSign = false
|
||||||
|
|
||||||
|
// Restore trailing '\n'
|
||||||
|
if newline {
|
||||||
|
line = append(line, '\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(wrapped) >= maxLines {
|
||||||
|
return wrapped, true
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapped = append(wrapped, line)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapped, overflow
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestToCharsAscii(t *testing.T) {
|
func TestToCharsAscii(t *testing.T) {
|
||||||
chars := ToChars([]byte("foobar"))
|
chars := ToChars([]byte("foobar"))
|
||||||
@@ -44,3 +47,37 @@ func TestTrimLength(t *testing.T) {
|
|||||||
check(" h o ", 5)
|
check(" h o ", 5)
|
||||||
check(" ", 0)
|
check(" ", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCharsLines(t *testing.T) {
|
||||||
|
chars := ToChars([]byte("abcdef\n가나다\n\tdef"))
|
||||||
|
check := func(multiLine bool, maxLines int, wrapCols int, wrapSignWidth int, tabstop int, expectedNumLines int, expectedOverflow bool) {
|
||||||
|
lines, overflow := chars.Lines(multiLine, maxLines, wrapCols, wrapSignWidth, tabstop)
|
||||||
|
fmt.Println(lines, overflow)
|
||||||
|
if len(lines) != expectedNumLines || overflow != expectedOverflow {
|
||||||
|
t.Errorf("Invalid result: %d %v (expected %d %v)", len(lines), overflow, expectedNumLines, expectedOverflow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No wrap
|
||||||
|
check(true, 1, 0, 0, 8, 1, true)
|
||||||
|
check(true, 2, 0, 0, 8, 2, true)
|
||||||
|
check(true, 3, 0, 0, 8, 3, false)
|
||||||
|
|
||||||
|
// Wrap (2)
|
||||||
|
check(true, 4, 2, 0, 8, 4, true)
|
||||||
|
check(true, 5, 2, 0, 8, 5, true)
|
||||||
|
check(true, 6, 2, 0, 8, 6, true)
|
||||||
|
check(true, 7, 2, 0, 8, 7, true)
|
||||||
|
check(true, 8, 2, 0, 8, 8, true)
|
||||||
|
check(true, 9, 2, 0, 8, 9, false)
|
||||||
|
check(true, 9, 2, 0, 1, 8, false) // Smaller tab size
|
||||||
|
|
||||||
|
// With wrap sign (3 + 1)
|
||||||
|
check(true, 100, 3, 1, 1, 8, false)
|
||||||
|
|
||||||
|
// With wrap sign (3 + 2)
|
||||||
|
check(true, 100, 3, 2, 1, 10, false)
|
||||||
|
|
||||||
|
// With wrap sign (3 + 2) and no multi-line
|
||||||
|
check(false, 100, 3, 2, 1, 13, false)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import "sync"
|
|||||||
type EventType int
|
type EventType int
|
||||||
|
|
||||||
// Events is a type that associates EventType to any data
|
// 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
|
// EventBox is used for coordinating events
|
||||||
type EventBox struct {
|
type EventBox struct {
|
||||||
@@ -36,7 +36,7 @@ func (b *EventBox) Wait(callback func(*Events)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set turns on the event type on the box
|
// 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.cond.L.Lock()
|
||||||
b.events[event] = value
|
b.events[event] = value
|
||||||
if _, found := b.ignore[event]; !found {
|
if _, found := b.ignore[event]; !found {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package util
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -137,14 +138,20 @@ func DurWithin(
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTty returns true if stdin is a terminal
|
// IsTty returns true if the file is a terminal
|
||||||
func IsTty() bool {
|
func IsTty(file *os.File) bool {
|
||||||
return isatty.IsTerminal(os.Stdin.Fd())
|
fd := file.Fd()
|
||||||
|
return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToTty returns true if stdout is a terminal
|
// RunOnce runs the given function only once
|
||||||
func ToTty() bool {
|
func RunOnce(f func()) func() {
|
||||||
return isatty.IsTerminal(os.Stdout.Fd())
|
once := Once(true)
|
||||||
|
return func() {
|
||||||
|
if once() {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once returns a function that returns the specified boolean value only once
|
// Once returns a function that returns the specified boolean value only once
|
||||||
@@ -152,7 +159,7 @@ func Once(nextResponse bool) func() bool {
|
|||||||
state := nextResponse
|
state := nextResponse
|
||||||
return func() bool {
|
return func() bool {
|
||||||
prevState := state
|
prevState := state
|
||||||
state = false
|
state = !nextResponse
|
||||||
return prevState
|
return prevState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,3 +195,34 @@ func ToKebabCase(s string) string {
|
|||||||
}
|
}
|
||||||
return strings.ToLower(name)
|
return strings.ToLower(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CompareVersions compares two version strings
|
||||||
|
func CompareVersions(v1, v2 string) int {
|
||||||
|
parts1 := strings.Split(v1, ".")
|
||||||
|
parts2 := strings.Split(v2, ".")
|
||||||
|
|
||||||
|
atoi := func(s string) int {
|
||||||
|
n, e := strconv.Atoi(s)
|
||||||
|
if e != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < Max(len(parts1), len(parts2)); i++ {
|
||||||
|
var p1, p2 int
|
||||||
|
if i < len(parts1) {
|
||||||
|
p1 = atoi(parts1[i])
|
||||||
|
}
|
||||||
|
if i < len(parts2) {
|
||||||
|
p2 = atoi(parts2[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
if p1 > p2 {
|
||||||
|
return 1
|
||||||
|
} else if p1 < p2 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -137,8 +137,11 @@ func TestOnce(t *testing.T) {
|
|||||||
if o() {
|
if o() {
|
||||||
t.Error("Expected: false")
|
t.Error("Expected: false")
|
||||||
}
|
}
|
||||||
if o() {
|
if !o() {
|
||||||
t.Error("Expected: false")
|
t.Error("Expected: true")
|
||||||
|
}
|
||||||
|
if !o() {
|
||||||
|
t.Error("Expected: true")
|
||||||
}
|
}
|
||||||
|
|
||||||
o = Once(true)
|
o = Once(true)
|
||||||
@@ -148,6 +151,9 @@ func TestOnce(t *testing.T) {
|
|||||||
if o() {
|
if o() {
|
||||||
t.Error("Expected: false")
|
t.Error("Expected: false")
|
||||||
}
|
}
|
||||||
|
if o() {
|
||||||
|
t.Error("Expected: false")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunesWidth(t *testing.T) {
|
func TestRunesWidth(t *testing.T) {
|
||||||
@@ -203,3 +209,34 @@ func TestStringWidth(t *testing.T) {
|
|||||||
t.Errorf("Expected: %d, Actual: %d", 1, w)
|
t.Errorf("Expected: %d, Actual: %d", 1, w)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCompareVersions(t *testing.T) {
|
||||||
|
assert := func(a, b string, expected int) {
|
||||||
|
if result := CompareVersions(a, b); result != expected {
|
||||||
|
t.Errorf("Expected: %d, Actual: %d", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert("2", "1", 1)
|
||||||
|
assert("2", "2", 0)
|
||||||
|
assert("2", "10", -1)
|
||||||
|
|
||||||
|
assert("2.1", "2.2", -1)
|
||||||
|
assert("2.1", "2.1.1", -1)
|
||||||
|
|
||||||
|
assert("1.2.3", "1.2.2", 1)
|
||||||
|
assert("1.2.3", "1.2.3", 0)
|
||||||
|
assert("1.2.3", "1.2.3.0", 0)
|
||||||
|
assert("1.2.3", "1.2.4", -1)
|
||||||
|
|
||||||
|
// Different number of parts
|
||||||
|
assert("1.0.0", "1", 0)
|
||||||
|
assert("1.0.0", "1.0", 0)
|
||||||
|
assert("1.0.0", "1.0.0", 0)
|
||||||
|
assert("1.0", "1.0.0", 0)
|
||||||
|
assert("1", "1.0.0", 0)
|
||||||
|
assert("1.0.0", "1.0.0.1", -1)
|
||||||
|
assert("1.0.0.1.0", "1.0.0.1", 0)
|
||||||
|
|
||||||
|
assert("", "3.4.5", -1)
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,10 +157,10 @@ func (x *Executor) QuoteEntry(entry string) string {
|
|||||||
*/
|
*/
|
||||||
return escapeArg(entry)
|
return escapeArg(entry)
|
||||||
case shellTypePowerShell:
|
case shellTypePowerShell:
|
||||||
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
escaped := strings.ReplaceAll(entry, `"`, `\"`)
|
||||||
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
return "'" + strings.ReplaceAll(escaped, "'", "''") + "'"
|
||||||
default:
|
default:
|
||||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
return "'" + strings.ReplaceAll(entry, "'", "'\\''") + "'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
13
src/winpty.go
Normal file
13
src/winpty.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package fzf
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
func needWinpty(_ *Options) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWinpty(_ []string, _ *Options) (int, error) {
|
||||||
|
return ExitError, errors.New("Not supported")
|
||||||
|
}
|
||||||
80
src/winpty_windows.go
Normal file
80
src/winpty_windows.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package fzf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func isMintty345() bool {
|
||||||
|
return util.CompareVersions(os.Getenv("TERM_PROGRAM_VERSION"), "3.4.5") >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func needWinpty(opts *Options) bool {
|
||||||
|
if os.Getenv("TERM_PROGRAM") != "mintty" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if isMintty345() {
|
||||||
|
/*
|
||||||
|
See: https://github.com/junegunn/fzf/issues/3809
|
||||||
|
|
||||||
|
"MSYS=enable_pcon" allows fzf to run properly on mintty 3.4.5 or later.
|
||||||
|
*/
|
||||||
|
if strings.Contains(os.Getenv("MSYS"), "enable_pcon") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting the environment variable here unfortunately doesn't help,
|
||||||
|
// so we need to start a child process with "MSYS=enable_pcon"
|
||||||
|
// os.Setenv("MSYS", "enable_pcon")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if opts.NoWinpty {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("winpty"); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWinpty(args []string, opts *Options) (int, error) {
|
||||||
|
argStr := escapeSingleQuote(args[0])
|
||||||
|
for _, arg := range args[1:] {
|
||||||
|
argStr += " " + escapeSingleQuote(arg)
|
||||||
|
}
|
||||||
|
argStr += ` --no-winpty`
|
||||||
|
|
||||||
|
if isMintty345() {
|
||||||
|
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||||
|
sh, err := sh(needBash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(sh, temp)
|
||||||
|
cmd.Env = append(os.Environ(), "MSYS=enable_pcon")
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd, nil
|
||||||
|
}, opts, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return runProxy(argStr, func(temp string, needBash bool) (*exec.Cmd, error) {
|
||||||
|
sh, err := sh(needBash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(sh, "-c", fmt.Sprintf(`winpty < /dev/tty > /dev/tty -- sh %q`, temp))
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd, nil
|
||||||
|
}, opts, false)
|
||||||
|
}
|
||||||
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 --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'
|
||||||
1942
test/test_core.rb
Normal file
1942
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: [' x '] + %w[/x/ [x] -x- -x_ _x- _x_],
|
||||||
|
path: ['/x/', ' x '] + %w[[x] -x- -x_ _x- _x_],
|
||||||
|
history: ['[x]', '-x-', ' x '] + %w[/x/ -x_ _x- _x_]
|
||||||
|
}.each do |scheme, expected|
|
||||||
|
result = `printf -- 'xxx\n-xx\nxx-\n_x_\n_x-\n-x_\n[x]\n-x-\n x \n/x/\n' | #{FZF} -f"'x'" --scheme=#{scheme}`.lines(chomp: true)
|
||||||
|
assert_equal expected, result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
3797
test/test_go.rb
3797
test/test_go.rb
File diff suppressed because it is too large
Load Diff
1006
test/test_layout.rb
Normal file
1006
test/test_layout.rb
Normal file
File diff suppressed because it is too large
Load Diff
561
test/test_preview.rb
Normal file
561
test/test_preview.rb
Normal file
@@ -0,0 +1,561 @@
|
|||||||
|
# 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_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
|
||||||
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)
|
execute 'lcd' fnameescape(cwd)
|
||||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
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 1, haslocaldir()
|
||||||
AssertEqual getcwd(), cwd
|
AssertEqual getcwd(), cwd
|
||||||
|
|
||||||
@@ -23,8 +23,8 @@ Execute (fzf#run with Funcref command):
|
|||||||
call add(g:ret, a:e)
|
call add(g:ret, a:e)
|
||||||
endfunction
|
endfunction
|
||||||
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
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'], result
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
AssertEqual ['fzf.vader'], sort(g:ret)
|
||||||
|
|
||||||
Execute (fzf#run with string source):
|
Execute (fzf#run with string source):
|
||||||
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
let result = sort(fzf#run({ 'source': 'echo hi', 'options': '-f i' }))
|
||||||
@@ -78,18 +78,18 @@ Execute (fzf#wrap):
|
|||||||
|
|
||||||
let opts = fzf#wrap('foobar')
|
let opts = fzf#wrap('foobar')
|
||||||
Log opts
|
Log opts
|
||||||
AssertEqual '~40%', opts.down
|
AssertEqual 0.9, opts.window.width
|
||||||
Assert opts.options =~ '--expect='
|
Assert opts.options =~ '--expect='
|
||||||
Assert !has_key(opts, 'sink')
|
Assert !has_key(opts, 'sink')
|
||||||
Assert has_key(opts, 'sink*')
|
Assert has_key(opts, 'sink*')
|
||||||
|
|
||||||
let opts = fzf#wrap('foobar', {}, 0)
|
let opts = fzf#wrap('foobar', {}, 0)
|
||||||
Log opts
|
Log opts
|
||||||
AssertEqual '~40%', opts.down
|
AssertEqual 0.9, opts.window.width
|
||||||
|
|
||||||
let opts = fzf#wrap('foobar', {}, 1)
|
let opts = fzf#wrap('foobar', {}, 1)
|
||||||
Log opts
|
Log opts
|
||||||
Assert !has_key(opts, 'down')
|
Assert !has_key(opts, 'window')
|
||||||
|
|
||||||
let opts = fzf#wrap('foobar', {'down': '50%'})
|
let opts = fzf#wrap('foobar', {'down': '50%'})
|
||||||
Log opts
|
Log opts
|
||||||
@@ -148,7 +148,7 @@ Execute (fzf#wrap):
|
|||||||
|
|
||||||
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
||||||
let opts = fzf#wrap({})
|
let opts = fzf#wrap({})
|
||||||
Assert opts.options =~ '^--color=fg:'
|
Assert opts.options =~ '--color=fg:'
|
||||||
|
|
||||||
Execute (fzf#shellescape with sh):
|
Execute (fzf#shellescape with sh):
|
||||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
AssertEqual '''''', fzf#shellescape('', 'sh')
|
||||||
Reference in New Issue
Block a user