mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-12 05:13:49 -05:00
Compare commits
1288 Commits
0.29.0
...
adjust-def
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc955771c | ||
|
|
c134c6f898 | ||
|
|
95697b96f8 | ||
|
|
3809ebb0c8 | ||
|
|
ca9b4d5a49 | ||
|
|
04888f5b94 | ||
|
|
3072b46218 | ||
|
|
a67aa85820 | ||
|
|
c5cabe1691 | ||
|
|
cbed41cd82 | ||
|
|
6684771cbf | ||
|
|
f5f894ea47 | ||
|
|
a0a334fc8d | ||
|
|
ae12e94b1f | ||
|
|
9ed971cc90 | ||
|
|
129cb23078 | ||
|
|
d22812e917 | ||
|
|
10d712824a | ||
|
|
de4059c8fa | ||
|
|
416aff86e9 | ||
|
|
59dc7f178f | ||
|
|
a3c9f8bfee | ||
|
|
5546c65491 | ||
|
|
f2179f015c | ||
|
|
9a53d84b9c | ||
|
|
0a8ff7899c | ||
|
|
f9d7877d8b | ||
|
|
9fe9976591 | ||
|
|
de1824f71d | ||
|
|
19a9296c47 | ||
|
|
49967f3d45 | ||
|
|
978b6254c7 | ||
|
|
1afd143810 | ||
|
|
e5cd7f0a3a | ||
|
|
51d3940c63 | ||
|
|
179aec1578 | ||
|
|
af0014aba8 | ||
|
|
da3d995709 | ||
|
|
04c4269db3 | ||
|
|
78f238294f | ||
|
|
354d0468c1 | ||
|
|
4efcc344c3 | ||
|
|
5818b58350 | ||
|
|
7941129cc4 | ||
|
|
069d71a840 | ||
|
|
08027e7a79 | ||
|
|
ead302981c | ||
|
|
fe0ffa14ff | ||
|
|
821b8e70a8 | ||
|
|
8ceda54c7d | ||
|
|
84e515bd6e | ||
|
|
dea1df6878 | ||
|
|
0076ec2e8d | ||
|
|
82c9671f79 | ||
|
|
d364a1122e | ||
|
|
fb570e94e7 | ||
|
|
6e3c830cd2 | ||
|
|
d7db7fc132 | ||
|
|
ff1550bb38 | ||
|
|
976001e474 | ||
|
|
531dd6fb4f | ||
|
|
ba035f2a76 | ||
|
|
d34675d3c9 | ||
|
|
ce95adc66c | ||
|
|
397fe8e395 | ||
|
|
111266d832 | ||
|
|
19d858f9b6 | ||
|
|
79690724d8 | ||
|
|
5ed87ffcb9 | ||
|
|
b99cb6323f | ||
|
|
debf3d8a8a | ||
|
|
4811e52af3 | ||
|
|
8d81730ec2 | ||
|
|
330a85c25c | ||
|
|
3a21116307 | ||
|
|
247d168af6 | ||
|
|
b2a8a283c7 | ||
|
|
c36ddce36f | ||
|
|
c35d9cff7d | ||
|
|
549ce3cf6c | ||
|
|
575bc0768c | ||
|
|
89334e881e | ||
|
|
dcec6354f5 | ||
|
|
16d338da84 | ||
|
|
27258f7207 | ||
|
|
4d2d6a5ced | ||
|
|
0c00b203e6 | ||
|
|
3b68dcdd81 | ||
|
|
39db026161 | ||
|
|
f6c589c606 | ||
|
|
2bd29c3172 | ||
|
|
4a61f53b85 | ||
|
|
adc9ad28da | ||
|
|
585cfaef8b | ||
|
|
b5cd8880b1 | ||
|
|
44ddab881e | ||
|
|
bfa287b66d | ||
|
|
243e52fa11 | ||
|
|
c166eaba6d | ||
|
|
09194c24f2 | ||
|
|
ec521e47aa | ||
|
|
e3f4a51c18 | ||
|
|
0a06fd6f63 | ||
|
|
70eace5290 | ||
|
|
40f9f254a9 | ||
|
|
15d6c17390 | ||
|
|
a9d1d42436 | ||
|
|
1ecfa38eee | ||
|
|
54fd92b7dd | ||
|
|
835906d392 | ||
|
|
1721e6a1ed | ||
|
|
c7ee3b833f | ||
|
|
ffb6e28ca7 | ||
|
|
a4c6846851 | ||
|
|
d18c0bf694 | ||
|
|
4e3f9854e6 | ||
|
|
b27943423e | ||
|
|
894a1016bc | ||
|
|
efe6cddd34 | ||
|
|
f1c6bdf3e8 | ||
|
|
710659bcf5 | ||
|
|
be67775da4 | ||
|
|
2c6381499c | ||
|
|
4df842e78c | ||
|
|
b81696fb64 | ||
|
|
d226d841a1 | ||
|
|
c6d83047e5 | ||
|
|
46dabccdf1 | ||
|
|
cd9517b679 | ||
|
|
cd6677ba1d | ||
|
|
9c1a47acf7 | ||
|
|
0c280a3ce1 | ||
|
|
53e8b6e705 | ||
|
|
ad33165fa7 | ||
|
|
2055db61c8 | ||
|
|
d2c662e54f | ||
|
|
d24b58ef3f | ||
|
|
06ae9b0f3b | ||
|
|
2a9c1c06a4 | ||
|
|
90ad1b7f22 | ||
|
|
f22fbcd1af | ||
|
|
1d761684c5 | ||
|
|
e491770f1c | ||
|
|
a41be61506 | ||
|
|
1a8f633611 | ||
|
|
af8fe918d8 | ||
|
|
8ef9dfd9a2 | ||
|
|
66df24040f | ||
|
|
ed4442d9ea | ||
|
|
0edb5d5ebb | ||
|
|
9ffc2c7ca3 | ||
|
|
93cb3758b5 | ||
|
|
d22e75dcdd | ||
|
|
a1b2a6fe2c | ||
|
|
e15cba0c8c | ||
|
|
31fd207ba2 | ||
|
|
ba6d1b8772 | ||
|
|
0dce561ec9 | ||
|
|
376142eb0d | ||
|
|
664ee1f483 | ||
|
|
dac5b6fde1 | ||
|
|
998c57442b | ||
|
|
4a0ab6c926 | ||
|
|
f43e82f17f | ||
|
|
62238620a5 | ||
|
|
200745011a | ||
|
|
82fd88339b | ||
|
|
de0f2efbfb | ||
|
|
29cf28d845 | ||
|
|
7e4dbb5f3b | ||
|
|
923c3a814d | ||
|
|
779e3cc5b5 | ||
|
|
3f3d1ef8f5 | ||
|
|
f92f9f137a | ||
|
|
87f7f436e8 | ||
|
|
4298c0b1eb | ||
|
|
6c104d771e | ||
|
|
aefb9a5bc4 | ||
|
|
8868d7cbb8 | ||
|
|
10cbac20f9 | ||
|
|
26bcd0c90d | ||
|
|
fbece2bb67 | ||
|
|
0012183ede | ||
|
|
8916cbc6ab | ||
|
|
21ce70054f | ||
|
|
3ba82b6d87 | ||
|
|
e771c5d057 | ||
|
|
4e5e925e39 | ||
|
|
b7248d4115 | ||
|
|
639253840f | ||
|
|
710ebdf9c1 | ||
|
|
bb64d84ce4 | ||
|
|
cd1da27ff2 | ||
|
|
c1accc2e5b | ||
|
|
e4489dcbc1 | ||
|
|
c0d407f7ce | ||
|
|
461115afde | ||
|
|
bae1965231 | ||
|
|
b89c77ec9a | ||
|
|
1ca5f09d7b | ||
|
|
d79902ae59 | ||
|
|
77568e114f | ||
|
|
a24d274a3c | ||
|
|
dac81432d6 | ||
|
|
309b5081ef | ||
|
|
91bc4f2671 | ||
|
|
4c9d37d919 | ||
|
|
7e9566f66a | ||
|
|
3f7e8a475d | ||
|
|
1cf7c0f334 | ||
|
|
ff8ee9ee4e | ||
|
|
cbbd939a94 | ||
|
|
f232df2887 | ||
|
|
16bfb2c80c | ||
|
|
0ba066123e | ||
|
|
81c51c26cc | ||
|
|
6fa8295ac5 | ||
|
|
f975b40236 | ||
|
|
01d9d9c8c8 | ||
|
|
1eafc4e5d9 | ||
|
|
38e4020aa8 | ||
|
|
ac32fbb3b2 | ||
|
|
7d26eca5cc | ||
|
|
3347d61591 | ||
|
|
9abf2c8c9c | ||
|
|
84e2262ad6 | ||
|
|
378137d34a | ||
|
|
66ca16f836 | ||
|
|
282884ad83 | ||
|
|
7877ac42f0 | ||
|
|
19ef8891e3 | ||
|
|
bfea9e53a6 | ||
|
|
a2420026ab | ||
|
|
1be1991299 | ||
|
|
67dd7e1923 | ||
|
|
2b584586ed | ||
|
|
a1994ff0ab | ||
|
|
ca0e858871 | ||
|
|
06c6615507 | ||
|
|
818d0be436 | ||
|
|
fcd2baa945 | ||
|
|
62e0a2824a | ||
|
|
bbe1721a18 | ||
|
|
c1470a51b8 | ||
|
|
6ee31d5dc5 | ||
|
|
65d74387e7 | ||
|
|
7d0ea599c4 | ||
|
|
b7795a3dea | ||
|
|
323f6f6202 | ||
|
|
0c61223884 | ||
|
|
32234be7a2 | ||
|
|
178b49832e | ||
|
|
18cbb4a84d | ||
|
|
e84afe196a | ||
|
|
e1e171a3c4 | ||
|
|
d075c00015 | ||
|
|
6c0ca4a64a | ||
|
|
6b5d461411 | ||
|
|
7419e0dde1 | ||
|
|
cf2bb5e40e | ||
|
|
f466e94d65 | ||
|
|
eb0257d48f | ||
|
|
b83dd6c6b4 | ||
|
|
51c207448d | ||
|
|
a6a558da30 | ||
|
|
2bf5fa27be | ||
|
|
af7940746f | ||
|
|
a2aa1a156c | ||
|
|
2f8a72a42a | ||
|
|
8179ca5eaa | ||
|
|
4b74f882c7 | ||
|
|
7cf45af502 | ||
|
|
46c21158d8 | ||
|
|
80da0776f8 | ||
|
|
e91f10ab16 | ||
|
|
2c15cd7923 | ||
|
|
d6584543e9 | ||
|
|
c13228f346 | ||
|
|
7220d8233e | ||
|
|
0237bf09bf | ||
|
|
04017c25bb | ||
|
|
02199cd609 | ||
|
|
26b9f5831a | ||
|
|
243a76002c | ||
|
|
c71e4ddee4 | ||
|
|
32eb8c1be9 | ||
|
|
c587017830 | ||
|
|
fb885652cc | ||
|
|
afc2f05e5e | ||
|
|
06547d0cbe | ||
|
|
578108280e | ||
|
|
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 | ||
|
|
6432f00f0d | ||
|
|
4e9e842aa4 | ||
|
|
07880ca441 | ||
|
|
bcda25a513 | ||
|
|
8256fcde15 | ||
|
|
af65aa298a | ||
|
|
6834d17844 | ||
|
|
ed511d7867 | ||
|
|
cd8d736a9f | ||
|
|
0952b2dfd4 | ||
|
|
4bedd33c59 | ||
|
|
c5fb0c43f9 | ||
|
|
9e4780510e | ||
|
|
e8405f40fe | ||
|
|
065b9e6fb2 | ||
|
|
98141ca7d8 | ||
|
|
501577ab28 | ||
|
|
5669f48343 | ||
|
|
24ff66d4a9 | ||
|
|
bf184449bc | ||
|
|
7b98c2c653 | ||
|
|
b6add2a257 | ||
|
|
2bd41f1330 | ||
|
|
c37cd11ca5 | ||
|
|
9dee8edc0c | ||
|
|
f6aa28c380 | ||
|
|
dba1644518 | ||
|
|
260a65b0fb | ||
|
|
835d2fb98c | ||
|
|
a9811addaa | ||
|
|
ee9d88b637 | ||
|
|
194a763c46 | ||
|
|
8d74446bef | ||
|
|
7ed6c7905c | ||
|
|
159a37fa37 | ||
|
|
f39ae0e7c1 | ||
|
|
4a68eac99b | ||
|
|
2665580120 | ||
|
|
a4391aeedd | ||
|
|
b86a967ee2 | ||
|
|
608232568b | ||
|
|
7f85beccb5 | ||
|
|
767f1255ab | ||
|
|
fddbfe7b0e | ||
|
|
4ab7fdc28e | ||
|
|
e352b68878 | ||
|
|
207deeadba | ||
|
|
d18d92f925 | ||
|
|
af3ce47c44 | ||
|
|
d8bfb6712d | ||
|
|
f864f8b5f7 | ||
|
|
31d72efba7 | ||
|
|
d169c951f3 | ||
|
|
90d7e38909 | ||
|
|
938f23e429 | ||
|
|
f97d275413 | ||
|
|
3acb4ca90e | ||
|
|
e86b81bbf5 | ||
|
|
a5447b8b75 | ||
|
|
7ce6452d83 | ||
|
|
5643a306bd | ||
|
|
3c877c504b | ||
|
|
892d1acccb | ||
|
|
1a9c282f76 | ||
|
|
fd1ba46f77 | ||
|
|
a4745626dd | ||
|
|
17bb7ad278 | ||
|
|
152988c17b | ||
|
|
4cd37fc02b | ||
|
|
69b9d674a3 | ||
|
|
bad8061547 | ||
|
|
62963dcefd | ||
|
|
68a35e4735 | ||
|
|
9b9ad77e1c | ||
|
|
118b4d4a01 | ||
|
|
da14ab6f16 | ||
|
|
09a4ca6ab5 | ||
|
|
8a2df79711 | ||
|
|
c30e486b64 | ||
|
|
a575c0c54b | ||
|
|
77fe96ac0d | ||
|
|
5234c3759a | ||
|
|
41b3511ad9 | ||
|
|
128e4a2e8d | ||
|
|
07ac90d798 | ||
|
|
7de87a9b2c | ||
|
|
dff865239a | ||
|
|
07f8f70c5b | ||
|
|
f625c5aabe | ||
|
|
8a74976c1f | ||
|
|
b6bfd4a5cb | ||
|
|
008fb9d258 | ||
|
|
db6db49ed6 | ||
|
|
05453881c3 | ||
|
|
5e47ab9431 | ||
|
|
ec70acd0b9 | ||
|
|
25e61056b6 | ||
|
|
d579e335b5 | ||
|
|
7e344ceb85 | ||
|
|
0145b82ea0 | ||
|
|
b4efe7aab7 | ||
|
|
9ffe951f6d | ||
|
|
a5ea4f57bd | ||
|
|
88f4c16755 | ||
|
|
c7ee071efa | ||
|
|
0740ef7ceb | ||
|
|
b29bd809ac | ||
|
|
8977c9257a | ||
|
|
091b7eacba | ||
|
|
e74b1251c0 | ||
|
|
d282a1649d | ||
|
|
6ce8d49d1b | ||
|
|
c5b197078a | ||
|
|
0494f20d62 | ||
|
|
73aff476dd | ||
|
|
98ee5e651a | ||
|
|
01871ea383 | ||
|
|
1dbdb9438f | ||
|
|
c70f0eadb8 | ||
|
|
26244ad8c2 | ||
|
|
fa0aa5510d | ||
|
|
eec557b6aa | ||
|
|
0cc27c3cc1 | ||
|
|
507089d7b2 | ||
|
|
a6b3517b75 | ||
|
|
2d6beb7813 | ||
|
|
61bc129e1d | ||
|
|
52210a57f0 | ||
|
|
8061a2f108 | ||
|
|
7444eff6d4 | ||
|
|
f35a9da99a | ||
|
|
c3098e9ab2 | ||
|
|
686f9288fc | ||
|
|
1833670fb9 | ||
|
|
3dd42f5aa2 | ||
|
|
99a7beba57 | ||
|
|
edee2b753c | ||
|
|
545d5770be | ||
|
|
ca747a2b54 | ||
|
|
17da165cfe | ||
|
|
5e6788c679 | ||
|
|
425deadca9 | ||
|
|
2c8e9dd3a5 | ||
|
|
7a72f1a253 | ||
|
|
208e556332 | ||
|
|
c65d11bfb5 | ||
|
|
3b5b52d89a | ||
|
|
a4f6c8f990 | ||
|
|
670c329852 | ||
|
|
f3551c8422 | ||
|
|
90b8187882 | ||
|
|
1a43259989 | ||
|
|
3c0a630475 | ||
|
|
2a1e5a9729 | ||
|
|
413c66beba | ||
|
|
1416e696b1 | ||
|
|
d373cf89c7 | ||
|
|
dd886d22f0 | ||
|
|
472569a27c | ||
|
|
76cf6559cc | ||
|
|
a34e8dcdc9 | ||
|
|
da752fc9a4 | ||
|
|
beb2de2dd9 | ||
|
|
2a8b65e105 | ||
|
|
62a916bc24 | ||
|
|
c47b833e7b | ||
|
|
09b0958b5f | ||
|
|
3a4c3d3e58 | ||
|
|
7484292e63 | ||
|
|
687c2741b8 | ||
|
|
2fb285e530 | ||
|
|
16f6473938 | ||
|
|
66546208b2 | ||
|
|
532274045e | ||
|
|
9347c72fb6 | ||
|
|
e90bb7169c | ||
|
|
8a2c41e183 | ||
|
|
59fb65293a | ||
|
|
e7718b92b7 | ||
|
|
cdfaf761df | ||
|
|
1a9ea6f738 | ||
|
|
945c1c8597 | ||
|
|
e4d0f7acd5 | ||
|
|
250496c953 | ||
|
|
e47dc758c9 | ||
|
|
b92a843c5f | ||
|
|
91bea9c5b3 | ||
|
|
d75bb5cbe1 | ||
|
|
2671259fdb | ||
|
|
2024010119 | ||
|
|
412040f77e | ||
|
|
d210660ce8 | ||
|
|
863a12562b | ||
|
|
5da606a9ac | ||
|
|
8d20f3d5c4 | ||
|
|
5d360180af | ||
|
|
f0fbed6007 | ||
|
|
519de7c833 | ||
|
|
97ccef1a04 | ||
|
|
c4df0dd06e | ||
|
|
cd114c6818 | ||
|
|
1707b8cdba | ||
|
|
41d4d70b98 | ||
|
|
0e999482cb | ||
|
|
65b2c06027 | ||
|
|
d7b61ede07 | ||
|
|
87fc1c84b8 | ||
|
|
d4b5f12383 | ||
|
|
eb62b0d665 | ||
|
|
91387a741b | ||
|
|
e8b34cb00d | ||
|
|
82954258c1 | ||
|
|
50f092551b | ||
|
|
c36a64be68 | ||
|
|
a343b20775 | ||
|
|
a714e76ae1 | ||
|
|
d21d5c9510 | ||
|
|
cd6788a2bb | ||
|
|
6b99399c41 | ||
|
|
952b6af445 | ||
|
|
7c674ad7fa | ||
|
|
d7d2ac3951 | ||
|
|
29e67d307a | ||
|
|
7320b7df62 | ||
|
|
11fb4233f7 | ||
|
|
84bb350b14 | ||
|
|
38e3694d1c | ||
|
|
1084935241 | ||
|
|
f5f0b9ecaa | ||
|
|
230fc49ae2 | ||
|
|
250d507bdf | ||
|
|
a818653174 | ||
|
|
5c3b044740 | ||
|
|
c5aa8729a1 | ||
|
|
3f78d76da1 | ||
|
|
70c19ccf16 | ||
|
|
68db9cb499 | ||
|
|
d0466fa777 | ||
|
|
21ab64e962 | ||
|
|
a0145cebf2 | ||
|
|
69176fc5f4 | ||
|
|
278dce9ba6 | ||
|
|
1cfa3ee4c7 | ||
|
|
9a95cd5794 | ||
|
|
a62fe3df6f | ||
|
|
7701244a08 | ||
|
|
96e31e4b78 | ||
|
|
ec208af474 | ||
|
|
242641264d | ||
|
|
d3a9a0615b | ||
|
|
3277e8c89c | ||
|
|
d02b9442a5 | ||
|
|
bac385b59c | ||
|
|
b1a0ab8086 | ||
|
|
a33749eb71 | ||
|
|
f5e4ee90e4 | ||
|
|
690d5e6dbd | ||
|
|
a76c055b63 | ||
|
|
70c461c60b | ||
|
|
d51b71ee80 | ||
|
|
3666448ca6 | ||
|
|
d3311d9f43 | ||
|
|
3e1735b06e | ||
|
|
de7ef7eace | ||
|
|
7e89458a3b | ||
|
|
f212bafe46 | ||
|
|
86fe40708b | ||
|
|
d718747c5b | ||
|
|
46ee9ac41c | ||
|
|
f1d306feab | ||
|
|
2d0db98e83 | ||
|
|
3df06a1c68 | ||
|
|
a8f9432a3a | ||
|
|
561e0b04a8 | ||
|
|
404b6a864b | ||
|
|
4feaf31225 | ||
|
|
391aa14845 | ||
|
|
a0d61b4c37 | ||
|
|
2952737755 | ||
|
|
f103aa4753 | ||
|
|
884856023a | ||
|
|
d8188fce7b | ||
|
|
0f15f1ab73 | ||
|
|
488a236b7a | ||
|
|
e833823e15 | ||
|
|
ee4ba104e7 | ||
|
|
4fdc08295b | ||
|
|
a3ff49aaf1 | ||
|
|
76364ea767 | ||
|
|
8eec50d764 | ||
|
|
32b659b346 | ||
|
|
00809909ae | ||
|
|
9f7684f6fe | ||
|
|
2bed7d370e | ||
|
|
d2b852f7cb | ||
|
|
901939bd96 | ||
|
|
edfdcc8cee | ||
|
|
3982c9a552 | ||
|
|
4490b2d209 | ||
|
|
eb4bbf3294 | ||
|
|
dc97d48491 | ||
|
|
0f50dc848e | ||
|
|
c5e4b83de3 | ||
|
|
a08ab46713 | ||
|
|
f50a7058d6 | ||
|
|
2c74f0a040 | ||
|
|
58835e40f3 | ||
|
|
8befa5918a | ||
|
|
df80f7ff2a | ||
|
|
5f66786ef1 | ||
|
|
3a965856a5 | ||
|
|
03df609d77 | ||
|
|
178581b560 | ||
|
|
ffd2314120 | ||
|
|
815b595d2f | ||
|
|
11e56403dd | ||
|
|
4baadecda5 | ||
|
|
cf552b5f3b | ||
|
|
1894304d33 | ||
|
|
9d5392fb02 | ||
|
|
c280645671 | ||
|
|
45f92e6b38 | ||
|
|
6509f09961 | ||
|
|
3c279a6f0e | ||
|
|
9ec3f03871 | ||
|
|
84a9c2c112 | ||
|
|
af368119cb | ||
|
|
89b9189efa | ||
|
|
dd59b8c7b9 | ||
|
|
f83491274f | ||
|
|
c0435fdff4 | ||
|
|
3c09c77269 | ||
|
|
547e101f1d | ||
|
|
0130f64934 | ||
|
|
361e0543ee | ||
|
|
63aa5d3b4e | ||
|
|
01302d097c | ||
|
|
e6095cb7e8 | ||
|
|
b876b8af11 | ||
|
|
a7c41f3fcd | ||
|
|
4772bd8d4c | ||
|
|
d471067e3f | ||
|
|
d0b7780239 | ||
|
|
e627ca6bd7 | ||
|
|
c97172bdd4 | ||
|
|
ce8a745fb4 | ||
|
|
3e9efd1401 | ||
|
|
20340190b5 | ||
|
|
265040a78c | ||
|
|
448d7e0c5a | ||
|
|
6eb1874c5a | ||
|
|
4c70745cc1 | ||
|
|
7795748a3f | ||
|
|
098ef4d7cf | ||
|
|
e3f91bfe1b | ||
|
|
7374fe73a3 | ||
|
|
d2bde205f0 | ||
|
|
5620f70f9a | ||
|
|
37f258b1bf | ||
|
|
e2dd2a133e | ||
|
|
7514644e07 | ||
|
|
16b0aeda7d | ||
|
|
86e4f4a841 | ||
|
|
607eacf8c7 | ||
|
|
7a049644a8 | ||
|
|
17a13f00f8 | ||
|
|
43436e48e0 | ||
|
|
5a39102405 | ||
|
|
94999101e3 | ||
|
|
e619b7c4f4 | ||
|
|
b7c2e8cb67 | ||
|
|
fb76893e18 | ||
|
|
88d812fe82 | ||
|
|
77f9f4664a | ||
|
|
5c2f85c39e | ||
|
|
ac4d22cd12 | ||
|
|
cf95e44cb4 | ||
|
|
65dd2bb429 | ||
|
|
6be855be6a | ||
|
|
b6e3f4423b | ||
|
|
0c61d81713 | ||
|
|
7c6f5dba63 | ||
|
|
44cfc7e62a | ||
|
|
96670d5f16 | ||
|
|
36b971ee4e | ||
|
|
f1a9629652 | ||
|
|
20230402d0 | ||
|
|
5c2c3a6c88 | ||
|
|
fb019d43bf | ||
|
|
025aa33773 | ||
|
|
302e21fd58 | ||
|
|
211512ae64 | ||
|
|
8ec917b1c3 | ||
|
|
1c7534f009 | ||
|
|
ae745d9397 | ||
|
|
60f37aae2f | ||
|
|
d7daf5f724 | ||
|
|
e5103d9429 | ||
|
|
8fecb29848 | ||
|
|
290ea6179d | ||
|
|
9695a40fc9 | ||
|
|
1913b95227 | ||
|
|
a874aea692 | ||
|
|
69c52099e7 | ||
|
|
cfc0747d5d | ||
|
|
fcd7e8768d | ||
|
|
3c34dd8275 | ||
|
|
1116e481be | ||
|
|
63cf9d04de | ||
|
|
3364d4d147 | ||
|
|
57ad21e4bd | ||
|
|
414f87981f | ||
|
|
b1459c79cf | ||
|
|
352ea07226 | ||
|
|
27018787af | ||
|
|
4e305eca26 | ||
|
|
9e9c0ceaf4 | ||
|
|
b3bf18b1c0 | ||
|
|
b1619f675f | ||
|
|
96c3de12eb | ||
|
|
719dbb8bae | ||
|
|
f38a7f7f8f | ||
|
|
6ea38b4438 | ||
|
|
f7447aece1 | ||
|
|
aa2b9ec476 | ||
|
|
3ee00f8bc2 | ||
|
|
fccab60a5c | ||
|
|
0f4af38457 | ||
|
|
aef39f1160 | ||
|
|
2023012408 | ||
|
|
95a7661bb1 | ||
|
|
618d317803 | ||
|
|
ae897c8cdb | ||
|
|
d0a0f3c052 | ||
|
|
91b9591b10 | ||
|
|
aa7361337d | ||
|
|
284d77fe2e | ||
|
|
826178f1e2 | ||
|
|
acccf8a9b8 | ||
|
|
57c066f0be | ||
|
|
e44f64ae92 | ||
|
|
d51980a3f5 | ||
|
|
c3d73e7ecb | ||
|
|
b077f6821d | ||
|
|
a79de11af7 | ||
|
|
2023011763 | ||
|
|
b46e40e86b | ||
|
|
a6d6cdd165 | ||
|
|
dc8da605f9 | ||
|
|
8b299a29c7 | ||
|
|
3109b865d2 | ||
|
|
0c5956c43c | ||
|
|
1c83b39691 | ||
|
|
77874b473c | ||
|
|
b7cce7be15 | ||
|
|
3cd3362417 | ||
|
|
e97e925efb | ||
|
|
0f032235cf | ||
|
|
e0f0984da7 | ||
|
|
4d22b5aaef | ||
|
|
80b8846318 | ||
|
|
bf641faafa | ||
|
|
23d8b78ce1 | ||
|
|
3b2244077d | ||
|
|
ee5cdb9713 | ||
|
|
03d02d67f7 | ||
|
|
5798145581 | ||
|
|
51ef0b7f66 | ||
|
|
97b4542c73 | ||
|
|
c1cd0c09a2 | ||
|
|
1fc1f47d80 | ||
|
|
ec471a5bc2 | ||
|
|
a893fc0ca2 | ||
|
|
3761dc0433 | ||
|
|
aa71a07fbe | ||
|
|
088293f5e7 | ||
|
|
7c660aa86e | ||
|
|
435d8fa0a2 | ||
|
|
5cd6f1d064 | ||
|
|
ec20dfe312 | ||
|
|
924ffb5a35 | ||
|
|
62c7f59b94 | ||
|
|
e97176b1d7 | ||
|
|
d649f5d826 | ||
|
|
6c37177cf5 | ||
|
|
14775aa975 | ||
|
|
44b6336372 | ||
|
|
36d2bb332b | ||
|
|
4dbe45640a | ||
|
|
4b3f0b9f08 | ||
|
|
12af069dca | ||
|
|
d42e708d31 | ||
|
|
b7bb973118 | ||
|
|
750b2a6313 | ||
|
|
de0da86bd7 | ||
|
|
8e283f512a | ||
|
|
73162a4bc3 | ||
|
|
1a9761736e | ||
|
|
fd1f7665a7 | ||
|
|
6d14573fd0 | ||
|
|
cf69b836ac | ||
|
|
a7a771b92b | ||
|
|
def011c029 | ||
|
|
4b055bf260 | ||
|
|
1ba7484d60 | ||
|
|
51c518da1e | ||
|
|
a3b6b03dfb | ||
|
|
18e3b38c69 | ||
|
|
0ad30063ff | ||
|
|
7812c64a31 | ||
|
|
3d2376ab52 | ||
|
|
6b207bbf2b | ||
|
|
3f079ba7c6 | ||
|
|
8f4c89f50e | ||
|
|
6b7a543c82 | ||
|
|
2ba68d24f2 | ||
|
|
46877e0a92 | ||
|
|
b55f555487 | ||
|
|
a38b63be18 | ||
|
|
1bebd6f4f5 | ||
|
|
3da63f394d | ||
|
|
2a54e3d770 | ||
|
|
06b02ba46e | ||
|
|
d01ae55109 | ||
|
|
8868d7d188 | ||
|
|
2eec9892be | ||
|
|
01ae621f11 | ||
|
|
f984aa0d2c | ||
|
|
0881a6bc17 | ||
|
|
2c6a73546d | ||
|
|
a29944660e | ||
|
|
f6ce624c6f | ||
|
|
c09ec8e4d1 | ||
|
|
31bbaad06e | ||
|
|
b9ca1fe830 | ||
|
|
e61585f2f3 | ||
|
|
0de1aacb0c | ||
|
|
168829b555 | ||
|
|
170fc517d0 | ||
|
|
0fbf1c7c71 | ||
|
|
694be39c71 | ||
|
|
dad26d81df | ||
|
|
bcaea097ea | ||
|
|
d56fe74e24 | ||
|
|
4603d540c3 | ||
|
|
f9d53303bb | ||
|
|
d04faa6505 | ||
|
|
07da058eae | ||
|
|
cefa6b9878 | ||
|
|
04d0b0223f | ||
|
|
78ad6d2d88 | ||
|
|
22cbd9fa58 | ||
|
|
984049586a | ||
|
|
cdfc2b92e3 | ||
|
|
4530abe8df | ||
|
|
586020b8b6 | ||
|
|
3a8626fd04 | ||
|
|
a6e483a434 | ||
|
|
6a942e56b1 | ||
|
|
87c91550ad | ||
|
|
731daf0f37 | ||
|
|
f931e53890 | ||
|
|
b5efc68737 | ||
|
|
b9e6e7926c | ||
|
|
845034c81c | ||
|
|
54d42e3f40 | ||
|
|
e03ac3136e | ||
|
|
6fb41a202a | ||
|
|
4bef330ce1 | ||
|
|
8a5f719964 | ||
|
|
209d5e8e90 | ||
|
|
9d041aa582 | ||
|
|
6532b3e655 | ||
|
|
c1c355160d | ||
|
|
83515d5610 | ||
|
|
aa10dccf90 | ||
|
|
f4fd53211a | ||
|
|
4993d19466 | ||
|
|
19f9bbca0d | ||
|
|
779d8e1627 | ||
|
|
bb07410448 | ||
|
|
d826f9e72f | ||
|
|
6a6130615d | ||
|
|
a8e7021be2 | ||
|
|
38259d0382 | ||
|
|
f7e7259910 | ||
|
|
f0bfeba733 | ||
|
|
c3a7a24eea | ||
|
|
bbbcd780c9 | ||
|
|
475469a2e7 | ||
|
|
3a7447dcb6 | ||
|
|
e5d8cbd383 | ||
|
|
3c08dca7e7 | ||
|
|
d083f01d22 | ||
|
|
68cf393644 | ||
|
|
18f7230662 | ||
|
|
728f735281 | ||
|
|
ecc418ba77 | ||
|
|
3af5b7f2ac | ||
|
|
7a7cfcacbe | ||
|
|
52594355bf | ||
|
|
0d06c28b19 | ||
|
|
ccc4677252 | ||
|
|
821fc9feed | ||
|
|
82b46726fc | ||
|
|
8df872a482 | ||
|
|
c79c306adb | ||
|
|
51fdaad002 | ||
|
|
885cd8ff04 | ||
|
|
2707af403a | ||
|
|
2d227e5222 | ||
|
|
70529878e2 | ||
|
|
3b7a962dc6 | ||
|
|
6dcf5c3d7d | ||
|
|
b089bb5e7b | ||
|
|
a91a67668e | ||
|
|
220a908118 | ||
|
|
54841248e7 | ||
|
|
a0b42e6538 | ||
|
|
3312cf525d | ||
|
|
2093667548 | ||
|
|
3c868d7961 | ||
|
|
707f4f5816 | ||
|
|
b3ab6311c5 | ||
|
|
d56f605b63 | ||
|
|
f8b713f425 | ||
|
|
5209e95bc7 | ||
|
|
ef67a45702 | ||
|
|
b88eb72ac2 | ||
|
|
32847f7254 | ||
|
|
71df93b534 | ||
|
|
bb028191f8 | ||
|
|
19af8fc7d8 | ||
|
|
a06671b47f | ||
|
|
5f385d88e0 | ||
|
|
9cb7a364a3 | ||
|
|
f68cbc577d |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
||||
custom: ["https://paypal.me/junegunn", "https://www.buymeacoffee.com/junegunn"]
|
||||
github: junegunn
|
||||
|
||||
22
.github/ISSUE_TEMPLATE.md
vendored
22
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,22 +0,0 @@
|
||||
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
|
||||
|
||||
<!-- Check all that apply [x] -->
|
||||
|
||||
- [ ] I have read through the manual page (`man fzf`)
|
||||
- [ ] I have the latest version of fzf
|
||||
- [ ] I have searched through the existing issues
|
||||
|
||||
## Info
|
||||
|
||||
- OS
|
||||
- [ ] Linux
|
||||
- [ ] Mac OS X
|
||||
- [ ] Windows
|
||||
- [ ] Etc.
|
||||
- Shell
|
||||
- [ ] bash
|
||||
- [ ] zsh
|
||||
- [ ] fish
|
||||
|
||||
## Problem / Steps to reproduce
|
||||
|
||||
49
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
49
.github/ISSUE_TEMPLATE/issue_template.yml
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
name: Issue Template
|
||||
description: Report a problem or bug related to fzf to help us improve
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Checklist
|
||||
options:
|
||||
- label: I have read through the manual page (`man fzf`)
|
||||
required: true
|
||||
- label: I have searched through the existing issues
|
||||
required: true
|
||||
- label: For bug reports, I have checked if the bug is reproducible in the latest version of fzf
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Output of `fzf --version`
|
||||
placeholder: e.g. 0.48.1 (d579e33)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: OS
|
||||
options:
|
||||
- label: Linux
|
||||
- label: macOS
|
||||
- label: Windows
|
||||
- label: Etc.
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Shell
|
||||
options:
|
||||
- label: bash
|
||||
- label: zsh
|
||||
- label: fish
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem / Steps to reproduce
|
||||
validations:
|
||||
required: true
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -4,3 +4,7 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
15
.github/workflows/codeql-analysis.yml
vendored
15
.github/workflows/codeql-analysis.yml
vendored
@@ -8,8 +8,15 @@ on:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
permissions:
|
||||
actions: read # for github/codeql-action/init to get workflow details
|
||||
contents: read # for actions/checkout to fetch code
|
||||
security-events: write # for github/codeql-action/autobuild to send a status report
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -20,18 +27,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
||||
uses: github/codeql-action/analyze@v3
|
||||
|
||||
14
.github/workflows/depsreview.yaml
vendored
Normal file
14
.github/workflows/depsreview.yaml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: 'Dependency Review'
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 'Checkout Repository'
|
||||
uses: actions/checkout@v5
|
||||
- name: 'Dependency Review'
|
||||
uses: actions/dependency-review-action@v4
|
||||
29
.github/workflows/linux.yml
vendored
29
.github/workflows/linux.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Test fzf on Linux
|
||||
name: build
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -8,38 +8,41 @@ on:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: [1.14, 1.16]
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1.62.0
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
ruby-version: 3.4.1
|
||||
|
||||
- name: Install packages
|
||||
run: sudo apt-get install --yes zsh fish tmux
|
||||
|
||||
- name: Install Ruby gems
|
||||
run: sudo gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||
run: bundle install
|
||||
|
||||
- name: Rubocop
|
||||
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||
run: make lint
|
||||
|
||||
- name: Unit test
|
||||
run: make test
|
||||
|
||||
- name: Integration test
|
||||
run: make install && ./install --all && LC_ALL=C tmux new-session -d && ruby test/test_go.rb --verbose
|
||||
run: make install && ./install --all && tmux new-session -d && ruby test/runner.rb --verbose
|
||||
|
||||
14
.github/workflows/macos.yml
vendored
14
.github/workflows/macos.yml
vendored
@@ -8,24 +8,24 @@ on:
|
||||
branches: [ master ]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: macos-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go: [1.14, 1.16]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go }}
|
||||
go-version: "1.23"
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@v1.62.0
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.0.0
|
||||
|
||||
|
||||
24
.github/workflows/sponsors.yml
vendored
Normal file
24
.github/workflows/sponsors.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Generate Sponsors README
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 0 * * 0
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@v1
|
||||
with:
|
||||
token: ${{ secrets.SPONSORS_TOKEN }}
|
||||
file: 'README.md'
|
||||
|
||||
- name: Deploy to GitHub Pages 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
branch: master
|
||||
folder: '.'
|
||||
10
.github/workflows/typos.yml
vendored
Normal file
10
.github/workflows/typos.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
name: "Spell Check"
|
||||
on: [pull_request]
|
||||
|
||||
jobs:
|
||||
typos:
|
||||
name: Spell Check with Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: crate-ci/typos@v1.29.4
|
||||
14
.github/workflows/winget.yml
vendored
Normal file
14
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Publish to Winget
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||
with:
|
||||
identifier: junegunn.fzf
|
||||
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,7 +3,6 @@ bin/fzf.exe
|
||||
dist
|
||||
target
|
||||
pkg
|
||||
Gemfile.lock
|
||||
.DS_Store
|
||||
doc/tags
|
||||
vendor
|
||||
@@ -12,3 +11,4 @@ gopath
|
||||
fzf
|
||||
tmp
|
||||
*.patch
|
||||
.idea
|
||||
|
||||
116
.goreleaser.yml
116
.goreleaser.yml
@@ -1,4 +1,5 @@
|
||||
---
|
||||
version: 2
|
||||
project_name: fzf
|
||||
|
||||
before:
|
||||
@@ -6,76 +7,27 @@ before:
|
||||
- go mod download
|
||||
|
||||
builds:
|
||||
- id: fzf-macos
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
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/fzf"]
|
||||
bundle_id = "kr.junegunn.fzf"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
zip {
|
||||
output_path = "./dist/fzf-{{ .Version }}-darwin_amd64.zip"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon-amd64.hcl
|
||||
'
|
||||
|
||||
- id: fzf-macos-arm
|
||||
binary: fzf
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
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 = "kr.junegunn.fzf"
|
||||
apple_id {
|
||||
username = "junegunn.c@gmail.com"
|
||||
password = "@env:AC_PASSWORD"
|
||||
}
|
||||
sign {
|
||||
application_identity = "Developer ID Application: Junegunn Choi (Y254DRW44Z)"
|
||||
}
|
||||
zip {
|
||||
output_path = "./dist/fzf-{{ .Version }}-darwin_arm64.zip"
|
||||
}
|
||||
EOF
|
||||
gon /tmp/fzf-gon-arm64.hcl
|
||||
'
|
||||
|
||||
- id: fzf
|
||||
goos:
|
||||
- darwin
|
||||
- linux
|
||||
- windows
|
||||
- freebsd
|
||||
- openbsd
|
||||
- android
|
||||
goarch:
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- loong64
|
||||
- ppc64le
|
||||
- s390x
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
- "5"
|
||||
- "6"
|
||||
- "7"
|
||||
flags:
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||
ignore:
|
||||
@@ -87,6 +39,46 @@ builds:
|
||||
goarch: arm64
|
||||
- goos: openbsd
|
||||
goarch: arm64
|
||||
- goos: android
|
||||
goarch: amd64
|
||||
- goos: android
|
||||
goarch: arm
|
||||
|
||||
# .goreleaser.yaml
|
||||
notarize:
|
||||
macos:
|
||||
- # Whether this configuration is enabled or not.
|
||||
#
|
||||
# Default: false.
|
||||
# Templates: allowed.
|
||||
enabled: "{{ not .IsSnapshot }}"
|
||||
|
||||
# Before notarizing, we need to sign the binary.
|
||||
# This blocks defines the configuration for doing so.
|
||||
sign:
|
||||
# The .p12 certificate file path or its base64'd contents.
|
||||
certificate: "{{.Env.MACOS_SIGN_P12}}"
|
||||
|
||||
# The password to be used to open the certificate.
|
||||
password: "{{.Env.MACOS_SIGN_PASSWORD}}"
|
||||
|
||||
# Then, we notarize the binaries.
|
||||
notarize:
|
||||
# The issuer ID.
|
||||
# Its the UUID you see when creating the App Store Connect key.
|
||||
issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}"
|
||||
|
||||
# Key ID.
|
||||
# You can see it in the list of App Store Connect Keys.
|
||||
# It will also be in the ApiKey filename.
|
||||
key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}"
|
||||
|
||||
# The .p8 key file path or its base64'd contents.
|
||||
key: "{{.Env.MACOS_NOTARY_KEY}}"
|
||||
|
||||
# Whether to wait for the notarization to finish.
|
||||
# Not recommended, as it could take a really long time.
|
||||
wait: true
|
||||
|
||||
archives:
|
||||
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||
@@ -104,12 +96,10 @@ release:
|
||||
owner: junegunn
|
||||
name: fzf
|
||||
prerelease: auto
|
||||
name_template: '{{ .Tag }}'
|
||||
extra_files:
|
||||
- glob: ./dist/fzf-*darwin*.zip
|
||||
name_template: '{{ .Version }}'
|
||||
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-devel"
|
||||
name_template: "{{ .Version }}-devel"
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
|
||||
18
.rubocop.yml
18
.rubocop.yml
@@ -1,12 +1,16 @@
|
||||
AllCops:
|
||||
NewCops: enable
|
||||
Layout/LineLength:
|
||||
Enabled: false
|
||||
Metrics:
|
||||
Enabled: false
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: false
|
||||
Lint/NestedMethodDefinition:
|
||||
Enabled: false
|
||||
Style/MethodCallWithArgsParentheses:
|
||||
Enabled: true
|
||||
IgnoredMethods:
|
||||
AllowedMethods:
|
||||
- assert
|
||||
- exit
|
||||
- paste
|
||||
@@ -15,7 +19,7 @@ Style/MethodCallWithArgsParentheses:
|
||||
- refute
|
||||
- require
|
||||
- send_keys
|
||||
IgnoredPatterns:
|
||||
AllowedPatterns:
|
||||
- ^assert_
|
||||
- ^refute_
|
||||
Style/NumericPredicate:
|
||||
@@ -26,3 +30,13 @@ Style/OptionalBooleanParameter:
|
||||
Enabled: false
|
||||
Style/WordArray:
|
||||
MinSize: 1
|
||||
Minitest/AssertEqual:
|
||||
Enabled: false
|
||||
Minitest/EmptyLineBeforeAssertionMethods:
|
||||
Enabled: false
|
||||
Naming/VariableNumber:
|
||||
Enabled: false
|
||||
Lint/EmptyBlock:
|
||||
Enabled: false
|
||||
Style/SafeNavigationChainLength:
|
||||
Enabled: false
|
||||
|
||||
2
.tool-versions
Normal file
2
.tool-versions
Normal file
@@ -0,0 +1,2 @@
|
||||
golang 1.23.12
|
||||
ruby 3.4.1
|
||||
363
ADVANCED.md
363
ADVANCED.md
@@ -1,29 +1,36 @@
|
||||
Advanced fzf examples
|
||||
======================
|
||||
|
||||
*(Last update: 2021/05/22)*
|
||||
* *Last update: 2025/02/02*
|
||||
* *Requires fzf 0.59.0 or later*
|
||||
|
||||
---
|
||||
|
||||
<!-- vim-markdown-toc GFM -->
|
||||
|
||||
* [Introduction](#introduction)
|
||||
* [Screen Layout](#screen-layout)
|
||||
* [`--height`](#--height)
|
||||
* [`fzf-tmux`](#fzf-tmux)
|
||||
* [Popup window support](#popup-window-support)
|
||||
* [Display modes](#display-modes)
|
||||
* [`--height`](#--height)
|
||||
* [`--tmux`](#--tmux)
|
||||
* [Dynamic reloading of the list](#dynamic-reloading-of-the-list)
|
||||
* [Updating the list of processes by pressing CTRL-R](#updating-the-list-of-processes-by-pressing-ctrl-r)
|
||||
* [Toggling between data sources](#toggling-between-data-sources)
|
||||
* [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 with a single key binding](#toggling-with-a-single-key-binding)
|
||||
* [Ripgrep integration](#ripgrep-integration)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interative Ripgrep launcher](#using-fzf-as-interative-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Using fzf as the secondary filter](#using-fzf-as-the-secondary-filter)
|
||||
* [Using fzf as interactive Ripgrep launcher](#using-fzf-as-interactive-ripgrep-launcher)
|
||||
* [Switching to fzf-only search mode](#switching-to-fzf-only-search-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode](#switching-between-ripgrep-mode-and-fzf-mode)
|
||||
* [Switching between Ripgrep mode and fzf mode using a single key binding](#switching-between-ripgrep-mode-and-fzf-mode-using-a-single-key-binding)
|
||||
* [Controlling Ripgrep search and fzf search simultaneously](#controlling-ripgrep-search-and-fzf-search-simultaneously)
|
||||
* [Log tailing](#log-tailing)
|
||||
* [Key bindings for git objects](#key-bindings-for-git-objects)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
* [Branches](#branches)
|
||||
* [Commit hashes](#commit-hashes)
|
||||
* [Files listed in `git status`](#files-listed-in-git-status)
|
||||
* [Branches](#branches)
|
||||
* [Commit hashes](#commit-hashes)
|
||||
* [Color themes](#color-themes)
|
||||
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||
* [fzf Theme Playground](#fzf-theme-playground)
|
||||
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||
|
||||
<!-- vim-markdown-toc -->
|
||||
|
||||
@@ -56,7 +63,7 @@ learn its wide variety of features.
|
||||
This document will guide you through some examples that will familiarize you
|
||||
with the advanced features of fzf.
|
||||
|
||||
Screen Layout
|
||||
Display modes
|
||||
-------------
|
||||
|
||||
### `--height`
|
||||
@@ -86,7 +93,7 @@ fzf --height=40% --layout=reverse --info=inline --border --margin=1 --padding=1
|
||||
|
||||

|
||||
|
||||
*(See `Layout` section of the man page to see the full list of options)*
|
||||
*(See man page to see the full list of options)*
|
||||
|
||||
But you definitely don't want to repeat `--height=40% --layout=reverse
|
||||
--info=inline --border --margin=1 --padding=1` every time you use fzf. You
|
||||
@@ -97,56 +104,55 @@ Define `$FZF_DEFAULT_OPTS` like so:
|
||||
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||
```
|
||||
|
||||
### `fzf-tmux`
|
||||
### `--tmux`
|
||||
|
||||
Before fzf had `--height` option, we would open fzf in a tmux split pane not
|
||||
to take up the whole screen. This is done using `fzf-tmux` script.
|
||||
(Requires tmux 3.3 or later)
|
||||
|
||||
If you're using tmux, you can open fzf in a tmux popup using `--tmux` option.
|
||||
|
||||
```sh
|
||||
# Open fzf on a tmux split pane below the current pane.
|
||||
# Takes the same set of options.
|
||||
fzf-tmux --layout=reverse
|
||||
# Open fzf in a tmux popup at the center of the screen with 70% width and height
|
||||
fzf --tmux 70%
|
||||
```
|
||||
|
||||

|
||||

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

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

|
||||
|
||||

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

|
||||

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

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

|
||||
|
||||
> [!TIP]
|
||||
> You might also want to check out my tmux plugins which support this popup
|
||||
> window layout.
|
||||
>
|
||||
@@ -204,6 +210,30 @@ find * | fzf --prompt 'All> ' \
|
||||
|
||||

|
||||
|
||||
### Toggling with a single key binding
|
||||
|
||||
The above example uses two different key bindings to toggle between two modes,
|
||||
but can we just use a single key binding?
|
||||
|
||||
To make a key binding behave differently each time it is pressed, we need:
|
||||
|
||||
1. a way to store the current state. i.e. "which mode are we in?"
|
||||
2. and a way to dynamically perform different actions depending on the state.
|
||||
|
||||
The following example shows how to 1. store the current mode in the prompt
|
||||
string, 2. and use this information (`$FZF_PROMPT`) to determine which
|
||||
actions to perform using the `transform` action.
|
||||
|
||||
```sh
|
||||
fd --type file |
|
||||
fzf --prompt 'Files> ' \
|
||||
--header 'CTRL-T: Switch between Files/Directories' \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ Files ]] &&
|
||||
echo "change-prompt(Files> )+reload(fd --type file)" ||
|
||||
echo "change-prompt(Directories> )+reload(fd --type directory)"' \
|
||||
--preview '[[ $FZF_PROMPT =~ Files ]] && bat --color=always {} || tree -C {}'
|
||||
```
|
||||
|
||||
Ripgrep integration
|
||||
-------------------
|
||||
|
||||
@@ -235,15 +265,13 @@ file called `rfv`.
|
||||
# 1. Search for text in files using Ripgrep
|
||||
# 2. Interactively narrow down the list using fzf
|
||||
# 3. Open the file in Vim
|
||||
IFS=: read -ra selected < <(
|
||||
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||
)
|
||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||
rg --color=always --line-number --no-heading --smart-case "${*:-}" |
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
And run it with an initial query string.
|
||||
@@ -306,10 +334,14 @@ I know it's a lot to digest, let's try to break down the code.
|
||||
position in the window
|
||||
- `~3` makes the top three lines fixed header so that they are always
|
||||
visible regardless of the scroll offset
|
||||
- Once we selected a line, we open the file with `vim` (`vim
|
||||
"${selected[0]}"`) and move the cursor to the line (`+${selected[1]}`).
|
||||
- Instead of using shell script to process the final output of fzf, we use
|
||||
`become(...)` action which was added in [fzf 0.38.0][0.38.0] to turn fzf
|
||||
into a new process that opens the file with `vim` (`vim {1}`) and move the
|
||||
cursor to the line (`+{2}`).
|
||||
|
||||
### Using fzf as interative Ripgrep launcher
|
||||
[0.38.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0380
|
||||
|
||||
### Using fzf as interactive Ripgrep launcher
|
||||
|
||||
We have learned that we can bind `reload` action to a key (e.g.
|
||||
`--bind=ctrl-r:execute(ps -ef)`). In the next example, we are going to **bind
|
||||
@@ -330,25 +362,20 @@ projects, and it will free up memory as you narrow down the results.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
IFS=: read -ra selected < <(
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||
)
|
||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||

|
||||
|
||||
- Instead of starting fzf in `rg ... | fzf` form, we start fzf without an
|
||||
explicit input, but with a custom `FZF_DEFAULT_COMMAND` variable. This way
|
||||
fzf can kill the initial Ripgrep process it starts with the initial query.
|
||||
Otherwise, the initial Ripgrep process will keep consuming system resources
|
||||
even after `reload` is triggered.
|
||||
- Instead of starting fzf in the usual `rg ... | fzf` form, we make it start
|
||||
the initial Ripgrep process immediately via `start:reload` binding for the
|
||||
consistency of the code.
|
||||
- Filtering is no longer a responsibility of fzf; hence `--disabled`
|
||||
- `{q}` in the reload command evaluates to the query string on fzf prompt.
|
||||
- `sleep 0.1` in the reload command is for "debouncing". This small delay will
|
||||
@@ -357,8 +384,6 @@ IFS=: read -ra selected < <(
|
||||
|
||||
### Switching to fzf-only search mode
|
||||
|
||||
*(Requires fzf 0.27.1 or above)*
|
||||
|
||||
In the previous example, we lost fuzzy matching capability as we completely
|
||||
delegated search functionality to Ripgrep. But we can dynamically switch to
|
||||
fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
@@ -374,19 +399,16 @@ fzf-only search mode by *"unbinding"* `reload` action from `change` event.
|
||||
# 3. Open the file in Vim
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
IFS=: read -ra selected < <(
|
||||
FZF_DEFAULT_COMMAND="$RG_PREFIX $(printf %q "$INITIAL_QUERY")" \
|
||||
fzf --ansi \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--disabled --query "$INITIAL_QUERY" \
|
||||
--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" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3'
|
||||
)
|
||||
[ -n "${selected[0]}" ] && vim "${selected[0]}" "+${selected[1]}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "alt-enter:unbind(change,alt-enter)+change-prompt(2. fzf> )+enable-search+clear-query" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
* Phase 1. Filtering with Ripgrep
|
||||
@@ -405,6 +427,118 @@ IFS=: read -ra selected < <(
|
||||
- We reverted `--color` option for customizing how the matching chunks are
|
||||
displayed in the second phase
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode
|
||||
|
||||
[fzf 0.30.0][0.30.0] added `rebind` action so we can "rebind" the bindings
|
||||
that were previously "unbound" via `unbind`.
|
||||
|
||||
This is an improved version of the previous example that allows us to switch
|
||||
between Ripgrep launcher mode and fzf-only filtering mode via CTRL-R and
|
||||
CTRL-F.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep launcher mode (CTRL-R) and fzf filtering mode (CTRL-F)
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload($RG_PREFIX {q})+unbind(ctrl-r)" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind "ctrl-f:unbind(change,ctrl-f)+change-prompt(2. fzf> )+enable-search+rebind(ctrl-r)+transform-query(echo {q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f)" \
|
||||
--bind "ctrl-r:unbind(ctrl-r)+change-prompt(1. ripgrep> )+disable-search+reload($RG_PREFIX {q} || true)+rebind(change,ctrl-f)+transform-query(echo {q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r)" \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header '╱ CTRL-R (ripgrep mode) ╱ CTRL-F (fzf mode) ╱' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--bind 'enter:become(vim {1} +{2})'
|
||||
```
|
||||
|
||||
- To restore the query string when switching between modes, we store the
|
||||
current query in `/tmp/rg-fzf-{r,f}` files and restore the query using
|
||||
`transform-query` action which was added in [fzf 0.36.0][0.36.0].
|
||||
- Also note that we unbind `ctrl-r` binding on `start` event which is
|
||||
triggered once when fzf starts.
|
||||
|
||||
[0.30.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0300
|
||||
[0.36.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0360
|
||||
|
||||
### Switching between Ripgrep mode and fzf mode using a single key binding
|
||||
|
||||
In contrast to the previous version, we use just one hotkey to toggle between
|
||||
ripgrep and fzf mode. This is achieved by using the `$FZF_PROMPT` as a state
|
||||
within the `transform` action, a feature introduced in [fzf 0.45.0][0.45.0]. A
|
||||
more detailed explanation of this feature can be found in a previous section -
|
||||
[Toggling with a single keybinding](#toggling-with-a-single-key-binding).
|
||||
|
||||
[0.45.0]: https://github.com/junegunn/fzf/blob/master/CHANGELOG.md#0450
|
||||
|
||||
When using the `transform` action, the placeholder (`\{q}`) should be escaped to
|
||||
prevent immediate evaluation.
|
||||
|
||||
```sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Switch between Ripgrep mode and fzf filtering mode (CTRL-T)
|
||||
rm -f /tmp/rg-fzf-{r,f}
|
||||
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||
INITIAL_QUERY="${*:-}"
|
||||
fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||
--bind "start:reload:$RG_PREFIX {q}" \
|
||||
--bind "change:reload:sleep 0.1; $RG_PREFIX {q} || true" \
|
||||
--bind 'ctrl-t:transform:[[ ! $FZF_PROMPT =~ ripgrep ]] &&
|
||||
echo "rebind(change)+change-prompt(1. ripgrep> )+disable-search+transform-query:echo \{q} > /tmp/rg-fzf-f; cat /tmp/rg-fzf-r" ||
|
||||
echo "unbind(change)+change-prompt(2. fzf> )+enable-search+transform-query:echo \{q} > /tmp/rg-fzf-r; cat /tmp/rg-fzf-f"' \
|
||||
--color "hl:-1:underline,hl+:-1:underline:reverse" \
|
||||
--prompt '1. ripgrep> ' \
|
||||
--delimiter : \
|
||||
--header 'CTRL-T: Switch between ripgrep/fzf' \
|
||||
--preview 'bat --color=always {1} --highlight-line {2}' \
|
||||
--preview-window 'up,60%,border-bottom,+{2}+3/3,~3' \
|
||||
--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
|
||||
-----------
|
||||
|
||||
@@ -430,16 +564,16 @@ Kubernetes pods.
|
||||
|
||||
```bash
|
||||
pods() {
|
||||
FZF_DEFAULT_COMMAND="kubectl get pods --all-namespaces" \
|
||||
fzf --info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||
--bind 'ctrl-r:reload:$FZF_DEFAULT_COMMAND' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
command='kubectl get pods --all-namespaces' fzf \
|
||||
--info=inline --layout=reverse --header-lines=1 \
|
||||
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||
--bind 'start,ctrl-r:reload:$command' \
|
||||
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash' \
|
||||
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2})' \
|
||||
--preview-window up:follow \
|
||||
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -452,7 +586,7 @@ pods() {
|
||||
- Press enter key on a pod to `kubectl exec` into it
|
||||
- Press CTRL-O to open the log in your editor
|
||||
- Press CTRL-R to reload the pod list
|
||||
- Press CTRL-/ repeatedly to to rotate through a different sets of preview
|
||||
- Press CTRL-/ repeatedly to rotate through a different sets of preview
|
||||
window options
|
||||
1. `80%,border-bottom`
|
||||
1. `hidden`
|
||||
@@ -461,9 +595,17 @@ pods() {
|
||||
Key bindings for git objects
|
||||
----------------------------
|
||||
|
||||
I have [blogged](https://junegunn.kr/2016/07/fzf-git) about my fzf+git key
|
||||
bindings a few years ago. I'm going to show them here again, because they are
|
||||
seriously useful.
|
||||
Oftentimes, you want to put the identifiers of various Git object to the
|
||||
command-line. For example, it is common to write commands like these:
|
||||
|
||||
```sh
|
||||
git checkout [SOME_COMMIT_HASH or BRANCH or TAG]
|
||||
git diff [SOME_COMMIT_HASH or BRANCH or TAG] [SOME_COMMIT_HASH or BRANCH or TAG]
|
||||
```
|
||||
|
||||
[fzf-git.sh](https://github.com/junegunn/fzf-git.sh) project defines a set of
|
||||
fzf-based key bindings for Git objects. I strongly recommend that you check
|
||||
them out because they are seriously useful.
|
||||
|
||||
### Files listed in `git status`
|
||||
|
||||
@@ -483,9 +625,6 @@ seriously useful.
|
||||
|
||||

|
||||
|
||||
|
||||
The full source code can be found [here](https://gist.github.com/junegunn/8b572b8d4b5eddd8b85e5f4d40f17236).
|
||||
|
||||
Color themes
|
||||
------------
|
||||
|
||||
@@ -528,6 +667,12 @@ export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#
|
||||
|
||||

|
||||
|
||||
### fzf Theme Playground
|
||||
|
||||
[fzf Theme Playground](https://vitormv.github.io/fzf-themes/) created by
|
||||
[Vitor Mello](https://github.com/vitormv) is a webpage where you can
|
||||
interactively create fzf themes.
|
||||
|
||||
### Generating fzf color theme from Vim color schemes
|
||||
|
||||
The Vim plugin of fzf can generate `--color` option from the current color
|
||||
|
||||
36
BUILD.md
36
BUILD.md
@@ -6,7 +6,7 @@ Build instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.13 or above
|
||||
- Go 1.23 or above
|
||||
|
||||
### Using Makefile
|
||||
|
||||
@@ -24,24 +24,50 @@ make build
|
||||
make release
|
||||
```
|
||||
|
||||
> :warning: Makefile uses git commands to determine the version and the
|
||||
> revision information for `fzf --version`. So if you're building fzf from an
|
||||
> [!WARNING]
|
||||
> Makefile uses git commands to determine the version and the revision
|
||||
> information for `fzf --version`. So if you're building fzf from an
|
||||
> environment where its git information is not available, you have to manually
|
||||
> set `$FZF_VERSION` and `$FZF_REVISION`.
|
||||
>
|
||||
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
|
||||
|
||||
> [!TIP]
|
||||
> To build fzf with profiling options enabled, set `TAGS=pprof`
|
||||
>
|
||||
> ```sh
|
||||
> TAGS=pprof make clean install
|
||||
> fzf --profile-cpu /tmp/cpu.pprof --profile-mem /tmp/mem.pprof \
|
||||
> --profile-block /tmp/block.pprof --profile-mutex /tmp/mutex.pprof
|
||||
> ```
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
```sh
|
||||
# Run go unit tests
|
||||
make test
|
||||
|
||||
# Run integration tests (requires to be on tmux)
|
||||
make itest
|
||||
|
||||
# Run a single test case
|
||||
ruby test/runner.rb --name test_something
|
||||
```
|
||||
|
||||
Third-party libraries used
|
||||
--------------------------
|
||||
|
||||
- [mattn/go-runewidth](https://github.com/mattn/go-runewidth)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [rivo/uniseg](https://github.com/rivo/uniseg)
|
||||
- Licensed under [MIT](https://raw.githubusercontent.com/rivo/uniseg/master/LICENSE.txt)
|
||||
- [mattn/go-shellwords](https://github.com/mattn/go-shellwords)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [mattn/go-isatty](https://github.com/mattn/go-isatty)
|
||||
- Licensed under [MIT](http://mattn.mit-license.org)
|
||||
- [tcell](https://github.com/gdamore/tcell)
|
||||
- Licensed under [Apache License 2.0](https://github.com/gdamore/tcell/blob/master/LICENSE)
|
||||
- [fastwalk](https://github.com/charlievieth/fastwalk)
|
||||
- Licensed under [MIT](https://raw.githubusercontent.com/charlievieth/fastwalk/master/LICENSE)
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
1619
CHANGELOG.md
1619
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
FROM archlinux
|
||||
RUN pacman -Sy && pacman --noconfirm -S awk git tmux zsh fish ruby procps go make gcc
|
||||
RUN gem install --no-document -v 5.14.2 minitest
|
||||
FROM rubylang/ruby:3.4.1-noble
|
||||
RUN apt-get update -y && apt install -y git make golang zsh fish tmux
|
||||
RUN gem install --no-document -v 5.22.3 minitest
|
||||
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
|
||||
@@ -8,4 +8,5 @@ RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||
RUN rm -f /etc/bash.bashrc
|
||||
COPY . /fzf
|
||||
RUN cd /fzf && make install && ./install --all
|
||||
CMD tmux new 'set -o pipefail; ruby /fzf/test/test_go.rb | tee out && touch ok' && cat out && [ -e ok ]
|
||||
ENV LANG=C.UTF-8
|
||||
CMD ["bash", "-ic", "tmux new 'set -o pipefail; ruby /fzf/test/runner.rb | tee out && touch ok' && cat out && [ -e ok ]"]
|
||||
|
||||
8
Gemfile
Normal file
8
Gemfile
Normal file
@@ -0,0 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'minitest', '5.25.4'
|
||||
gem 'rubocop', '1.71.0'
|
||||
gem 'rubocop-minitest', '0.36.0'
|
||||
gem 'rubocop-performance', '1.23.1'
|
||||
47
Gemfile.lock
Normal file
47
Gemfile.lock
Normal file
@@ -0,0 +1,47 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
ast (2.4.2)
|
||||
json (2.9.1)
|
||||
language_server-protocol (3.17.0.3)
|
||||
minitest (5.25.4)
|
||||
parallel (1.26.3)
|
||||
parser (3.3.7.0)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
racc (1.8.1)
|
||||
rainbow (3.1.1)
|
||||
regexp_parser (2.10.0)
|
||||
rubocop (1.71.0)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (>= 3.17.0)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.3.0.2)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 2.9.3, < 3.0)
|
||||
rubocop-ast (>= 1.36.2, < 2.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 2.4.0, < 4.0)
|
||||
rubocop-ast (1.37.0)
|
||||
parser (>= 3.3.1.0)
|
||||
rubocop-minitest (0.36.0)
|
||||
rubocop (>= 1.61, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
rubocop-performance (1.23.1)
|
||||
rubocop (>= 1.48.1, < 2.0)
|
||||
rubocop-ast (>= 1.31.1, < 2.0)
|
||||
ruby-progressbar (1.13.0)
|
||||
unicode-display_width (2.6.0)
|
||||
|
||||
PLATFORMS
|
||||
arm64-darwin-23
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest (= 5.25.4)
|
||||
rubocop (= 1.71.0)
|
||||
rubocop-minitest (= 0.36.0)
|
||||
rubocop-performance (= 1.23.1)
|
||||
|
||||
BUNDLED WITH
|
||||
2.6.2
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
58
Makefile
58
Makefile
@@ -1,40 +1,41 @@
|
||||
SHELL := bash
|
||||
GO ?= go
|
||||
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
|
||||
GOOS ?= $(shell $(GO) env GOOS)
|
||||
|
||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
|
||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go shell/*sh man/man1/*.1) $(MAKEFILE)
|
||||
|
||||
ifdef FZF_VERSION
|
||||
VERSION := $(FZF_VERSION)
|
||||
else
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null)
|
||||
VERSION := $(shell git describe --abbrev=0 2> /dev/null | sed "s/^v//")
|
||||
endif
|
||||
ifeq ($(VERSION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||
endif
|
||||
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
|
||||
VERSION_TRIM := $(shell echo $(VERSION) | sed "s/^v//; s/-.*//")
|
||||
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||
|
||||
ifdef FZF_REVISION
|
||||
REVISION := $(FZF_REVISION)
|
||||
else
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES) 2> /dev/null)
|
||||
REVISION := $(shell git log -n 1 --pretty=format:%h --abbrev=8 -- $(SOURCES) 2> /dev/null)
|
||||
endif
|
||||
ifeq ($(REVISION),)
|
||||
$(error Not on git repository; cannot determine $$FZF_REVISION)
|
||||
endif
|
||||
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)"
|
||||
BUILD_FLAGS := -a -ldflags "-s -w -X main.version=$(VERSION) -X main.revision=$(REVISION)" -tags "$(TAGS)" -trimpath
|
||||
|
||||
BINARY32 := fzf-$(GOOS)_386
|
||||
BINARY64 := fzf-$(GOOS)_amd64
|
||||
BINARYS390 := fzf-$(GOOS)_s390x
|
||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||
BINARYRISCV64 := fzf-$(GOOS)_riscv64
|
||||
BINARYLOONG64 := fzf-$(GOOS)_loong64
|
||||
|
||||
# https://en.wikipedia.org/wiki/Uname
|
||||
UNAME_M := $(shell uname -m)
|
||||
@@ -42,6 +43,8 @@ ifeq ($(UNAME_M),x86_64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),amd64)
|
||||
BINARY := $(BINARY64)
|
||||
else ifeq ($(UNAME_M),s390x)
|
||||
BINARY := $(BINARYS390)
|
||||
else ifeq ($(UNAME_M),i686)
|
||||
BINARY := $(BINARY32)
|
||||
else ifeq ($(UNAME_M),i386)
|
||||
@@ -53,7 +56,9 @@ else ifeq ($(UNAME_M),armv6l)
|
||||
else ifeq ($(UNAME_M),armv7l)
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),armv8l)
|
||||
BINARY := $(BINARYARM8)
|
||||
# armv8l is always 32-bit and should implement the armv7 ISA, so
|
||||
# just use the same filename as for armv7.
|
||||
BINARY := $(BINARYARM7)
|
||||
else ifeq ($(UNAME_M),arm64)
|
||||
BINARY := $(BINARYARM8)
|
||||
else ifeq ($(UNAME_M),aarch64)
|
||||
@@ -62,6 +67,8 @@ else ifeq ($(UNAME_M),ppc64le)
|
||||
BINARY := $(BINARYPPC64LE)
|
||||
else ifeq ($(UNAME_M),riscv64)
|
||||
BINARY := $(BINARYRISCV64)
|
||||
else ifeq ($(UNAME_M),loongarch64)
|
||||
BINARY := $(BINARYLOONG64)
|
||||
else
|
||||
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||
endif
|
||||
@@ -69,22 +76,35 @@ endif
|
||||
all: target/$(BINARY)
|
||||
|
||||
test: $(SOURCES)
|
||||
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||
github.com/junegunn/fzf/src \
|
||||
github.com/junegunn/fzf/src/algo \
|
||||
github.com/junegunn/fzf/src/tui \
|
||||
github.com/junegunn/fzf/src/util
|
||||
|
||||
itest:
|
||||
ruby test/runner.rb
|
||||
|
||||
bench:
|
||||
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
|
||||
|
||||
generate:
|
||||
PATH=$(PATH):$(GOPATH)/bin $(GO) generate ./...
|
||||
|
||||
build:
|
||||
goreleaser --rm-dist --snapshot
|
||||
goreleaser build --clean --snapshot --skip=post-hooks
|
||||
|
||||
release:
|
||||
# Make sure that the tests pass and the build works
|
||||
TAGS=tcell make test
|
||||
make test build clean
|
||||
|
||||
ifndef GITHUB_TOKEN
|
||||
$(error GITHUB_TOKEN is not defined)
|
||||
endif
|
||||
@@ -111,7 +131,7 @@ endif
|
||||
git push origin temp --follow-tags --force
|
||||
|
||||
# Make a GitHub release
|
||||
goreleaser --rm-dist --release-notes tmp/release-note
|
||||
goreleaser --clean --release-notes tmp/release-note
|
||||
|
||||
# Push to master
|
||||
git checkout master
|
||||
@@ -129,6 +149,8 @@ target/$(BINARY32): $(SOURCES)
|
||||
target/$(BINARY64): $(SOURCES)
|
||||
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYS390): $(SOURCES)
|
||||
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
|
||||
# https://github.com/golang/go/wiki/GoArm
|
||||
target/$(BINARYARM5): $(SOURCES)
|
||||
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
@@ -148,19 +170,23 @@ target/$(BINARYPPC64LE): $(SOURCES)
|
||||
target/$(BINARYRISCV64): $(SOURCES)
|
||||
GOARCH=riscv64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
target/$(BINARYLOONG64): $(SOURCES)
|
||||
GOARCH=loong64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||
|
||||
bin/fzf: target/$(BINARY) | bin
|
||||
-rm -f bin/fzf
|
||||
cp -f target/$(BINARY) bin/fzf
|
||||
|
||||
docker:
|
||||
docker build -t fzf-arch .
|
||||
docker run -it fzf-arch tmux
|
||||
docker build -t fzf-ubuntu .
|
||||
docker run -it fzf-ubuntu tmux
|
||||
|
||||
docker-test:
|
||||
docker build -t fzf-arch .
|
||||
docker run -it fzf-arch
|
||||
docker build -t fzf-ubuntu .
|
||||
docker run -it fzf-ubuntu
|
||||
|
||||
update:
|
||||
$(GO) get -u
|
||||
$(GO) mod tidy
|
||||
|
||||
.PHONY: all 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
|
||||
|
||||
@@ -12,7 +12,10 @@ differ depending on the package manager.
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
```
|
||||
|
||||
@@ -23,7 +26,10 @@ written as:
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
Plug '/opt/homebrew/opt/fzf'
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
```
|
||||
|
||||
@@ -115,7 +121,7 @@ let g:fzf_action = {
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
@@ -149,6 +155,7 @@ let g:fzf_layout = { 'window': '10new' }
|
||||
let g:fzf_colors =
|
||||
\ { 'fg': ['fg', 'Normal'],
|
||||
\ 'bg': ['bg', 'Normal'],
|
||||
\ 'query': ['fg', 'Normal'],
|
||||
\ 'hl': ['fg', 'Comment'],
|
||||
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||
@@ -232,19 +239,20 @@ call fzf#run({'sink': 'e'})
|
||||
```
|
||||
|
||||
We haven't specified the `source`, so this is equivalent to starting fzf on
|
||||
command line without standard input pipe; fzf will use find command (or
|
||||
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
|
||||
directory. When you select one, it will open it with the sink, `:e` command.
|
||||
If you want to open it in a new tab, you can pass `:tabedit` command instead
|
||||
as the sink.
|
||||
command line without standard input pipe; fzf will traverse the file system
|
||||
under the current directory to get the list of files. (If
|
||||
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
|
||||
instead.) When you select one, it will open it with the sink, `:e` command. If
|
||||
you want to open it in a new tab, you can pass `:tabedit` command instead as
|
||||
the sink.
|
||||
|
||||
```vim
|
||||
call fzf#run({'sink': 'tabedit'})
|
||||
```
|
||||
|
||||
Instead of using the default find command, you can use any shell command as
|
||||
the source. The following example will list the files managed by git. It's
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
You can use any shell command as the source to generate the list. The
|
||||
following example will list the files managed by git. It's equivalent to
|
||||
running `git ls-files | fzf` on shell.
|
||||
|
||||
```vim
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
@@ -282,12 +290,13 @@ The following table summarizes the available options.
|
||||
| `source` | string | External command to generate input to fzf (e.g. `find .`) |
|
||||
| `source` | list | Vim list as input to fzf |
|
||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||
| `sink` | funcref | Reference to function to process each selected item |
|
||||
| `sink` | funcref | Function to be called with each selected item |
|
||||
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||
| `exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130) |
|
||||
| `options` | string/list | Options to fzf |
|
||||
| `dir` | string | Working directory |
|
||||
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) |
|
||||
| `tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%`) |
|
||||
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||
| `window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}`) |
|
||||
|
||||
@@ -309,7 +318,7 @@ following options are allowed:
|
||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `relative` [boolean default v:false]
|
||||
- `border` [string default `rounded`]: Border style
|
||||
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||
|
||||
`fzf#wrap`
|
||||
@@ -450,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
|
||||
or above) by putting fzf-tmux options in `tmux` key.
|
||||
or above) by putting `--tmux` option value in `tmux` key.
|
||||
|
||||
```vim
|
||||
" See `man fzf-tmux` for available options
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
@@ -483,4 +493,4 @@ autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
33
SECURITY.md
Normal file
33
SECURITY.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Security Reporting
|
||||
|
||||
If you wish to report a security vulnerability privately, we appreciate your diligence. Please follow the guidelines below to submit your report.
|
||||
|
||||
## Reporting
|
||||
|
||||
To report a security vulnerability, please provide the following information:
|
||||
|
||||
1. **PROJECT**
|
||||
- https://github.com/junegunn/fzf
|
||||
|
||||
2. **PUBLIC**
|
||||
- Indicate whether this vulnerability has already been publicly discussed or disclosed.
|
||||
- If so, provide relevant links.
|
||||
|
||||
3. **DESCRIPTION**
|
||||
- Provide a detailed description of the security vulnerability.
|
||||
- Include as much information as possible to help us understand and address the issue.
|
||||
|
||||
Send this information, along with any additional relevant details, to <junegunn.c AT gmail DOT com>.
|
||||
|
||||
## Confidentiality
|
||||
|
||||
We kindly ask you to keep the report confidential until a public announcement is made.
|
||||
|
||||
## Notes
|
||||
|
||||
- Vulnerabilities will be handled on a best-effort basis.
|
||||
- You may request an advance copy of the patched release, but we cannot guarantee early access before the public release.
|
||||
- You will be notified via email simultaneously with the public announcement.
|
||||
- We will respond within a few weeks to confirm whether your report has been accepted or rejected.
|
||||
|
||||
Thank you for helping to improve the security of our project!
|
||||
86
bin/fzf-preview.sh
Executable file
86
bin/fzf-preview.sh
Executable file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# The purpose of this script is to demonstrate how to preview a file or an
|
||||
# image in the preview window of fzf.
|
||||
#
|
||||
# Dependencies:
|
||||
# - https://github.com/sharkdp/bat
|
||||
# - https://github.com/hpjansson/chafa
|
||||
# - https://iterm2.com/utilities/imgcat
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
>&2 echo "usage: $0 FILENAME[:LINENO][:IGNORED]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
file=${1/#\~\//$HOME/}
|
||||
|
||||
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 =~ =binary ]]; then
|
||||
file "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
# Sometimes bat is installed as batcat.
|
||||
if command -v batcat > /dev/null; then
|
||||
batname="batcat"
|
||||
elif command -v bat > /dev/null; then
|
||||
batname="bat"
|
||||
else
|
||||
cat "$1"
|
||||
exit
|
||||
fi
|
||||
|
||||
${batname} --style="${BAT_STYLE:-numbers}" --color=always --pager=never --highlight-line="${center:-0}" -- "$file"
|
||||
exit
|
||||
fi
|
||||
|
||||
dim=${FZF_PREVIEW_COLUMNS}x${FZF_PREVIEW_LINES}
|
||||
if [[ $dim = x ]]; then
|
||||
dim=$(stty size < /dev/tty | awk '{print $2 "x" $1}')
|
||||
elif ! [[ $KITTY_WINDOW_ID ]] && (( FZF_PREVIEW_TOP + FZF_PREVIEW_LINES == $(stty size < /dev/tty | awk '{print $1}') )); then
|
||||
# Avoid scrolling issue when the Sixel image touches the bottom of the screen
|
||||
# * https://github.com/junegunn/fzf/issues/2544
|
||||
dim=${FZF_PREVIEW_COLUMNS}x$((FZF_PREVIEW_LINES - 1))
|
||||
fi
|
||||
|
||||
# 1. Use icat (from Kitty) if kitten is installed
|
||||
if [[ $KITTY_WINDOW_ID ]] || [[ $GHOSTTY_RESOURCES_DIR ]] && command -v kitten > /dev/null; then
|
||||
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||
# you have to use 'stream'.
|
||||
#
|
||||
# 2. The last line of the output is the ANSI reset code without newline.
|
||||
# This confuses fzf and makes it render scroll offset indicator.
|
||||
# So we remove the last line and append the reset code to its previous line.
|
||||
kitten icat --clear --transfer-mode=memory --unicode-placeholder --stdin=no --place="$dim@0x0" "$file" | sed '$d' | sed $'$s/$/\e[m/'
|
||||
|
||||
# 2. Use chafa with Sixel output
|
||||
elif command -v chafa > /dev/null; then
|
||||
chafa -s "$dim" "$file"
|
||||
# Add a new line character so that fzf can display multiple images in the preview window
|
||||
echo
|
||||
|
||||
# 3. If chafa is not found but imgcat is available, use it on iTerm2
|
||||
elif command -v imgcat > /dev/null; then
|
||||
# NOTE: We should use https://iterm2.com/utilities/it2check to check if the
|
||||
# user is running iTerm2. But for the sake of simplicity, we just assume
|
||||
# that's the case here.
|
||||
imgcat -W "${dim%%x*}" -H "${dim##*x}" "$file"
|
||||
|
||||
# 4. Cannot find any suitable method to preview the image
|
||||
else
|
||||
file "$file"
|
||||
fi
|
||||
79
bin/fzf-tmux
79
bin/fzf-tmux
@@ -7,10 +7,9 @@ fail() {
|
||||
exit 2
|
||||
}
|
||||
|
||||
fzf="$(command -v fzf 2> /dev/null)" || fzf="$(dirname "$0")/fzf"
|
||||
fzf="$(command which fzf)" || fzf="$(dirname "$0")/fzf"
|
||||
[[ -x "$fzf" ]] || fail 'fzf executable not found'
|
||||
|
||||
tmux_args=()
|
||||
args=()
|
||||
opt=""
|
||||
skip=""
|
||||
@@ -20,6 +19,9 @@ term=""
|
||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||
|
||||
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||
tmux_32=$(awk '{print ($1 >= 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version >= 3.2")
|
||||
|
||||
help() {
|
||||
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
|
||||
@@ -58,7 +60,7 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
||||
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
||||
[[ "$opt" =~ "-K -E" ]] || opt="-K -E"
|
||||
[[ "$opt" =~ "-E" ]] || opt="-E"
|
||||
elif [[ "$arg" =~ ^.[lr] ]]; then
|
||||
opt="-h"
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
@@ -95,10 +97,18 @@ while [[ $# -gt 0 ]]; do
|
||||
opt="$opt ${arg:0:2}$size"
|
||||
elif [[ "$size" =~ %$ ]]; then
|
||||
size=${size:0:((${#size}-1))}
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
if [[ $tmux_32 = 1 ]]; then
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -l $(( 100 - size ))%"
|
||||
else
|
||||
opt="$opt -l $size%"
|
||||
fi
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
if [[ -n "$swap" ]]; then
|
||||
opt="$opt -p $(( 100 - size ))"
|
||||
else
|
||||
opt="$opt -p $size"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
if [[ -n "$swap" ]]; then
|
||||
@@ -119,8 +129,6 @@ while [[ $# -gt 0 ]]; do
|
||||
# "--" can be used to separate fzf-tmux options from fzf options to
|
||||
# avoid conflicts
|
||||
skip=1
|
||||
tmux_args=("${args[@]}")
|
||||
args=()
|
||||
continue
|
||||
;;
|
||||
*)
|
||||
@@ -135,11 +143,13 @@ if [[ -z "$TMUX" ]]; then
|
||||
exit $?
|
||||
fi
|
||||
|
||||
# --height option is not allowed. CTRL-Z is also disabled.
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
|
||||
# * --height option is not allowed
|
||||
# * CTRL-Z is also disabled
|
||||
# * fzf-tmux script is not compatible with --tmux option in fzf 0.53.0 or later
|
||||
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore" "--no-tmux")
|
||||
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! "$opt" =~ "-K -E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed_without_popup=1
|
||||
original_window=$(tmux display-message -p "#{window_id}")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c fzf-tmux is running\\r\"; done; done'")
|
||||
@@ -154,12 +164,18 @@ argsf="${TMPDIR:-/tmp}/fzf-args-$id"
|
||||
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
|
||||
tmux_off_opts='; set-option -p synchronize-panes off ; set-option -p remain-on-exit off'
|
||||
else
|
||||
tmux_win_opts=( $(tmux show-window-options remain-on-exit \; show-window-options synchronize-panes | sed '/ off/d; s/^/set-window-option /; s/$/ \\;/') )
|
||||
tmux_off_opts='; set-window-option synchronize-panes off ; set-window-option remain-on-exit off'
|
||||
fi
|
||||
cleanup() {
|
||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||
|
||||
# Restore tmux window options
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 0 ]]; then
|
||||
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
|
||||
eval "tmux ${tmux_win_opts[*]}"
|
||||
fi
|
||||
|
||||
@@ -181,9 +197,22 @@ trap 'cleanup 1' SIGUSR1
|
||||
trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
[[ "$opt" =~ "-K -E" ]] && FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
if [[ $tmux_version = 3.2 ]]; then
|
||||
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||
elif [[ $tmux_32 = 1 ]]; then
|
||||
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||
opt="-B $opt"
|
||||
else
|
||||
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||
envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS")"
|
||||
envs="$envs FZF_DEFAULT_OPTS_FILE=$(printf %q "$FZF_DEFAULT_OPTS_FILE")"
|
||||
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
||||
# Build arguments to fzf
|
||||
@@ -195,7 +224,7 @@ close="; trap - EXIT SIGINT SIGTERM $close"
|
||||
|
||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||
mkfifo -m o+w $fifo2
|
||||
if [[ "$opt" =~ "-K -E" ]]; then
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
cat $fifo2 &
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
@@ -205,15 +234,7 @@ if [[ "$opt" =~ "-K -E" ]]; then
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
|
||||
# tmux dropped the support for `-K`, `-R` to popup command
|
||||
# TODO: We can remove this once tmux 3.2 is released
|
||||
if [[ ! "$(tmux popup --help 2>&1)" =~ '-R shell-command' ]]; then
|
||||
opt="${opt/-K/}"
|
||||
else
|
||||
opt="${opt} -R"
|
||||
fi
|
||||
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
|
||||
tmux popup -d "$PWD" $opt "bash $argsf" > /dev/null 2>&1
|
||||
exit $?
|
||||
fi
|
||||
|
||||
@@ -225,9 +246,9 @@ else
|
||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux set-window-option synchronize-panes off \;\
|
||||
set-window-option remain-on-exit off \;\
|
||||
split-window -c "$PWD" $opt "${tmux_args[@]}" "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
tmux \
|
||||
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||
$tmux_off_opts \
|
||||
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
||||
cat $fifo2
|
||||
exit "$(cat $fifo3)"
|
||||
|
||||
91
doc/fzf.txt
91
doc/fzf.txt
@@ -1,4 +1,4 @@
|
||||
fzf.txt fzf Last change: May 19 2021
|
||||
fzf.txt fzf Last change: February 15 2024
|
||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||
==============================================================================
|
||||
|
||||
@@ -32,7 +32,10 @@ depending on the package manager.
|
||||
" If installed using Homebrew
|
||||
set rtp+=/usr/local/opt/fzf
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
set rtp+=/opt/homebrew/opt/fzf
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
set rtp+=~/.fzf
|
||||
<
|
||||
If you use {vim-plug}{1}, the same can be written as:
|
||||
@@ -40,7 +43,10 @@ If you use {vim-plug}{1}, the same can be written as:
|
||||
" If installed using Homebrew
|
||||
Plug '/usr/local/opt/fzf'
|
||||
|
||||
" If installed using git
|
||||
" If installed using Homebrew on Apple Silicon
|
||||
Plug '/opt/homebrew/opt/fzf'
|
||||
|
||||
" If you have cloned fzf on ~/.fzf directory
|
||||
Plug '~/.fzf'
|
||||
<
|
||||
But if you want the latest Vim plugin file from GitHub rather than the one
|
||||
@@ -68,16 +74,16 @@ SUMMARY *fzf-summary*
|
||||
The Vim plugin of fzf provides two core functions, and `:FZF` command which is
|
||||
the basic file selector command built on top of them.
|
||||
|
||||
1. `fzf#run([spec dict])`
|
||||
1. `fzf#run([spec dict])`
|
||||
- Starts fzf inside Vim with the given spec
|
||||
- `:call fzf#run({'source': 'ls'})`
|
||||
2. `fzf#wrap([spec dict]) -> (dict)`
|
||||
- `:call fzf#run({'source': 'ls'})`
|
||||
2. `fzf#wrap([spec dict]) -> (dict)`
|
||||
- Takes a spec for `fzf#run` and returns an extended version of it with
|
||||
additional options for addressing global preferences (`g:fzf_xxx`)
|
||||
- `:echo fzf#wrap({'source': 'ls'})`
|
||||
- `:echo fzf#wrap({'source': 'ls'})`
|
||||
- We usually wrap a spec with `fzf#wrap` before passing it to `fzf#run`
|
||||
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
|
||||
3. `:FZF [fzf_options string] [path string]`
|
||||
- `:call fzf#run(fzf#wrap({'source': 'ls'}))`
|
||||
3. `:FZF [fzf_options string] [path string]`
|
||||
- Basic fuzzy file selector
|
||||
- A reference implementation for those who don't want to write VimScript to
|
||||
implement custom commands
|
||||
@@ -143,7 +149,7 @@ Examples~
|
||||
|
||||
" An action can be a reference to a function that processes selected lines
|
||||
function! s:build_quickfix_list(lines)
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val }'))
|
||||
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||
copen
|
||||
cc
|
||||
endfunction
|
||||
@@ -222,12 +228,12 @@ list:
|
||||
`spinner` | Streaming input indicator
|
||||
`query` | Query string
|
||||
`disabled` | Query string when search is disabled
|
||||
`prompt` | Prompt before query ( `> ` )
|
||||
`prompt` | Prompt before query ( `> ` )
|
||||
`pointer` | Pointer to the current line ( `>` )
|
||||
----------------------------+------------------------------------------------------
|
||||
- `component` specifies the component (`fg` / `bg`) from which to extract the
|
||||
color when considering each of the following highlight groups
|
||||
- `group1 [, group2, ...]` is a list of highlight groups that are searched (in
|
||||
- `group1 [, group2, ...]` is a list of highlight groups that are searched (in
|
||||
order) for a matching color definition
|
||||
|
||||
For example, consider the following specification:
|
||||
@@ -258,17 +264,18 @@ entry.
|
||||
call fzf#run({'sink': 'e'})
|
||||
<
|
||||
We haven't specified the `source`, so this is equivalent to starting fzf on
|
||||
command line without standard input pipe; fzf will use find command (or
|
||||
`$FZF_DEFAULT_COMMAND` if defined) to list the files under the current
|
||||
directory. When you select one, it will open it with the sink, `:e` command.
|
||||
If you want to open it in a new tab, you can pass `:tabedit` command instead
|
||||
as the sink.
|
||||
command line without standard input pipe; fzf will traverse the file system
|
||||
under the current directory to get the list of files. (If
|
||||
`$FZF_DEFAULT_COMMAND` is set, fzf will use the output of the command
|
||||
instead.) When you select one, it will open it with the sink, `:e` command. If
|
||||
you want to open it in a new tab, you can pass `:tabedit` command instead as
|
||||
the sink.
|
||||
>
|
||||
call fzf#run({'sink': 'tabedit'})
|
||||
<
|
||||
Instead of using the default find command, you can use any shell command as
|
||||
the source. The following example will list the files managed by git. It's
|
||||
equivalent to running `git ls-files | fzf` on shell.
|
||||
You can use any shell command as the source to generate the list. The
|
||||
following example will list the files managed by git. It's equivalent to
|
||||
running `git ls-files | fzf` on shell.
|
||||
>
|
||||
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||
<
|
||||
@@ -296,17 +303,18 @@ The following table summarizes the available options.
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
Option name | Type | Description ~
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
`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
|
||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||
`sink` | funcref | Reference to function to process each selected item
|
||||
`sink` | funcref | Function to be called with each selected item
|
||||
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||
`exit` | funcref | Function to be called with the exit status of fzf (e.g. 0, 1, 2, 130)
|
||||
`options` | string/list | Options to fzf
|
||||
`dir` | string | Working directory
|
||||
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
|
||||
`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}` )
|
||||
`tmux` | string | (Layout) `--tmux` options (e.g. `90%,70%` )
|
||||
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||
`window` (Vim 8 / Neovim) | dict | (Layout) Popup window settings (e.g. `{'width': 0.9, 'height': 0.6}` )
|
||||
---------------------------+---------------+----------------------------------------------------------------------
|
||||
|
||||
`options` entry can be either a string or a list. For simple cases, string
|
||||
@@ -325,7 +333,7 @@ following options are allowed:
|
||||
- `yoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `xoffset` [float default 0.5 range [0 ~ 1]]
|
||||
- `relative` [boolean default v:false]
|
||||
- `border` [string default `rounded`]: Border style
|
||||
- `border` [string default `rounded` (`sharp` on Windows)]: Border style
|
||||
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||
|
||||
|
||||
@@ -343,7 +351,7 @@ So how can we make our custom `fzf#run` calls also respect those variables?
|
||||
Simply by "wrapping" the spec dictionary with `fzf#wrap` before passing it to
|
||||
`fzf#run`.
|
||||
|
||||
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
|
||||
- `fzf#wrap([name string], [spec dict], [fullscreen bool]) -> (dict)`
|
||||
- All arguments are optional. Usually we only need to pass a spec
|
||||
dictionary.
|
||||
- `name` is for managing history files. It is ignored if `g:fzf_history_dir`
|
||||
@@ -377,7 +385,7 @@ last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
|
||||
command! -bang LS call fzf#run(fzf#wrap({'source': 'ls'}, <bang>0))
|
||||
<
|
||||
Our `:LS` command will be much more useful if we can pass a directory argument
|
||||
to it, so that something like `:LS /tmp` is possible.
|
||||
to it, so that something like `:LS /tmp` is possible.
|
||||
>
|
||||
command! -bang -complete=dir -nargs=? LS
|
||||
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||
@@ -396,10 +404,10 @@ unique name to our command and pass it as the first argument to `fzf#wrap`.
|
||||
|
||||
- `g:fzf_layout`
|
||||
- `g:fzf_action`
|
||||
- Works only when no custom `sink` (or `sink*`) is provided
|
||||
- Works only when no custom `sink` (or `sinklist`) is provided
|
||||
- Having custom sink usually means that each entry is not an ordinary
|
||||
file path (e.g. name of color scheme), so we can't blindly apply the
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
same strategy (i.e. `tabedit some-color-scheme` doesn't make sense)
|
||||
- `g:fzf_colors`
|
||||
- `g:fzf_history_dir`
|
||||
|
||||
@@ -411,24 +419,12 @@ TIPS *fzf-tips*
|
||||
< fzf inside terminal buffer >________________________________________________~
|
||||
*fzf-inside-terminal-buffer*
|
||||
|
||||
The latest versions of Vim and Neovim include builtin terminal emulator
|
||||
(`:terminal`) and fzf will start in a terminal buffer in the following cases:
|
||||
|
||||
- On Neovim
|
||||
- On GVim
|
||||
- On Terminal Vim with a non-default layout
|
||||
- `call fzf#run({'left': '30%'})` or `let g:fzf_layout = {'left': '30%'}`
|
||||
|
||||
On the latest versions of Vim and Neovim, fzf will start in a terminal buffer.
|
||||
If you find the default ANSI colors to be different, consider configuring the
|
||||
colors using `g:terminal_ansi_colors` in regular Vim or `g:terminal_color_x`
|
||||
in Neovim.
|
||||
|
||||
*g:terminal_color_15* *g:terminal_color_14* *g:terminal_color_13*
|
||||
*g:terminal_color_12* *g:terminal_color_11* *g:terminal_color_10* *g:terminal_color_9*
|
||||
*g:terminal_color_8* *g:terminal_color_7* *g:terminal_color_6* *g:terminal_color_5*
|
||||
*g:terminal_color_4* *g:terminal_color_3* *g:terminal_color_2* *g:terminal_color_1*
|
||||
*g:terminal_color_0*
|
||||
>
|
||||
" Terminal colors for seoul256 color scheme
|
||||
if has('nvim')
|
||||
@@ -474,11 +470,12 @@ in Neovim.
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
<
|
||||
Alternatively, you can make fzf open in a tmux popup window (requires tmux 3.2
|
||||
or above) by putting fzf-tmux options in `tmux` key.
|
||||
or above) by putting `--tmux` options in `tmux` key.
|
||||
>
|
||||
" See `man fzf-tmux` for available options
|
||||
" See `--tmux` option in `man fzf` for available options
|
||||
" [center|top|bottom|left|right][,SIZE[%]][,SIZE[%]]
|
||||
if exists('$TMUX')
|
||||
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||
let g:fzf_layout = { 'tmux': '90%,70%' }
|
||||
else
|
||||
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||
endif
|
||||
@@ -488,7 +485,7 @@ or above) by putting fzf-tmux options in `tmux` key.
|
||||
*fzf-hide-statusline*
|
||||
|
||||
When fzf starts in a terminal buffer, the file type of the buffer is set to
|
||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||
`fzf`. So you can set up `FileType fzf` autocmd to customize the settings of
|
||||
the window.
|
||||
|
||||
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||
@@ -506,7 +503,7 @@ LICENSE *fzf-license*
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
==============================================================================
|
||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||
|
||||
27
go.mod
27
go.mod
@@ -1,17 +1,20 @@
|
||||
module github.com/junegunn/fzf
|
||||
|
||||
require (
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mattn/go-runewidth v0.0.13
|
||||
github.com/mattn/go-shellwords v1.0.12
|
||||
github.com/rivo/uniseg v0.2.0
|
||||
github.com/saracen/walker v0.1.2
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
github.com/charlievieth/fastwalk v1.0.14
|
||||
github.com/gdamore/tcell/v2 v2.9.0
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/rivo/uniseg v0.4.7
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/term v0.34.0
|
||||
)
|
||||
|
||||
go 1.13
|
||||
require (
|
||||
github.com/gdamore/encoding v1.0.1 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
||||
go 1.23.0
|
||||
|
||||
74
go.sum
74
go.sum
@@ -1,31 +1,55 @@
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/charlievieth/fastwalk v1.0.14 h1:3Eh5uaFGwHZd8EGwTjJnSpBkfwfsak9h6ICgnWlhAyg=
|
||||
github.com/charlievieth/fastwalk v1.0.14/go.mod h1:diVcUreiU1aQ4/Wu3NbxxH4/KYdKpLDojrQ1Bb2KgNY=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.9.0 h1:N6t+eqK7/xwtRPwxzs1PXeRWnm0H9l02CrgJ7DLn1ys=
|
||||
github.com/gdamore/tcell/v2 v2.9.0/go.mod h1:8/ZoqM9rxzYphT9tH/9LnunhV9oPBqwS8WHGYm5nrmo=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741 h1:7dYDtfMDfKzjT+DVfIS4iqknSEKtZpEcXtu6vuaasHs=
|
||||
github.com/junegunn/go-shellwords v0.0.0-20250127100254-2aa3b3277741/go.mod h1:6EILKtGpo5t+KLb85LNZLAF6P9LKp78hJI80PXMcn3c=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||
github.com/mattn/go-runewidth v0.0.13/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 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/saracen/walker v0.1.2 h1:/o1TxP82n8thLvmL4GpJXduYaRmJ7qXp8u9dSlV0zmo=
|
||||
github.com/saracen/walker v0.1.2/go.mod h1:0oKYMsKVhSJ+ful4p/XbjvXbMgLEkLITZaxozsl4CGE=
|
||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/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/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/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-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
136
install
136
install
@@ -2,7 +2,7 @@
|
||||
|
||||
set -u
|
||||
|
||||
version=0.29.0
|
||||
version=0.65.2
|
||||
auto_completion=
|
||||
key_bindings=
|
||||
update_config=2
|
||||
@@ -83,7 +83,7 @@ ask() {
|
||||
check_binary() {
|
||||
echo -n " - Checking fzf executable ... "
|
||||
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
|
||||
echo "Error: $output"
|
||||
binary_error="Invalid binary"
|
||||
@@ -115,7 +115,7 @@ link_fzf_in_path() {
|
||||
try_curl() {
|
||||
command -v curl > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
curl -fL $1 | tar -xzf -
|
||||
curl -fL $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
curl -fLo "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
@@ -125,7 +125,7 @@ try_curl() {
|
||||
try_wget() {
|
||||
command -v wget > /dev/null &&
|
||||
if [[ $1 =~ tar.gz$ ]]; then
|
||||
wget -O - $1 | tar -xzf -
|
||||
wget -O - $1 | tar --no-same-owner -xzf -
|
||||
else
|
||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||
wget -O "$temp" $1 && unzip -o "$temp" && rm -f "$temp"
|
||||
@@ -146,7 +146,7 @@ download() {
|
||||
fi
|
||||
|
||||
local url
|
||||
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
|
||||
url=https://github.com/junegunn/fzf/releases/download/v$version/${1}
|
||||
set -o pipefail
|
||||
if ! (try_curl $url || try_wget $url); then
|
||||
set +o pipefail
|
||||
@@ -164,25 +164,29 @@ download() {
|
||||
}
|
||||
|
||||
# Try to download binary executable
|
||||
archi=$(uname -sm)
|
||||
archi=$(uname -smo 2>/dev/null || uname -sm)
|
||||
binary_available=1
|
||||
binary_error=""
|
||||
case "$archi" in
|
||||
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
||||
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64) download fzf-$version-windows_amd64.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
Darwin\ arm64*) download fzf-$version-darwin_arm64.tar.gz ;;
|
||||
Darwin\ x86_64*) download fzf-$version-darwin_amd64.tar.gz ;;
|
||||
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ aarch64\ Android) download fzf-$version-android_arm64.tar.gz ;;
|
||||
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||
Linux\ loongarch64*) download fzf-$version-linux_loong64.tar.gz ;;
|
||||
Linux\ ppc64le*) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||
Linux\ *64*) download fzf-$version-linux_amd64.tar.gz ;;
|
||||
Linux\ s390x*) download fzf-$version-linux_s390x.tar.gz ;;
|
||||
FreeBSD\ *64*) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||
OpenBSD\ *64*) download fzf-$version-openbsd_amd64.tar.gz ;;
|
||||
CYGWIN*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MINGW*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
MSYS*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
Windows*\ *64*) download fzf-$version-windows_amd64.zip ;;
|
||||
*) binary_available=0 binary_error=1 ;;
|
||||
esac
|
||||
|
||||
cd "$fzf_base"
|
||||
@@ -193,12 +197,12 @@ if [ -n "$binary_error" ]; then
|
||||
echo " - $binary_error !!!"
|
||||
fi
|
||||
if command -v go > /dev/null; then
|
||||
echo -n "Building binary (go get -u github.com/junegunn/fzf) ... "
|
||||
echo -n "Building binary (go install github.com/junegunn/fzf) ... "
|
||||
if [ -z "${GOPATH-}" ]; then
|
||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||
mkdir -p "$GOPATH"
|
||||
fi
|
||||
if go get -ldflags "-s -w -X main.version=$version -X main.revision=go-get" github.com/junegunn/fzf; then
|
||||
if go install -ldflags "-s -w -X main.version=$version -X main.revision=go-install" github.com/junegunn/fzf; then
|
||||
echo "OK"
|
||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||
else
|
||||
@@ -242,7 +246,7 @@ for shell in $shells; do
|
||||
src=${prefix_expand}.${shell}
|
||||
echo -n "Generate $src ... "
|
||||
|
||||
fzf_completion="[[ \$- == *i* ]] && source \"$fzf_base/shell/completion.${shell}\" 2> /dev/null"
|
||||
fzf_completion="source \"$fzf_base/shell/completion.${shell}\""
|
||||
if [ $auto_completion -eq 0 ]; then
|
||||
fzf_completion="# $fzf_completion"
|
||||
fi
|
||||
@@ -256,9 +260,19 @@ for shell in $shells; do
|
||||
# Setup fzf
|
||||
# ---------
|
||||
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
|
||||
export PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||
fi
|
||||
|
||||
EOF
|
||||
|
||||
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"
|
||||
fi
|
||||
else
|
||||
cat >> "$src" << EOF
|
||||
# Auto-completion
|
||||
# ---------------
|
||||
$fzf_completion
|
||||
@@ -267,6 +281,7 @@ $fzf_completion
|
||||
# ------------
|
||||
$fzf_key_bindings
|
||||
EOF
|
||||
fi
|
||||
echo "OK"
|
||||
done
|
||||
|
||||
@@ -278,55 +293,47 @@ if [[ "$shells" =~ fish ]]; then
|
||||
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
|
||||
EOF
|
||||
[ $? -eq 0 ] && echo "OK" || echo "Failed"
|
||||
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
if [ -e "${fish_dir}/functions/fzf.fish" ]; then
|
||||
echo -n "Remove unnecessary ${fish_dir}/functions/fzf.fish ... "
|
||||
rm -f "${fish_dir}/functions/fzf.fish" && echo "OK" || echo "Failed"
|
||||
fi
|
||||
|
||||
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
|
||||
if [ $key_bindings -ne 0 ]; then
|
||||
echo -n "Symlink $fish_binding ... "
|
||||
ln -sf "$fzf_base/shell/key-bindings.fish" \
|
||||
"$fish_binding" && echo "OK" || echo "Failed"
|
||||
else
|
||||
echo -n "Removing $fish_binding ... "
|
||||
rm -f "$fish_binding"
|
||||
echo "OK"
|
||||
fi
|
||||
fi
|
||||
|
||||
append_line() {
|
||||
set -e
|
||||
|
||||
local update line file pat lno
|
||||
local update line file pat lines
|
||||
update="$1"
|
||||
line="$2"
|
||||
file="$3"
|
||||
pat="${4:-}"
|
||||
lno=""
|
||||
lines=""
|
||||
|
||||
echo "Update $file:"
|
||||
echo " - $line"
|
||||
if [ -f "$file" ]; then
|
||||
if [ $# -lt 4 ]; then
|
||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
lines=$(\grep -nF "$line" "$file")
|
||||
else
|
||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
lines=$(\grep -nF "$pat" "$file")
|
||||
fi
|
||||
fi
|
||||
if [ -n "$lno" ]; then
|
||||
echo " - Already exists: line #$lno"
|
||||
|
||||
if [ -n "$lines" ]; then
|
||||
echo " - Already exists:"
|
||||
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"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
if [ $update -eq 1 ]; then
|
||||
[ -f "$file" ] && echo >> "$file"
|
||||
echo "$line" >> "$file"
|
||||
echo " + Added"
|
||||
else
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
echo " ~ Skipped"
|
||||
fi
|
||||
|
||||
echo
|
||||
set +e
|
||||
}
|
||||
@@ -357,12 +364,23 @@ done
|
||||
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
||||
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||
if [ ! -e "$bind_file" ]; then
|
||||
mkdir -p "${fish_dir}/functions"
|
||||
create_file "$bind_file" \
|
||||
'function fish_user_key_bindings' \
|
||||
' fzf_key_bindings' \
|
||||
' fzf --fish | source' \
|
||||
'end'
|
||||
else
|
||||
append_line $update_config "fzf_key_bindings" "$bind_file"
|
||||
echo "Check $bind_file:"
|
||||
lno=$(\grep -nF "fzf_key_bindings" "$bind_file" | sed 's/:.*//' | tr '\n' ' ')
|
||||
if [[ -n $lno ]]; then
|
||||
echo " ** Found 'fzf_key_bindings' in line #$lno"
|
||||
echo " ** You have to replace the line to 'fzf --fish | source'"
|
||||
echo
|
||||
else
|
||||
echo " - Clear"
|
||||
echo
|
||||
append_line $update_config "fzf --fish | source" "$bind_file"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
$version="0.29.0"
|
||||
$version="0.65.2"
|
||||
|
||||
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||
|
||||
@@ -40,7 +40,7 @@ function download {
|
||||
return
|
||||
}
|
||||
cd "$fzf_base\bin"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
||||
$url="https://github.com/junegunn/fzf/releases/download/v$version/$file"
|
||||
$temp=$env:TMP + "\fzf.zip"
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||
|
||||
93
main.go
93
main.go
@@ -1,14 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
fzf "github.com/junegunn/fzf/src"
|
||||
"github.com/junegunn/fzf/src/protector"
|
||||
)
|
||||
|
||||
var version string = "0.29"
|
||||
var revision string = "devel"
|
||||
var version = "0.65"
|
||||
var revision = "devel"
|
||||
|
||||
//go:embed shell/key-bindings.bash
|
||||
var bashKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.bash
|
||||
var bashCompletion []byte
|
||||
|
||||
//go:embed shell/key-bindings.zsh
|
||||
var zshKeyBindings []byte
|
||||
|
||||
//go:embed shell/completion.zsh
|
||||
var zshCompletion []byte
|
||||
|
||||
//go:embed shell/key-bindings.fish
|
||||
var fishKeyBindings []byte
|
||||
|
||||
//go:embed man/man1/fzf.1
|
||||
var manPage []byte
|
||||
|
||||
func printScript(label string, content []byte) {
|
||||
fmt.Println("### " + label + " ###")
|
||||
fmt.Println(strings.TrimSpace(string(content)))
|
||||
fmt.Println("### end: " + label + " ###")
|
||||
}
|
||||
|
||||
func exit(code int, err error) {
|
||||
if code == fzf.ExitError && err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func main() {
|
||||
protector.Protect()
|
||||
fzf.Run(fzf.ParseOptions(), version, revision)
|
||||
|
||||
options, err := fzf.ParseOptions(true, os.Args[1:])
|
||||
if err != nil {
|
||||
exit(fzf.ExitError, err)
|
||||
return
|
||||
}
|
||||
if options.Bash {
|
||||
printScript("key-bindings.bash", bashKeyBindings)
|
||||
printScript("completion.bash", bashCompletion)
|
||||
return
|
||||
}
|
||||
if options.Zsh {
|
||||
printScript("key-bindings.zsh", zshKeyBindings)
|
||||
printScript("completion.zsh", zshCompletion)
|
||||
return
|
||||
}
|
||||
if options.Fish {
|
||||
printScript("key-bindings.fish", fishKeyBindings)
|
||||
fmt.Println("fzf_key_bindings")
|
||||
return
|
||||
}
|
||||
if options.Help {
|
||||
fmt.Print(fzf.Usage)
|
||||
return
|
||||
}
|
||||
if options.Version {
|
||||
if len(revision) > 0 {
|
||||
fmt.Printf("%s (%s)\n", version, revision)
|
||||
} else {
|
||||
fmt.Println(version)
|
||||
}
|
||||
return
|
||||
}
|
||||
if options.Man {
|
||||
file := fzf.WriteTemporaryFile([]string{string(manPage)}, "\n")
|
||||
if len(file) == 0 {
|
||||
fmt.Print(string(manPage))
|
||||
return
|
||||
}
|
||||
defer os.Remove(file)
|
||||
cmd := exec.Command("man", file)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Print(string(manPage))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
code, err := fzf.Run(options)
|
||||
exit(code, err)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.ig
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
@@ -21,48 +21,48 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH fzf-tmux 1 "Dec 2021" "fzf 0.29.0" "fzf-tmux - open fzf in tmux split pane"
|
||||
.TH fzf\-tmux 1 "Aug 2025" "fzf 0.65.2" "fzf\-tmux - open fzf in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
fzf-tmux - open fzf in tmux split pane
|
||||
fzf\-tmux - open fzf in tmux split pane
|
||||
|
||||
.SH SYNOPSIS
|
||||
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||
.B fzf\-tmux [\fILAYOUT OPTIONS\fR] [\-\-] [\fIFZF OPTIONS\fR]
|
||||
|
||||
.SH DESCRIPTION
|
||||
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
fzf\-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||
a tmux popup window. It is designed to work just like fzf except that it does
|
||||
not take up the whole screen. You can safely use fzf-tmux instead of fzf in
|
||||
not take up the whole screen. You can safely use fzf\-tmux instead of fzf in
|
||||
your scripts as the extra options will be silently ignored if you're not on
|
||||
tmux.
|
||||
|
||||
.SH LAYOUT OPTIONS
|
||||
|
||||
(default layout: \fB-d 50%\fR)
|
||||
(default layout: \fB\-d 50%\fR)
|
||||
|
||||
.SS Popup window
|
||||
(requires tmux 3.2 or above)
|
||||
.TP
|
||||
.B "-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.B "\-p [WIDTH[%][,HEIGHT[%]]]"
|
||||
.TP
|
||||
.B "-w WIDTH[%]"
|
||||
.B "\-w WIDTH[%]"
|
||||
.TP
|
||||
.B "-h WIDTH[%]"
|
||||
.B "\-h WIDTH[%]"
|
||||
.TP
|
||||
.B "-x COL"
|
||||
.B "\-x COL"
|
||||
.TP
|
||||
.B "-y ROW"
|
||||
.B "\-y ROW"
|
||||
|
||||
.SS Split pane
|
||||
.TP
|
||||
.B "-u [height[%]]"
|
||||
.B "\-u [height[%]]"
|
||||
Split above (up)
|
||||
.TP
|
||||
.B "-d [height[%]]"
|
||||
.B "\-d [height[%]]"
|
||||
Split below (down)
|
||||
.TP
|
||||
.B "-l [width[%]]"
|
||||
.B "\-l [width[%]]"
|
||||
Split left
|
||||
.TP
|
||||
.B "-r [width[%]]"
|
||||
.B "\-r [width[%]]"
|
||||
Split right
|
||||
|
||||
2103
man/man1/fzf.1
2103
man/man1/fzf.1
File diff suppressed because it is too large
Load Diff
231
plugin/fzf.vim
231
plugin/fzf.vim
@@ -1,4 +1,4 @@
|
||||
" Copyright (c) 2017 Junegunn Choi
|
||||
" Copyright (c) 2013-2025 Junegunn Choi
|
||||
"
|
||||
" MIT License
|
||||
"
|
||||
@@ -59,12 +59,9 @@ if s:is_win
|
||||
return iconv(a:str, &encoding, 'cp'.s:codepage)
|
||||
endfunction
|
||||
function! s:wrap_cmds(cmds)
|
||||
return map([
|
||||
\ '@echo off',
|
||||
\ 'setlocal enabledelayedexpansion']
|
||||
return map(['@echo off']
|
||||
\ + (has('gui_running') ? ['set TERM= > nul'] : [])
|
||||
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds])
|
||||
\ + ['endlocal'],
|
||||
\ + (type(a:cmds) == type([]) ? a:cmds : [a:cmds]),
|
||||
\ '<SID>enc_to_cp(v:val."\r")')
|
||||
endfunction
|
||||
else
|
||||
@@ -84,11 +81,21 @@ else
|
||||
endif
|
||||
|
||||
function! s:shellesc_cmd(arg)
|
||||
let escaped = substitute(a:arg, '[&|<>()@^]', '^&', 'g')
|
||||
let escaped = substitute(escaped, '%', '%%', 'g')
|
||||
let escaped = substitute(escaped, '"', '\\^&', 'g')
|
||||
let escaped = substitute(escaped, '\(\\\+\)\(\\^\)', '\1\1\2', 'g')
|
||||
return '^"'.substitute(escaped, '\(\\\+\)$', '\1\1', '').'^"'
|
||||
let e = '"'
|
||||
let slashes = 0
|
||||
for c in split(a:arg, '\zs')
|
||||
if c ==# '\'
|
||||
let slashes += 1
|
||||
elseif c ==# '"'
|
||||
let e .= repeat('\', slashes + 1)
|
||||
let slashes = 0
|
||||
else
|
||||
let slashes = 0
|
||||
endif
|
||||
let e .= c
|
||||
endfor
|
||||
let e .= repeat('\', slashes) .'"'
|
||||
return substitute(substitute(e, '[&|<>()^!"]', '^&', 'g'), '%', '%%', 'g')
|
||||
endfunction
|
||||
|
||||
function! fzf#shellescape(arg, ...)
|
||||
@@ -96,7 +103,12 @@ function! fzf#shellescape(arg, ...)
|
||||
if shell =~# 'cmd.exe$'
|
||||
return s:shellesc_cmd(a:arg)
|
||||
endif
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
try
|
||||
let [shell, &shell] = [&shell, shell]
|
||||
return s:fzf_call('shellescape', a:arg)
|
||||
finally
|
||||
let [shell, &shell] = [&shell, shell]
|
||||
endtry
|
||||
endfunction
|
||||
|
||||
function! s:fzf_getcwd()
|
||||
@@ -138,7 +150,7 @@ function! fzf#install()
|
||||
if !filereadable(script)
|
||||
throw script.' not found'
|
||||
endif
|
||||
let script = 'powershell -ExecutionPolicy Bypass -file ' . script
|
||||
let script = 'powershell -ExecutionPolicy Bypass -file ' . shellescape(script)
|
||||
else
|
||||
let script = s:base_dir.'/install'
|
||||
if !executable(script)
|
||||
@@ -159,7 +171,7 @@ function s:get_version(bin)
|
||||
if has_key(s:versions, a:bin)
|
||||
return s:versions[a:bin]
|
||||
end
|
||||
let command = a:bin . ' --version --no-height'
|
||||
let command = (&shell =~ 'powershell\|pwsh' ? '&' : '') . s:fzf_call('shellescape', a:bin) . ' --version --no-height'
|
||||
let output = systemlist(command)
|
||||
if v:shell_error || empty(output)
|
||||
return ''
|
||||
@@ -186,6 +198,7 @@ function! s:compare_binary_versions(a, b)
|
||||
return s:compare_versions(s:get_version(a:a), s:get_version(a:b))
|
||||
endfunction
|
||||
|
||||
let s:min_version = '0.53.0'
|
||||
let s:checked = {}
|
||||
function! fzf#exec(...)
|
||||
if !exists('s:exec')
|
||||
@@ -213,7 +226,11 @@ function! fzf#exec(...)
|
||||
let s:exec = binaries[-1]
|
||||
endif
|
||||
|
||||
if a:0 && !has_key(s:checked, a:1)
|
||||
let min_version = s:min_version
|
||||
if a:0 && s:compare_versions(a:1, min_version) > 0
|
||||
let min_version = a:1
|
||||
endif
|
||||
if !has_key(s:checked, min_version)
|
||||
let fzf_version = s:get_version(s:exec)
|
||||
if empty(fzf_version)
|
||||
let message = printf('Failed to run "%s --version"', s:exec)
|
||||
@@ -221,17 +238,17 @@ function! fzf#exec(...)
|
||||
throw message
|
||||
end
|
||||
|
||||
if s:compare_versions(fzf_version, a:1) >= 0
|
||||
let s:checked[a:1] = 1
|
||||
if s:compare_versions(fzf_version, min_version) >= 0
|
||||
let s:checked[min_version] = 1
|
||||
return s:exec
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', a:1, fzf_version)) =~? '^y'
|
||||
elseif a:0 < 2 && input(printf('You need fzf %s or above. Found: %s. Download binary? (y/n) ', min_version, fzf_version)) =~? '^y'
|
||||
let s:versions = {}
|
||||
unlet s:exec
|
||||
redraw
|
||||
call fzf#install()
|
||||
return fzf#exec(a:1, 1)
|
||||
return fzf#exec(min_version, 1)
|
||||
else
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', a:1)
|
||||
throw printf('You need to upgrade fzf (required: %s or above)', min_version)
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -315,7 +332,10 @@ function! s:common_sink(action, lines) abort
|
||||
" the execution (e.g. `set autochdir` or `autocmd BufEnter * lcd ...`)
|
||||
let cwd = exists('w:fzf_pushd') ? w:fzf_pushd.dir : expand('%:p:h')
|
||||
for item in a:lines
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^[A-Z]:\' : '^/')
|
||||
if has('win32unix') && item !~ '/'
|
||||
let item = substitute(item, '\', '/', 'g')
|
||||
end
|
||||
if item[0] != '~' && item !~ (s:is_win ? '^\([A-Z]:\)\?\' : '^/')
|
||||
let sep = s:is_win ? '\' : '/'
|
||||
let item = join([cwd, item], cwd[len(cwd)-1] == sep ? '' : sep)
|
||||
endif
|
||||
@@ -337,7 +357,8 @@ function! s:common_sink(action, lines) abort
|
||||
endfunction
|
||||
|
||||
function! s:get_color(attr, ...)
|
||||
let gui = !s:is_win && !has('win32unix') && has('termguicolors') && &termguicolors
|
||||
" 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('gui_running') || has('termguicolors') && &termguicolors))
|
||||
let fam = gui ? 'gui' : 'cterm'
|
||||
let pat = gui ? '^#[a-f0-9]\+' : '^[0-9]\+$'
|
||||
for group in a:000
|
||||
@@ -450,6 +471,32 @@ function! s:writefile(...)
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! s:extract_option(opts, name)
|
||||
let opt = ''
|
||||
let expect = 0
|
||||
" There are a few cases where this function doesn't work as expected.
|
||||
" Let's just assume such cases are extremely unlikely in real world.
|
||||
" e.g. --query --border
|
||||
for word in split(a:opts)
|
||||
if expect && word !~ '^"\=-'
|
||||
let opt = opt . ' ' . word
|
||||
let expect = 0
|
||||
elseif word == '--no-'.a:name
|
||||
let opt = ''
|
||||
elseif word =~ '^--'.a:name.'='
|
||||
let opt = word
|
||||
elseif word =~ '^--'.a:name.'$'
|
||||
let opt = word
|
||||
let expect = 1
|
||||
elseif expect
|
||||
let expect = 0
|
||||
endif
|
||||
endfor
|
||||
return opt
|
||||
endfunction
|
||||
|
||||
let s:need_cmd_window = has('win32unix') && $TERM_PROGRAM ==# 'mintty' && s:compare_versions($TERM_PROGRAM_VERSION, '3.4.5') < 0 && !executable('winpty')
|
||||
|
||||
function! fzf#run(...) abort
|
||||
try
|
||||
let [shell, shellslash, shellcmdflag, shellxquote] = s:use_sh()
|
||||
@@ -458,7 +505,7 @@ try
|
||||
let temps = { 'result': s:fzf_tempname() }
|
||||
let optstr = s:evaluate_opts(get(dict, 'options', ''))
|
||||
try
|
||||
let fzf_exec = fzf#shellescape(fzf#exec())
|
||||
let fzf_exec = shellescape(fzf#exec())
|
||||
catch
|
||||
throw v:exception
|
||||
endtry
|
||||
@@ -471,19 +518,19 @@ try
|
||||
endif
|
||||
|
||||
if has_key(dict, 'source')
|
||||
let source = remove(dict, 'source')
|
||||
let source = dict.source
|
||||
let type = type(source)
|
||||
if type == 1
|
||||
let source_command = source
|
||||
let prefix = '('.source.')|'
|
||||
elseif type == 3
|
||||
let temps.input = s:fzf_tempname()
|
||||
call s:writefile(source, temps.input)
|
||||
let source_command = (s:is_win ? 'type ' : 'cat ').fzf#shellescape(temps.input)
|
||||
let prefix = (s:is_win ? 'type ' : 'command cat ').fzf#shellescape(temps.input).'|'
|
||||
else
|
||||
throw 'Invalid source type'
|
||||
endif
|
||||
else
|
||||
let source_command = ''
|
||||
let prefix = ''
|
||||
endif
|
||||
|
||||
let prefer_tmux = get(g:, 'fzf_prefer_tmux', 0) || has_key(dict, 'tmux')
|
||||
@@ -492,25 +539,30 @@ try
|
||||
\ executable('tput') && filereadable('/dev/tty')
|
||||
let has_vim8_term = has('terminal') && has('patch-8.0.995')
|
||||
let has_nvim_term = has('nvim-0.2.1') || has('nvim') && !s:is_win
|
||||
let use_term = has_nvim_term ||
|
||||
\ has_vim8_term && !has('win32unix') && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_term = has_nvim_term || has_vim8_term
|
||||
\ && !s:need_cmd_window
|
||||
\ && (has('gui_running') || s:is_win || s:present(dict, 'down', 'up', 'left', 'right', 'window'))
|
||||
let use_tmux = (has_key(dict, 'tmux') || (!use_height && !use_term || prefer_tmux) && !has('win32unix') && s:splittable(dict)) && s:tmux_enabled()
|
||||
if prefer_tmux && use_tmux
|
||||
let use_height = 0
|
||||
let use_term = 0
|
||||
endif
|
||||
if use_term
|
||||
let optstr .= ' --no-height'
|
||||
let optstr .= ' --no-height --no-tmux'
|
||||
elseif use_height
|
||||
let height = s:calc_size(&lines, dict.down, dict)
|
||||
let optstr .= ' --height='.height
|
||||
let optstr .= ' --no-tmux --height='.height
|
||||
endif
|
||||
let optstr .= s:border_opt(get(dict, 'window', 0))
|
||||
let prev_default_command = $FZF_DEFAULT_COMMAND
|
||||
if len(source_command)
|
||||
let $FZF_DEFAULT_COMMAND = source_command
|
||||
|
||||
if exists('&winborder') && &winborder !=# '' && &winborder !=# 'none'
|
||||
" Add 1-column horizontal margin
|
||||
let optstr = join(['--margin 0,1', optstr])
|
||||
else
|
||||
" Respect --border option given in $FZF_DEFAULT_OPTS and 'options'
|
||||
let optstr = join([s:border_opt(get(dict, 'window', 0)), s:extract_option($FZF_DEFAULT_OPTS, 'border'), optstr])
|
||||
endif
|
||||
let command = (use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
let command = prefix.(use_tmux ? s:fzf_tmux(dict) : fzf_exec).' '.optstr.' > '.temps.result
|
||||
|
||||
if use_term
|
||||
return s:execute_term(dict, command, temps)
|
||||
@@ -521,14 +573,6 @@ try
|
||||
call s:callback(dict, lines)
|
||||
return lines
|
||||
finally
|
||||
if exists('source_command') && len(source_command)
|
||||
if len(prev_default_command)
|
||||
let $FZF_DEFAULT_COMMAND = prev_default_command
|
||||
else
|
||||
let $FZF_DEFAULT_COMMAND = ''
|
||||
silent! execute 'unlet $FZF_DEFAULT_COMMAND'
|
||||
endif
|
||||
endif
|
||||
let [&shell, &shellslash, &shellcmdflag, &shellxquote] = [shell, shellslash, shellcmdflag, shellxquote]
|
||||
endtry
|
||||
endfunction
|
||||
@@ -547,19 +591,21 @@ function! s:fzf_tmux(dict)
|
||||
if empty(size)
|
||||
for o in ['up', 'down', 'left', 'right']
|
||||
if s:present(a:dict, o)
|
||||
let spec = a:dict[o]
|
||||
if (o == 'up' || o == 'down') && spec[0] == '~'
|
||||
let size = '-'.o[0].s:calc_size(&lines, spec, a:dict)
|
||||
else
|
||||
" Legacy boolean option
|
||||
let size = '-'.o[0].(spec == 1 ? '' : substitute(spec, '^\~', '', ''))
|
||||
endif
|
||||
let size = o . ',' . a:dict[o]
|
||||
break
|
||||
endif
|
||||
endfor
|
||||
endif
|
||||
return printf('LINES=%d COLUMNS=%d %s %s - --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size)
|
||||
|
||||
" Legacy fzf-tmux options
|
||||
if size =~ '-'
|
||||
return printf('LINES=%d COLUMNS=%d %s %s %s --',
|
||||
\ &lines, &columns, fzf#shellescape(s:fzf_tmux), size, (has_key(a:dict, 'source') ? '' : '-'))
|
||||
end
|
||||
|
||||
" Using native --tmux option
|
||||
let in = (has_key(a:dict, 'source') ? '' : ' --force-tty-in')
|
||||
return printf('%s --tmux %s%s', fzf#shellescape(fzf#exec()), size, in)
|
||||
endfunction
|
||||
|
||||
function! s:splittable(dict)
|
||||
@@ -631,21 +677,17 @@ else
|
||||
let s:launcher = function('s:xterm_launcher')
|
||||
endif
|
||||
|
||||
function! s:exit_handler(code, command, ...)
|
||||
if a:code == 130
|
||||
return 0
|
||||
elseif has('nvim') && a:code == 129
|
||||
" When deleting the terminal buffer while fzf is still running,
|
||||
" Nvim sends SIGHUP.
|
||||
return 0
|
||||
elseif a:code > 1
|
||||
function! s:exit_handler(dict, code, command, ...)
|
||||
if has_key(a:dict, 'exit')
|
||||
call a:dict.exit(a:code)
|
||||
endif
|
||||
if a:code == 2
|
||||
call s:error('Error running ' . a:command)
|
||||
if !empty(a:000)
|
||||
sleep
|
||||
endif
|
||||
return 0
|
||||
endif
|
||||
return 1
|
||||
return a:code
|
||||
endfunction
|
||||
|
||||
function! s:execute(dict, command, use_height, temps) abort
|
||||
@@ -682,21 +724,22 @@ function! s:execute(dict, command, use_height, temps) abort
|
||||
call jobstart(cmd, fzf)
|
||||
return []
|
||||
endif
|
||||
elseif has('win32unix') && $TERM !=# 'cygwin'
|
||||
elseif s:need_cmd_window
|
||||
let shellscript = s:fzf_tempname()
|
||||
call s:writefile([command], shellscript)
|
||||
let command = 'cmd.exe /C '.fzf#shellescape('set "TERM=" & start /WAIT sh -c '.shellscript)
|
||||
let command = 'start //WAIT sh -c '.shellscript
|
||||
let a:temps.shellscript = shellscript
|
||||
endif
|
||||
if a:use_height
|
||||
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s < /dev/tty 2> /dev/tty', &lines, command))
|
||||
let stdin = has_key(a:dict, 'source') ? '' : '< /dev/tty'
|
||||
call system(printf('tput cup %d > /dev/tty; tput cnorm > /dev/tty; %s %s 2> /dev/tty', &lines, command, stdin))
|
||||
else
|
||||
execute 'silent !'.command
|
||||
endif
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(exit_status, command) ? lines : []
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:execute_tmux(dict, command, temps) abort
|
||||
@@ -711,7 +754,7 @@ function! s:execute_tmux(dict, command, temps) abort
|
||||
let exit_status = v:shell_error
|
||||
redraw!
|
||||
let lines = s:collect(a:temps)
|
||||
return s:exit_handler(exit_status, command) ? lines : []
|
||||
return s:exit_handler(a:dict, exit_status, command) < 2 ? lines : []
|
||||
endfunction
|
||||
|
||||
function! s:calc_size(max, val, dict)
|
||||
@@ -732,7 +775,7 @@ function! s:calc_size(max, val, dict)
|
||||
return size
|
||||
endif
|
||||
let margin = match(opts, '--inline-info\|--info[^-]\{-}inline') > match(opts, '--no-inline-info\|--info[^-]\{-}\(default\|hidden\)') ? 1 : 2
|
||||
let margin += stridx(opts, '--border') > stridx(opts, '--no-border') ? 2 : 0
|
||||
let margin += match(opts, '--border\([^-]\|$\)') > match(opts, '--no-border\([^-]\|$\)') ? 2 : 0
|
||||
if stridx(opts, '--header') > stridx(opts, '--no-header')
|
||||
let margin += len(split(opts, "\n"))
|
||||
endif
|
||||
@@ -749,9 +792,9 @@ function! s:border_opt(window)
|
||||
endif
|
||||
|
||||
" Border style
|
||||
let style = tolower(get(a:window, 'border', 'rounded'))
|
||||
if !has_key(a:window, 'border') && !get(a:window, 'rounded', 1)
|
||||
let style = 'sharp'
|
||||
let style = tolower(get(a:window, 'border', ''))
|
||||
if !has_key(a:window, 'border') && has_key(a:window, 'rounded')
|
||||
let style = a:window.rounded ? 'rounded' : 'sharp'
|
||||
endif
|
||||
if style == 'none' || style == 'no'
|
||||
return ''
|
||||
@@ -759,7 +802,7 @@ function! s:border_opt(window)
|
||||
|
||||
" For --border styles, we need fzf 0.24.0 or above
|
||||
call fzf#exec('0.24.0')
|
||||
let opt = ' --border=' . style
|
||||
let opt = ' --border ' . style
|
||||
if has_key(a:window, 'highlight')
|
||||
let color = s:get_color('fg', a:window.highlight)
|
||||
if len(color)
|
||||
@@ -821,6 +864,17 @@ if exists(':tnoremap')
|
||||
tnoremap <silent> <Plug>(fzf-normal) <C-\><C-n>
|
||||
endif
|
||||
|
||||
let s:warned = 0
|
||||
function! s:handle_ambidouble(dict)
|
||||
if &ambiwidth == 'double'
|
||||
let a:dict.env = { 'RUNEWIDTH_EASTASIAN': '1' }
|
||||
elseif !s:warned && $RUNEWIDTH_EASTASIAN == '1' && &ambiwidth !=# 'double'
|
||||
call s:warn("$RUNEWIDTH_EASTASIAN is '1' but &ambiwidth is not 'double'")
|
||||
2sleep
|
||||
let s:warned = 1
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! s:execute_term(dict, command, temps) abort
|
||||
let winrest = winrestcmd()
|
||||
let pbuf = bufnr('')
|
||||
@@ -866,7 +920,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
|
||||
let lines = s:collect(self.temps)
|
||||
if !s:exit_handler(a:code, self.command, 1)
|
||||
if s:exit_handler(self.dict, a:code, self.command, 1) >= 2
|
||||
return
|
||||
endif
|
||||
|
||||
@@ -890,6 +944,7 @@ function! s:execute_term(dict, command, temps) abort
|
||||
endif
|
||||
let command .= s:term_marker
|
||||
if has('nvim')
|
||||
call s:handle_ambidouble(fzf)
|
||||
call termopen(command, fzf)
|
||||
else
|
||||
let term_opts = {'exit_cb': function(fzf.on_exit)}
|
||||
@@ -901,7 +956,8 @@ function! s:execute_term(dict, command, temps) abort
|
||||
else
|
||||
let term_opts.curwin = 1
|
||||
endif
|
||||
let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
||||
call s:handle_ambidouble(term_opts)
|
||||
keepjumps let fzf.buf = term_start([&shell, &shellcmdflag, command], term_opts)
|
||||
if is_popup && exists('#TerminalWinOpen')
|
||||
doautocmd <nomodeline> TerminalWinOpen
|
||||
endif
|
||||
@@ -967,16 +1023,31 @@ function! s:callback(dict, lines) abort
|
||||
endfunction
|
||||
|
||||
if has('nvim')
|
||||
function s:create_popup(hl, opts) abort
|
||||
function s:create_popup(opts) abort
|
||||
let buf = nvim_create_buf(v:false, v:true)
|
||||
let opts = extend({'relative': 'editor', 'style': 'minimal'}, a:opts)
|
||||
let win = nvim_open_win(buf, v:true, opts)
|
||||
call setwinvar(win, '&winhighlight', 'NormalFloat:'..a:hl)
|
||||
call setwinvar(win, '&colorcolumn', '')
|
||||
|
||||
" Colors
|
||||
try
|
||||
call setwinvar(win, '&winhighlight', 'Pmenu:,Normal:Normal')
|
||||
let rules = get(g:, 'fzf_colors', {})
|
||||
if has_key(rules, 'bg')
|
||||
let color = call('s:get_color', rules.bg)
|
||||
if len(color)
|
||||
let ns = nvim_create_namespace('fzf_popup')
|
||||
let hl = nvim_set_hl(ns, 'Normal',
|
||||
\ &termguicolors ? { 'bg': color } : { 'ctermbg': str2nr(color) })
|
||||
call nvim_win_set_hl_ns(win, ns)
|
||||
endif
|
||||
endif
|
||||
catch
|
||||
endtry
|
||||
return buf
|
||||
endfunction
|
||||
else
|
||||
function! s:create_popup(hl, opts) abort
|
||||
function! s:create_popup(opts) abort
|
||||
let s:popup_create = {buf -> popup_create(buf, #{
|
||||
\ line: a:opts.row,
|
||||
\ col: a:opts.col,
|
||||
@@ -1011,7 +1082,7 @@ function! s:popup(opts) abort
|
||||
let row += !has('nvim')
|
||||
let col += !has('nvim')
|
||||
|
||||
call s:create_popup('Normal', {
|
||||
call s:create_popup({
|
||||
\ 'row': row, 'col': col, 'width': width, 'height': height
|
||||
\ })
|
||||
endfunction
|
||||
@@ -1032,7 +1103,7 @@ endfunction
|
||||
|
||||
function! s:cmd(bang, ...) abort
|
||||
let args = copy(a:000)
|
||||
let opts = { 'options': ['--multi'] }
|
||||
let opts = { 'options': ['--multi', '--scheme', 'path'] }
|
||||
if len(args) && isdirectory(expand(args[-1]))
|
||||
let opts.dir = substitute(substitute(remove(args, -1), '\\\(["'']\)', '\1', 'g'), '[/\\]*$', '/', '')
|
||||
if s:is_win && !&shellslash
|
||||
|
||||
38
shell/common.sh
Normal file
38
shell/common.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
|
||||
__fzf_defaults() {
|
||||
# $1: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $2: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
# This function performs `exec awk "$@"` safely by working around awk
|
||||
# compatibility issues.
|
||||
#
|
||||
# To reduce an extra fork, this function performs "exec" so is expected to be
|
||||
# run as the last command in a subshell.
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
# Note: Solaris awk at /usr/bin/awk is meant for backward compatibility
|
||||
# with an ancient implementation of 1977 awk in the original UNIX. It
|
||||
# lacks many features of POSIX awk, so it is essentially useless in the
|
||||
# modern point of view. To use a standard-conforming version in Solaris,
|
||||
# one needs to explicitly use /usr/xpg4/bin/awk.
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk >/dev/null 2>&1; then
|
||||
# choose the faster mawk if: it's installed && build date >= 20230322 &&
|
||||
# version >= 1.3.4
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
# Note: macOS awk has a quirk that it stops processing at all when it sees
|
||||
# any data not following UTF-8 in the input stream when the current LC_CTYPE
|
||||
# specifies the UTF-8 encoding. To work around this quirk, one needs to
|
||||
# specify LC_ALL=C to change the current encoding to the plain one.
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
@@ -4,40 +4,63 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.bash
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
#
|
||||
# _fzf_compgen_path() {
|
||||
# echo "$1"
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
# }
|
||||
#
|
||||
# _fzf_compgen_dir() {
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
# -a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||
# }
|
||||
|
||||
###########################################################
|
||||
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk >/dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; }; then
|
||||
elif [[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; }; then
|
||||
shift
|
||||
fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- "$@"
|
||||
else
|
||||
@@ -55,72 +78,181 @@ __fzf_orig_completion() {
|
||||
cmd="${BASH_REMATCH[3]}"
|
||||
[[ "$f" = _fzf_* ]] && continue
|
||||
printf -v "_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}" "%s" "${comp} %s ${cmd} #${f}"
|
||||
if [[ "$l" = *" -o nospace "* ]] && [[ ! "$__fzf_nospace_commands" = *" $cmd "* ]]; then
|
||||
__fzf_nospace_commands="$__fzf_nospace_commands $cmd "
|
||||
if [[ "$l" = *" -o nospace "* ]] && [[ ! "${__fzf_nospace_commands-}" = *" $cmd "* ]]; then
|
||||
__fzf_nospace_commands="${__fzf_nospace_commands-} $cmd "
|
||||
fi
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# @param $1 cmd - Command name for which the original completion is searched
|
||||
# @var[out] REPLY - Original function name is returned
|
||||
__fzf_orig_completion_get_orig_func() {
|
||||
local cmd orig_var orig
|
||||
cmd=$1
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
REPLY="${orig##*#}"
|
||||
[[ $REPLY ]] && type "$REPLY" &> /dev/null
|
||||
}
|
||||
|
||||
# @param $1 cmd - Command name for which the original completion is searched
|
||||
# @param $2 func - Fzf's completion function to replace the original function
|
||||
# @var[out] REPLY - Completion setting is returned as a string to "eval"
|
||||
__fzf_orig_completion_instantiate() {
|
||||
local cmd func orig_var orig
|
||||
cmd=$1
|
||||
func=$2
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var-}"
|
||||
orig="${orig%#*}"
|
||||
[[ $orig == *' %s '* ]] || return 1
|
||||
printf -v REPLY "$orig" "$func"
|
||||
}
|
||||
|
||||
_fzf_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="
|
||||
-x --extended
|
||||
-e --exact
|
||||
--algo
|
||||
-i +i
|
||||
-n --nth
|
||||
--with-nth
|
||||
-d --delimiter
|
||||
+c --no-color
|
||||
+i --no-ignore-case
|
||||
+s --no-sort
|
||||
--tac
|
||||
--tiebreak
|
||||
-m --multi
|
||||
--no-mouse
|
||||
--bind
|
||||
--cycle
|
||||
--no-hscroll
|
||||
--jump-labels
|
||||
--height
|
||||
--literal
|
||||
--reverse
|
||||
--margin
|
||||
--inline-info
|
||||
--prompt
|
||||
--pointer
|
||||
--marker
|
||||
--header
|
||||
--header-lines
|
||||
+x --no-extended
|
||||
--ansi
|
||||
--tabstop
|
||||
--bash
|
||||
--bind
|
||||
--border
|
||||
--border-label
|
||||
--border-label-pos
|
||||
--color
|
||||
--no-bold
|
||||
--cycle
|
||||
--disabled
|
||||
--ellipsis
|
||||
--expect
|
||||
--filepath-word
|
||||
--fish
|
||||
--header
|
||||
--header-first
|
||||
--header-lines
|
||||
--height
|
||||
--highlight-line
|
||||
--history
|
||||
--history-size
|
||||
--hscroll-off
|
||||
--info
|
||||
--jump-labels
|
||||
--keep-right
|
||||
--layout
|
||||
--listen
|
||||
--listen-unsafe
|
||||
--literal
|
||||
--man
|
||||
--margin
|
||||
--marker
|
||||
--min-height
|
||||
--no-bold
|
||||
--no-clear
|
||||
--no-hscroll
|
||||
--no-mouse
|
||||
--no-scrollbar
|
||||
--no-separator
|
||||
--no-unicode
|
||||
--padding
|
||||
--pointer
|
||||
--preview
|
||||
--preview-label
|
||||
--preview-label-pos
|
||||
--preview-window
|
||||
-q --query
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
-f --filter
|
||||
--print-query
|
||||
--expect
|
||||
--sync"
|
||||
--print0
|
||||
--prompt
|
||||
--read0
|
||||
--reverse
|
||||
--scheme
|
||||
--scroll-off
|
||||
--separator
|
||||
--sync
|
||||
--tabstop
|
||||
--tac
|
||||
--tiebreak
|
||||
--tmux
|
||||
--track
|
||||
--version
|
||||
--with-nth
|
||||
--with-shell
|
||||
--wrap
|
||||
--zsh
|
||||
-0 --exit-0
|
||||
-1 --select-1
|
||||
-d --delimiter
|
||||
-e --exact
|
||||
-f --filter
|
||||
-h --help
|
||||
-i --ignore-case
|
||||
-m --multi
|
||||
-n --nth
|
||||
-q --query
|
||||
--"
|
||||
|
||||
case "${prev}" in
|
||||
--scheme)
|
||||
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
|
||||
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--history)
|
||||
COMPREPLY=()
|
||||
--layout)
|
||||
COMPREPLY=( $(compgen -W "default reverse reverse-list" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--info)
|
||||
COMPREPLY=( $(compgen -W "default right hidden inline inline-right" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--preview-window)
|
||||
COMPREPLY=( $(compgen -W "
|
||||
default
|
||||
hidden
|
||||
nohidden
|
||||
wrap
|
||||
nowrap
|
||||
cycle
|
||||
nocycle
|
||||
up top
|
||||
down bottom
|
||||
left
|
||||
right
|
||||
rounded border border-rounded
|
||||
sharp border-sharp
|
||||
border-bold
|
||||
border-block
|
||||
border-thinblock
|
||||
border-double
|
||||
noborder border-none
|
||||
border-horizontal
|
||||
border-vertical
|
||||
border-up border-top
|
||||
border-down border-bottom
|
||||
border-left
|
||||
border-right
|
||||
follow
|
||||
nofollow" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--border)
|
||||
COMPREPLY=( $(compgen -W "rounded sharp bold block thinblock double horizontal vertical top bottom left right none" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--border-label-pos|--preview-label-pos)
|
||||
COMPREPLY=( $(compgen -W "center bottom top" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
@@ -134,41 +266,51 @@ _fzf_opts_completion() {
|
||||
}
|
||||
|
||||
_fzf_handle_dynamic_completion() {
|
||||
local cmd orig_var orig ret orig_cmd orig_complete
|
||||
local cmd ret REPLY orig_cmd orig_complete
|
||||
cmd="$1"
|
||||
shift
|
||||
orig_cmd="$1"
|
||||
orig_var="_fzf_orig_completion_$cmd"
|
||||
orig="${!orig_var##*#}"
|
||||
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
|
||||
$orig "$@"
|
||||
elif [[ -n "$_fzf_completion_loader" ]]; then
|
||||
if __fzf_orig_completion_get_orig_func "$cmd"; then
|
||||
"$REPLY" "$@"
|
||||
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_completion_loader "$@"
|
||||
$_fzf_completion_loader "$@"
|
||||
ret=$?
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [[ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]]; then
|
||||
__fzf_orig_completion < <(complete -p "$orig_cmd" 2> /dev/null)
|
||||
if [[ "$__fzf_nospace_commands" = *" $orig_cmd "* ]]; then
|
||||
__fzf_orig_completion_get_orig_func "$cmd" || ret=1
|
||||
|
||||
# Update orig_complete by _fzf_orig_completion entry
|
||||
[[ $orig_complete =~ ' -F '(_fzf_[^ ]+)' ' ]] &&
|
||||
__fzf_orig_completion_instantiate "$cmd" "${BASH_REMATCH[1]}" &&
|
||||
orig_complete=$REPLY
|
||||
|
||||
if [[ "${__fzf_nospace_commands-}" = *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
fi
|
||||
fi
|
||||
[[ $ret -eq 0 ]] && return 124
|
||||
return $ret
|
||||
fi
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local cur base dir leftover matches trigger cmd
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
if [[ $cmd == \\* ]]; then
|
||||
cmd="${cmd:1}"
|
||||
fi
|
||||
COMPREPLY=()
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
[[ $COMP_CWORD -ge 0 ]] && cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base"
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
|
||||
dir=
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
||||
@@ -176,20 +318,37 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[[ -z "$dir" ]] && dir='.'
|
||||
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $2" __fzf_comprun "$4" -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-} $2")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if declare -F "$1" > /dev/null; then
|
||||
eval "$1 $(printf %q "$dir")" | __fzf_comprun "$4" -q "$leftover"
|
||||
else
|
||||
if [[ $1 =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
eval "rest=(${FZF_COMPLETION_DIR_OPTS-})"
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
eval "rest=(${FZF_COMPLETION_PATH_OPTS-})"
|
||||
fi
|
||||
__fzf_comprun "$4" -q "$leftover" --walker "$walker" --walker-root="$dir" "${rest[@]}"
|
||||
fi | while read -r item; do
|
||||
printf "%q " "${item%$3}$3"
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "$__fzf_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [[ -n "$matches" ]]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
fi
|
||||
# To redraw line after fzf closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
dir=$(command dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
@@ -223,22 +382,26 @@ _fzf_complete() {
|
||||
fi
|
||||
|
||||
local cur selected trigger cmd post
|
||||
post="$(caller 0 | awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
||||
post="$(caller 0 | __fzf_exec_awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
cmd="${COMP_WORDS[0]}"
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "${rest[0]}" "${args[@]}" -q "$cur" | eval "$post" | command tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [[ -n "$selected" ]]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
@@ -260,81 +423,218 @@ _fzf_dir_completion() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
local trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ -z "$cur" ]]; then
|
||||
COMP_WORDS[$COMP_CWORD]=$trigger
|
||||
elif [[ "$cur" != *"$trigger" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_fzf_proc_completion "$@"
|
||||
}
|
||||
|
||||
_fzf_proc_completion() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_proc_completion_post() {
|
||||
awk '{print $2}'
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
# To use custom hostname lists, override __fzf_list_hosts.
|
||||
# The function is expected to print hostnames, one per line as well as in the
|
||||
# desired sorting and with any duplicates removed, to standard output.
|
||||
#
|
||||
# e.g.
|
||||
# # Use bash-completions’s _known_hosts_real() for getting the list of hosts
|
||||
# __fzf_list_hosts() {
|
||||
# # Set the local attribute for any non-local variable that is set by _known_hosts_real()
|
||||
# local COMPREPLY=()
|
||||
# _known_hosts_real ''
|
||||
# printf '%s\n' "${COMPREPLY[@]}" | command sort -u --version-sort
|
||||
# }
|
||||
if ! declare -F __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "set -f" and "GLOBIGNORE", which disable the pathname expansion
|
||||
# totally or partially. We need to unset "dotglob" and "nocaseglob" to
|
||||
# avoid matching unwanted files. We need to unset "failglob" to avoid
|
||||
# outputting the error messages to the terminal when no matching is
|
||||
# found. We need to set "nullglob" to avoid attempting to read the
|
||||
# literal filename '~/.ssh/config.d/*' when no matching is found.
|
||||
set +f
|
||||
GLOBIGNORE=
|
||||
shopt -u dotglob nocaseglob failglob
|
||||
shopt -s nullglob
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
_fzf_host_completion() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||
}
|
||||
|
||||
# Values for $1 $2 $3 are described here
|
||||
# https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
|
||||
# > the first argument ($1) is the name of the command whose arguments are being completed,
|
||||
# > the second argument ($2) is the word being completed,
|
||||
# > and the third argument ($3) is the word preceding the word being completed on the current command line.
|
||||
_fzf_complete_ssh() {
|
||||
case $3 in
|
||||
-i|-F|-E)
|
||||
_fzf_path_completion "$@"
|
||||
;;
|
||||
*)
|
||||
local user=
|
||||
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_fzf_var_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_alias_completion() {
|
||||
_fzf_complete -m -- "$@" < <(
|
||||
alias | sed 's/=.*//' | sed 's/.* //'
|
||||
alias | command sed -En 's|^alias ([^=]+).*|\1|p'
|
||||
)
|
||||
}
|
||||
|
||||
# fzf options
|
||||
complete -o default -F _fzf_opts_completion fzf
|
||||
# fzf-tmux is a thin fzf wrapper that has only a few more options than fzf
|
||||
# itself. As a quick improvement we take fzf's completion. Adding the few extra
|
||||
# fzf-tmux specific options (like `-w WIDTH`) are left as a future patch.
|
||||
complete -o default -F _fzf_opts_completion fzf-tmux
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
||||
# Default path completion
|
||||
__fzf_default_completion() {
|
||||
__fzf_generic_path_completion _fzf_compgen_path "-m" "" "$@"
|
||||
|
||||
# Dynamic completion loader has updated the completion for the command
|
||||
if [[ $? -eq 124 ]]; then
|
||||
# We trigger _fzf_setup_completion so that fuzzy completion for the command
|
||||
# still works. However, loader can update the completion for multiple
|
||||
# commands at once, and fuzzy completion will no longer work for those
|
||||
# other commands. e.g. pytest -> py.test, pytest-2, pytest-3, etc
|
||||
_fzf_setup_completion path "$1"
|
||||
return 124
|
||||
fi
|
||||
}
|
||||
|
||||
# Set fuzzy path completion as the default completion for all commands.
|
||||
# We can't set up default completion,
|
||||
# 1. if it's already set up by another script
|
||||
# 2. or if the current version of bash doesn't support -D option
|
||||
complete | command grep -q __fzf_default_completion ||
|
||||
complete | command grep -- '-D$' | command grep -qv _comp_complete_load ||
|
||||
complete -D -F __fzf_default_completion -o default -o bashdefault 2> /dev/null
|
||||
|
||||
d_cmds="${FZF_COMPLETION_DIR_COMMANDS-cd pushd rmdir}"
|
||||
|
||||
# NOTE: $FZF_COMPLETION_PATH_COMMANDS and $FZF_COMPLETION_VAR_COMMANDS are
|
||||
# undocumented and subject to change in the future.
|
||||
#
|
||||
# NOTE: Although we have default completion, we still need to set up completion
|
||||
# for each command in case they already have completion set up by another script.
|
||||
a_cmds="${FZF_COMPLETION_PATH_COMMANDS-"
|
||||
awk bat cat code diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||
find git grep gunzip gzip hg jar
|
||||
ln ls mv open rm rsync scp
|
||||
svn tar unzip zip"
|
||||
svn tar unzip zip"}"
|
||||
v_cmds="${FZF_COMPLETION_VAR_COMMANDS-export unset printenv}"
|
||||
|
||||
# Preserve existing completion
|
||||
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds 2> /dev/null)
|
||||
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds $v_cmds unalias kill ssh 2> /dev/null)
|
||||
|
||||
if type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=1
|
||||
if type _comp_load > /dev/null 2>&1; then
|
||||
# _comp_load was added in bash-completion 2.12 to replace _completion_loader.
|
||||
# We use it without -D option so that it does not use _comp_complete_minimal as the fallback.
|
||||
_fzf_completion_loader=_comp_load
|
||||
elif type __load_completion > /dev/null 2>&1; then
|
||||
# In bash-completion 2.11, _completion_loader internally calls __load_completion
|
||||
# and if it returns a non-zero status, it sets the default 'minimal' completion.
|
||||
_fzf_completion_loader=__load_completion
|
||||
elif type _completion_loader > /dev/null 2>&1; then
|
||||
_fzf_completion_loader=_completion_loader
|
||||
fi
|
||||
|
||||
__fzf_defc() {
|
||||
local cmd func opts orig_var orig def
|
||||
local cmd func opts REPLY
|
||||
cmd="$1"
|
||||
func="$2"
|
||||
opts="$3"
|
||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var}"
|
||||
if [[ -n "$orig" ]]; then
|
||||
printf -v def "$orig" "$func"
|
||||
eval "$def"
|
||||
if __fzf_orig_completion_instantiate "$cmd" "$func"; then
|
||||
eval "$REPLY"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
eval "complete -F \"$func\" $opts \"$cmd\""
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -345,13 +645,24 @@ done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||
__fzf_defc "$cmd" _fzf_dir_completion "-o bashdefault -o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# Kill completion (supports empty completion trigger)
|
||||
complete -F _fzf_complete_kill -o default -o bashdefault kill
|
||||
# Variables
|
||||
for cmd in $v_cmds; do
|
||||
__fzf_defc "$cmd" _fzf_var_completion "-o default -o nospace -v"
|
||||
done
|
||||
|
||||
unset cmd d_cmds a_cmds
|
||||
# Aliases
|
||||
__fzf_defc unalias _fzf_alias_completion "-a"
|
||||
|
||||
# Processes
|
||||
__fzf_defc kill _fzf_proc_completion "-o default -o bashdefault"
|
||||
|
||||
# ssh
|
||||
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
|
||||
|
||||
unset cmd d_cmds a_cmds v_cmds
|
||||
|
||||
_fzf_setup_completion() {
|
||||
local kind fn cmd
|
||||
@@ -373,9 +684,4 @@ _fzf_setup_completion() {
|
||||
done
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts
|
||||
_fzf_setup_completion 'var' export unset
|
||||
_fzf_setup_completion 'alias' unalias
|
||||
_fzf_setup_completion 'host' ssh telnet
|
||||
|
||||
fi
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.zsh
|
||||
#
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: '-d 40%')
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_TMUX (default: 0)
|
||||
# - $FZF_TMUX_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_PATH_OPTS (default: empty)
|
||||
# - $FZF_COMPLETION_DIR_OPTS (default: empty)
|
||||
|
||||
|
||||
# Both branches of the following `if` do the same thing -- define
|
||||
# __fzf_completion_options such that `eval $__fzf_completion_options` sets
|
||||
@@ -67,41 +70,64 @@ fi
|
||||
# control. There are several others that could wreck havoc if they are set
|
||||
# to values we don't expect. With the following `emulate` command we
|
||||
# sidestep this issue entirely.
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||
|
||||
# This brace is the start of try-always block. The `always` part is like
|
||||
# `finally` in lesser languages. We use it to *always* restore user options.
|
||||
{
|
||||
|
||||
# Bail out if not interactive shell.
|
||||
[[ -o interactive ]] || return 0
|
||||
# The 'emulate' command should not be placed inside the interactive if check;
|
||||
# placing it there fails to disable alias expansion. See #3731.
|
||||
if [[ -o interactive ]]; then
|
||||
|
||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||
_fzf_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||
_fzf_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
#
|
||||
# _fzf_compgen_path() {
|
||||
# echo "$1"
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
# }
|
||||
#
|
||||
# _fzf_compgen_dir() {
|
||||
# command find -L "$1" \
|
||||
# -name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
# -a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
# }
|
||||
|
||||
###########################################################
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk >/dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_comprun() {
|
||||
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
|
||||
_fzf_comprun "$@"
|
||||
elif [ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; }; then
|
||||
elif [ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; }; then
|
||||
shift
|
||||
if [ -n "$FZF_TMUX_OPTS" ]; then
|
||||
if [ -n "${FZF_TMUX_OPTS-}" ]; then
|
||||
fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@"
|
||||
else
|
||||
fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@"
|
||||
@@ -112,32 +138,28 @@ __fzf_comprun() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
||||
# Extract the name of the command. e.g. ls; foo=1 ssh **<tab>
|
||||
__fzf_extract_command() {
|
||||
local token tokens
|
||||
tokens=(${(z)1})
|
||||
for token in $tokens; do
|
||||
token=${(Q)token}
|
||||
if [[ "$token" =~ [[:alnum:]] && ! "$token" =~ "=" ]]; then
|
||||
echo "$token"
|
||||
return
|
||||
fi
|
||||
done
|
||||
echo "${tokens[1]}"
|
||||
# Control completion with the "compstate" parameter, insert and list nothing
|
||||
compstate[insert]=
|
||||
compstate[list]=
|
||||
cmd_word="${(Q)words[1]}"
|
||||
}
|
||||
|
||||
__fzf_generic_path_completion() {
|
||||
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
|
||||
local base lbuf compgen fzf_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
compgen=$3
|
||||
fzf_opts=$4
|
||||
suffix=$5
|
||||
tail=$6
|
||||
|
||||
setopt localoptions nonomatch
|
||||
eval "base=$base"
|
||||
if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
eval "base=$base" 2> /dev/null || return
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while [ 1 ]; do
|
||||
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||
@@ -145,9 +167,26 @@ __fzf_generic_path_completion() {
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
|
||||
echo -n "${(q)item}$suffix "
|
||||
done)
|
||||
matches=$(
|
||||
export FZF_DEFAULT_OPTS
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --scheme=path" "${FZF_COMPLETION_OPTS-}")
|
||||
unset FZF_DEFAULT_COMMAND FZF_DEFAULT_OPTS_FILE
|
||||
if declare -f "$compgen" > /dev/null; then
|
||||
eval "$compgen $(printf %q "$dir")" | __fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover"
|
||||
else
|
||||
if [[ $compgen =~ dir ]]; then
|
||||
walker=dir,follow
|
||||
rest=${FZF_COMPLETION_DIR_OPTS-}
|
||||
else
|
||||
walker=file,dir,follow,hidden
|
||||
rest=${FZF_COMPLETION_PATH_OPTS-}
|
||||
fi
|
||||
__fzf_comprun "$cmd_word" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" --walker "$walker" --walker-root="$dir" ${(Q)${(Z+n+)rest}} < /dev/tty
|
||||
fi | while read -r item; do
|
||||
item="${item%$suffix}$suffix"
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
)
|
||||
matches=${matches% }
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
@@ -170,11 +209,11 @@ _fzf_dir_completion() {
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_fzf_feed_fifo() (
|
||||
_fzf_feed_fifo() {
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
cat <&0 > "$1" &|
|
||||
}
|
||||
|
||||
_fzf_complete() {
|
||||
setopt localoptions ksh_arrays
|
||||
@@ -183,7 +222,7 @@ _fzf_complete() {
|
||||
args=("$@")
|
||||
sep=
|
||||
for i in {0..${#args[@]}}; do
|
||||
if [[ "${args[$i]}" = -- ]]; then
|
||||
if [[ "${args[$i]-}" = -- ]]; then
|
||||
sep=$i
|
||||
break
|
||||
fi
|
||||
@@ -199,36 +238,94 @@ _fzf_complete() {
|
||||
rest=("$@")
|
||||
fi
|
||||
|
||||
local fifo lbuf cmd matches post
|
||||
local fifo lbuf matches post
|
||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||
lbuf=${rest[0]}
|
||||
cmd=$(__fzf_extract_command "$lbuf")
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
_fzf_feed_fifo "$fifo"
|
||||
matches=$(FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS $str_arg" __fzf_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
matches=$(
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse" "${FZF_COMPLETION_OPTS-} $str_arg") \
|
||||
FZF_DEFAULT_OPTS_FILE='' \
|
||||
__fzf_comprun "$cmd_word" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
command rm -f "$fifo"
|
||||
}
|
||||
|
||||
# To use custom hostname lists, override __fzf_list_hosts.
|
||||
# The function is expected to print hostnames, one per line as well as in the
|
||||
# desired sorting and with any duplicates removed, to standard output.
|
||||
if ! declare -f __fzf_list_hosts > /dev/null; then
|
||||
__fzf_list_hosts() {
|
||||
command sort -u \
|
||||
<(
|
||||
# Note: To make the pathname expansion of "~/.ssh/config.d/*" work
|
||||
# properly, we need to adjust the related shell options. We need to
|
||||
# unset "NO_GLOB" (or reset "GLOB"), which disable the pathname
|
||||
# expansion totally. We need to unset "DOT_GLOB" and set "CASE_GLOB"
|
||||
# to avoid matching unwanted files. We need to set "NULL_GLOB" to
|
||||
# avoid attempting to read the literal filename '~/.ssh/config.d/*'
|
||||
# when no matching is found.
|
||||
setopt GLOB NO_DOT_GLOB CASE_GLOB NO_NOMATCH NULL_GLOB
|
||||
|
||||
__fzf_exec_awk '
|
||||
# Note: mawk <= 1.3.3-20090705 does not support the POSIX brackets of
|
||||
# the form [[:blank:]], and Ubuntu 18.04 LTS still uses this
|
||||
# 16-year-old mawk unfortunately. We need to use [ \t] instead.
|
||||
match(tolower($0), /^[ \t]*host(name)?[ \t]*[ \t=]/) {
|
||||
$0 = substr($0, RLENGTH + 1) # Remove "Host(name)?=?"
|
||||
sub(/#.*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
if ($i !~ /[*?%]/)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk -F ',' '
|
||||
match($0, /^[][a-zA-Z0-9.,:-]+/) {
|
||||
$0 = substr($0, 1, RLENGTH)
|
||||
gsub(/[][]|:[^,]*/, "")
|
||||
for (i = 1; i <= NF; i++)
|
||||
print $i
|
||||
}
|
||||
' ~/.ssh/known_hosts 2> /dev/null
|
||||
) \
|
||||
<(
|
||||
__fzf_exec_awk '
|
||||
{
|
||||
sub(/#.*/, "")
|
||||
for (i = 2; i <= NF; i++)
|
||||
if ($i != "0.0.0.0")
|
||||
print $i
|
||||
}
|
||||
' /etc/hosts 2> /dev/null
|
||||
)
|
||||
}
|
||||
fi
|
||||
|
||||
_fzf_complete_telnet() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||
}
|
||||
|
||||
# The first and the only argument is the LBUFFER without the current word that contains the trigger.
|
||||
# The current word without the trigger is in the $prefix variable passed from the caller.
|
||||
_fzf_complete_ssh() {
|
||||
_fzf_complete +m -- "$@" < <(
|
||||
setopt localoptions nonomatch
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
local -a tokens
|
||||
tokens=(${(z)1})
|
||||
case ${tokens[-1]} in
|
||||
-i|-F|-E)
|
||||
_fzf_path_completion "$prefix" "$1"
|
||||
;;
|
||||
*)
|
||||
local user
|
||||
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | __fzf_exec_awk -v user="$user" '{print user $0}')
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
_fzf_complete_export() {
|
||||
@@ -250,17 +347,45 @@ _fzf_complete_unalias() {
|
||||
}
|
||||
|
||||
_fzf_complete_kill() {
|
||||
_fzf_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
local transformer
|
||||
transformer='
|
||||
if [[ $FZF_KEY =~ ctrl|alt|shift ]] && [[ -n $FZF_NTH ]]; then
|
||||
nths=( ${FZF_NTH//,/ } )
|
||||
new_nths=()
|
||||
found=0
|
||||
for nth in ${nths[@]}; do
|
||||
if [[ $nth = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
found=1
|
||||
else
|
||||
new_nths+=($nth)
|
||||
fi
|
||||
done
|
||||
[[ $found = 0 ]] && new_nths+=($FZF_CLICK_HEADER_NTH)
|
||||
new_nths=${new_nths[*]}
|
||||
new_nths=${new_nths// /,}
|
||||
echo "change-nth($new_nths)+change-prompt($new_nths> )"
|
||||
else
|
||||
if [[ $FZF_NTH = $FZF_CLICK_HEADER_NTH ]]; then
|
||||
echo "change-nth()+change-prompt(> )"
|
||||
else
|
||||
echo "change-nth($FZF_CLICK_HEADER_NTH)+change-prompt($FZF_CLICK_HEADER_WORD> )"
|
||||
fi
|
||||
fi
|
||||
'
|
||||
_fzf_complete -m --header-lines=1 --no-preview --wrap --color fg:dim,nth:regular \
|
||||
--bind "click-header:transform:$transformer" -- "$@" < <(
|
||||
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||
command ps -eo user,pid,ppid,time,args 2> /dev/null || # For BusyBox
|
||||
command ps --everyone --full --windows # For cygwin
|
||||
)
|
||||
}
|
||||
|
||||
_fzf_complete_kill_post() {
|
||||
awk '{print $2}'
|
||||
__fzf_exec_awk '{print $2}'
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
@@ -271,38 +396,54 @@ fzf-completion() {
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=$(__fzf_extract_command "$LBUFFER")
|
||||
|
||||
# Explicitly allow for empty 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
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
|
||||
tokens[-2]="${tokens[-2]}${tokens[-1]}"
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
||||
tokens[-2]="${tokens[-2]-}${tokens[-1]}"
|
||||
tokens=(${tokens[0,-2]})
|
||||
fi
|
||||
|
||||
lbuf=$LBUFFER
|
||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||
# Kill completion (do not require trigger sequence)
|
||||
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
||||
tail=$trigger
|
||||
tokens+=$trigger
|
||||
lbuf="$lbuf$trigger"
|
||||
fi
|
||||
|
||||
# Trigger sequence given
|
||||
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}}
|
||||
if [[ $prefix = *'$('* ]] || [[ $prefix = *'<('* ]] || [[ $prefix = *'>('* ]] || [[ $prefix = *':='* ]] || [[ $prefix = *'`'* ]]; then
|
||||
return
|
||||
fi
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
||||
if eval "noglob type _fzf_complete_${cmd_word} >/dev/null"; then
|
||||
prefix="$prefix" eval _fzf_complete_${cmd_word} ${(q)lbuf}
|
||||
zle reset-prompt
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
elif [ ${d_cmds[(i)$cmd_word]} -le ${#d_cmds} ]; then
|
||||
_fzf_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_fzf_path_completion "$prefix" "$lbuf"
|
||||
@@ -319,8 +460,10 @@ fzf-completion() {
|
||||
unset binding
|
||||
}
|
||||
|
||||
# Normal widget
|
||||
zle -N fzf-completion
|
||||
bindkey '^I' fzf-completion
|
||||
fi
|
||||
|
||||
} always {
|
||||
# Restore the original options.
|
||||
|
||||
@@ -11,53 +11,109 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
__fzf_select__() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk >/dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
__fzf_select__() {
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" |
|
||||
while read -r item; do
|
||||
printf '%q ' "$item" # escape special chars
|
||||
done
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[[ -n "$TMUX_PANE" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "$FZF_TMUX_OPTS" ]]; } &&
|
||||
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
local selected="$(__fzf_select__)"
|
||||
local selected="$(__fzf_select__ "$@")"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||
}
|
||||
|
||||
__fzf_cd__() {
|
||||
local cmd dir
|
||||
cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd -- %q' "$dir"
|
||||
local dir
|
||||
dir=$(
|
||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd)
|
||||
) && printf 'builtin cd -- %q' "$(builtin unset CDPATH && builtin cd -- "$dir" && builtin pwd)"
|
||||
}
|
||||
|
||||
__fzf_history__() {
|
||||
local output
|
||||
output=$(
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m --read0" $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
if command -v perl > /dev/null; then
|
||||
__fzf_history__() {
|
||||
local output script
|
||||
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; s/\n/\n\t/gm; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=$(command perl -pe 's/^\d*\t//' <<< "$output")
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
else # awk - fallback for POSIX systems
|
||||
__fzf_history__() {
|
||||
local output script
|
||||
[[ $(HISTTIMEFORMAT='' builtin history 1) =~ [[:digit:]]+ ]] # how many history entries
|
||||
script='function P(b) { ++n; sub(/^[ *]/, "", b); if (!seen[b]++) { printf "%d\t%s%c", '$((BASH_REMATCH + 1))' - n, b, 0 } }
|
||||
NR==1 { b = substr($0, 2); next }
|
||||
/^\t/ { P(b); b = substr($0, 2); next }
|
||||
{ b = b RS $0 }
|
||||
END { if (NR) P(b) }'
|
||||
output=$(
|
||||
set +o pipefail
|
||||
builtin fc -lnr -2147483648 2> /dev/null | # ( $'\t '<lines>$'\n' )* ; <lines> ::= [^\n]* ( $'\n'<lines> )*
|
||||
__fzf_exec_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '"$'\t'"↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} +m --read0") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [[ -z "$READLINE_POINT" ]]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
fi
|
||||
|
||||
# Required to refresh the prompt after fzf
|
||||
bind -m emacs-standard '"\er": redraw-current-line'
|
||||
@@ -68,19 +124,23 @@ bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||
|
||||
if (( BASH_VERSINFO[0] < 4 )); then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__fzf_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__fzf_history__)"\e\C-e\er"'
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u`__fzf_history__`\e\C-e\er"'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||
@@ -89,8 +149,10 @@ else
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__fzf_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d\C-y\ey\C-_"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
@@ -11,162 +11,220 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
||||
# $dir itself, even if hidden.
|
||||
test -n "$FZF_CTRL_T_COMMAND"; or set -l FZF_CTRL_T_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
commandline -f repaint
|
||||
return
|
||||
else
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
end
|
||||
for i in $result
|
||||
commandline -it -- $prefix
|
||||
commandline -it -- (string escape $i)
|
||||
commandline -it -- ' '
|
||||
end
|
||||
commandline -f repaint
|
||||
# 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-history-widget -d "Show command history"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS +m"
|
||||
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
|
||||
# history's -z flag is needed for multi-line support.
|
||||
# history's -z flag was added in fish 2.4.0, so don't use it for versions
|
||||
# before 2.4.0.
|
||||
if [ "$FISH_MAJOR" -gt 2 -o \( "$FISH_MAJOR" -eq 2 -a "$FISH_MINOR" -ge 4 \) ];
|
||||
history -z | eval (__fzfcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||
and commandline -- $result
|
||||
end
|
||||
end
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -l dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
test -n "$FZF_ALT_C_COMMAND"; or set -l FZF_ALT_C_COMMAND "
|
||||
command find -L \$dir -mindepth 1 \\( -path \$dir'*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | sed 's@^\./@@'"
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
cd -- $result
|
||||
|
||||
# Remove last token from commandline.
|
||||
commandline -t ""
|
||||
commandline -it -- $prefix
|
||||
end
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
function __fzf_defaults
|
||||
# $argv[1]: Prepend to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
# $argv[2..]: Append to FZF_DEFAULT_OPTS_FILE and FZF_DEFAULT_OPTS
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
|
||||
string join ' ' -- \
|
||||
"--height $FZF_TMUX_HEIGHT --min-height=20+ --bind=ctrl-z:ignore" $argv[1] \
|
||||
(test -r "$FZF_DEFAULT_OPTS_FILE"; and string join -- ' ' <$FZF_DEFAULT_OPTS_FILE) \
|
||||
$FZF_DEFAULT_OPTS $argv[2..-1]
|
||||
end
|
||||
|
||||
function __fzfcmd
|
||||
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||
if [ -n "$FZF_TMUX_OPTS" ]
|
||||
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 [ $FZF_TMUX -eq 1 ]
|
||||
else if test "$FZF_TMUX" = "1"
|
||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||
else
|
||||
echo "fzf"
|
||||
end
|
||||
end
|
||||
|
||||
bind \ct fzf-file-widget
|
||||
bind \cr fzf-history-widget
|
||||
bind \ec fzf-cd-widget
|
||||
|
||||
if bind -M insert > /dev/null 2>&1
|
||||
bind -M insert \ct fzf-file-widget
|
||||
bind -M insert \cr fzf-history-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||
set -l commandline (commandline -t)
|
||||
set -l fzf_query ''
|
||||
set -l prefix ''
|
||||
set -l dir '.'
|
||||
|
||||
# strip -option= from token if present
|
||||
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||
set commandline (string replace -- "$prefix" '' $commandline)
|
||||
# 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]
|
||||
|
||||
# eval is used to do shell expansion on paths
|
||||
eval set commandline $commandline
|
||||
# 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
|
||||
|
||||
if [ -z $commandline ]
|
||||
# Default to current directory with no --query
|
||||
set dir '.'
|
||||
set fzf_query ''
|
||||
# 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
|
||||
set dir (__fzf_get_dir $commandline)
|
||||
# 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 [ "$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
|
||||
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
|
||||
# Also remove trailing slash after dir, to "split" input properly
|
||||
set fzf_query (string replace -r "^$dir/?" -- '' "$commandline")
|
||||
# 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
|
||||
|
||||
echo $dir
|
||||
echo $fzf_query
|
||||
echo $prefix
|
||||
string escape -n -- "$dir" "$fzf_query" "$prefix"
|
||||
end
|
||||
|
||||
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||
set dir $argv
|
||||
# Store current token in $dir as root for the 'find' command
|
||||
function fzf-file-widget -d "List files and folders"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||
if [ (string length -- $dir) -gt 1 ]
|
||||
set dir (string replace -r '/*$' -- '' $dir)
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=file,dir,follow,hidden --scheme=path" \
|
||||
"$FZF_CTRL_T_OPTS --multi --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_CTRL_T_COMMAND"
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
|
||||
set -l result (eval (__fzfcmd) --walker-root=$dir --query=$fzf_query | string split0)
|
||||
and commandline -rt -- (string join -- ' ' $prefix(string escape -- $result))' '
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-history-widget -d "Show command history"
|
||||
set -l -- command_line (commandline)
|
||||
set -l -- current_line (commandline -L)
|
||||
set -l -- total_lines (count $command_line)
|
||||
set -l -- fzf_query (string escape -- $command_line[$current_line])
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults '' \
|
||||
'--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)
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND
|
||||
|
||||
if type -q perl
|
||||
set -a FZF_DEFAULT_OPTS '--tac'
|
||||
set FZF_DEFAULT_COMMAND 'builtin history -z --reverse | command perl -0 -pe \'s/^/$.\t/g; s/\n/\n\t/gm\''
|
||||
else
|
||||
set FZF_DEFAULT_COMMAND \
|
||||
'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
|
||||
|
||||
# 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")
|
||||
# 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
|
||||
|
||||
echo $dir
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
function fzf-cd-widget -d "Change directory"
|
||||
set -l commandline (__fzf_parse_commandline)
|
||||
set -lx dir $commandline[1]
|
||||
set -l fzf_query $commandline[2]
|
||||
set -l prefix $commandline[3]
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS (__fzf_defaults \
|
||||
"--reverse --walker=dir,follow,hidden --scheme=path" \
|
||||
"$FZF_ALT_C_OPTS --no-multi --print0")
|
||||
|
||||
set -lx FZF_DEFAULT_OPTS_FILE
|
||||
set -lx FZF_DEFAULT_COMMAND "$FZF_ALT_C_COMMAND"
|
||||
|
||||
if set -l result (eval (__fzfcmd) --query=$fzf_query --walker-root=$dir | string split0)
|
||||
cd -- $result
|
||||
commandline -rt -- $prefix
|
||||
end
|
||||
|
||||
commandline -f repaint
|
||||
end
|
||||
|
||||
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"
|
||||
bind \ct fzf-file-widget
|
||||
bind -M insert \ct fzf-file-widget
|
||||
end
|
||||
|
||||
if not set -q FZF_ALT_C_COMMAND; or test -n "$FZF_ALT_C_COMMAND"
|
||||
bind \ec fzf-cd-widget
|
||||
bind -M insert \ec fzf-cd-widget
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
# - $FZF_ALT_C_COMMAND
|
||||
# - $FZF_ALT_C_OPTS
|
||||
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
@@ -32,22 +33,45 @@ else
|
||||
}
|
||||
fi
|
||||
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||
|
||||
{
|
||||
if [[ -o interactive ]]; then
|
||||
|
||||
[[ -o interactive ]] || return 0
|
||||
#----BEGIN INCLUDE common.sh
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
|
||||
__fzf_defaults() {
|
||||
printf '%s\n' "--height ${FZF_TMUX_HEIGHT:-40%} --min-height 20+ --bind=ctrl-z:ignore $1"
|
||||
command cat "${FZF_DEFAULT_OPTS_FILE-}" 2> /dev/null
|
||||
printf '%s\n' "${FZF_DEFAULT_OPTS-} $2"
|
||||
}
|
||||
|
||||
__fzf_exec_awk() {
|
||||
if [[ -z ${__fzf_awk-} ]]; then
|
||||
__fzf_awk=awk
|
||||
if [[ $OSTYPE == solaris* && -x /usr/xpg4/bin/awk ]]; then
|
||||
__fzf_awk=/usr/xpg4/bin/awk
|
||||
elif command -v mawk >/dev/null 2>&1; then
|
||||
local n x y z d
|
||||
IFS=' .' read -r n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||
[[ $n == mawk ]] && (( d >= 20230302 && (x * 1000 + y) * 1000 + z >= 1003004 )) && __fzf_awk=mawk
|
||||
fi
|
||||
fi
|
||||
LC_ALL=C exec "$__fzf_awk" "$@"
|
||||
}
|
||||
#----END INCLUDE
|
||||
|
||||
# CTRL-T - Paste the selected file path(s) into the command line
|
||||
__fsel() {
|
||||
local cmd="${FZF_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
__fzf_select() {
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
local item
|
||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
FZF_DEFAULT_COMMAND=${FZF_CTRL_T_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=file,dir,follow,hidden --scheme=path" "${FZF_CTRL_T_OPTS-} -m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) "$@" < /dev/tty | while read -r item; do
|
||||
echo -n -E "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
echo
|
||||
@@ -55,58 +79,82 @@ __fsel() {
|
||||
}
|
||||
|
||||
__fzfcmd() {
|
||||
[ -n "$TMUX_PANE" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "$FZF_TMUX_OPTS" ]; } &&
|
||||
[ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; } &&
|
||||
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||
}
|
||||
|
||||
fzf-file-widget() {
|
||||
LBUFFER="${LBUFFER}$(__fsel)"
|
||||
LBUFFER="${LBUFFER}$(__fzf_select)"
|
||||
local ret=$?
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-file-widget
|
||||
bindkey '^T' fzf-file-widget
|
||||
if [[ "${FZF_CTRL_T_COMMAND-x}" != "" ]]; then
|
||||
zle -N fzf-file-widget
|
||||
bindkey -M emacs '^T' fzf-file-widget
|
||||
bindkey -M vicmd '^T' fzf-file-widget
|
||||
bindkey -M viins '^T' fzf-file-widget
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
fzf-cd-widget() {
|
||||
local cmd="${FZF_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
|
||||
local dir="$(
|
||||
FZF_DEFAULT_COMMAND=${FZF_ALT_C_COMMAND:-} \
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "--reverse --walker=dir,follow,hidden --scheme=path" "${FZF_ALT_C_OPTS-} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd) < /dev/tty)"
|
||||
if [[ -z "$dir" ]]; then
|
||||
zle redisplay
|
||||
return 0
|
||||
fi
|
||||
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||
BUFFER="cd -- ${(q)dir}"
|
||||
BUFFER="builtin cd -- ${(q)dir:a}"
|
||||
zle accept-line
|
||||
local ret=$?
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-cd-widget
|
||||
bindkey '\ec' fzf-cd-widget
|
||||
if [[ "${FZF_ALT_C_COMMAND-x}" != "" ]]; then
|
||||
zle -N fzf-cd-widget
|
||||
bindkey -M emacs '\ec' fzf-cd-widget
|
||||
bindkey -M vicmd '\ec' fzf-cd-widget
|
||||
bindkey -M viins '\ec' fzf-cd-widget
|
||||
fi
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
fzf-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||
selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' |
|
||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort,ctrl-z:ignore $FZF_CTRL_R_OPTS --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
|
||||
local selected
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases noglob nobash_rematch 2> /dev/null
|
||||
# Ensure the module is loaded if not already, and the required features, such
|
||||
# as the associative 'history' array, which maps event numbers to full history
|
||||
# lines, are set. Also, make sure Perl is installed for multi-line output.
|
||||
if zmodload -F zsh/parameter p:{commands,history} 2>/dev/null && (( ${+commands[perl]} )); then
|
||||
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))"
|
||||
else
|
||||
selected="$(fc -rl 1 | __fzf_exec_awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||
FZF_DEFAULT_OPTS=$(__fzf_defaults "" "-n2..,.. --scheme=history --bind=ctrl-r:toggle-sort --wrap-sign '\t↳ ' --highlight-line ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m") \
|
||||
FZF_DEFAULT_OPTS_FILE='' $(__fzfcmd))"
|
||||
fi
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
if [ -n "$num" ]; then
|
||||
zle vi-fetch-history -n $num
|
||||
if [[ $(__fzf_exec_awk '{print $1; exit}' <<< "$selected") =~ ^[1-9][0-9]* ]]; then
|
||||
zle vi-fetch-history -n $MATCH
|
||||
else # selected is a custom query, not from history
|
||||
LBUFFER="$selected"
|
||||
fi
|
||||
fi
|
||||
zle reset-prompt
|
||||
return $ret
|
||||
}
|
||||
zle -N fzf-history-widget
|
||||
bindkey '^R' fzf-history-widget
|
||||
zle -N fzf-history-widget
|
||||
bindkey -M emacs '^R' fzf-history-widget
|
||||
bindkey -M vicmd '^R' fzf-history-widget
|
||||
bindkey -M viins '^R' fzf-history-widget
|
||||
fi
|
||||
|
||||
} always {
|
||||
eval $__fzf_key_bindings_options
|
||||
|
||||
31
shell/update-common.sh
Executable file
31
shell/update-common.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/bin/sh
|
||||
|
||||
# This script applies the contents of "common.sh" to the other files.
|
||||
|
||||
set -e
|
||||
|
||||
# Go to the directory that contains this script
|
||||
dir=${0%"${0##*/}"}
|
||||
if [ -n "$dir" ]; then
|
||||
cd "$dir"
|
||||
fi
|
||||
|
||||
update() {
|
||||
{
|
||||
sed -n '1,/^#----BEGIN INCLUDE common\.sh/p' "$1"
|
||||
cat <<EOF
|
||||
# NOTE: Do not directly edit this section, which is copied from "common.sh".
|
||||
# To modify it, one can edit "common.sh" and run "./update-common.sh" to apply
|
||||
# the changes. See code comments in "common.sh" for the implementation details.
|
||||
EOF
|
||||
grep -v '^[[:blank:]]*#' common.sh # remove code comments in common.sh
|
||||
sed -n '/^#----END INCLUDE/,$p' "$1"
|
||||
} > "$1.part"
|
||||
|
||||
mv -f "$1.part" "$1"
|
||||
}
|
||||
|
||||
update completion.bash
|
||||
update completion.zsh
|
||||
update key-bindings.bash
|
||||
update key-bindings.zsh
|
||||
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
Copyright (c) 2013-2025 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
186
src/actiontype_string.go
Normal file
186
src/actiontype_string.go
Normal file
@@ -0,0 +1,186 @@
|
||||
// Code generated by "stringer -type=actionType"; DO NOT EDIT.
|
||||
|
||||
package fzf
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[actIgnore-0]
|
||||
_ = x[actStart-1]
|
||||
_ = x[actClick-2]
|
||||
_ = x[actInvalid-3]
|
||||
_ = x[actBracketedPasteBegin-4]
|
||||
_ = x[actBracketedPasteEnd-5]
|
||||
_ = x[actChar-6]
|
||||
_ = x[actMouse-7]
|
||||
_ = x[actBeginningOfLine-8]
|
||||
_ = x[actAbort-9]
|
||||
_ = x[actAccept-10]
|
||||
_ = x[actAcceptNonEmpty-11]
|
||||
_ = x[actAcceptOrPrintQuery-12]
|
||||
_ = x[actBackwardChar-13]
|
||||
_ = x[actBackwardDeleteChar-14]
|
||||
_ = x[actBackwardDeleteCharEof-15]
|
||||
_ = x[actBackwardWord-16]
|
||||
_ = x[actBackwardSubWord-17]
|
||||
_ = x[actCancel-18]
|
||||
_ = x[actChangeBorderLabel-19]
|
||||
_ = x[actChangeGhost-20]
|
||||
_ = x[actChangeHeader-21]
|
||||
_ = x[actChangeFooter-22]
|
||||
_ = x[actChangeHeaderLabel-23]
|
||||
_ = x[actChangeFooterLabel-24]
|
||||
_ = x[actChangeInputLabel-25]
|
||||
_ = x[actChangeListLabel-26]
|
||||
_ = x[actChangeMulti-27]
|
||||
_ = x[actChangeNth-28]
|
||||
_ = x[actChangePointer-29]
|
||||
_ = x[actChangePreview-30]
|
||||
_ = x[actChangePreviewLabel-31]
|
||||
_ = x[actChangePreviewWindow-32]
|
||||
_ = x[actChangePrompt-33]
|
||||
_ = x[actChangeQuery-34]
|
||||
_ = x[actClearScreen-35]
|
||||
_ = x[actClearQuery-36]
|
||||
_ = x[actClearSelection-37]
|
||||
_ = x[actClose-38]
|
||||
_ = x[actDeleteChar-39]
|
||||
_ = x[actDeleteCharEof-40]
|
||||
_ = x[actEndOfLine-41]
|
||||
_ = x[actFatal-42]
|
||||
_ = x[actForwardChar-43]
|
||||
_ = x[actForwardWord-44]
|
||||
_ = x[actForwardSubWord-45]
|
||||
_ = x[actKillLine-46]
|
||||
_ = x[actKillWord-47]
|
||||
_ = x[actKillSubWord-48]
|
||||
_ = x[actUnixLineDiscard-49]
|
||||
_ = x[actUnixWordRubout-50]
|
||||
_ = x[actYank-51]
|
||||
_ = x[actBackwardKillWord-52]
|
||||
_ = x[actBackwardKillSubWord-53]
|
||||
_ = x[actSelectAll-54]
|
||||
_ = x[actDeselectAll-55]
|
||||
_ = x[actToggle-56]
|
||||
_ = x[actToggleSearch-57]
|
||||
_ = x[actToggleAll-58]
|
||||
_ = x[actToggleDown-59]
|
||||
_ = x[actToggleUp-60]
|
||||
_ = x[actToggleIn-61]
|
||||
_ = x[actToggleOut-62]
|
||||
_ = x[actToggleTrack-63]
|
||||
_ = x[actToggleTrackCurrent-64]
|
||||
_ = x[actToggleHeader-65]
|
||||
_ = x[actToggleWrap-66]
|
||||
_ = x[actToggleMultiLine-67]
|
||||
_ = x[actToggleHscroll-68]
|
||||
_ = x[actTrackCurrent-69]
|
||||
_ = x[actToggleInput-70]
|
||||
_ = x[actHideInput-71]
|
||||
_ = x[actShowInput-72]
|
||||
_ = x[actUntrackCurrent-73]
|
||||
_ = x[actDown-74]
|
||||
_ = x[actUp-75]
|
||||
_ = x[actPageUp-76]
|
||||
_ = x[actPageDown-77]
|
||||
_ = x[actPosition-78]
|
||||
_ = x[actHalfPageUp-79]
|
||||
_ = x[actHalfPageDown-80]
|
||||
_ = x[actOffsetUp-81]
|
||||
_ = x[actOffsetDown-82]
|
||||
_ = x[actOffsetMiddle-83]
|
||||
_ = x[actJump-84]
|
||||
_ = x[actJumpAccept-85]
|
||||
_ = x[actPrintQuery-86]
|
||||
_ = x[actRefreshPreview-87]
|
||||
_ = x[actReplaceQuery-88]
|
||||
_ = x[actToggleSort-89]
|
||||
_ = x[actShowPreview-90]
|
||||
_ = x[actHidePreview-91]
|
||||
_ = x[actTogglePreview-92]
|
||||
_ = x[actTogglePreviewWrap-93]
|
||||
_ = x[actTransform-94]
|
||||
_ = x[actTransformBorderLabel-95]
|
||||
_ = x[actTransformGhost-96]
|
||||
_ = x[actTransformHeader-97]
|
||||
_ = x[actTransformFooter-98]
|
||||
_ = x[actTransformHeaderLabel-99]
|
||||
_ = x[actTransformFooterLabel-100]
|
||||
_ = x[actTransformInputLabel-101]
|
||||
_ = x[actTransformListLabel-102]
|
||||
_ = x[actTransformNth-103]
|
||||
_ = x[actTransformPointer-104]
|
||||
_ = x[actTransformPreviewLabel-105]
|
||||
_ = x[actTransformPrompt-106]
|
||||
_ = x[actTransformQuery-107]
|
||||
_ = x[actTransformSearch-108]
|
||||
_ = x[actTrigger-109]
|
||||
_ = x[actBgTransform-110]
|
||||
_ = x[actBgTransformBorderLabel-111]
|
||||
_ = x[actBgTransformGhost-112]
|
||||
_ = x[actBgTransformHeader-113]
|
||||
_ = x[actBgTransformFooter-114]
|
||||
_ = x[actBgTransformHeaderLabel-115]
|
||||
_ = x[actBgTransformFooterLabel-116]
|
||||
_ = x[actBgTransformInputLabel-117]
|
||||
_ = x[actBgTransformListLabel-118]
|
||||
_ = x[actBgTransformNth-119]
|
||||
_ = x[actBgTransformPointer-120]
|
||||
_ = x[actBgTransformPreviewLabel-121]
|
||||
_ = x[actBgTransformPrompt-122]
|
||||
_ = x[actBgTransformQuery-123]
|
||||
_ = x[actBgTransformSearch-124]
|
||||
_ = x[actBgCancel-125]
|
||||
_ = x[actSearch-126]
|
||||
_ = x[actPreview-127]
|
||||
_ = x[actPreviewTop-128]
|
||||
_ = x[actPreviewBottom-129]
|
||||
_ = x[actPreviewUp-130]
|
||||
_ = x[actPreviewDown-131]
|
||||
_ = x[actPreviewPageUp-132]
|
||||
_ = x[actPreviewPageDown-133]
|
||||
_ = x[actPreviewHalfPageUp-134]
|
||||
_ = x[actPreviewHalfPageDown-135]
|
||||
_ = x[actPrevHistory-136]
|
||||
_ = x[actPrevSelected-137]
|
||||
_ = x[actPrint-138]
|
||||
_ = x[actPut-139]
|
||||
_ = x[actNextHistory-140]
|
||||
_ = x[actNextSelected-141]
|
||||
_ = x[actExecute-142]
|
||||
_ = x[actExecuteSilent-143]
|
||||
_ = x[actExecuteMulti-144]
|
||||
_ = x[actSigStop-145]
|
||||
_ = x[actFirst-146]
|
||||
_ = x[actLast-147]
|
||||
_ = x[actReload-148]
|
||||
_ = x[actReloadSync-149]
|
||||
_ = x[actDisableSearch-150]
|
||||
_ = x[actEnableSearch-151]
|
||||
_ = x[actSelect-152]
|
||||
_ = x[actDeselect-153]
|
||||
_ = x[actUnbind-154]
|
||||
_ = x[actRebind-155]
|
||||
_ = x[actToggleBind-156]
|
||||
_ = x[actBecome-157]
|
||||
_ = x[actShowHeader-158]
|
||||
_ = x[actHideHeader-159]
|
||||
_ = x[actBell-160]
|
||||
_ = x[actExclude-161]
|
||||
_ = x[actExcludeMulti-162]
|
||||
_ = x[actAsync-163]
|
||||
}
|
||||
|
||||
const _actionType_name = "actIgnoreactStartactClickactInvalidactBracketedPasteBeginactBracketedPasteEndactCharactMouseactBeginningOfLineactAbortactAcceptactAcceptNonEmptyactAcceptOrPrintQueryactBackwardCharactBackwardDeleteCharactBackwardDeleteCharEofactBackwardWordactBackwardSubWordactCancelactChangeBorderLabelactChangeGhostactChangeHeaderactChangeFooteractChangeHeaderLabelactChangeFooterLabelactChangeInputLabelactChangeListLabelactChangeMultiactChangeNthactChangePointeractChangePreviewactChangePreviewLabelactChangePreviewWindowactChangePromptactChangeQueryactClearScreenactClearQueryactClearSelectionactCloseactDeleteCharactDeleteCharEofactEndOfLineactFatalactForwardCharactForwardWordactForwardSubWordactKillLineactKillWordactKillSubWordactUnixLineDiscardactUnixWordRuboutactYankactBackwardKillWordactBackwardKillSubWordactSelectAllactDeselectAllactToggleactToggleSearchactToggleAllactToggleDownactToggleUpactToggleInactToggleOutactToggleTrackactToggleTrackCurrentactToggleHeaderactToggleWrapactToggleMultiLineactToggleHscrollactTrackCurrentactToggleInputactHideInputactShowInputactUntrackCurrentactDownactUpactPageUpactPageDownactPositionactHalfPageUpactHalfPageDownactOffsetUpactOffsetDownactOffsetMiddleactJumpactJumpAcceptactPrintQueryactRefreshPreviewactReplaceQueryactToggleSortactShowPreviewactHidePreviewactTogglePreviewactTogglePreviewWrapactTransformactTransformBorderLabelactTransformGhostactTransformHeaderactTransformFooteractTransformHeaderLabelactTransformFooterLabelactTransformInputLabelactTransformListLabelactTransformNthactTransformPointeractTransformPreviewLabelactTransformPromptactTransformQueryactTransformSearchactTriggeractBgTransformactBgTransformBorderLabelactBgTransformGhostactBgTransformHeaderactBgTransformFooteractBgTransformHeaderLabelactBgTransformFooterLabelactBgTransformInputLabelactBgTransformListLabelactBgTransformNthactBgTransformPointeractBgTransformPreviewLabelactBgTransformPromptactBgTransformQueryactBgTransformSearchactBgCancelactSearchactPreviewactPreviewTopactPreviewBottomactPreviewUpactPreviewDownactPreviewPageUpactPreviewPageDownactPreviewHalfPageUpactPreviewHalfPageDownactPrevHistoryactPrevSelectedactPrintactPutactNextHistoryactNextSelectedactExecuteactExecuteSilentactExecuteMultiactSigStopactFirstactLastactReloadactReloadSyncactDisableSearchactEnableSearchactSelectactDeselectactUnbindactRebindactToggleBindactBecomeactShowHeaderactHideHeaderactBellactExcludeactExcludeMultiactAsync"
|
||||
|
||||
var _actionType_index = [...]uint16{0, 9, 17, 25, 35, 57, 77, 84, 92, 110, 118, 127, 144, 165, 180, 201, 225, 240, 258, 267, 287, 301, 316, 331, 351, 371, 390, 408, 422, 434, 450, 466, 487, 509, 524, 538, 552, 565, 582, 590, 603, 619, 631, 639, 653, 667, 684, 695, 706, 720, 738, 755, 762, 781, 803, 815, 829, 838, 853, 865, 878, 889, 900, 912, 926, 947, 962, 975, 993, 1009, 1024, 1038, 1050, 1062, 1079, 1086, 1091, 1100, 1111, 1122, 1135, 1150, 1161, 1174, 1189, 1196, 1209, 1222, 1239, 1254, 1267, 1281, 1295, 1311, 1331, 1343, 1366, 1383, 1401, 1419, 1442, 1465, 1487, 1508, 1523, 1542, 1566, 1584, 1601, 1619, 1629, 1643, 1668, 1687, 1707, 1727, 1752, 1777, 1801, 1824, 1841, 1862, 1888, 1908, 1927, 1947, 1958, 1967, 1977, 1990, 2006, 2018, 2032, 2048, 2066, 2086, 2108, 2122, 2137, 2145, 2151, 2165, 2180, 2190, 2206, 2221, 2231, 2239, 2246, 2255, 2268, 2284, 2299, 2308, 2319, 2328, 2337, 2350, 2359, 2372, 2385, 2392, 2402, 2417, 2425}
|
||||
|
||||
func (i actionType) String() string {
|
||||
if i < 0 || i >= actionType(len(_actionType_index)-1) {
|
||||
return "actionType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _actionType_name[_actionType_index[i]:_actionType_index[i+1]]
|
||||
}
|
||||
288
src/algo/algo.go
288
src/algo/algo.go
@@ -80,6 +80,7 @@ Scoring criteria
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -89,6 +90,10 @@ import (
|
||||
|
||||
var DEBUG bool
|
||||
|
||||
var delimiterChars = "/,:;|"
|
||||
|
||||
const whiteChars = " \t\n\v\f\r\x85\xA0"
|
||||
|
||||
func indexAt(index int, max int, forward bool) int {
|
||||
if forward {
|
||||
return index
|
||||
@@ -140,16 +145,78 @@ const (
|
||||
bonusFirstCharMultiplier = 2
|
||||
)
|
||||
|
||||
var (
|
||||
// Extra bonus for word boundary after whitespace character or beginning of the string
|
||||
bonusBoundaryWhite int16 = bonusBoundary + 2
|
||||
|
||||
// Extra bonus for word boundary after slash, colon, semi-colon, and comma
|
||||
bonusBoundaryDelimiter int16 = bonusBoundary + 1
|
||||
|
||||
initialCharClass = charWhite
|
||||
|
||||
// A minor optimization that can give 15%+ performance boost
|
||||
asciiCharClasses [unicode.MaxASCII + 1]charClass
|
||||
|
||||
// A minor optimization that can give yet another 5% performance boost
|
||||
bonusMatrix [charNumber + 1][charNumber + 1]int16
|
||||
)
|
||||
|
||||
type charClass int
|
||||
|
||||
const (
|
||||
charNonWord charClass = iota
|
||||
charWhite charClass = iota
|
||||
charNonWord
|
||||
charDelimiter
|
||||
charLower
|
||||
charUpper
|
||||
charLetter
|
||||
charNumber
|
||||
)
|
||||
|
||||
func Init(scheme string) bool {
|
||||
switch scheme {
|
||||
case "default":
|
||||
bonusBoundaryWhite = bonusBoundary + 2
|
||||
bonusBoundaryDelimiter = bonusBoundary + 1
|
||||
case "path":
|
||||
bonusBoundaryWhite = bonusBoundary
|
||||
bonusBoundaryDelimiter = bonusBoundary + 1
|
||||
if os.PathSeparator == '/' {
|
||||
delimiterChars = "/"
|
||||
} else {
|
||||
delimiterChars = string([]rune{os.PathSeparator, '/'})
|
||||
}
|
||||
initialCharClass = charDelimiter
|
||||
case "history":
|
||||
bonusBoundaryWhite = bonusBoundary
|
||||
bonusBoundaryDelimiter = bonusBoundary
|
||||
default:
|
||||
return false
|
||||
}
|
||||
for i := 0; i <= unicode.MaxASCII; i++ {
|
||||
char := rune(i)
|
||||
c := charNonWord
|
||||
if char >= 'a' && char <= 'z' {
|
||||
c = charLower
|
||||
} else if char >= 'A' && char <= 'Z' {
|
||||
c = charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
c = charNumber
|
||||
} else if strings.ContainsRune(whiteChars, char) {
|
||||
c = charWhite
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
c = charDelimiter
|
||||
}
|
||||
asciiCharClasses[i] = c
|
||||
}
|
||||
for i := 0; i <= int(charNumber); i++ {
|
||||
for j := 0; j <= int(charNumber); j++ {
|
||||
bonusMatrix[i][j] = bonusFor(charClass(i), charClass(j))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func posArray(withPos bool, len int) *[]int {
|
||||
if withPos {
|
||||
pos := make([]int, 0, len)
|
||||
@@ -174,17 +241,6 @@ func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
|
||||
return offset, make([]int32, size)
|
||||
}
|
||||
|
||||
func charClassOfAscii(char rune) charClass {
|
||||
if char >= 'a' && char <= 'z' {
|
||||
return charLower
|
||||
} else if char >= 'A' && char <= 'Z' {
|
||||
return charUpper
|
||||
} else if char >= '0' && char <= '9' {
|
||||
return charNumber
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
|
||||
func charClassOfNonAscii(char rune) charClass {
|
||||
if unicode.IsLower(char) {
|
||||
return charLower
|
||||
@@ -194,40 +250,60 @@ func charClassOfNonAscii(char rune) charClass {
|
||||
return charNumber
|
||||
} else if unicode.IsLetter(char) {
|
||||
return charLetter
|
||||
} else if unicode.IsSpace(char) {
|
||||
return charWhite
|
||||
} else if strings.ContainsRune(delimiterChars, char) {
|
||||
return charDelimiter
|
||||
}
|
||||
return charNonWord
|
||||
}
|
||||
|
||||
func charClassOf(char rune) charClass {
|
||||
if char <= unicode.MaxASCII {
|
||||
return charClassOfAscii(char)
|
||||
return asciiCharClasses[char]
|
||||
}
|
||||
return charClassOfNonAscii(char)
|
||||
}
|
||||
|
||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||
if prevClass == charNonWord && class != charNonWord {
|
||||
// Word boundary
|
||||
return bonusBoundary
|
||||
} else if prevClass == charLower && class == charUpper ||
|
||||
if class > charNonWord {
|
||||
switch prevClass {
|
||||
case charWhite:
|
||||
// Word boundary after whitespace
|
||||
return bonusBoundaryWhite
|
||||
case charDelimiter:
|
||||
// Word boundary after a delimiter character
|
||||
return bonusBoundaryDelimiter
|
||||
case charNonWord:
|
||||
// Word boundary
|
||||
return bonusBoundary
|
||||
}
|
||||
}
|
||||
|
||||
if prevClass == charLower && class == charUpper ||
|
||||
prevClass != charNumber && class == charNumber {
|
||||
// camelCase letter123
|
||||
return bonusCamel123
|
||||
} else if class == charNonWord {
|
||||
}
|
||||
|
||||
switch class {
|
||||
case charNonWord, charDelimiter:
|
||||
return bonusNonWord
|
||||
case charWhite:
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func bonusAt(input *util.Chars, idx int) int16 {
|
||||
if idx == 0 {
|
||||
return bonusBoundary
|
||||
return bonusBoundaryWhite
|
||||
}
|
||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||
return bonusMatrix[charClassOf(input.Get(idx-1))][charClassOf(input.Get(idx))]
|
||||
}
|
||||
|
||||
func normalizeRune(r rune) rune {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -276,30 +352,45 @@ func isAscii(runes []rune) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
|
||||
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) (int, int) {
|
||||
// Can't determine
|
||||
if !input.IsBytes() {
|
||||
return 0
|
||||
return 0, input.Length()
|
||||
}
|
||||
|
||||
// Not possible
|
||||
if !isAscii(pattern) {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
firstIdx, idx := 0, 0
|
||||
firstIdx, idx, lastIdx := 0, 0, 0
|
||||
var b byte
|
||||
for pidx := 0; pidx < len(pattern); pidx++ {
|
||||
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
|
||||
b = byte(pattern[pidx])
|
||||
idx = trySkip(input, caseSensitive, b, idx)
|
||||
if idx < 0 {
|
||||
return -1
|
||||
return -1, -1
|
||||
}
|
||||
if pidx == 0 && idx > 0 {
|
||||
// Step back to find the right bonus point
|
||||
firstIdx = idx - 1
|
||||
}
|
||||
lastIdx = idx
|
||||
idx++
|
||||
}
|
||||
return firstIdx
|
||||
|
||||
// Find the last appearance of the last character of the pattern to limit the search scope
|
||||
bu := b
|
||||
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||
bu = b - 32
|
||||
}
|
||||
scope := input.Bytes()[lastIdx:]
|
||||
for offset := len(scope) - 1; offset > 0; offset-- {
|
||||
if scope[offset] == b || scope[offset] == bu {
|
||||
return firstIdx, lastIdx + offset + 1
|
||||
}
|
||||
}
|
||||
return firstIdx, lastIdx + 1
|
||||
}
|
||||
|
||||
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
|
||||
@@ -310,7 +401,7 @@ func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []in
|
||||
if i == 0 {
|
||||
fmt.Print(" ")
|
||||
for j := int(f); j <= lastIdx; j++ {
|
||||
fmt.Printf(" " + string(T[j]) + " ")
|
||||
fmt.Print(" " + string(T[j]) + " ")
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
@@ -348,6 +439,9 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
return Result{0, 0, 0}, posArray(withPos, M)
|
||||
}
|
||||
N := input.Length()
|
||||
if M > N {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||
// we fall back to the greedy algorithm.
|
||||
@@ -356,10 +450,12 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
}
|
||||
|
||||
// Phase 1. Optimized search for ASCII string
|
||||
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
minIdx, maxIdx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||
if minIdx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
// fmt.Println(N, maxIdx, idx, maxIdx-idx, input.ToString())
|
||||
N = maxIdx - minIdx
|
||||
|
||||
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
||||
offset16 := 0
|
||||
@@ -372,20 +468,19 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
offset32, F := alloc32(offset32, slab, M)
|
||||
// Rune array
|
||||
_, T := alloc32(offset32, slab, N)
|
||||
input.CopyRunes(T)
|
||||
input.CopyRunes(T, minIdx)
|
||||
|
||||
// Phase 2. Calculate bonus for each point
|
||||
maxScore, maxScorePos := int16(0), 0
|
||||
pidx, lastIdx := 0, 0
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), charNonWord, false
|
||||
Tsub := T[idx:]
|
||||
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
|
||||
for off, char := range Tsub {
|
||||
pchar0, pchar, prevH0, prevClass, inGap := pattern[0], pattern[0], int16(0), initialCharClass, false
|
||||
for off, char := range T {
|
||||
var class charClass
|
||||
if char <= unicode.MaxASCII {
|
||||
class = charClassOfAscii(char)
|
||||
class = asciiCharClasses[char]
|
||||
if !caseSensitive && class == charUpper {
|
||||
char += 32
|
||||
T[off] = char
|
||||
}
|
||||
} else {
|
||||
class = charClassOfNonAscii(char)
|
||||
@@ -395,53 +490,53 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
T[off] = char
|
||||
}
|
||||
|
||||
Tsub[off] = char
|
||||
bonus := bonusFor(prevClass, class)
|
||||
Bsub[off] = bonus
|
||||
bonus := bonusMatrix[prevClass][class]
|
||||
B[off] = bonus
|
||||
prevClass = class
|
||||
|
||||
if char == pchar {
|
||||
if pidx < M {
|
||||
F[pidx] = int32(idx + off)
|
||||
F[pidx] = int32(off)
|
||||
pidx++
|
||||
pchar = pattern[util.Min(pidx, M-1)]
|
||||
}
|
||||
lastIdx = idx + off
|
||||
lastIdx = off
|
||||
}
|
||||
|
||||
if char == pchar0 {
|
||||
score := scoreMatch + bonus*bonusFirstCharMultiplier
|
||||
H0sub[off] = score
|
||||
C0sub[off] = 1
|
||||
H0[off] = score
|
||||
C0[off] = 1
|
||||
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||
maxScore, maxScorePos = score, idx+off
|
||||
if forward && bonus == bonusBoundary {
|
||||
maxScore, maxScorePos = score, off
|
||||
if forward && bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
}
|
||||
inGap = false
|
||||
} else {
|
||||
if inGap {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||
H0[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||
} else {
|
||||
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
H0[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||
}
|
||||
C0sub[off] = 0
|
||||
C0[off] = 0
|
||||
inGap = true
|
||||
}
|
||||
prevH0 = H0sub[off]
|
||||
prevH0 = H0[off]
|
||||
}
|
||||
if pidx != M {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
if M == 1 {
|
||||
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
|
||||
result := Result{minIdx + maxScorePos, minIdx + maxScorePos + 1, int(maxScore)}
|
||||
if !withPos {
|
||||
return result, nil
|
||||
}
|
||||
pos := []int{maxScorePos}
|
||||
pos := []int{minIdx + maxScorePos}
|
||||
return result, &pos
|
||||
}
|
||||
|
||||
@@ -486,11 +581,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
s1 = Hdiag[off] + scoreMatch
|
||||
b := Bsub[off]
|
||||
consecutive = Cdiag[off] + 1
|
||||
// Break consecutive chunk
|
||||
if b == bonusBoundary {
|
||||
consecutive = 1
|
||||
} else if consecutive > 1 {
|
||||
b = util.Max16(b, util.Max16(bonusConsecutive, B[col-int(consecutive)+1]))
|
||||
if consecutive > 1 {
|
||||
fb := B[col-int(consecutive)+1]
|
||||
// Break consecutive chunk
|
||||
if b >= bonusBoundary && b > fb {
|
||||
consecutive = 1
|
||||
} else {
|
||||
b = util.Max16(b, util.Max16(bonusConsecutive, fb))
|
||||
}
|
||||
}
|
||||
if s1+b < s2 {
|
||||
s1 += Bsub[off]
|
||||
@@ -535,7 +633,7 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
}
|
||||
|
||||
if s > s1 && (s > s2 || s == s2 && preferMatch) {
|
||||
*pos = append(*pos, j)
|
||||
*pos = append(*pos, j+minIdx)
|
||||
if i == 0 {
|
||||
break
|
||||
}
|
||||
@@ -548,14 +646,14 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.
|
||||
// Start offset we return here is only relevant when begin tiebreak is used.
|
||||
// However finding the accurate offset requires backtracking, and we don't
|
||||
// want to pay extra cost for the option that has lost its importance.
|
||||
return Result{j, maxScorePos + 1, int(maxScore)}, pos
|
||||
return Result{minIdx + j, minIdx + maxScorePos + 1, int(maxScore)}, pos
|
||||
}
|
||||
|
||||
// Implement the same sorting criteria as V2
|
||||
func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, pattern []rune, sidx int, eidx int, withPos bool) (int, *[]int) {
|
||||
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
|
||||
pos := posArray(withPos, len(pattern))
|
||||
prevClass := charNonWord
|
||||
prevClass := initialCharClass
|
||||
if sidx > 0 {
|
||||
prevClass = charClassOf(text.Get(sidx - 1))
|
||||
}
|
||||
@@ -578,12 +676,12 @@ func calculateScore(caseSensitive bool, normalize bool, text *util.Chars, patter
|
||||
*pos = append(*pos, idx)
|
||||
}
|
||||
score += scoreMatch
|
||||
bonus := bonusFor(prevClass, class)
|
||||
bonus := bonusMatrix[prevClass][class]
|
||||
if consecutive == 0 {
|
||||
firstBonus = bonus
|
||||
} else {
|
||||
// Break consecutive chunk
|
||||
if bonus == bonusBoundary {
|
||||
if bonus >= bonusBoundary && bonus > firstBonus {
|
||||
firstBonus = bonus
|
||||
}
|
||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
||||
@@ -616,7 +714,8 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
@@ -668,6 +767,9 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.C
|
||||
char = unicode.To(unicode.LowerCase, char)
|
||||
}
|
||||
}
|
||||
if normalize {
|
||||
char = normalizeRune(char)
|
||||
}
|
||||
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
@@ -699,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 pattern.
|
||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, false, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func ExactMatchBoundary(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
return exactMatchNaive(caseSensitive, normalize, forward, true, text, pattern, withPos, slab)
|
||||
}
|
||||
|
||||
func exactMatchNaive(caseSensitive bool, normalize bool, forward bool, boundaryCheck bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||
if len(pattern) == 0 {
|
||||
return Result{0, 0, 0}, nil
|
||||
}
|
||||
@@ -710,13 +820,14 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||
idx, _ := asciiFuzzyIndex(text, pattern, caseSensitive)
|
||||
if idx < 0 {
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
// For simplicity, only look at the bonus at the first character position
|
||||
pidx := 0
|
||||
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
||||
bestPos, bonus, bbonus, bestBonus := -1, int16(0), int16(0), int16(-1)
|
||||
for index := 0; index < lenRunes; index++ {
|
||||
index_ := indexAt(index, lenRunes, forward)
|
||||
char := text.Get(index_)
|
||||
@@ -732,16 +843,37 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
}
|
||||
pidx_ := indexAt(pidx, lenPattern, forward)
|
||||
pchar := pattern[pidx_]
|
||||
if pchar == char {
|
||||
ok := pchar == char
|
||||
if ok {
|
||||
if pidx_ == 0 {
|
||||
bonus = bonusAt(text, index_)
|
||||
}
|
||||
if boundaryCheck {
|
||||
if forward && pidx_ == 0 {
|
||||
bbonus = bonus
|
||||
} else if !forward && pidx_ == lenPattern-1 {
|
||||
if index_ < lenRunes-1 {
|
||||
bbonus = bonusAt(text, index_+1)
|
||||
} else {
|
||||
bbonus = bonusBoundaryWhite
|
||||
}
|
||||
}
|
||||
ok = bbonus >= bonusBoundary
|
||||
if ok && pidx_ == 0 {
|
||||
ok = index_ == 0 || charClassOf(text.Get(index_-1)) <= charDelimiter
|
||||
}
|
||||
if ok && pidx_ == len(pattern)-1 {
|
||||
ok = index_ == lenRunes-1 || charClassOf(text.Get(index_+1)) <= charDelimiter
|
||||
}
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
pidx++
|
||||
if pidx == lenPattern {
|
||||
if bonus > bestBonus {
|
||||
bestPos, bestBonus = index, bonus
|
||||
}
|
||||
if bonus == bonusBoundary {
|
||||
if bonus >= bonusBoundary {
|
||||
break
|
||||
}
|
||||
index -= pidx - 1
|
||||
@@ -761,7 +893,23 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *uti
|
||||
sidx = lenRunes - (bestPos + 1)
|
||||
eidx = lenRunes - (bestPos - lenPattern + 1)
|
||||
}
|
||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
var score int
|
||||
if boundaryCheck {
|
||||
// Underscore boundaries should be ranked lower than the other types of boundaries
|
||||
score = int(bonus)
|
||||
deduct := int(bonus-bonusBoundary) + 1
|
||||
if sidx > 0 && text.Get(sidx-1) == '_' {
|
||||
score -= deduct + 1
|
||||
deduct = 1
|
||||
}
|
||||
if eidx < lenRunes && text.Get(eidx) == '_' {
|
||||
score -= deduct
|
||||
}
|
||||
// Add base score so that this can compete with other match types e.g. 'foo' | bar
|
||||
score += scoreMatch*lenPattern + int(bonusBoundaryWhite)*(lenPattern+1)
|
||||
} else {
|
||||
score, _ = calculateScore(caseSensitive, normalize, text, pattern, sidx, eidx, false)
|
||||
}
|
||||
return Result{sidx, eidx, score}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
@@ -877,8 +1025,8 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Cha
|
||||
match = runesStr == string(pattern)
|
||||
}
|
||||
if match {
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
||||
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+int(bonusBoundaryWhite))*lenPattern +
|
||||
(bonusFirstCharMultiplier-1)*int(bonusBoundaryWhite)}, nil
|
||||
}
|
||||
return Result{-1, -1, 0}, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Init("default")
|
||||
}
|
||||
|
||||
func assertMatch(t *testing.T, fun Algo, caseSensitive, forward bool, input, pattern string, sidx int, eidx int, score int) {
|
||||
assertMatch2(t, fun, caseSensitive, false, forward, input, pattern, sidx, eidx, score)
|
||||
}
|
||||
@@ -45,29 +49,29 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
int(bonusBoundaryWhite)*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
scoreMatch*4+int(bonusBoundaryDelimiter)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*3)
|
||||
assertMatch(t, fn, false, forward, "/.oh-my-zsh/cache", "zshc", 8, 13,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3+scoreGapStart)
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+scoreGapStart+int(bonusBoundaryDelimiter))
|
||||
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
int(bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
|
||||
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)+
|
||||
scoreGapStart*2+scoreGapExtension*3)
|
||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*3)
|
||||
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*3)
|
||||
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
bonusNonWord+bonusBoundary)
|
||||
@@ -75,14 +79,14 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
||||
scoreMatch*3+bonusBoundary*(bonusFirstCharMultiplier+2)+
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryDelimiter)*2+
|
||||
scoreGapStart*2+scoreGapExtension*4)
|
||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||
scoreGapStart*2+scoreGapExtension*2)
|
||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
||||
util.Max(bonusCamel123, bonusBoundary))
|
||||
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
|
||||
util.Max(bonusCamel123, int(bonusBoundaryWhite)))
|
||||
|
||||
// Consecutive bonus updated
|
||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||
@@ -98,10 +102,10 @@ func TestFuzzyMatch(t *testing.T) {
|
||||
|
||||
func TestFuzzyMatchBackward(t *testing.T) {
|
||||
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
||||
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||
scoreGapStart+scoreGapExtension)
|
||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary)
|
||||
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite))
|
||||
}
|
||||
|
||||
func TestExactMatchNaive(t *testing.T) {
|
||||
@@ -114,9 +118,9 @@ func TestExactMatchNaive(t *testing.T) {
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
|
||||
scoreMatch*4+bonusBoundary*(bonusFirstCharMultiplier+3))
|
||||
scoreMatch*4+int(bonusBoundaryDelimiter)*(bonusFirstCharMultiplier+3))
|
||||
assertMatch(t, ExactMatchNaive, false, dir, "/.oh-my-zsh/cache", "zsh/c", 8, 13,
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+4))
|
||||
scoreMatch*5+bonusBoundary*(bonusFirstCharMultiplier+3)+int(bonusBoundaryDelimiter))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +132,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPrefixMatch(t *testing.T) {
|
||||
score := (scoreMatch+bonusBoundary)*3 + bonusBoundary*(bonusFirstCharMultiplier-1)
|
||||
score := scoreMatch*3 + int(bonusBoundaryWhite)*bonusFirstCharMultiplier + int(bonusBoundaryWhite)*2
|
||||
|
||||
for _, dir := range []bool{true, false} {
|
||||
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
||||
@@ -156,9 +160,10 @@ func TestSuffixMatch(t *testing.T) {
|
||||
// Strip trailing white space from the string
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz", 6, 9,
|
||||
scoreMatch*3+bonusConsecutive*2)
|
||||
|
||||
// Only when the pattern doesn't end with a space
|
||||
assertMatch(t, SuffixMatch, false, dir, "fooBarbaz ", "baz ", 6, 10,
|
||||
scoreMatch*4+bonusConsecutive*2+bonusNonWord)
|
||||
scoreMatch*4+bonusConsecutive*2+int(bonusBoundaryWhite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,9 +187,9 @@ func TestNormalize(t *testing.T) {
|
||||
input, pattern, sidx, eidx, score)
|
||||
}
|
||||
}
|
||||
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||
test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2)
|
||||
test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||
}
|
||||
|
||||
func TestLongString(t *testing.T) {
|
||||
@@ -195,3 +200,12 @@ func TestLongString(t *testing.T) {
|
||||
bytes[math.MaxUint16] = 'z'
|
||||
assertMatch(t, FuzzyMatchV2, true, true, string(bytes), "zx", math.MaxUint16, math.MaxUint16+2, scoreMatch*2+bonusConsecutive)
|
||||
}
|
||||
|
||||
func TestLongStringWithNormalize(t *testing.T) {
|
||||
bytes := make([]byte, 30000)
|
||||
for i := range bytes {
|
||||
bytes[i] = 'x'
|
||||
}
|
||||
unicodeString := string(bytes) + " Minímal example"
|
||||
assertMatch2(t, FuzzyMatchV1, false, true, false, unicodeString, "minim", 30001, 30006, 140)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package algo
|
||||
|
||||
var normalized map[rune]rune = map[rune]rune{
|
||||
var normalized = map[rune]rune{
|
||||
0x00E1: 'a', // WITH ACUTE, LATIN SMALL LETTER
|
||||
0x0103: 'a', // WITH BREVE, LATIN SMALL LETTER
|
||||
0x01CE: 'a', // WITH CARON, LATIN SMALL LETTER
|
||||
@@ -473,6 +473,103 @@ var normalized map[rune]rune = map[rune]rune{
|
||||
'ử': 'u',
|
||||
'ữ': 'u',
|
||||
'ự': 'u',
|
||||
|
||||
// https://en.wikipedia.org/wiki/Halfwidth_and_Fullwidth_Forms_(Unicode_block)
|
||||
0xFF01: '!', // Fullwidth exclamation
|
||||
0xFF02: '"', // Fullwidth quotation mark
|
||||
0xFF03: '#', // Fullwidth number sign
|
||||
0xFF04: '$', // Fullwidth dollar sign
|
||||
0xFF05: '%', // Fullwidth percent
|
||||
0xFF06: '&', // Fullwidth ampersand
|
||||
0xFF07: '\'', // Fullwidth apostrophe
|
||||
0xFF08: '(', // Fullwidth left parenthesis
|
||||
0xFF09: ')', // Fullwidth right parenthesis
|
||||
0xFF0A: '*', // Fullwidth asterisk
|
||||
0xFF0B: '+', // Fullwidth plus
|
||||
0xFF0C: ',', // Fullwidth comma
|
||||
0xFF0D: '-', // Fullwidth hyphen-minus
|
||||
0xFF0E: '.', // Fullwidth period
|
||||
0xFF0F: '/', // Fullwidth slash
|
||||
0xFF10: '0',
|
||||
0xFF11: '1',
|
||||
0xFF12: '2',
|
||||
0xFF13: '3',
|
||||
0xFF14: '4',
|
||||
0xFF15: '5',
|
||||
0xFF16: '6',
|
||||
0xFF17: '7',
|
||||
0xFF18: '8',
|
||||
0xFF19: '9',
|
||||
0xFF1A: ':', // Fullwidth colon
|
||||
0xFF1B: ';', // Fullwidth semicolon
|
||||
0xFF1C: '<', // Fullwidth less-than
|
||||
0xFF1D: '=', // Fullwidth equal
|
||||
0xFF1E: '>', // Fullwidth greater-than
|
||||
0xFF1F: '?', // Fullwidth question mark
|
||||
0xFF20: '@', // Fullwidth at sign
|
||||
0xFF21: 'A',
|
||||
0xFF22: 'B',
|
||||
0xFF23: 'C',
|
||||
0xFF24: 'D',
|
||||
0xFF25: 'E',
|
||||
0xFF26: 'F',
|
||||
0xFF27: 'G',
|
||||
0xFF28: 'H',
|
||||
0xFF29: 'I',
|
||||
0xFF2A: 'J',
|
||||
0xFF2B: 'K',
|
||||
0xFF2C: 'L',
|
||||
0xFF2D: 'M',
|
||||
0xFF2E: 'N',
|
||||
0xFF2F: 'O',
|
||||
0xFF30: 'P',
|
||||
0xFF31: 'Q',
|
||||
0xFF32: 'R',
|
||||
0xFF33: 'S',
|
||||
0xFF34: 'T',
|
||||
0xFF35: 'U',
|
||||
0xFF36: 'V',
|
||||
0xFF37: 'W',
|
||||
0xFF38: 'X',
|
||||
0xFF39: 'Y',
|
||||
0xFF3A: 'Z',
|
||||
0xFF3B: '[', // Fullwidth left bracket
|
||||
0xFF3C: '\\', // Fullwidth backslash
|
||||
0xFF3D: ']', // Fullwidth right bracket
|
||||
0xFF3E: '^', // Fullwidth circumflex
|
||||
0xFF3F: '_', // Fullwidth underscore
|
||||
0xFF40: '`', // Fullwidth grave accent
|
||||
0xFF41: 'a',
|
||||
0xFF42: 'b',
|
||||
0xFF43: 'c',
|
||||
0xFF44: 'd',
|
||||
0xFF45: 'e',
|
||||
0xFF46: 'f',
|
||||
0xFF47: 'g',
|
||||
0xFF48: 'h',
|
||||
0xFF49: 'i',
|
||||
0xFF4A: 'j',
|
||||
0xFF4B: 'k',
|
||||
0xFF4C: 'l',
|
||||
0xFF4D: 'm',
|
||||
0xFF4E: 'n',
|
||||
0xFF4F: 'o',
|
||||
0xFF50: 'p',
|
||||
0xFF51: 'q',
|
||||
0xFF52: 'r',
|
||||
0xFF53: 's',
|
||||
0xFF54: 't',
|
||||
0xFF55: 'u',
|
||||
0xFF56: 'v',
|
||||
0xFF57: 'w',
|
||||
0xFF58: 'x',
|
||||
0xFF59: 'y',
|
||||
0xFF5A: 'z',
|
||||
0xFF5B: '{', // Fullwidth left brace
|
||||
0xFF5C: '|', // Fullwidth vertical bar
|
||||
0xFF5D: '}', // Fullwidth right brace
|
||||
0xFF5E: '~', // Fullwidth tilde
|
||||
0xFF61: '.', // Halfwidth ideographic full stop
|
||||
}
|
||||
|
||||
// NormalizeRunes normalizes latin script letters
|
||||
@@ -480,7 +577,7 @@ func NormalizeRunes(runes []rune) []rune {
|
||||
ret := make([]rune, len(runes))
|
||||
copy(ret, runes)
|
||||
for idx, r := range runes {
|
||||
if r < 0x00C0 || r > 0x2184 {
|
||||
if r < 0x00C0 || r > 0xFF61 {
|
||||
continue
|
||||
}
|
||||
n := normalized[r]
|
||||
|
||||
176
src/ansi.go
176
src/ansi.go
@@ -1,6 +1,7 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -13,22 +14,28 @@ type ansiOffset struct {
|
||||
color ansiState
|
||||
}
|
||||
|
||||
type url struct {
|
||||
uri string
|
||||
params string
|
||||
}
|
||||
|
||||
type ansiState struct {
|
||||
fg tui.Color
|
||||
bg tui.Color
|
||||
attr tui.Attr
|
||||
lbg tui.Color
|
||||
url *url
|
||||
}
|
||||
|
||||
func (s *ansiState) colored() bool {
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
|
||||
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0 || s.url != nil
|
||||
}
|
||||
|
||||
func (s *ansiState) equals(t *ansiState) bool {
|
||||
if t == nil {
|
||||
return !s.colored()
|
||||
}
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
|
||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg && s.url == t.url
|
||||
}
|
||||
|
||||
func (s *ansiState) ToString() string {
|
||||
@@ -37,7 +44,7 @@ func (s *ansiState) ToString() string {
|
||||
}
|
||||
|
||||
ret := ""
|
||||
if s.attr&tui.Bold > 0 {
|
||||
if s.attr&tui.Bold > 0 || s.attr&tui.BoldForce > 0 {
|
||||
ret += "1;"
|
||||
}
|
||||
if s.attr&tui.Dim > 0 {
|
||||
@@ -55,9 +62,16 @@ func (s *ansiState) ToString() string {
|
||||
if s.attr&tui.Reverse > 0 {
|
||||
ret += "7;"
|
||||
}
|
||||
if s.attr&tui.StrikeThrough > 0 {
|
||||
ret += "9;"
|
||||
}
|
||||
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 {
|
||||
@@ -84,55 +98,71 @@ func isPrint(c uint8) bool {
|
||||
return '\x20' <= c && c <= '\x7e'
|
||||
}
|
||||
|
||||
func matchOperatingSystemCommand(s string) int {
|
||||
// `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ match starting here
|
||||
func matchOperatingSystemCommand(s string, start int) int {
|
||||
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
// ^ 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++ {
|
||||
}
|
||||
if i < len(s) {
|
||||
if s[i] == '\x07' {
|
||||
return i + 1
|
||||
}
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------
|
||||
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||
return i + 2
|
||||
}
|
||||
}
|
||||
|
||||
// `\x1b]8;PARAMS;URI\x1b\\TITLE\x1b]8;;\x1b`
|
||||
// ------------
|
||||
if i < len(s) && s[:i+1] == "\x1b]8;;\x1b" {
|
||||
return i + 1
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
func matchControlSequence(s string) int {
|
||||
// `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
|
||||
// ^ match starting here
|
||||
// `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
|
||||
// ^ match starting here
|
||||
//
|
||||
i := 2 // prefix matched in nextAnsiEscapeSequence()
|
||||
for ; i < len(s) && (isNumeric(s[i]) || s[i] == ';' || s[i] == '?'); i++ {
|
||||
}
|
||||
if i < len(s) {
|
||||
for ; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
|
||||
return i + 1
|
||||
switch c {
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ';', ':', '?':
|
||||
// ok
|
||||
default:
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || c == '@' {
|
||||
return i + 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func isCtrlSeqStart(c uint8) bool {
|
||||
return c == '\\' || c == '[' || c == '(' || c == ')'
|
||||
switch c {
|
||||
case '\\', '[', '(', ')':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// nextAnsiEscapeSequence returns the ANSI escape sequence and is equivalent to
|
||||
// calling FindStringIndex() on the below regex (which was originally used):
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;?]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)"
|
||||
//
|
||||
// "(?:\x1b[\\[()][0-9;:?]*[a-zA-Z@]|\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)"
|
||||
func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
// fast check for ANSI escape sequences
|
||||
i := 0
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\x0e', '\x0f', '\x1b', '\x08':
|
||||
case '\x0e', '\x0f', '\x1b', '\x08', '\n':
|
||||
// We ignore the fact that '\x08' cannot be the first char
|
||||
// in the string and be an escape sequence for the sake of
|
||||
// speed and simplicity.
|
||||
@@ -144,6 +174,9 @@ func nextAnsiEscapeSequence(s string) (int, int) {
|
||||
Loop:
|
||||
for ; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '\n':
|
||||
// match: `\n`
|
||||
return i, i + 1
|
||||
case '\x08':
|
||||
// backtrack to match: `.\x08`
|
||||
if i > 0 && s[i-1] != '\n' {
|
||||
@@ -154,19 +187,27 @@ Loop:
|
||||
return i - n, i + 1
|
||||
}
|
||||
case '\x1b':
|
||||
// match: `\x1b[\\[()][0-9;?]*[a-zA-Z@]`
|
||||
// match: `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
|
||||
if i+2 < len(s) && isCtrlSeqStart(s[i+1]) {
|
||||
if j := matchControlSequence(s[i:]); j != -1 {
|
||||
return i, i + j
|
||||
}
|
||||
}
|
||||
|
||||
// match: `\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' && isNumeric(s[i+2]) &&
|
||||
s[i+3] == ';' && isPrint(s[i+4]) {
|
||||
// match: `\x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||
if i+5 < len(s) && s[i+1] == ']' {
|
||||
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 {
|
||||
return i, i + j
|
||||
// \x1b][0-9]+[;:][[:print:]]+(?:\x1b\\\\|\x07)
|
||||
// ---------------
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,13 +268,30 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
output.WriteString(prev)
|
||||
}
|
||||
|
||||
newState := interpretCode(str[start:idx], state)
|
||||
if !newState.equals(state) {
|
||||
code := str[start:idx]
|
||||
newState := interpretCode(code, state)
|
||||
if code == "\n" || !newState.equals(state) {
|
||||
if state != nil {
|
||||
// Update last offset
|
||||
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||
}
|
||||
|
||||
if code == "\n" {
|
||||
output.WriteRune('\n')
|
||||
runeCount++
|
||||
// Full-background marker
|
||||
if newState.lbg >= 0 {
|
||||
marker := newState
|
||||
marker.attr |= tui.FullBg
|
||||
offsets = append(offsets, ansiOffset{
|
||||
[2]int32{int32(runeCount), int32(runeCount)},
|
||||
marker,
|
||||
})
|
||||
// Reset the full-line background color
|
||||
newState.lbg = -1
|
||||
}
|
||||
}
|
||||
|
||||
if newState.colored() {
|
||||
// Append new offset
|
||||
if pstate == nil {
|
||||
@@ -282,7 +340,13 @@ func extractColor(str string, state *ansiState, proc func(string, *ansiState) bo
|
||||
|
||||
func parseAnsiCode(s string) (int, string) {
|
||||
var remaining string
|
||||
if i := strings.IndexByte(s, ';'); i >= 0 {
|
||||
var i int
|
||||
// Faster than strings.IndexAny(";:")
|
||||
i = strings.IndexByte(s, ';')
|
||||
if i < 0 {
|
||||
i = strings.IndexByte(s, ':')
|
||||
}
|
||||
if i >= 0 {
|
||||
remaining = s[i+1:]
|
||||
s = s[:i]
|
||||
}
|
||||
@@ -291,7 +355,7 @@ func parseAnsiCode(s string) (int, string) {
|
||||
// Inlined version of strconv.Atoi() that only handles positive
|
||||
// integers and does not allocate on error.
|
||||
code := 0
|
||||
for _, ch := range []byte(s) {
|
||||
for _, ch := range stringBytes(s) {
|
||||
ch -= '0'
|
||||
if ch > 9 {
|
||||
return -1, remaining
|
||||
@@ -305,23 +369,47 @@ func parseAnsiCode(s string) (int, string) {
|
||||
}
|
||||
|
||||
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
if ansiCode == "\n" {
|
||||
if prevState != nil {
|
||||
return *prevState
|
||||
}
|
||||
return ansiState{-1, -1, 0, -1, nil}
|
||||
}
|
||||
|
||||
var state ansiState
|
||||
if prevState == nil {
|
||||
state = ansiState{-1, -1, 0, -1}
|
||||
state = ansiState{-1, -1, 0, -1, nil}
|
||||
} else {
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg, prevState.url}
|
||||
}
|
||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||
if prevState != nil && (strings.HasSuffix(ansiCode, "0K") || strings.HasSuffix(ansiCode, "[K")) {
|
||||
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
|
||||
}
|
||||
|
||||
if len(ansiCode) <= 3 {
|
||||
reset := func() {
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
}
|
||||
|
||||
if len(ansiCode) <= 3 {
|
||||
reset()
|
||||
return state
|
||||
}
|
||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||
@@ -329,9 +417,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
state256 := 0
|
||||
ptr := &state.fg
|
||||
|
||||
count := 0
|
||||
for len(ansiCode) != 0 {
|
||||
var num int
|
||||
if num, ansiCode = parseAnsiCode(ansiCode); num != -1 {
|
||||
count++
|
||||
switch state256 {
|
||||
case 0:
|
||||
switch num {
|
||||
@@ -357,14 +447,23 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
state.attr = state.attr | tui.Blink
|
||||
case 7:
|
||||
state.attr = state.attr | tui.Reverse
|
||||
case 9:
|
||||
state.attr = state.attr | tui.StrikeThrough
|
||||
case 22:
|
||||
state.attr = state.attr &^ tui.Bold
|
||||
state.attr = state.attr &^ tui.Dim
|
||||
case 23: // tput rmso
|
||||
state.attr = state.attr &^ tui.Italic
|
||||
case 24: // tput rmul
|
||||
state.attr = state.attr &^ tui.Underline
|
||||
case 25:
|
||||
state.attr = state.attr &^ tui.Blink
|
||||
case 27:
|
||||
state.attr = state.attr &^ tui.Reverse
|
||||
case 29:
|
||||
state.attr = state.attr &^ tui.StrikeThrough
|
||||
case 0:
|
||||
state.fg = -1
|
||||
state.bg = -1
|
||||
state.attr = 0
|
||||
reset()
|
||||
state256 = 0
|
||||
default:
|
||||
if num >= 30 && num <= 37 {
|
||||
@@ -402,6 +501,11 @@ func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty sequence: reset
|
||||
if count == 0 {
|
||||
reset()
|
||||
}
|
||||
|
||||
if state256 > 0 {
|
||||
*ptr = -1
|
||||
}
|
||||
|
||||
@@ -15,14 +15,14 @@ import (
|
||||
// testing nextAnsiEscapeSequence().
|
||||
//
|
||||
// References:
|
||||
// - https://github.com/gnachman/iTerm2
|
||||
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
|
||||
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b][0-9];[[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08)")
|
||||
// - https://github.com/gnachman/iTerm2
|
||||
// - https://web.archive.org/web/20090204053813/http://ascii-table.com/ansi-escape-sequences.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences.php)
|
||||
// - https://web.archive.org/web/20090227051140/http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
||||
// (archived from http://ascii-table.com/ansi-escape-sequences-vt-100.php)
|
||||
// - http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
||||
// - https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
var ansiRegexReference = regexp.MustCompile("(?:\x1b[\\[()][0-9;:]*[a-zA-Z@]|\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)|\x1b.|[\x0e\x0f]|.\x08|\n)")
|
||||
|
||||
func testParserReference(t testing.TB, str string) {
|
||||
t.Helper()
|
||||
@@ -335,6 +335,28 @@ func TestExtractColor(t *testing.T) {
|
||||
assert((*offsets)[0], 0, 6, 2, -1, true)
|
||||
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) {
|
||||
@@ -342,12 +364,15 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
state := interpretCode(code, prevState)
|
||||
if expected != state.ToString() {
|
||||
t.Errorf("expected: %s, actual: %s",
|
||||
strings.Replace(expected, "\x1b[", "\\x1b[", -1),
|
||||
strings.Replace(state.ToString(), "\x1b[", "\\x1b[", -1))
|
||||
strings.ReplaceAll(expected, "\x1b[", "\\x1b["),
|
||||
strings.ReplaceAll(state.ToString(), "\x1b[", "\\x1b["))
|
||||
}
|
||||
}
|
||||
assert("\x1b[m", nil, "")
|
||||
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
|
||||
assert("\x1b[0m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
assert("\x1b[;;m", &ansiState{fg: 4, bg: 4, lbg: -1}, "")
|
||||
|
||||
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||
@@ -358,6 +383,7 @@ func TestAnsiCodeStringConversion(t *testing.T) {
|
||||
assert("\x1b[31m", &ansiState{fg: 4, bg: 4, lbg: -1}, "\x1b[31;44m")
|
||||
assert("\x1b[1;2;31m", &ansiState{fg: 2, bg: -1, attr: tui.Reverse, lbg: -1}, "\x1b[1;2;7;31;49m")
|
||||
assert("\x1b[38;5;100;48;5;200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||
assert("\x1b[38:5:100:48:5:200m", nil, "\x1b[38;5;100;48;5;200m")
|
||||
assert("\x1b[48;5;100;38;5;200m", nil, "\x1b[38;5;200;48;5;100m")
|
||||
assert("\x1b[48;5;100;38;2;10;20;30;1m", nil, "\x1b[1;38;2;10;20;30;48;5;100m")
|
||||
assert("\x1b[48;5;100;38;2;10;20;30;7m",
|
||||
@@ -385,9 +411,9 @@ func TestParseAnsiCode(t *testing.T) {
|
||||
}
|
||||
|
||||
// kernel/bpf/preload/iterators/README
|
||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38;5;81mbpf/" +
|
||||
"\x1b[0m\x1b[38;5;81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||
"\x1b[0m\x1b[38;5;149mMakefile\x1b[m\x1b[K\x1b[0m"
|
||||
const ansiBenchmarkString = "\x1b[38;5;81m\x1b[01;31m\x1b[Kkernel/\x1b[0m\x1b[38:5:81mbpf/" +
|
||||
"\x1b[0m\x1b[38:5:81mpreload/\x1b[0m\x1b[38;5;81miterators/" +
|
||||
"\x1b[0m\x1b[38:5:149mMakefile\x1b[m\x1b[K\x1b[0m"
|
||||
|
||||
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
|
||||
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||
|
||||
18
src/cache.go
18
src/cache.go
@@ -12,8 +12,22 @@ type ChunkCache struct {
|
||||
}
|
||||
|
||||
// NewChunkCache returns a new ChunkCache
|
||||
func NewChunkCache() ChunkCache {
|
||||
return ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
|
||||
func NewChunkCache() *ChunkCache {
|
||||
return &ChunkCache{sync.Mutex{}, make(map[*Chunk]*queryCache)}
|
||||
}
|
||||
|
||||
func (cc *ChunkCache) Clear() {
|
||||
cc.mutex.Lock()
|
||||
cc.cache = make(map[*Chunk]*queryCache)
|
||||
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
|
||||
|
||||
@@ -16,14 +16,16 @@ type ChunkList struct {
|
||||
chunks []*Chunk
|
||||
mutex sync.Mutex
|
||||
trans ItemBuilder
|
||||
cache *ChunkCache
|
||||
}
|
||||
|
||||
// NewChunkList returns a new ChunkList
|
||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||
func NewChunkList(cache *ChunkCache, trans ItemBuilder) *ChunkList {
|
||||
return &ChunkList{
|
||||
chunks: []*Chunk{},
|
||||
mutex: sync.Mutex{},
|
||||
trans: trans}
|
||||
trans: trans,
|
||||
cache: cache}
|
||||
}
|
||||
|
||||
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||
@@ -39,6 +41,13 @@ func (c *Chunk) IsFull() bool {
|
||||
return c.count == chunkSize
|
||||
}
|
||||
|
||||
func (c *Chunk) lastIndex(minValue int32) int32 {
|
||||
if c.count == 0 {
|
||||
return minValue
|
||||
}
|
||||
return c.items[c.count-1].Index() + 1 // Exclusive
|
||||
}
|
||||
|
||||
func (cl *ChunkList) lastChunk() *Chunk {
|
||||
return cl.chunks[len(cl.chunks)-1]
|
||||
}
|
||||
@@ -48,7 +57,12 @@ func CountItems(cs []*Chunk) int {
|
||||
if len(cs) == 0 {
|
||||
return 0
|
||||
}
|
||||
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
|
||||
if len(cs) == 1 {
|
||||
return cs[0].count
|
||||
}
|
||||
|
||||
// First chunk might not be full due to --tail=N
|
||||
return cs[0].count + chunkSize*(len(cs)-2) + cs[len(cs)-1].count
|
||||
}
|
||||
|
||||
// Push adds the item to the list
|
||||
@@ -72,18 +86,56 @@ func (cl *ChunkList) Clear() {
|
||||
}
|
||||
|
||||
// Snapshot returns immutable snapshot of the ChunkList
|
||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||
func (cl *ChunkList) Snapshot(tail int) ([]*Chunk, int, bool) {
|
||||
cl.mutex.Lock()
|
||||
|
||||
changed := false
|
||||
if tail > 0 && CountItems(cl.chunks) > tail {
|
||||
changed = true
|
||||
// Find the number of chunks to keep
|
||||
numChunks := 0
|
||||
for left, i := tail, len(cl.chunks)-1; left > 0 && i >= 0; i-- {
|
||||
numChunks++
|
||||
left -= cl.chunks[i].count
|
||||
}
|
||||
|
||||
// Copy the chunks to keep
|
||||
ret := make([]*Chunk, numChunks)
|
||||
minIndex := len(cl.chunks) - numChunks
|
||||
cl.cache.retire(cl.chunks[:minIndex]...)
|
||||
copy(ret, cl.chunks[minIndex:])
|
||||
|
||||
for left, i := tail, len(ret)-1; i >= 0; i-- {
|
||||
chunk := ret[i]
|
||||
if chunk.count > left {
|
||||
newChunk := *chunk
|
||||
newChunk.count = left
|
||||
oldCount := chunk.count
|
||||
for i := 0; i < left; i++ {
|
||||
newChunk.items[i] = chunk.items[oldCount-left+i]
|
||||
}
|
||||
ret[i] = &newChunk
|
||||
cl.cache.retire(chunk)
|
||||
break
|
||||
}
|
||||
left -= chunk.count
|
||||
}
|
||||
cl.chunks = ret
|
||||
}
|
||||
|
||||
ret := make([]*Chunk, len(cl.chunks))
|
||||
copy(ret, cl.chunks)
|
||||
|
||||
// Duplicate the last chunk
|
||||
// Duplicate the first and the last chunk
|
||||
if cnt := len(ret); cnt > 0 {
|
||||
if tail > 0 && cnt > 1 {
|
||||
newChunk := *ret[0]
|
||||
ret[0] = &newChunk
|
||||
}
|
||||
newChunk := *ret[cnt-1]
|
||||
ret[cnt-1] = &newChunk
|
||||
}
|
||||
|
||||
cl.mutex.Unlock()
|
||||
return ret, CountItems(ret)
|
||||
return ret, CountItems(ret), changed
|
||||
}
|
||||
|
||||
@@ -11,13 +11,13 @@ func TestChunkList(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
cl := NewChunkList(func(item *Item, s []byte) bool {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
|
||||
// Snapshot
|
||||
snapshot, count := cl.Snapshot()
|
||||
snapshot, count, _ := cl.Snapshot(0)
|
||||
if len(snapshot) > 0 || count > 0 {
|
||||
t.Error("Snapshot should be empty now")
|
||||
}
|
||||
@@ -32,7 +32,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// But the new snapshot should contain the added items
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 1 && count != 2 {
|
||||
t.Error("Snapshot should not be empty now")
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func TestChunkList(t *testing.T) {
|
||||
|
||||
// Add more data
|
||||
for i := 0; i < chunkSize*2; i++ {
|
||||
cl.Push([]byte(fmt.Sprintf("item %d", i)))
|
||||
cl.Push(fmt.Appendf(nil, "item %d", i))
|
||||
}
|
||||
|
||||
// Previous snapshot should remain the same
|
||||
@@ -61,7 +61,7 @@ func TestChunkList(t *testing.T) {
|
||||
}
|
||||
|
||||
// New snapshot
|
||||
snapshot, count = cl.Snapshot()
|
||||
snapshot, count, _ = cl.Snapshot(0)
|
||||
if len(snapshot) != 3 || !snapshot[0].IsFull() ||
|
||||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
||||
t.Error("Expected two full chunks and one more chunk")
|
||||
@@ -78,3 +78,39 @@ func TestChunkList(t *testing.T) {
|
||||
t.Error("Unexpected number of items:", lastChunkCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkListTail(t *testing.T) {
|
||||
cl := NewChunkList(NewChunkCache(), func(item *Item, s []byte) bool {
|
||||
item.text = util.ToChars(s)
|
||||
return true
|
||||
})
|
||||
total := chunkSize*2 + chunkSize/2
|
||||
for i := 0; i < total; i++ {
|
||||
cl.Push(fmt.Appendf(nil, "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)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
@@ -15,6 +14,7 @@ const (
|
||||
|
||||
// Reader
|
||||
readerBufferSize = 64 * 1024
|
||||
readerSlabSize = 128 * 1024
|
||||
readerPollIntervalMin = 10 * time.Millisecond
|
||||
readerPollIntervalStep = 5 * time.Millisecond
|
||||
readerPollIntervalMax = 50 * time.Millisecond
|
||||
@@ -26,9 +26,13 @@ const (
|
||||
previewCancelWait = 500 * time.Millisecond
|
||||
previewChunkDelay = 100 * time.Millisecond
|
||||
previewDelayed = 500 * time.Millisecond
|
||||
maxPatternLength = 300
|
||||
maxPatternLength = 1000
|
||||
maxMulti = math.MaxInt32
|
||||
|
||||
// Background processes
|
||||
maxBgProcesses = 30
|
||||
maxBgProcessesPerAction = 3
|
||||
|
||||
// Matcher
|
||||
numPartitionsMultiplier = 8
|
||||
maxPartitions = 32
|
||||
@@ -54,16 +58,6 @@ const (
|
||||
defaultJumpLabels string = "asdfghjklqwertyuiopzxcvbnm1234567890ASDFGHJKLQWERTYUIOPZXCVBNM`~;:,<.>/?'\"!@#$%^&*()[{]}-_=+"
|
||||
)
|
||||
|
||||
var defaultCommand string
|
||||
|
||||
func init() {
|
||||
if !util.IsWindows() {
|
||||
defaultCommand = `set -o pipefail; command find -L . -mindepth 1 \( -path '*/\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \) -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-`
|
||||
} else if os.Getenv("TERM") == "cygwin" {
|
||||
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/\.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
|
||||
}
|
||||
}
|
||||
|
||||
// fzf events
|
||||
const (
|
||||
EvtReadNew util.EventType = iota
|
||||
@@ -77,9 +71,9 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
exitCancel = -1
|
||||
exitOk = 0
|
||||
exitNoMatch = 1
|
||||
exitError = 2
|
||||
exitInterrupt = 130
|
||||
ExitOk = 0
|
||||
ExitNoMatch = 1
|
||||
ExitError = 2
|
||||
ExitBecome = 126
|
||||
ExitInterrupt = 130
|
||||
)
|
||||
|
||||
406
src/core.go
406
src/core.go
@@ -1,35 +1,12 @@
|
||||
/*
|
||||
Package fzf implements fzf, a command-line fuzzy finder.
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013-2021 Junegunn Choi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
// Package fzf implements fzf, a command-line fuzzy finder.
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
@@ -42,20 +19,52 @@ Matcher -> EvtSearchFin -> Terminal (update list)
|
||||
Matcher -> EvtHeader -> Terminal (update header)
|
||||
*/
|
||||
|
||||
type revision struct {
|
||||
major int
|
||||
minor int
|
||||
}
|
||||
|
||||
func (r *revision) bumpMajor() {
|
||||
r.major++
|
||||
r.minor = 0
|
||||
}
|
||||
|
||||
func (r *revision) bumpMinor() {
|
||||
r.minor++
|
||||
}
|
||||
|
||||
func (r revision) compatible(other revision) bool {
|
||||
return r.major == other.major
|
||||
}
|
||||
|
||||
// Run starts fzf
|
||||
func Run(opts *Options, version string, revision string) {
|
||||
func Run(opts *Options) (int, error) {
|
||||
if opts.Filter == nil {
|
||||
if opts.useTmux() {
|
||||
return runTmux(os.Args, opts)
|
||||
}
|
||||
|
||||
if needWinpty(opts) {
|
||||
return runWinpty(os.Args, opts)
|
||||
}
|
||||
}
|
||||
|
||||
if err := postProcessOptions(opts); err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
|
||||
defer util.RunAtExitFuncs()
|
||||
|
||||
// Output channel given
|
||||
if opts.Output != nil {
|
||||
opts.Printer = func(str string) {
|
||||
opts.Output <- str
|
||||
}
|
||||
}
|
||||
|
||||
sort := opts.Sort > 0
|
||||
sortCriteria = opts.Criteria
|
||||
|
||||
if opts.Version {
|
||||
if len(revision) > 0 {
|
||||
fmt.Printf("%s (%s)\n", version, revision)
|
||||
} else {
|
||||
fmt.Println(version)
|
||||
}
|
||||
os.Exit(exitOk)
|
||||
}
|
||||
|
||||
// Event channel
|
||||
eventBox := util.NewEventBox()
|
||||
|
||||
@@ -66,31 +75,36 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
var lineAnsiState, prevLineAnsiState *ansiState
|
||||
if opts.Ansi {
|
||||
if opts.Theme.Colored {
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
return util.ToChars([]byte(trimmed)), offsets
|
||||
}
|
||||
} else {
|
||||
// When color is disabled but ansi option is given,
|
||||
// we simply strip out ANSI codes from the input
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
trimmed, _, _ := extractColor(string(data), nil, nil)
|
||||
return util.ToChars([]byte(trimmed)), nil
|
||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||
prevLineAnsiState = lineAnsiState
|
||||
trimmed, offsets, newState := extractColor(byteString(data), lineAnsiState, nil)
|
||||
lineAnsiState = newState
|
||||
|
||||
// Full line background is found. Add a special marker.
|
||||
if offsets != nil && newState != nil && len(*offsets) > 0 && newState.lbg >= 0 {
|
||||
marker := (*offsets)[len(*offsets)-1]
|
||||
marker.offset[0] = marker.offset[1]
|
||||
marker.color.bg = newState.lbg
|
||||
marker.color.attr = marker.color.attr | tui.FullBg
|
||||
newOffsets := append(*offsets, marker)
|
||||
offsets = &newOffsets
|
||||
|
||||
// Reset the full-line background color
|
||||
lineAnsiState.lbg = -1
|
||||
}
|
||||
return util.ToChars(stringBytes(trimmed)), offsets
|
||||
}
|
||||
}
|
||||
|
||||
// Chunk list
|
||||
cache := NewChunkCache()
|
||||
var chunkList *ChunkList
|
||||
var itemIndex int32
|
||||
header := make([]string, 0, opts.HeaderLines)
|
||||
if len(opts.WithNth) == 0 {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
if opts.WithNth == nil {
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, string(data))
|
||||
header = append(header, byteString(data))
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
@@ -100,9 +114,10 @@ func Run(opts *Options, version string, revision string) {
|
||||
return true
|
||||
})
|
||||
} else {
|
||||
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(string(data), opts.Delimiter)
|
||||
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||
nthTransformer := opts.WithNth(opts.Delimiter)
|
||||
chunkList = NewChunkList(cache, func(item *Item, data []byte) bool {
|
||||
tokens := Tokenize(byteString(data), opts.Delimiter)
|
||||
if opts.Ansi && len(tokens) > 1 {
|
||||
var ansiState *ansiState
|
||||
if prevLineAnsiState != nil {
|
||||
ansiStateDup := *prevLineAnsiState
|
||||
@@ -118,15 +133,24 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
}
|
||||
}
|
||||
trans := Transform(tokens, opts.WithNth)
|
||||
transformed := joinTokens(trans)
|
||||
transformed := nthTransformer(tokens, itemIndex)
|
||||
if len(header) < opts.HeaderLines {
|
||||
header = append(header, transformed)
|
||||
eventBox.Set(EvtHeader, header)
|
||||
return false
|
||||
}
|
||||
item.text, item.colors = ansiProcessor([]byte(transformed))
|
||||
item.text.TrimTrailingWhitespaces()
|
||||
item.text, item.colors = ansiProcessor(stringBytes(transformed))
|
||||
|
||||
// We should not trim trailing whitespaces with background colors
|
||||
var maxColorOffset int32
|
||||
if item.colors != nil {
|
||||
for _, ansi := range *item.colors {
|
||||
if ansi.color.bg >= 0 {
|
||||
maxColorOffset = util.Max32(maxColorOffset, ansi.offset[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
item.text.TrimTrailingWhitespaces(int(maxColorOffset))
|
||||
item.text.Index = itemIndex
|
||||
item.origText = &data
|
||||
itemIndex++
|
||||
@@ -134,33 +158,83 @@ func Run(opts *Options, version string, revision string) {
|
||||
})
|
||||
}
|
||||
|
||||
// Process executor
|
||||
executor := util.NewExecutor(opts.WithShell)
|
||||
|
||||
// Terminal I/O
|
||||
var terminal *Terminal
|
||||
var err error
|
||||
var initialEnv []string
|
||||
initialReload := opts.extractReloadOnStart()
|
||||
if opts.Filter == nil {
|
||||
terminal, err = NewTerminal(opts, eventBox, executor)
|
||||
if err != nil {
|
||||
return ExitError, err
|
||||
}
|
||||
if len(initialReload) > 0 {
|
||||
var temps []string
|
||||
initialReload, temps = terminal.replacePlaceholderInInitialCommand(initialReload)
|
||||
initialEnv = terminal.environ()
|
||||
defer removeFiles(temps)
|
||||
}
|
||||
}
|
||||
|
||||
// Reader
|
||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||
var reader *Reader
|
||||
if !streamingFilter {
|
||||
reader = NewReader(func(data []byte) bool {
|
||||
return chunkList.Push(data)
|
||||
}, eventBox, opts.ReadZero, opts.Filter == nil)
|
||||
go reader.ReadSource()
|
||||
}, eventBox, executor, opts.ReadZero, opts.Filter == nil)
|
||||
|
||||
readyChan := make(chan bool)
|
||||
go reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
|
||||
// Matcher
|
||||
forward := true
|
||||
for _, cri := range opts.Criteria[1:] {
|
||||
if cri == byEnd {
|
||||
withPos := false
|
||||
for idx := len(opts.Criteria) - 1; idx > 0; idx-- {
|
||||
switch opts.Criteria[idx] {
|
||||
case byChunk:
|
||||
withPos = true
|
||||
case byEnd:
|
||||
forward = false
|
||||
case byBegin:
|
||||
forward = true
|
||||
case byPathname:
|
||||
withPos = true
|
||||
forward = false
|
||||
break
|
||||
}
|
||||
if cri == byBegin {
|
||||
break
|
||||
}
|
||||
|
||||
nth := opts.Nth
|
||||
inputRevision := revision{}
|
||||
snapshotRevision := revision{}
|
||||
patternCache := make(map[string]*Pattern)
|
||||
denyMutex := sync.Mutex{}
|
||||
denylist := make(map[int32]struct{})
|
||||
clearDenylist := func() {
|
||||
denyMutex.Lock()
|
||||
if len(denylist) > 0 {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
denylist = make(map[int32]struct{})
|
||||
denyMutex.Unlock()
|
||||
}
|
||||
patternBuilder := func(runes []rune) *Pattern {
|
||||
return BuildPattern(
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
|
||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||
denyMutex.Lock()
|
||||
denylistCopy := make(map[int32]struct{})
|
||||
for k, v := range denylist {
|
||||
denylistCopy[k] = v
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
return BuildPattern(cache, patternCache,
|
||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||
opts.Filter == nil, nth, opts.Delimiter, inputRevision, runes, denylistCopy)
|
||||
}
|
||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
||||
matcher := NewMatcher(cache, patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||
|
||||
// Filtering mode
|
||||
if opts.Filter != nil {
|
||||
@@ -174,23 +248,27 @@ func Run(opts *Options, version string, revision string) {
|
||||
found := false
|
||||
if streamingFilter {
|
||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||
mutex := sync.Mutex{}
|
||||
reader := NewReader(
|
||||
func(runes []byte) bool {
|
||||
item := Item{}
|
||||
if chunkList.trans(&item, runes) {
|
||||
mutex.Lock()
|
||||
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||
opts.Printer(item.text.ToString())
|
||||
found = true
|
||||
}
|
||||
mutex.Unlock()
|
||||
}
|
||||
return false
|
||||
}, eventBox, opts.ReadZero, false)
|
||||
reader.ReadSource()
|
||||
}, eventBox, executor, opts.ReadZero, false)
|
||||
reader.ReadSource(opts.Input, opts.WalkerRoot, opts.WalkerOpts, opts.WalkerSkip, initialReload, initialEnv, nil)
|
||||
} else {
|
||||
eventBox.Unwatch(EvtReadNew)
|
||||
eventBox.WaitFor(EvtReadFin)
|
||||
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
// NOTE: Streaming filter is inherently not compatible with --tail
|
||||
snapshot, _, _ := chunkList.Snapshot(opts.Tail)
|
||||
merger, _ := matcher.scan(MatchRequest{
|
||||
chunks: snapshot,
|
||||
pattern: pattern})
|
||||
@@ -200,9 +278,9 @@ func Run(opts *Options, version string, revision string) {
|
||||
}
|
||||
}
|
||||
if found {
|
||||
os.Exit(exitOk)
|
||||
return ExitOk, nil
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
return ExitNoMatch, nil
|
||||
}
|
||||
|
||||
// Synchronous search
|
||||
@@ -213,32 +291,64 @@ func Run(opts *Options, version string, revision string) {
|
||||
|
||||
// Go interactive
|
||||
go matcher.Loop()
|
||||
defer matcher.Stop()
|
||||
|
||||
// Terminal I/O
|
||||
terminal := NewTerminal(opts, eventBox)
|
||||
deferred := opts.Select1 || opts.Exit0
|
||||
// Handling adaptive height
|
||||
maxFit := 0 // Maximum number of items that can fit on screen
|
||||
padHeight := 0
|
||||
heightUnknown := opts.Height.auto
|
||||
if heightUnknown {
|
||||
maxFit, padHeight = terminal.MaxFitAndPad()
|
||||
}
|
||||
deferred := opts.Select1 || opts.Exit0 || opts.Sync
|
||||
go terminal.Loop()
|
||||
if !deferred {
|
||||
terminal.startChan <- true
|
||||
if !deferred && !heightUnknown {
|
||||
// Start right away
|
||||
terminal.startChan <- fitpad{-1, -1}
|
||||
}
|
||||
|
||||
// Event coordination
|
||||
reading := true
|
||||
clearCache := util.Once(false)
|
||||
clearSelection := util.Once(false)
|
||||
ticks := 0
|
||||
var nextCommand *string
|
||||
restart := func(command string) {
|
||||
startTick := 0
|
||||
var nextCommand *commandSpec
|
||||
var nextEnviron []string
|
||||
eventBox.Watch(EvtReadNew)
|
||||
total := 0
|
||||
query := []rune{}
|
||||
determine := func(final bool) {
|
||||
if heightUnknown {
|
||||
if total >= maxFit || final {
|
||||
deferred = false
|
||||
heightUnknown = false
|
||||
terminal.startChan <- fitpad{util.Min(total, maxFit), padHeight}
|
||||
}
|
||||
} else if deferred {
|
||||
deferred = false
|
||||
terminal.startChan <- fitpad{-1, -1}
|
||||
}
|
||||
}
|
||||
|
||||
useSnapshot := false
|
||||
var snapshot []*Chunk
|
||||
var count int
|
||||
restart := func(command commandSpec, environ []string) {
|
||||
if !useSnapshot {
|
||||
clearDenylist()
|
||||
}
|
||||
reading = true
|
||||
clearCache = util.Once(true)
|
||||
clearSelection = util.Once(true)
|
||||
startTick = ticks
|
||||
chunkList.Clear()
|
||||
itemIndex = 0
|
||||
inputRevision.bumpMajor()
|
||||
header = make([]string, 0, opts.HeaderLines)
|
||||
go reader.restart(command)
|
||||
readyChan := make(chan bool)
|
||||
go reader.restart(command, environ, readyChan)
|
||||
<-readyChan
|
||||
}
|
||||
eventBox.Watch(EvtReadNew)
|
||||
query := []rune{}
|
||||
|
||||
exitCode := ExitOk
|
||||
stop := false
|
||||
for {
|
||||
delay := true
|
||||
ticks++
|
||||
@@ -259,41 +369,103 @@ func Run(opts *Options, version string, revision string) {
|
||||
if reading {
|
||||
reader.terminate()
|
||||
}
|
||||
os.Exit(value.(int))
|
||||
quitSignal := value.(quitSignal)
|
||||
exitCode = quitSignal.code
|
||||
err = quitSignal.err
|
||||
stop = true
|
||||
return
|
||||
case EvtReadNew, EvtReadFin:
|
||||
if evt == EvtReadFin && nextCommand != nil {
|
||||
restart(*nextCommand)
|
||||
restart(*nextCommand, nextEnviron)
|
||||
nextCommand = nil
|
||||
nextEnviron = nil
|
||||
break
|
||||
} else {
|
||||
reading = reading && evt == EvtReadNew
|
||||
}
|
||||
snapshot, count := chunkList.Snapshot()
|
||||
terminal.UpdateCount(count, !reading, value.(*string))
|
||||
if opts.Sync {
|
||||
opts.Sync = false
|
||||
terminal.UpdateList(PassMerger(&snapshot, opts.Tac), false)
|
||||
if useSnapshot && evt == EvtReadFin { // reload-sync
|
||||
clearDenylist()
|
||||
useSnapshot = false
|
||||
}
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, clearCache())
|
||||
if !useSnapshot {
|
||||
if !snapshotRevision.compatible(inputRevision) {
|
||||
query = []rune{}
|
||||
}
|
||||
var changed bool
|
||||
snapshot, count, changed = chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
total = count
|
||||
terminal.UpdateCount(total, !reading, value.(*string))
|
||||
if heightUnknown && !deferred {
|
||||
determine(!reading)
|
||||
}
|
||||
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||
|
||||
case EvtSearchNew:
|
||||
var command *string
|
||||
var command *commandSpec
|
||||
var environ []string
|
||||
var changed bool
|
||||
switch val := value.(type) {
|
||||
case searchRequest:
|
||||
sort = val.sort
|
||||
command = val.command
|
||||
environ = val.environ
|
||||
changed = val.changed
|
||||
bump := false
|
||||
if len(val.denylist) > 0 && val.revision.compatible(inputRevision) {
|
||||
denyMutex.Lock()
|
||||
for _, itemIndex := range val.denylist {
|
||||
denylist[itemIndex] = struct{}{}
|
||||
}
|
||||
denyMutex.Unlock()
|
||||
bump = true
|
||||
}
|
||||
if val.nth != nil {
|
||||
// Change nth and clear caches
|
||||
nth = *val.nth
|
||||
bump = true
|
||||
}
|
||||
if bump {
|
||||
patternCache = make(map[string]*Pattern)
|
||||
cache.Clear()
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
if command != nil {
|
||||
useSnapshot = val.sync
|
||||
}
|
||||
}
|
||||
if command != nil {
|
||||
if reading {
|
||||
reader.terminate()
|
||||
nextCommand = command
|
||||
nextEnviron = environ
|
||||
} else {
|
||||
restart(*command)
|
||||
restart(*command, environ)
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
break
|
||||
}
|
||||
snapshot, _ := chunkList.Snapshot()
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, clearCache())
|
||||
if !useSnapshot {
|
||||
newSnapshot, newCount, changed := chunkList.Snapshot(opts.Tail)
|
||||
if changed {
|
||||
inputRevision.bumpMinor()
|
||||
}
|
||||
// We want to avoid showing empty list when reload is triggered
|
||||
// and the query string is changed at the same time i.e. command != nil && changed
|
||||
if command == nil || newCount > 0 {
|
||||
if snapshotRevision != inputRevision {
|
||||
query = []rune{}
|
||||
}
|
||||
snapshot = newSnapshot
|
||||
snapshotRevision = inputRevision
|
||||
}
|
||||
}
|
||||
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||
delay = false
|
||||
|
||||
case EvtSearchProgress:
|
||||
@@ -313,8 +485,7 @@ func Run(opts *Options, version string, revision string) {
|
||||
if deferred {
|
||||
count := val.Length()
|
||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||
deferred = false
|
||||
terminal.startChan <- true
|
||||
determine(val.final)
|
||||
} else if val.final {
|
||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||
if opts.PrintQuery {
|
||||
@@ -323,29 +494,42 @@ func Run(opts *Options, version string, revision string) {
|
||||
if len(opts.Expect) > 0 {
|
||||
opts.Printer("")
|
||||
}
|
||||
transformer := func(item *Item) string {
|
||||
return item.AsString(opts.Ansi)
|
||||
}
|
||||
if opts.AcceptNth != nil {
|
||||
fn := opts.AcceptNth(opts.Delimiter)
|
||||
transformer = func(item *Item) string {
|
||||
return item.acceptNth(opts.Ansi, opts.Delimiter, fn)
|
||||
}
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
opts.Printer(val.Get(i).item.AsString(opts.Ansi))
|
||||
opts.Printer(transformer(val.Get(i).item))
|
||||
}
|
||||
if count > 0 {
|
||||
os.Exit(exitOk)
|
||||
if count == 0 {
|
||||
exitCode = ExitNoMatch
|
||||
}
|
||||
os.Exit(exitNoMatch)
|
||||
stop = true
|
||||
return
|
||||
}
|
||||
deferred = false
|
||||
terminal.startChan <- true
|
||||
determine(val.final)
|
||||
}
|
||||
}
|
||||
terminal.UpdateList(val, clearSelection())
|
||||
terminal.UpdateList(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
if delay && reading {
|
||||
dur := util.DurWithin(
|
||||
time.Duration(ticks)*coordinatorDelayStep,
|
||||
time.Duration(ticks-startTick)*coordinatorDelayStep,
|
||||
0, coordinatorDelayMax)
|
||||
time.Sleep(dur)
|
||||
}
|
||||
}
|
||||
return exitCode, err
|
||||
}
|
||||
|
||||
35
src/functions.go
Normal file
35
src/functions.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func WriteTemporaryFile(data []string, printSep string) string {
|
||||
f, err := os.CreateTemp("", "fzf-temp-*")
|
||||
if err != nil {
|
||||
// Unable to create temporary file
|
||||
// FIXME: Should we terminate the program?
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
f.WriteString(strings.Join(data, printSep))
|
||||
f.WriteString(printSep)
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
func removeFiles(files []string) {
|
||||
for _, filename := range files {
|
||||
os.Remove(filename)
|
||||
}
|
||||
}
|
||||
|
||||
func stringBytes(data string) []byte {
|
||||
return unsafe.Slice(unsafe.StringData(data), len(data))
|
||||
}
|
||||
|
||||
func byteString(data []byte) string {
|
||||
return unsafe.String(unsafe.SliceData(data), len(data))
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package fzf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -26,12 +25,12 @@ func NewHistory(path string, maxSize int) (*History, error) {
|
||||
}
|
||||
|
||||
// Read history file
|
||||
data, err := ioutil.ReadFile(path)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
// If it doesn't exist, check if we can create a file with the name
|
||||
if os.IsNotExist(err) {
|
||||
data = []byte{}
|
||||
if err := ioutil.WriteFile(path, data, 0600); err != nil {
|
||||
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||
return nil, fmtError(err)
|
||||
}
|
||||
} else {
|
||||
@@ -62,11 +61,11 @@ func (h *History) append(line string) error {
|
||||
lines = lines[len(lines)-h.maxSize:]
|
||||
}
|
||||
h.lines = append(lines, "")
|
||||
return ioutil.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
|
||||
return os.WriteFile(h.path, []byte(strings.Join(h.lines, "\n")), 0600)
|
||||
}
|
||||
|
||||
func (h *History) override(str string) {
|
||||
// You can update the history but they're not written to the file
|
||||
// You can update the history, but they're not written to the file
|
||||
if h.cursor == len(h.lines)-1 {
|
||||
h.lines[h.cursor] = str
|
||||
} else if h.cursor < len(h.lines)-1 {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
@@ -25,7 +24,7 @@ func TestHistory(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f, _ := os.CreateTemp("", "fzf-history")
|
||||
f.Close()
|
||||
|
||||
{ // Append lines
|
||||
|
||||
19
src/item.go
19
src/item.go
@@ -1,13 +1,22 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"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.
|
||||
type Item struct {
|
||||
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||
transformed *[]Token // 8
|
||||
transformed *transformed // 8
|
||||
origText *[]byte // 8
|
||||
colors *[]ansiOffset // 8
|
||||
}
|
||||
@@ -17,7 +26,7 @@ func (item *Item) Index() int32 {
|
||||
return item.text.Index
|
||||
}
|
||||
|
||||
var minItem = Item{text: util.Chars{Index: -1}}
|
||||
var minItem = Item{text: util.Chars{Index: math.MinInt32}}
|
||||
|
||||
func (item *Item) TrimLength() uint16 {
|
||||
return item.text.TrimLength()
|
||||
@@ -42,3 +51,9 @@ func (item *Item) AsString(stripAnsi bool) string {
|
||||
}
|
||||
return item.text.ToString()
|
||||
}
|
||||
|
||||
func (item *Item) acceptNth(stripAnsi bool, delimiter Delimiter, transformer func([]Token, int32) string) string {
|
||||
tokens := Tokenize(item.AsString(stripAnsi), delimiter)
|
||||
transformed := transformer(tokens, item.Index())
|
||||
return StripLastDelimiter(transformed, delimiter)
|
||||
}
|
||||
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
|
||||
// MatchRequest represents a search request
|
||||
type MatchRequest struct {
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
clearCache bool
|
||||
chunks []*Chunk
|
||||
pattern *Pattern
|
||||
final bool
|
||||
sort bool
|
||||
revision revision
|
||||
}
|
||||
|
||||
// Matcher is responsible for performing search
|
||||
type Matcher struct {
|
||||
cache *ChunkCache
|
||||
patternBuilder func([]rune) *Pattern
|
||||
sort bool
|
||||
tac bool
|
||||
@@ -29,6 +30,7 @@ type Matcher struct {
|
||||
partitions int
|
||||
slab []*util.Slab
|
||||
mergerCache map[string]*Merger
|
||||
revision revision
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -37,10 +39,11 @@ const (
|
||||
)
|
||||
|
||||
// NewMatcher returns a new Matcher
|
||||
func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
|
||||
func NewMatcher(cache *ChunkCache, patternBuilder func([]rune) *Pattern,
|
||||
sort bool, tac bool, eventBox *util.EventBox, revision revision) *Matcher {
|
||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||
return &Matcher{
|
||||
cache: cache,
|
||||
patternBuilder: patternBuilder,
|
||||
sort: sort,
|
||||
tac: tac,
|
||||
@@ -48,7 +51,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||
reqBox: util.NewEventBox(),
|
||||
partitions: partitions,
|
||||
slab: make([]*util.Slab, partitions),
|
||||
mergerCache: make(map[string]*Merger)}
|
||||
mergerCache: make(map[string]*Merger),
|
||||
revision: revision}
|
||||
}
|
||||
|
||||
// Loop puts Matcher in action
|
||||
@@ -58,8 +62,13 @@ func (m *Matcher) Loop() {
|
||||
for {
|
||||
var request MatchRequest
|
||||
|
||||
stop := false
|
||||
m.reqBox.Wait(func(events *util.Events) {
|
||||
for _, val := range *events {
|
||||
for t, val := range *events {
|
||||
if t == reqQuit {
|
||||
stop = true
|
||||
return
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case MatchRequest:
|
||||
request = val
|
||||
@@ -69,11 +78,19 @@ func (m *Matcher) Loop() {
|
||||
}
|
||||
events.Clear()
|
||||
})
|
||||
if stop {
|
||||
break
|
||||
}
|
||||
|
||||
if request.sort != m.sort || request.clearCache {
|
||||
cacheCleared := false
|
||||
if request.sort != m.sort || request.revision != m.revision {
|
||||
m.sort = request.sort
|
||||
m.revision = request.revision
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
clearChunkCache()
|
||||
if !request.revision.compatible(m.revision) {
|
||||
m.cache.Clear()
|
||||
}
|
||||
cacheCleared = true
|
||||
}
|
||||
|
||||
// Restart search
|
||||
@@ -82,20 +99,20 @@ func (m *Matcher) Loop() {
|
||||
cancelled := false
|
||||
count := CountItems(request.chunks)
|
||||
|
||||
foundCache := false
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found {
|
||||
foundCache = true
|
||||
merger = cached
|
||||
if !cacheCleared {
|
||||
if count == prevCount {
|
||||
// Look up mergerCache
|
||||
if cached, found := m.mergerCache[patternString]; found && cached.final == request.final {
|
||||
merger = cached
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
} else {
|
||||
// Invalidate mergerCache
|
||||
prevCount = count
|
||||
m.mergerCache = make(map[string]*Merger)
|
||||
}
|
||||
|
||||
if !foundCache {
|
||||
if merger == nil {
|
||||
merger, cancelled = m.scan(request)
|
||||
}
|
||||
|
||||
@@ -140,13 +157,15 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
|
||||
numChunks := len(request.chunks)
|
||||
if numChunks == 0 {
|
||||
return EmptyMerger, false
|
||||
return EmptyMerger(request.revision), false
|
||||
}
|
||||
pattern := request.pattern
|
||||
if pattern.IsEmpty() {
|
||||
return PassMerger(&request.chunks, m.tac), false
|
||||
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||
}
|
||||
|
||||
minIndex := request.chunks[0].items[0].Index()
|
||||
maxIndex := request.chunks[numChunks-1].lastIndex(minIndex)
|
||||
cancelled := util.NewAtomicBool(false)
|
||||
|
||||
slices := m.sliceChunks(request.chunks)
|
||||
@@ -177,7 +196,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
for _, matches := range allMatches {
|
||||
sliceMatches = append(sliceMatches, matches...)
|
||||
}
|
||||
if m.sort {
|
||||
if m.sort && request.pattern.sortable {
|
||||
if m.tac {
|
||||
sort.Sort(ByRelevanceTac(sliceMatches))
|
||||
} else {
|
||||
@@ -218,11 +237,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||
partialResult := <-resultChan
|
||||
partialResults[partialResult.index] = partialResult.matches
|
||||
}
|
||||
return NewMerger(pattern, partialResults, m.sort, m.tac), false
|
||||
return NewMerger(pattern, partialResults, m.sort && request.pattern.sortable, m.tac, request.revision, minIndex, maxIndex), false
|
||||
}
|
||||
|
||||
// Reset is called to interrupt/signal the ongoing search
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, clearCache bool) {
|
||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision revision) {
|
||||
pattern := m.patternBuilder(patternRunes)
|
||||
|
||||
var event util.EventType
|
||||
@@ -231,5 +250,9 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
||||
} else {
|
||||
event = reqRetry
|
||||
}
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, clearCache})
|
||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort, revision})
|
||||
}
|
||||
|
||||
func (m *Matcher) Stop() {
|
||||
m.reqBox.Set(reqQuit, nil)
|
||||
}
|
||||
|
||||
106
src/merger.go
106
src/merger.go
@@ -3,30 +3,45 @@ package fzf
|
||||
import "fmt"
|
||||
|
||||
// EmptyMerger is a Merger with no data
|
||||
var EmptyMerger = NewMerger(nil, [][]Result{}, false, false)
|
||||
func EmptyMerger(revision revision) *Merger {
|
||||
return NewMerger(nil, [][]Result{}, false, false, revision, 0, 0)
|
||||
}
|
||||
|
||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||
// a single, globally-sorted list
|
||||
type Merger struct {
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pattern *Pattern
|
||||
lists [][]Result
|
||||
merged []Result
|
||||
chunks *[]*Chunk
|
||||
cursors []int
|
||||
sorted bool
|
||||
tac bool
|
||||
final bool
|
||||
count int
|
||||
pass bool
|
||||
revision revision
|
||||
minIndex int32
|
||||
maxIndex int32
|
||||
}
|
||||
|
||||
// PassMerger returns a new Merger that simply returns the items in the
|
||||
// original order
|
||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
func PassMerger(chunks *[]*Chunk, tac bool, revision revision) *Merger {
|
||||
var minIndex, maxIndex int32
|
||||
if len(*chunks) > 0 {
|
||||
minIndex = (*chunks)[0].items[0].Index()
|
||||
maxIndex = (*chunks)[len(*chunks)-1].lastIndex(minIndex)
|
||||
}
|
||||
mg := Merger{
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0}
|
||||
pattern: nil,
|
||||
chunks: chunks,
|
||||
tac: tac,
|
||||
count: 0,
|
||||
pass: true,
|
||||
revision: revision,
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, chunk := range *mg.chunks {
|
||||
mg.count += chunk.count
|
||||
@@ -35,17 +50,20 @@ func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
||||
}
|
||||
|
||||
// NewMerger returns a new Merger
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merger {
|
||||
func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool, revision revision, minIndex int32, maxIndex int32) *Merger {
|
||||
mg := Merger{
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0}
|
||||
pattern: pattern,
|
||||
lists: lists,
|
||||
merged: []Result{},
|
||||
chunks: nil,
|
||||
cursors: make([]int, len(lists)),
|
||||
sorted: sorted,
|
||||
tac: tac,
|
||||
final: false,
|
||||
count: 0,
|
||||
revision: revision,
|
||||
minIndex: minIndex,
|
||||
maxIndex: maxIndex}
|
||||
|
||||
for _, list := range mg.lists {
|
||||
mg.count += len(list)
|
||||
@@ -53,17 +71,55 @@ func NewMerger(pattern *Pattern, lists [][]Result, sorted bool, tac bool) *Merge
|
||||
return &mg
|
||||
}
|
||||
|
||||
// Revision returns revision number
|
||||
func (mg *Merger) Revision() revision {
|
||||
return mg.revision
|
||||
}
|
||||
|
||||
// Length returns the number of items
|
||||
func (mg *Merger) Length() int {
|
||||
return mg.count
|
||||
}
|
||||
|
||||
func (mg *Merger) First() Result {
|
||||
if mg.tac && !mg.sorted {
|
||||
return mg.Get(mg.count - 1)
|
||||
}
|
||||
return mg.Get(0)
|
||||
}
|
||||
|
||||
// FindIndex returns the index of the item with the given item index
|
||||
func (mg *Merger) FindIndex(itemIndex int32) int {
|
||||
index := -1
|
||||
if mg.pass {
|
||||
index = int(itemIndex - mg.minIndex)
|
||||
if mg.tac {
|
||||
index = mg.count - index - 1
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < mg.count; i++ {
|
||||
if mg.Get(i).item.Index() == itemIndex {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
// Get returns the pointer to the Result object indexed by the given integer
|
||||
func (mg *Merger) Get(idx int) Result {
|
||||
if mg.chunks != nil {
|
||||
if mg.tac {
|
||||
idx = mg.count - idx - 1
|
||||
}
|
||||
firstChunk := (*mg.chunks)[0]
|
||||
if firstChunk.count < chunkSize && idx >= firstChunk.count {
|
||||
idx -= firstChunk.count
|
||||
|
||||
chunk := (*mg.chunks)[idx/chunkSize+1]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
chunk := (*mg.chunks)[idx/chunkSize]
|
||||
return Result{item: &chunk.items[idx%chunkSize]}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,11 @@ func randResult() Result {
|
||||
}
|
||||
|
||||
func TestEmptyMerger(t *testing.T) {
|
||||
assert(t, EmptyMerger.Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger.count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
||||
r := revision{}
|
||||
assert(t, EmptyMerger(r).Length() == 0, "Not empty")
|
||||
assert(t, EmptyMerger(r).count == 0, "Invalid count")
|
||||
assert(t, len(EmptyMerger(r).lists) == 0, "Invalid lists")
|
||||
assert(t, len(EmptyMerger(r).merged) == 0, "Invalid merged list")
|
||||
}
|
||||
|
||||
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||
@@ -57,7 +58,7 @@ func TestMergerUnsorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Not sorted: same order
|
||||
mg := NewMerger(nil, lists, false, false)
|
||||
mg := NewMerger(nil, lists, false, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
for i := 0; i < cnt; i++ {
|
||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||
@@ -69,7 +70,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
cnt := len(items)
|
||||
|
||||
// Sorted sorted order
|
||||
mg := NewMerger(nil, lists, true, false)
|
||||
mg := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||
sort.Sort(ByRelevance(items))
|
||||
for i := 0; i < cnt; i++ {
|
||||
@@ -79,7 +80,7 @@ func TestMergerSorted(t *testing.T) {
|
||||
}
|
||||
|
||||
// Inverse order
|
||||
mg2 := NewMerger(nil, lists, true, false)
|
||||
mg2 := NewMerger(nil, lists, true, false, revision{}, 0, 0)
|
||||
for i := cnt - 1; i >= 0; i-- {
|
||||
if items[i] != mg2.Get(i) {
|
||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||
|
||||
3679
src/options.go
3679
src/options.go
File diff suppressed because it is too large
Load Diff
13
src/options_no_pprof.go
Normal file
13
src/options_no_pprof.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build !pprof
|
||||
// +build !pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import "errors"
|
||||
|
||||
func (o *Options) initProfiling() error {
|
||||
if o.CPUProfile != "" || o.MEMProfile != "" || o.BlockProfile != "" || o.MutexProfile != "" {
|
||||
return errors.New("error: profiling not supported: FZF must be built with '-tags=pprof' to enable profiling")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
73
src/options_pprof.go
Normal file
73
src/options_pprof.go
Normal file
@@ -0,0 +1,73 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func (o *Options) initProfiling() error {
|
||||
if o.CPUProfile != "" {
|
||||
f, err := os.Create(o.CPUProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create CPU profile: %w", err)
|
||||
}
|
||||
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
return fmt.Errorf("could not start CPU profile: %w", err)
|
||||
}
|
||||
|
||||
util.AtExit(func() {
|
||||
pprof.StopCPUProfile()
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "Error: closing cpu profile:", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
stopProfile := func(name string, f *os.File) {
|
||||
if err := pprof.Lookup(name).WriteTo(f, 0); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: could not write %s profile: %v\n", name, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: closing %s profile: %v\n", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if o.MEMProfile != "" {
|
||||
f, err := os.Create(o.MEMProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create MEM profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() {
|
||||
runtime.GC()
|
||||
stopProfile("allocs", f)
|
||||
})
|
||||
}
|
||||
|
||||
if o.BlockProfile != "" {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
f, err := os.Create(o.BlockProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create BLOCK profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() { stopProfile("block", f) })
|
||||
}
|
||||
|
||||
if o.MutexProfile != "" {
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
f, err := os.Create(o.MutexProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create MUTEX profile: %w", err)
|
||||
}
|
||||
util.AtExit(func() { stopProfile("mutex", f) })
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
89
src/options_pprof_test.go
Normal file
89
src/options_pprof_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
//go:build pprof
|
||||
// +build pprof
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
// runInitProfileTests is an internal flag used TestInitProfiling
|
||||
var runInitProfileTests = flag.Bool("test-init-profile", false, "run init profile tests")
|
||||
|
||||
func TestInitProfiling(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("short test")
|
||||
}
|
||||
|
||||
// Run this test in a separate process since it interferes with
|
||||
// profiling and modifies the global atexit state. Without this
|
||||
// running `go test -bench . -cpuprofile cpu.out` will fail.
|
||||
if !*runInitProfileTests {
|
||||
t.Parallel()
|
||||
|
||||
// Make sure we are not the child process.
|
||||
if os.Getenv("_FZF_CHILD_PROC") != "" {
|
||||
t.Fatal("already running as child process!")
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0],
|
||||
"-test.timeout", "30s",
|
||||
"-test.run", "^"+t.Name()+"$",
|
||||
"-test-init-profile",
|
||||
)
|
||||
cmd.Env = append(os.Environ(), "_FZF_CHILD_PROC=1")
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
out = bytes.TrimSpace(out)
|
||||
if err != nil {
|
||||
t.Fatalf("Child test process failed: %v:\n%s", err, out)
|
||||
}
|
||||
// Make sure the test actually ran
|
||||
if bytes.Contains(out, []byte("no tests to run")) {
|
||||
t.Fatalf("Failed to run test %q:\n%s", t.Name(), out)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Child process
|
||||
|
||||
tempdir := t.TempDir()
|
||||
t.Cleanup(util.RunAtExitFuncs)
|
||||
|
||||
o := Options{
|
||||
CPUProfile: filepath.Join(tempdir, "cpu.prof"),
|
||||
MEMProfile: filepath.Join(tempdir, "mem.prof"),
|
||||
BlockProfile: filepath.Join(tempdir, "block.prof"),
|
||||
MutexProfile: filepath.Join(tempdir, "mutex.prof"),
|
||||
}
|
||||
if err := o.initProfiling(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
profiles := []string{
|
||||
o.CPUProfile,
|
||||
o.MEMProfile,
|
||||
o.BlockProfile,
|
||||
o.MutexProfile,
|
||||
}
|
||||
for _, name := range profiles {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Errorf("Failed to create profile %s: %v", filepath.Base(name), err)
|
||||
}
|
||||
}
|
||||
|
||||
util.RunAtExitFuncs()
|
||||
|
||||
for _, name := range profiles {
|
||||
if _, err := os.Stat(name); err != nil {
|
||||
t.Errorf("Failed to write profile %s: %v", filepath.Base(name), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,20 @@ package fzf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/junegunn/fzf/src/tui"
|
||||
)
|
||||
|
||||
func TestDelimiterRegex(t *testing.T) {
|
||||
// Valid regex
|
||||
// Valid regex, but a single character -> string
|
||||
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)
|
||||
}
|
||||
// Broken regex -> string
|
||||
@@ -65,9 +69,22 @@ func TestDelimiterRegexRegex(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelimiterRegexRegexCaret(t *testing.T) {
|
||||
delim := delimiterRegexp(`(^\s*|\s+)`)
|
||||
tokens := Tokenize("foo bar baz", delim)
|
||||
if delim.str != nil ||
|
||||
len(tokens) != 4 ||
|
||||
tokens[0].text.ToString() != "" ||
|
||||
tokens[1].text.ToString() != "foo " ||
|
||||
tokens[2].text.ToString() != "bar " ||
|
||||
tokens[3].text.ToString() != "baz" {
|
||||
t.Errorf("%s %d", tokens, len(tokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitNth(t *testing.T) {
|
||||
{
|
||||
ranges := splitNth("..")
|
||||
ranges, _ := splitNth("..")
|
||||
if len(ranges) != 1 ||
|
||||
ranges[0].begin != rangeEllipsis ||
|
||||
ranges[0].end != rangeEllipsis {
|
||||
@@ -75,7 +92,7 @@ func TestSplitNth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
|
||||
ranges, _ := splitNth("..3,1..,2..3,4..-1,-3..-2,..,2,-2,2..-2,1..-1")
|
||||
if len(ranges) != 10 ||
|
||||
ranges[0].begin != rangeEllipsis || ranges[0].end != 3 ||
|
||||
ranges[1].begin != rangeEllipsis || ranges[1].end != rangeEllipsis ||
|
||||
@@ -93,10 +110,11 @@ func TestSplitNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIrrelevantNth(t *testing.T) {
|
||||
index := 0
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words := []string{"--nth", "..", "-x"}
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -105,7 +123,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 0 {
|
||||
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||
@@ -114,7 +132,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
{
|
||||
opts := defaultOptions()
|
||||
words = append(words, "-x")
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if len(opts.Nth) != 2 {
|
||||
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||
@@ -124,7 +142,7 @@ func TestIrrelevantNth(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseKeys(t *testing.T) {
|
||||
pairs := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
pairs, _, _ := parseKeyChords("ctrl-z,alt-z,f2,@,Alt-a,!,ctrl-G,J,g,ctrl-alt-a,ALT-enter,alt-SPACE", "")
|
||||
checkEvent := func(e tui.Event, s string) {
|
||||
if pairs[e] != s {
|
||||
t.Errorf("%s != %s", pairs[e], s)
|
||||
@@ -150,35 +168,35 @@ func TestParseKeys(t *testing.T) {
|
||||
checkEvent(tui.AltKey(' '), "alt-SPACE")
|
||||
|
||||
// Synonyms
|
||||
pairs = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
pairs, _, _ = parseKeyChords("enter,Return,space,tab,btab,esc,up,down,left,right", "")
|
||||
if len(pairs) != 9 {
|
||||
t.Error(9)
|
||||
}
|
||||
check(tui.CtrlM, "Return")
|
||||
check(tui.Enter, "Return")
|
||||
checkEvent(tui.Key(' '), "space")
|
||||
check(tui.Tab, "tab")
|
||||
check(tui.BTab, "btab")
|
||||
check(tui.ESC, "esc")
|
||||
check(tui.ShiftTab, "btab")
|
||||
check(tui.Esc, "esc")
|
||||
check(tui.Up, "up")
|
||||
check(tui.Down, "down")
|
||||
check(tui.Left, "left")
|
||||
check(tui.Right, "right")
|
||||
|
||||
pairs = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
pairs, _, _ = parseKeyChords("Tab,Ctrl-I,PgUp,page-up,pgdn,Page-Down,Home,End,Alt-BS,Alt-BSpace,shift-left,shift-right,btab,shift-tab,return,Enter,bspace", "")
|
||||
if len(pairs) != 11 {
|
||||
t.Error(11)
|
||||
}
|
||||
check(tui.Tab, "Ctrl-I")
|
||||
check(tui.PgUp, "page-up")
|
||||
check(tui.PgDn, "Page-Down")
|
||||
check(tui.PageUp, "page-up")
|
||||
check(tui.PageDown, "Page-Down")
|
||||
check(tui.Home, "Home")
|
||||
check(tui.End, "End")
|
||||
check(tui.AltBS, "Alt-BSpace")
|
||||
check(tui.SLeft, "shift-left")
|
||||
check(tui.SRight, "shift-right")
|
||||
check(tui.BTab, "shift-tab")
|
||||
check(tui.CtrlM, "Enter")
|
||||
check(tui.BSpace, "bspace")
|
||||
check(tui.AltBackspace, "Alt-BSpace")
|
||||
check(tui.ShiftLeft, "shift-left")
|
||||
check(tui.ShiftRight, "shift-right")
|
||||
check(tui.ShiftTab, "shift-tab")
|
||||
check(tui.Enter, "Enter")
|
||||
check(tui.Backspace, "bspace")
|
||||
}
|
||||
|
||||
func TestParseKeysWithComma(t *testing.T) {
|
||||
@@ -193,40 +211,40 @@ func TestParseKeysWithComma(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
pairs := parseKeyChords(",", "")
|
||||
pairs, _, _ := parseKeyChords(",", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords(",,a,b", "")
|
||||
pairs, _, _ = parseKeyChords(",,a,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords("a,b,,", "")
|
||||
pairs, _, _ = parseKeyChords("a,b,,", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords("a,,,b", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b", "")
|
||||
checkN(len(pairs), 3)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords("a,,,b,c", "")
|
||||
pairs, _, _ = parseKeyChords("a,,,b,c", "")
|
||||
checkN(len(pairs), 4)
|
||||
check(pairs, tui.Key('a'), "a")
|
||||
check(pairs, tui.Key('b'), "b")
|
||||
check(pairs, tui.Key('c'), "c")
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords(",,,", "")
|
||||
pairs, _, _ = parseKeyChords(",,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.Key(','), ",")
|
||||
|
||||
pairs = parseKeyChords(",ALT-,,", "")
|
||||
pairs, _, _ = parseKeyChords(",ALT-,,", "")
|
||||
checkN(len(pairs), 1)
|
||||
check(pairs, tui.AltKey(','), "ALT-,")
|
||||
}
|
||||
@@ -251,7 +269,7 @@ func TestBind(t *testing.T) {
|
||||
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
|
||||
parseKeymap(keymap,
|
||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo {+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"f1:execute(ls {+})+abort+execute(echo \n{+})+select-all,f2:execute/echo {}, {}, {}/,f3:execute[echo '({})'],f4:execute;less {};,"+
|
||||
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||
",f1:+first,f1:+top"+
|
||||
@@ -283,7 +301,7 @@ func TestBind(t *testing.T) {
|
||||
|
||||
func TestColorSpec(t *testing.T) {
|
||||
theme := tui.Dark256
|
||||
dark := parseTheme(theme, "dark")
|
||||
dark, _ := parseTheme(theme, "dark")
|
||||
if *dark != *theme {
|
||||
t.Errorf("colors should be equivalent")
|
||||
}
|
||||
@@ -291,7 +309,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
light := parseTheme(theme, "dark,light")
|
||||
light, _ := parseTheme(theme, "dark,light")
|
||||
if *light == *theme {
|
||||
t.Errorf("should not be equivalent")
|
||||
}
|
||||
@@ -302,7 +320,7 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("point should not be equivalent")
|
||||
}
|
||||
|
||||
customized := parseTheme(theme, "fg:231,bg:232")
|
||||
customized, _ := parseTheme(theme, "fg:231,bg:232")
|
||||
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
@@ -315,17 +333,18 @@ func TestColorSpec(t *testing.T) {
|
||||
t.Errorf("colors should now be equivalent: %v, %v", tui.Dark256, customized)
|
||||
}
|
||||
|
||||
customized = parseTheme(theme, "fg:231,dark,bg:232")
|
||||
customized, _ = parseTheme(theme, "fg:231,dark bg:232")
|
||||
if customized.Fg != tui.Dark256.Fg || customized.Bg == tui.Dark256.Bg {
|
||||
t.Errorf("color not customized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultCtrlNP(t *testing.T) {
|
||||
index := 0
|
||||
check := func(words []string, et tui.EventType, expected actionType) {
|
||||
e := et.AsEvent()
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
if opts.Keymap[e][0].t != expected {
|
||||
t.Error()
|
||||
@@ -337,22 +356,23 @@ func TestDefaultCtrlNP(t *testing.T) {
|
||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
|
||||
f, _ := ioutil.TempFile("", "fzf-history")
|
||||
f, _ := os.CreateTemp("", "fzf-history")
|
||||
f.Close()
|
||||
hist := "--history=" + f.Name()
|
||||
check([]string{hist}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPreviousHistory)
|
||||
check([]string{hist}, tui.CtrlP, actPrevHistory)
|
||||
|
||||
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPreviousHistory)
|
||||
check([]string{hist, "--bind=ctrl-n:accept"}, tui.CtrlP, actPrevHistory)
|
||||
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlN, actNextHistory)
|
||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||
}
|
||||
|
||||
func optsFor(words ...string) *Options {
|
||||
index := 0
|
||||
opts := defaultOptions()
|
||||
parseOptions(opts, words)
|
||||
parseOptions(&index, opts, words)
|
||||
postProcessOptions(opts)
|
||||
return opts
|
||||
}
|
||||
@@ -438,14 +458,11 @@ func TestValidateSign(t *testing.T) {
|
||||
{"> ", true},
|
||||
{"아", true},
|
||||
{"😀", true},
|
||||
{"", false},
|
||||
{">>>", false},
|
||||
{"\n", false},
|
||||
{"\t", false},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
err := validateSign(testCase.inputSign, "")
|
||||
err := validateSign(testCase.inputSign, "", 2)
|
||||
if testCase.isValid && err != nil {
|
||||
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
||||
}
|
||||
@@ -455,3 +472,35 @@ func TestValidateSign(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleActionList(t *testing.T) {
|
||||
actions, _ := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down")
|
||||
if len(actions) != 4 {
|
||||
t.Errorf("Invalid number of actions parsed:%d", len(actions))
|
||||
}
|
||||
if actions[0].t != actExecute || actions[0].a != "foo+bar,baz" {
|
||||
t.Errorf("Invalid action parsed: %v", actions[0])
|
||||
}
|
||||
if actions[1].t != actUp || actions[2].t != actUp {
|
||||
t.Errorf("Invalid action parsed: %v / %v", actions[1], actions[2])
|
||||
}
|
||||
if actions[3].t != actReload || actions[3].a != "down+down" {
|
||||
t.Errorf("Invalid action parsed: %v", actions[3])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSingleActionListError(t *testing.T) {
|
||||
_, err := parseSingleActionList("change-query(foobar)baz")
|
||||
if err == nil {
|
||||
t.Errorf("Failed to detect error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaskActionContents(t *testing.T) {
|
||||
original := ":execute((f)(o)(o)(b)(a)(r))+change-query@qu@ry@+up,x:reload:hello:world"
|
||||
expected := ":execute +change-query +up,x:reload "
|
||||
masked := maskActionContents(original)
|
||||
if masked != expected {
|
||||
t.Errorf("Not masked: %s", masked)
|
||||
}
|
||||
}
|
||||
|
||||
101
src/pattern.go
101
src/pattern.go
@@ -23,6 +23,7 @@ type termType int
|
||||
const (
|
||||
termFuzzy termType = iota
|
||||
termExact
|
||||
termExactBoundary
|
||||
termPrefix
|
||||
termSuffix
|
||||
termEqual
|
||||
@@ -51,6 +52,7 @@ type Pattern struct {
|
||||
caseSensitive bool
|
||||
normalize bool
|
||||
forward bool
|
||||
withPos bool
|
||||
text []rune
|
||||
termSets []termSet
|
||||
sortable bool
|
||||
@@ -58,34 +60,21 @@ type Pattern struct {
|
||||
cacheKey string
|
||||
delimiter Delimiter
|
||||
nth []Range
|
||||
revision revision
|
||||
procFun map[termType]algo.Algo
|
||||
cache *ChunkCache
|
||||
denylist map[int32]struct{}
|
||||
}
|
||||
|
||||
var (
|
||||
_patternCache map[string]*Pattern
|
||||
_splitRegex *regexp.Regexp
|
||||
_cache ChunkCache
|
||||
)
|
||||
var _splitRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
_splitRegex = regexp.MustCompile(" +")
|
||||
clearPatternCache()
|
||||
clearChunkCache()
|
||||
}
|
||||
|
||||
func clearPatternCache() {
|
||||
// We can uniquely identify the pattern for a given string since
|
||||
// search mode and caseMode do not change while the program is running
|
||||
_patternCache = make(map[string]*Pattern)
|
||||
}
|
||||
|
||||
func clearChunkCache() {
|
||||
_cache = NewChunkCache()
|
||||
}
|
||||
|
||||
// BuildPattern builds Pattern object from the given arguments
|
||||
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
func BuildPattern(cache *ChunkCache, patternCache map[string]*Pattern, fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, revision revision, runes []rune, denylist map[int32]struct{}) *Pattern {
|
||||
|
||||
var asString string
|
||||
if extended {
|
||||
@@ -97,7 +86,9 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
asString = string(runes)
|
||||
}
|
||||
|
||||
cached, found := _patternCache[asString]
|
||||
// We can uniquely identify the pattern for a given string since
|
||||
// search mode and caseMode do not change while the program is running
|
||||
cached, found := patternCache[asString]
|
||||
if found {
|
||||
return cached
|
||||
}
|
||||
@@ -145,34 +136,39 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
||||
caseSensitive: caseSensitive,
|
||||
normalize: normalize,
|
||||
forward: forward,
|
||||
withPos: withPos,
|
||||
text: []rune(asString),
|
||||
termSets: termSets,
|
||||
sortable: sortable,
|
||||
cacheable: cacheable,
|
||||
nth: nth,
|
||||
revision: revision,
|
||||
delimiter: delimiter,
|
||||
cache: cache,
|
||||
denylist: denylist,
|
||||
procFun: make(map[termType]algo.Algo)}
|
||||
|
||||
ptr.cacheKey = ptr.buildCacheKey()
|
||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||
ptr.procFun[termEqual] = algo.EqualMatch
|
||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||
ptr.procFun[termExactBoundary] = algo.ExactMatchBoundary
|
||||
ptr.procFun[termPrefix] = algo.PrefixMatch
|
||||
ptr.procFun[termSuffix] = algo.SuffixMatch
|
||||
|
||||
_patternCache[asString] = ptr
|
||||
patternCache[asString] = ptr
|
||||
return ptr
|
||||
}
|
||||
|
||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||
str = strings.ReplaceAll(str, "\\ ", "\t")
|
||||
tokens := _splitRegex.Split(str, -1)
|
||||
sets := []termSet{}
|
||||
set := termSet{}
|
||||
switchSet := false
|
||||
afterBar := false
|
||||
for _, token := range tokens {
|
||||
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
||||
typ, inv, text := termFuzzy, false, strings.ReplaceAll(token, "\t", " ")
|
||||
lowerText := strings.ToLower(text)
|
||||
caseSensitive := caseMode == CaseRespect ||
|
||||
caseMode == CaseSmart && text != lowerText
|
||||
@@ -203,15 +199,17 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
text = text[:len(text)-1]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(text, "'") {
|
||||
if len(text) > 2 && strings.HasPrefix(text, "'") && strings.HasSuffix(text, "'") {
|
||||
typ = termExactBoundary
|
||||
text = text[1 : len(text)-1]
|
||||
} else if strings.HasPrefix(text, "'") {
|
||||
// Flip exactness
|
||||
if fuzzy && !inv {
|
||||
typ = termExact
|
||||
text = text[1:]
|
||||
} else {
|
||||
typ = termFuzzy
|
||||
text = text[1:]
|
||||
}
|
||||
text = text[1:]
|
||||
} else if strings.HasPrefix(text, "^") {
|
||||
if typ == termSuffix {
|
||||
typ = termEqual
|
||||
@@ -247,6 +245,9 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
||||
|
||||
// IsEmpty returns true if the pattern is effectively empty
|
||||
func (p *Pattern) IsEmpty() bool {
|
||||
if len(p.denylist) > 0 {
|
||||
return false
|
||||
}
|
||||
if !p.extended {
|
||||
return len(p.text) == 0
|
||||
}
|
||||
@@ -281,18 +282,18 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
// ChunkCache: Exact match
|
||||
cacheKey := p.CacheKey()
|
||||
if p.cacheable {
|
||||
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
if cached := p.cache.Lookup(chunk, cacheKey); cached != nil {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix/suffix cache
|
||||
space := _cache.Search(chunk, cacheKey)
|
||||
space := p.cache.Search(chunk, cacheKey)
|
||||
|
||||
matches := p.matchChunk(chunk, space, slab)
|
||||
|
||||
if p.cacheable {
|
||||
_cache.Add(chunk, cacheKey, matches)
|
||||
p.cache.Add(chunk, cacheKey, matches)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
@@ -300,15 +301,39 @@ func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||
matches := []Result{}
|
||||
|
||||
if len(p.denylist) == 0 {
|
||||
// Huge code duplication for minimizing unnecessary map lookups
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, result := range space {
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
if space == nil {
|
||||
for idx := 0; idx < chunk.count; idx++ {
|
||||
if match, _, _ := p.MatchItem(&chunk.items[idx], false, slab); match != nil {
|
||||
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 match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
|
||||
if _, prs := p.denylist[result.item.Index()]; prs {
|
||||
continue
|
||||
}
|
||||
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||
matches = append(matches, *match)
|
||||
}
|
||||
}
|
||||
@@ -399,12 +424,22 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
||||
|
||||
func (p *Pattern) transformInput(item *Item) []Token {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,15 @@ func TestParseTermsEmpty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func buildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||
return BuildPattern(NewChunkCache(), make(map[string]*Pattern),
|
||||
fuzzy, fuzzyAlgo, extended, caseMode, normalize, forward,
|
||||
withPos, cacheable, nth, delimiter, revision{}, runes, nil)
|
||||
}
|
||||
|
||||
func TestExact(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true,
|
||||
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true,
|
||||
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||
chars := util.ToChars([]byte("aabbcc abc"))
|
||||
res, pos := algo.ExactMatchNaive(
|
||||
@@ -81,9 +86,7 @@ func TestExact(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestEqual(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("^AbC$"))
|
||||
|
||||
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||
chars := util.ToChars([]byte(str))
|
||||
@@ -104,19 +107,12 @@ func TestEqual(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCaseSensitivity(t *testing.T) {
|
||||
defer clearPatternCache()
|
||||
clearPatternCache()
|
||||
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
clearPatternCache()
|
||||
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
clearPatternCache()
|
||||
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat1 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat2 := buildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat3 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat4 := buildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
pat5 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||
pat6 := buildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||
|
||||
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
|
||||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
||||
@@ -129,7 +125,7 @@ func TestCaseSensitivity(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestOrigTextAndTransformed(t *testing.T) {
|
||||
pattern := BuildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
pattern := buildPattern(true, algo.FuzzyMatchV2, true, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("jg"))
|
||||
tokens := Tokenize("junegunn", Delimiter{})
|
||||
trans := Transform(tokens, []Range{{1, 1}})
|
||||
|
||||
@@ -139,12 +135,12 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
chunk.items[0] = Item{
|
||||
text: util.ToChars([]byte("junegunn")),
|
||||
origText: &origBytes,
|
||||
transformed: &trans}
|
||||
transformed: &transformed{pattern.revision, trans}}
|
||||
pattern.extended = extended
|
||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -152,7 +148,7 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
if !(match.item.text.ToString() == "junegunn" &&
|
||||
string(*match.item.origText) == "junegunn.choi" &&
|
||||
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)
|
||||
}
|
||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||
@@ -163,15 +159,13 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
||||
|
||||
func TestCacheKey(t *testing.T) {
|
||||
test := func(extended bool, patStr string, expected string, cacheable bool) {
|
||||
clearPatternCache()
|
||||
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
pat := buildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||
if pat.CacheKey() != expected {
|
||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||
}
|
||||
if pat.cacheable != cacheable {
|
||||
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
|
||||
}
|
||||
clearPatternCache()
|
||||
}
|
||||
test(false, "foo !bar", "foo !bar", true)
|
||||
test(false, "foo | bar !baz", "foo | bar !baz", true)
|
||||
@@ -187,15 +181,13 @@ func TestCacheKey(t *testing.T) {
|
||||
|
||||
func TestCacheable(t *testing.T) {
|
||||
test := func(fuzzy bool, str string, expected string, cacheable bool) {
|
||||
clearPatternCache()
|
||||
pat := BuildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, true, []Range{}, Delimiter{}, []rune(str))
|
||||
pat := buildPattern(fuzzy, algo.FuzzyMatchV2, true, CaseSmart, true, true, false, true, []Range{}, Delimiter{}, []rune(str))
|
||||
if pat.CacheKey() != expected {
|
||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||
}
|
||||
if cacheable != pat.cacheable {
|
||||
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
|
||||
}
|
||||
clearPatternCache()
|
||||
}
|
||||
test(true, "foo bar", "foo\tbar", true)
|
||||
test(true, "foo 'bar", "foo\tbar", false)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// +build !openbsd
|
||||
//go:build !openbsd
|
||||
|
||||
package protector
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
return
|
||||
}
|
||||
func Protect() {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build openbsd
|
||||
//go:build openbsd
|
||||
|
||||
package protector
|
||||
|
||||
@@ -6,5 +6,5 @@ import "golang.org/x/sys/unix"
|
||||
|
||||
// Protect calls OS specific protections like pledge on OpenBSD
|
||||
func Protect() {
|
||||
unix.PledgePromises("stdio rpath tty proc exec")
|
||||
unix.PledgePromises("stdio dpath wpath rpath tty proc exec inet tmppath")
|
||||
}
|
||||
|
||||
162
src/proxy.go
Normal file
162
src/proxy.go
Normal file
@@ -0,0 +1,162 @@
|
||||
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.
|
||||
var exports []string
|
||||
needBash := false
|
||||
if withExports {
|
||||
// Nullify FZF_DEFAULT_* variables as tmux popup may inject them even when undefined.
|
||||
exports = []string{"FZF_DEFAULT_COMMAND=", "FZF_DEFAULT_OPTS=", "FZF_DEFAULT_OPTS_FILE="}
|
||||
validIdentifier := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*$`)
|
||||
for _, pairStr := range os.Environ() {
|
||||
pair := strings.SplitN(pairStr, "=", 2)
|
||||
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
|
||||
}
|
||||
311
src/reader.go
311
src/reader.go
@@ -1,37 +1,50 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/charlievieth/fastwalk"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"github.com/saracen/walker"
|
||||
)
|
||||
|
||||
// Reader reads from command or standard input
|
||||
type Reader struct {
|
||||
pusher func([]byte) bool
|
||||
executor *util.Executor
|
||||
eventBox *util.EventBox
|
||||
delimNil bool
|
||||
event int32
|
||||
finChan chan bool
|
||||
mutex sync.Mutex
|
||||
exec *exec.Cmd
|
||||
command *string
|
||||
killed bool
|
||||
termFunc func()
|
||||
command *string
|
||||
wait bool
|
||||
}
|
||||
|
||||
// NewReader returns new Reader object
|
||||
func NewReader(pusher func([]byte) bool, eventBox *util.EventBox, delimNil bool, wait bool) *Reader {
|
||||
return &Reader{pusher, eventBox, delimNil, int32(EvtReady), make(chan bool, 1), sync.Mutex{}, nil, nil, false, wait}
|
||||
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{},
|
||||
false,
|
||||
func() { os.Stdin.Close() },
|
||||
nil,
|
||||
wait}
|
||||
}
|
||||
|
||||
func (r *Reader) startEventPoller() {
|
||||
@@ -76,73 +89,151 @@ func (r *Reader) fin(success bool) {
|
||||
|
||||
func (r *Reader) terminate() {
|
||||
r.mutex.Lock()
|
||||
defer func() { r.mutex.Unlock() }()
|
||||
|
||||
r.killed = true
|
||||
if r.exec != nil && r.exec.Process != nil {
|
||||
util.KillCommand(r.exec)
|
||||
} else if defaultCommand != "" {
|
||||
os.Stdin.Close()
|
||||
if r.termFunc != nil {
|
||||
r.termFunc()
|
||||
r.termFunc = nil
|
||||
}
|
||||
r.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (r *Reader) restart(command string) {
|
||||
func (r *Reader) restart(command commandSpec, environ []string, readyChan chan bool) {
|
||||
r.event = int32(EvtReady)
|
||||
r.startEventPoller()
|
||||
success := r.readFromCommand(nil, command)
|
||||
success := r.readFromCommand(command.command, environ, func() {
|
||||
readyChan <- true
|
||||
})
|
||||
r.fin(success)
|
||||
removeFiles(command.tempFiles)
|
||||
}
|
||||
|
||||
func (r *Reader) readChannel(inputChan chan string) bool {
|
||||
for {
|
||||
item, more := <-inputChan
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
if r.pusher([]byte(item)) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ReadSource reads data from the default command or from standard input
|
||||
func (r *Reader) ReadSource() {
|
||||
func (r *Reader) ReadSource(inputChan chan string, roots []string, opts walkerOpts, ignores []string, initCmd string, initEnv []string, readyChan chan bool) {
|
||||
r.startEventPoller()
|
||||
var success bool
|
||||
if util.IsTty() {
|
||||
// The default command for *nix requires bash
|
||||
shell := "bash"
|
||||
signalReady := func() {
|
||||
if readyChan != nil {
|
||||
readyChan <- true
|
||||
}
|
||||
}
|
||||
if inputChan != nil {
|
||||
signalReady()
|
||||
success = r.readChannel(inputChan)
|
||||
} else if len(initCmd) > 0 {
|
||||
success = r.readFromCommand(initCmd, initEnv, signalReady)
|
||||
} else if util.IsTty(os.Stdin) {
|
||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||
if len(cmd) == 0 {
|
||||
if defaultCommand != "" {
|
||||
success = r.readFromCommand(&shell, defaultCommand)
|
||||
} else {
|
||||
success = r.readFiles()
|
||||
}
|
||||
signalReady()
|
||||
success = r.readFiles(roots, opts, ignores)
|
||||
} else {
|
||||
success = r.readFromCommand(nil, cmd)
|
||||
success = r.readFromCommand(cmd, initEnv, signalReady)
|
||||
}
|
||||
} else {
|
||||
signalReady()
|
||||
success = r.readFromStdin()
|
||||
}
|
||||
r.fin(success)
|
||||
}
|
||||
|
||||
func (r *Reader) feed(src io.Reader) {
|
||||
/*
|
||||
readerSlabSize, ae := strconv.Atoi(os.Getenv("SLAB_KB"))
|
||||
if ae != nil {
|
||||
readerSlabSize = 128 * 1024
|
||||
} else {
|
||||
readerSlabSize *= 1024
|
||||
}
|
||||
readerBufferSize, be := strconv.Atoi(os.Getenv("BUF_KB"))
|
||||
if be != nil {
|
||||
readerBufferSize = 64 * 1024
|
||||
} else {
|
||||
readerBufferSize *= 1024
|
||||
}
|
||||
*/
|
||||
|
||||
delim := byte('\n')
|
||||
trimCR := util.IsWindows()
|
||||
if r.delimNil {
|
||||
delim = '\000'
|
||||
trimCR = false
|
||||
}
|
||||
reader := bufio.NewReaderSize(src, readerBufferSize)
|
||||
|
||||
slab := make([]byte, readerSlabSize)
|
||||
leftover := []byte{}
|
||||
var err error
|
||||
for {
|
||||
// ReadBytes returns err != nil if and only if the returned data does not
|
||||
// end in delim.
|
||||
bytea, err := reader.ReadBytes(delim)
|
||||
byteaLen := len(bytea)
|
||||
if byteaLen > 0 {
|
||||
if err == nil {
|
||||
// get rid of carriage return if under Windows:
|
||||
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
|
||||
bytea = bytea[:byteaLen-2]
|
||||
} else {
|
||||
bytea = bytea[:byteaLen-1]
|
||||
}
|
||||
}
|
||||
if r.pusher(bytea) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
n := 0
|
||||
scope := slab[:util.Min(len(slab), readerBufferSize)]
|
||||
for i := 0; i < 100; i++ {
|
||||
n, err = src.Read(scope)
|
||||
if n > 0 || err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
// We're not making any progress after 100 tries. Stop.
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
buf := slab[:n]
|
||||
slab = slab[n:]
|
||||
|
||||
for len(buf) > 0 {
|
||||
if i := bytes.IndexByte(buf, delim); i >= 0 {
|
||||
// Found the delimiter
|
||||
slice := buf[:i+1]
|
||||
buf = buf[i+1:]
|
||||
if trimCR && len(slice) >= 2 && slice[len(slice)-2] == byte('\r') {
|
||||
slice = slice[:len(slice)-2]
|
||||
} else {
|
||||
slice = slice[:len(slice)-1]
|
||||
}
|
||||
if len(leftover) > 0 {
|
||||
slice = append(leftover, slice...)
|
||||
leftover = []byte{}
|
||||
}
|
||||
if (err == nil || len(slice) > 0) && r.pusher(slice) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
} else {
|
||||
// Could not find the delimiter in the buffer
|
||||
// NOTE: We can further optimize this by keeping track of the cursor
|
||||
// position in the slab so that a straddling item that doesn't go
|
||||
// beyond the boundary of a slab doesn't need to be copied to
|
||||
// another buffer. However, the performance gain is negligible in
|
||||
// practice (< 0.1%) and is not
|
||||
// worth the added complexity.
|
||||
leftover = append(leftover, buf...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
leftover = append(leftover, buf...)
|
||||
break
|
||||
}
|
||||
|
||||
if len(slab) == 0 {
|
||||
slab = make([]byte, readerSlabSize)
|
||||
}
|
||||
}
|
||||
if len(leftover) > 0 && r.pusher(leftover) {
|
||||
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,16 +242,92 @@ func (r *Reader) readFromStdin() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles() bool {
|
||||
r.killed = false
|
||||
fn := func(path string, mode os.FileInfo) error {
|
||||
path = filepath.Clean(path)
|
||||
if path != "." {
|
||||
isDir := mode.Mode().IsDir()
|
||||
if isDir && filepath.Base(path)[0] == '.' {
|
||||
return filepath.SkipDir
|
||||
func isSymlinkToDir(path string, de os.DirEntry) bool {
|
||||
if de.Type()&fs.ModeSymlink == 0 {
|
||||
return false
|
||||
}
|
||||
if s, err := os.Stat(path); err == nil {
|
||||
return s.IsDir()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func trimPath(path string) string {
|
||||
bytes := stringBytes(path)
|
||||
|
||||
for len(bytes) > 1 && bytes[0] == '.' && (bytes[1] == '/' || bytes[1] == '\\') {
|
||||
bytes = bytes[2:]
|
||||
}
|
||||
|
||||
if len(bytes) == 0 {
|
||||
return "."
|
||||
}
|
||||
|
||||
return byteString(bytes)
|
||||
}
|
||||
|
||||
func (r *Reader) readFiles(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
|
||||
// * 'foo/bar'
|
||||
// * 'baz/foo/bar'
|
||||
// * but NOT 'bazfoo/bar'
|
||||
ignoresFull = append(ignoresFull, ignore)
|
||||
ignoresSuffix = append(ignoresSuffix, sep+ignore)
|
||||
}
|
||||
if !isDir && r.pusher([]byte(path)) {
|
||||
} else {
|
||||
ignoresBase = append(ignoresBase, ignore)
|
||||
}
|
||||
}
|
||||
fn := func(path string, de os.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
path = trimPath(path)
|
||||
if path != "." {
|
||||
isDir := de.IsDir()
|
||||
if isDir || opts.follow && isSymlinkToDir(path, de) {
|
||||
base := filepath.Base(path)
|
||||
if !opts.hidden && base[0] == '.' && base != ".." {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
for _, ignore := range ignoresBase {
|
||||
if ignore == base {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
for _, ignore := range ignoresFull {
|
||||
if ignore == path {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -171,31 +338,39 @@ func (r *Reader) readFiles() bool {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
cb := walker.WithErrorCallback(func(pathname string, err error) error {
|
||||
return nil
|
||||
})
|
||||
return walker.Walk(".", fn, cb) == nil
|
||||
noerr := true
|
||||
for _, root := range roots {
|
||||
noerr = noerr && (fastwalk.Walk(&conf, root, fn) == nil)
|
||||
}
|
||||
return noerr
|
||||
}
|
||||
|
||||
func (r *Reader) readFromCommand(shell *string, command string) bool {
|
||||
func (r *Reader) readFromCommand(command string, environ []string, signalReady func()) bool {
|
||||
r.mutex.Lock()
|
||||
|
||||
r.killed = false
|
||||
r.termFunc = nil
|
||||
r.command = &command
|
||||
if shell != nil {
|
||||
r.exec = util.ExecCommandWith(*shell, command, true)
|
||||
} else {
|
||||
r.exec = util.ExecCommand(command, true)
|
||||
exec := r.executor.ExecCommand(command, true)
|
||||
if environ != nil {
|
||||
exec.Env = environ
|
||||
}
|
||||
out, err := r.exec.StdoutPipe()
|
||||
if err != nil {
|
||||
execOut, err := exec.StdoutPipe()
|
||||
if err != nil || exec.Start() != nil {
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
return false
|
||||
}
|
||||
err = r.exec.Start()
|
||||
r.mutex.Unlock()
|
||||
if err != nil {
|
||||
return false
|
||||
|
||||
// Function to call to terminate the running command
|
||||
r.termFunc = func() {
|
||||
execOut.Close()
|
||||
util.KillCommand(exec)
|
||||
}
|
||||
r.feed(out)
|
||||
return r.exec.Wait() == nil
|
||||
|
||||
signalReady()
|
||||
r.mutex.Unlock()
|
||||
|
||||
r.feed(execOut)
|
||||
return exec.Wait() == nil
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
func TestReadFromCommand(t *testing.T) {
|
||||
strs := []string{}
|
||||
eb := util.NewEventBox()
|
||||
exec := util.NewExecutor("")
|
||||
reader := NewReader(
|
||||
func(s []byte) bool { strs = append(strs, string(s)); return true },
|
||||
eb, false, true)
|
||||
eb, exec, false, true)
|
||||
|
||||
reader.startEventPoller()
|
||||
|
||||
@@ -22,8 +23,12 @@ func TestReadFromCommand(t *testing.T) {
|
||||
}
|
||||
|
||||
// Normal command
|
||||
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||
counter := 0
|
||||
ready := func() {
|
||||
counter++
|
||||
}
|
||||
reader.fin(reader.readFromCommand(`echo abc&&echo def`, nil, ready))
|
||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" || counter != 1 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
@@ -47,9 +52,9 @@ func TestReadFromCommand(t *testing.T) {
|
||||
reader.startEventPoller()
|
||||
|
||||
// Failing command
|
||||
reader.fin(reader.readFromCommand(nil, `no-such-command`))
|
||||
reader.fin(reader.readFromCommand(`no-such-command`, nil, ready))
|
||||
strs = []string{}
|
||||
if len(strs) > 0 {
|
||||
if len(strs) > 0 || counter != 2 {
|
||||
t.Errorf("%s", strs)
|
||||
}
|
||||
|
||||
|
||||
148
src/result.go
148
src/result.go
@@ -15,6 +15,12 @@ type Offset [2]int32
|
||||
type colorOffset struct {
|
||||
offset [2]int32
|
||||
color tui.ColorPair
|
||||
match bool
|
||||
url *url
|
||||
}
|
||||
|
||||
func (co colorOffset) IsFullBgMarker(at int32) bool {
|
||||
return at == co.offset[0] && at == co.offset[1] && co.color.Attr()&tui.FullBg > 0
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
@@ -49,8 +55,39 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
case byScore:
|
||||
// Higher is better
|
||||
val = math.MaxUint16 - util.AsUint16(score)
|
||||
case byChunk:
|
||||
if validOffsetFound {
|
||||
b := minBegin
|
||||
e := maxEnd
|
||||
for ; b >= 1; b-- {
|
||||
if unicode.IsSpace(item.text.Get(b - 1)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
for ; e < numChars; e++ {
|
||||
if unicode.IsSpace(item.text.Get(e)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
val = util.AsUint16(e - b)
|
||||
}
|
||||
case byLength:
|
||||
val = item.TrimLength()
|
||||
case byPathname:
|
||||
if validOffsetFound {
|
||||
// lastDelim := strings.LastIndexByte(item.text.ToString(), '/')
|
||||
lastDelim := -1
|
||||
s := item.text.ToString()
|
||||
for i := len(s) - 1; i >= 0; i-- {
|
||||
if s[i] == '/' || s[i] == '\\' {
|
||||
lastDelim = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if lastDelim <= minBegin {
|
||||
val = util.AsUint16(minBegin - lastDelim)
|
||||
}
|
||||
}
|
||||
case byBegin, byEnd:
|
||||
if validOffsetFound {
|
||||
whitePrefixLen := 0
|
||||
@@ -64,7 +101,7 @@ func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||
if criterion == byBegin {
|
||||
val = util.AsUint16(minEnd - whitePrefixLen)
|
||||
} else {
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/int(item.TrimLength()))
|
||||
val = util.AsUint16(math.MaxUint16 - math.MaxUint16*(maxEnd-whitePrefixLen)/(int(item.TrimLength())+1))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,21 +123,21 @@ func minRank() Result {
|
||||
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()
|
||||
|
||||
// No ANSI codes
|
||||
if len(itemColors) == 0 {
|
||||
var offsets []colorOffset
|
||||
for _, off := range matchOffsets {
|
||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
||||
if len(itemColors) == 0 && len(nthOffsets) == 0 {
|
||||
offsets := make([]colorOffset, len(matchOffsets))
|
||||
for i, off := range matchOffsets {
|
||||
offsets[i] = colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch, match: true}
|
||||
}
|
||||
return offsets
|
||||
}
|
||||
|
||||
// Find max column
|
||||
var maxCol int32
|
||||
for _, off := range matchOffsets {
|
||||
for _, off := range append(matchOffsets, nthOffsets...) {
|
||||
if off[1] > maxCol {
|
||||
maxCol = off[1]
|
||||
}
|
||||
@@ -111,18 +148,37 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
}
|
||||
}
|
||||
|
||||
cols := make([]int, maxCol)
|
||||
type cellInfo struct {
|
||||
index int
|
||||
color bool
|
||||
match bool
|
||||
nth bool
|
||||
fbg tui.Color
|
||||
}
|
||||
|
||||
cols := make([]cellInfo, maxCol+1)
|
||||
for idx := range cols {
|
||||
cols[idx].fbg = -1
|
||||
}
|
||||
for colorIndex, ansi := range itemColors {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = colorIndex + 1 // 1-based index of itemColors
|
||||
if ansi.offset[0] == ansi.offset[1] && ansi.color.attr&tui.FullBg > 0 {
|
||||
cols[ansi.offset[0]].fbg = ansi.color.lbg
|
||||
} else {
|
||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||
cols[i] = cellInfo{colorIndex, true, false, false, cols[i].fbg}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range matchOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
// Negative of 1-based index of itemColors
|
||||
// - The extra -1 means highlighted
|
||||
cols[i] = cols[i]*-1 - 1
|
||||
cols[i].match = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, off := range nthOffsets {
|
||||
for i := off[0]; i < off[1]; i++ {
|
||||
cols[i].nth = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,34 +188,44 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
// ------------ ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
curr := 0
|
||||
curr := cellInfo{0, false, false, false, -1}
|
||||
start := 0
|
||||
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||
if !theme.Colored {
|
||||
return tui.NewColorPair(-1, -1, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
fg := ansi.color.fg
|
||||
bg := ansi.color.bg
|
||||
if fg == -1 {
|
||||
if current {
|
||||
fg = theme.Current.Color
|
||||
} else {
|
||||
fg = theme.Fg.Color
|
||||
}
|
||||
fg = colBase.Fg()
|
||||
}
|
||||
if bg == -1 {
|
||||
if current {
|
||||
bg = theme.DarkBg.Color
|
||||
} else {
|
||||
bg = theme.Bg.Color
|
||||
}
|
||||
bg = colBase.Bg()
|
||||
}
|
||||
return tui.NewColorPair(fg, bg, ansi.color.attr).MergeAttr(base)
|
||||
}
|
||||
var colors []colorOffset
|
||||
add := func(idx int) {
|
||||
if curr != 0 && idx > start {
|
||||
if curr < 0 {
|
||||
color := colMatch
|
||||
if curr < -1 && theme.Colored {
|
||||
origColor := ansiToColorPair(itemColors[-curr-2], colMatch)
|
||||
if curr.fbg >= 0 {
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(start)},
|
||||
color: tui.NewColorPair(-1, curr.fbg, tui.FullBg),
|
||||
match: false,
|
||||
url: nil})
|
||||
}
|
||||
if (curr.color || curr.nth || curr.match) && idx > start {
|
||||
if curr.match {
|
||||
var color tui.ColorPair
|
||||
if curr.nth {
|
||||
color = colBase.WithAttr(attrNth).Merge(colMatch)
|
||||
} else {
|
||||
color = colBase.Merge(colMatch)
|
||||
}
|
||||
var url *url
|
||||
if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
url = ansi.color.url
|
||||
origColor := ansiToColorPair(ansi, colMatch)
|
||||
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||
// combination of either [hl and bg] or [hl+ and bg+].
|
||||
//
|
||||
@@ -170,17 +236,33 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
||||
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||
color = origColor
|
||||
if curr.nth {
|
||||
color = color.WithAttr(attrNth &^ tui.AttrRegular)
|
||||
}
|
||||
} else {
|
||||
color = origColor.MergeNonDefault(color)
|
||||
}
|
||||
}
|
||||
colors = append(colors, colorOffset{
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
||||
} else {
|
||||
ansi := itemColors[curr-1]
|
||||
offset: [2]int32{int32(start), int32(idx)}, color: color, match: true, url: url})
|
||||
} else if curr.color {
|
||||
ansi := itemColors[curr.index]
|
||||
base := colBase
|
||||
if curr.nth {
|
||||
base = base.WithAttr(attrNth)
|
||||
}
|
||||
color := ansiToColorPair(ansi, base)
|
||||
colors = append(colors, colorOffset{
|
||||
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})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build !386,!amd64
|
||||
//go:build !386 && !amd64
|
||||
|
||||
package fzf
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// +build !tcell
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
@@ -56,9 +54,9 @@ func TestResultRank(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byLength}
|
||||
|
||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
||||
str := []rune("foo")
|
||||
item1 := buildResult(
|
||||
withIndex(&Item{text: util.RunesToChars(strs[0])}, 1), []Offset{}, 2)
|
||||
withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
|
||||
if item1.points[3] != math.MaxUint16-2 || // Bonus
|
||||
item1.points[2] != 3 || // Length
|
||||
item1.points[1] != 0 || // Unused
|
||||
@@ -67,7 +65,7 @@ func TestResultRank(t *testing.T) {
|
||||
t.Error(item1)
|
||||
}
|
||||
// Only differ in index
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(strs[0])}, []Offset{}, 2)
|
||||
item2 := buildResult(&Item{text: util.RunesToChars(str)}, []Offset{}, 2)
|
||||
|
||||
items := []Result{item1, item2}
|
||||
sort.Sort(ByRelevance(items))
|
||||
@@ -100,23 +98,40 @@ func TestResultRank(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestChunkTiebreak(t *testing.T) {
|
||||
// FIXME global
|
||||
sortCriteria = []criterion{byScore, byChunk}
|
||||
|
||||
score := 100
|
||||
test := func(input string, offset Offset, chunk string) {
|
||||
item := buildResult(withIndex(&Item{text: util.RunesToChars([]rune(input))}, 1), []Offset{offset}, score)
|
||||
if !(item.points[3] == math.MaxUint16-uint16(score) && item.points[2] == uint16(len(chunk))) {
|
||||
t.Error(item.points)
|
||||
}
|
||||
}
|
||||
test("hello foobar goodbye", Offset{8, 9}, "foobar")
|
||||
test("hello foobar goodbye", Offset{7, 18}, "foobar goodbye")
|
||||
test("hello foobar goodbye", Offset{0, 1}, "hello")
|
||||
test("hello foobar goodbye", Offset{5, 7}, "hello foobar") // TBD
|
||||
}
|
||||
|
||||
func TestColorOffset(t *testing.T) {
|
||||
// ------------ 20 ---- -- ----
|
||||
// ++++++++ ++++++++++
|
||||
// --++++++++-- --++++++++++---
|
||||
|
||||
offsets := []Offset{{5, 15}, {25, 35}}
|
||||
offsets := []Offset{{5, 15}, {10, 12}, {25, 35}}
|
||||
item := Result{
|
||||
item: &Item{
|
||||
colors: &[]ansiOffset{
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
|
||||
{[2]int32{0, 20}, ansiState{1, 5, 0, -1, nil}},
|
||||
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1, nil}},
|
||||
{[2]int32{30, 32}, ansiState{3, 7, 0, -1, nil}},
|
||||
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1, nil}}}}}
|
||||
|
||||
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||
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) {
|
||||
o := colors[idx]
|
||||
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||
@@ -140,20 +155,30 @@ func TestColorOffset(t *testing.T) {
|
||||
|
||||
colRegular := tui.NewColorPair(-1, -1, tui.AttrUndefined)
|
||||
colUnderline := tui.NewColorPair(-1, -1, tui.Underline)
|
||||
colors = item.colorOffsets(offsets, tui.Dark256, colRegular, colUnderline, true)
|
||||
|
||||
// [{[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}}
|
||||
// {[30 32] {3 7 8}} {[32 33] {-1 -1 8}} {[33 35] {4 8 9}}
|
||||
// {[35 40] {4 8 1}}]
|
||||
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||
assert(5, 27, 30, colUnderline)
|
||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||
assert(7, 32, 33, colUnderline)
|
||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
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}}
|
||||
// {[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}}
|
||||
// {[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(1, 5, 15, tui.NewColorPair(1, 5, tui.Underline))
|
||||
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||
assert(4, 25, 27, tui.NewColorPair(2, 6, tui.Bold|tui.Underline))
|
||||
assert(5, 27, 30, colUnderline)
|
||||
assert(6, 30, 32, tui.NewColorPair(3, 7, tui.Underline))
|
||||
assert(7, 32, 33, colUnderline)
|
||||
assert(8, 33, 35, tui.NewColorPair(4, 8, tui.Bold|tui.Underline))
|
||||
assert(9, 35, 37, tui.NewColorPair(4, 8, tui.Bold))
|
||||
expected := tui.Bold | attr
|
||||
if attr == tui.AttrRegular {
|
||||
expected = tui.Bold
|
||||
}
|
||||
assert(10, 37, 39, tui.NewColorPair(4, 8, expected))
|
||||
assert(11, 39, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// +build 386 amd64
|
||||
//go:build 386 || amd64
|
||||
|
||||
package fzf
|
||||
|
||||
|
||||
246
src/server.go
Normal file
246
src/server.go
Normal file
@@ -0,0 +1,246 @@
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var getRegex *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
getRegex = regexp.MustCompile(`^GET /(?:\?([a-z0-9=&]+))? HTTP`)
|
||||
}
|
||||
|
||||
type getParams struct {
|
||||
limit int
|
||||
offset int
|
||||
}
|
||||
|
||||
const (
|
||||
crlf = "\r\n"
|
||||
httpOk = "HTTP/1.1 200 OK" + crlf
|
||||
httpBadRequest = "HTTP/1.1 400 Bad Request" + crlf
|
||||
httpUnauthorized = "HTTP/1.1 401 Unauthorized" + crlf
|
||||
httpUnavailable = "HTTP/1.1 503 Service Unavailable" + crlf
|
||||
httpReadTimeout = 10 * time.Second
|
||||
channelTimeout = 2 * time.Second
|
||||
jsonContentType = "Content-Type: application/json" + crlf
|
||||
maxContentLength = 1024 * 1024
|
||||
)
|
||||
|
||||
type httpServer struct {
|
||||
apiKey []byte
|
||||
actionChannel chan []*action
|
||||
getHandler func(getParams) string
|
||||
}
|
||||
|
||||
type listenAddress struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (addr listenAddress) IsLocal() bool {
|
||||
return addr.host == "localhost" || addr.host == "127.0.0.1"
|
||||
}
|
||||
|
||||
var defaultListenAddr = listenAddress{"localhost", 0}
|
||||
|
||||
func parseListenAddress(address string) (listenAddress, error) {
|
||||
parts := strings.SplitN(address, ":", 3)
|
||||
if len(parts) == 1 {
|
||||
parts = []string{"localhost", parts[0]}
|
||||
}
|
||||
if len(parts) != 2 {
|
||||
return defaultListenAddr, fmt.Errorf("invalid listen address: %s", address)
|
||||
}
|
||||
portStr := parts[len(parts)-1]
|
||||
port, err := strconv.Atoi(portStr)
|
||||
if err != nil || port < 0 || port > 65535 {
|
||||
return defaultListenAddr, fmt.Errorf("invalid listen port: %s", portStr)
|
||||
}
|
||||
if len(parts[0]) == 0 {
|
||||
parts[0] = "localhost"
|
||||
}
|
||||
return listenAddress{parts[0], port}, nil
|
||||
}
|
||||
|
||||
func startHttpServer(address listenAddress, actionChannel chan []*action, getHandler func(getParams) string) (net.Listener, int, error) {
|
||||
host := address.host
|
||||
port := address.port
|
||||
apiKey := os.Getenv("FZF_API_KEY")
|
||||
if !address.IsLocal() && len(apiKey) == 0 {
|
||||
return nil, port, errors.New("FZF_API_KEY is required to allow remote access")
|
||||
}
|
||||
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||
listener, err := net.Listen("tcp", addrStr)
|
||||
if err != nil {
|
||||
return nil, port, fmt.Errorf("failed to listen on %s", addrStr)
|
||||
}
|
||||
if port == 0 {
|
||||
addr := listener.Addr().String()
|
||||
parts := strings.Split(addr, ":")
|
||||
if len(parts) < 2 {
|
||||
return nil, port, fmt.Errorf("cannot extract port: %s", addr)
|
||||
}
|
||||
var err error
|
||||
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||
if err != nil {
|
||||
return nil, port, err
|
||||
}
|
||||
}
|
||||
|
||||
server := httpServer{
|
||||
apiKey: []byte(apiKey),
|
||||
actionChannel: actionChannel,
|
||||
getHandler: getHandler,
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
conn.Write([]byte(server.handleHttpRequest(conn)))
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
return listener, port, nil
|
||||
}
|
||||
|
||||
// Here we are writing a simplistic HTTP server without using net/http
|
||||
// package to reduce the size of the binary.
|
||||
//
|
||||
// * No --listen: 2.8MB
|
||||
// * --listen with net/http: 5.7MB
|
||||
// * --listen w/o net/http: 3.3MB
|
||||
func (server *httpServer) handleHttpRequest(conn net.Conn) string {
|
||||
contentLength := 0
|
||||
apiKey := ""
|
||||
body := ""
|
||||
answer := func(code string, message string) string {
|
||||
message += "\n"
|
||||
return code + fmt.Sprintf("Content-Length: %d%s", len(message), crlf+crlf+message)
|
||||
}
|
||||
unauthorized := func(message string) string {
|
||||
return answer(httpUnauthorized, message)
|
||||
}
|
||||
bad := func(message string) string {
|
||||
return answer(httpBadRequest, message)
|
||||
}
|
||||
good := func(message string) string {
|
||||
return answer(httpOk+jsonContentType, message)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(httpReadTimeout))
|
||||
scanner := bufio.NewScanner(conn)
|
||||
scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
|
||||
found := bytes.Index(data, []byte(crlf))
|
||||
if found >= 0 {
|
||||
token := data[:found+len(crlf)]
|
||||
return len(token), token, nil
|
||||
}
|
||||
if atEOF || len(body)+len(data) >= contentLength {
|
||||
return 0, data, bufio.ErrFinalToken
|
||||
}
|
||||
return 0, nil, nil
|
||||
})
|
||||
|
||||
section := 0
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
switch section {
|
||||
case 0:
|
||||
getMatch := getRegex.FindStringSubmatch(text)
|
||||
if len(getMatch) > 0 {
|
||||
response := server.getHandler(parseGetParams(getMatch[1]))
|
||||
if len(response) > 0 {
|
||||
return good(response)
|
||||
}
|
||||
return answer(httpUnavailable+jsonContentType, `{"error":"timeout"}`)
|
||||
} else if !strings.HasPrefix(text, "POST / HTTP") {
|
||||
return bad("invalid request method")
|
||||
}
|
||||
section++
|
||||
case 1:
|
||||
if text == crlf {
|
||||
if contentLength == 0 {
|
||||
return bad("content-length header missing")
|
||||
}
|
||||
section++
|
||||
continue
|
||||
}
|
||||
pair := strings.SplitN(text, ":", 2)
|
||||
if len(pair) == 2 {
|
||||
switch strings.ToLower(pair[0]) {
|
||||
case "content-length":
|
||||
length, err := strconv.Atoi(strings.TrimSpace(pair[1]))
|
||||
if err != nil || length <= 0 || length > maxContentLength {
|
||||
return bad("invalid content length")
|
||||
}
|
||||
contentLength = length
|
||||
case "x-api-key":
|
||||
apiKey = strings.TrimSpace(pair[1])
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
body += text
|
||||
}
|
||||
}
|
||||
|
||||
if len(server.apiKey) != 0 && subtle.ConstantTimeCompare([]byte(apiKey), server.apiKey) != 1 {
|
||||
return unauthorized("invalid api key")
|
||||
}
|
||||
|
||||
if len(body) < contentLength {
|
||||
return bad("incomplete request")
|
||||
}
|
||||
body = body[:contentLength]
|
||||
|
||||
actions, err := parseSingleActionList(strings.Trim(string(body), "\r\n"))
|
||||
if err != nil {
|
||||
return bad(err.Error())
|
||||
}
|
||||
if len(actions) == 0 {
|
||||
return bad("no action specified")
|
||||
}
|
||||
|
||||
select {
|
||||
case server.actionChannel <- actions:
|
||||
case <-time.After(channelTimeout):
|
||||
return httpUnavailable + crlf
|
||||
}
|
||||
return httpOk + crlf
|
||||
}
|
||||
|
||||
func parseGetParams(query string) getParams {
|
||||
params := getParams{limit: 100, offset: 0}
|
||||
for _, pair := range strings.Split(query, "&") {
|
||||
parts := strings.SplitN(pair, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
switch parts[0] {
|
||||
case "limit", "offset":
|
||||
if val, err := strconv.Atoi(parts[1]); err == nil {
|
||||
if parts[0] == "limit" {
|
||||
params.limit = val
|
||||
} else {
|
||||
params.offset = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
5844
src/terminal.go
5844
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -12,13 +12,29 @@ import (
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func replacePlaceholderTest(template string, stripAnsi bool, delimiter Delimiter, printsep string, forcePlus bool, query string, allItems [3][]*Item) string {
|
||||
replaced, _ := replacePlaceholder(replacePlaceholderParams{
|
||||
template: template,
|
||||
stripAnsi: stripAnsi,
|
||||
delimiter: delimiter,
|
||||
printsep: printsep,
|
||||
forcePlus: forcePlus,
|
||||
query: query,
|
||||
allItems: allItems,
|
||||
lastAction: actBackwardDeleteCharEof,
|
||||
prompt: "prompt",
|
||||
executor: util.NewExecutor(""),
|
||||
})
|
||||
return replaced
|
||||
}
|
||||
|
||||
func TestReplacePlaceholder(t *testing.T) {
|
||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||
items1 := []*Item{item1, item1}
|
||||
items2 := []*Item{
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||
items1 := [3][]*Item{{item1}, {item1}, nil}
|
||||
items2 := [3][]*Item{
|
||||
{newItem("foo'bar \x1b[31mbaz\x1b[m")},
|
||||
{newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}, nil}
|
||||
|
||||
delim := "'"
|
||||
var regex *regexp.Regexp
|
||||
@@ -52,110 +68,118 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
*/
|
||||
|
||||
// {}, preserve ansi
|
||||
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {}, strip ansi
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {r}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {r..}, strip ansi
|
||||
result = replacePlaceholderTest("echo {r..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo foo'bar baz")
|
||||
|
||||
// {}, with multiple items
|
||||
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, preserve ansi
|
||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||
|
||||
// {..}, strip leading whitespaces, strip ansi
|
||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// {q}
|
||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
||||
|
||||
// {q}, multiple items
|
||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
result = replacePlaceholderTest("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
result = replacePlaceholderTest("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}{{.O}}query {{.I}}string{{.I}}{{.O}}{{.O}}foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}bazfoo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}}/{{.O}}baz{{.O}}/{{.O}}baz{{.O}}/{{.O}}foo{{.I}}bar{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
result = replacePlaceholderTest("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// forcePlus
|
||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
result = replacePlaceholderTest("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||
checkFormat("echo {{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}baz{{.O}} {{.O}}BAZ{{.O}}/{{.O}}foo{{.I}}bar{{.O}} {{.O}}FOO{{.I}}BAR{{.O}}/{{.O}}foo{{.I}}bar baz{{.O}} {{.O}}FOO{{.I}}BAR BAZ{{.O}}/{n.t}/{}/{1}/{q}/{{.O}}{{.O}} {{.O}}{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with "'" delimiter
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s1}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s..}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// Whitespace preserving flag with regex delimiter
|
||||
regex = regexp.MustCompile(`\w+`)
|
||||
|
||||
result = replacePlaceholder("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s1}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s2}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}}{{.I}}{{.O}}")
|
||||
|
||||
result = replacePlaceholder("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {s3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} {{.O}}")
|
||||
|
||||
// No match
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, nil, nil})
|
||||
check("echo /")
|
||||
|
||||
// No match, but with selections
|
||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||
result = replacePlaceholderTest("echo {}/{+}", true, Delimiter{}, printsep, false, "query", [3][]*Item{nil, {item1}, nil})
|
||||
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||
|
||||
// String delimiter
|
||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
||||
|
||||
// Regex delimiter
|
||||
regex = regexp.MustCompile("[oa]+")
|
||||
// foo'bar baz
|
||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
result = replacePlaceholderTest("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}f{{.O}}/{{.O}}r b{{.O}}/{{.O}}{{.I}}bar b{{.O}}")
|
||||
|
||||
/*
|
||||
Test single placeholders, but focus on the placeholders' parameters (e.g. flags).
|
||||
see: TestParsePlaceholder
|
||||
*/
|
||||
items3 := []*Item{
|
||||
items3 := [3][]*Item{
|
||||
// single line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
{newItem("1a 1b 1c 1d 1e 1f")},
|
||||
// multi line
|
||||
newItem("1a 1b 1c 1d 1e 1f"),
|
||||
newItem("2a 2b 2c 2d 2e 2f"),
|
||||
newItem("3a 3b 3c 3d 3e 3f"),
|
||||
newItem("4a 4b 4c 4d 4e 4f"),
|
||||
newItem("5a 5b 5c 5d 5e 5f"),
|
||||
newItem("6a 6b 6c 6d 6e 6f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f"),
|
||||
{newItem("1a 1b 1c 1d 1e 1f"),
|
||||
newItem("2a 2b 2c 2d 2e 2f"),
|
||||
newItem("3a 3b 3c 3d 3e 3f"),
|
||||
newItem("4a 4b 4c 4d 4e 4f"),
|
||||
newItem("5a 5b 5c 5d 5e 5f"),
|
||||
newItem("6a 6b 6c 6d 6e 6f"),
|
||||
newItem("7a 7b 7c 7d 7e 7f")},
|
||||
nil,
|
||||
}
|
||||
stripAnsi := false
|
||||
printsep = "\n"
|
||||
forcePlus := false
|
||||
query := "sample query"
|
||||
|
||||
@@ -198,18 +222,23 @@ func TestReplacePlaceholder(t *testing.T) {
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
|
||||
templateToOutput[`{fzf:query}`] = "{{.O}}" + query + "{{.O}}"
|
||||
templateToOutput[`{fzf:action} {fzf:prompt}`] = "backward-delete-char-eof 'prompt'"
|
||||
|
||||
// IV. escaping placeholder
|
||||
templateToOutput[`\{}`] = `{}`
|
||||
templateToOutput[`\{q}`] = `{q}`
|
||||
templateToOutput[`\{fzf:query}`] = `{fzf:query}`
|
||||
templateToOutput[`\{fzf:action}`] = `{fzf:action}`
|
||||
templateToOutput[`\{++}`] = `{++}`
|
||||
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
||||
|
||||
for giveTemplate, wantOutput := range templateToOutput {
|
||||
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
result = replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
checkFormat(wantOutput)
|
||||
}
|
||||
for giveTemplate, wantOutput := range templateToFile {
|
||||
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
path := replacePlaceholderTest(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||
|
||||
data, err := readFile(path)
|
||||
if err != nil {
|
||||
@@ -226,6 +255,7 @@ func TestQuoteEntry(t *testing.T) {
|
||||
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
||||
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
||||
var effectiveStyle quotes
|
||||
exec := util.NewExecutor("")
|
||||
|
||||
if util.IsWindows() {
|
||||
effectiveStyle = windowsStyle
|
||||
@@ -260,7 +290,7 @@ func TestQuoteEntry(t *testing.T) {
|
||||
}
|
||||
|
||||
for input, expected := range tests {
|
||||
escaped := quoteEntry(input)
|
||||
escaped := exec.QuoteEntry(input)
|
||||
expected = templateToString(expected, effectiveStyle)
|
||||
if escaped != expected {
|
||||
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
||||
@@ -299,9 +329,9 @@ func TestUnixCommands(t *testing.T) {
|
||||
|
||||
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows
|
||||
func TestWindowsCommands(t *testing.T) {
|
||||
if !util.IsWindows() {
|
||||
t.SkipNow()
|
||||
}
|
||||
// XXX Deprecated
|
||||
t.SkipNow()
|
||||
|
||||
tests := []testCase{
|
||||
// reference: give{template, query, items}, want{output OR match}
|
||||
|
||||
@@ -421,10 +451,10 @@ func TestPowershellCommands(t *testing.T) {
|
||||
}
|
||||
|
||||
/*
|
||||
Test typical valid placeholders and parsing of them.
|
||||
Test typical valid placeholders and parsing of them.
|
||||
|
||||
Also since the parser assumes the input is matched with `placeholder` regex,
|
||||
the regex is tested here as well.
|
||||
Also since the parser assumes the input is matched with `placeholder` regex,
|
||||
the regex is tested here as well.
|
||||
*/
|
||||
func TestParsePlaceholder(t *testing.T) {
|
||||
// give, want pairs
|
||||
@@ -463,7 +493,12 @@ func TestParsePlaceholder(t *testing.T) {
|
||||
// III. query type placeholder
|
||||
// query flag is not removed after parsing, so it gets doubled
|
||||
// while the double q is invalid, it is useful here for testing purposes
|
||||
`{q}`: `{qq}`,
|
||||
`{q}`: `{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
|
||||
`\{}`: `{}`,
|
||||
@@ -486,6 +521,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 */
|
||||
|
||||
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||
@@ -495,14 +558,14 @@ func newItem(str string) *Item {
|
||||
return &Item{origText: &bytes, text: util.ToChars([]byte(trimmed))}
|
||||
}
|
||||
|
||||
// Functions tested in this file require array of items (allItems). The array needs
|
||||
// to consist of at least two nils. This is helper function.
|
||||
func newItems(str ...string) []*Item {
|
||||
result := make([]*Item, util.Max(len(str), 2))
|
||||
// Functions tested in this file require array of items (allItems).
|
||||
// This is helper function.
|
||||
func newItems(str ...string) [3][]*Item {
|
||||
result := make([]*Item, len(str))
|
||||
for i, s := range str {
|
||||
result[i] = newItem(s)
|
||||
}
|
||||
return result
|
||||
return [3][]*Item{result, nil, nil}
|
||||
}
|
||||
|
||||
// (for logging purposes)
|
||||
@@ -511,7 +574,7 @@ func (item *Item) String() string {
|
||||
}
|
||||
|
||||
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||
func templateToString(format string, data interface{}) string {
|
||||
func templateToString(format string, data any) string {
|
||||
bb := &bytes.Buffer{}
|
||||
|
||||
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||
@@ -526,7 +589,7 @@ func templateToString(format string, data interface{}) string {
|
||||
type give struct {
|
||||
template string
|
||||
query string
|
||||
allItems []*Item
|
||||
allItems [3][]*Item
|
||||
}
|
||||
type want struct {
|
||||
/*
|
||||
@@ -563,26 +626,26 @@ func testCommands(t *testing.T, tests []testCase) {
|
||||
|
||||
// evaluate the test cases
|
||||
for idx, test := range tests {
|
||||
gotOutput := replacePlaceholder(
|
||||
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.give.query,
|
||||
test.give.allItems)
|
||||
gotOutput := replacePlaceholderTest(
|
||||
test.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||
test.query,
|
||||
test.allItems)
|
||||
switch {
|
||||
case test.want.output != "":
|
||||
if gotOutput != test.want.output {
|
||||
case test.output != "":
|
||||
if gotOutput != test.output {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.output)
|
||||
test.template, test.query, test.allItems,
|
||||
gotOutput, test.output)
|
||||
}
|
||||
case test.want.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.want.match, `\`, `\\`)
|
||||
case test.match != "":
|
||||
wantMatch := strings.ReplaceAll(test.match, `\`, `\\`)
|
||||
wantRegex := regexp.MustCompile(wantMatch)
|
||||
if !wantRegex.MatchString(gotOutput) {
|
||||
t.Errorf("tests[%v]:\ngave{\n\ttemplate: '%s',\n\tquery: '%s',\n\tallItems: %s}\nand got '%s',\nbut want '%s'",
|
||||
idx,
|
||||
test.give.template, test.give.query, test.give.allItems,
|
||||
gotOutput, test.want.match)
|
||||
test.template, test.query, test.allItems,
|
||||
gotOutput, test.match)
|
||||
}
|
||||
default:
|
||||
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
|
||||
@@ -605,7 +668,7 @@ func (flags placeholderFlags) encodePlaceholder() string {
|
||||
if flags.file {
|
||||
encoded += "f"
|
||||
}
|
||||
if flags.query {
|
||||
if flags.forceUpdate { // FIXME
|
||||
encoded += "q"
|
||||
}
|
||||
return encoded
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
@@ -14,13 +15,10 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
}
|
||||
|
||||
func notifyStop(p *os.Process) {
|
||||
p.Signal(syscall.SIGSTOP)
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
pid := p.Pid
|
||||
pgid, err := unix.Getpgid(pid)
|
||||
if err == nil {
|
||||
pid = pgid * -1
|
||||
}
|
||||
unix.Kill(pid, syscall.SIGTSTP)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package fzf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
@@ -15,31 +13,3 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||
func notifyStop(p *os.Process) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
func quoteEntry(entry string) string {
|
||||
shell := os.Getenv("SHELL")
|
||||
if len(shell) == 0 {
|
||||
shell = "cmd"
|
||||
}
|
||||
|
||||
if strings.Contains(shell, "cmd") {
|
||||
// backslash escaping is done here for applications
|
||||
// (see ripgrep test case in terminal_test.go#TestWindowsCommands)
|
||||
escaped := strings.Replace(entry, `\`, `\\`, -1)
|
||||
escaped = `"` + strings.Replace(escaped, `"`, `\"`, -1) + `"`
|
||||
// caret is the escape character for cmd shell
|
||||
r, _ := regexp.Compile(`[&|<>()@^%!"]`)
|
||||
return r.ReplaceAllStringFunc(escaped, func(match string) string {
|
||||
return "^" + match
|
||||
})
|
||||
} else if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
||||
escaped := strings.Replace(entry, `"`, `\"`, -1)
|
||||
return "'" + strings.Replace(escaped, "'", "''", -1) + "'"
|
||||
} else {
|
||||
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||
}
|
||||
}
|
||||
|
||||
72
src/tmux.go
Normal file
72
src/tmux.go
Normal file
@@ -0,0 +1,72 @@
|
||||
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 || opts.BorderShape == tui.BorderLine) {
|
||||
// We append --border option at the end, because `--style=full:STYLE`
|
||||
// may have changed the default border style.
|
||||
if tui.DefaultBorderShape == tui.BorderRounded {
|
||||
rest = append(rest, "--border=rounded")
|
||||
} else {
|
||||
rest = append(rest, "--border=sharp")
|
||||
}
|
||||
}
|
||||
if opts.Tmux.border && opts.Margin == defaultMargin() {
|
||||
args = append(args, "--margin=0,1")
|
||||
}
|
||||
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"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
@@ -18,6 +19,48 @@ type Range struct {
|
||||
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
|
||||
type Token struct {
|
||||
text *util.Chars
|
||||
@@ -35,13 +78,18 @@ type Delimiter struct {
|
||||
str *string
|
||||
}
|
||||
|
||||
// IsAwk returns true if the delimiter is an AWK-style delimiter
|
||||
func (d Delimiter) IsAwk() bool {
|
||||
return d.regex == nil && d.str == nil
|
||||
}
|
||||
|
||||
// String returns the string representation of a Delimiter.
|
||||
func (d Delimiter) String() string {
|
||||
return fmt.Sprintf("Delimiter{regex: %v, str: &%q}", d.regex, *d.str)
|
||||
}
|
||||
|
||||
func newRange(begin int, end int) Range {
|
||||
if begin == 1 {
|
||||
if begin == 1 && end != 1 {
|
||||
begin = rangeEllipsis
|
||||
}
|
||||
if end == -1 {
|
||||
@@ -73,7 +121,7 @@ func ParseRange(str *string) (Range, bool) {
|
||||
}
|
||||
begin, err1 := strconv.Atoi(ns[0])
|
||||
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 newRange(begin, end), true
|
||||
@@ -91,7 +139,7 @@ func withPrefixLengths(tokens []string, begin int) []Token {
|
||||
|
||||
prefixLength := begin
|
||||
for idx := range tokens {
|
||||
chars := util.ToChars([]byte(tokens[idx]))
|
||||
chars := util.ToChars(stringBytes(tokens[idx]))
|
||||
ret[idx] = Token{&chars, int32(prefixLength)}
|
||||
prefixLength += chars.Length()
|
||||
}
|
||||
@@ -156,20 +204,37 @@ func Tokenize(text string, delimiter Delimiter) []Token {
|
||||
// FIXME performance
|
||||
var tokens []string
|
||||
if delimiter.regex != nil {
|
||||
for len(text) > 0 {
|
||||
loc := delimiter.regex.FindStringIndex(text)
|
||||
if len(loc) < 2 {
|
||||
loc = []int{0, len(text)}
|
||||
}
|
||||
last := util.Max(loc[1], 1)
|
||||
tokens = append(tokens, text[:last])
|
||||
text = text[last:]
|
||||
locs := delimiter.regex.FindAllStringIndex(text, -1)
|
||||
begin := 0
|
||||
for _, loc := range locs {
|
||||
tokens = append(tokens, text[begin:loc[1]])
|
||||
begin = loc[1]
|
||||
}
|
||||
if begin < len(text) {
|
||||
tokens = append(tokens, text[begin:])
|
||||
}
|
||||
}
|
||||
return withPrefixLengths(tokens, 0)
|
||||
}
|
||||
|
||||
func joinTokens(tokens []Token) string {
|
||||
// StripLastDelimiter removes the trailing delimiter and whitespaces
|
||||
func StripLastDelimiter(str string, delimiter Delimiter) string {
|
||||
if delimiter.str != nil {
|
||||
str = strings.TrimSuffix(str, *delimiter.str)
|
||||
} else if delimiter.regex != nil {
|
||||
locs := delimiter.regex.FindAllStringIndex(str, -1)
|
||||
if len(locs) > 0 {
|
||||
lastLoc := locs[len(locs)-1]
|
||||
if lastLoc[1] == len(str) {
|
||||
str = str[:lastLoc[0]]
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimRightFunc(str, unicode.IsSpace)
|
||||
}
|
||||
|
||||
// JoinTokens concatenates the tokens into a single string
|
||||
func JoinTokens(tokens []Token) string {
|
||||
var output bytes.Buffer
|
||||
for _, token := range tokens {
|
||||
output.WriteString(token.text.ToString())
|
||||
@@ -187,7 +252,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
||||
if r.begin == r.end {
|
||||
idx := r.begin
|
||||
if idx == rangeEllipsis {
|
||||
chars := util.ToChars([]byte(joinTokens(tokens)))
|
||||
chars := util.ToChars(stringBytes(JoinTokens(tokens)))
|
||||
parts = append(parts, &chars)
|
||||
} else {
|
||||
if idx < 0 {
|
||||
|
||||
@@ -40,6 +40,18 @@ func TestParseRange(t *testing.T) {
|
||||
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) {
|
||||
@@ -71,16 +83,16 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
tokens := Tokenize(input, Delimiter{})
|
||||
{
|
||||
ranges := splitNth("1,2,3")
|
||||
ranges, _ := splitNth("1,2,3")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != "abc: def: ghi: " {
|
||||
if JoinTokens(tx) != "abc: def: ghi: " {
|
||||
t.Errorf("%s", tx)
|
||||
}
|
||||
}
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if string(joinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
if string(JoinTokens(tx)) != "abc: def: ghi: def: ghi: jklabc: " ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != "abc: def: " || tx[0].prefixLength != 2 ||
|
||||
tx[1].text.ToString() != "ghi: " || tx[1].prefixLength != 14 ||
|
||||
@@ -93,9 +105,9 @@ func TestTransform(t *testing.T) {
|
||||
{
|
||||
tokens := Tokenize(input, delimiterRegexp(":"))
|
||||
{
|
||||
ranges := splitNth("1..2,3,2..,1")
|
||||
ranges, _ := splitNth("1..2,3,2..,1")
|
||||
tx := Transform(tokens, ranges)
|
||||
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
if JoinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||
len(tx) != 4 ||
|
||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||
@@ -108,5 +120,6 @@ func TestTransform(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTransformIndexOutOfBounds(t *testing.T) {
|
||||
Transform([]Token{}, splitNth("1"))
|
||||
s, _ := splitNth("1")
|
||||
Transform([]Token{}, s)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
// +build !ncurses
|
||||
// +build !tcell
|
||||
// +build !windows
|
||||
//go:build !tcell && !windows
|
||||
|
||||
package tui
|
||||
|
||||
@@ -10,37 +8,57 @@ func HasFullscreenRenderer() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var DefaultBorderShape = BorderRounded
|
||||
|
||||
func (a Attr) Merge(b Attr) Attr {
|
||||
return a | b
|
||||
if b&AttrRegular > 0 {
|
||||
// Only keep bold attribute set by the system
|
||||
return (b &^ AttrRegular) | (a & BoldForce)
|
||||
}
|
||||
|
||||
return (a &^ AttrRegular) | b
|
||||
}
|
||||
|
||||
const (
|
||||
AttrUndefined = Attr(0)
|
||||
AttrRegular = Attr(1 << 7)
|
||||
AttrClear = Attr(1 << 8)
|
||||
AttrRegular = Attr(1 << 8)
|
||||
AttrClear = Attr(1 << 9)
|
||||
BoldForce = Attr(1 << 10)
|
||||
FullBg = Attr(1 << 11)
|
||||
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
Underline = Attr(1 << 3)
|
||||
Blink = Attr(1 << 4)
|
||||
Blink2 = Attr(1 << 5)
|
||||
Reverse = Attr(1 << 6)
|
||||
Bold = Attr(1)
|
||||
Dim = Attr(1 << 1)
|
||||
Italic = Attr(1 << 2)
|
||||
Underline = Attr(1 << 3)
|
||||
Blink = Attr(1 << 4)
|
||||
Blink2 = Attr(1 << 5)
|
||||
Reverse = Attr(1 << 6)
|
||||
StrikeThrough = Attr(1 << 7)
|
||||
)
|
||||
|
||||
func (r *FullscreenRenderer) Init() {}
|
||||
func (r *FullscreenRenderer) Pause(bool) {}
|
||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
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) Pause(bool) {}
|
||||
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||
func (r *FullscreenRenderer) PassThrough(string) {}
|
||||
func (r *FullscreenRenderer) Clear() {}
|
||||
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||
func (r *FullscreenRenderer) ShouldEmitResizeEvent() bool { return false }
|
||||
func (r *FullscreenRenderer) Bell() {}
|
||||
func (r *FullscreenRenderer) HideCursor() {}
|
||||
func (r *FullscreenRenderer) ShowCursor() {}
|
||||
func (r *FullscreenRenderer) Refresh() {}
|
||||
func (r *FullscreenRenderer) Close() {}
|
||||
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||
|
||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||
func (r *FullscreenRenderer) Top() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
177
src/tui/eventtype_string.go
Normal file
177
src/tui/eventtype_string.go
Normal file
@@ -0,0 +1,177 @@
|
||||
// Code generated by "stringer -type=EventType"; DO NOT EDIT.
|
||||
|
||||
package tui
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[Rune-0]
|
||||
_ = x[CtrlA-1]
|
||||
_ = x[CtrlB-2]
|
||||
_ = x[CtrlC-3]
|
||||
_ = x[CtrlD-4]
|
||||
_ = x[CtrlE-5]
|
||||
_ = x[CtrlF-6]
|
||||
_ = x[CtrlG-7]
|
||||
_ = x[CtrlH-8]
|
||||
_ = x[Tab-9]
|
||||
_ = x[CtrlJ-10]
|
||||
_ = x[CtrlK-11]
|
||||
_ = x[CtrlL-12]
|
||||
_ = x[Enter-13]
|
||||
_ = x[CtrlN-14]
|
||||
_ = x[CtrlO-15]
|
||||
_ = x[CtrlP-16]
|
||||
_ = x[CtrlQ-17]
|
||||
_ = x[CtrlR-18]
|
||||
_ = x[CtrlS-19]
|
||||
_ = x[CtrlT-20]
|
||||
_ = x[CtrlU-21]
|
||||
_ = x[CtrlV-22]
|
||||
_ = x[CtrlW-23]
|
||||
_ = x[CtrlX-24]
|
||||
_ = x[CtrlY-25]
|
||||
_ = x[CtrlZ-26]
|
||||
_ = x[Esc-27]
|
||||
_ = x[CtrlSpace-28]
|
||||
_ = x[CtrlBackSlash-29]
|
||||
_ = x[CtrlRightBracket-30]
|
||||
_ = x[CtrlCaret-31]
|
||||
_ = x[CtrlSlash-32]
|
||||
_ = x[ShiftTab-33]
|
||||
_ = x[Backspace-34]
|
||||
_ = x[Delete-35]
|
||||
_ = x[PageUp-36]
|
||||
_ = x[PageDown-37]
|
||||
_ = x[Up-38]
|
||||
_ = x[Down-39]
|
||||
_ = x[Left-40]
|
||||
_ = x[Right-41]
|
||||
_ = x[Home-42]
|
||||
_ = x[End-43]
|
||||
_ = x[Insert-44]
|
||||
_ = x[ShiftUp-45]
|
||||
_ = x[ShiftDown-46]
|
||||
_ = x[ShiftLeft-47]
|
||||
_ = x[ShiftRight-48]
|
||||
_ = x[ShiftDelete-49]
|
||||
_ = x[ShiftHome-50]
|
||||
_ = x[ShiftEnd-51]
|
||||
_ = x[ShiftPageUp-52]
|
||||
_ = x[ShiftPageDown-53]
|
||||
_ = x[F1-54]
|
||||
_ = x[F2-55]
|
||||
_ = x[F3-56]
|
||||
_ = x[F4-57]
|
||||
_ = x[F5-58]
|
||||
_ = x[F6-59]
|
||||
_ = x[F7-60]
|
||||
_ = x[F8-61]
|
||||
_ = x[F9-62]
|
||||
_ = x[F10-63]
|
||||
_ = x[F11-64]
|
||||
_ = x[F12-65]
|
||||
_ = x[AltBackspace-66]
|
||||
_ = x[AltUp-67]
|
||||
_ = x[AltDown-68]
|
||||
_ = x[AltLeft-69]
|
||||
_ = x[AltRight-70]
|
||||
_ = x[AltDelete-71]
|
||||
_ = x[AltHome-72]
|
||||
_ = x[AltEnd-73]
|
||||
_ = x[AltPageUp-74]
|
||||
_ = x[AltPageDown-75]
|
||||
_ = x[AltShiftUp-76]
|
||||
_ = x[AltShiftDown-77]
|
||||
_ = x[AltShiftLeft-78]
|
||||
_ = x[AltShiftRight-79]
|
||||
_ = x[AltShiftDelete-80]
|
||||
_ = x[AltShiftHome-81]
|
||||
_ = x[AltShiftEnd-82]
|
||||
_ = x[AltShiftPageUp-83]
|
||||
_ = x[AltShiftPageDown-84]
|
||||
_ = x[CtrlUp-85]
|
||||
_ = x[CtrlDown-86]
|
||||
_ = x[CtrlLeft-87]
|
||||
_ = x[CtrlRight-88]
|
||||
_ = x[CtrlHome-89]
|
||||
_ = x[CtrlEnd-90]
|
||||
_ = x[CtrlBackspace-91]
|
||||
_ = x[CtrlDelete-92]
|
||||
_ = x[CtrlPageUp-93]
|
||||
_ = x[CtrlPageDown-94]
|
||||
_ = x[Alt-95]
|
||||
_ = x[CtrlAlt-96]
|
||||
_ = x[CtrlAltUp-97]
|
||||
_ = x[CtrlAltDown-98]
|
||||
_ = x[CtrlAltLeft-99]
|
||||
_ = x[CtrlAltRight-100]
|
||||
_ = x[CtrlAltHome-101]
|
||||
_ = x[CtrlAltEnd-102]
|
||||
_ = x[CtrlAltBackspace-103]
|
||||
_ = x[CtrlAltDelete-104]
|
||||
_ = x[CtrlAltPageUp-105]
|
||||
_ = x[CtrlAltPageDown-106]
|
||||
_ = x[CtrlShiftUp-107]
|
||||
_ = x[CtrlShiftDown-108]
|
||||
_ = x[CtrlShiftLeft-109]
|
||||
_ = x[CtrlShiftRight-110]
|
||||
_ = x[CtrlShiftHome-111]
|
||||
_ = x[CtrlShiftEnd-112]
|
||||
_ = x[CtrlShiftDelete-113]
|
||||
_ = x[CtrlShiftPageUp-114]
|
||||
_ = x[CtrlShiftPageDown-115]
|
||||
_ = x[CtrlAltShiftUp-116]
|
||||
_ = x[CtrlAltShiftDown-117]
|
||||
_ = x[CtrlAltShiftLeft-118]
|
||||
_ = x[CtrlAltShiftRight-119]
|
||||
_ = x[CtrlAltShiftHome-120]
|
||||
_ = x[CtrlAltShiftEnd-121]
|
||||
_ = x[CtrlAltShiftDelete-122]
|
||||
_ = x[CtrlAltShiftPageUp-123]
|
||||
_ = x[CtrlAltShiftPageDown-124]
|
||||
_ = x[Invalid-125]
|
||||
_ = x[Fatal-126]
|
||||
_ = x[BracketedPasteBegin-127]
|
||||
_ = x[BracketedPasteEnd-128]
|
||||
_ = x[Mouse-129]
|
||||
_ = x[DoubleClick-130]
|
||||
_ = x[LeftClick-131]
|
||||
_ = x[RightClick-132]
|
||||
_ = x[SLeftClick-133]
|
||||
_ = x[SRightClick-134]
|
||||
_ = x[ScrollUp-135]
|
||||
_ = x[ScrollDown-136]
|
||||
_ = x[SScrollUp-137]
|
||||
_ = x[SScrollDown-138]
|
||||
_ = x[PreviewScrollUp-139]
|
||||
_ = x[PreviewScrollDown-140]
|
||||
_ = x[Resize-141]
|
||||
_ = x[Change-142]
|
||||
_ = x[BackwardEOF-143]
|
||||
_ = x[Start-144]
|
||||
_ = x[Load-145]
|
||||
_ = x[Focus-146]
|
||||
_ = x[One-147]
|
||||
_ = x[Zero-148]
|
||||
_ = x[Result-149]
|
||||
_ = x[Jump-150]
|
||||
_ = x[JumpCancel-151]
|
||||
_ = x[ClickHeader-152]
|
||||
_ = x[ClickFooter-153]
|
||||
_ = x[Multi-154]
|
||||
}
|
||||
|
||||
const _EventType_name = "RuneCtrlACtrlBCtrlCCtrlDCtrlECtrlFCtrlGCtrlHTabCtrlJCtrlKCtrlLEnterCtrlNCtrlOCtrlPCtrlQCtrlRCtrlSCtrlTCtrlUCtrlVCtrlWCtrlXCtrlYCtrlZEscCtrlSpaceCtrlBackSlashCtrlRightBracketCtrlCaretCtrlSlashShiftTabBackspaceDeletePageUpPageDownUpDownLeftRightHomeEndInsertShiftUpShiftDownShiftLeftShiftRightShiftDeleteShiftHomeShiftEndShiftPageUpShiftPageDownF1F2F3F4F5F6F7F8F9F10F11F12AltBackspaceAltUpAltDownAltLeftAltRightAltDeleteAltHomeAltEndAltPageUpAltPageDownAltShiftUpAltShiftDownAltShiftLeftAltShiftRightAltShiftDeleteAltShiftHomeAltShiftEndAltShiftPageUpAltShiftPageDownCtrlUpCtrlDownCtrlLeftCtrlRightCtrlHomeCtrlEndCtrlBackspaceCtrlDeleteCtrlPageUpCtrlPageDownAltCtrlAltCtrlAltUpCtrlAltDownCtrlAltLeftCtrlAltRightCtrlAltHomeCtrlAltEndCtrlAltBackspaceCtrlAltDeleteCtrlAltPageUpCtrlAltPageDownCtrlShiftUpCtrlShiftDownCtrlShiftLeftCtrlShiftRightCtrlShiftHomeCtrlShiftEndCtrlShiftDeleteCtrlShiftPageUpCtrlShiftPageDownCtrlAltShiftUpCtrlAltShiftDownCtrlAltShiftLeftCtrlAltShiftRightCtrlAltShiftHomeCtrlAltShiftEndCtrlAltShiftDeleteCtrlAltShiftPageUpCtrlAltShiftPageDownInvalidFatalBracketedPasteBeginBracketedPasteEndMouseDoubleClickLeftClickRightClickSLeftClickSRightClickScrollUpScrollDownSScrollUpSScrollDownPreviewScrollUpPreviewScrollDownResizeChangeBackwardEOFStartLoadFocusOneZeroResultJumpJumpCancelClickHeaderClickFooterMulti"
|
||||
|
||||
var _EventType_index = [...]uint16{0, 4, 9, 14, 19, 24, 29, 34, 39, 44, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 102, 107, 112, 117, 122, 127, 132, 135, 144, 157, 173, 182, 191, 199, 208, 214, 220, 228, 230, 234, 238, 243, 247, 250, 256, 263, 272, 281, 291, 302, 311, 319, 330, 343, 345, 347, 349, 351, 353, 355, 357, 359, 361, 364, 367, 370, 382, 387, 394, 401, 409, 418, 425, 431, 440, 451, 461, 473, 485, 498, 512, 524, 535, 549, 565, 571, 579, 587, 596, 604, 611, 624, 634, 644, 656, 659, 666, 675, 686, 697, 709, 720, 730, 746, 759, 772, 787, 798, 811, 824, 838, 851, 863, 878, 893, 910, 924, 940, 956, 973, 989, 1004, 1022, 1040, 1060, 1067, 1072, 1091, 1108, 1113, 1124, 1133, 1143, 1153, 1164, 1172, 1182, 1191, 1202, 1217, 1234, 1240, 1246, 1257, 1262, 1266, 1271, 1274, 1278, 1284, 1288, 1298, 1309, 1320, 1325}
|
||||
|
||||
func (i EventType) String() string {
|
||||
if i < 0 || i >= EventType(len(_EventType_index)-1) {
|
||||
return "EventType(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _EventType_name[_EventType_index[i]:_EventType_index[i+1]]
|
||||
}
|
||||
925
src/tui/light.go
925
src/tui/light.go
File diff suppressed because it is too large
Load Diff
335
src/tui/light_test.go
Normal file
335
src/tui/light_test.go
Normal file
@@ -0,0 +1,335 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func TestLightRenderer(t *testing.T) {
|
||||
tty_file, _ := os.Open("")
|
||||
renderer, _ := NewLightRenderer(
|
||||
"", tty_file, &ColorTheme{}, true, false, 0, false, true,
|
||||
func(h int) int { return h })
|
||||
|
||||
light_renderer := renderer.(*LightRenderer)
|
||||
|
||||
assertCharSequence := func(sequence string, name string) {
|
||||
bytes := []byte(sequence)
|
||||
light_renderer.buffer = bytes
|
||||
event := light_renderer.GetChar()
|
||||
if event.KeyName() != name {
|
||||
t.Errorf(
|
||||
"sequence: %q | %v | '%s' (%s) != %s",
|
||||
string(bytes), bytes,
|
||||
event.KeyName(), event.Type.String(), name)
|
||||
}
|
||||
}
|
||||
|
||||
assertEscSequence := func(sequence string, name string) {
|
||||
bytes := []byte(sequence)
|
||||
light_renderer.buffer = bytes
|
||||
|
||||
sz := 1
|
||||
event := light_renderer.escSequence(&sz)
|
||||
if fmt.Sprintf("!%s", event.Type.String()) == name {
|
||||
// this is fine
|
||||
} else if event.KeyName() != name {
|
||||
t.Errorf(
|
||||
"sequence: %q | %v | '%s' (%s) != %s",
|
||||
string(bytes), bytes,
|
||||
event.KeyName(), event.Type.String(), name)
|
||||
}
|
||||
}
|
||||
|
||||
// invalid
|
||||
assertEscSequence("\x1b[<", "!Invalid")
|
||||
assertEscSequence("\x1b[1;1R", "!Invalid")
|
||||
assertEscSequence("\x1b[", "!Invalid")
|
||||
assertEscSequence("\x1b[1", "!Invalid")
|
||||
assertEscSequence("\x1b[3;3~1", "!Invalid")
|
||||
assertEscSequence("\x1b[13", "!Invalid")
|
||||
assertEscSequence("\x1b[1;3", "!Invalid")
|
||||
assertEscSequence("\x1b[1;10", "!Invalid")
|
||||
assertEscSequence("\x1b[220~", "!Invalid")
|
||||
assertEscSequence("\x1b[5;30~", "!Invalid")
|
||||
assertEscSequence("\x1b[6;30~", "!Invalid")
|
||||
|
||||
// general
|
||||
for r := 'a'; r < 'z'; r++ {
|
||||
lower_r := fmt.Sprintf("%c", r)
|
||||
upper_r := fmt.Sprintf("%c", unicode.ToUpper(r))
|
||||
assertCharSequence(lower_r, lower_r)
|
||||
assertCharSequence(upper_r, upper_r)
|
||||
}
|
||||
|
||||
assertCharSequence("\x01", "ctrl-a")
|
||||
assertCharSequence("\x02", "ctrl-b")
|
||||
assertCharSequence("\x03", "ctrl-c")
|
||||
assertCharSequence("\x04", "ctrl-d")
|
||||
assertCharSequence("\x05", "ctrl-e")
|
||||
assertCharSequence("\x06", "ctrl-f")
|
||||
assertCharSequence("\x07", "ctrl-g")
|
||||
// ctrl-h is the same as ctrl-backspace
|
||||
// ctrl-i is the same as tab
|
||||
assertCharSequence("\n", "ctrl-j")
|
||||
assertCharSequence("\x0b", "ctrl-k")
|
||||
assertCharSequence("\x0c", "ctrl-l")
|
||||
assertCharSequence("\r", "enter") // enter
|
||||
assertCharSequence("\x0e", "ctrl-n")
|
||||
assertCharSequence("\x0f", "ctrl-o")
|
||||
assertCharSequence("\x10", "ctrl-p")
|
||||
assertCharSequence("\x11", "ctrl-q")
|
||||
assertCharSequence("\x12", "ctrl-r")
|
||||
assertCharSequence("\x13", "ctrl-s")
|
||||
assertCharSequence("\x14", "ctrl-t")
|
||||
assertCharSequence("\x15", "ctrl-u")
|
||||
assertCharSequence("\x16", "ctrl-v")
|
||||
assertCharSequence("\x17", "ctrl-w")
|
||||
assertCharSequence("\x18", "ctrl-x")
|
||||
assertCharSequence("\x19", "ctrl-y")
|
||||
assertCharSequence("\x1a", "ctrl-z")
|
||||
|
||||
assertCharSequence("\x00", "ctrl-space")
|
||||
assertCharSequence("\x1c", "ctrl-\\")
|
||||
assertCharSequence("\x1d", "ctrl-]")
|
||||
assertCharSequence("\x1e", "ctrl-^")
|
||||
assertCharSequence("\x1f", "ctrl-/")
|
||||
|
||||
assertEscSequence("\x1ba", "alt-a")
|
||||
assertEscSequence("\x1bb", "alt-b")
|
||||
assertEscSequence("\x1bc", "alt-c")
|
||||
assertEscSequence("\x1bd", "alt-d")
|
||||
assertEscSequence("\x1be", "alt-e")
|
||||
assertEscSequence("\x1bf", "alt-f")
|
||||
assertEscSequence("\x1bg", "alt-g")
|
||||
assertEscSequence("\x1bh", "alt-h")
|
||||
assertEscSequence("\x1bi", "alt-i")
|
||||
assertEscSequence("\x1bj", "alt-j")
|
||||
assertEscSequence("\x1bk", "alt-k")
|
||||
assertEscSequence("\x1bl", "alt-l")
|
||||
assertEscSequence("\x1bm", "alt-m")
|
||||
assertEscSequence("\x1bn", "alt-n")
|
||||
assertEscSequence("\x1bo", "alt-o")
|
||||
assertEscSequence("\x1bp", "alt-p")
|
||||
assertEscSequence("\x1bq", "alt-q")
|
||||
assertEscSequence("\x1br", "alt-r")
|
||||
assertEscSequence("\x1bs", "alt-s")
|
||||
assertEscSequence("\x1bt", "alt-t")
|
||||
assertEscSequence("\x1bu", "alt-u")
|
||||
assertEscSequence("\x1bv", "alt-v")
|
||||
assertEscSequence("\x1bw", "alt-w")
|
||||
assertEscSequence("\x1bx", "alt-x")
|
||||
assertEscSequence("\x1by", "alt-y")
|
||||
assertEscSequence("\x1bz", "alt-z")
|
||||
|
||||
assertEscSequence("\x1bOP", "f1")
|
||||
assertEscSequence("\x1bOQ", "f2")
|
||||
assertEscSequence("\x1bOR", "f3")
|
||||
assertEscSequence("\x1bOS", "f4")
|
||||
assertEscSequence("\x1b[15~", "f5")
|
||||
assertEscSequence("\x1b[17~", "f6")
|
||||
assertEscSequence("\x1b[18~", "f7")
|
||||
assertEscSequence("\x1b[19~", "f8")
|
||||
assertEscSequence("\x1b[20~", "f9")
|
||||
assertEscSequence("\x1b[21~", "f10")
|
||||
assertEscSequence("\x1b[23~", "f11")
|
||||
assertEscSequence("\x1b[24~", "f12")
|
||||
|
||||
assertEscSequence("\x1b", "esc")
|
||||
assertCharSequence("\t", "tab")
|
||||
assertEscSequence("\x1b[Z", "shift-tab")
|
||||
|
||||
assertCharSequence("\x7f", "backspace")
|
||||
assertEscSequence("\x1b\x7f", "alt-backspace")
|
||||
assertCharSequence("\b", "ctrl-backspace")
|
||||
assertEscSequence("\x1b\b", "ctrl-alt-backspace")
|
||||
|
||||
assertEscSequence("\x1b[A", "up")
|
||||
assertEscSequence("\x1b[B", "down")
|
||||
assertEscSequence("\x1b[C", "right")
|
||||
assertEscSequence("\x1b[D", "left")
|
||||
assertEscSequence("\x1b[H", "home")
|
||||
assertEscSequence("\x1b[F", "end")
|
||||
assertEscSequence("\x1b[2~", "insert")
|
||||
assertEscSequence("\x1b[3~", "delete")
|
||||
assertEscSequence("\x1b[5~", "page-up")
|
||||
assertEscSequence("\x1b[6~", "page-down")
|
||||
assertEscSequence("\x1b[7~", "home")
|
||||
assertEscSequence("\x1b[8~", "end")
|
||||
|
||||
assertEscSequence("\x1b[1;2A", "shift-up")
|
||||
assertEscSequence("\x1b[1;2B", "shift-down")
|
||||
assertEscSequence("\x1b[1;2C", "shift-right")
|
||||
assertEscSequence("\x1b[1;2D", "shift-left")
|
||||
assertEscSequence("\x1b[1;2H", "shift-home")
|
||||
assertEscSequence("\x1b[1;2F", "shift-end")
|
||||
assertEscSequence("\x1b[3;2~", "shift-delete")
|
||||
assertEscSequence("\x1b[5;2~", "shift-page-up")
|
||||
assertEscSequence("\x1b[6;2~", "shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b\x1b", "esc")
|
||||
assertEscSequence("\x1b\x1b[A", "alt-up")
|
||||
assertEscSequence("\x1b\x1b[B", "alt-down")
|
||||
assertEscSequence("\x1b\x1b[C", "alt-right")
|
||||
assertEscSequence("\x1b\x1b[D", "alt-left")
|
||||
|
||||
assertEscSequence("\x1b[1;3A", "alt-up")
|
||||
assertEscSequence("\x1b[1;3B", "alt-down")
|
||||
assertEscSequence("\x1b[1;3C", "alt-right")
|
||||
assertEscSequence("\x1b[1;3D", "alt-left")
|
||||
assertEscSequence("\x1b[1;3H", "alt-home")
|
||||
assertEscSequence("\x1b[1;3F", "alt-end")
|
||||
assertEscSequence("\x1b[3;3~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;3~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;3~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;4A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;4B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;4C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;4D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;4H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;4F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;4~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;4~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;4~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;5A", "ctrl-up")
|
||||
assertEscSequence("\x1b[1;5B", "ctrl-down")
|
||||
assertEscSequence("\x1b[1;5C", "ctrl-right")
|
||||
assertEscSequence("\x1b[1;5D", "ctrl-left")
|
||||
assertEscSequence("\x1b[1;5H", "ctrl-home")
|
||||
assertEscSequence("\x1b[1;5F", "ctrl-end")
|
||||
assertEscSequence("\x1b[3;5~", "ctrl-delete")
|
||||
assertEscSequence("\x1b[5;5~", "ctrl-page-up")
|
||||
assertEscSequence("\x1b[6;5~", "ctrl-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;7A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;7B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;7C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;7D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;7H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;7F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;7~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;7~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;7~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;6A", "ctrl-shift-up")
|
||||
assertEscSequence("\x1b[1;6B", "ctrl-shift-down")
|
||||
assertEscSequence("\x1b[1;6C", "ctrl-shift-right")
|
||||
assertEscSequence("\x1b[1;6D", "ctrl-shift-left")
|
||||
assertEscSequence("\x1b[1;6H", "ctrl-shift-home")
|
||||
assertEscSequence("\x1b[1;6F", "ctrl-shift-end")
|
||||
assertEscSequence("\x1b[3;6~", "ctrl-shift-delete")
|
||||
assertEscSequence("\x1b[5;6~", "ctrl-shift-page-up")
|
||||
assertEscSequence("\x1b[6;6~", "ctrl-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;8A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;8B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;8C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;8D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;8H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;8F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;8~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;8~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;8~", "ctrl-alt-shift-page-down")
|
||||
|
||||
// xterm meta & mac
|
||||
assertEscSequence("\x1b[1;9A", "alt-up")
|
||||
assertEscSequence("\x1b[1;9B", "alt-down")
|
||||
assertEscSequence("\x1b[1;9C", "alt-right")
|
||||
assertEscSequence("\x1b[1;9D", "alt-left")
|
||||
assertEscSequence("\x1b[1;9H", "alt-home")
|
||||
assertEscSequence("\x1b[1;9F", "alt-end")
|
||||
assertEscSequence("\x1b[3;9~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;9~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;9~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;10A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;10B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;10C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;10D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;10H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;10F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;10~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;10~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;10~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;11A", "alt-up")
|
||||
assertEscSequence("\x1b[1;11B", "alt-down")
|
||||
assertEscSequence("\x1b[1;11C", "alt-right")
|
||||
assertEscSequence("\x1b[1;11D", "alt-left")
|
||||
assertEscSequence("\x1b[1;11H", "alt-home")
|
||||
assertEscSequence("\x1b[1;11F", "alt-end")
|
||||
assertEscSequence("\x1b[3;11~", "alt-delete")
|
||||
assertEscSequence("\x1b[5;11~", "alt-page-up")
|
||||
assertEscSequence("\x1b[6;11~", "alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;12A", "alt-shift-up")
|
||||
assertEscSequence("\x1b[1;12B", "alt-shift-down")
|
||||
assertEscSequence("\x1b[1;12C", "alt-shift-right")
|
||||
assertEscSequence("\x1b[1;12D", "alt-shift-left")
|
||||
assertEscSequence("\x1b[1;12H", "alt-shift-home")
|
||||
assertEscSequence("\x1b[1;12F", "alt-shift-end")
|
||||
assertEscSequence("\x1b[3;12~", "alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;12~", "alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;12~", "alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;13A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;13B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;13C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;13D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;13H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;13F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;13~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;13~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;13~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;14A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;14B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;14C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;14D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;14H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;14F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;14~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;14~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;14~", "ctrl-alt-shift-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;15A", "ctrl-alt-up")
|
||||
assertEscSequence("\x1b[1;15B", "ctrl-alt-down")
|
||||
assertEscSequence("\x1b[1;15C", "ctrl-alt-right")
|
||||
assertEscSequence("\x1b[1;15D", "ctrl-alt-left")
|
||||
assertEscSequence("\x1b[1;15H", "ctrl-alt-home")
|
||||
assertEscSequence("\x1b[1;15F", "ctrl-alt-end")
|
||||
assertEscSequence("\x1b[3;15~", "ctrl-alt-delete")
|
||||
assertEscSequence("\x1b[5;15~", "ctrl-alt-page-up")
|
||||
assertEscSequence("\x1b[6;15~", "ctrl-alt-page-down")
|
||||
|
||||
assertEscSequence("\x1b[1;16A", "ctrl-alt-shift-up")
|
||||
assertEscSequence("\x1b[1;16B", "ctrl-alt-shift-down")
|
||||
assertEscSequence("\x1b[1;16C", "ctrl-alt-shift-right")
|
||||
assertEscSequence("\x1b[1;16D", "ctrl-alt-shift-left")
|
||||
assertEscSequence("\x1b[1;16H", "ctrl-alt-shift-home")
|
||||
assertEscSequence("\x1b[1;16F", "ctrl-alt-shift-end")
|
||||
assertEscSequence("\x1b[3;16~", "ctrl-alt-shift-delete")
|
||||
assertEscSequence("\x1b[5;16~", "ctrl-alt-shift-page-up")
|
||||
assertEscSequence("\x1b[6;16~", "ctrl-alt-shift-page-down")
|
||||
|
||||
// tmux & emacs
|
||||
assertEscSequence("\x1bOA", "up")
|
||||
assertEscSequence("\x1bOB", "down")
|
||||
assertEscSequence("\x1bOC", "right")
|
||||
assertEscSequence("\x1bOD", "left")
|
||||
assertEscSequence("\x1bOH", "home")
|
||||
assertEscSequence("\x1bOF", "end")
|
||||
|
||||
// rrvt
|
||||
assertEscSequence("\x1b[1~", "home")
|
||||
assertEscSequence("\x1b[4~", "end")
|
||||
assertEscSequence("\x1b[11~", "f1")
|
||||
assertEscSequence("\x1b[12~", "f2")
|
||||
assertEscSequence("\x1b[13~", "f3")
|
||||
assertEscSequence("\x1b[14~", "f4")
|
||||
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
"golang.org/x/sys/unix"
|
||||
"golang.org/x/term"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,7 @@ func IsLightRendererSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||
func (r *LightRenderer) DefaultTheme() *ColorTheme {
|
||||
if strings.Contains(os.Getenv("TERM"), "256") {
|
||||
return Dark256
|
||||
}
|
||||
@@ -32,34 +33,44 @@ func (r *LightRenderer) fd() int {
|
||||
return int(r.ttyin.Fd())
|
||||
}
|
||||
|
||||
func (r *LightRenderer) initPlatform() error {
|
||||
fd := r.fd()
|
||||
origState, err := term.GetState(fd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.origState = origState
|
||||
term.MakeRaw(fd)
|
||||
return nil
|
||||
func (r *LightRenderer) initPlatform() (err error) {
|
||||
r.origState, err = term.MakeRaw(r.fd())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *LightRenderer) closePlatform() {
|
||||
// NOOP
|
||||
r.ttyout.Close()
|
||||
}
|
||||
|
||||
func openTtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
func openTty(ttyDefault string, mode int) (*os.File, error) {
|
||||
var in *os.File
|
||||
var err error
|
||||
if len(ttyDefault) > 0 {
|
||||
in, err = os.OpenFile(ttyDefault, mode, 0)
|
||||
}
|
||||
if in == nil || err != nil || ttyDefault != DefaultTtyDevice && !util.IsTty(in) {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
return in
|
||||
if in, err := os.OpenFile(tty, mode, 0); err == nil {
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
|
||||
os.Exit(2)
|
||||
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
|
||||
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() {
|
||||
@@ -85,9 +96,14 @@ func (r *LightRenderer) updateTerminalSize() {
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
r.csi("6n")
|
||||
r.flush()
|
||||
var err error
|
||||
bytes := []byte{}
|
||||
for tries := 0; tries < offsetPollTries; tries++ {
|
||||
bytes = r.getBytesInternal(bytes, tries > 0)
|
||||
bytes, err = r.getBytesInternal(bytes, tries > 0)
|
||||
if err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
offsets := offsetRegexp.FindSubmatch(bytes)
|
||||
if len(offsets) > 3 {
|
||||
// Add anything we skipped over to the input buffer
|
||||
@@ -108,3 +124,11 @@ func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
}
|
||||
return int(b[0]), true
|
||||
}
|
||||
|
||||
func (r *LightRenderer) Size() TermSize {
|
||||
ws, err := unix.IoctlGetWinsize(int(r.ttyin.Fd()), unix.TIOCGWINSZ)
|
||||
if err != nil {
|
||||
return TermSize{}
|
||||
}
|
||||
return TermSize{int(ws.Row), int(ws.Col), int(ws.Xpixel), int(ws.Ypixel)}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//+build windows
|
||||
//go:build windows
|
||||
|
||||
package tui
|
||||
|
||||
@@ -18,6 +18,7 @@ const (
|
||||
var (
|
||||
consoleFlagsInput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_INPUT | windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_EXTENDED_FLAGS)
|
||||
consoleFlagsOutput = uint32(windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING | windows.ENABLE_PROCESSED_OUTPUT | windows.DISABLE_NEWLINE_AUTO_RETURN)
|
||||
counter = uint64(0)
|
||||
)
|
||||
|
||||
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||
@@ -39,7 +40,7 @@ func IsLightRendererSupported() bool {
|
||||
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:
|
||||
if !IsLightRendererSupported() || os.Getenv("ConEmuPID") != "" || os.Getenv("TCELL_TRUECOLOR") == "disable" {
|
||||
return Default16
|
||||
@@ -61,27 +62,11 @@ func (r *LightRenderer) initPlatform() error {
|
||||
}
|
||||
r.inHandle = uintptr(inHandle)
|
||||
|
||||
r.setupTerminal()
|
||||
|
||||
// channel for non-blocking reads. Buffer to make sure
|
||||
// we get the ESC sets:
|
||||
r.ttyinChannel = make(chan byte, 1024)
|
||||
|
||||
// the following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
// HACK: if run from PSReadline, something resets ConsoleMode to remove ENABLE_VIRTUAL_TERMINAL_INPUT.
|
||||
_ = windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
_, err := util.Read(fd, b)
|
||||
if err == nil {
|
||||
r.ttyinChannel <- b[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
r.setupTerminal()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -91,35 +76,71 @@ func (r *LightRenderer) closePlatform() {
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||
}
|
||||
|
||||
func openTtyIn() *os.File {
|
||||
func openTtyIn(ttyDefault string) (*os.File, error) {
|
||||
// not used
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) setupTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput); err != nil {
|
||||
return err
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
func openTtyOut(ttyDefault string) (*os.File, error) {
|
||||
return os.Stderr, nil
|
||||
}
|
||||
|
||||
func (r *LightRenderer) restoreTerminal() error {
|
||||
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
||||
return err
|
||||
func (r *LightRenderer) setupTerminal() {
|
||||
windows.SetConsoleMode(windows.Handle(r.outHandle), consoleFlagsOutput)
|
||||
windows.SetConsoleMode(windows.Handle(r.inHandle), consoleFlagsInput)
|
||||
|
||||
// The following allows for non-blocking IO.
|
||||
// syscall.SetNonblock() is a NOOP under Windows.
|
||||
current := counter
|
||||
go func() {
|
||||
fd := int(r.inHandle)
|
||||
b := make([]byte, 1)
|
||||
for {
|
||||
if _, err := util.Read(fd, b); err == nil {
|
||||
r.mutex.Lock()
|
||||
// This condition prevents the goroutine from running after the renderer
|
||||
// has been closed or paused.
|
||||
if current != counter {
|
||||
r.mutex.Unlock()
|
||||
break
|
||||
}
|
||||
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 {
|
||||
var w, h int
|
||||
var bufferInfo windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
w = getEnv("COLUMNS", defaultWidth)
|
||||
h = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
|
||||
} else {
|
||||
w = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
|
||||
h = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
|
||||
}
|
||||
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||
return TermSize{h, w, 0, 0}
|
||||
}
|
||||
|
||||
func (r *LightRenderer) updateTerminalSize() {
|
||||
var bufferInfo windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
r.width = getEnv("COLUMNS", defaultWidth)
|
||||
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||
|
||||
} else {
|
||||
r.width = int(bufferInfo.Window.Right - bufferInfo.Window.Left)
|
||||
r.height = r.maxHeightFunc(int(bufferInfo.Window.Bottom - bufferInfo.Window.Top))
|
||||
}
|
||||
size := r.Size()
|
||||
r.width = size.Columns
|
||||
r.height = size.Lines
|
||||
}
|
||||
|
||||
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
@@ -127,7 +148,7 @@ func (r *LightRenderer) findOffset() (row int, col int) {
|
||||
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||
return -1, -1
|
||||
}
|
||||
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
|
||||
return int(bufferInfo.CursorPosition.Y), int(bufferInfo.CursorPosition.X)
|
||||
}
|
||||
|
||||
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||
|
||||
686
src/tui/tcell.go
686
src/tui/tcell.go
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,16 @@
|
||||
// +build tcell windows
|
||||
//go:build tcell || windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell"
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/junegunn/fzf/src/util"
|
||||
)
|
||||
|
||||
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||
func assert(t *testing.T, context string, got any, want any) bool {
|
||||
if got == want {
|
||||
return true
|
||||
} else {
|
||||
@@ -20,7 +21,7 @@ func assert(t *testing.T, context string, got interface{}, want interface{}) boo
|
||||
|
||||
// Test the handling of the tcell keyboard events.
|
||||
func TestGetCharEventKey(t *testing.T) {
|
||||
if util.ToTty() {
|
||||
if util.IsTty(os.Stdout) {
|
||||
// This test is skipped when output goes to terminal, because it causes
|
||||
// some glitches:
|
||||
// - output lines may not start at the beginning of a row which makes
|
||||
@@ -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}}, // fabricated, unhandled
|
||||
// KeyEnter is alias for KeyCR
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{CtrlM, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlM, rune(tcell.KeyCtrlM), tcell.ModNone}, wantKey{Enter, 0, nil}}, // actual "Enter" keystroke
|
||||
{giveKey{tcell.KeyCR, rune(tcell.KeyCR), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEnter, rune(tcell.KeyEnter), tcell.ModNone}, wantKey{Enter, 0, nil}}, // fabricated, unhandled
|
||||
// Ctrl+Alt keys
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'a', nil}}, // fabricated
|
||||
@@ -102,22 +103,24 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// KeyBackspace2 is alias for KeyDEL = 0x7F (ASCII) (allegedly unused by Windows)
|
||||
// KeyDelete = 0x2E (VK_DELETE constant in Windows)
|
||||
// KeyBackspace is alias for KeyBS = 0x08 (ASCII) (implicit alias with KeyCtrlH)
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{BSpace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Del, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{Del, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyBackspace2, 0, tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyDEL, 0, tcell.ModNone}, wantKey{Backspace, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}},
|
||||
{giveKey{tcell.KeyBackspace, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyBS, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{BSpace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBS, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{BSpace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModNone}, wantKey{Backspace, 0, nil}}, // actual "Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyDEL, rune(tcell.KeyDEL), tcell.ModCtrl}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift}, wantKey{Backspace, 0, nil}}, // actual "Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlBackspace, 0, nil}}, // actual "Ctrl+Shift+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModShift | tcell.ModAlt}, wantKey{AltBackspace, 0, nil}}, // actual "Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, 0, tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAltBackspace, 0, nil}}, // actual "Ctrl+Shift+Alt+Backspace" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Alt+H" keystroke
|
||||
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlH, 0, nil}}, // actual "Ctrl+Shift+H" keystroke
|
||||
@@ -125,9 +128,41 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
|
||||
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModNone}, wantKey{Down, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModNone}, wantKey{Right, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModNone}, wantKey{Left, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl}, wantKey{CtrlUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl}, wantKey{CtrlDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl}, wantKey{CtrlRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl}, wantKey{CtrlLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModShift}, wantKey{ShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModShift}, wantKey{ShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift}, wantKey{ShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{ShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModAlt}, wantKey{AltUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModAlt}, wantKey{AltRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModAlt}, wantKey{AltLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftUp, 0, nil}},
|
||||
{giveKey{tcell.KeyDown, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDown, 0, nil}},
|
||||
{giveKey{tcell.KeyRight, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftRight, 0, nil}},
|
||||
{giveKey{tcell.KeyLeft, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftLeft, 0, nil}},
|
||||
{giveKey{tcell.KeyUpLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyUpRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyDownLeft, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||
@@ -136,6 +171,46 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||
{giveKey{tcell.KeyInsert, 0, tcell.ModNone}, wantKey{Insert, 0, nil}},
|
||||
{giveKey{tcell.KeyF1, 0, tcell.ModNone}, wantKey{F1, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModNone}, wantKey{Home, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModNone}, wantKey{End, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModNone}, wantKey{Delete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModNone}, wantKey{PageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModNone}, wantKey{PageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl}, wantKey{CtrlHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl}, wantKey{CtrlEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl}, wantKey{CtrlDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl}, wantKey{CtrlPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl}, wantKey{CtrlPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModShift}, wantKey{ShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModShift}, wantKey{ShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModShift}, wantKey{ShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift}, wantKey{ShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift}, wantKey{ShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModAlt}, wantKey{AltHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModAlt}, wantKey{AltEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModAlt}, wantKey{AltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModAlt}, wantKey{AltPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModAlt}, wantKey{AltPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift}, wantKey{CtrlShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModAlt}, wantKey{CtrlAltPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltShiftPageDown, 0, nil}},
|
||||
{giveKey{tcell.KeyHome, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftHome, 0, nil}},
|
||||
{giveKey{tcell.KeyEnd, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftEnd, 0, nil}},
|
||||
{giveKey{tcell.KeyDelete, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftDelete, 0, nil}},
|
||||
{giveKey{tcell.KeyPgUp, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageUp, 0, nil}},
|
||||
{giveKey{tcell.KeyPgDn, 0, tcell.ModCtrl | tcell.ModShift | tcell.ModAlt}, wantKey{CtrlAltShiftPageDown, 0, nil}},
|
||||
// section 6: (Ctrl+Alt)+'rune'
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModNone}, wantKey{Rune, 'a', nil}},
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModCtrl}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
@@ -161,11 +236,11 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
|
||||
// section 7: Esc
|
||||
// KeyEsc and KeyEscape are aliases for KeyESC
|
||||
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{ESC, 0, nil}}, // unhandled
|
||||
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // actual Ctrl+[ keystroke
|
||||
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{ESC, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyEsc, rune(tcell.KeyEsc), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModNone}, wantKey{Esc, 0, nil}}, // unhandled
|
||||
{giveKey{tcell.KeyEscape, rune(tcell.KeyEscape), tcell.ModNone}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
|
||||
{giveKey{tcell.KeyESC, rune(tcell.KeyESC), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // actual Ctrl+[ keystroke
|
||||
{giveKey{tcell.KeyCtrlLeftSq, rune(tcell.KeyCtrlLeftSq), tcell.ModCtrl}, wantKey{Esc, 0, nil}}, // fabricated, unhandled
|
||||
|
||||
// section 8: Invalid
|
||||
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||
@@ -182,6 +257,7 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
r.Init()
|
||||
|
||||
// run and evaluate the tests
|
||||
initialResizeAsInvalid := true
|
||||
for _, test := range tests {
|
||||
// generate key event
|
||||
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
|
||||
@@ -191,7 +267,12 @@ func TestGetCharEventKey(t *testing.T) {
|
||||
// process the event in fzf and evaluate the test
|
||||
gotEvent := r.GetChar()
|
||||
// skip Resize events, those are sometimes put in the buffer outside of this test
|
||||
for gotEvent.Type == Resize {
|
||||
if initialResizeAsInvalid && gotEvent.Type == Invalid {
|
||||
t.Logf("Resize as Invalid swallowed")
|
||||
initialResizeAsInvalid = false
|
||||
gotEvent = r.GetChar()
|
||||
}
|
||||
if gotEvent.Type == Resize {
|
||||
t.Logf("Resize swallowed")
|
||||
gotEvent = r.GetChar()
|
||||
}
|
||||
@@ -230,7 +311,7 @@ Quick reference
|
||||
10 1 KeyCtrlJ KeyLF = ^J CtrlJ
|
||||
11 1 KeyCtrlK KeyVT = ^K CtrlK
|
||||
12 1 KeyCtrlL KeyFF = ^L CtrlL
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter CtrlM
|
||||
13 1 KeyCtrlM KeyCR = ^M KeyEnter Enter
|
||||
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||
@@ -257,7 +338,7 @@ Quick reference
|
||||
37 LeftClick
|
||||
38 RightClick
|
||||
39 BTab
|
||||
40 BSpace
|
||||
40 Backspace
|
||||
41 Del
|
||||
42 PgUp
|
||||
43 PgDn
|
||||
@@ -270,7 +351,7 @@ Quick reference
|
||||
50 Insert
|
||||
51 SUp
|
||||
52 SDown
|
||||
53 SLeft
|
||||
53 ShiftLeft
|
||||
54 SRight
|
||||
55 F1
|
||||
56 F2
|
||||
@@ -286,15 +367,15 @@ Quick reference
|
||||
66 F12
|
||||
67 Change
|
||||
68 BackwardEOF
|
||||
69 AltBS
|
||||
69 AltBackspace
|
||||
70 AltUp
|
||||
71 AltDown
|
||||
72 AltLeft
|
||||
73 AltRight
|
||||
74 AltSUp
|
||||
75 AltSDown
|
||||
76 AltSLeft
|
||||
77 AltSRight
|
||||
76 AltShiftLeft
|
||||
77 AltShiftRight
|
||||
78 Alt
|
||||
79 CtrlAlt
|
||||
..
|
||||
|
||||
@@ -1,47 +1,54 @@
|
||||
// +build !windows
|
||||
//go:build !windows
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
||||
|
||||
var tty atomic.Value
|
||||
|
||||
func ttyname() string {
|
||||
if cached := tty.Load(); cached != nil {
|
||||
return cached.(string)
|
||||
}
|
||||
|
||||
var stderr syscall.Stat_t
|
||||
if syscall.Fstat(2, &stderr) != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, prefix := range devPrefixes {
|
||||
files, err := ioutil.ReadDir(prefix)
|
||||
files, err := os.ReadDir(prefix)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if stat, ok := file.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
return prefix + file.Name()
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||
value := prefix + file.Name()
|
||||
tty.Store(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn returns terminal device to be used as STDIN, falls back to os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
in, err := os.OpenFile(consoleDevice, syscall.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
tty := ttyname()
|
||||
if len(tty) > 0 {
|
||||
if in, err := os.OpenFile(tty, syscall.O_RDONLY, 0); err == nil {
|
||||
return in
|
||||
}
|
||||
}
|
||||
return os.Stdin
|
||||
}
|
||||
return in
|
||||
// TtyIn returns terminal device to read user input
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return openTtyIn(ttyDefault)
|
||||
}
|
||||
|
||||
// TtyOut returns terminal device to write to
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return openTtyOut(ttyDefault)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
// +build windows
|
||||
//go:build windows
|
||||
|
||||
package tui
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func ttyname() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// TtyIn on Windows returns os.Stdin
|
||||
func TtyIn() *os.File {
|
||||
return os.Stdin
|
||||
func TtyIn(ttyDefault string) (*os.File, error) {
|
||||
return os.Stdin, nil
|
||||
}
|
||||
|
||||
// TtyOut on Windows returns nil
|
||||
func TtyOut(ttyDefault string) (*os.File, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
1003
src/tui/tui.go
1003
src/tui/tui.go
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user