mirror of
https://github.com/junegunn/fzf.git
synced 2025-11-16 07:13:48 -05:00
Compare commits
1154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
dc975e8974 | ||
|
|
4311ade535 | ||
|
|
cd23401411 | ||
|
|
176ee6910f | ||
|
|
13c8f3d3aa | ||
|
|
ce9af687bc | ||
|
|
43f0d0cacd | ||
|
|
20b4e6953e | ||
|
|
7da287e3aa | ||
|
|
205f885d69 | ||
|
|
3715cd349d | ||
|
|
e4c3ecc57e | ||
|
|
673c5d886d | ||
|
|
f799b568d1 | ||
|
|
7bff4661f6 | ||
|
|
ffd8bef808 | ||
|
|
02cee2234d | ||
|
|
e0dd2be3fb | ||
|
|
a33c011c21 | ||
|
|
7c3f42bbba | ||
|
|
edac9820b5 | ||
|
|
84a47f7102 | ||
|
|
97ae8afb6f | ||
|
|
4138333f5c | ||
|
|
61339a8ae2 | ||
|
|
50eb2e3855 | ||
|
|
5fc78e4584 | ||
|
|
2736a2f69e | ||
|
|
179993f0cd | ||
|
|
b734f657f9 | ||
|
|
c29d7d02c2 | ||
|
|
3df6b2a58c | ||
|
|
b8aa2d2c32 | ||
|
|
0ff885461b | ||
|
|
ca43f95fb1 | ||
|
|
09700f676b | ||
|
|
4271e9cffa | ||
|
|
f3dc8a10d5 | ||
|
|
00fb486f6a | ||
|
|
4173e94c6f | ||
|
|
261d3d3340 | ||
|
|
15e20fcae1 | ||
|
|
f4f47f5fe3 | ||
|
|
71d11de7ca | ||
|
|
88d74a15aa | ||
|
|
0f02fc0c77 | ||
|
|
3f90fb42d8 | ||
|
|
9bd8988300 | ||
|
|
3c804bcfec | ||
|
|
cca4cdc4f1 | ||
|
|
8f899aaf8a | ||
|
|
e53b4bb439 | ||
|
|
ab247a1309 | ||
|
|
c21e9edad4 | ||
|
|
9c21a20f8b | ||
|
|
7191ebb615 | ||
|
|
a74731d7f5 | ||
|
|
e086f0b3fe | ||
|
|
8255aa23f4 | ||
|
|
a4bc08f5a3 | ||
|
|
7e5aa1e2a5 | ||
|
|
0818dbc36a | ||
|
|
347c4b2625 | ||
|
|
34f0d4d0c4 | ||
|
|
cbedb57511 | ||
|
|
9ef825d2fd | ||
|
|
85ae745910 | ||
|
|
7411da8d5a | ||
|
|
3f75a8369f | ||
|
|
4cd621e877 | ||
|
|
6e3a2fe0bf | ||
|
|
8b0e1f941a | ||
|
|
c7c5e7670a | ||
|
|
f6c621ef1b | ||
|
|
faf32d451d | ||
|
|
252fd7ecb1 | ||
|
|
7fa89dddb4 | ||
|
|
fefdb8c84e | ||
|
|
a6cc05936e | ||
|
|
b209843545 | ||
|
|
19759ed36e | ||
|
|
1a7ae8e7b9 | ||
|
|
da1f645670 | ||
|
|
3a2015ee26 | ||
|
|
c440418ce6 | ||
|
|
3d37a5ba1d | ||
|
|
15f4cfb6d9 | ||
|
|
be36de2482 | ||
|
|
391237f7df | ||
|
|
977e5effd9 | ||
|
|
8b36a4cb19 | ||
|
|
c8cd94a772 | ||
|
|
764316a53d | ||
|
|
2048fd4042 | ||
|
|
f84b3de24b | ||
|
|
6a1f3ec08b | ||
|
|
2e353aee96 | ||
|
|
8edfd14a37 | ||
|
|
1a191ec6f7 | ||
|
|
e7171e94b4 | ||
|
|
398d937419 | ||
|
|
34fe5ab143 | ||
|
|
1b08f43f82 | ||
|
|
b24a2e2fdc | ||
|
|
4c4c6e626e | ||
|
|
7310370a31 | ||
|
|
8ae94f0059 | ||
|
|
8fccf20892 | ||
|
|
5a874ae241 | ||
|
|
f4e1ed25f2 | ||
|
|
cbfbb49ab4 | ||
|
|
489b16efce | ||
|
|
b82c1693c0 | ||
|
|
019bfc4e35 | ||
|
|
dfda5c054a | ||
|
|
f657169616 | ||
|
|
4c06da8b70 | ||
|
|
9fe2393a00 | ||
|
|
e2e8d94b14 | ||
|
|
4f9a7f8c87 | ||
|
|
bb0502ff44 | ||
|
|
c256442245 | ||
|
|
1137404190 | ||
|
|
d57c6d0284 | ||
|
|
76bbf57b3d | ||
|
|
806a47a7cc | ||
|
|
29851c18aa | ||
|
|
dea950c2c8 | ||
|
|
a367dfb22e | ||
|
|
9fe1a7b373 | ||
|
|
8e2d21c548 | ||
|
|
bedf1cd357 | ||
|
|
13f180a70c | ||
|
|
6654239c94 | ||
|
|
1b61e5e9e9 | ||
|
|
43b3b907f8 | ||
|
|
fcd896508b | ||
|
|
f55c990e86 | ||
|
|
d110372f99 | ||
|
|
c862af09f2 | ||
|
|
1cfeec0ca3 | ||
|
|
a0649edc1e | ||
|
|
0e0bcb3e10 | ||
|
|
686528d627 | ||
|
|
3afa920151 | ||
|
|
32c493e994 | ||
|
|
1a76bdf891 | ||
|
|
af48b3df29 | ||
|
|
58ac1fb2fa | ||
|
|
e922704f72 | ||
|
|
c6115735c7 | ||
|
|
9ddf5c72be | ||
|
|
cc5640326b | ||
|
|
bf447d7703 | ||
|
|
cbb008c938 | ||
|
|
eaa0c52b45 | ||
|
|
82791f7efc | ||
|
|
8c533e34ea | ||
|
|
37708ad9cd | ||
|
|
090dee857f | ||
|
|
d779ff7e6d | ||
|
|
fd8858f8c9 | ||
|
|
b234647a63 | ||
|
|
6e93eefc82 | ||
|
|
38fca30125 | ||
|
|
012ee9ca85 | ||
|
|
151252e33a | ||
|
|
7136cfc68b | ||
|
|
408c04f25f | ||
|
|
7f8e0dbc40 | ||
|
|
0de7ab18f6 | ||
|
|
e9bc7331bd | ||
|
|
797dd7c449 | ||
|
|
f37ccaa64f | ||
|
|
ab3937ee5a | ||
|
|
00f4551a7b | ||
|
|
257ddd028d | ||
|
|
e0a22e76f8 | ||
|
|
00a3610331 | ||
|
|
f502725120 | ||
|
|
b62a74b315 | ||
|
|
2ec382ae0e | ||
|
|
cbfee31593 | ||
|
|
6d647e13ff | ||
|
|
d2af3ff98d | ||
|
|
052d17e66a | ||
|
|
a9bc954e17 | ||
|
|
2983426771 | ||
|
|
c61eb94b3f | ||
|
|
3829eab1cf | ||
|
|
3fe8eeedc5 | ||
|
|
1efef88b6e | ||
|
|
7acdaf0b43 | ||
|
|
1ed25d76ba | ||
|
|
474c1f5e32 | ||
|
|
8b71fea5dc | ||
|
|
7bd99a22ee | ||
|
|
75b8cca3b3 | ||
|
|
ef2c29d5d4 | ||
|
|
218b3c8274 | ||
|
|
db9cb2ddda | ||
|
|
722d66e85a | ||
|
|
f6269f0193 | ||
|
|
520eae817a | ||
|
|
d099941360 | ||
|
|
e3e76fa8c5 | ||
|
|
2553806e79 | ||
|
|
1bcbc5a353 | ||
|
|
15d351b0f0 | ||
|
|
c144c95cda | ||
|
|
f08f4fd87d | ||
|
|
f8aaeef218 | ||
|
|
7915e365b3 | ||
|
|
1c68f81c37 | ||
|
|
d4c9db0a27 | ||
|
|
b5e0e29ec6 | ||
|
|
569be4c6c9 | ||
|
|
e7ca237b07 | ||
|
|
a7d3b72117 | ||
|
|
3ba7b5cf2d | ||
|
|
254e9765fe | ||
|
|
3304f284a5 | ||
|
|
0d5f862daf | ||
|
|
51dfacd542 | ||
|
|
c691d52fa7 | ||
|
|
de3d09fe79 | ||
|
|
eaa413c566 | ||
|
|
407205e52b | ||
|
|
552414978e | ||
|
|
607081bbaa | ||
|
|
e73383fbbb | ||
|
|
2e8e63fb0b | ||
|
|
874f7dd416 | ||
|
|
8b0e3b1624 | ||
|
|
9b946f2b7a | ||
|
|
11841f688b | ||
|
|
03c4f04246 | ||
|
|
a1f06ae27f | ||
|
|
69dffd78a6 | ||
|
|
2750e19657 | ||
|
|
b0987f727b | ||
|
|
a4d9b0b468 | ||
|
|
e2b87e0d74 | ||
|
|
2166b4ca17 | ||
|
|
d2d4d68585 | ||
|
|
faf68dbc5c | ||
|
|
305896fcb3 | ||
|
|
6c9adea0d3 | ||
|
|
fc7630a66d | ||
|
|
3248153d9f | ||
|
|
246b9f3130 | ||
|
|
865144850d | ||
|
|
d9752a4c21 | ||
|
|
dba14d2630 | ||
|
|
2986e64a49 | ||
|
|
1d8bd11b67 | ||
|
|
bafb99d520 | ||
|
|
3cc8a74a91 | ||
|
|
c0aa5a438f | ||
|
|
825d401403 | ||
|
|
9dfca77c36 | ||
|
|
82c4af2902 | ||
|
|
736344e151 | ||
|
|
6f9663da62 | ||
|
|
f8ae1786dd | ||
|
|
c60ed17583 | ||
|
|
e0f0b5bcf9 | ||
|
|
9e96073128 | ||
|
|
0db65c22d3 | ||
|
|
d785135606 | ||
|
|
ae15eda546 | ||
|
|
f2d44ab5a7 | ||
|
|
43798fc2e8 | ||
|
|
9dc4b40d7a | ||
|
|
1cb19dbf65 | ||
|
|
1ab4289ad6 | ||
|
|
e2ae1b249c | ||
|
|
92b7efafca | ||
|
|
f092e4038f | ||
|
|
aa5dae391b | ||
|
|
08a6fd4ad4 | ||
|
|
a61150a96c | ||
|
|
0f9cb5590e | ||
|
|
c0a83b27eb | ||
|
|
f79b1f71b8 | ||
|
|
8e027c445f | ||
|
|
dda3e3c39a | ||
|
|
fd5157998c | ||
|
|
e0217e8c79 | ||
|
|
3ab1c42266 | ||
|
|
d1676776aa | ||
|
|
bdde69d011 | ||
|
|
6dec42a33a | ||
|
|
199bc3f0ad | ||
|
|
17dd833925 | ||
|
|
4ec144c969 | ||
|
|
3e36f2b0ac | ||
|
|
07a03b3e73 | ||
|
|
c33258832e | ||
|
|
a7aa08ce07 | ||
|
|
06d63a862e | ||
|
|
43d1c4c4b5 | ||
|
|
f81feb1e69 | ||
|
|
01cf01e084 | ||
|
|
97a725fbd0 | ||
|
|
ace92ba281 | ||
|
|
d631c76e8d | ||
|
|
e6d33f77da | ||
|
|
a6d3e3687b | ||
|
|
08c2bcb952 | ||
|
|
98ca4bdede | ||
|
|
3f8e741562 | ||
|
|
6e464ebd9b | ||
|
|
c329279339 | ||
|
|
cf04753ad7 | ||
|
|
69e7eab11f | ||
|
|
dea206b023 | ||
|
|
5deaf58928 | ||
|
|
15e2952a2b | ||
|
|
a9fba1c849 | ||
|
|
71e573d082 | ||
|
|
334a4fa159 | ||
|
|
21f94ee800 | ||
|
|
540bfd7a72 | ||
|
|
8fbed2b13a | ||
|
|
aa17510e0a | ||
|
|
bf65e8cd12 | ||
|
|
0f5c6e8f04 | ||
|
|
b1b916ce15 | ||
|
|
a6a732e1fc | ||
|
|
a5c2f28539 | ||
|
|
18261fe31c | ||
|
|
079046863c | ||
|
|
07b965bba1 | ||
|
|
c39113ee41 | ||
|
|
14f90502a4 | ||
|
|
b0673c3563 | ||
|
|
373c6d8d55 | ||
|
|
b8fc828955 | ||
|
|
b43b040512 | ||
|
|
50b7608f9d | ||
|
|
7085e5b629 | ||
|
|
7d5985baf9 | ||
|
|
7c40a424c0 | ||
|
|
baf882ace7 | ||
|
|
ba82f0bef9 | ||
|
|
d9c6a0305b | ||
|
|
d9b1211191 | ||
|
|
99f1e02766 | ||
|
|
242c0db26b | ||
|
|
dd49e41c42 | ||
|
|
6db15e8693 | ||
|
|
4c9cab3f8a | ||
|
|
b2c0413a98 | ||
|
|
e34c7c00b1 | ||
|
|
7c447bbdc7 | ||
|
|
7bf1f2cc84 | ||
|
|
afa2c4e0af | ||
|
|
2ff7db1b36 | ||
|
|
9f0626da64 | ||
|
|
d8cb5c1cf5 | ||
|
|
dca56da0ef | ||
|
|
ec75d16ea8 | ||
|
|
5cae8ea733 | ||
|
|
1ccd8f6a64 | ||
|
|
9c293bb82b | ||
|
|
9897ee9591 | ||
|
|
5215415315 | ||
|
|
54891d11e0 | ||
|
|
567c8303bf | ||
|
|
2a60edcd52 | ||
|
|
d61ac32d7b | ||
|
|
b57e6cff7e | ||
|
|
5b99f19dac | ||
|
|
6c03571887 | ||
|
|
4fb410a93c | ||
|
|
5e1db9fdd3 | ||
|
|
9d7480ae3c | ||
|
|
f39cf6d855 | ||
|
|
001d116884 | ||
|
|
02c5e62efe | ||
|
|
446df07b62 | ||
|
|
8583b150c9 | ||
|
|
a859aa72ee | ||
|
|
0896036266 | ||
|
|
311b78ae82 | ||
|
|
f5cf4fc8fb | ||
|
|
7ceb58b2aa | ||
|
|
293dd76af1 | ||
|
|
3918c45ced | ||
|
|
4ec403347c | ||
|
|
e01266ffcb | ||
|
|
f246fb2fc2 | ||
|
|
f7b26b34cb | ||
|
|
a1bcdc225e | ||
|
|
7771241cc0 | ||
|
|
6e3af646b2 | ||
|
|
82bf8c138d | ||
|
|
e21b001116 | ||
|
|
577024f1e9 | ||
|
|
d4ad4a25db | ||
|
|
30577b0c17 | ||
|
|
212de25409 | ||
|
|
5da8bbf45a | ||
|
|
aa0e10ead7 | ||
|
|
a9906c7c29 | ||
|
|
9fefe08b3f | ||
|
|
684bfff713 | ||
|
|
3db6b88d82 | ||
|
|
8ae96774df | ||
|
|
f68017d21e | ||
|
|
2b725a4db5 | ||
|
|
af1a5f130b | ||
|
|
86e3994e87 | ||
|
|
1e6ac5590e | ||
|
|
5e42b1c9f8 | ||
|
|
9d842630c9 | ||
|
|
77cb906dfe | ||
|
|
a59e846f74 | ||
|
|
6e6340a0c9 | ||
|
|
357e82e51b | ||
|
|
394d8cfd18 | ||
|
|
ef80bd401f | ||
|
|
f51d61d57a | ||
|
|
1dd256a68a | ||
|
|
85644aa3fb | ||
|
|
effbc258bb | ||
|
|
e615600ff1 | ||
|
|
60465c4664 | ||
|
|
c03c058bd5 | ||
|
|
7238c8944d | ||
|
|
9a41fd5327 | ||
|
|
b471042037 | ||
|
|
2886f06977 | ||
|
|
d630484eeb | ||
|
|
e24299239e | ||
|
|
d2fa470165 | ||
|
|
168453da71 | ||
|
|
23a06d63ac | ||
|
|
751aa1944a | ||
|
|
05b5f3f845 | ||
|
|
16fc6862a8 | ||
|
|
7e1c0f39e7 | ||
|
|
deccf20a35 | ||
|
|
73c0a645e0 | ||
|
|
e975bd0c8d | ||
|
|
78da928727 | ||
|
|
11962dabba | ||
|
|
dceb5d09cd | ||
|
|
b4cccf23d4 | ||
|
|
b911af200c | ||
|
|
68683c444f | ||
|
|
a185593d65 | ||
|
|
525040238e | ||
|
|
33f89a08f3 | ||
|
|
11645e1fac | ||
|
|
6390140539 | ||
|
|
072066c49c | ||
|
|
a2e9366c84 | ||
|
|
391669a451 | ||
|
|
0c6c76e081 | ||
|
|
f1520bdde6 | ||
|
|
3089880f18 | ||
|
|
ab11b74be4 | ||
|
|
a5a97be017 | ||
|
|
80b5bc1b68 | ||
|
|
5c7dcaffe8 | ||
|
|
5095899245 | ||
|
|
4800e5d2ae | ||
|
|
3b1e37f718 | ||
|
|
6577388250 | ||
|
|
3b9dbd4146 | ||
|
|
a1260feeed | ||
|
|
7322504ad0 | ||
|
|
de569f0052 | ||
|
|
e7097a9d25 | ||
|
|
c1dbc800e5 | ||
|
|
951746297e | ||
|
|
984304568d | ||
|
|
723217bdea | ||
|
|
0fdb71f7e4 | ||
|
|
12ce76b56a | ||
|
|
0030d18448 | ||
|
|
0e3e6ac442 | ||
|
|
430e8193e0 | ||
|
|
03e8ed4d88 | ||
|
|
ef492f6178 | ||
|
|
8eea45ef50 | ||
|
|
ff951341c9 | ||
|
|
df570afd52 | ||
|
|
07d755df11 | ||
|
|
37585bd5a5 | ||
|
|
89e24bf8f2 | ||
|
|
8d2fcd3518 | ||
|
|
f39ab3875e | ||
|
|
82efe6c60d | ||
|
|
75972d59a8 | ||
|
|
e7d60aac9c | ||
|
|
a0bfbdd49c | ||
|
|
ba594982f0 | ||
|
|
2157f4f193 | ||
|
|
309bae423c | ||
|
|
4f8bf2ae78 | ||
|
|
85c1f8a9e0 | ||
|
|
e00e7e1e56 | ||
|
|
1a6defdbcc | ||
|
|
ef577a6509 | ||
|
|
b7c6838e45 | ||
|
|
91d04cec5c | ||
|
|
3bd8441079 | ||
|
|
8cf45a5197 | ||
|
|
8dc1377efb | ||
|
|
6c32148f90 | ||
|
|
315e568de0 | ||
|
|
5d16b28869 | ||
|
|
5624a89231 | ||
|
|
63c42b14f2 | ||
|
|
6f1eaa9b39 | ||
|
|
ca42e5e00a | ||
|
|
61feee690c | ||
|
|
d4ed955aee | ||
|
|
b46227dcb6 | ||
|
|
fd8d371ac7 | ||
|
|
0e06e298d4 | ||
|
|
72df905902 | ||
|
|
0d748a0699 | ||
|
|
27c40dc6b0 | ||
|
|
8e34e6fbb4 | ||
|
|
3bc98ed623 | ||
|
|
70a92a858a | ||
|
|
49d04374a4 | ||
|
|
8540902a35 | ||
|
|
8c6fcee3ca | ||
|
|
13803d0dbb | ||
|
|
423986996a | ||
|
|
1c9e7b7ea6 | ||
|
|
6de1ad9d3d | ||
|
|
5004ae3457 | ||
|
|
e67cc75063 | ||
|
|
0edbcbdf19 | ||
|
|
f0fe79dd3b | ||
|
|
daa1958f86 | ||
|
|
2c26f02f5c | ||
|
|
af87650bc4 | ||
|
|
2b19c0bc68 | ||
|
|
76a2dcb5a9 | ||
|
|
68ec3d1c10 | ||
|
|
2ff19084ca | ||
|
|
62f062ecfa | ||
|
|
cce17ad0a0 | ||
|
|
b8296a91b9 | ||
|
|
6e9452b06e | ||
|
|
888fd35689 | ||
|
|
1fb0fbca58 | ||
|
|
ddd2a109e4 | ||
|
|
87504a528e | ||
|
|
6eac4af7db | ||
|
|
89de1340af | ||
|
|
9e753a0d44 | ||
|
|
f57920ad90 | ||
|
|
7dbbbef51a | ||
|
|
7add75126d | ||
|
|
d207672bd5 | ||
|
|
851fa38251 | ||
|
|
43345fb642 | ||
|
|
9ff33814ea | ||
|
|
21b94d2de5 | ||
|
|
24236860c8 | ||
|
|
3f868fd792 | ||
|
|
417bca03df | ||
|
|
cce6aef557 | ||
|
|
eb3afc03b5 | ||
|
|
7f0caf0683 | ||
|
|
7f606665cb | ||
|
|
202872c2dc | ||
|
|
93aeae1985 | ||
|
|
5c34ab6692 | ||
|
|
390b49653b | ||
|
|
b877c385f0 | ||
|
|
9c47739c0e | ||
|
|
04aa2992e7 | ||
|
|
2f1edeff78 | ||
|
|
306d51cdcf | ||
|
|
54a026525a | ||
|
|
d6588fc835 | ||
|
|
5a7b41a2cf | ||
|
|
338a73d764 | ||
|
|
c20954f020 | ||
|
|
1e8e1d3c9d | ||
|
|
f6b1962056 | ||
|
|
b3b101a89c | ||
|
|
9615c4edf1 | ||
|
|
85a75ee035 | ||
|
|
1e5bd55672 | ||
|
|
37d4015d56 | ||
|
|
6b27554cdb | ||
|
|
fc1b119159 | ||
|
|
2cd0d4a9f7 | ||
|
|
fd03aabeb2 | ||
|
|
8068c975c2 | ||
|
|
a6d2ab3360 | ||
|
|
fe7b91dfd9 | ||
|
|
5784101bea | ||
|
|
eaf6eb8978 | ||
|
|
3af63bcf1f | ||
|
|
80a21f7a75 | ||
|
|
0b33dc6ce1 | ||
|
|
64a6ced62e | ||
|
|
438f6c96cd | ||
|
|
6ae085f974 | ||
|
|
cb8e97274e | ||
|
|
c4185e81e8 | ||
|
|
0580fe9046 | ||
|
|
1b1bc9ea36 | ||
|
|
c2614467cf | ||
|
|
077ae51f05 | ||
|
|
ee40212e97 | ||
|
|
7f5f6efbac | ||
|
|
45d4c57d91 | ||
|
|
41e0208335 | ||
|
|
2f8238342b | ||
|
|
e1582b8323 | ||
|
|
7cfa6f0265 | ||
|
|
e3973c74e7 | ||
|
|
a8deca2dd9 | ||
|
|
a78ade1771 | ||
|
|
79d2ef4616 | ||
|
|
5edc3f755c | ||
|
|
288976310b | ||
|
|
58b5be8ab6 | ||
|
|
26d7896877 | ||
|
|
fd6bc7308f | ||
|
|
6c41c95f28 | ||
|
|
446e04469d | ||
|
|
5097e563df | ||
|
|
c7ad97c641 | ||
|
|
9516fe3324 | ||
|
|
20cdbac8c3 | ||
|
|
e3e7b3360c | ||
|
|
655dfb8328 | ||
|
|
9b9c67b768 | ||
|
|
5b7457ff08 | ||
|
|
48adad5454 | ||
|
|
b27dc3eb17 | ||
|
|
e89eebb7ba | ||
|
|
fee404399a | ||
|
|
6b4805ca1a | ||
|
|
159699b5d7 | ||
|
|
af809c9661 | ||
|
|
329de8f416 | ||
|
|
e825b07e85 | ||
|
|
71fdb99a07 | ||
|
|
55ee4186aa | ||
|
|
941b0a0ff7 | ||
|
|
6aae12288e | ||
|
|
302cc552ef | ||
|
|
a2a4df0886 | ||
|
|
3399e39968 | ||
|
|
87874bba88 | ||
|
|
c304fc4333 | ||
|
|
6977cf268f | ||
|
|
931c78a70c | ||
|
|
8d23646fe6 | ||
|
|
656963e018 | ||
|
|
644277faf1 | ||
|
|
0558dfee79 | ||
|
|
487c8fe88f | ||
|
|
0d171ba1d8 | ||
|
|
2069bbc8b5 | ||
|
|
053d628b53 | ||
|
|
6bc592e6c9 | ||
|
|
6c76d8cd1c | ||
|
|
a09e411936 | ||
|
|
02a7b96f33 | ||
|
|
e55e029ae8 | ||
|
|
6b18b144cf | ||
|
|
6d53089cc1 | ||
|
|
e85a8a68d0 | ||
|
|
dc55e68524 | ||
|
|
462c68b625 | ||
|
|
999d374f0c | ||
|
|
b208aa675e | ||
|
|
2b98fee136 | ||
|
|
e5e75efebc | ||
|
|
4a4fef2daf | ||
|
|
ecb6b234cc | ||
|
|
39dbc8acdb | ||
|
|
a56489bc7f | ||
|
|
99927c7071 | ||
|
|
3e28403978 | ||
|
|
37370f057f | ||
|
|
f4b46fad27 | ||
|
|
9d2c6a95f4 | ||
|
|
376a76d1d3 | ||
|
|
1fcc07e54e | ||
|
|
8db3345c2f | ||
|
|
69aa2fea68 | ||
|
|
298749bfcd | ||
|
|
f1f31baae1 | ||
|
|
e1c8f19e8f | ||
|
|
5e302c70e9 | ||
|
|
4c5a679066 | ||
|
|
41f0b2c354 | ||
|
|
a0a3c349c9 | ||
|
|
bc3983181d | ||
|
|
980b58ef5a | ||
|
|
a2604c0963 | ||
|
|
6dbc108da2 | ||
|
|
bd98f988f0 | ||
|
|
06301c7847 | ||
|
|
18a1aeaa91 | ||
|
|
c9f16b6430 | ||
|
|
bc9d2abdb6 | ||
|
|
28810c178f | ||
|
|
a9e64efe45 | ||
|
|
6b5886c034 | ||
|
|
7727ad43af | ||
|
|
bbe10f4f77 | ||
|
|
5e72709613 | ||
|
|
9e85cba0d0 | ||
|
|
4b59ced08f | ||
|
|
8dbdd55730 | ||
|
|
6725151a99 | ||
|
|
d4f3d5a164 | ||
|
|
7b5ccc45bc | ||
|
|
940214a1a2 | ||
|
|
68bd410159 | ||
|
|
b13fcfd831 | ||
|
|
07ef2b051c | ||
|
|
3fc795340d | ||
|
|
70cfa6af13 | ||
|
|
dbcaec59ae | ||
|
|
faedae708e | ||
|
|
0c66521b23 | ||
|
|
bf92862459 | ||
|
|
1a68698d76 | ||
|
|
842a73357c | ||
|
|
5efdeccdbb | ||
|
|
050777b8c4 | ||
|
|
a4d78e2200 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: junegunn
|
||||||
28
.github/ISSUE_TEMPLATE.md
vendored
28
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,30 +1,22 @@
|
|||||||
|
<!-- ISSUES NOT FOLLOWING THIS TEMPLATE WILL BE CLOSED AND DELETED -->
|
||||||
|
|
||||||
<!-- Check all that apply [x] -->
|
<!-- Check all that apply [x] -->
|
||||||
- Category
|
|
||||||
- [ ] fzf binary
|
- [ ] I have read through the manual page (`man fzf`)
|
||||||
- [ ] fzf-tmux script
|
- [ ] I have the latest version of fzf
|
||||||
- [ ] Key bindings
|
- [ ] I have searched through the existing issues
|
||||||
- [ ] Completion
|
|
||||||
- [ ] Vim
|
## Info
|
||||||
- [ ] Neovim
|
|
||||||
- [ ] Etc.
|
|
||||||
- OS
|
- OS
|
||||||
- [ ] Linux
|
- [ ] Linux
|
||||||
- [ ] Mac OS X
|
- [ ] Mac OS X
|
||||||
- [ ] Windows
|
- [ ] Windows
|
||||||
- [ ] Windows Subsystem for Linux
|
|
||||||
- [ ] Etc.
|
- [ ] Etc.
|
||||||
- Shell
|
- Shell
|
||||||
- [ ] bash
|
- [ ] bash
|
||||||
- [ ] zsh
|
- [ ] zsh
|
||||||
- [ ] fish
|
- [ ] fish
|
||||||
|
|
||||||
<!--
|
## Problem / Steps to reproduce
|
||||||
### Before submitting
|
|
||||||
|
|
||||||
- Make sure that you have the latest version of fzf
|
|
||||||
- If you use tmux, make sure $TERM is set to screen or screen-256color
|
|
||||||
- For more Vim stuff, check out https://github.com/junegunn/fzf.vim
|
|
||||||
|
|
||||||
Describe your problem or suggestion from here ...
|
|
||||||
-->
|
|
||||||
|
|
||||||
|
|||||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
44
.github/workflows/codeql-analysis.yml
vendored
Normal file
44
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning
|
||||||
|
name: CodeQL
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, devel ]
|
||||||
|
pull_request:
|
||||||
|
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
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ['go']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v2
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Autobuild
|
||||||
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v2
|
||||||
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@v3
|
||||||
|
- name: 'Dependency Review'
|
||||||
|
uses: actions/dependency-review-action@v3
|
||||||
45
.github/workflows/linux.yml
vendored
Normal file
45
.github/workflows/linux.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Test fzf on Linux
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, devel ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.19
|
||||||
|
|
||||||
|
- name: Setup Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.1.0
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
run: sudo apt-get install --yes zsh fish tmux
|
||||||
|
|
||||||
|
- name: Install Ruby gems
|
||||||
|
run: sudo gem install --no-document minitest:5.17.0 rubocop:1.43.0 rubocop-minitest:0.25.1 rubocop-performance:1.15.2
|
||||||
|
|
||||||
|
- name: Rubocop
|
||||||
|
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||||
|
|
||||||
|
- 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
|
||||||
45
.github/workflows/macos.yml
vendored
Normal file
45
.github/workflows/macos.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
name: Test fzf on macOS
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, devel ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: 1.18
|
||||||
|
|
||||||
|
- name: Setup Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: 3.0.0
|
||||||
|
|
||||||
|
- name: Install packages
|
||||||
|
run: HOMEBREW_NO_INSTALL_CLEANUP=1 brew install fish zsh tmux
|
||||||
|
|
||||||
|
- name: Install Ruby gems
|
||||||
|
run: gem install --no-document minitest:5.14.2 rubocop:1.0.0 rubocop-minitest:0.10.1 rubocop-performance:1.8.1
|
||||||
|
|
||||||
|
- name: Rubocop
|
||||||
|
run: rubocop --require rubocop-minitest --require rubocop-performance
|
||||||
|
|
||||||
|
- 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
|
||||||
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@v3
|
||||||
|
|
||||||
|
- 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@v3
|
||||||
|
- uses: crate-ci/typos@v1.16.4
|
||||||
15
.github/workflows/winget.yml
vendored
Normal file
15
.github/workflows/winget.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
name: Publish to Winget
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: vedantmgoyal2009/winget-releaser@v2
|
||||||
|
with:
|
||||||
|
identifier: junegunn.fzf
|
||||||
|
version: ${{ github.event.release.tag_name }}
|
||||||
|
installers-regex: '-windows_(armv7|arm64|amd64)\.zip$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,7 +1,14 @@
|
|||||||
bin/fzf
|
bin/fzf
|
||||||
|
bin/fzf.exe
|
||||||
|
dist
|
||||||
target
|
target
|
||||||
pkg
|
pkg
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
.DS_Store
|
.DS_Store
|
||||||
doc/tags
|
doc/tags
|
||||||
vendor
|
vendor
|
||||||
|
gopath
|
||||||
|
*.zwc
|
||||||
|
fzf
|
||||||
|
tmp
|
||||||
|
*.patch
|
||||||
|
|||||||
126
.goreleaser.yml
Normal file
126
.goreleaser.yml
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
---
|
||||||
|
project_name: fzf
|
||||||
|
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- 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_v1/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:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- freebsd
|
||||||
|
- openbsd
|
||||||
|
goarch:
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
- loong64
|
||||||
|
- ppc64le
|
||||||
|
- s390x
|
||||||
|
goarm:
|
||||||
|
- 5
|
||||||
|
- 6
|
||||||
|
- 7
|
||||||
|
ldflags:
|
||||||
|
- "-s -w -X main.version={{ .Version }} -X main.revision={{ .ShortCommit }}"
|
||||||
|
ignore:
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm
|
||||||
|
- goos: openbsd
|
||||||
|
goarch: arm
|
||||||
|
- goos: freebsd
|
||||||
|
goarch: arm64
|
||||||
|
- goos: openbsd
|
||||||
|
goarch: arm64
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
|
||||||
|
builds:
|
||||||
|
- fzf
|
||||||
|
format: tar.gz
|
||||||
|
format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
files:
|
||||||
|
- non-existent*
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
extra_files:
|
||||||
|
- glob: ./dist/fzf-*darwin*.zip
|
||||||
|
|
||||||
|
release:
|
||||||
|
github:
|
||||||
|
owner: junegunn
|
||||||
|
name: fzf
|
||||||
|
prerelease: auto
|
||||||
|
name_template: '{{ .Tag }}'
|
||||||
|
extra_files:
|
||||||
|
- glob: ./dist/fzf-*darwin*.zip
|
||||||
|
|
||||||
|
snapshot:
|
||||||
|
name_template: "{{ .Tag }}-devel"
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- README
|
||||||
|
- test
|
||||||
32
.rubocop.yml
Normal file
32
.rubocop.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
Layout/LineLength:
|
||||||
|
Enabled: false
|
||||||
|
Metrics:
|
||||||
|
Enabled: false
|
||||||
|
Lint/ShadowingOuterLocalVariable:
|
||||||
|
Enabled: false
|
||||||
|
Style/MethodCallWithArgsParentheses:
|
||||||
|
Enabled: true
|
||||||
|
IgnoredMethods:
|
||||||
|
- assert
|
||||||
|
- exit
|
||||||
|
- paste
|
||||||
|
- puts
|
||||||
|
- raise
|
||||||
|
- refute
|
||||||
|
- require
|
||||||
|
- send_keys
|
||||||
|
IgnoredPatterns:
|
||||||
|
- ^assert_
|
||||||
|
- ^refute_
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
Style/StringConcatenation:
|
||||||
|
Enabled: false
|
||||||
|
Style/OptionalBooleanParameter:
|
||||||
|
Enabled: false
|
||||||
|
Style/WordArray:
|
||||||
|
MinSize: 1
|
||||||
|
Minitest/AssertEqual:
|
||||||
|
Enabled: false
|
||||||
|
Naming/VariableNumber:
|
||||||
|
Enabled: false
|
||||||
1
.tool-versions
Normal file
1
.tool-versions
Normal file
@@ -0,0 +1 @@
|
|||||||
|
golang 1.20.4
|
||||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
|||||||
language: ruby
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- env: TAGS=
|
|
||||||
rvm: 2.3.3
|
|
||||||
# - env: TAGS=tcell
|
|
||||||
# rvm: 2.2.0
|
|
||||||
|
|
||||||
install:
|
|
||||||
- sudo apt-get update
|
|
||||||
- sudo apt-get install -y libncurses-dev lib32ncurses5-dev libgpm-dev
|
|
||||||
- sudo add-apt-repository -y ppa:pi-rho/dev
|
|
||||||
- sudo apt-add-repository -y ppa:fish-shell/release-2
|
|
||||||
- sudo apt-get update
|
|
||||||
- sudo apt-get install -y tmux=1.9a-1~ppa1~p
|
|
||||||
- sudo apt-get install -y zsh fish
|
|
||||||
|
|
||||||
script: |
|
|
||||||
make test install &&
|
|
||||||
./install --all &&
|
|
||||||
tmux new "ruby test/test_go.rb > out && touch ok" && cat out && [ -e ok ]
|
|
||||||
612
ADVANCED.md
Normal file
612
ADVANCED.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
Advanced fzf examples
|
||||||
|
======================
|
||||||
|
|
||||||
|
* *Last update: 2023/05/26*
|
||||||
|
* *Requires fzf 0.41.0 or above*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc GFM -->
|
||||||
|
|
||||||
|
* [Introduction](#introduction)
|
||||||
|
* [Screen Layout](#screen-layout)
|
||||||
|
* [`--height`](#--height)
|
||||||
|
* [`fzf-tmux`](#fzf-tmux)
|
||||||
|
* [Popup window support](#popup-window-support)
|
||||||
|
* [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)
|
||||||
|
* [Ripgrep integration](#ripgrep-integration)
|
||||||
|
* [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)
|
||||||
|
* [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)
|
||||||
|
* [Color themes](#color-themes)
|
||||||
|
* [Generating fzf color theme from Vim color schemes](#generating-fzf-color-theme-from-vim-color-schemes)
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
|
Introduction
|
||||||
|
------------
|
||||||
|
|
||||||
|
fzf is an interactive [Unix filter][filter] program that is designed to be
|
||||||
|
used with other Unix tools. It reads a list of items from the standard input,
|
||||||
|
allows you to select a subset of the items, and prints the selected ones to
|
||||||
|
the standard output. You can think of it as an interactive version of *grep*,
|
||||||
|
and it's already useful even if you don't know any of its options.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. ps: Feed the list of processes to fzf
|
||||||
|
# 2. fzf: Interactively select a process using fuzzy matching algorithm
|
||||||
|
# 3. awk: Take the PID from the selected line
|
||||||
|
# 3. kill: Kill the process with the PID
|
||||||
|
ps -ef | fzf | awk '{print $2}' | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|
[filter]: https://en.wikipedia.org/wiki/Filter_(software)
|
||||||
|
|
||||||
|
While the above example succinctly summarizes the fundamental concept of fzf,
|
||||||
|
you can build much more sophisticated interactive workflows using fzf once you
|
||||||
|
learn its wide variety of features.
|
||||||
|
|
||||||
|
- To see the full list of options and features, see `man fzf`
|
||||||
|
- To see the latest additions, see [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
|
This document will guide you through some examples that will familiarize you
|
||||||
|
with the advanced features of fzf.
|
||||||
|
|
||||||
|
Screen Layout
|
||||||
|
-------------
|
||||||
|
|
||||||
|
### `--height`
|
||||||
|
|
||||||
|
fzf by default opens in fullscreen mode, but it's not always desirable.
|
||||||
|
Oftentimes, you want to see the current context of the terminal while using
|
||||||
|
fzf. `--height` is an option for opening fzf below the cursor in
|
||||||
|
non-fullscreen mode so you can still see the previous commands and their
|
||||||
|
results above it.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --height=40%
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You might also want to experiment with other layout options such as
|
||||||
|
`--layout=reverse`, `--info=inline`, `--border`, `--margin`, etc.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fzf --height=40% --layout=reverse
|
||||||
|
fzf --height=40% --layout=reverse --info=inline
|
||||||
|
fzf --height=40% --layout=reverse --info=inline --border
|
||||||
|
fzf --height=40% --layout=reverse --info=inline --border --margin=1
|
||||||
|
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)*
|
||||||
|
|
||||||
|
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
|
||||||
|
could write a wrapper script or shell alias, but there is an easier option.
|
||||||
|
Define `$FZF_DEFAULT_OPTS` like so:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
export FZF_DEFAULT_OPTS="--height=40% --layout=reverse --info=inline --border --margin=1 --padding=1"
|
||||||
|
```
|
||||||
|
|
||||||
|
### `fzf-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.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Open fzf on a tmux split pane below the current pane.
|
||||||
|
# Takes the same set of options.
|
||||||
|
fzf-tmux --layout=reverse
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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.)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# On the right (50%)
|
||||||
|
fzf-tmux -r
|
||||||
|
|
||||||
|
# On the left (30%)
|
||||||
|
fzf-tmux -l30%
|
||||||
|
|
||||||
|
# Above the cursor
|
||||||
|
fzf-tmux -u30%
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Popup window support
|
||||||
|
|
||||||
|
But here's the really cool part; tmux 3.2 added support for popup windows. So
|
||||||
|
you can open fzf in a popup window, which is quite useful if you frequently
|
||||||
|
use split panes.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 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%
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
> You might also want to check out my tmux plugins which support this popup
|
||||||
|
> window layout.
|
||||||
|
>
|
||||||
|
> - https://github.com/junegunn/tmux-fzf-url
|
||||||
|
> - https://github.com/junegunn/tmux-fzf-maccy
|
||||||
|
|
||||||
|
Dynamic reloading of the list
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
fzf can dynamically update the candidate list using an arbitrary program with
|
||||||
|
`reload` bindings (The design document for `reload` can be found
|
||||||
|
[here][reload]).
|
||||||
|
|
||||||
|
[reload]: https://github.com/junegunn/fzf/issues/1750
|
||||||
|
|
||||||
|
### Updating the list of processes by pressing CTRL-R
|
||||||
|
|
||||||
|
This example shows how you can set up a binding for dynamically updating the
|
||||||
|
list without restarting fzf.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
(date; ps -ef) |
|
||||||
|
fzf --bind='ctrl-r:reload(date; ps -ef)' \
|
||||||
|
--header=$'Press CTRL-R to reload\n\n' --header-lines=2 \
|
||||||
|
--preview='echo {}' --preview-window=down,3,wrap \
|
||||||
|
--layout=reverse --height=80% | awk '{print $2}' | xargs kill -9
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- The initial command is `(date; ps -ef)`. It prints the current date and
|
||||||
|
time, and the list of the processes.
|
||||||
|
- With `--header` option, you can show any message as the fixed header.
|
||||||
|
- To disallow selecting the first two lines (`date` and `ps` header), we use
|
||||||
|
`--header-lines=2` option.
|
||||||
|
- `--bind='ctrl-r:reload(date; ps -ef)'` binds CTRL-R to `reload` action that
|
||||||
|
runs `date; ps -ef`, so we can update the list of the processes by pressing
|
||||||
|
CTRL-R.
|
||||||
|
- We use simple `echo {}` preview option, so we can see the entire line on the
|
||||||
|
preview window below even if it's too long
|
||||||
|
|
||||||
|
### Toggling between data sources
|
||||||
|
|
||||||
|
You're not limited to just one reload binding. Set up multiple bindings so
|
||||||
|
you can switch between data sources.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
find * | fzf --prompt 'All> ' \
|
||||||
|
--header 'CTRL-D: Directories / CTRL-F: Files' \
|
||||||
|
--bind 'ctrl-d:change-prompt(Directories> )+reload(find * -type d)' \
|
||||||
|
--bind 'ctrl-f:change-prompt(Files> )+reload(find * -type f)'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Ripgrep integration
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
### Using fzf as the secondary filter
|
||||||
|
|
||||||
|
* Requires [bat][bat]
|
||||||
|
* Requires [Ripgrep][rg]
|
||||||
|
|
||||||
|
[bat]: https://github.com/sharkdp/bat
|
||||||
|
[rg]: https://github.com/BurntSushi/ripgrep
|
||||||
|
|
||||||
|
fzf is pretty fast for filtering a list that you will rarely have to think
|
||||||
|
about its performance. But it is not the right tool for searching for text
|
||||||
|
inside many large files, and in that case you should definitely use something
|
||||||
|
like [Ripgrep][rg].
|
||||||
|
|
||||||
|
In the next example, Ripgrep is the primary filter that searches for the given
|
||||||
|
text in files, and fzf is used as the secondary fuzzy filter that adds
|
||||||
|
interactivity to the workflow. And we use [bat][bat] to show the matching line in
|
||||||
|
the preview window.
|
||||||
|
|
||||||
|
This is a bash script and it will not run as expected on other non-compliant
|
||||||
|
shells. To avoid the compatibility issue, let's save this snippet as a script
|
||||||
|
file called `rfv`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 1. Search for text in files using Ripgrep
|
||||||
|
# 2. Interactively narrow down the list using fzf
|
||||||
|
# 3. Open the file in Vim
|
||||||
|
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.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Make the script executable
|
||||||
|
chmod +x rfv
|
||||||
|
|
||||||
|
# Run it with the initial query "algo"
|
||||||
|
./rfv algo
|
||||||
|
```
|
||||||
|
|
||||||
|
> Ripgrep will perform the initial search and list all the lines that contain
|
||||||
|
`algo`. Then we further narrow down the list on fzf.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
I know it's a lot to digest, let's try to break down the code.
|
||||||
|
|
||||||
|
- Ripgrep prints the matching lines in the following format
|
||||||
|
```
|
||||||
|
man/man1/fzf.1:54:.BI "--algo=" TYPE
|
||||||
|
man/man1/fzf.1:55:Fuzzy matching algorithm (default: v2)
|
||||||
|
man/man1/fzf.1:58:.BR v2 " Optimal scoring algorithm (quality)"
|
||||||
|
src/pattern_test.go:7: "github.com/junegunn/fzf/src/algo"
|
||||||
|
```
|
||||||
|
The first token delimited by `:` is the file path, and the second token is
|
||||||
|
the line number of the matching line. They respectively correspond to `{1}`
|
||||||
|
and `{2}` in the preview command.
|
||||||
|
- `--preview 'bat --color=always {1} --highlight-line {2}'`
|
||||||
|
- As we run `rg` with `--color=always` option, we should tell fzf to parse
|
||||||
|
ANSI color codes in the input by setting `--ansi`.
|
||||||
|
- We customize how fzf colors various text elements using `--color` option.
|
||||||
|
`-1` tells fzf to keep the original color from the input. See `man fzf` for
|
||||||
|
available color options.
|
||||||
|
- The value of `--preview-window` option consists of 5 components delimited
|
||||||
|
by `,`
|
||||||
|
1. `up` — Position of the preview window
|
||||||
|
1. `60%` — Size of the preview window
|
||||||
|
1. `border-bottom` — Preview window border only on the bottom side
|
||||||
|
1. `+{2}+3/3` — Scroll offset of the preview contents
|
||||||
|
1. `~3` — Fixed header
|
||||||
|
- Let's break down the latter two. We want to display the bat output in the
|
||||||
|
preview window with a certain scroll offset so that the matching line is
|
||||||
|
positioned near the center of the preview window.
|
||||||
|
- `+{2}` — The base offset is extracted from the second token
|
||||||
|
- `+3` — We add 3 lines to the base offset to compensate for the header
|
||||||
|
part of `bat` output
|
||||||
|
- ```
|
||||||
|
───────┬──────────────────────────────────────────────────────────
|
||||||
|
│ File: CHANGELOG.md
|
||||||
|
───────┼──────────────────────────────────────────────────────────
|
||||||
|
1 │ CHANGELOG
|
||||||
|
2 │ =========
|
||||||
|
3 │
|
||||||
|
4 │ 0.26.0
|
||||||
|
5 │ ------
|
||||||
|
```
|
||||||
|
- `/3` adjusts the offset so that the matching line is shown at a third
|
||||||
|
position in the window
|
||||||
|
- `~3` makes the top three lines fixed header so that they are always
|
||||||
|
visible regardless of the scroll offset
|
||||||
|
- 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}`).
|
||||||
|
|
||||||
|
[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
|
||||||
|
`reload` action to `change` event** so that whenever the user *changes* the
|
||||||
|
query string on fzf, `reload` action is triggered.
|
||||||
|
|
||||||
|
Here is a variation of the above `rfv` script. fzf will restart Ripgrep every
|
||||||
|
time the user updates the query string on fzf. Searching and filtering is
|
||||||
|
completely done by Ripgrep, and fzf merely provides the interactive interface.
|
||||||
|
So we lose the "fuzziness", but the performance will be better on larger
|
||||||
|
projects, and it will free up memory as you narrow down the results.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# 1. Search for text in files using Ripgrep
|
||||||
|
# 2. Interactively restart Ripgrep with reload action
|
||||||
|
# 3. Open the file in Vim
|
||||||
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
|
INITIAL_QUERY="${*:-}"
|
||||||
|
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
|
--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 the usual `rg ... | fzf` form, we start fzf with
|
||||||
|
an empty input (`: | fzf`), then we make it start the initial Ripgrep
|
||||||
|
process immediately via `start:reload` binding. This way, fzf owns the
|
||||||
|
initial Ripgrep process so it can kill it on the next `reload`. Otherwise,
|
||||||
|
the process will keep running in the background.
|
||||||
|
- 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
|
||||||
|
reduce the number of intermediate Ripgrep processes while we're typing in
|
||||||
|
a query.
|
||||||
|
|
||||||
|
### Switching to fzf-only search mode
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Two-phase filtering with Ripgrep and fzf
|
||||||
|
#
|
||||||
|
# 1. Search for text in files using Ripgrep
|
||||||
|
# 2. Interactively restart Ripgrep with reload action
|
||||||
|
# * Press alt-enter to switch to fzf-only filtering
|
||||||
|
# 3. Open the file in Vim
|
||||||
|
RG_PREFIX="rg --column --line-number --no-heading --color=always --smart-case "
|
||||||
|
INITIAL_QUERY="${*:-}"
|
||||||
|
: | fzf --ansi --disabled --query "$INITIAL_QUERY" \
|
||||||
|
--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
|
||||||
|

|
||||||
|
* Phase 2. Filtering with fzf
|
||||||
|

|
||||||
|
|
||||||
|
- We added `--prompt` option to show that fzf is initially running in "Ripgrep
|
||||||
|
launcher mode".
|
||||||
|
- We added `alt-enter` binding that
|
||||||
|
1. unbinds `change` event, so Ripgrep is no longer restarted on key press
|
||||||
|
2. changes the prompt to `2. fzf>`
|
||||||
|
3. enables search functionality of fzf
|
||||||
|
4. clears the current query string that was used to start Ripgrep process
|
||||||
|
5. and unbinds `alt-enter` itself as this is a one-off event
|
||||||
|
- 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
|
||||||
|
|
||||||
|
Log tailing
|
||||||
|
-----------
|
||||||
|
|
||||||
|
fzf can run long-running preview commands and render partial results before
|
||||||
|
completion. And when you specify `follow` flag in `--preview-window` option,
|
||||||
|
fzf will "`tail -f`" the result, automatically scrolling to the bottom.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With "follow", preview window will automatically scroll to the bottom.
|
||||||
|
# "\033[2J" is an ANSI escape sequence for clearing the screen.
|
||||||
|
# When fzf reads this code it clears the previous preview contents.
|
||||||
|
fzf --preview-window follow --preview 'for i in $(seq 100000); do
|
||||||
|
echo "$i"
|
||||||
|
sleep 0.01
|
||||||
|
(( i % 300 == 0 )) && printf "\033[2J"
|
||||||
|
done'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Admittedly, that was a silly example. Here's a practical one for browsing
|
||||||
|
Kubernetes pods.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pods() {
|
||||||
|
: | command='kubectl get pods --all-namespaces' fzf \
|
||||||
|
--info=inline --layout=reverse --header-lines=1 \
|
||||||
|
--prompt "$(kubectl config current-context | sed 's/-context$//')> " \
|
||||||
|
--header $'╱ Enter (kubectl exec) ╱ CTRL-O (open log in editor) ╱ CTRL-R (reload) ╱\n\n' \
|
||||||
|
--bind 'start:reload:$command' \
|
||||||
|
--bind 'ctrl-r:reload:$command' \
|
||||||
|
--bind 'ctrl-/:change-preview-window(80%,border-bottom|hidden|)' \
|
||||||
|
--bind 'enter:execute:kubectl exec -it --namespace {1} {2} -- bash > /dev/tty' \
|
||||||
|
--bind 'ctrl-o:execute:${EDITOR:-vim} <(kubectl logs --all-containers --namespace {1} {2}) > /dev/tty' \
|
||||||
|
--preview-window up:follow \
|
||||||
|
--preview 'kubectl logs --follow --all-containers --tail=10000 --namespace {1} {2}' "$@"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- The preview window will *"log tail"* the pod
|
||||||
|
- Holding on to a large amount of log will consume a lot of memory. So we
|
||||||
|
limited the initial log amount with `--tail=10000`.
|
||||||
|
- `execute` bindings allow you to run any command without leaving fzf
|
||||||
|
- 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
|
||||||
|
window options
|
||||||
|
1. `80%,border-bottom`
|
||||||
|
1. `hidden`
|
||||||
|
1. Empty string after `|` translates to the default options from `--preview-window`
|
||||||
|
|
||||||
|
Key bindings for git objects
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
<kbd>CTRL-G</kbd><kbd>CTRL-F</kbd>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Branches
|
||||||
|
|
||||||
|
<kbd>CTRL-G</kbd><kbd>CTRL-B</kbd>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Commit hashes
|
||||||
|
|
||||||
|
<kbd>CTRL-G</kbd><kbd>CTRL-H</kbd>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Color themes
|
||||||
|
------------
|
||||||
|
|
||||||
|
You can customize how fzf colors the text elements with `--color` option. Here
|
||||||
|
are a few color themes. Note that you need a terminal emulator that can
|
||||||
|
display 24-bit colors.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# junegunn/seoul256.vim (dark)
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#3F3F3F,bg:#4B4B4B,border:#6B6B6B,spinner:#98BC99,hl:#719872,fg:#D9D9D9,header:#719872,info:#BDBB72,pointer:#E12672,marker:#E17899,fg+:#D9D9D9,preview-bg:#3F3F3F,prompt:#98BEDE,hl+:#98BC99'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# junegunn/seoul256.vim (light)
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#D9D9D9,bg:#E1E1E1,border:#C8C8C8,spinner:#719899,hl:#719872,fg:#616161,header:#719872,info:#727100,pointer:#E12672,marker:#E17899,fg+:#616161,preview-bg:#D9D9D9,prompt:#0099BD,hl+:#719899'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# morhetz/gruvbox
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#3c3836,bg:#32302f,spinner:#fb4934,hl:#928374,fg:#ebdbb2,header:#928374,info:#8ec07c,pointer:#fb4934,marker:#fb4934,fg+:#ebdbb2,prompt:#fb4934,hl+:#fb4934'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# arcticicestudio/nord-vim
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#3B4252,bg:#2E3440,spinner:#81A1C1,hl:#616E88,fg:#D8DEE9,header:#616E88,info:#81A1C1,pointer:#81A1C1,marker:#81A1C1,fg+:#D8DEE9,prompt:#81A1C1,hl+:#81A1C1'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```sh
|
||||||
|
# tomasr/molokai
|
||||||
|
export FZF_DEFAULT_OPTS='--color=bg+:#293739,bg:#1B1D1E,border:#808080,spinner:#E6DB74,hl:#7E8E91,fg:#F8F8F2,header:#7E8E91,info:#A6E22E,pointer:#A6E22E,marker:#F92672,fg+:#F8F8F2,prompt:#F92672,hl+:#F92672'
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Generating fzf color theme from Vim color schemes
|
||||||
|
|
||||||
|
The Vim plugin of fzf can generate `--color` option from the current color
|
||||||
|
scheme according to `g:fzf_colors` variable. You can find the detailed
|
||||||
|
explanation [here](https://github.com/junegunn/fzf/blob/master/README-VIM.md#explanation-of-gfzf_colors).
|
||||||
|
|
||||||
|
Here is an example. Add this to your Vim configuration file.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
let g:fzf_colors =
|
||||||
|
\ { 'fg': ['fg', 'Normal'],
|
||||||
|
\ 'bg': ['bg', 'Normal'],
|
||||||
|
\ 'preview-bg': ['bg', 'NormalFloat'],
|
||||||
|
\ 'hl': ['fg', 'Comment'],
|
||||||
|
\ 'fg+': ['fg', 'CursorLine', 'CursorColumn', 'Normal'],
|
||||||
|
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||||
|
\ 'hl+': ['fg', 'Statement'],
|
||||||
|
\ 'info': ['fg', 'PreProc'],
|
||||||
|
\ 'border': ['fg', 'Ignore'],
|
||||||
|
\ 'prompt': ['fg', 'Conditional'],
|
||||||
|
\ 'pointer': ['fg', 'Exception'],
|
||||||
|
\ 'marker': ['fg', 'Keyword'],
|
||||||
|
\ 'spinner': ['fg', 'Label'],
|
||||||
|
\ 'header': ['fg', 'Comment'] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can see how the `--color` option is generated by printing the result
|
||||||
|
of `fzf#wrap()`.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
:echo fzf#wrap()
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this command to append `export FZF_DEFAULT_OPTS="..."` line to the end of
|
||||||
|
the current file.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
:call append('$', printf('export FZF_DEFAULT_OPTS="%s"', matchstr(fzf#wrap().options, "--color[^']*")))
|
||||||
|
```
|
||||||
26
BUILD.md
26
BUILD.md
@@ -6,12 +6,10 @@ Build instructions
|
|||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- `go` executable in $PATH
|
- Go 1.18 or above
|
||||||
|
|
||||||
### Using Makefile
|
### Using Makefile
|
||||||
|
|
||||||
Makefile will set up and use its own `$GOPATH` under the project root.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Build fzf binary for your platform in target
|
# Build fzf binary for your platform in target
|
||||||
make
|
make
|
||||||
@@ -19,21 +17,19 @@ make
|
|||||||
# Build fzf binary and copy it to bin directory
|
# Build fzf binary and copy it to bin directory
|
||||||
make install
|
make install
|
||||||
|
|
||||||
# Build 32-bit and 64-bit executables and tarballs in target
|
# Build fzf binaries and archives for all platforms using goreleaser
|
||||||
|
make build
|
||||||
|
|
||||||
|
# Publish GitHub release
|
||||||
make release
|
make release
|
||||||
|
|
||||||
# Make release archives for all supported platforms in target
|
|
||||||
make release-all
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Using `go get`
|
> :warning: Makefile uses git commands to determine the version and the
|
||||||
|
> revision information for `fzf --version`. So if you're building fzf from an
|
||||||
Alternatively, you can build fzf directly with `go get` command without
|
> environment where its git information is not available, you have to manually
|
||||||
manually cloning the repository.
|
> set `$FZF_VERSION` and `$FZF_REVISION`.
|
||||||
|
>
|
||||||
```sh
|
> e.g. `FZF_VERSION=0.24.0 FZF_REVISION=tarball make`
|
||||||
go get -u github.com/junegunn/fzf
|
|
||||||
```
|
|
||||||
|
|
||||||
Third-party libraries used
|
Third-party libraries used
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|||||||
1167
CHANGELOG.md
1167
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
11
Dockerfile
Normal file
11
Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM --platform=linux/amd64 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
|
||||||
|
RUN echo '. /usr/share/bash-completion/completions/git' >> ~/.bashrc
|
||||||
|
RUN echo '. ~/.bashrc' >> ~/.bash_profile
|
||||||
|
|
||||||
|
# Do not set default PS1
|
||||||
|
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 ]
|
||||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2016 Junegunn Choi
|
Copyright (c) 2013-2023 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
216
Makefile
216
Makefile
@@ -1,41 +1,42 @@
|
|||||||
ifndef GOOS
|
SHELL := bash
|
||||||
UNAME_S := $(shell uname -s)
|
GO ?= go
|
||||||
ifeq ($(UNAME_S),Darwin)
|
GOOS ?= $(word 1, $(subst /, " ", $(word 4, $(shell go version))))
|
||||||
GOOS := darwin
|
|
||||||
else ifeq ($(UNAME_S),Linux)
|
|
||||||
GOOS := linux
|
|
||||||
else
|
|
||||||
$(error "$$GOOS is not defined.")
|
|
||||||
endif
|
|
||||||
endif
|
|
||||||
|
|
||||||
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
MAKEFILE := $(realpath $(lastword $(MAKEFILE_LIST)))
|
||||||
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
ROOT_DIR := $(shell dirname $(MAKEFILE))
|
||||||
GOPATH := $(ROOT_DIR)/gopath
|
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(MAKEFILE)
|
||||||
SRC_LINK := $(GOPATH)/src/github.com/junegunn/fzf/src
|
|
||||||
VENDOR_LINK := $(GOPATH)/src/github.com/junegunn/fzf/vendor
|
|
||||||
export GOPATH
|
|
||||||
|
|
||||||
GLIDE_YAML := glide.yaml
|
ifdef FZF_VERSION
|
||||||
GLIDE_LOCK := glide.lock
|
VERSION := $(FZF_VERSION)
|
||||||
SOURCES := $(wildcard *.go src/*.go src/*/*.go) $(SRC_LINK) $(VENDOR_LINK) $(GLIDE_LOCK) $(MAKEFILE)
|
else
|
||||||
|
VERSION := $(shell git describe --abbrev=0 2> /dev/null)
|
||||||
|
endif
|
||||||
|
ifeq ($(VERSION),)
|
||||||
|
$(error Not on git repository; cannot determine $$FZF_VERSION)
|
||||||
|
endif
|
||||||
|
VERSION_TRIM := $(shell sed "s/-.*//" <<< $(VERSION))
|
||||||
|
VERSION_REGEX := $(subst .,\.,$(VERSION_TRIM))
|
||||||
|
|
||||||
REVISION := $(shell git log -n 1 --pretty=format:%h -- $(SOURCES))
|
ifdef FZF_REVISION
|
||||||
BUILD_FLAGS := -a -ldflags "-X main.revision=$(REVISION) -w -extldflags=$(LDFLAGS)" -tags "$(TAGS)"
|
REVISION := $(FZF_REVISION)
|
||||||
|
else
|
||||||
|
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)"
|
||||||
|
|
||||||
BINARY32 := fzf-$(GOOS)_386
|
BINARY32 := fzf-$(GOOS)_386
|
||||||
BINARY64 := fzf-$(GOOS)_amd64
|
BINARY64 := fzf-$(GOOS)_amd64
|
||||||
|
BINARYS390 := fzf-$(GOOS)_s390x
|
||||||
BINARYARM5 := fzf-$(GOOS)_arm5
|
BINARYARM5 := fzf-$(GOOS)_arm5
|
||||||
BINARYARM6 := fzf-$(GOOS)_arm6
|
BINARYARM6 := fzf-$(GOOS)_arm6
|
||||||
BINARYARM7 := fzf-$(GOOS)_arm7
|
BINARYARM7 := fzf-$(GOOS)_arm7
|
||||||
BINARYARM8 := fzf-$(GOOS)_arm8
|
BINARYARM8 := fzf-$(GOOS)_arm8
|
||||||
VERSION := $(shell awk -F= '/version =/ {print $$2}' src/constants.go | tr -d "\" ")
|
BINARYPPC64LE := fzf-$(GOOS)_ppc64le
|
||||||
RELEASE32 := fzf-$(VERSION)-$(GOOS)_386
|
BINARYRISCV64 := fzf-$(GOOS)_riscv64
|
||||||
RELEASE64 := fzf-$(VERSION)-$(GOOS)_amd64
|
BINARYLOONG64 := fzf-$(GOOS)_loong64
|
||||||
RELEASEARM5 := fzf-$(VERSION)-$(GOOS)_arm5
|
|
||||||
RELEASEARM6 := fzf-$(VERSION)-$(GOOS)_arm6
|
|
||||||
RELEASEARM7 := fzf-$(VERSION)-$(GOOS)_arm7
|
|
||||||
RELEASEARM8 := fzf-$(VERSION)-$(GOOS)_arm8
|
|
||||||
|
|
||||||
# https://en.wikipedia.org/wiki/Uname
|
# https://en.wikipedia.org/wiki/Uname
|
||||||
UNAME_M := $(shell uname -m)
|
UNAME_M := $(shell uname -m)
|
||||||
@@ -43,6 +44,8 @@ ifeq ($(UNAME_M),x86_64)
|
|||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
else ifeq ($(UNAME_M),amd64)
|
else ifeq ($(UNAME_M),amd64)
|
||||||
BINARY := $(BINARY64)
|
BINARY := $(BINARY64)
|
||||||
|
else ifeq ($(UNAME_M),s390x)
|
||||||
|
BINARY := $(BINARYS390)
|
||||||
else ifeq ($(UNAME_M),i686)
|
else ifeq ($(UNAME_M),i686)
|
||||||
BINARY := $(BINARY32)
|
BINARY := $(BINARY32)
|
||||||
else ifeq ($(UNAME_M),i386)
|
else ifeq ($(UNAME_M),i386)
|
||||||
@@ -53,86 +56,129 @@ else ifeq ($(UNAME_M),armv6l)
|
|||||||
BINARY := $(BINARYARM6)
|
BINARY := $(BINARYARM6)
|
||||||
else ifeq ($(UNAME_M),armv7l)
|
else ifeq ($(UNAME_M),armv7l)
|
||||||
BINARY := $(BINARYARM7)
|
BINARY := $(BINARYARM7)
|
||||||
|
else ifeq ($(UNAME_M),armv8l)
|
||||||
|
# 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)
|
||||||
|
BINARY := $(BINARYARM8)
|
||||||
|
else ifeq ($(UNAME_M),ppc64le)
|
||||||
|
BINARY := $(BINARYPPC64LE)
|
||||||
|
else ifeq ($(UNAME_M),riscv64)
|
||||||
|
BINARY := $(BINARYRISCV64)
|
||||||
|
else ifeq ($(UNAME_M),loongarch64)
|
||||||
|
BINARY := $(BINARYLOONG64)
|
||||||
else
|
else
|
||||||
$(error "Build on $(UNAME_M) is not supported, yet.")
|
$(error Build on $(UNAME_M) is not supported, yet.)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
all: target/$(BINARY)
|
all: target/$(BINARY)
|
||||||
|
|
||||||
target:
|
test: $(SOURCES)
|
||||||
mkdir -p $@
|
[ -z "$$(gofmt -s -d src)" ] || (gofmt -s -d src; exit 1)
|
||||||
|
SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" \
|
||||||
ifeq ($(GOOS),windows)
|
|
||||||
release: target/$(BINARY32) target/$(BINARY64)
|
|
||||||
cd target && cp -f $(BINARY32) fzf.exe && zip $(RELEASE32).zip fzf.exe
|
|
||||||
cd target && cp -f $(BINARY64) fzf.exe && zip $(RELEASE64).zip fzf.exe
|
|
||||||
cd target && rm -f fzf.exe
|
|
||||||
else ifeq ($(GOOS),linux)
|
|
||||||
release: target/$(BINARY32) target/$(BINARY64) target/$(BINARYARM5) target/$(BINARYARM6) target/$(BINARYARM7) target/$(BINARYARM8)
|
|
||||||
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
|
|
||||||
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
|
|
||||||
cd target && cp -f $(BINARYARM5) fzf && tar -czf $(RELEASEARM5).tgz fzf
|
|
||||||
cd target && cp -f $(BINARYARM6) fzf && tar -czf $(RELEASEARM6).tgz fzf
|
|
||||||
cd target && cp -f $(BINARYARM7) fzf && tar -czf $(RELEASEARM7).tgz fzf
|
|
||||||
cd target && cp -f $(BINARYARM8) fzf && tar -czf $(RELEASEARM8).tgz fzf
|
|
||||||
cd target && rm -f fzf
|
|
||||||
else
|
|
||||||
release: target/$(BINARY32) target/$(BINARY64)
|
|
||||||
cd target && cp -f $(BINARY32) fzf && tar -czf $(RELEASE32).tgz fzf
|
|
||||||
cd target && cp -f $(BINARY64) fzf && tar -czf $(RELEASE64).tgz fzf
|
|
||||||
cd target && rm -f fzf
|
|
||||||
endif
|
|
||||||
|
|
||||||
release-all: clean test
|
|
||||||
GOOS=darwin make release
|
|
||||||
GOOS=linux make release
|
|
||||||
GOOS=freebsd make release
|
|
||||||
GOOS=openbsd make release
|
|
||||||
GOOS=windows make release
|
|
||||||
|
|
||||||
$(SRC_LINK):
|
|
||||||
mkdir -p $(shell dirname $(SRC_LINK))
|
|
||||||
ln -sf $(ROOT_DIR)/src $(SRC_LINK)
|
|
||||||
|
|
||||||
$(VENDOR_LINK):
|
|
||||||
mkdir -p $(shell dirname $(VENDOR_LINK))
|
|
||||||
ln -sf $(ROOT_DIR)/vendor $(VENDOR_LINK)
|
|
||||||
|
|
||||||
vendor: $(GLIDE_YAML)
|
|
||||||
go get -u github.com/Masterminds/glide && $(GOPATH)/bin/glide install && touch $@
|
|
||||||
|
|
||||||
test: $(SOURCES) vendor
|
|
||||||
SHELL=/bin/sh GOOS= go test -v -tags "$(TAGS)" \
|
|
||||||
github.com/junegunn/fzf/src \
|
github.com/junegunn/fzf/src \
|
||||||
github.com/junegunn/fzf/src/algo \
|
github.com/junegunn/fzf/src/algo \
|
||||||
github.com/junegunn/fzf/src/tui \
|
github.com/junegunn/fzf/src/tui \
|
||||||
github.com/junegunn/fzf/src/util
|
github.com/junegunn/fzf/src/util
|
||||||
|
|
||||||
|
bench:
|
||||||
|
cd src && SHELL=/bin/sh GOOS= $(GO) test -v -tags "$(TAGS)" -run=Bench -bench=. -benchmem
|
||||||
|
|
||||||
install: bin/fzf
|
install: bin/fzf
|
||||||
|
|
||||||
|
build:
|
||||||
|
goreleaser build --rm-dist --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
|
||||||
|
|
||||||
|
# Check if we are on master branch
|
||||||
|
ifneq ($(shell git symbolic-ref --short HEAD),master)
|
||||||
|
$(error Not on master branch)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# Check if version numbers are properly updated
|
||||||
|
grep -q ^$(VERSION_REGEX)$$ CHANGELOG.md
|
||||||
|
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf.1
|
||||||
|
grep -qF '"fzf $(VERSION_TRIM)"' man/man1/fzf-tmux.1
|
||||||
|
grep -qF $(VERSION) install
|
||||||
|
grep -qF $(VERSION) install.ps1
|
||||||
|
|
||||||
|
# Make release note out of CHANGELOG.md
|
||||||
|
mkdir -p tmp
|
||||||
|
sed -n '/^$(VERSION_REGEX)$$/,/^[0-9]/p' CHANGELOG.md | tail -r | \
|
||||||
|
sed '1,/^ *$$/d' | tail -r | sed 1,2d | tee tmp/release-note
|
||||||
|
|
||||||
|
# Push to temp branch first so that install scripts always works on master branch
|
||||||
|
git checkout -B temp master
|
||||||
|
git push origin temp --follow-tags --force
|
||||||
|
|
||||||
|
# Make a GitHub release
|
||||||
|
goreleaser --rm-dist --release-notes tmp/release-note
|
||||||
|
|
||||||
|
# Push to master
|
||||||
|
git checkout master
|
||||||
|
git push origin master
|
||||||
|
|
||||||
|
# Delete temp branch
|
||||||
|
git push origin --delete temp
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf target
|
$(RM) -r dist target
|
||||||
|
|
||||||
target/$(BINARY32): $(SOURCES) vendor
|
target/$(BINARY32): $(SOURCES)
|
||||||
GOARCH=386 go build $(BUILD_FLAGS) -o $@
|
GOARCH=386 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
target/$(BINARY64): $(SOURCES) vendor
|
target/$(BINARY64): $(SOURCES)
|
||||||
GOARCH=amd64 go build $(BUILD_FLAGS) -o $@
|
GOARCH=amd64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
|
target/$(BINARYS390): $(SOURCES)
|
||||||
|
GOARCH=s390x $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
# https://github.com/golang/go/wiki/GoArm
|
# https://github.com/golang/go/wiki/GoArm
|
||||||
target/$(BINARYARM5): $(SOURCES) vendor
|
target/$(BINARYARM5): $(SOURCES)
|
||||||
GOARCH=arm GOARM=5 go build $(BUILD_FLAGS) -o $@
|
GOARCH=arm GOARM=5 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
target/$(BINARYARM6): $(SOURCES) vendor
|
target/$(BINARYARM6): $(SOURCES)
|
||||||
GOARCH=arm GOARM=6 go build $(BUILD_FLAGS) -o $@
|
GOARCH=arm GOARM=6 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
target/$(BINARYARM7): $(SOURCES) vendor
|
target/$(BINARYARM7): $(SOURCES)
|
||||||
GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@
|
GOARCH=arm GOARM=7 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
target/$(BINARYARM8): $(SOURCES) vendor
|
target/$(BINARYARM8): $(SOURCES)
|
||||||
GOARCH=arm64 go build $(BUILD_FLAGS) -o $@
|
GOARCH=arm64 $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
|
target/$(BINARYPPC64LE): $(SOURCES)
|
||||||
|
GOARCH=ppc64le $(GO) build $(BUILD_FLAGS) -o $@
|
||||||
|
|
||||||
|
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
|
bin/fzf: target/$(BINARY) | bin
|
||||||
|
-rm -f bin/fzf
|
||||||
cp -f target/$(BINARY) bin/fzf
|
cp -f target/$(BINARY) bin/fzf
|
||||||
|
|
||||||
.PHONY: all release release-all test install clean
|
docker:
|
||||||
|
docker build -t fzf-arch .
|
||||||
|
docker run -it fzf-arch tmux
|
||||||
|
|
||||||
|
docker-test:
|
||||||
|
docker build -t fzf-arch .
|
||||||
|
docker run -it fzf-arch
|
||||||
|
|
||||||
|
update:
|
||||||
|
$(GO) get -u
|
||||||
|
$(GO) mod tidy
|
||||||
|
|
||||||
|
.PHONY: all build release test bench install clean docker docker-test update
|
||||||
|
|||||||
431
README-VIM.md
431
README-VIM.md
@@ -1,16 +1,83 @@
|
|||||||
FZF Vim integration
|
FZF Vim integration
|
||||||
===================
|
===================
|
||||||
|
|
||||||
This repository only enables basic integration with Vim. If you're looking for
|
Installation
|
||||||
more, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
|
------------
|
||||||
|
|
||||||
(Note: To use fzf in GVim, an external terminal emulator is required.)
|
Once you have fzf installed, you can enable it inside Vim simply by adding the
|
||||||
|
directory to `&runtimepath` in your Vim configuration file. The path may
|
||||||
|
differ depending on the package manager.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
" If installed using Homebrew
|
||||||
|
set rtp+=/usr/local/opt/fzf
|
||||||
|
|
||||||
|
" 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](https://github.com/junegunn/vim-plug), the same can be
|
||||||
|
written as:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
" If installed using Homebrew
|
||||||
|
Plug '/usr/local/opt/fzf'
|
||||||
|
|
||||||
|
" 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
|
||||||
|
included in the package, write:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
Plug 'junegunn/fzf'
|
||||||
|
```
|
||||||
|
|
||||||
|
The Vim plugin will pick up fzf binary available on the system. If fzf is not
|
||||||
|
found on `$PATH`, it will ask you if it should download the latest binary for
|
||||||
|
you.
|
||||||
|
|
||||||
|
To make sure that you have the latest version of the binary, set up
|
||||||
|
post-update hook like so:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
|
||||||
|
```
|
||||||
|
|
||||||
|
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])`**
|
||||||
|
- Starts fzf inside Vim with the given spec
|
||||||
|
- `: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'})`
|
||||||
|
- 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]`**
|
||||||
|
- Basic fuzzy file selector
|
||||||
|
- A reference implementation for those who don't want to write VimScript
|
||||||
|
to implement custom commands
|
||||||
|
- If you're looking for more such commands, check out [fzf.vim](https://github.com/junegunn/fzf.vim) project.
|
||||||
|
|
||||||
|
The most important of all is `fzf#run`, but it would be easier to understand
|
||||||
|
the whole if we start off with `:FZF` command.
|
||||||
|
|
||||||
`:FZF[!]`
|
`:FZF[!]`
|
||||||
---------
|
---------
|
||||||
|
|
||||||
If you have set up fzf for Vim, `:FZF` command will be added.
|
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
" Look for files under current directory
|
" Look for files under current directory
|
||||||
:FZF
|
:FZF
|
||||||
@@ -18,8 +85,8 @@ If you have set up fzf for Vim, `:FZF` command will be added.
|
|||||||
" Look for files under your home directory
|
" Look for files under your home directory
|
||||||
:FZF ~
|
:FZF ~
|
||||||
|
|
||||||
" With options
|
" With fzf command-line options
|
||||||
:FZF --no-sort --reverse --inline-info /tmp
|
:FZF --reverse --info=inline /tmp
|
||||||
|
|
||||||
" Bang version starts fzf in fullscreen mode
|
" Bang version starts fzf in fullscreen mode
|
||||||
:FZF!
|
:FZF!
|
||||||
@@ -37,14 +104,11 @@ Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
|||||||
- `g:fzf_action`
|
- `g:fzf_action`
|
||||||
- Customizable extra key bindings for opening selected files in different ways
|
- Customizable extra key bindings for opening selected files in different ways
|
||||||
- `g:fzf_layout`
|
- `g:fzf_layout`
|
||||||
- Determines the size and position of fzf window (tmux pane or Neovim split)
|
- Determines the size and position of fzf window
|
||||||
- `g:fzf_colors`
|
- `g:fzf_colors`
|
||||||
- Customizes fzf colors to match the current color scheme
|
- Customizes fzf colors to match the current color scheme
|
||||||
- `g:fzf_history_dir`
|
- `g:fzf_history_dir`
|
||||||
- Enables history feature
|
- Enables history feature
|
||||||
- `g:fzf_launcher`
|
|
||||||
- (Only in GVim) Terminal emulator to open fzf with
|
|
||||||
- `g:Fzf_launcher` for function reference
|
|
||||||
|
|
||||||
#### Examples
|
#### Examples
|
||||||
|
|
||||||
@@ -55,16 +119,39 @@ let g:fzf_action = {
|
|||||||
\ 'ctrl-x': 'split',
|
\ 'ctrl-x': 'split',
|
||||||
\ 'ctrl-v': 'vsplit' }
|
\ 'ctrl-v': 'vsplit' }
|
||||||
|
|
||||||
" Default fzf layout
|
" An action can be a reference to a function that processes selected lines
|
||||||
" - down / up / left / right
|
function! s:build_quickfix_list(lines)
|
||||||
let g:fzf_layout = { 'down': '~40%' }
|
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||||
|
copen
|
||||||
|
cc
|
||||||
|
endfunction
|
||||||
|
|
||||||
" In Neovim, you can set up fzf window using a Vim command
|
let g:fzf_action = {
|
||||||
|
\ 'ctrl-q': function('s:build_quickfix_list'),
|
||||||
|
\ 'ctrl-t': 'tab split',
|
||||||
|
\ 'ctrl-x': 'split',
|
||||||
|
\ 'ctrl-v': 'vsplit' }
|
||||||
|
|
||||||
|
" Default fzf layout
|
||||||
|
" - Popup window (center of the screen)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
|
|
||||||
|
" - Popup window (center of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
|
||||||
|
|
||||||
|
" - Popup window (anchored to the bottom of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||||
|
|
||||||
|
" - down / up / left / right
|
||||||
|
let g:fzf_layout = { 'down': '40%' }
|
||||||
|
|
||||||
|
" - Window using a Vim command
|
||||||
let g:fzf_layout = { 'window': 'enew' }
|
let g:fzf_layout = { 'window': 'enew' }
|
||||||
let g:fzf_layout = { 'window': '-tabnew' }
|
let g:fzf_layout = { 'window': '-tabnew' }
|
||||||
let g:fzf_layout = { 'window': '10split enew' }
|
let g:fzf_layout = { 'window': '10new' }
|
||||||
|
|
||||||
" Customize fzf colors to match your color scheme
|
" Customize fzf colors to match your color scheme
|
||||||
|
" - fzf#wrap translates this to a set of `--color` options
|
||||||
let g:fzf_colors =
|
let g:fzf_colors =
|
||||||
\ { 'fg': ['fg', 'Normal'],
|
\ { 'fg': ['fg', 'Normal'],
|
||||||
\ 'bg': ['bg', 'Normal'],
|
\ 'bg': ['bg', 'Normal'],
|
||||||
@@ -73,24 +160,128 @@ let g:fzf_colors =
|
|||||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||||
\ 'hl+': ['fg', 'Statement'],
|
\ 'hl+': ['fg', 'Statement'],
|
||||||
\ 'info': ['fg', 'PreProc'],
|
\ 'info': ['fg', 'PreProc'],
|
||||||
|
\ 'border': ['fg', 'Ignore'],
|
||||||
\ 'prompt': ['fg', 'Conditional'],
|
\ 'prompt': ['fg', 'Conditional'],
|
||||||
\ 'pointer': ['fg', 'Exception'],
|
\ 'pointer': ['fg', 'Exception'],
|
||||||
\ 'marker': ['fg', 'Keyword'],
|
\ 'marker': ['fg', 'Keyword'],
|
||||||
\ 'spinner': ['fg', 'Label'],
|
\ 'spinner': ['fg', 'Label'],
|
||||||
\ 'header': ['fg', 'Comment'] }
|
\ 'header': ['fg', 'Comment'] }
|
||||||
|
|
||||||
" Enable per-command history.
|
" Enable per-command history
|
||||||
" CTRL-N and CTRL-P will be automatically bound to next-history and
|
" - History files will be stored in the specified directory
|
||||||
" previous-history instead of down and up. If you don't like the change,
|
" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
|
||||||
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
|
" 'previous-history' instead of 'down' and 'up'.
|
||||||
let g:fzf_history_dir = '~/.local/share/fzf-history'
|
let g:fzf_history_dir = '~/.local/share/fzf-history'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Explanation of `g:fzf_colors`
|
||||||
|
|
||||||
|
`g:fzf_colors` is a dictionary mapping fzf elements to a color specification
|
||||||
|
list:
|
||||||
|
|
||||||
|
element: [ component, group1 [, group2, ...] ]
|
||||||
|
|
||||||
|
- `element` is an fzf element to apply a color to:
|
||||||
|
|
||||||
|
| Element | Description |
|
||||||
|
| --- | --- |
|
||||||
|
| `fg` / `bg` / `hl` | Item (foreground / background / highlight) |
|
||||||
|
| `fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight) |
|
||||||
|
| `preview-fg` / `preview-bg` | Preview window text and background |
|
||||||
|
| `hl` / `hl+` | Highlighted substrings (normal / current) |
|
||||||
|
| `gutter` | Background of the gutter on the left |
|
||||||
|
| `pointer` | Pointer to the current line (`>`) |
|
||||||
|
| `marker` | Multi-select marker (`>`) |
|
||||||
|
| `border` | Border around the window (`--border` and `--preview`) |
|
||||||
|
| `header` | Header (`--header` or `--header-lines`) |
|
||||||
|
| `info` | Info line (match counters) |
|
||||||
|
| `spinner` | Streaming input indicator |
|
||||||
|
| `query` | Query string |
|
||||||
|
| `disabled` | Query string when search is disabled |
|
||||||
|
| `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
|
||||||
|
order) for a matching color definition
|
||||||
|
|
||||||
|
For example, consider the following specification:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
'prompt': ['fg', 'Conditional', 'Comment'],
|
||||||
|
```
|
||||||
|
|
||||||
|
This means we color the **prompt**
|
||||||
|
- using the `fg` attribute of the `Conditional` if it exists,
|
||||||
|
- otherwise use the `fg` attribute of the `Comment` highlight group if it exists,
|
||||||
|
- otherwise fall back to the default color settings for the **prompt**.
|
||||||
|
|
||||||
|
You can examine the color option generated according the setting by printing
|
||||||
|
the result of `fzf#wrap()` function like so:
|
||||||
|
|
||||||
|
```vim
|
||||||
|
:echo fzf#wrap()
|
||||||
|
```
|
||||||
|
|
||||||
`fzf#run`
|
`fzf#run`
|
||||||
---------
|
---------
|
||||||
|
|
||||||
For more advanced uses, you can use `fzf#run([options])` function with the
|
`fzf#run()` function is the core of Vim integration. It takes a single
|
||||||
following options.
|
dictionary argument, *a spec*, and starts fzf process accordingly. At the very
|
||||||
|
least, specify `sink` option to tell what it should do with the selected
|
||||||
|
entry.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
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.
|
||||||
|
|
||||||
|
```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.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||||
|
```
|
||||||
|
|
||||||
|
fzf options can be specified as `options` entry in spec dictionary.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also pass a layout option if you don't want fzf window to take up the
|
||||||
|
entire screen.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
" up / down / left / right / window are allowed
|
||||||
|
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
|
||||||
|
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
|
||||||
|
```
|
||||||
|
|
||||||
|
`source` doesn't have to be an external shell command, you can pass a Vim
|
||||||
|
array as the source. In the next example, we pass the names of color
|
||||||
|
schemes as the source to implement a color scheme selector.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
|
||||||
|
\ 'fnamemodify(v:val, ":t:r")'),
|
||||||
|
\ 'sink': 'colo', 'left': '25%'})
|
||||||
|
```
|
||||||
|
|
||||||
|
The following table summarizes the available options.
|
||||||
|
|
||||||
| Option name | Type | Description |
|
| Option name | Type | Description |
|
||||||
| -------------------------- | ------------- | ---------------------------------------------------------------- |
|
| -------------------------- | ------------- | ---------------------------------------------------------------- |
|
||||||
@@ -98,58 +289,204 @@ following options.
|
|||||||
| `source` | list | Vim list as input to fzf |
|
| `source` | list | Vim list as input to fzf |
|
||||||
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
| `sink` | string | Vim command to handle the selected item (e.g. `e`, `tabe`) |
|
||||||
| `sink` | funcref | Reference to function to process each selected item |
|
| `sink` | funcref | Reference to function to process each selected item |
|
||||||
| `sink*` | funcref | Similar to `sink`, but takes the list of output lines at once |
|
| `sinklist` (or `sink*`) | funcref | Similar to `sink`, but takes the list of output lines at once |
|
||||||
| `options` | string/list | Options to fzf |
|
| `options` | string/list | Options to fzf |
|
||||||
| `dir` | string | Working directory |
|
| `dir` | string | Working directory |
|
||||||
| `up`/`down`/`left`/`right` | number/string | Use tmux pane with the given size (e.g. `20`, `50%`) |
|
| `up`/`down`/`left`/`right` | number/string | (Layout) Window position and size (e.g. `20`, `50%`) |
|
||||||
| `window` (*Neovim only*) | string | Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
| `tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%`) |
|
||||||
| `launcher` | string | External terminal emulator to start fzf with (GVim only) |
|
| `window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new`) |
|
||||||
| `launcher` | funcref | Function for generating `launcher` string (GVim only) |
|
| `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
|
`options` entry can be either a string or a list. For simple cases, string
|
||||||
should suffice, but prefer to use list type if you're concerned about escaping
|
should suffice, but prefer to use list type to avoid escaping issues.
|
||||||
issues on different platforms.
|
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
||||||
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When `window` entry is a dictionary, fzf will start in a popup window. The
|
||||||
|
following options are allowed:
|
||||||
|
|
||||||
|
- Required:
|
||||||
|
- `width` [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||||
|
- `height` [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||||
|
- Optional:
|
||||||
|
- `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` (`sharp` on Windows)]: Border style
|
||||||
|
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||||
|
|
||||||
`fzf#wrap`
|
`fzf#wrap`
|
||||||
----------
|
----------
|
||||||
|
|
||||||
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
|
We have seen that several aspects of `:FZF` command can be configured with
|
||||||
function that decorates the options dictionary so that it understands
|
a set of global option variables; different ways to open files
|
||||||
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
|
(`g:fzf_action`), window position and size (`g:fzf_layout`), color palette
|
||||||
`:FZF`.
|
(`g:fzf_colors`), etc.
|
||||||
|
|
||||||
|
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)`**
|
||||||
|
- 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` is not defined.
|
||||||
|
- `fullscreen` can be either `0` or `1` (default: 0).
|
||||||
|
|
||||||
|
`fzf#wrap` takes a spec and returns an extended version of it (also
|
||||||
|
a dictionary) with additional options for addressing global preferences. You
|
||||||
|
can examine the return value of it like so:
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
command! -bang MyStuff
|
echo fzf#wrap({'source': 'ls'})
|
||||||
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
GVim
|
After we *"wrap"* our spec, we pass it to `fzf#run`.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||||
|
```
|
||||||
|
|
||||||
|
Now it supports `CTRL-T`, `CTRL-V`, and `CTRL-X` key bindings (configurable
|
||||||
|
via `g:fzf_action`) and it opens fzf window according to `g:fzf_layout`
|
||||||
|
setting.
|
||||||
|
|
||||||
|
To make it easier to use, let's define `LS` command.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||||
|
```
|
||||||
|
|
||||||
|
Type `:LS` and see how it works.
|
||||||
|
|
||||||
|
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
|
||||||
|
`:FZF!`. Add `-bang` to command definition, and use `<bang>` value to set
|
||||||
|
the last `fullscreen` argument of `fzf#wrap` (see `:help <bang>`).
|
||||||
|
|
||||||
|
```vim
|
||||||
|
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
|
||||||
|
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.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
command! -bang -complete=dir -nargs=? LS
|
||||||
|
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign
|
||||||
|
a unique name to our command and pass it as the first argument to `fzf#wrap`.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
||||||
|
" The name is ignored if g:fzf_history_dir is not defined.
|
||||||
|
command! -bang -complete=dir -nargs=? LS
|
||||||
|
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global options supported by `fzf#wrap`
|
||||||
|
|
||||||
|
- `g:fzf_layout`
|
||||||
|
- `g:fzf_action`
|
||||||
|
- **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)
|
||||||
|
- `g:fzf_colors`
|
||||||
|
- `g:fzf_history_dir`
|
||||||
|
|
||||||
|
Tips
|
||||||
----
|
----
|
||||||
|
|
||||||
In GVim, you need an external terminal emulator to start fzf with. `xterm`
|
### fzf inside terminal buffer
|
||||||
command is used by default, but you can customize it with `g:fzf_launcher`.
|
|
||||||
|
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.
|
||||||
|
|
||||||
```vim
|
```vim
|
||||||
" This is the default. %s is replaced with fzf command
|
" Terminal colors for seoul256 color scheme
|
||||||
let g:fzf_launcher = 'xterm -e bash -ic %s'
|
if has('nvim')
|
||||||
|
let g:terminal_color_0 = '#4e4e4e'
|
||||||
" Use urxvt instead
|
let g:terminal_color_1 = '#d68787'
|
||||||
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
|
let g:terminal_color_2 = '#5f865f'
|
||||||
|
let g:terminal_color_3 = '#d8af5f'
|
||||||
|
let g:terminal_color_4 = '#85add4'
|
||||||
|
let g:terminal_color_5 = '#d7afaf'
|
||||||
|
let g:terminal_color_6 = '#87afaf'
|
||||||
|
let g:terminal_color_7 = '#d0d0d0'
|
||||||
|
let g:terminal_color_8 = '#626262'
|
||||||
|
let g:terminal_color_9 = '#d75f87'
|
||||||
|
let g:terminal_color_10 = '#87af87'
|
||||||
|
let g:terminal_color_11 = '#ffd787'
|
||||||
|
let g:terminal_color_12 = '#add4fb'
|
||||||
|
let g:terminal_color_13 = '#ffafaf'
|
||||||
|
let g:terminal_color_14 = '#87d7d7'
|
||||||
|
let g:terminal_color_15 = '#e4e4e4'
|
||||||
|
else
|
||||||
|
let g:terminal_ansi_colors = [
|
||||||
|
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
|
||||||
|
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
|
||||||
|
\ '#626262', '#d75f87', '#87af87', '#ffd787',
|
||||||
|
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
|
||||||
|
\ ]
|
||||||
|
endif
|
||||||
```
|
```
|
||||||
|
|
||||||
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
|
### Starting fzf in a popup window
|
||||||
launcher. Refer to the [this wiki page][macvim-iterm2] to see how to set up.
|
|
||||||
|
|
||||||
[macvim-iterm2]: https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
|
```vim
|
||||||
|
" Required:
|
||||||
|
" - width [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||||
|
" - height [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||||
|
"
|
||||||
|
" Optional:
|
||||||
|
" - xoffset [float default 0.5 range [0 ~ 1]]
|
||||||
|
" - yoffset [float default 0.5 range [0 ~ 1]]
|
||||||
|
" - relative [boolean default v:false]
|
||||||
|
" - border [string default 'rounded']: Border style
|
||||||
|
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
||||||
|
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.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
" See `man fzf-tmux` for available options
|
||||||
|
if exists('$TMUX')
|
||||||
|
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||||
|
else
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
|
endif
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
the window.
|
||||||
|
|
||||||
|
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||||
|
'40%'}`), you might want to temporarily disable the statusline for a cleaner
|
||||||
|
look.
|
||||||
|
|
||||||
|
```vim
|
||||||
|
let g:fzf_layout = { 'down': '30%' }
|
||||||
|
autocmd! FileType fzf
|
||||||
|
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||||
|
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||||
|
```
|
||||||
|
|
||||||
[License](LICENSE)
|
[License](LICENSE)
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017 Junegunn Choi
|
Copyright (c) 2013-2023 Junegunn Choi
|
||||||
|
|||||||
74
bin/fzf-preview.sh
Executable file
74
bin/fzf-preview.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/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"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
file=${1/#\~\//$HOME/}
|
||||||
|
type=$(file --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 -- "$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 kitty icat on kitty terminal
|
||||||
|
if [[ $KITTY_WINDOW_ID ]]; then
|
||||||
|
# 1. 'memory' is the fastest option but if you want the image to be scrollable,
|
||||||
|
# you have to use 'stream'.
|
||||||
|
#
|
||||||
|
# 2. The last line of the output is the ANSI reset code without newline.
|
||||||
|
# This confuses fzf and makes it render scroll offset indicator.
|
||||||
|
# So we remove the last line and append the reset code to its previous line.
|
||||||
|
kitty icat --clear --transfer-mode=memory --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 -f sixel -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
|
||||||
138
bin/fzf-tmux
138
bin/fzf-tmux
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# fzf-tmux: starts fzf in a tmux pane
|
# fzf-tmux: starts fzf in a tmux pane
|
||||||
# usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
|
# usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
>&2 echo "$1"
|
>&2 echo "$1"
|
||||||
@@ -16,19 +16,27 @@ skip=""
|
|||||||
swap=""
|
swap=""
|
||||||
close=""
|
close=""
|
||||||
term=""
|
term=""
|
||||||
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines)
|
[[ -n "$LINES" ]] && lines=$LINES || lines=$(tput lines) || lines=$(tmux display-message -p "#{pane_height}")
|
||||||
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols)
|
[[ -n "$COLUMNS" ]] && columns=$COLUMNS || columns=$(tput cols) || columns=$(tmux display-message -p "#{pane_width}")
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
>&2 echo 'usage: fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
|
>&2 echo 'usage: fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||||
|
|
||||||
Layout
|
LAYOUT OPTIONS:
|
||||||
|
(default layout: -d 50%)
|
||||||
|
|
||||||
|
Popup window (requires tmux 3.2 or above):
|
||||||
|
-p [WIDTH[%][,HEIGHT[%]]] (default: 50%)
|
||||||
|
-w WIDTH[%]
|
||||||
|
-h HEIGHT[%]
|
||||||
|
-x COL
|
||||||
|
-y ROW
|
||||||
|
|
||||||
|
Split pane:
|
||||||
-u [HEIGHT[%]] Split above (up)
|
-u [HEIGHT[%]] Split above (up)
|
||||||
-d [HEIGHT[%]] Split below (down)
|
-d [HEIGHT[%]] Split below (down)
|
||||||
-l [WIDTH[%]] Split left
|
-l [WIDTH[%]] Split left
|
||||||
-r [WIDTH[%]] Split right
|
-r [WIDTH[%]] Split right
|
||||||
|
|
||||||
(default: -d 50%)
|
|
||||||
'
|
'
|
||||||
exit
|
exit
|
||||||
}
|
}
|
||||||
@@ -47,8 +55,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
echo "fzf-tmux (with fzf $("$fzf" --version))"
|
||||||
exit
|
exit
|
||||||
;;
|
;;
|
||||||
-w*|-h*|-d*|-u*|-r*|-l*)
|
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
||||||
if [[ "$arg" =~ ^.[lrw] ]]; then
|
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
||||||
|
[[ "$opt" =~ "-E" ]] || opt="-E"
|
||||||
|
elif [[ "$arg" =~ ^.[lr] ]]; then
|
||||||
opt="-h"
|
opt="-h"
|
||||||
if [[ "$arg" =~ ^.l ]]; then
|
if [[ "$arg" =~ ^.l ]]; then
|
||||||
opt="$opt -d"
|
opt="$opt -d"
|
||||||
@@ -66,7 +76,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
if [[ ${#arg} -gt 2 ]]; then
|
if [[ ${#arg} -gt 2 ]]; then
|
||||||
size="${arg:2}"
|
size="${arg:2}"
|
||||||
else
|
else
|
||||||
if [[ "$1" =~ ^[0-9]+%?$ ]]; then
|
if [[ "$1" =~ ^[0-9%,]+$ ]] || [[ "$1" =~ ^[A-Z]$ ]]; then
|
||||||
size="$1"
|
size="$1"
|
||||||
shift
|
shift
|
||||||
else
|
else
|
||||||
@@ -74,7 +84,15 @@ while [[ $# -gt 0 ]]; do
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$size" =~ %$ ]]; then
|
if [[ "$arg" =~ ^-p ]]; then
|
||||||
|
if [[ -n "$size" ]]; then
|
||||||
|
w=${size%%,*}
|
||||||
|
h=${size##*,}
|
||||||
|
opt="$opt -w$w -h$h"
|
||||||
|
fi
|
||||||
|
elif [[ "$arg" =~ ^-[whxy] ]]; then
|
||||||
|
opt="$opt ${arg:0:2}$size"
|
||||||
|
elif [[ "$size" =~ %$ ]]; then
|
||||||
size=${size:0:((${#size}-1))}
|
size=${size:0:((${#size}-1))}
|
||||||
if [[ -n "$swap" ]]; then
|
if [[ -n "$swap" ]]; then
|
||||||
opt="$opt -p $(( 100 - size ))"
|
opt="$opt -p $(( 100 - size ))"
|
||||||
@@ -109,17 +127,17 @@ while [[ $# -gt 0 ]]; do
|
|||||||
[[ -n "$skip" ]] && args+=("$arg")
|
[[ -n "$skip" ]] && args+=("$arg")
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z "$TMUX" || "$opt" =~ ^-h && "$columns" -le 40 || ! "$opt" =~ ^-h && "$lines" -le 15 ]]; then
|
if [[ -z "$TMUX" ]]; then
|
||||||
"$fzf" "${args[@]}"
|
"$fzf" "${args[@]}"
|
||||||
exit $?
|
exit $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --height option is not allowed
|
# --height option is not allowed. CTRL-Z is also disabled.
|
||||||
args+=("--no-height")
|
args=("${args[@]}" "--no-height" "--bind=ctrl-z:ignore")
|
||||||
|
|
||||||
# Handle zoomed tmux pane by moving it to a temp window
|
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||||
if tmux list-panes -F '#F' | grep -q Z; then
|
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||||
zoomed=1
|
zoomed_without_popup=1
|
||||||
original_window=$(tmux display-message -p "#{window_id}")
|
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'")
|
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'")
|
||||||
tmux swap-pane -t $tmp_window \; select-window -t $tmp_window
|
tmux swap-pane -t $tmp_window \; select-window -t $tmp_window
|
||||||
@@ -133,11 +151,23 @@ argsf="${TMPDIR:-/tmp}/fzf-args-$id"
|
|||||||
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
fifo1="${TMPDIR:-/tmp}/fzf-fifo1-$id"
|
||||||
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
fifo2="${TMPDIR:-/tmp}/fzf-fifo2-$id"
|
||||||
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
fifo3="${TMPDIR:-/tmp}/fzf-fifo3-$id"
|
||||||
|
if tmux_win_opts=$(tmux show-options -p remain-on-exit \; show-options -p synchronize-panes 2> /dev/null); then
|
||||||
|
tmux_win_opts=( $(sed '/ off/d; s/synchronize-panes/set-option -p synchronize-panes/; s/remain-on-exit/set-option -p remain-on-exit/; s/$/ \\;/' <<< "$tmux_win_opts") )
|
||||||
|
tmux_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() {
|
cleanup() {
|
||||||
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
\rm -f $argsf $fifo1 $fifo2 $fifo3
|
||||||
|
|
||||||
# Remove temp window if we were zoomed
|
# Restore tmux window options
|
||||||
if [[ -n "$zoomed" ]]; then
|
if [[ "${#tmux_win_opts[@]}" -gt 1 ]]; then
|
||||||
|
eval "tmux ${tmux_win_opts[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove temp window if we were zoomed without popup options
|
||||||
|
if [[ -n "$zoomed_without_popup" ]]; then
|
||||||
tmux display-message -p "#{window_id}" > /dev/null
|
tmux display-message -p "#{window_id}" > /dev/null
|
||||||
tmux swap-pane -t $original_window \; \
|
tmux swap-pane -t $original_window \; \
|
||||||
select-window -t $original_window \; \
|
select-window -t $original_window \; \
|
||||||
@@ -145,49 +175,67 @@ cleanup() {
|
|||||||
resize-pane -Z
|
resize-pane -Z
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ $# -gt 0 ]; then
|
if [[ $# -gt 0 ]]; then
|
||||||
|
trap - EXIT
|
||||||
exit 130
|
exit 130
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
trap 'cleanup 1' SIGUSR1
|
trap 'cleanup 1' SIGUSR1
|
||||||
trap 'cleanup' EXIT
|
trap 'cleanup' EXIT
|
||||||
|
|
||||||
envs="env TERM=$TERM "
|
envs="export TERM=$TERM "
|
||||||
|
if [[ "$opt" =~ "-E" ]]; then
|
||||||
|
tmux_version=$(tmux -V | sed 's/[^0-9.]//g')
|
||||||
|
if [[ $(awk '{print ($1 > 3.2)}' <<< "$tmux_version" 2> /dev/null || bc -l <<< "$tmux_version > 3.2") = 1 ]]; then
|
||||||
|
FZF_DEFAULT_OPTS="--border $FZF_DEFAULT_OPTS"
|
||||||
|
opt="-B $opt"
|
||||||
|
elif [[ $tmux_version = 3.2 ]]; then
|
||||||
|
FZF_DEFAULT_OPTS="--margin 0,1 $FZF_DEFAULT_OPTS"
|
||||||
|
else
|
||||||
|
echo "fzf-tmux: tmux 3.2 or above is required for popup mode" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
[[ -n "$FZF_DEFAULT_OPTS" ]] && envs="$envs FZF_DEFAULT_OPTS=$(printf %q "$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")"
|
[[ -n "$FZF_DEFAULT_COMMAND" ]] && envs="$envs FZF_DEFAULT_COMMAND=$(printf %q "$FZF_DEFAULT_COMMAND")"
|
||||||
|
[[ -n "$RUNEWIDTH_EASTASIAN" ]] && envs="$envs RUNEWIDTH_EASTASIAN=$(printf %q "$RUNEWIDTH_EASTASIAN")"
|
||||||
mkfifo -m o+w $fifo2
|
[[ -n "$BAT_THEME" ]] && envs="$envs BAT_THEME=$(printf %q "$BAT_THEME")"
|
||||||
mkfifo -m o+w $fifo3
|
echo "$envs;" > "$argsf"
|
||||||
|
|
||||||
# Build arguments to fzf
|
# Build arguments to fzf
|
||||||
opts=""
|
opts=$(printf "%q " "${args[@]}")
|
||||||
for arg in "${args[@]}"; do
|
|
||||||
arg="${arg//\\/\\\\}"
|
|
||||||
arg="${arg//\"/\\\"}"
|
|
||||||
arg="${arg//\`/\\\`}"
|
|
||||||
arg="${arg//$/\\$}"
|
|
||||||
opts="$opts \"$arg\""
|
|
||||||
done
|
|
||||||
|
|
||||||
pppid=$$
|
pppid=$$
|
||||||
trap_set="trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM"
|
echo -n "trap 'kill -SIGUSR1 -$pppid' EXIT SIGINT SIGTERM;" >> $argsf
|
||||||
trap_unset="trap - EXIT SIGINT SIGTERM"
|
close="; trap - EXIT SIGINT SIGTERM $close"
|
||||||
|
|
||||||
|
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||||
|
mkfifo -m o+w $fifo2
|
||||||
|
if [[ "$opt" =~ "-E" ]]; then
|
||||||
|
cat $fifo2 &
|
||||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||||
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" > $argsf
|
cat <<< "\"$fzf\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||||
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
|
|
||||||
set-window-option remain-on-exit off \;\
|
|
||||||
split-window $opt "$trap_set;cd $(printf %q "$PWD");$envs bash $argsf;$trap_unset" $swap \
|
|
||||||
> /dev/null 2>&1
|
|
||||||
else
|
else
|
||||||
mkfifo $fifo1
|
mkfifo $fifo1
|
||||||
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" > $argsf
|
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||||
TMUX=$(echo $TMUX | cut -d , -f 1,2) tmux set-window-option synchronize-panes off \;\
|
|
||||||
set-window-option remain-on-exit off \;\
|
|
||||||
split-window $opt "$trap_set;$envs bash $argsf;$trap_unset" $swap \
|
|
||||||
> /dev/null 2>&1
|
|
||||||
cat <&0 > $fifo1 &
|
cat <&0 > $fifo1 &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
tmux popup -d "$PWD" $opt "bash $argsf" > /dev/null 2>&1
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkfifo -m o+w $fifo3
|
||||||
|
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||||
|
cat <<< "\"$fzf\" $opts > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||||
|
else
|
||||||
|
mkfifo $fifo1
|
||||||
|
cat <<< "\"$fzf\" $opts < $fifo1 > $fifo2; echo \$? > $fifo3 $close" >> $argsf
|
||||||
|
cat <&0 > $fifo1 &
|
||||||
|
fi
|
||||||
|
tmux \
|
||||||
|
split-window -c "$PWD" $opt "bash -c 'exec -a fzf bash $argsf'" $swap \
|
||||||
|
$tmux_off_opts \
|
||||||
|
> /dev/null 2>&1 || { "$fzf" "${args[@]}"; exit $?; }
|
||||||
cat $fifo2
|
cat $fifo2
|
||||||
exit "$(cat $fifo3)"
|
exit "$(cat $fifo3)"
|
||||||
|
|
||||||
|
|||||||
470
doc/fzf.txt
470
doc/fzf.txt
@@ -1,33 +1,104 @@
|
|||||||
fzf.txt fzf Last change: April 28 2017
|
fzf.txt fzf Last change: September 17 2023
|
||||||
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
FZF - TABLE OF CONTENTS *fzf* *fzf-toc*
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
FZF Vim integration
|
FZF Vim integration |fzf-vim-integration|
|
||||||
:FZF[!]
|
Installation |fzf-installation|
|
||||||
Configuration
|
Summary |fzf-summary|
|
||||||
Examples
|
:FZF[!] |:FZF|
|
||||||
fzf#run
|
Configuration |fzf-configuration|
|
||||||
fzf#wrap
|
Examples |fzf-examples|
|
||||||
GVim
|
Explanation of g:fzf_colors |fzf-explanation-of-gfzfcolors|
|
||||||
License
|
fzf#run |fzf#run|
|
||||||
|
fzf#wrap |fzf#wrap|
|
||||||
|
Global options supported by fzf#wrap |fzf-global-options-supported-by-fzf#wrap|
|
||||||
|
Tips |fzf-tips|
|
||||||
|
fzf inside terminal buffer |fzf-inside-terminal-buffer|
|
||||||
|
Starting fzf in a popup window |fzf-starting-fzf-in-a-popup-window|
|
||||||
|
Hide statusline |fzf-hide-statusline|
|
||||||
|
License |fzf-license|
|
||||||
|
|
||||||
FZF VIM INTEGRATION *fzf-vim-integration*
|
FZF VIM INTEGRATION *fzf-vim-integration*
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
This repository only enables basic integration with Vim. If you're looking for
|
|
||||||
more, check out {fzf.vim}{1} project.
|
|
||||||
|
|
||||||
(Note: To use fzf in GVim, an external terminal emulator is required.)
|
INSTALLATION *fzf-installation*
|
||||||
|
==============================================================================
|
||||||
|
|
||||||
{1} https://github.com/junegunn/fzf.vim
|
Once you have fzf installed, you can enable it inside Vim simply by adding the
|
||||||
|
directory to 'runtimepath' in your Vim configuration file. The path may differ
|
||||||
|
depending on the package manager.
|
||||||
|
>
|
||||||
|
" If installed using Homebrew
|
||||||
|
set rtp+=/usr/local/opt/fzf
|
||||||
|
|
||||||
|
" 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:
|
||||||
|
>
|
||||||
|
" If installed using Homebrew
|
||||||
|
Plug '/usr/local/opt/fzf'
|
||||||
|
|
||||||
|
" 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
|
||||||
|
included in the package, write:
|
||||||
|
>
|
||||||
|
Plug 'junegunn/fzf'
|
||||||
|
<
|
||||||
|
The Vim plugin will pick up fzf binary available on the system. If fzf is not
|
||||||
|
found on `$PATH`, it will ask you if it should download the latest binary for
|
||||||
|
you.
|
||||||
|
|
||||||
|
To make sure that you have the latest version of the binary, set up
|
||||||
|
post-update hook like so:
|
||||||
|
|
||||||
|
*fzf#install*
|
||||||
|
>
|
||||||
|
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
|
||||||
|
<
|
||||||
|
{1} https://github.com/junegunn/vim-plug
|
||||||
|
|
||||||
|
|
||||||
|
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])`
|
||||||
|
- Starts fzf inside Vim with the given spec
|
||||||
|
- `: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'})`
|
||||||
|
- 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]`
|
||||||
|
- Basic fuzzy file selector
|
||||||
|
- A reference implementation for those who don't want to write VimScript to
|
||||||
|
implement custom commands
|
||||||
|
- If you're looking for more such commands, check out {fzf.vim}{2} project.
|
||||||
|
|
||||||
|
The most important of all is `fzf#run`, but it would be easier to understand
|
||||||
|
the whole if we start off with `:FZF` command.
|
||||||
|
|
||||||
|
{2} https://github.com/junegunn/fzf.vim
|
||||||
|
|
||||||
|
|
||||||
:FZF[!]
|
:FZF[!]
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
*:FZF*
|
*:FZF*
|
||||||
|
|
||||||
If you have set up fzf for Vim, `:FZF` command will be added.
|
|
||||||
>
|
>
|
||||||
" Look for files under current directory
|
" Look for files under current directory
|
||||||
:FZF
|
:FZF
|
||||||
@@ -35,40 +106,36 @@ If you have set up fzf for Vim, `:FZF` command will be added.
|
|||||||
" Look for files under your home directory
|
" Look for files under your home directory
|
||||||
:FZF ~
|
:FZF ~
|
||||||
|
|
||||||
" With options
|
" With fzf command-line options
|
||||||
:FZF --no-sort --reverse --inline-info /tmp
|
:FZF --reverse --info=inline /tmp
|
||||||
|
|
||||||
" Bang version starts fzf in fullscreen mode
|
" Bang version starts fzf in fullscreen mode
|
||||||
:FZF!
|
:FZF!
|
||||||
<
|
<
|
||||||
Similarly to {ctrlp.vim}{2}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
|
Similarly to {ctrlp.vim}{3}, use enter key, CTRL-T, CTRL-X or CTRL-V to open
|
||||||
selected files in the current window, in new tabs, in horizontal splits, or in
|
selected files in the current window, in new tabs, in horizontal splits, or in
|
||||||
vertical splits respectively.
|
vertical splits respectively.
|
||||||
|
|
||||||
Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
Note that the environment variables `FZF_DEFAULT_COMMAND` and
|
||||||
`FZF_DEFAULT_OPTS` also apply here.
|
`FZF_DEFAULT_OPTS` also apply here.
|
||||||
|
|
||||||
{2} https://github.com/kien/ctrlp.vim
|
{3} https://github.com/kien/ctrlp.vim
|
||||||
|
|
||||||
|
|
||||||
< Configuration >_____________________________________________________________~
|
< Configuration >_____________________________________________________________~
|
||||||
*fzf-configuration*
|
*fzf-configuration*
|
||||||
|
|
||||||
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir* *g:fzf_launcher*
|
*g:fzf_action* *g:fzf_layout* *g:fzf_colors* *g:fzf_history_dir*
|
||||||
*g:Fzf_launcher*
|
|
||||||
|
|
||||||
- `g:fzf_action`
|
- `g:fzf_action`
|
||||||
- Customizable extra key bindings for opening selected files in different
|
- Customizable extra key bindings for opening selected files in different
|
||||||
ways
|
ways
|
||||||
- `g:fzf_layout`
|
- `g:fzf_layout`
|
||||||
- Determines the size and position of fzf window (tmux pane or Neovim split)
|
- Determines the size and position of fzf window
|
||||||
- `g:fzf_colors`
|
- `g:fzf_colors`
|
||||||
- Customizes fzf colors to match the current color scheme
|
- Customizes fzf colors to match the current color scheme
|
||||||
- `g:fzf_history_dir`
|
- `g:fzf_history_dir`
|
||||||
- Enables history feature
|
- Enables history feature
|
||||||
- `g:fzf_launcher`
|
|
||||||
- (Only in GVim) Terminal emulator to open fzf with
|
|
||||||
- `g:Fzf_launcher` for function reference
|
|
||||||
|
|
||||||
|
|
||||||
Examples~
|
Examples~
|
||||||
@@ -80,16 +147,39 @@ Examples~
|
|||||||
\ 'ctrl-x': 'split',
|
\ 'ctrl-x': 'split',
|
||||||
\ 'ctrl-v': 'vsplit' }
|
\ 'ctrl-v': 'vsplit' }
|
||||||
|
|
||||||
" Default fzf layout
|
" An action can be a reference to a function that processes selected lines
|
||||||
" - down / up / left / right
|
function! s:build_quickfix_list(lines)
|
||||||
let g:fzf_layout = { 'down': '~40%' }
|
call setqflist(map(copy(a:lines), '{ "filename": v:val, "lnum": 1 }'))
|
||||||
|
copen
|
||||||
|
cc
|
||||||
|
endfunction
|
||||||
|
|
||||||
" In Neovim, you can set up fzf window using a Vim command
|
let g:fzf_action = {
|
||||||
|
\ 'ctrl-q': function('s:build_quickfix_list'),
|
||||||
|
\ 'ctrl-t': 'tab split',
|
||||||
|
\ 'ctrl-x': 'split',
|
||||||
|
\ 'ctrl-v': 'vsplit' }
|
||||||
|
|
||||||
|
" Default fzf layout
|
||||||
|
" - Popup window (center of the screen)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
|
|
||||||
|
" - Popup window (center of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true } }
|
||||||
|
|
||||||
|
" - Popup window (anchored to the bottom of the current window)
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6, 'relative': v:true, 'yoffset': 1.0 } }
|
||||||
|
|
||||||
|
" - down / up / left / right
|
||||||
|
let g:fzf_layout = { 'down': '40%' }
|
||||||
|
|
||||||
|
" - Window using a Vim command
|
||||||
let g:fzf_layout = { 'window': 'enew' }
|
let g:fzf_layout = { 'window': 'enew' }
|
||||||
let g:fzf_layout = { 'window': '-tabnew' }
|
let g:fzf_layout = { 'window': '-tabnew' }
|
||||||
let g:fzf_layout = { 'window': '10split enew' }
|
let g:fzf_layout = { 'window': '10new' }
|
||||||
|
|
||||||
" Customize fzf colors to match your color scheme
|
" Customize fzf colors to match your color scheme
|
||||||
|
" - fzf#wrap translates this to a set of `--color` options
|
||||||
let g:fzf_colors =
|
let g:fzf_colors =
|
||||||
\ { 'fg': ['fg', 'Normal'],
|
\ { 'fg': ['fg', 'Normal'],
|
||||||
\ 'bg': ['bg', 'Normal'],
|
\ 'bg': ['bg', 'Normal'],
|
||||||
@@ -98,85 +188,331 @@ Examples~
|
|||||||
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
\ 'bg+': ['bg', 'CursorLine', 'CursorColumn'],
|
||||||
\ 'hl+': ['fg', 'Statement'],
|
\ 'hl+': ['fg', 'Statement'],
|
||||||
\ 'info': ['fg', 'PreProc'],
|
\ 'info': ['fg', 'PreProc'],
|
||||||
|
\ 'border': ['fg', 'Ignore'],
|
||||||
\ 'prompt': ['fg', 'Conditional'],
|
\ 'prompt': ['fg', 'Conditional'],
|
||||||
\ 'pointer': ['fg', 'Exception'],
|
\ 'pointer': ['fg', 'Exception'],
|
||||||
\ 'marker': ['fg', 'Keyword'],
|
\ 'marker': ['fg', 'Keyword'],
|
||||||
\ 'spinner': ['fg', 'Label'],
|
\ 'spinner': ['fg', 'Label'],
|
||||||
\ 'header': ['fg', 'Comment'] }
|
\ 'header': ['fg', 'Comment'] }
|
||||||
|
|
||||||
" Enable per-command history.
|
" Enable per-command history
|
||||||
" CTRL-N and CTRL-P will be automatically bound to next-history and
|
" - History files will be stored in the specified directory
|
||||||
" previous-history instead of down and up. If you don't like the change,
|
" - When set, CTRL-N and CTRL-P will be bound to 'next-history' and
|
||||||
" explicitly bind the keys to down and up in your $FZF_DEFAULT_OPTS.
|
" 'previous-history' instead of 'down' and 'up'.
|
||||||
let g:fzf_history_dir = '~/.local/share/fzf-history'
|
let g:fzf_history_dir = '~/.local/share/fzf-history'
|
||||||
<
|
<
|
||||||
|
|
||||||
FZF#RUN *fzf#run*
|
Explanation of g:fzf_colors~
|
||||||
|
*fzf-explanation-of-gfzfcolors*
|
||||||
|
|
||||||
|
`g:fzf_colors` is a dictionary mapping fzf elements to a color specification
|
||||||
|
list:
|
||||||
|
>
|
||||||
|
element: [ component, group1 [, group2, ...] ]
|
||||||
|
<
|
||||||
|
- `element` is an fzf element to apply a color to:
|
||||||
|
|
||||||
|
----------------------------+------------------------------------------------------
|
||||||
|
Element | Description ~
|
||||||
|
----------------------------+------------------------------------------------------
|
||||||
|
`fg` / `bg` / `hl` | Item (foreground / background / highlight)
|
||||||
|
`fg+` / `bg+` / `hl+` | Current item (foreground / background / highlight)
|
||||||
|
`preview-fg` / `preview-bg` | Preview window text and background
|
||||||
|
`hl` / `hl+` | Highlighted substrings (normal / current)
|
||||||
|
`gutter` | Background of the gutter on the left
|
||||||
|
`pointer` | Pointer to the current line ( `>` )
|
||||||
|
`marker` | Multi-select marker ( `>` )
|
||||||
|
`border` | Border around the window ( `--border` and `--preview` )
|
||||||
|
`header` | Header ( `--header` or `--header-lines` )
|
||||||
|
`info` | Info line (match counters)
|
||||||
|
`spinner` | Streaming input indicator
|
||||||
|
`query` | Query string
|
||||||
|
`disabled` | Query string when search is disabled
|
||||||
|
`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
|
||||||
|
order) for a matching color definition
|
||||||
|
|
||||||
|
For example, consider the following specification:
|
||||||
|
>
|
||||||
|
'prompt': ['fg', 'Conditional', 'Comment'],
|
||||||
|
<
|
||||||
|
This means we color the prompt - using the `fg` attribute of the `Conditional`
|
||||||
|
if it exists, - otherwise use the `fg` attribute of the `Comment` highlight
|
||||||
|
group if it exists, - otherwise fall back to the default color settings for
|
||||||
|
the prompt.
|
||||||
|
|
||||||
|
You can examine the color option generated according the setting by printing
|
||||||
|
the result of `fzf#wrap()` function like so:
|
||||||
|
>
|
||||||
|
:echo fzf#wrap()
|
||||||
|
<
|
||||||
|
|
||||||
|
FZF#RUN
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
For more advanced uses, you can use `fzf#run([options])` function with the
|
*fzf#run*
|
||||||
following options.
|
|
||||||
|
|
||||||
---------------------------+---------------+--------------------------------------------------------------
|
`fzf#run()` function is the core of Vim integration. It takes a single
|
||||||
|
dictionary argument, a spec, and starts fzf process accordingly. At the very
|
||||||
|
least, specify `sink` option to tell what it should do with the selected
|
||||||
|
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.
|
||||||
|
>
|
||||||
|
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.
|
||||||
|
>
|
||||||
|
call fzf#run({'source': 'git ls-files', 'sink': 'e'})
|
||||||
|
<
|
||||||
|
fzf options can be specified as `options` entry in spec dictionary.
|
||||||
|
>
|
||||||
|
call fzf#run({'sink': 'tabedit', 'options': '--multi --reverse'})
|
||||||
|
<
|
||||||
|
You can also pass a layout option if you don't want fzf window to take up the
|
||||||
|
entire screen.
|
||||||
|
>
|
||||||
|
" up / down / left / right / window are allowed
|
||||||
|
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'left': '40%'})
|
||||||
|
call fzf#run({'source': 'git ls-files', 'sink': 'e', 'window': '30vnew'})
|
||||||
|
<
|
||||||
|
`source` doesn't have to be an external shell command, you can pass a Vim
|
||||||
|
array as the source. In the next example, we pass the names of color schemes
|
||||||
|
as the source to implement a color scheme selector.
|
||||||
|
>
|
||||||
|
call fzf#run({'source': map(split(globpath(&rtp, 'colors/*.vim')),
|
||||||
|
\ 'fnamemodify(v:val, ":t:r")'),
|
||||||
|
\ 'sink': 'colo', 'left': '25%'})
|
||||||
|
<
|
||||||
|
The following table summarizes the available options.
|
||||||
|
|
||||||
|
---------------------------+---------------+----------------------------------------------------------------------
|
||||||
Option name | Type | Description ~
|
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
|
`source` | list | Vim list as input to fzf
|
||||||
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
`sink` | string | Vim command to handle the selected item (e.g. `e` , `tabe` )
|
||||||
`sink` | funcref | Reference to function to process each selected item
|
`sink` | funcref | Reference to function to process each selected item
|
||||||
`sink*` | funcref | Similar to `sink` , but takes the list of output lines at once
|
`sinklist` (or `sink*` ) | funcref | Similar to `sink` , but takes the list of output lines at once
|
||||||
`options` | string/list | Options to fzf
|
`options` | string/list | Options to fzf
|
||||||
`dir` | string | Working directory
|
`dir` | string | Working directory
|
||||||
`up` / `down` / `left` / `right` | number/string | Use tmux pane with the given size (e.g. `20` , `50%` )
|
`up` / `down` / `left` / `right` | number/string | (Layout) Window position and size (e.g. `20` , `50%` )
|
||||||
`window` (Neovim only) | string | Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
`tmux` | string | (Layout) fzf-tmux options (e.g. `-p90%,60%` )
|
||||||
`launcher` | string | External terminal emulator to start fzf with (GVim only)
|
`window` (Vim 8 / Neovim) | string | (Layout) Command to open fzf window (e.g. `vertical aboveleft 30new` )
|
||||||
`launcher` | funcref | Function for generating `launcher` string (GVim only)
|
`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
|
`options` entry can be either a string or a list. For simple cases, string
|
||||||
should suffice, but prefer to use list type if you're concerned about escaping
|
should suffice, but prefer to use list type to avoid escaping issues.
|
||||||
issues on different platforms.
|
|
||||||
>
|
>
|
||||||
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
call fzf#run({'options': '--reverse --prompt "C:\\Program Files\\"'})
|
||||||
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
call fzf#run({'options': ['--reverse', '--prompt', 'C:\Program Files\']})
|
||||||
<
|
<
|
||||||
|
When `window` entry is a dictionary, fzf will start in a popup window. The
|
||||||
|
following options are allowed:
|
||||||
|
|
||||||
FZF#WRAP *fzf#wrap*
|
- Required:
|
||||||
|
- `width` [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||||
|
- `height` [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||||
|
- Optional:
|
||||||
|
- `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` (`sharp` on Windows)]: Border style
|
||||||
|
- `rounded` / `sharp` / `horizontal` / `vertical` / `top` / `bottom` / `left` / `right` / `no[ne]`
|
||||||
|
|
||||||
|
|
||||||
|
FZF#WRAP
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
`fzf#wrap([name string,] [opts dict,] [fullscreen boolean])` is a helper
|
*fzf#wrap*
|
||||||
function that decorates the options dictionary so that it understands
|
|
||||||
`g:fzf_layout`, `g:fzf_action`, `g:fzf_colors`, and `g:fzf_history_dir` like
|
We have seen that several aspects of `:FZF` command can be configured with a
|
||||||
`:FZF`.
|
set of global option variables; different ways to open files (`g:fzf_action`),
|
||||||
|
window position and size (`g:fzf_layout`), color palette (`g:fzf_colors`),
|
||||||
|
etc.
|
||||||
|
|
||||||
|
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)`
|
||||||
|
- 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`
|
||||||
|
is not defined.
|
||||||
|
- `fullscreen` can be either `0` or `1` (default: 0).
|
||||||
|
|
||||||
|
`fzf#wrap` takes a spec and returns an extended version of it (also a
|
||||||
|
dictionary) with additional options for addressing global preferences. You can
|
||||||
|
examine the return value of it like so:
|
||||||
>
|
>
|
||||||
command! -bang MyStuff
|
echo fzf#wrap({'source': 'ls'})
|
||||||
\ call fzf#run(fzf#wrap('my-stuff', {'dir': '~/my-stuff'}, <bang>0))
|
<
|
||||||
|
After we "wrap" our spec, we pass it to `fzf#run`.
|
||||||
|
>
|
||||||
|
call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||||
|
<
|
||||||
|
Now it supports CTRL-T, CTRL-V, and CTRL-X key bindings (configurable via
|
||||||
|
`g:fzf_action`) and it opens fzf window according to `g:fzf_layout` setting.
|
||||||
|
|
||||||
|
To make it easier to use, let's define `LS` command.
|
||||||
|
>
|
||||||
|
command! LS call fzf#run(fzf#wrap({'source': 'ls'}))
|
||||||
|
<
|
||||||
|
Type `:LS` and see how it works.
|
||||||
|
|
||||||
|
We would like to make `:LS!` (bang version) open fzf in fullscreen, just like
|
||||||
|
`:FZF!`. Add `-bang` to command definition, and use <bang> value to set the
|
||||||
|
last `fullscreen` argument of `fzf#wrap` (see :help <bang>).
|
||||||
|
>
|
||||||
|
" On :LS!, <bang> evaluates to '!', and '!0' becomes 1
|
||||||
|
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.
|
||||||
|
>
|
||||||
|
command! -bang -complete=dir -nargs=? LS
|
||||||
|
\ call fzf#run(fzf#wrap({'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
|
<
|
||||||
|
Lastly, if you have enabled `g:fzf_history_dir`, you might want to assign a
|
||||||
|
unique name to our command and pass it as the first argument to `fzf#wrap`.
|
||||||
|
>
|
||||||
|
" The query history for this command will be stored as 'ls' inside g:fzf_history_dir.
|
||||||
|
" The name is ignored if g:fzf_history_dir is not defined.
|
||||||
|
command! -bang -complete=dir -nargs=? LS
|
||||||
|
\ call fzf#run(fzf#wrap('ls', {'source': 'ls', 'dir': <q-args>}, <bang>0))
|
||||||
<
|
<
|
||||||
|
|
||||||
GVIM *fzf-gvim*
|
< Global options supported by fzf#wrap >______________________________________~
|
||||||
|
*fzf-global-options-supported-by-fzf#wrap*
|
||||||
|
|
||||||
|
- `g:fzf_layout`
|
||||||
|
- `g:fzf_action`
|
||||||
|
- 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)
|
||||||
|
- `g:fzf_colors`
|
||||||
|
- `g:fzf_history_dir`
|
||||||
|
|
||||||
|
|
||||||
|
TIPS *fzf-tips*
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
In GVim, you need an external terminal emulator to start fzf with. `xterm`
|
|
||||||
command is used by default, but you can customize it with `g:fzf_launcher`.
|
< 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*
|
||||||
>
|
>
|
||||||
" This is the default. %s is replaced with fzf command
|
" Terminal colors for seoul256 color scheme
|
||||||
let g:fzf_launcher = 'xterm -e bash -ic %s'
|
if has('nvim')
|
||||||
|
let g:terminal_color_0 = '#4e4e4e'
|
||||||
" Use urxvt instead
|
let g:terminal_color_1 = '#d68787'
|
||||||
let g:fzf_launcher = 'urxvt -geometry 120x30 -e sh -c %s'
|
let g:terminal_color_2 = '#5f865f'
|
||||||
|
let g:terminal_color_3 = '#d8af5f'
|
||||||
|
let g:terminal_color_4 = '#85add4'
|
||||||
|
let g:terminal_color_5 = '#d7afaf'
|
||||||
|
let g:terminal_color_6 = '#87afaf'
|
||||||
|
let g:terminal_color_7 = '#d0d0d0'
|
||||||
|
let g:terminal_color_8 = '#626262'
|
||||||
|
let g:terminal_color_9 = '#d75f87'
|
||||||
|
let g:terminal_color_10 = '#87af87'
|
||||||
|
let g:terminal_color_11 = '#ffd787'
|
||||||
|
let g:terminal_color_12 = '#add4fb'
|
||||||
|
let g:terminal_color_13 = '#ffafaf'
|
||||||
|
let g:terminal_color_14 = '#87d7d7'
|
||||||
|
let g:terminal_color_15 = '#e4e4e4'
|
||||||
|
else
|
||||||
|
let g:terminal_ansi_colors = [
|
||||||
|
\ '#4e4e4e', '#d68787', '#5f865f', '#d8af5f',
|
||||||
|
\ '#85add4', '#d7afaf', '#87afaf', '#d0d0d0',
|
||||||
|
\ '#626262', '#d75f87', '#87af87', '#ffd787',
|
||||||
|
\ '#add4fb', '#ffafaf', '#87d7d7', '#e4e4e4'
|
||||||
|
\ ]
|
||||||
|
endif
|
||||||
<
|
<
|
||||||
If you're running MacVim on OSX, I recommend you to use iTerm2 as the
|
|
||||||
launcher. Refer to the {this wiki page}{3} to see how to set up.
|
|
||||||
|
|
||||||
{3} https://github.com/junegunn/fzf/wiki/On-MacVim-with-iTerm2
|
< Starting fzf in a popup window >____________________________________________~
|
||||||
|
*fzf-starting-fzf-in-a-popup-window*
|
||||||
|
>
|
||||||
|
" Required:
|
||||||
|
" - width [float range [0 ~ 1]] or [integer range [8 ~ ]]
|
||||||
|
" - height [float range [0 ~ 1]] or [integer range [4 ~ ]]
|
||||||
|
"
|
||||||
|
" Optional:
|
||||||
|
" - xoffset [float default 0.5 range [0 ~ 1]]
|
||||||
|
" - yoffset [float default 0.5 range [0 ~ 1]]
|
||||||
|
" - relative [boolean default v:false]
|
||||||
|
" - border [string default 'rounded']: Border style
|
||||||
|
" - 'rounded' / 'sharp' / 'horizontal' / 'vertical' / 'top' / 'bottom' / 'left' / 'right'
|
||||||
|
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.
|
||||||
|
>
|
||||||
|
" See `man fzf-tmux` for available options
|
||||||
|
if exists('$TMUX')
|
||||||
|
let g:fzf_layout = { 'tmux': '-p90%,60%' }
|
||||||
|
else
|
||||||
|
let g:fzf_layout = { 'window': { 'width': 0.9, 'height': 0.6 } }
|
||||||
|
endif
|
||||||
|
<
|
||||||
|
|
||||||
|
< Hide statusline >___________________________________________________________~
|
||||||
|
*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
|
||||||
|
the window.
|
||||||
|
|
||||||
|
For example, if you open fzf on the bottom on the screen (e.g. `{'down':
|
||||||
|
'40%'}`), you might want to temporarily disable the statusline for a cleaner
|
||||||
|
look.
|
||||||
|
>
|
||||||
|
let g:fzf_layout = { 'down': '30%' }
|
||||||
|
autocmd! FileType fzf
|
||||||
|
autocmd FileType fzf set laststatus=0 noshowmode noruler
|
||||||
|
\| autocmd BufLeave <buffer> set laststatus=2 showmode ruler
|
||||||
|
<
|
||||||
|
|
||||||
LICENSE *fzf-license*
|
LICENSE *fzf-license*
|
||||||
==============================================================================
|
==============================================================================
|
||||||
|
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017 Junegunn Choi
|
Copyright (c) 2013-2023 Junegunn Choi
|
||||||
|
|
||||||
==============================================================================
|
==============================================================================
|
||||||
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
vim:tw=78:sw=2:ts=2:ft=help:norl:nowrap:
|
||||||
|
|||||||
38
glide.lock
generated
38
glide.lock
generated
@@ -1,38 +0,0 @@
|
|||||||
hash: d68dd0bd779ac4ffca1e0c49ca38d85f90d5d68fa8e2d5d7db70a8ce8c662ec1
|
|
||||||
updated: 2017-06-01T15:48:41.653745249-07:00
|
|
||||||
imports:
|
|
||||||
- name: github.com/gdamore/encoding
|
|
||||||
version: b23993cbb6353f0e6aa98d0ee318a34728f628b9
|
|
||||||
- name: github.com/gdamore/tcell
|
|
||||||
version: 44772c121bb7838819d3ba4a7e84c0c2d617328e
|
|
||||||
subpackages:
|
|
||||||
- encoding
|
|
||||||
- name: github.com/lucasb-eyer/go-colorful
|
|
||||||
version: c900de9dbbc73129068f5af6a823068fc5f2308c
|
|
||||||
- name: github.com/mattn/go-isatty
|
|
||||||
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
|
|
||||||
- name: github.com/mattn/go-runewidth
|
|
||||||
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
|
||||||
- name: github.com/mattn/go-shellwords
|
|
||||||
version: 02e3cf038dcea8290e44424da473dd12be796a8a
|
|
||||||
- name: golang.org/x/crypto
|
|
||||||
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
|
|
||||||
subpackages:
|
|
||||||
- ssh/terminal
|
|
||||||
- name: golang.org/x/sys
|
|
||||||
version: b90f89a1e7a9c1f6b918820b3daa7f08488c8594
|
|
||||||
subpackages:
|
|
||||||
- unix
|
|
||||||
- name: golang.org/x/text
|
|
||||||
version: 4ee4af566555f5fbe026368b75596286a312663a
|
|
||||||
subpackages:
|
|
||||||
- encoding
|
|
||||||
- encoding/charmap
|
|
||||||
- encoding/internal
|
|
||||||
- encoding/internal/identifier
|
|
||||||
- encoding/japanese
|
|
||||||
- encoding/korean
|
|
||||||
- encoding/simplifiedchinese
|
|
||||||
- encoding/traditionalchinese
|
|
||||||
- transform
|
|
||||||
testImports: []
|
|
||||||
16
glide.yaml
16
glide.yaml
@@ -1,16 +0,0 @@
|
|||||||
package: github.com/junegunn/fzf
|
|
||||||
import:
|
|
||||||
- package: github.com/mattn/go-isatty
|
|
||||||
version: 66b8e73f3f5cda9f96b69efd03dd3d7fc4a5cdb8
|
|
||||||
- package: github.com/mattn/go-runewidth
|
|
||||||
version: 14207d285c6c197daabb5c9793d63e7af9ab2d50
|
|
||||||
- package: github.com/mattn/go-shellwords
|
|
||||||
version: 02e3cf038dcea8290e44424da473dd12be796a8a
|
|
||||||
- package: github.com/gdamore/tcell
|
|
||||||
version: 44772c121bb7838819d3ba4a7e84c0c2d617328e
|
|
||||||
subpackages:
|
|
||||||
- encoding
|
|
||||||
- package: golang.org/x/crypto
|
|
||||||
version: e1a4589e7d3ea14a3352255d04b6f1a418845e5e
|
|
||||||
subpackages:
|
|
||||||
- ssh/terminal
|
|
||||||
21
go.mod
Normal file
21
go.mod
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
module github.com/junegunn/fzf
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gdamore/tcell/v2 v2.5.4
|
||||||
|
github.com/mattn/go-isatty v0.0.17
|
||||||
|
github.com/mattn/go-runewidth v0.0.14
|
||||||
|
github.com/mattn/go-shellwords v1.0.12
|
||||||
|
github.com/rivo/uniseg v0.4.4
|
||||||
|
github.com/saracen/walker v0.1.3
|
||||||
|
golang.org/x/sys v0.14.0
|
||||||
|
golang.org/x/term v0.13.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gdamore/encoding v1.0.0 // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 // indirect
|
||||||
|
golang.org/x/text v0.5.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
go 1.17
|
||||||
48
go.sum
Normal file
48
go.sum
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||||
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
|
github.com/gdamore/tcell/v2 v2.5.4 h1:TGU4tSjD3sCL788vFNeJnTdzpNKIw1H5dgLnJRQVv/k=
|
||||||
|
github.com/gdamore/tcell/v2 v2.5.4/go.mod h1:dZgRy5v4iMobMEcWNYBtREnDZAT9DYmfqIkrgEMxLyw=
|
||||||
|
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.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
|
||||||
|
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
|
||||||
|
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
|
||||||
|
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/saracen/walker v0.1.3 h1:YtcKKmpRPy6XJTHJ75J2QYXXZYWnZNQxPCVqZSHVV/g=
|
||||||
|
github.com/saracen/walker v0.1.3/go.mod h1:FU+7qU8DeQQgSZDmmThMJi93kPkLFgy0oVAcLxurjIk=
|
||||||
|
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/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/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4 h1:uVc8UZUe6tr40fFVnUP5Oj+veunVezqYl9z7DYw9xzw=
|
||||||
|
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/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-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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||||
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
|
||||||
|
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||||
|
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
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/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
164
install
164
install
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
version=0.16.8
|
version=0.44.0
|
||||||
auto_completion=
|
auto_completion=
|
||||||
key_bindings=
|
key_bindings=
|
||||||
update_config=2
|
update_config=2
|
||||||
binary_arch=
|
shells="bash zsh fish"
|
||||||
allow_legacy=
|
prefix='~/.fzf'
|
||||||
|
prefix_expand=~/.fzf
|
||||||
|
fish_dir=${XDG_CONFIG_HOME:-$HOME/.config}/fish
|
||||||
|
|
||||||
help() {
|
help() {
|
||||||
cat << EOF
|
cat << EOF
|
||||||
@@ -17,12 +19,14 @@ usage: $0 [OPTIONS]
|
|||||||
--bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh}
|
--bin Download fzf binary only; Do not generate ~/.fzf.{bash,zsh}
|
||||||
--all Download fzf binary and update configuration files
|
--all Download fzf binary and update configuration files
|
||||||
to enable key bindings and fuzzy completion
|
to enable key bindings and fuzzy completion
|
||||||
|
--xdg Generate files under \$XDG_CONFIG_HOME/fzf
|
||||||
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
|
--[no-]key-bindings Enable/disable key bindings (CTRL-T, CTRL-R, ALT-C)
|
||||||
--[no-]completion Enable/disable fuzzy completion (bash & zsh)
|
--[no-]completion Enable/disable fuzzy completion (bash & zsh)
|
||||||
--[no-]update-rc Whether or not to update shell configuration files
|
--[no-]update-rc Whether or not to update shell configuration files
|
||||||
|
|
||||||
--32 Download 32-bit binary
|
--no-bash Do not set up bash configuration
|
||||||
--64 Download 64-bit binary
|
--no-zsh Do not set up zsh configuration
|
||||||
|
--no-fish Do not set up fish configuration
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +40,11 @@ for opt in "$@"; do
|
|||||||
auto_completion=1
|
auto_completion=1
|
||||||
key_bindings=1
|
key_bindings=1
|
||||||
update_config=1
|
update_config=1
|
||||||
allow_legacy=1
|
;;
|
||||||
|
--xdg)
|
||||||
|
prefix='"${XDG_CONFIG_HOME:-$HOME/.config}"/fzf/fzf'
|
||||||
|
prefix_expand=${XDG_CONFIG_HOME:-$HOME/.config}/fzf/fzf
|
||||||
|
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/fzf"
|
||||||
;;
|
;;
|
||||||
--key-bindings) key_bindings=1 ;;
|
--key-bindings) key_bindings=1 ;;
|
||||||
--no-key-bindings) key_bindings=0 ;;
|
--no-key-bindings) key_bindings=0 ;;
|
||||||
@@ -44,9 +52,10 @@ for opt in "$@"; do
|
|||||||
--no-completion) auto_completion=0 ;;
|
--no-completion) auto_completion=0 ;;
|
||||||
--update-rc) update_config=1 ;;
|
--update-rc) update_config=1 ;;
|
||||||
--no-update-rc) update_config=0 ;;
|
--no-update-rc) update_config=0 ;;
|
||||||
--32) binary_arch=386 ;;
|
|
||||||
--64) binary_arch=amd64 ;;
|
|
||||||
--bin) ;;
|
--bin) ;;
|
||||||
|
--no-bash) shells=${shells/bash/} ;;
|
||||||
|
--no-zsh) shells=${shells/zsh/} ;;
|
||||||
|
--no-fish) shells=${shells/fish/} ;;
|
||||||
*)
|
*)
|
||||||
echo "unknown option: $opt"
|
echo "unknown option: $opt"
|
||||||
help
|
help
|
||||||
@@ -56,27 +65,31 @@ for opt in "$@"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
fzf_base="$(pwd)"
|
fzf_base=$(pwd)
|
||||||
|
fzf_base_esc=$(printf %q "$fzf_base")
|
||||||
|
|
||||||
ask() {
|
ask() {
|
||||||
# If stdin is a tty, we are "interactive".
|
while true; do
|
||||||
# non-interactive shell: wait for a linefeed
|
read -p "$1 ([y]/n) " -r
|
||||||
# interactive shell: continue after a single keypress
|
REPLY=${REPLY:-"y"}
|
||||||
read_n=$([ -t 0 ] && echo "-n 1")
|
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
return 1
|
||||||
read -p "$1 ([y]/n) " $read_n -r
|
elif [[ $REPLY =~ ^[Nn]$ ]]; then
|
||||||
echo
|
return 0
|
||||||
[[ $REPLY =~ ^[Nn]$ ]]
|
fi
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
check_binary() {
|
check_binary() {
|
||||||
echo -n " - Checking fzf executable ... "
|
echo -n " - Checking fzf executable ... "
|
||||||
local output
|
local output
|
||||||
output=$("$fzf_base"/bin/fzf --version 2>&1 | awk '{print $1}')
|
output=$("$fzf_base"/bin/fzf --version 2>&1)
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
echo "Error: $output"
|
echo "Error: $output"
|
||||||
binary_error="Invalid binary"
|
binary_error="Invalid binary"
|
||||||
elif [ "$version" != "$output" ]; then
|
else
|
||||||
|
output=${output/ */}
|
||||||
|
if [ "$version" != "$output" ]; then
|
||||||
echo "$output != $version"
|
echo "$output != $version"
|
||||||
binary_error="Invalid version"
|
binary_error="Invalid version"
|
||||||
else
|
else
|
||||||
@@ -84,6 +97,7 @@ check_binary() {
|
|||||||
binary_error=""
|
binary_error=""
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
rm -f "$fzf_base"/bin/fzf
|
rm -f "$fzf_base"/bin/fzf
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
@@ -91,7 +105,7 @@ check_binary() {
|
|||||||
link_fzf_in_path() {
|
link_fzf_in_path() {
|
||||||
if which_fzf="$(command -v fzf)"; then
|
if which_fzf="$(command -v fzf)"; then
|
||||||
echo " - Found in \$PATH"
|
echo " - Found in \$PATH"
|
||||||
echo " - Creating symlink: $which_fzf -> bin/fzf"
|
echo " - Creating symlink: bin/fzf -> $which_fzf"
|
||||||
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
(cd "$fzf_base"/bin && rm -f fzf && ln -sf "$which_fzf" fzf)
|
||||||
check_binary && return
|
check_binary && return
|
||||||
fi
|
fi
|
||||||
@@ -100,7 +114,7 @@ link_fzf_in_path() {
|
|||||||
|
|
||||||
try_curl() {
|
try_curl() {
|
||||||
command -v curl > /dev/null &&
|
command -v curl > /dev/null &&
|
||||||
if [[ $1 =~ tgz$ ]]; then
|
if [[ $1 =~ tar.gz$ ]]; then
|
||||||
curl -fL $1 | tar -xzf -
|
curl -fL $1 | tar -xzf -
|
||||||
else
|
else
|
||||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||||
@@ -110,7 +124,7 @@ try_curl() {
|
|||||||
|
|
||||||
try_wget() {
|
try_wget() {
|
||||||
command -v wget > /dev/null &&
|
command -v wget > /dev/null &&
|
||||||
if [[ $1 =~ tgz$ ]]; then
|
if [[ $1 =~ tar.gz$ ]]; then
|
||||||
wget -O - $1 | tar -xzf -
|
wget -O - $1 | tar -xzf -
|
||||||
else
|
else
|
||||||
local temp=${TMPDIR:-/tmp}/fzf.zip
|
local temp=${TMPDIR:-/tmp}/fzf.zip
|
||||||
@@ -120,13 +134,11 @@ try_wget() {
|
|||||||
|
|
||||||
download() {
|
download() {
|
||||||
echo "Downloading bin/fzf ..."
|
echo "Downloading bin/fzf ..."
|
||||||
if [[ ! "$version" =~ alpha ]]; then
|
|
||||||
if [ -x "$fzf_base"/bin/fzf ]; then
|
if [ -x "$fzf_base"/bin/fzf ]; then
|
||||||
echo " - Already exists"
|
echo " - Already exists"
|
||||||
check_binary && return
|
check_binary && return
|
||||||
fi
|
fi
|
||||||
link_fzf_in_path && return
|
link_fzf_in_path && return
|
||||||
fi
|
|
||||||
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
|
mkdir -p "$fzf_base"/bin && cd "$fzf_base"/bin
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
binary_error="Failed to create bin directory"
|
binary_error="Failed to create bin directory"
|
||||||
@@ -134,9 +146,7 @@ download() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
local url
|
local url
|
||||||
[[ "$version" =~ alpha ]] &&
|
url=https://github.com/junegunn/fzf/releases/download/$version/${1}
|
||||||
url=https://github.com/junegunn/fzf-bin/releases/download/alpha/${1} ||
|
|
||||||
url=https://github.com/junegunn/fzf-bin/releases/download/$version/${1}
|
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
if ! (try_curl $url || try_wget $url); then
|
if ! (try_curl $url || try_wget $url); then
|
||||||
set +o pipefail
|
set +o pipefail
|
||||||
@@ -158,19 +168,23 @@ archi=$(uname -sm)
|
|||||||
binary_available=1
|
binary_available=1
|
||||||
binary_error=""
|
binary_error=""
|
||||||
case "$archi" in
|
case "$archi" in
|
||||||
Darwin\ *64) download fzf-$version-darwin_${binary_arch:-amd64}.tgz ;;
|
Darwin\ arm64) download fzf-$version-darwin_arm64.zip ;;
|
||||||
Darwin\ *86) download fzf-$version-darwin_${binary_arch:-386}.tgz ;;
|
Darwin\ x86_64) download fzf-$version-darwin_amd64.zip ;;
|
||||||
Linux\ *64) download fzf-$version-linux_${binary_arch:-amd64}.tgz ;;
|
Linux\ armv5*) download fzf-$version-linux_armv5.tar.gz ;;
|
||||||
Linux\ *86) download fzf-$version-linux_${binary_arch:-386}.tgz ;;
|
Linux\ armv6*) download fzf-$version-linux_armv6.tar.gz ;;
|
||||||
Linux\ armv5*) download fzf-$version-linux_${binary_arch:-arm5}.tgz ;;
|
Linux\ armv7*) download fzf-$version-linux_armv7.tar.gz ;;
|
||||||
Linux\ armv6*) download fzf-$version-linux_${binary_arch:-arm6}.tgz ;;
|
Linux\ armv8*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||||
Linux\ armv7*) download fzf-$version-linux_${binary_arch:-arm7}.tgz ;;
|
Linux\ aarch64*) download fzf-$version-linux_arm64.tar.gz ;;
|
||||||
Linux\ armv8*) download fzf-$version-linux_${binary_arch:-arm8}.tgz ;;
|
Linux\ loongarch64) download fzf-$version-linux_loong64.tar.gz ;;
|
||||||
FreeBSD\ *64) download fzf-$version-freebsd_${binary_arch:-amd64}.tgz ;;
|
Linux\ ppc64le) download fzf-$version-linux_ppc64le.tar.gz ;;
|
||||||
FreeBSD\ *86) download fzf-$version-freebsd_${binary_arch:-386}.tgz ;;
|
Linux\ *64) download fzf-$version-linux_amd64.tar.gz ;;
|
||||||
OpenBSD\ *64) download fzf-$version-openbsd_${binary_arch:-amd64}.tgz ;;
|
Linux\ s390x) download fzf-$version-linux_s390x.tar.gz ;;
|
||||||
OpenBSD\ *86) download fzf-$version-openbsd_${binary_arch:-386}.tgz ;;
|
FreeBSD\ *64) download fzf-$version-freebsd_amd64.tar.gz ;;
|
||||||
CYGWIN*\ *64) download fzf-$version-windows_${binary_arch:-amd64}.zip ;;
|
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 ;;
|
*) binary_available=0 binary_error=1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -182,12 +196,12 @@ if [ -n "$binary_error" ]; then
|
|||||||
echo " - $binary_error !!!"
|
echo " - $binary_error !!!"
|
||||||
fi
|
fi
|
||||||
if command -v go > /dev/null; then
|
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
|
if [ -z "${GOPATH-}" ]; then
|
||||||
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
export GOPATH="${TMPDIR:-/tmp}/fzf-gopath"
|
||||||
mkdir -p "$GOPATH"
|
mkdir -p "$GOPATH"
|
||||||
fi
|
fi
|
||||||
if go get -u 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"
|
echo "OK"
|
||||||
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
cp "$GOPATH/bin/fzf" "$fzf_base/bin/"
|
||||||
else
|
else
|
||||||
@@ -202,6 +216,17 @@ fi
|
|||||||
|
|
||||||
[[ "$*" =~ "--bin" ]] && exit 0
|
[[ "$*" =~ "--bin" ]] && exit 0
|
||||||
|
|
||||||
|
for s in $shells; do
|
||||||
|
if ! command -v "$s" > /dev/null; then
|
||||||
|
shells=${shells/$s/}
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ ${#shells} -lt 3 ]]; then
|
||||||
|
echo "No shell configuration to be updated."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Auto-completion
|
# Auto-completion
|
||||||
if [ -z "$auto_completion" ]; then
|
if [ -z "$auto_completion" ]; then
|
||||||
ask "Do you want to enable fuzzy auto-completion?"
|
ask "Do you want to enable fuzzy auto-completion?"
|
||||||
@@ -215,13 +240,12 @@ if [ -z "$key_bindings" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
has_zsh=$(command -v zsh > /dev/null && echo 1 || echo 0)
|
|
||||||
shells=$([ $has_zsh -eq 1 ] && echo "bash zsh" || echo "bash")
|
|
||||||
for shell in $shells; do
|
for shell in $shells; do
|
||||||
echo -n "Generate ~/.fzf.$shell ... "
|
[[ "$shell" = fish ]] && continue
|
||||||
src=~/.fzf.${shell}
|
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
|
if [ $auto_completion -eq 0 ]; then
|
||||||
fzf_completion="# $fzf_completion"
|
fzf_completion="# $fzf_completion"
|
||||||
fi
|
fi
|
||||||
@@ -231,11 +255,11 @@ for shell in $shells; do
|
|||||||
fzf_key_bindings="# $fzf_key_bindings"
|
fzf_key_bindings="# $fzf_key_bindings"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > $src << EOF
|
cat > "$src" << EOF
|
||||||
# Setup fzf
|
# Setup fzf
|
||||||
# ---------
|
# ---------
|
||||||
if [[ ! "\$PATH" == *$fzf_base/bin* ]]; then
|
if [[ ! "\$PATH" == *$fzf_base_esc/bin* ]]; then
|
||||||
export PATH="\$PATH:$fzf_base/bin"
|
PATH="\${PATH:+\${PATH}:}$fzf_base/bin"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Auto-completion
|
# Auto-completion
|
||||||
@@ -245,28 +269,21 @@ $fzf_completion
|
|||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
$fzf_key_bindings
|
$fzf_key_bindings
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
echo "OK"
|
echo "OK"
|
||||||
done
|
done
|
||||||
|
|
||||||
# fish
|
# fish
|
||||||
has_fish=$(command -v fish > /dev/null && echo 1 || echo 0)
|
if [[ "$shells" =~ fish ]]; then
|
||||||
if [ $has_fish -eq 1 ]; then
|
|
||||||
echo -n "Update fish_user_paths ... "
|
echo -n "Update fish_user_paths ... "
|
||||||
fish << EOF
|
fish << EOF
|
||||||
echo \$fish_user_paths | grep $fzf_base/bin > /dev/null
|
echo \$fish_user_paths | \grep "$fzf_base"/bin > /dev/null
|
||||||
or set --universal fish_user_paths \$fish_user_paths $fzf_base/bin
|
or set --universal fish_user_paths \$fish_user_paths "$fzf_base"/bin
|
||||||
EOF
|
EOF
|
||||||
[ $? -eq 0 ] && echo "OK" || echo "Failed"
|
[ $? -eq 0 ] && echo "OK" || echo "Failed"
|
||||||
|
|
||||||
mkdir -p ~/.config/fish/functions
|
mkdir -p "${fish_dir}/functions"
|
||||||
if [ -e ~/.config/fish/functions/fzf.fish ]; then
|
fish_binding="${fish_dir}/functions/fzf_key_bindings.fish"
|
||||||
echo -n "Remove unnecessary ~/.config/fish/functions/fzf.fish ... "
|
|
||||||
rm -f ~/.config/fish/functions/fzf.fish && echo "OK" || echo "Failed"
|
|
||||||
fi
|
|
||||||
|
|
||||||
fish_binding=~/.config/fish/functions/fzf_key_bindings.fish
|
|
||||||
if [ $key_bindings -ne 0 ]; then
|
if [ $key_bindings -ne 0 ]; then
|
||||||
echo -n "Symlink $fish_binding ... "
|
echo -n "Symlink $fish_binding ... "
|
||||||
ln -sf "$fzf_base/shell/key-bindings.fish" \
|
ln -sf "$fzf_base/shell/key-bindings.fish" \
|
||||||
@@ -286,20 +303,22 @@ append_line() {
|
|||||||
line="$2"
|
line="$2"
|
||||||
file="$3"
|
file="$3"
|
||||||
pat="${4:-}"
|
pat="${4:-}"
|
||||||
|
lno=""
|
||||||
|
|
||||||
echo "Update $file:"
|
echo "Update $file:"
|
||||||
echo " - $line"
|
echo " - $line"
|
||||||
[ -f "$file" ] || touch "$file"
|
if [ -f "$file" ]; then
|
||||||
if [ $# -lt 4 ]; then
|
if [ $# -lt 4 ]; then
|
||||||
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
lno=$(\grep -nF "$line" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||||
else
|
else
|
||||||
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
lno=$(\grep -nF "$pat" "$file" | sed 's/:.*//' | tr '\n' ' ')
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
if [ -n "$lno" ]; then
|
if [ -n "$lno" ]; then
|
||||||
echo " - Already exists: line #$lno"
|
echo " - Already exists: line #$lno"
|
||||||
else
|
else
|
||||||
if [ $update -eq 1 ]; then
|
if [ $update -eq 1 ]; then
|
||||||
echo >> "$file"
|
[ -f "$file" ] && echo >> "$file"
|
||||||
echo "$line" >> "$file"
|
echo "$line" >> "$file"
|
||||||
echo " + Added"
|
echo " + Added"
|
||||||
else
|
else
|
||||||
@@ -328,12 +347,13 @@ if [ $update_config -eq 2 ]; then
|
|||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
for shell in $shells; do
|
for shell in $shells; do
|
||||||
|
[[ "$shell" = fish ]] && continue
|
||||||
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
[ $shell = zsh ] && dest=${ZDOTDIR:-~}/.zshrc || dest=~/.bashrc
|
||||||
append_line $update_config "[ -f ~/.fzf.${shell} ] && source ~/.fzf.${shell}" "$dest" "~/.fzf.${shell}"
|
append_line $update_config "[ -f ${prefix}.${shell} ] && source ${prefix}.${shell}" "$dest" "${prefix}.${shell}"
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ $key_bindings -eq 1 ] && [ $has_fish -eq 1 ]; then
|
if [ $key_bindings -eq 1 ] && [[ "$shells" =~ fish ]]; then
|
||||||
bind_file=~/.config/fish/functions/fish_user_key_bindings.fish
|
bind_file="${fish_dir}/functions/fish_user_key_bindings.fish"
|
||||||
if [ ! -e "$bind_file" ]; then
|
if [ ! -e "$bind_file" ]; then
|
||||||
create_file "$bind_file" \
|
create_file "$bind_file" \
|
||||||
'function fish_user_key_bindings' \
|
'function fish_user_key_bindings' \
|
||||||
@@ -346,9 +366,13 @@ fi
|
|||||||
|
|
||||||
if [ $update_config -eq 1 ]; then
|
if [ $update_config -eq 1 ]; then
|
||||||
echo 'Finished. Restart your shell or reload config file.'
|
echo 'Finished. Restart your shell or reload config file.'
|
||||||
echo ' source ~/.bashrc # bash'
|
if [[ "$shells" =~ bash ]]; then
|
||||||
[ $has_zsh -eq 1 ] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
echo -n ' source ~/.bashrc # bash'
|
||||||
[ $has_fish -eq 1 ] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
[[ "$archi" =~ Darwin ]] && echo -n ' (.bashrc should be loaded from .bash_profile)'
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
[[ "$shells" =~ zsh ]] && echo " source ${ZDOTDIR:-~}/.zshrc # zsh"
|
||||||
|
[[ "$shells" =~ fish ]] && [ $key_bindings -eq 1 ] && echo ' fzf_key_bindings # fish'
|
||||||
echo
|
echo
|
||||||
echo 'Use uninstall script to remove fzf.'
|
echo 'Use uninstall script to remove fzf.'
|
||||||
echo
|
echo
|
||||||
|
|||||||
65
install.ps1
Normal file
65
install.ps1
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
$version="0.44.0"
|
||||||
|
|
||||||
|
$fzf_base=Split-Path -Parent $MyInvocation.MyCommand.Definition
|
||||||
|
|
||||||
|
function check_binary () {
|
||||||
|
Write-Host " - Checking fzf executable ... " -NoNewline
|
||||||
|
$output=cmd /c $fzf_base\bin\fzf.exe --version 2>&1
|
||||||
|
if (-not $?) {
|
||||||
|
Write-Host "Error: $output"
|
||||||
|
$binary_error="Invalid binary"
|
||||||
|
} else {
|
||||||
|
$output=(-Split $output)[0]
|
||||||
|
if ($version -ne $output) {
|
||||||
|
Write-Host "$output != $version"
|
||||||
|
$binary_error="Invalid version"
|
||||||
|
} else {
|
||||||
|
Write-Host "$output"
|
||||||
|
$binary_error=""
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Remove-Item "$fzf_base\bin\fzf.exe"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function download {
|
||||||
|
param($file)
|
||||||
|
Write-Host "Downloading bin/fzf ..."
|
||||||
|
if (Test-Path "$fzf_base\bin\fzf.exe") {
|
||||||
|
Write-Host " - Already exists"
|
||||||
|
if (check_binary) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (-not (Test-Path "$fzf_base\bin")) {
|
||||||
|
md "$fzf_base\bin"
|
||||||
|
}
|
||||||
|
if (-not $?) {
|
||||||
|
$binary_error="Failed to create bin directory"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cd "$fzf_base\bin"
|
||||||
|
$url="https://github.com/junegunn/fzf/releases/download/$version/$file"
|
||||||
|
$temp=$env:TMP + "\fzf.zip"
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
if ($PSVersionTable.PSVersion.Major -ge 3) {
|
||||||
|
Invoke-WebRequest -Uri $url -OutFile $temp
|
||||||
|
} else {
|
||||||
|
(New-Object Net.WebClient).DownloadFile($url, $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath("$temp"))
|
||||||
|
}
|
||||||
|
if ($?) {
|
||||||
|
(Microsoft.PowerShell.Archive\Expand-Archive -Path $temp -DestinationPath .); (Remove-Item $temp)
|
||||||
|
} else {
|
||||||
|
$binary_error="Failed to download with powershell"
|
||||||
|
}
|
||||||
|
if (-not (Test-Path fzf.exe)) {
|
||||||
|
$binary_error="Failed to download $file"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
echo y | icacls $fzf_base\bin\fzf.exe /grant Administrator:F ; check_binary >$null
|
||||||
|
}
|
||||||
|
|
||||||
|
download "fzf-$version-windows_amd64.zip"
|
||||||
|
|
||||||
|
Write-Host 'For more information, see: https://github.com/junegunn/fzf'
|
||||||
11
main.go
11
main.go
@@ -1,9 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "github.com/junegunn/fzf/src"
|
import (
|
||||||
|
fzf "github.com/junegunn/fzf/src"
|
||||||
|
"github.com/junegunn/fzf/src/protector"
|
||||||
|
)
|
||||||
|
|
||||||
var revision string
|
var version string = "0.44"
|
||||||
|
var revision string = "devel"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fzf.Run(fzf.ParseOptions(), revision)
|
protector.Protect()
|
||||||
|
fzf.Run(fzf.ParseOptions(), version, revision)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.ig
|
.ig
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017 Junegunn Choi
|
Copyright (c) 2013-2023 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -21,25 +21,39 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
..
|
..
|
||||||
.TH fzf-tmux 1 "Jun 2017" "fzf 0.16.8" "fzf-tmux - open fzf in tmux split pane"
|
.TH fzf-tmux 1 "Nov 2023" "fzf 0.44.0" "fzf-tmux - open fzf in tmux split pane"
|
||||||
|
|
||||||
.SH NAME
|
.SH NAME
|
||||||
fzf-tmux - open fzf in tmux split pane
|
fzf-tmux - open fzf in tmux split pane
|
||||||
|
|
||||||
.SH SYNOPSIS
|
.SH SYNOPSIS
|
||||||
.B fzf-tmux [-u|-d [HEIGHT[%]]] [-l|-r [WIDTH[%]]] [--] [FZF OPTIONS]
|
.B fzf-tmux [LAYOUT OPTIONS] [--] [FZF OPTIONS]
|
||||||
|
|
||||||
.SH DESCRIPTION
|
.SH DESCRIPTION
|
||||||
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane. It is
|
fzf-tmux is a wrapper script for fzf that opens fzf in a tmux split pane or in
|
||||||
designed to work just like fzf except that it does not take up the whole
|
a tmux popup window. It is designed to work just like fzf except that it does
|
||||||
screen. You can safely use fzf-tmux instead of fzf in your scripts as the extra
|
not take up the whole screen. You can safely use fzf-tmux instead of fzf in
|
||||||
options will be silently ignored if you're not on tmux.
|
your scripts as the extra options will be silently ignored if you're not on
|
||||||
|
tmux.
|
||||||
|
|
||||||
.SH OPTIONS
|
.SH LAYOUT OPTIONS
|
||||||
.SS Layout
|
|
||||||
|
|
||||||
(default: \fB-d 50%\fR)
|
(default layout: \fB-d 50%\fR)
|
||||||
|
|
||||||
|
.SS Popup window
|
||||||
|
(requires tmux 3.2 or above)
|
||||||
|
.TP
|
||||||
|
.B "-p [WIDTH[%][,HEIGHT[%]]]"
|
||||||
|
.TP
|
||||||
|
.B "-w WIDTH[%]"
|
||||||
|
.TP
|
||||||
|
.B "-h WIDTH[%]"
|
||||||
|
.TP
|
||||||
|
.B "-x COL"
|
||||||
|
.TP
|
||||||
|
.B "-y ROW"
|
||||||
|
|
||||||
|
.SS Split pane
|
||||||
.TP
|
.TP
|
||||||
.B "-u [height[%]]"
|
.B "-u [height[%]]"
|
||||||
Split above (up)
|
Split above (up)
|
||||||
|
|||||||
991
man/man1/fzf.1
991
man/man1/fzf.1
File diff suppressed because it is too large
Load Diff
643
plugin/fzf.vim
643
plugin/fzf.vim
File diff suppressed because it is too large
Load Diff
@@ -1,46 +1,66 @@
|
|||||||
#!/bin/bash
|
|
||||||
# ____ ____
|
# ____ ____
|
||||||
# / __/___ / __/
|
# / __/___ / __/
|
||||||
# / /_/_ / / /_
|
# / /_/_ / / /_
|
||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/-completion.bash
|
# /_/ /___/_/ completion.bash
|
||||||
#
|
#
|
||||||
# - $FZF_TMUX (default: 0)
|
# - $FZF_TMUX (default: 0)
|
||||||
# - $FZF_TMUX_HEIGHT (default: '40%')
|
# - $FZF_TMUX_OPTS (default: empty)
|
||||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||||
|
|
||||||
|
[[ $- =~ i ]] || return 0
|
||||||
|
|
||||||
|
|
||||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
if ! declare -F _fzf_compgen_path > /dev/null; then
|
||||||
_fzf_compgen_path() {
|
_fzf_compgen_path() {
|
||||||
echo "$1"
|
echo "$1"
|
||||||
command find -L "$1" \
|
command find -L "$1" \
|
||||||
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
-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@^\./@@'
|
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
if ! declare -F _fzf_compgen_dir > /dev/null; then
|
||||||
_fzf_compgen_dir() {
|
_fzf_compgen_dir() {
|
||||||
command find -L "$1" \
|
command find -L "$1" \
|
||||||
-name .git -prune -o -name .svn -prune -o -type d \
|
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
-a -not -path "$1" -print 2> /dev/null | command sed 's@^\./@@'
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
# To redraw line after fzf closes (printf '\e[5n')
|
# To redraw line after fzf closes (printf '\e[5n')
|
||||||
bind '"\e[0n": redraw-current-line'
|
bind '"\e[0n": redraw-current-line' 2> /dev/null
|
||||||
|
|
||||||
__fzfcmd_complete() {
|
__fzf_comprun() {
|
||||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
|
if [[ "$(type -t _fzf_comprun 2>&1)" = function ]]; then
|
||||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
_fzf_comprun "$@"
|
||||||
|
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
|
||||||
|
shift
|
||||||
|
fzf "$@"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_orig_completion_filter() {
|
__fzf_orig_completion() {
|
||||||
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _fzf_orig_completion_\3="\1 %s \3 #\2";/' |
|
local l comp f cmd
|
||||||
awk -F= '{gsub(/[^A-Za-z0-9_= ;]/, "_", $1); print $1"="$2}'
|
while read -r l; do
|
||||||
|
if [[ "$l" =~ ^(.*\ -F)\ *([^ ]*).*\ ([^ ]*)$ ]]; then
|
||||||
|
comp="${BASH_REMATCH[1]}"
|
||||||
|
f="${BASH_REMATCH[2]}"
|
||||||
|
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 "
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_opts_completion() {
|
_fzf_opts_completion() {
|
||||||
@@ -49,57 +69,185 @@ _fzf_opts_completion() {
|
|||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||||
opts="
|
opts="
|
||||||
|
-h --help
|
||||||
-x --extended
|
-x --extended
|
||||||
-e --exact
|
-e --exact
|
||||||
|
--extended-exact
|
||||||
|
+x --no-extended
|
||||||
|
+e --no-exact
|
||||||
|
-q --query
|
||||||
|
-f --filter
|
||||||
|
--literal
|
||||||
|
--no-literal
|
||||||
--algo
|
--algo
|
||||||
-i +i
|
--scheme
|
||||||
|
--expect
|
||||||
|
--no-expect
|
||||||
|
--enabled --no-phony
|
||||||
|
--disabled --phony
|
||||||
|
--tiebreak
|
||||||
|
--bind
|
||||||
|
--color
|
||||||
|
--toggle-sort
|
||||||
|
-d --delimiter
|
||||||
-n --nth
|
-n --nth
|
||||||
--with-nth
|
--with-nth
|
||||||
-d --delimiter
|
-s --sort
|
||||||
+s --no-sort
|
+s --no-sort
|
||||||
|
--track
|
||||||
|
--no-track
|
||||||
--tac
|
--tac
|
||||||
--tiebreak
|
--no-tac
|
||||||
|
-i
|
||||||
|
+i
|
||||||
-m --multi
|
-m --multi
|
||||||
--no-mouse
|
+m --no-multi
|
||||||
--bind
|
|
||||||
--cycle
|
|
||||||
--no-hscroll
|
|
||||||
--jump-labels
|
|
||||||
--height
|
|
||||||
--literal
|
|
||||||
--reverse
|
|
||||||
--margin
|
|
||||||
--inline-info
|
|
||||||
--prompt
|
|
||||||
--header
|
|
||||||
--header-lines
|
|
||||||
--ansi
|
--ansi
|
||||||
--tabstop
|
--no-ansi
|
||||||
--color
|
--no-mouse
|
||||||
|
+c --no-color
|
||||||
|
+2 --no-256
|
||||||
|
--black
|
||||||
|
--no-black
|
||||||
|
--bold
|
||||||
--no-bold
|
--no-bold
|
||||||
|
--layout
|
||||||
|
--reverse
|
||||||
|
--no-reverse
|
||||||
|
--cycle
|
||||||
|
--no-cycle
|
||||||
|
--keep-right
|
||||||
|
--no-keep-right
|
||||||
|
--hscroll
|
||||||
|
--no-hscroll
|
||||||
|
--hscroll-off
|
||||||
|
--scroll-off
|
||||||
|
--filepath-word
|
||||||
|
--no-filepath-word
|
||||||
|
--info
|
||||||
|
--no-info
|
||||||
|
--inline-info
|
||||||
|
--no-inline-info
|
||||||
|
--separator
|
||||||
|
--no-separator
|
||||||
|
--scrollbar
|
||||||
|
--no-scrollbar
|
||||||
|
--jump-labels
|
||||||
|
-1 --select-1
|
||||||
|
+1 --no-select-1
|
||||||
|
-0 --exit-0
|
||||||
|
+0 --no-exit-0
|
||||||
|
--read0
|
||||||
|
--no-read0
|
||||||
|
--print0
|
||||||
|
--no-print0
|
||||||
|
--print-query
|
||||||
|
--no-print-query
|
||||||
|
--prompt
|
||||||
|
--pointer
|
||||||
|
--marker
|
||||||
|
--sync
|
||||||
|
--no-sync
|
||||||
|
--async
|
||||||
|
--no-history
|
||||||
--history
|
--history
|
||||||
--history-size
|
--history-size
|
||||||
|
--no-header
|
||||||
|
--no-header-lines
|
||||||
|
--header
|
||||||
|
--header-lines
|
||||||
|
--header-first
|
||||||
|
--no-header-first
|
||||||
|
--ellipsis
|
||||||
--preview
|
--preview
|
||||||
|
--no-preview
|
||||||
--preview-window
|
--preview-window
|
||||||
-q --query
|
--height
|
||||||
-1 --select-1
|
--min-height
|
||||||
-0 --exit-0
|
--no-height
|
||||||
-f --filter
|
--no-margin
|
||||||
--print-query
|
--no-padding
|
||||||
--expect
|
--no-border
|
||||||
--sync"
|
--border
|
||||||
|
--no-border-label
|
||||||
|
--border-label
|
||||||
|
--border-label-pos
|
||||||
|
--no-preview-label
|
||||||
|
--preview-label
|
||||||
|
--preview-label-pos
|
||||||
|
--no-unicode
|
||||||
|
--unicode
|
||||||
|
--margin
|
||||||
|
--padding
|
||||||
|
--tabstop
|
||||||
|
--listen
|
||||||
|
--no-listen
|
||||||
|
--clear
|
||||||
|
--no-clear
|
||||||
|
--version
|
||||||
|
--"
|
||||||
|
|
||||||
case "${prev}" in
|
case "${prev}" in
|
||||||
|
--algo)
|
||||||
|
COMPREPLY=( $(compgen -W "v1 v2" -- "$cur") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
--scheme)
|
||||||
|
COMPREPLY=( $(compgen -W "default path history" -- "$cur") )
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
--tiebreak)
|
--tiebreak)
|
||||||
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
|
COMPREPLY=( $(compgen -W "length chunk begin end index" -- "$cur") )
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--color)
|
--color)
|
||||||
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
|
COMPREPLY=( $(compgen -W "dark light 16 bw no" -- "$cur") )
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
--history)
|
--layout)
|
||||||
COMPREPLY=()
|
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
|
return 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -113,46 +261,60 @@ _fzf_opts_completion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_handle_dynamic_completion() {
|
_fzf_handle_dynamic_completion() {
|
||||||
local cmd orig_var orig ret orig_cmd
|
local cmd orig_var orig ret orig_cmd orig_complete
|
||||||
cmd="$1"
|
cmd="$1"
|
||||||
shift
|
shift
|
||||||
orig_cmd="$1"
|
orig_cmd="$1"
|
||||||
orig_var="_fzf_orig_completion_$cmd"
|
orig_var="_fzf_orig_completion_$cmd"
|
||||||
orig="${!orig_var##*#}"
|
orig="${!orig_var-}"
|
||||||
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
|
orig="${orig##*#}"
|
||||||
|
if [[ -n "$orig" ]] && type "$orig" > /dev/null 2>&1; then
|
||||||
$orig "$@"
|
$orig "$@"
|
||||||
elif [ -n "$_fzf_completion_loader" ]; then
|
elif [[ -n "${_fzf_completion_loader-}" ]]; then
|
||||||
|
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||||
_completion_loader "$@"
|
_completion_loader "$@"
|
||||||
ret=$?
|
ret=$?
|
||||||
eval "$(complete | command grep "\-F.* $orig_cmd$" | _fzf_orig_completion_filter)"
|
# _completion_loader may not have updated completion for the command
|
||||||
source "${BASH_SOURCE[0]}"
|
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
|
||||||
|
eval "${orig_complete/ -F / -o nospace -F }"
|
||||||
|
else
|
||||||
|
eval "$orig_complete"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
return $ret
|
return $ret
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_generic_path_completion() {
|
__fzf_generic_path_completion() {
|
||||||
local cur base dir leftover matches trigger cmd fzf
|
local cur base dir leftover matches trigger cmd
|
||||||
fzf="$(__fzfcmd_complete)"
|
cmd="${COMP_WORDS[0]}"
|
||||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
if [[ $cmd == \\* ]]; then
|
||||||
|
cmd="${cmd:1}"
|
||||||
|
fi
|
||||||
|
cmd="${cmd//[^A-Za-z0-9_=]/_}"
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
if [[ "$cur" == *"$trigger" ]]; then
|
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||||
base=${cur:0:${#cur}-${#trigger}}
|
base=${cur:0:${#cur}-${#trigger}}
|
||||||
eval "base=$base"
|
eval "base=$base" 2> /dev/null || return
|
||||||
|
|
||||||
dir="$base"
|
dir=
|
||||||
|
[[ $base = *"/"* ]] && dir="$base"
|
||||||
while true; do
|
while true; do
|
||||||
if [ -z "$dir" ] || [ -d "$dir" ]; then
|
if [[ -z "$dir" ]] || [[ -d "$dir" ]]; then
|
||||||
leftover=${base/#"$dir"}
|
leftover=${base/#"$dir"}
|
||||||
leftover=${leftover/#\/}
|
leftover=${leftover/#\/}
|
||||||
[ -z "$dir" ] && dir='.'
|
[[ -z "$dir" ]] && dir='.'
|
||||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
[[ "$dir" != "/" ]] && dir="${dir/%\//}"
|
||||||
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $2 -q "$leftover" | while read -r item; do
|
matches=$(eval "$1 $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --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"
|
printf "%q " "${item%$3}$3"
|
||||||
done)
|
done)
|
||||||
matches=${matches% }
|
matches=${matches% }
|
||||||
if [ -n "$matches" ]; then
|
[[ -z "$3" ]] && [[ "${__fzf_nospace_commands-}" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||||
|
if [[ -n "$matches" ]]; then
|
||||||
COMPREPLY=( "$matches" )
|
COMPREPLY=( "$matches" )
|
||||||
else
|
else
|
||||||
COMPREPLY=( "$cur" )
|
COMPREPLY=( "$cur" )
|
||||||
@@ -160,7 +322,7 @@ __fzf_generic_path_completion() {
|
|||||||
printf '\e[5n'
|
printf '\e[5n'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
dir=$(dirname "$dir")
|
dir=$(command dirname "$dir")
|
||||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
@@ -172,28 +334,48 @@ __fzf_generic_path_completion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete() {
|
_fzf_complete() {
|
||||||
local cur selected trigger cmd fzf post
|
# Split arguments around --
|
||||||
post="$(caller 0 | awk '{print $2}')_post"
|
local args rest str_arg i sep
|
||||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
args=("$@")
|
||||||
fzf="$(__fzfcmd_complete)"
|
sep=
|
||||||
|
for i in "${!args[@]}"; do
|
||||||
|
if [[ "${args[$i]}" = -- ]]; then
|
||||||
|
sep=$i
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$sep" ]]; then
|
||||||
|
str_arg=
|
||||||
|
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||||
|
args=("${args[@]:0:$sep}")
|
||||||
|
else
|
||||||
|
str_arg=$1
|
||||||
|
args=()
|
||||||
|
shift
|
||||||
|
rest=("$@")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local cur selected trigger cmd post
|
||||||
|
post="$(caller 0 | command awk '{print $2}')_post"
|
||||||
|
type -t "$post" > /dev/null 2>&1 || post='command cat'
|
||||||
|
|
||||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||||
if [[ "$cur" == *"$trigger" ]]; then
|
if [[ "$cur" == *"$trigger" ]] && [[ $cur != *'$('* ]] && [[ $cur != *':='* ]] && [[ $cur != *'`'* ]]; then
|
||||||
cur=${cur:0:${#cur}-${#trigger}}
|
cur=${cur:0:${#cur}-${#trigger}}
|
||||||
|
|
||||||
selected=$(cat | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" $fzf $1 -q "$cur" | $post | tr '\n' ' ')
|
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 | command tr '\n' ' ')
|
||||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||||
printf '\e[5n'
|
if [[ -n "$selected" ]]; then
|
||||||
|
|
||||||
if [ -n "$selected" ]; then
|
|
||||||
COMPREPLY=("$selected")
|
COMPREPLY=("$selected")
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
shift
|
COMPREPLY=("$cur")
|
||||||
_fzf_handle_dynamic_completion "$cmd" "$@"
|
fi
|
||||||
|
printf '\e[5n'
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
_fzf_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,85 +393,108 @@ _fzf_dir_completion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_kill() {
|
_fzf_complete_kill() {
|
||||||
[ -n "${COMP_WORDS[COMP_CWORD]}" ] && return 1
|
_fzf_proc_completion "$@"
|
||||||
|
}
|
||||||
|
|
||||||
local selected fzf
|
_fzf_proc_completion() {
|
||||||
fzf="$(__fzfcmd_complete)"
|
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||||
selected=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" $fzf -m | awk '{print $2}' | tr '\n' ' ')
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
printf '\e[5n'
|
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if [ -n "$selected" ]; then
|
_fzf_proc_completion_post() {
|
||||||
COMPREPLY=( "$selected" )
|
command awk '{print $2}'
|
||||||
return 0
|
}
|
||||||
|
|
||||||
|
# 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 cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | command awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||||
|
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | command tr ',' '\n' | command tr -d '[' | command awk '{ print $1 " " $1 }') \
|
||||||
|
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||||
|
command awk '{for (i = 2; i <= NF; i++) print $i}' | command sort -u
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
_fzf_host_completion() {
|
||||||
|
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_telnet() {
|
# Values for $1 $2 $3 are described here
|
||||||
_fzf_complete '+m' "$@" < <(
|
# https://www.gnu.org/software/bash/manual/html_node/Programmable-Completion.html
|
||||||
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
# > the first argument ($1) is the name of the command whose arguments are being completed,
|
||||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
# > 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() {
|
_fzf_complete_ssh() {
|
||||||
_fzf_complete '+m' "$@" < <(
|
case $3 in
|
||||||
cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
|
-i|-F|-E)
|
||||||
<(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
_fzf_path_completion "$@"
|
||||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
;;
|
||||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
*)
|
||||||
|
local user=
|
||||||
|
[[ "$2" =~ '@' ]] && user="${2%%@*}@"
|
||||||
|
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | command awk -v user="$user" '{print user $0}')
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_var_completion() {
|
||||||
|
_fzf_complete -m -- "$@" < <(
|
||||||
|
declare -xp | command sed -En 's|^declare [^ ]+ ([^=]+).*|\1|p'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_unset() {
|
_fzf_alias_completion() {
|
||||||
_fzf_complete '-m' "$@" < <(
|
_fzf_complete -m -- "$@" < <(
|
||||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
alias | command sed -En 's|^alias ([^=]+).*|\1|p'
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_fzf_complete_export() {
|
|
||||||
_fzf_complete '-m' "$@" < <(
|
|
||||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
_fzf_complete_unalias() {
|
|
||||||
_fzf_complete '-m' "$@" < <(
|
|
||||||
alias | sed 's/=.*//' | sed 's/.* //'
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
# fzf options
|
# fzf options
|
||||||
complete -o default -F _fzf_opts_completion fzf
|
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}"
|
d_cmds="${FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||||
a_cmds="
|
a_cmds="
|
||||||
awk cat diff diff3
|
awk bat cat diff diff3
|
||||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
emacs emacsclient ex file ftp g++ gcc gvim head hg hx java
|
||||||
javac ld less more mvim nvim patch perl python ruby
|
javac ld less more mvim nvim patch perl python ruby
|
||||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||||
find git grep gunzip gzip hg jar
|
find git grep gunzip gzip hg jar
|
||||||
ln ls mv open rm rsync scp
|
ln ls mv open rm rsync scp
|
||||||
svn tar unzip zip"
|
svn tar unzip zip"
|
||||||
x_cmds="kill ssh telnet unset unalias export"
|
|
||||||
|
|
||||||
# Preserve existing completion
|
# Preserve existing completion
|
||||||
eval $(complete |
|
__fzf_orig_completion < <(complete -p $d_cmds $a_cmds ssh 2> /dev/null)
|
||||||
sed -E '/-F/!d; / _fzf/d; '"/ ($(echo $d_cmds $a_cmds $x_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
|
|
||||||
_fzf_orig_completion_filter)
|
|
||||||
|
|
||||||
if type _completion_loader > /dev/null 2>&1; then
|
if type _completion_loader > /dev/null 2>&1; then
|
||||||
_fzf_completion_loader=1
|
_fzf_completion_loader=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_fzf_defc() {
|
__fzf_defc() {
|
||||||
local cmd func opts orig_var orig def
|
local cmd func opts orig_var orig def
|
||||||
cmd="$1"
|
cmd="$1"
|
||||||
func="$2"
|
func="$2"
|
||||||
opts="$3"
|
opts="$3"
|
||||||
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
orig_var="_fzf_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||||
orig="${!orig_var}"
|
orig="${!orig_var-}"
|
||||||
if [ -n "$orig" ]; then
|
if [[ -n "$orig" ]]; then
|
||||||
printf -v def "$orig" "$func"
|
printf -v def "$orig" "$func"
|
||||||
eval "$def"
|
eval "$def"
|
||||||
else
|
else
|
||||||
@@ -299,26 +504,41 @@ _fzf_defc() {
|
|||||||
|
|
||||||
# Anything
|
# Anything
|
||||||
for cmd in $a_cmds; do
|
for cmd in $a_cmds; do
|
||||||
_fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
|
__fzf_defc "$cmd" _fzf_path_completion "-o default -o bashdefault"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Directory
|
# Directory
|
||||||
for cmd in $d_cmds; do
|
for cmd in $d_cmds; do
|
||||||
_fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
__fzf_defc "$cmd" _fzf_dir_completion "-o nospace -o dirnames"
|
||||||
done
|
done
|
||||||
|
|
||||||
unset _fzf_defc
|
# ssh
|
||||||
|
__fzf_defc ssh _fzf_complete_ssh "-o default -o bashdefault"
|
||||||
|
|
||||||
# Kill completion
|
unset cmd d_cmds a_cmds
|
||||||
complete -F _fzf_complete_kill -o nospace -o default -o bashdefault kill
|
|
||||||
|
|
||||||
# Host completion
|
_fzf_setup_completion() {
|
||||||
complete -F _fzf_complete_ssh -o default -o bashdefault ssh
|
local kind fn cmd
|
||||||
complete -F _fzf_complete_telnet -o default -o bashdefault telnet
|
kind=$1
|
||||||
|
fn=_fzf_${1}_completion
|
||||||
|
if [[ $# -lt 2 ]] || ! type -t "$fn" > /dev/null; then
|
||||||
|
echo "usage: ${FUNCNAME[0]} path|dir|var|alias|host|proc COMMANDS..."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
__fzf_orig_completion < <(complete -p "$@" 2> /dev/null)
|
||||||
|
for cmd in "$@"; do
|
||||||
|
case "$kind" in
|
||||||
|
dir) __fzf_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||||
|
var) __fzf_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||||
|
alias) __fzf_defc "$cmd" "$fn" "-a" ;;
|
||||||
|
*) __fzf_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Environment variables / Aliases
|
# Environment variables / Aliases / Hosts / Process
|
||||||
complete -F _fzf_complete_unset -o default -o bashdefault unset
|
_fzf_setup_completion 'var' export unset printenv
|
||||||
complete -F _fzf_complete_export -o default -o bashdefault export
|
_fzf_setup_completion 'alias' unalias
|
||||||
complete -F _fzf_complete_unalias -o default -o bashdefault unalias
|
_fzf_setup_completion 'host' telnet
|
||||||
|
_fzf_setup_completion 'proc' kill
|
||||||
unset cmd d_cmds a_cmds x_cmds
|
|
||||||
|
|||||||
@@ -1,21 +1,87 @@
|
|||||||
#!/bin/zsh
|
|
||||||
# ____ ____
|
# ____ ____
|
||||||
# / __/___ / __/
|
# / __/___ / __/
|
||||||
# / /_/_ / / /_
|
# / /_/_ / / /_
|
||||||
# / __/ / /_/ __/
|
# / __/ / /_/ __/
|
||||||
# /_/ /___/_/-completion.zsh
|
# /_/ /___/_/ completion.zsh
|
||||||
#
|
#
|
||||||
# - $FZF_TMUX (default: 0)
|
# - $FZF_TMUX (default: 0)
|
||||||
# - $FZF_TMUX_HEIGHT (default: '40%')
|
# - $FZF_TMUX_OPTS (default: '-d 40%')
|
||||||
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
# - $FZF_COMPLETION_TRIGGER (default: '**')
|
||||||
# - $FZF_COMPLETION_OPTS (default: empty)
|
# - $FZF_COMPLETION_OPTS (default: empty)
|
||||||
|
|
||||||
|
[[ -o interactive ]] || return 0
|
||||||
|
|
||||||
|
|
||||||
|
# Both branches of the following `if` do the same thing -- define
|
||||||
|
# __fzf_completion_options such that `eval $__fzf_completion_options` sets
|
||||||
|
# all options to the same values they currently have. We'll do just that at
|
||||||
|
# the bottom of the file after changing options to what we prefer.
|
||||||
|
#
|
||||||
|
# IMPORTANT: Until we get to the `emulate` line, all words that *can* be quoted
|
||||||
|
# *must* be quoted in order to prevent alias expansion. In addition, code must
|
||||||
|
# be written in a way works with any set of zsh options. This is very tricky, so
|
||||||
|
# careful when you change it.
|
||||||
|
#
|
||||||
|
# Start by loading the builtin zsh/parameter module. It provides `options`
|
||||||
|
# associative array that stores current shell options.
|
||||||
|
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
|
||||||
|
# This is the fast branch and it gets taken on virtually all Zsh installations.
|
||||||
|
#
|
||||||
|
# ${(kv)options[@]} expands to array of keys (option names) and values ("on"
|
||||||
|
# or "off"). The subsequent expansion# with (j: :) flag joins all elements
|
||||||
|
# together separated by spaces. __fzf_completion_options ends up with a value
|
||||||
|
# like this: "options=(shwordsplit off aliases on ...)".
|
||||||
|
__fzf_completion_options="options=(${(j: :)${(kv)options[@]}})"
|
||||||
|
else
|
||||||
|
# This branch is much slower because it forks to get the names of all
|
||||||
|
# zsh options. It's possible to eliminate this fork but it's not worth the
|
||||||
|
# trouble because this branch gets taken only on very ancient or broken
|
||||||
|
# zsh installations.
|
||||||
|
() {
|
||||||
|
# That `()` above defines an anonymous function. This is essentially a scope
|
||||||
|
# for local parameters. We use it to avoid polluting global scope.
|
||||||
|
'local' '__fzf_opt'
|
||||||
|
__fzf_completion_options="setopt"
|
||||||
|
# `set -o` prints one line for every zsh option. Each line contains option
|
||||||
|
# name, some spaces, and then either "on" or "off". We just want option names.
|
||||||
|
# Expansion with (@f) flag splits a string into lines. The outer expansion
|
||||||
|
# removes spaces and everything that follow them on every line. __fzf_opt
|
||||||
|
# ends up iterating over option names: shwordsplit, aliases, etc.
|
||||||
|
for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
||||||
|
if [[ -o "$__fzf_opt" ]]; then
|
||||||
|
# Option $__fzf_opt is currently on, so remember to set it back on.
|
||||||
|
__fzf_completion_options+=" -o $__fzf_opt"
|
||||||
|
else
|
||||||
|
# Option $__fzf_opt is currently off, so remember to set it back off.
|
||||||
|
__fzf_completion_options+=" +o $__fzf_opt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
# The value of __fzf_completion_options here looks like this:
|
||||||
|
# "setopt +o shwordsplit -o aliases ..."
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enable the default zsh options (those marked with <Z> in `man zshoptions`)
|
||||||
|
# but without `aliases`. Aliases in functions are expanded when functions are
|
||||||
|
# defined, so if we disable aliases here, we'll be sure to have no pesky
|
||||||
|
# aliases in any of our functions. This way we won't need prefix every
|
||||||
|
# command with `command` or to quote every word to defend against global
|
||||||
|
# aliases. Note that `aliases` is not the only option that's important to
|
||||||
|
# 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.
|
||||||
|
'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.
|
||||||
|
{
|
||||||
|
|
||||||
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
# To use custom commands instead of find, override _fzf_compgen_{path,dir}
|
||||||
if ! declare -f _fzf_compgen_path > /dev/null; then
|
if ! declare -f _fzf_compgen_path > /dev/null; then
|
||||||
_fzf_compgen_path() {
|
_fzf_compgen_path() {
|
||||||
echo "$1"
|
echo "$1"
|
||||||
command find -L "$1" \
|
command find -L "$1" \
|
||||||
-name .git -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
-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@^\./@@'
|
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
@@ -23,47 +89,74 @@ fi
|
|||||||
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
if ! declare -f _fzf_compgen_dir > /dev/null; then
|
||||||
_fzf_compgen_dir() {
|
_fzf_compgen_dir() {
|
||||||
command find -L "$1" \
|
command find -L "$1" \
|
||||||
-name .git -prune -o -name .svn -prune -o -type d \
|
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||||
}
|
}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
###########################################################
|
###########################################################
|
||||||
|
|
||||||
__fzfcmd_complete() {
|
__fzf_comprun() {
|
||||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ] &&
|
if [[ "$(type _fzf_comprun 2>&1)" =~ function ]]; then
|
||||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
_fzf_comprun "$@"
|
||||||
|
elif [ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; }; then
|
||||||
|
shift
|
||||||
|
if [ -n "${FZF_TMUX_OPTS-}" ]; then
|
||||||
|
fzf-tmux ${(Q)${(Z+n+)FZF_TMUX_OPTS}} -- "$@"
|
||||||
|
else
|
||||||
|
fzf-tmux -d ${FZF_TMUX_HEIGHT:-40%} -- "$@"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
shift
|
||||||
|
fzf "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract the name of the command. e.g. foo=1 bar baz**<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]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_generic_path_completion() {
|
__fzf_generic_path_completion() {
|
||||||
local base lbuf compgen fzf_opts suffix tail fzf dir leftover matches
|
local base lbuf cmd compgen fzf_opts suffix tail dir leftover matches
|
||||||
# (Q) flag removes a quoting level: "foo\ bar" => "foo bar"
|
base=$1
|
||||||
base=${(Q)1}
|
|
||||||
lbuf=$2
|
lbuf=$2
|
||||||
|
cmd=$(__fzf_extract_command "$lbuf")
|
||||||
compgen=$3
|
compgen=$3
|
||||||
fzf_opts=$4
|
fzf_opts=$4
|
||||||
suffix=$5
|
suffix=$5
|
||||||
tail=$6
|
tail=$6
|
||||||
fzf="$(__fzfcmd_complete)"
|
|
||||||
|
|
||||||
setopt localoptions nonomatch
|
setopt localoptions nonomatch
|
||||||
dir="$base"
|
if [[ $base = *'$('* ]] || [[ $base = *'<('* ]] || [[ $base = *'>('* ]] || [[ $base = *':='* ]] || [[ $base = *'`'* ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
eval "base=$base" 2> /dev/null || return
|
||||||
|
[[ $base = *"/"* ]] && dir="$base"
|
||||||
while [ 1 ]; do
|
while [ 1 ]; do
|
||||||
if [[ -z "$dir" || -d ${~dir} ]]; then
|
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||||
leftover=${base/#"$dir"}
|
leftover=${base/#"$dir"}
|
||||||
leftover=${leftover/#\/}
|
leftover=${leftover/#\/}
|
||||||
[ -z "$dir" ] && dir='.'
|
[ -z "$dir" ] && dir='.'
|
||||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||||
dir=${~dir}
|
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_COMPLETION_OPTS-}" __fzf_comprun "$cmd" ${(Q)${(Z+n+)fzf_opts}} -q "$leftover" | while read item; do
|
||||||
matches=$(eval "$compgen $(printf %q "$dir")" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "$leftover" | while read item; do
|
item="${item%$suffix}$suffix"
|
||||||
echo -n "${(q)item}$suffix "
|
echo -n "${(q)item} "
|
||||||
done)
|
done)
|
||||||
matches=${matches% }
|
matches=${matches% }
|
||||||
if [ -n "$matches" ]; then
|
if [ -n "$matches" ]; then
|
||||||
LBUFFER="$lbuf$matches$tail"
|
LBUFFER="$lbuf$matches$tail"
|
||||||
fi
|
fi
|
||||||
zle redisplay
|
zle reset-prompt
|
||||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
dir=$(dirname "$dir")
|
dir=$(dirname "$dir")
|
||||||
@@ -88,61 +181,107 @@ _fzf_feed_fifo() (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_fzf_complete() {
|
_fzf_complete() {
|
||||||
local fifo fzf_opts lbuf fzf matches post
|
setopt localoptions ksh_arrays
|
||||||
|
# Split arguments around --
|
||||||
|
local args rest str_arg i sep
|
||||||
|
args=("$@")
|
||||||
|
sep=
|
||||||
|
for i in {0..${#args[@]}}; do
|
||||||
|
if [[ "${args[$i]-}" = -- ]]; then
|
||||||
|
sep=$i
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [[ -n "$sep" ]]; then
|
||||||
|
str_arg=
|
||||||
|
rest=("${args[@]:$((sep + 1)):${#args[@]}}")
|
||||||
|
args=("${args[@]:0:$sep}")
|
||||||
|
else
|
||||||
|
str_arg=$1
|
||||||
|
args=()
|
||||||
|
shift
|
||||||
|
rest=("$@")
|
||||||
|
fi
|
||||||
|
|
||||||
|
local fifo lbuf cmd matches post
|
||||||
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
fifo="${TMPDIR:-/tmp}/fzf-complete-fifo-$$"
|
||||||
fzf_opts=$1
|
lbuf=${rest[0]}
|
||||||
lbuf=$2
|
cmd=$(__fzf_extract_command "$lbuf")
|
||||||
post="${funcstack[2]}_post"
|
post="${funcstack[1]}_post"
|
||||||
type $post > /dev/null 2>&1 || post=cat
|
type $post > /dev/null 2>&1 || post=cat
|
||||||
|
|
||||||
fzf="$(__fzfcmd_complete)"
|
|
||||||
|
|
||||||
_fzf_feed_fifo "$fifo"
|
_fzf_feed_fifo "$fifo"
|
||||||
matches=$(cat "$fifo" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_COMPLETION_OPTS" ${=fzf} ${=fzf_opts} -q "${(Q)prefix}" | $post | tr '\n' ' ')
|
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' ' ')
|
||||||
if [ -n "$matches" ]; then
|
if [ -n "$matches" ]; then
|
||||||
LBUFFER="$lbuf$matches"
|
LBUFFER="$lbuf$matches"
|
||||||
fi
|
fi
|
||||||
zle redisplay
|
|
||||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
|
||||||
command rm -f "$fifo"
|
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() {
|
||||||
|
setopt localoptions nonomatch
|
||||||
|
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?%]') \
|
||||||
|
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts 2> /dev/null | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||||
|
<(command grep -v '^\s*\(#\|$\)' /etc/hosts 2> /dev/null | command grep -Fv '0.0.0.0' | command sed 's/#.*//') |
|
||||||
|
awk '{for (i = 2; i <= NF; i++) print $i}' | sort -u
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
_fzf_complete_telnet() {
|
_fzf_complete_telnet() {
|
||||||
_fzf_complete '+m' "$@" < <(
|
_fzf_complete +m -- "$@" < <(__fzf_list_hosts)
|
||||||
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
|
||||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 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_ssh() {
|
||||||
_fzf_complete '+m' "$@" < <(
|
local tokens=(${(z)1})
|
||||||
command cat <(cat ~/.ssh/config /etc/ssh/ssh_config 2> /dev/null | command grep -i '^host' | command grep -v '*') \
|
case ${tokens[-1]} in
|
||||||
<(command grep -oE '^[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | awk '{ print $1 " " $1 }') \
|
-i|-F|-E)
|
||||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
_fzf_path_completion "$prefix" "$1"
|
||||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
;;
|
||||||
)
|
*)
|
||||||
|
local user=
|
||||||
|
[[ $prefix =~ @ ]] && user="${prefix%%@*}@"
|
||||||
|
_fzf_complete +m -- "$@" < <(__fzf_list_hosts | awk -v user="$user" '{print user $0}')
|
||||||
|
;;
|
||||||
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_export() {
|
_fzf_complete_export() {
|
||||||
_fzf_complete '-m' "$@" < <(
|
_fzf_complete -m -- "$@" < <(
|
||||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_unset() {
|
_fzf_complete_unset() {
|
||||||
_fzf_complete '-m' "$@" < <(
|
_fzf_complete -m -- "$@" < <(
|
||||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
_fzf_complete_unalias() {
|
_fzf_complete_unalias() {
|
||||||
_fzf_complete '+m' "$@" < <(
|
_fzf_complete +m -- "$@" < <(
|
||||||
alias | sed 's/=.*//'
|
alias | sed 's/=.*//'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_fzf_complete_kill() {
|
||||||
|
_fzf_complete -m --header-lines=1 --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||||
|
command ps -eo user,pid,ppid,start,time,command 2> /dev/null ||
|
||||||
|
command ps -eo user,pid,ppid,time,args # For BusyBox
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
_fzf_complete_kill_post() {
|
||||||
|
awk '{print $2}'
|
||||||
|
}
|
||||||
|
|
||||||
fzf-completion() {
|
fzf-completion() {
|
||||||
local tokens cmd prefix trigger tail fzf matches lbuf d_cmds
|
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
||||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||||
|
|
||||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||||
@@ -153,31 +292,34 @@ fzf-completion() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cmd=${tokens[1]}
|
cmd=$(__fzf_extract_command "$LBUFFER")
|
||||||
|
|
||||||
# Explicitly allow for empty trigger.
|
# Explicitly allow for empty trigger.
|
||||||
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
trigger=${FZF_COMPLETION_TRIGGER-'**'}
|
||||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||||
|
|
||||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
# When the trigger starts with ';', it becomes a separate token
|
||||||
# Kill completion (do not require trigger sequence)
|
if [[ ${LBUFFER} = *"${tokens[-2]-}${tokens[-1]}" ]]; then
|
||||||
if [ $cmd = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
tokens[-2]="${tokens[-2]-}${tokens[-1]}"
|
||||||
fzf="$(__fzfcmd_complete)"
|
tokens=(${tokens[0,-2]})
|
||||||
matches=$(ps -ef | sed 1d | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-50%} --min-height 15 --reverse $FZF_DEFAULT_OPTS --preview 'echo {}' --preview-window down:3:wrap $FZF_COMPLETION_OPTS" ${=fzf} -m | awk '{print $2}' | tr '\n' ' ')
|
|
||||||
if [ -n "$matches" ]; then
|
|
||||||
LBUFFER="$LBUFFER$matches"
|
|
||||||
fi
|
fi
|
||||||
zle redisplay
|
|
||||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
lbuf=$LBUFFER
|
||||||
|
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||||
|
|
||||||
# Trigger sequence given
|
# Trigger sequence given
|
||||||
elif [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||||
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
d_cmds=(${=FZF_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||||
|
|
||||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||||
[ -z "${tokens[-1]}" ] && lbuf=$LBUFFER || lbuf=${LBUFFER:0:-${#tokens[-1]}}
|
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
|
if eval "type _fzf_complete_${cmd} > /dev/null"; then
|
||||||
eval "prefix=\"$prefix\" _fzf_complete_${cmd} \"$lbuf\""
|
prefix="$prefix" eval _fzf_complete_${cmd} ${(q)lbuf}
|
||||||
|
zle reset-prompt
|
||||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||||
_fzf_dir_completion "$prefix" "$lbuf"
|
_fzf_dir_completion "$prefix" "$lbuf"
|
||||||
else
|
else
|
||||||
@@ -197,3 +339,9 @@ fzf-completion() {
|
|||||||
|
|
||||||
zle -N fzf-completion
|
zle -N fzf-completion
|
||||||
bindkey '^I' fzf-completion
|
bindkey '^I' fzf-completion
|
||||||
|
|
||||||
|
} always {
|
||||||
|
# Restore the original options.
|
||||||
|
eval $__fzf_completion_options
|
||||||
|
'unset' '__fzf_completion_options'
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,121 +1,133 @@
|
|||||||
|
# ____ ____
|
||||||
|
# / __/___ / __/
|
||||||
|
# / /_/_ / / /_
|
||||||
|
# / __/ / /_/ __/
|
||||||
|
# /_/ /___/_/ key-bindings.bash
|
||||||
|
#
|
||||||
|
# - $FZF_TMUX_OPTS
|
||||||
|
# - $FZF_CTRL_T_COMMAND
|
||||||
|
# - $FZF_CTRL_T_OPTS
|
||||||
|
# - $FZF_CTRL_R_OPTS
|
||||||
|
# - $FZF_ALT_C_COMMAND
|
||||||
|
# - $FZF_ALT_C_OPTS
|
||||||
|
|
||||||
|
[[ $- =~ i ]] || return 0
|
||||||
|
|
||||||
|
|
||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
__fzf_select__() {
|
__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 \
|
local cmd opts
|
||||||
|
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 f -print \
|
||||||
-o -type d -print \
|
-o -type d -print \
|
||||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
-o -type l -print 2> /dev/null | command cut -b3-"}"
|
||||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" fzf -m "$@" | while read -r item; do
|
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-} -m"
|
||||||
printf '%q ' "$item"
|
eval "$cmd" |
|
||||||
|
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) "$@" |
|
||||||
|
while read -r item; do
|
||||||
|
printf '%q ' "$item" # escape special chars
|
||||||
done
|
done
|
||||||
echo
|
|
||||||
}
|
|
||||||
|
|
||||||
if [[ $- =~ i ]]; then
|
|
||||||
|
|
||||||
__fzf_use_tmux__() {
|
|
||||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzfcmd() {
|
__fzfcmd() {
|
||||||
__fzf_use_tmux__ &&
|
[[ -n "${TMUX_PANE-}" ]] && { [[ "${FZF_TMUX:-0}" != 0 ]] || [[ -n "${FZF_TMUX_OPTS-}" ]]; } &&
|
||||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||||
}
|
|
||||||
|
|
||||||
__fzf_select_tmux__() {
|
|
||||||
local height
|
|
||||||
height=${FZF_TMUX_HEIGHT:-40%}
|
|
||||||
if [[ $height =~ %$ ]]; then
|
|
||||||
height="-p ${height%\%}"
|
|
||||||
else
|
|
||||||
height="-l $height"
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmux split-window $height "cd $(printf %q "$PWD"); FZF_DEFAULT_OPTS=$(printf %q "$FZF_DEFAULT_OPTS") PATH=$(printf %q "$PATH") FZF_CTRL_T_COMMAND=$(printf %q "$FZF_CTRL_T_COMMAND") FZF_CTRL_T_OPTS=$(printf %q "$FZF_CTRL_T_OPTS") bash -c 'source \"${BASH_SOURCE[0]}\"; RESULT=\"\$(__fzf_select__ --no-height)\"; tmux setb -b fzf \"\$RESULT\" \\; pasteb -b fzf -t $TMUX_PANE \\; deleteb -b fzf || tmux send-keys -t $TMUX_PANE \"\$RESULT\"'"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fzf-file-widget() {
|
fzf-file-widget() {
|
||||||
if __fzf_use_tmux__; then
|
local selected="$(__fzf_select__ "$@")"
|
||||||
__fzf_select_tmux__
|
|
||||||
else
|
|
||||||
local selected="$(__fzf_select__)"
|
|
||||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_cd__() {
|
__fzf_cd__() {
|
||||||
local cmd dir
|
local cmd opts 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 \
|
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-"}"
|
-o -type d -print 2> /dev/null | command cut -b3-"}"
|
||||||
dir=$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m) && printf 'cd %q' "$dir"
|
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore --reverse --scheme=path ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-} +m"
|
||||||
|
dir=$(set +o pipefail; eval "$cmd" | FZF_DEFAULT_OPTS="$opts" $(__fzfcmd)) && printf 'builtin cd -- %q' "$dir"
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_history__() (
|
if command -v perl > /dev/null; then
|
||||||
local line
|
__fzf_history__() {
|
||||||
shopt -u nocaseglob nocasematch
|
local output opts script
|
||||||
line=$(
|
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
|
||||||
HISTTIMEFORMAT= history |
|
script='BEGIN { getc; $/ = "\n\t"; $HISTCOUNT = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCOUNT - $. . "\t$_" if !$seen{$_}++'
|
||||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m" $(__fzfcmd) |
|
output=$(
|
||||||
command grep '^ *[0-9]') &&
|
set +o pipefail
|
||||||
if [[ $- =~ H ]]; then
|
builtin fc -lnr -2147483648 |
|
||||||
sed 's/^ *\([0-9]*\)\** .*/!\1/' <<< "$line"
|
last_hist=$(HISTTIMEFORMAT='' builtin history 1) command perl -n -l0 -e "$script" |
|
||||||
|
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
|
) || return
|
||||||
|
READLINE_LINE=${output#*$'\t'}
|
||||||
|
if [[ -z "$READLINE_POINT" ]]; then
|
||||||
|
echo "$READLINE_LINE"
|
||||||
else
|
else
|
||||||
sed 's/^ *\([0-9]*\)\** *//' <<< "$line"
|
READLINE_POINT=0x7fffffff
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
else # awk - fallback for POSIX systems
|
||||||
|
__fzf_history__() {
|
||||||
|
local output opts script n x y z d
|
||||||
|
if [[ -z $__fzf_awk ]]; then
|
||||||
|
__fzf_awk=awk
|
||||||
|
# choose the faster mawk if: it's installed && build date >= 20230322 && version >= 1.3.4
|
||||||
|
IFS=' .' read n x y z d <<< $(command mawk -W version 2> /dev/null)
|
||||||
|
[[ $n == mawk ]] && (( d >= 20230302 && (x *1000 +y) *1000 +z >= 1003004 )) && __fzf_awk=mawk
|
||||||
|
fi
|
||||||
|
opts="--height ${FZF_TMUX_HEIGHT:-40%} --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort ${FZF_CTRL_R_OPTS-} +m --read0"
|
||||||
|
[[ $(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> )*
|
||||||
|
command $__fzf_awk "$script" | # ( <counter>$'\t'<lines>$'\000' )*
|
||||||
|
FZF_DEFAULT_OPTS="$opts" $(__fzfcmd) --query "$READLINE_LINE"
|
||||||
|
) || return
|
||||||
|
READLINE_LINE=${output#*$'\t'}
|
||||||
|
if [[ -z "$READLINE_POINT" ]]; then
|
||||||
|
echo "$READLINE_LINE"
|
||||||
|
else
|
||||||
|
READLINE_POINT=0x7fffffff
|
||||||
|
fi
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
)
|
|
||||||
|
|
||||||
if [[ ! -o vi ]]; then
|
|
||||||
# Required to refresh the prompt after fzf
|
# Required to refresh the prompt after fzf
|
||||||
bind '"\er": redraw-current-line'
|
bind -m emacs-standard '"\er": redraw-current-line'
|
||||||
bind '"\e^": history-expand-line'
|
|
||||||
|
|
||||||
|
bind -m vi-command '"\C-z": emacs-editing-mode'
|
||||||
|
bind -m vi-insert '"\C-z": emacs-editing-mode'
|
||||||
|
bind -m emacs-standard '"\C-z": vi-editing-mode'
|
||||||
|
|
||||||
|
if (( BASH_VERSINFO[0] < 4 )); then
|
||||||
# CTRL-T - Paste the selected file path into the command line
|
# CTRL-T - Paste the selected file path into the command line
|
||||||
if [ $BASH_VERSINFO -gt 3 ]; 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"'
|
||||||
bind -x '"\C-t": "fzf-file-widget"'
|
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||||
elif __fzf_use_tmux__; then
|
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||||
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select_tmux__`\e\C-e\C-y\C-a\C-d\C-y\ey\C-h"'
|
|
||||||
else
|
|
||||||
bind '"\C-t": " \C-u \C-a\C-k`__fzf_select__`\e\C-e\C-y\C-a\C-y\ey\C-h\C-e\er \C-h"'
|
|
||||||
fi
|
|
||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
bind '"\C-r": " \C-e\C-u`__fzf_history__`\e\C-e\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"'
|
||||||
# ALT-C - cd into the selected directory
|
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||||
bind '"\ec": " \C-e\C-u`__fzf_cd__`\e\C-e\er\C-m"'
|
|
||||||
else
|
else
|
||||||
# We'd usually use "\e" to enter vi-movement-mode so we can do our magic,
|
|
||||||
# but this incurs a very noticeable delay of a half second or so,
|
|
||||||
# because many other commands start with "\e".
|
|
||||||
# Instead, we bind an unused key, "\C-x\C-a",
|
|
||||||
# to also enter vi-movement-mode,
|
|
||||||
# and then use that thereafter.
|
|
||||||
# (We imagine that "\C-x\C-a" is relatively unlikely to be in use.)
|
|
||||||
bind '"\C-x\C-a": vi-movement-mode'
|
|
||||||
|
|
||||||
bind '"\C-x\C-e": shell-expand-line'
|
|
||||||
bind '"\C-x\C-r": redraw-current-line'
|
|
||||||
bind '"\C-x^": history-expand-line'
|
|
||||||
|
|
||||||
# CTRL-T - Paste the selected file path into the command line
|
# CTRL-T - Paste the selected file path into the command line
|
||||||
# - FIXME: Selected items are attached to the end regardless of cursor position
|
bind -m emacs-standard -x '"\C-t": fzf-file-widget'
|
||||||
if [ $BASH_VERSINFO -gt 3 ]; then
|
bind -m vi-command -x '"\C-t": fzf-file-widget'
|
||||||
bind -x '"\C-t": "fzf-file-widget"'
|
bind -m vi-insert -x '"\C-t": fzf-file-widget'
|
||||||
elif __fzf_use_tmux__; then
|
|
||||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select_tmux__`\C-x\C-e\C-x\C-a0P$xa"'
|
|
||||||
else
|
|
||||||
bind '"\C-t": "\C-x\C-a$a \C-x\C-addi`__fzf_select__`\C-x\C-e\C-x\C-a0Px$a \C-x\C-r\C-x\C-axa "'
|
|
||||||
fi
|
|
||||||
bind -m vi-command '"\C-t": "i\C-t"'
|
|
||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
bind '"\C-r": "\C-x\C-addi`__fzf_history__`\C-x\C-e\C-x^\C-x\C-a$a\C-x\C-r"'
|
bind -m emacs-standard -x '"\C-r": __fzf_history__'
|
||||||
bind -m vi-command '"\C-r": "i\C-r"'
|
bind -m vi-command -x '"\C-r": __fzf_history__'
|
||||||
|
bind -m vi-insert -x '"\C-r": __fzf_history__'
|
||||||
|
fi
|
||||||
|
|
||||||
# ALT-C - cd into the selected directory
|
# ALT-C - cd into the selected directory
|
||||||
bind '"\ec": "\C-x\C-addi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
|
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": "ddi`__fzf_cd__`\C-x\C-e\C-x\C-r\C-m"'
|
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||||
fi
|
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||||
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -1,41 +1,52 @@
|
|||||||
|
# ____ ____
|
||||||
|
# / __/___ / __/
|
||||||
|
# / /_/_ / / /_
|
||||||
|
# / __/ / /_/ __/
|
||||||
|
# /_/ /___/_/ key-bindings.fish
|
||||||
|
#
|
||||||
|
# - $FZF_TMUX_OPTS
|
||||||
|
# - $FZF_CTRL_T_COMMAND
|
||||||
|
# - $FZF_CTRL_T_OPTS
|
||||||
|
# - $FZF_CTRL_R_OPTS
|
||||||
|
# - $FZF_ALT_C_COMMAND
|
||||||
|
# - $FZF_ALT_C_OPTS
|
||||||
|
|
||||||
|
status is-interactive; or exit 0
|
||||||
|
|
||||||
|
|
||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
function fzf_key_bindings
|
function fzf_key_bindings
|
||||||
|
|
||||||
# Store current token in $dir as root for the 'find' command
|
# Store current token in $dir as root for the 'find' command
|
||||||
function fzf-file-widget -d "List files and folders"
|
function fzf-file-widget -d "List files and folders"
|
||||||
set -l dir (commandline -t)
|
set -l commandline (__fzf_parse_commandline)
|
||||||
# The commandline token might be escaped, we need to unescape it.
|
set -l dir $commandline[1]
|
||||||
set dir (eval "printf '%s' $dir")
|
set -l fzf_query $commandline[2]
|
||||||
if [ ! -d "$dir" ]
|
set -l prefix $commandline[3]
|
||||||
set dir .
|
|
||||||
end
|
|
||||||
# Some 'find' versions print undesired duplicated slashes if the path ends with slashes.
|
|
||||||
set dir (string replace --regex '(.)/+$' '$1' "$dir")
|
|
||||||
|
|
||||||
# "-path \$dir'*/\\.*'" matches hidden files/folders inside $dir but not
|
# "-path \$dir'*/.*'" matches hidden files/folders inside $dir but not
|
||||||
# $dir itself, even if hidden.
|
# $dir itself, even if hidden.
|
||||||
set -q FZF_CTRL_T_COMMAND; or set -l FZF_CTRL_T_COMMAND "
|
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 \
|
command find -L \$dir -mindepth 1 \\( -path \$dir'*/.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
||||||
-o -type f -print \
|
-o -type f -print \
|
||||||
-o -type d -print \
|
-o -type d -print \
|
||||||
-o -type l -print 2> /dev/null | cut -b3-"
|
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||||
|
|
||||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||||
begin
|
begin
|
||||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS"
|
||||||
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)" -m" | while read -l r; set result $result $r; end
|
eval "$FZF_CTRL_T_COMMAND | "(__fzfcmd)' -m --query "'$fzf_query'"' | while read -l r; set result $result $r; end
|
||||||
end
|
end
|
||||||
if [ -z "$result" ]
|
if [ -z "$result" ]
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
return
|
return
|
||||||
end
|
else
|
||||||
|
|
||||||
if [ "$dir" != . ]
|
|
||||||
# Remove last token from commandline.
|
# Remove last token from commandline.
|
||||||
commandline -t ""
|
commandline -t ""
|
||||||
end
|
end
|
||||||
for i in $result
|
for i in $result
|
||||||
|
commandline -it -- $prefix
|
||||||
commandline -it -- (string escape $i)
|
commandline -it -- (string escape $i)
|
||||||
commandline -it -- ' '
|
commandline -it -- ' '
|
||||||
end
|
end
|
||||||
@@ -43,33 +54,60 @@ function fzf_key_bindings
|
|||||||
end
|
end
|
||||||
|
|
||||||
function fzf-history-widget -d "Show command history"
|
function fzf-history-widget -d "Show command history"
|
||||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||||
begin
|
begin
|
||||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS +m"
|
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT $FZF_DEFAULT_OPTS --scheme=history --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
|
history | eval (__fzfcmd) -q '(commandline)' | read -l result
|
||||||
and commandline -- $result
|
and commandline -- $result
|
||||||
end
|
end
|
||||||
|
end
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
function fzf-cd-widget -d "Change directory"
|
function fzf-cd-widget -d "Change directory"
|
||||||
set -q FZF_ALT_C_COMMAND; or set -l FZF_ALT_C_COMMAND "
|
set -l commandline (__fzf_parse_commandline)
|
||||||
command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' \\) -prune \
|
set -l dir $commandline[1]
|
||||||
-o -type d -print 2> /dev/null | cut -b3-"
|
set -l fzf_query $commandline[2]
|
||||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
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
|
begin
|
||||||
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
set -lx FZF_DEFAULT_OPTS "--height $FZF_TMUX_HEIGHT --reverse --scheme=path --bind=ctrl-z:ignore $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS"
|
||||||
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)" +m" | read -l result
|
eval "$FZF_ALT_C_COMMAND | "(__fzfcmd)' +m --query "'$fzf_query'"' | read -l result
|
||||||
[ "$result" ]; and cd $result
|
|
||||||
|
if [ -n "$result" ]
|
||||||
|
cd -- $result
|
||||||
|
|
||||||
|
# Remove last token from commandline.
|
||||||
|
commandline -t ""
|
||||||
|
commandline -it -- $prefix
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
commandline -f repaint
|
commandline -f repaint
|
||||||
end
|
end
|
||||||
|
|
||||||
function __fzfcmd
|
function __fzfcmd
|
||||||
set -q FZF_TMUX; or set FZF_TMUX 0
|
test -n "$FZF_TMUX"; or set FZF_TMUX 0
|
||||||
set -q FZF_TMUX_HEIGHT; or set FZF_TMUX_HEIGHT 40%
|
test -n "$FZF_TMUX_HEIGHT"; or set FZF_TMUX_HEIGHT 40%
|
||||||
if [ $FZF_TMUX -eq 1 ]
|
if [ -n "$FZF_TMUX_OPTS" ]
|
||||||
echo "fzf-tmux -d$FZF_TMUX_HEIGHT"
|
echo "fzf-tmux $FZF_TMUX_OPTS -- "
|
||||||
|
else if [ $FZF_TMUX -eq 1 ]
|
||||||
|
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
|
||||||
else
|
else
|
||||||
echo "fzf"
|
echo "fzf"
|
||||||
end
|
end
|
||||||
@@ -84,4 +122,54 @@ function fzf_key_bindings
|
|||||||
bind -M insert \cr fzf-history-widget
|
bind -M insert \cr fzf-history-widget
|
||||||
bind -M insert \ec fzf-cd-widget
|
bind -M insert \ec fzf-cd-widget
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function __fzf_parse_commandline -d 'Parse the current command line token and return split of existing filepath, fzf query, and optional -option= prefix'
|
||||||
|
set -l commandline (commandline -t)
|
||||||
|
|
||||||
|
# strip -option= from token if present
|
||||||
|
set -l prefix (string match -r -- '^-[^\s=]+=' $commandline)
|
||||||
|
set commandline (string replace -- "$prefix" '' $commandline)
|
||||||
|
|
||||||
|
# eval is used to do shell expansion on paths
|
||||||
|
eval set commandline $commandline
|
||||||
|
|
||||||
|
if [ -z $commandline ]
|
||||||
|
# Default to current directory with no --query
|
||||||
|
set dir '.'
|
||||||
|
set fzf_query ''
|
||||||
|
else
|
||||||
|
set dir (__fzf_get_dir $commandline)
|
||||||
|
|
||||||
|
if [ "$dir" = "." -a (string sub -l 1 -- $commandline) != '.' ]
|
||||||
|
# if $dir is "." but commandline is not a relative path, this means no file path found
|
||||||
|
set fzf_query $commandline
|
||||||
|
else
|
||||||
|
# Also remove trailing slash after dir, to "split" input properly
|
||||||
|
set fzf_query (string replace -r "^$dir/?" -- '' "$commandline")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
echo $dir
|
||||||
|
echo $fzf_query
|
||||||
|
echo $prefix
|
||||||
|
end
|
||||||
|
|
||||||
|
function __fzf_get_dir -d 'Find the longest existing filepath from input string'
|
||||||
|
set dir $argv
|
||||||
|
|
||||||
|
# Strip all trailing slashes. Ignore if $dir is root dir (/)
|
||||||
|
if [ (string length -- $dir) -gt 1 ]
|
||||||
|
set dir (string replace -r '/*$' -- '' $dir)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Iteratively check if dir exists and strip tail end of path
|
||||||
|
while [ ! -d "$dir" ]
|
||||||
|
# If path is absolute, this can keep going until ends up at /
|
||||||
|
# If path is relative, this can keep going until entire input is consumed, dirname returns "."
|
||||||
|
set dir (dirname -- "$dir")
|
||||||
|
end
|
||||||
|
|
||||||
|
echo $dir
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,15 +1,53 @@
|
|||||||
|
# ____ ____
|
||||||
|
# / __/___ / __/
|
||||||
|
# / /_/_ / / /_
|
||||||
|
# / __/ / /_/ __/
|
||||||
|
# /_/ /___/_/ key-bindings.zsh
|
||||||
|
#
|
||||||
|
# - $FZF_TMUX_OPTS
|
||||||
|
# - $FZF_CTRL_T_COMMAND
|
||||||
|
# - $FZF_CTRL_T_OPTS
|
||||||
|
# - $FZF_CTRL_R_OPTS
|
||||||
|
# - $FZF_ALT_C_COMMAND
|
||||||
|
# - $FZF_ALT_C_OPTS
|
||||||
|
|
||||||
|
[[ -o interactive ]] || return 0
|
||||||
|
|
||||||
|
|
||||||
# Key bindings
|
# Key bindings
|
||||||
# ------------
|
# ------------
|
||||||
if [[ $- == *i* ]]; then
|
|
||||||
|
# The code at the top and the bottom of this file is the same as in completion.zsh.
|
||||||
|
# Refer to that file for explanation.
|
||||||
|
if 'zmodload' 'zsh/parameter' 2>'/dev/null' && (( ${+options} )); then
|
||||||
|
__fzf_key_bindings_options="options=(${(j: :)${(kv)options[@]}})"
|
||||||
|
else
|
||||||
|
() {
|
||||||
|
__fzf_key_bindings_options="setopt"
|
||||||
|
'local' '__fzf_opt'
|
||||||
|
for __fzf_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
||||||
|
if [[ -o "$__fzf_opt" ]]; then
|
||||||
|
__fzf_key_bindings_options+=" -o $__fzf_opt"
|
||||||
|
else
|
||||||
|
__fzf_key_bindings_options+=" +o $__fzf_opt"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
'builtin' 'emulate' 'zsh' && 'builtin' 'setopt' 'no_aliases'
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
# CTRL-T - Paste the selected file path(s) into the command line
|
# CTRL-T - Paste the selected file path(s) into the command line
|
||||||
__fsel() {
|
__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 \
|
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 f -print \
|
||||||
-o -type d -print \
|
-o -type d -print \
|
||||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||||
setopt localoptions pipefail 2> /dev/null
|
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||||
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_CTRL_T_OPTS" $(__fzfcmd) -m "$@" | while read item; do
|
local item
|
||||||
|
eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_CTRL_T_OPTS-}" $(__fzfcmd) -m "$@" | while read item; do
|
||||||
echo -n "${(q)item} "
|
echo -n "${(q)item} "
|
||||||
done
|
done
|
||||||
local ret=$?
|
local ret=$?
|
||||||
@@ -17,50 +55,51 @@ __fsel() {
|
|||||||
return $ret
|
return $ret
|
||||||
}
|
}
|
||||||
|
|
||||||
__fzf_use_tmux__() {
|
|
||||||
[ -n "$TMUX_PANE" ] && [ "${FZF_TMUX:-0}" != 0 ] && [ ${LINES:-40} -gt 15 ]
|
|
||||||
}
|
|
||||||
|
|
||||||
__fzfcmd() {
|
__fzfcmd() {
|
||||||
__fzf_use_tmux__ &&
|
[ -n "${TMUX_PANE-}" ] && { [ "${FZF_TMUX:-0}" != 0 ] || [ -n "${FZF_TMUX_OPTS-}" ]; } &&
|
||||||
echo "fzf-tmux -d${FZF_TMUX_HEIGHT:-40%}" || echo "fzf"
|
echo "fzf-tmux ${FZF_TMUX_OPTS:--d${FZF_TMUX_HEIGHT:-40%}} -- " || echo "fzf"
|
||||||
}
|
}
|
||||||
|
|
||||||
fzf-file-widget() {
|
fzf-file-widget() {
|
||||||
LBUFFER="${LBUFFER}$(__fsel)"
|
LBUFFER="${LBUFFER}$(__fsel)"
|
||||||
local ret=$?
|
local ret=$?
|
||||||
zle redisplay
|
zle reset-prompt
|
||||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
|
||||||
return $ret
|
return $ret
|
||||||
}
|
}
|
||||||
zle -N fzf-file-widget
|
zle -N fzf-file-widget
|
||||||
bindkey '^T' fzf-file-widget
|
bindkey -M emacs '^T' fzf-file-widget
|
||||||
|
bindkey -M vicmd '^T' fzf-file-widget
|
||||||
|
bindkey -M viins '^T' fzf-file-widget
|
||||||
|
|
||||||
# ALT-C - cd into the selected directory
|
# ALT-C - cd into the selected directory
|
||||||
fzf-cd-widget() {
|
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 \
|
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-"}"
|
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||||
setopt localoptions pipefail 2> /dev/null
|
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||||
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse $FZF_DEFAULT_OPTS $FZF_ALT_C_OPTS" $(__fzfcmd) +m)"
|
local dir="$(eval "$cmd" | FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} --reverse --scheme=path --bind=ctrl-z:ignore ${FZF_DEFAULT_OPTS-} ${FZF_ALT_C_OPTS-}" $(__fzfcmd) +m)"
|
||||||
if [[ -z "$dir" ]]; then
|
if [[ -z "$dir" ]]; then
|
||||||
zle redisplay
|
zle redisplay
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
cd "$dir"
|
zle push-line # Clear buffer. Auto-restored on next prompt.
|
||||||
|
BUFFER="builtin cd -- ${(q)dir}"
|
||||||
|
zle accept-line
|
||||||
local ret=$?
|
local ret=$?
|
||||||
|
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||||
zle reset-prompt
|
zle reset-prompt
|
||||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
|
||||||
return $ret
|
return $ret
|
||||||
}
|
}
|
||||||
zle -N fzf-cd-widget
|
zle -N fzf-cd-widget
|
||||||
bindkey '\ec' fzf-cd-widget
|
bindkey -M emacs '\ec' fzf-cd-widget
|
||||||
|
bindkey -M vicmd '\ec' fzf-cd-widget
|
||||||
|
bindkey -M viins '\ec' fzf-cd-widget
|
||||||
|
|
||||||
# CTRL-R - Paste the selected command from history into the command line
|
# CTRL-R - Paste the selected command from history into the command line
|
||||||
fzf-history-widget() {
|
fzf-history-widget() {
|
||||||
local selected num
|
local selected num
|
||||||
setopt localoptions noglobsubst noposixbuiltins pipefail 2> /dev/null
|
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||||
selected=( $(fc -l 1 |
|
selected=( $(fc -rl 1 | awk '{ cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) print $0 }' |
|
||||||
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} $FZF_DEFAULT_OPTS --tac -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $FZF_CTRL_R_OPTS --query=${(q)LBUFFER} +m" $(__fzfcmd)) )
|
FZF_DEFAULT_OPTS="--height ${FZF_TMUX_HEIGHT:-40%} ${FZF_DEFAULT_OPTS-} -n2..,.. --scheme=history --bind=ctrl-r:toggle-sort,ctrl-z:ignore ${FZF_CTRL_R_OPTS-} --query=${(qqq)LBUFFER} +m" $(__fzfcmd)) )
|
||||||
local ret=$?
|
local ret=$?
|
||||||
if [ -n "$selected" ]; then
|
if [ -n "$selected" ]; then
|
||||||
num=$selected[1]
|
num=$selected[1]
|
||||||
@@ -68,11 +107,15 @@ fzf-history-widget() {
|
|||||||
zle vi-fetch-history -n $num
|
zle vi-fetch-history -n $num
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
zle redisplay
|
zle reset-prompt
|
||||||
typeset -f zle-line-init >/dev/null && zle zle-line-init
|
|
||||||
return $ret
|
return $ret
|
||||||
}
|
}
|
||||||
zle -N fzf-history-widget
|
zle -N fzf-history-widget
|
||||||
bindkey '^R' 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
|
||||||
|
'unset' '__fzf_key_bindings_options'
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2017 Junegunn Choi
|
Copyright (c) 2013-2023 Junegunn Choi
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
537
src/algo/algo.go
537
src/algo/algo.go
@@ -78,15 +78,22 @@ Scoring criteria
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DEBUG bool
|
var DEBUG bool
|
||||||
|
|
||||||
|
var delimiterChars = "/,:;|"
|
||||||
|
|
||||||
|
const whiteChars = " \t\n\v\f\r\x85\xA0"
|
||||||
|
|
||||||
func indexAt(index int, max int, forward bool) int {
|
func indexAt(index int, max int, forward bool) int {
|
||||||
if forward {
|
if forward {
|
||||||
return index
|
return index
|
||||||
@@ -105,7 +112,7 @@ type Result struct {
|
|||||||
const (
|
const (
|
||||||
scoreMatch = 16
|
scoreMatch = 16
|
||||||
scoreGapStart = -3
|
scoreGapStart = -3
|
||||||
scoreGapExtention = -1
|
scoreGapExtension = -1
|
||||||
|
|
||||||
// We prefer matches at the beginning of a word, but the bonus should not be
|
// We prefer matches at the beginning of a word, but the bonus should not be
|
||||||
// too great to prevent the longer acronym matches from always winning over
|
// too great to prevent the longer acronym matches from always winning over
|
||||||
@@ -123,31 +130,66 @@ const (
|
|||||||
// Edge-triggered bonus for matches in camelCase words.
|
// Edge-triggered bonus for matches in camelCase words.
|
||||||
// Compared to word-boundary case, they don't accompany single-character gaps
|
// Compared to word-boundary case, they don't accompany single-character gaps
|
||||||
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
// (e.g. FooBar vs. foo-bar), so we deduct bonus point accordingly.
|
||||||
bonusCamel123 = bonusBoundary + scoreGapExtention
|
bonusCamel123 = bonusBoundary + scoreGapExtension
|
||||||
|
|
||||||
// Minimum bonus point given to characters in consecutive chunks.
|
// Minimum bonus point given to characters in consecutive chunks.
|
||||||
// Note that bonus points for consecutive matches shouldn't have needed if we
|
// Note that bonus points for consecutive matches shouldn't have needed if we
|
||||||
// used fixed match score as in the original algorithm.
|
// used fixed match score as in the original algorithm.
|
||||||
bonusConsecutive = -(scoreGapStart + scoreGapExtention)
|
bonusConsecutive = -(scoreGapStart + scoreGapExtension)
|
||||||
|
|
||||||
// The first character in the typed pattern usually has more significance
|
// The first character in the typed pattern usually has more significance
|
||||||
// than the rest so it's important that it appears at special positions where
|
// than the rest so it's important that it appears at special positions where
|
||||||
// bonus points are given. e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
// bonus points are given, e.g. "to-go" vs. "ongoing" on "og" or on "ogo".
|
||||||
// The amount of the extra bonus should be limited so that the gap penalty is
|
// The amount of the extra bonus should be limited so that the gap penalty is
|
||||||
// still respected.
|
// still respected.
|
||||||
bonusFirstCharMultiplier = 2
|
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 charClass = charWhite
|
||||||
|
)
|
||||||
|
|
||||||
type charClass int
|
type charClass int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
charNonWord charClass = iota
|
charWhite charClass = iota
|
||||||
|
charNonWord
|
||||||
|
charDelimiter
|
||||||
charLower
|
charLower
|
||||||
charUpper
|
charUpper
|
||||||
charLetter
|
charLetter
|
||||||
charNumber
|
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
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func posArray(withPos bool, len int) *[]int {
|
func posArray(withPos bool, len int) *[]int {
|
||||||
if withPos {
|
if withPos {
|
||||||
pos := make([]int, 0, len)
|
pos := make([]int, 0, len)
|
||||||
@@ -156,27 +198,17 @@ func posArray(withPos bool, len int) *[]int {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func alloc16(offset int, slab *util.Slab, size int, clear bool) (int, []int16) {
|
func alloc16(offset int, slab *util.Slab, size int) (int, []int16) {
|
||||||
if slab != nil && cap(slab.I16) > offset+size {
|
if slab != nil && cap(slab.I16) > offset+size {
|
||||||
slice := slab.I16[offset : offset+size]
|
slice := slab.I16[offset : offset+size]
|
||||||
if clear {
|
|
||||||
for idx := range slice {
|
|
||||||
slice[idx] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return offset + size, slice
|
return offset + size, slice
|
||||||
}
|
}
|
||||||
return offset, make([]int16, size)
|
return offset, make([]int16, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
func alloc32(offset int, slab *util.Slab, size int, clear bool) (int, []int32) {
|
func alloc32(offset int, slab *util.Slab, size int) (int, []int32) {
|
||||||
if slab != nil && cap(slab.I32) > offset+size {
|
if slab != nil && cap(slab.I32) > offset+size {
|
||||||
slice := slab.I32[offset : offset+size]
|
slice := slab.I32[offset : offset+size]
|
||||||
if clear {
|
|
||||||
for idx := range slice {
|
|
||||||
slice[idx] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return offset + size, slice
|
return offset + size, slice
|
||||||
}
|
}
|
||||||
return offset, make([]int32, size)
|
return offset, make([]int32, size)
|
||||||
@@ -189,6 +221,10 @@ func charClassOfAscii(char rune) charClass {
|
|||||||
return charUpper
|
return charUpper
|
||||||
} else if char >= '0' && char <= '9' {
|
} else if char >= '0' && char <= '9' {
|
||||||
return charNumber
|
return charNumber
|
||||||
|
} else if strings.ContainsRune(whiteChars, char) {
|
||||||
|
return charWhite
|
||||||
|
} else if strings.ContainsRune(delimiterChars, char) {
|
||||||
|
return charDelimiter
|
||||||
}
|
}
|
||||||
return charNonWord
|
return charNonWord
|
||||||
}
|
}
|
||||||
@@ -202,6 +238,10 @@ func charClassOfNonAscii(char rune) charClass {
|
|||||||
return charNumber
|
return charNumber
|
||||||
} else if unicode.IsLetter(char) {
|
} else if unicode.IsLetter(char) {
|
||||||
return charLetter
|
return charLetter
|
||||||
|
} else if unicode.IsSpace(char) {
|
||||||
|
return charWhite
|
||||||
|
} else if strings.ContainsRune(delimiterChars, char) {
|
||||||
|
return charDelimiter
|
||||||
}
|
}
|
||||||
return charNonWord
|
return charNonWord
|
||||||
}
|
}
|
||||||
@@ -214,22 +254,33 @@ func charClassOf(char rune) charClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func bonusFor(prevClass charClass, class charClass) int16 {
|
func bonusFor(prevClass charClass, class charClass) int16 {
|
||||||
if prevClass == charNonWord && class != charNonWord {
|
if class > charNonWord {
|
||||||
|
if prevClass == charWhite {
|
||||||
|
// Word boundary after whitespace
|
||||||
|
return bonusBoundaryWhite
|
||||||
|
} else if prevClass == charDelimiter {
|
||||||
|
// Word boundary after a delimiter character
|
||||||
|
return bonusBoundaryDelimiter
|
||||||
|
} else if prevClass == charNonWord {
|
||||||
// Word boundary
|
// Word boundary
|
||||||
return bonusBoundary
|
return bonusBoundary
|
||||||
} else if prevClass == charLower && class == charUpper ||
|
}
|
||||||
|
}
|
||||||
|
if prevClass == charLower && class == charUpper ||
|
||||||
prevClass != charNumber && class == charNumber {
|
prevClass != charNumber && class == charNumber {
|
||||||
// camelCase letter123
|
// camelCase letter123
|
||||||
return bonusCamel123
|
return bonusCamel123
|
||||||
} else if class == charNonWord {
|
} else if class == charNonWord {
|
||||||
return bonusNonWord
|
return bonusNonWord
|
||||||
|
} else if class == charWhite {
|
||||||
|
return bonusBoundaryWhite
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func bonusAt(input util.Chars, idx int) int16 {
|
func bonusAt(input *util.Chars, idx int) int16 {
|
||||||
if idx == 0 {
|
if idx == 0 {
|
||||||
return bonusBoundary
|
return bonusBoundaryWhite
|
||||||
}
|
}
|
||||||
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
return bonusFor(charClassOf(input.Get(idx-1)), charClassOf(input.Get(idx)))
|
||||||
}
|
}
|
||||||
@@ -249,155 +300,84 @@ func normalizeRune(r rune) rune {
|
|||||||
// Algo functions make two assumptions
|
// Algo functions make two assumptions
|
||||||
// 1. "pattern" is given in lowercase if "caseSensitive" is false
|
// 1. "pattern" is given in lowercase if "caseSensitive" is false
|
||||||
// 2. "pattern" is already normalized if "normalize" is true
|
// 2. "pattern" is already normalized if "normalize" is true
|
||||||
type Algo func(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
|
type Algo func(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int)
|
||||||
|
|
||||||
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func trySkip(input *util.Chars, caseSensitive bool, b byte, from int) int {
|
||||||
// Assume that pattern is given in lowercase if case-insensitive.
|
byteArray := input.Bytes()[from:]
|
||||||
// First check if there's a match and calculate bonus for each position.
|
idx := bytes.IndexByte(byteArray, b)
|
||||||
// If the input string is too long, consider finding the matching chars in
|
if idx == 0 {
|
||||||
// this phase as well (non-optimal alignment).
|
// Can't skip any further
|
||||||
N := input.Length()
|
return from
|
||||||
M := len(pattern)
|
}
|
||||||
switch M {
|
// We may need to search for the uppercase letter again. We don't have to
|
||||||
case 0:
|
// consider normalization as we can be sure that this is an ASCII string.
|
||||||
return Result{0, 0, 0}, posArray(withPos, M)
|
if !caseSensitive && b >= 'a' && b <= 'z' {
|
||||||
case 1:
|
if idx > 0 {
|
||||||
return ExactMatchNaive(caseSensitive, normalize, forward, input, pattern[0:1], withPos, slab)
|
byteArray = byteArray[:idx]
|
||||||
|
}
|
||||||
|
uidx := bytes.IndexByte(byteArray, b-32)
|
||||||
|
if uidx >= 0 {
|
||||||
|
idx = uidx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if idx < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return from + idx
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
func isAscii(runes []rune) bool {
|
||||||
// we fall back to the greedy algorithm.
|
for _, r := range runes {
|
||||||
if slab != nil && N*M > cap(slab.I16) {
|
if r >= utf8.RuneSelf {
|
||||||
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
func asciiFuzzyIndex(input *util.Chars, pattern []rune, caseSensitive bool) int {
|
||||||
offset16 := 0
|
// Can't determine
|
||||||
offset32 := 0
|
if !input.IsBytes() {
|
||||||
// Bonus point for each position
|
return 0
|
||||||
offset16, B := alloc16(offset16, slab, N, false)
|
|
||||||
// The first occurrence of each character in the pattern
|
|
||||||
offset32, F := alloc32(offset32, slab, M, false)
|
|
||||||
// Rune array
|
|
||||||
offset32, T := alloc32(offset32, slab, N, false)
|
|
||||||
|
|
||||||
// Phase 1. Check if there's a match and calculate bonus for each point
|
|
||||||
pidx, lastIdx, prevClass := 0, 0, charNonWord
|
|
||||||
for idx := 0; idx < N; idx++ {
|
|
||||||
char := input.Get(idx)
|
|
||||||
var class charClass
|
|
||||||
if char <= unicode.MaxASCII {
|
|
||||||
class = charClassOfAscii(char)
|
|
||||||
} else {
|
|
||||||
class = charClassOfNonAscii(char)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !caseSensitive && class == charUpper {
|
// Not possible
|
||||||
if char <= unicode.MaxASCII {
|
if !isAscii(pattern) {
|
||||||
char += 32
|
return -1
|
||||||
} else {
|
|
||||||
char = unicode.To(unicode.LowerCase, char)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if normalize {
|
firstIdx, idx := 0, 0
|
||||||
char = normalizeRune(char)
|
for pidx := 0; pidx < len(pattern); pidx++ {
|
||||||
|
idx = trySkip(input, caseSensitive, byte(pattern[pidx]), idx)
|
||||||
|
if idx < 0 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if pidx == 0 && idx > 0 {
|
||||||
|
// Step back to find the right bonus point
|
||||||
|
firstIdx = idx - 1
|
||||||
|
}
|
||||||
|
idx++
|
||||||
|
}
|
||||||
|
return firstIdx
|
||||||
}
|
}
|
||||||
|
|
||||||
T[idx] = char
|
func debugV2(T []rune, pattern []rune, F []int32, lastIdx int, H []int16, C []int16) {
|
||||||
B[idx] = bonusFor(prevClass, class)
|
|
||||||
prevClass = class
|
|
||||||
|
|
||||||
if pidx < M {
|
|
||||||
if char == pattern[pidx] {
|
|
||||||
lastIdx = idx
|
|
||||||
F[pidx] = int32(idx)
|
|
||||||
pidx++
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if char == pattern[M-1] {
|
|
||||||
lastIdx = idx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if pidx != M {
|
|
||||||
return Result{-1, -1, 0}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2. Fill in score matrix (H)
|
|
||||||
// Unlike the original algorithm, we do not allow omission.
|
|
||||||
width := lastIdx - int(F[0]) + 1
|
width := lastIdx - int(F[0]) + 1
|
||||||
offset16, H := alloc16(offset16, slab, width*M, false)
|
|
||||||
|
|
||||||
// Possible length of consecutive chunk at each position.
|
for i, f := range F {
|
||||||
offset16, C := alloc16(offset16, slab, width*M, false)
|
|
||||||
|
|
||||||
maxScore, maxScorePos := int16(0), 0
|
|
||||||
for i := 0; i < M; i++ {
|
|
||||||
I := i * width
|
I := i * width
|
||||||
inGap := false
|
|
||||||
for j := int(F[i]); j <= lastIdx; j++ {
|
|
||||||
j0 := j - int(F[0])
|
|
||||||
var s1, s2, consecutive int16
|
|
||||||
|
|
||||||
if j > int(F[i]) {
|
|
||||||
if inGap {
|
|
||||||
s2 = H[I+j0-1] + scoreGapExtention
|
|
||||||
} else {
|
|
||||||
s2 = H[I+j0-1] + scoreGapStart
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if pattern[i] == T[j] {
|
|
||||||
var diag int16
|
|
||||||
if i > 0 && j0 > 0 {
|
|
||||||
diag = H[I-width+j0-1]
|
|
||||||
}
|
|
||||||
s1 = diag + scoreMatch
|
|
||||||
b := B[j]
|
|
||||||
if i > 0 {
|
|
||||||
// j > 0 if i > 0
|
|
||||||
consecutive = C[I-width+j0-1] + 1
|
|
||||||
// Break consecutive chunk
|
|
||||||
if b == bonusBoundary {
|
|
||||||
consecutive = 1
|
|
||||||
} else if consecutive > 1 {
|
|
||||||
b = util.Max16(b, util.Max16(bonusConsecutive, B[j-int(consecutive)+1]))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
consecutive = 1
|
|
||||||
b *= bonusFirstCharMultiplier
|
|
||||||
}
|
|
||||||
if s1+b < s2 {
|
|
||||||
s1 += B[j]
|
|
||||||
consecutive = 0
|
|
||||||
} else {
|
|
||||||
s1 += b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
C[I+j0] = consecutive
|
|
||||||
|
|
||||||
inGap = s1 < s2
|
|
||||||
score := util.Max16(util.Max16(s1, s2), 0)
|
|
||||||
if i == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
|
||||||
maxScore, maxScorePos = score, j
|
|
||||||
}
|
|
||||||
H[I+j0] = score
|
|
||||||
}
|
|
||||||
|
|
||||||
if DEBUG {
|
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
fmt.Print(" ")
|
fmt.Print(" ")
|
||||||
for j := int(F[i]); j <= lastIdx; j++ {
|
for j := int(f); j <= lastIdx; j++ {
|
||||||
fmt.Printf(" " + string(input.Get(j)) + " ")
|
fmt.Printf(" " + string(T[j]) + " ")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
fmt.Print(string(pattern[i]) + " ")
|
fmt.Print(string(pattern[i]) + " ")
|
||||||
for idx := int(F[0]); idx < int(F[i]); idx++ {
|
for idx := int(F[0]); idx < int(f); idx++ {
|
||||||
fmt.Print(" 0 ")
|
fmt.Print(" 0 ")
|
||||||
}
|
}
|
||||||
for idx := int(F[i]); idx <= lastIdx; idx++ {
|
for idx := int(f); idx <= lastIdx; idx++ {
|
||||||
fmt.Printf("%2d ", H[i*width+idx-int(F[0])])
|
fmt.Printf("%2d ", H[i*width+idx-int(F[0])])
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -407,22 +387,205 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
|
|||||||
if idx+int(F[0]) < int(F[i]) {
|
if idx+int(F[0]) < int(F[i]) {
|
||||||
p = 0
|
p = 0
|
||||||
}
|
}
|
||||||
|
if p > 0 {
|
||||||
fmt.Printf("%2d ", p)
|
fmt.Printf("%2d ", p)
|
||||||
|
} else {
|
||||||
|
fmt.Print(" ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase 3. (Optional) Backtrace to find character positions
|
func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
|
// Assume that pattern is given in lowercase if case-insensitive.
|
||||||
|
// First check if there's a match and calculate bonus for each position.
|
||||||
|
// If the input string is too long, consider finding the matching chars in
|
||||||
|
// this phase as well (non-optimal alignment).
|
||||||
|
M := len(pattern)
|
||||||
|
if M == 0 {
|
||||||
|
return Result{0, 0, 0}, posArray(withPos, M)
|
||||||
|
}
|
||||||
|
N := input.Length()
|
||||||
|
|
||||||
|
// Since O(nm) algorithm can be prohibitively expensive for large input,
|
||||||
|
// we fall back to the greedy algorithm.
|
||||||
|
if slab != nil && N*M > cap(slab.I16) {
|
||||||
|
return FuzzyMatchV1(caseSensitive, normalize, forward, input, pattern, withPos, slab)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 1. Optimized search for ASCII string
|
||||||
|
idx := asciiFuzzyIndex(input, pattern, caseSensitive)
|
||||||
|
if idx < 0 {
|
||||||
|
return Result{-1, -1, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reuse pre-allocated integer slice to avoid unnecessary sweeping of garbages
|
||||||
|
offset16 := 0
|
||||||
|
offset32 := 0
|
||||||
|
offset16, H0 := alloc16(offset16, slab, N)
|
||||||
|
offset16, C0 := alloc16(offset16, slab, N)
|
||||||
|
// Bonus point for each position
|
||||||
|
offset16, B := alloc16(offset16, slab, N)
|
||||||
|
// The first occurrence of each character in the pattern
|
||||||
|
offset32, F := alloc32(offset32, slab, M)
|
||||||
|
// Rune array
|
||||||
|
_, T := alloc32(offset32, slab, N)
|
||||||
|
input.CopyRunes(T)
|
||||||
|
|
||||||
|
// 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), initialCharClass, false
|
||||||
|
Tsub := T[idx:]
|
||||||
|
H0sub, C0sub, Bsub := H0[idx:][:len(Tsub)], C0[idx:][:len(Tsub)], B[idx:][:len(Tsub)]
|
||||||
|
for off, char := range Tsub {
|
||||||
|
var class charClass
|
||||||
|
if char <= unicode.MaxASCII {
|
||||||
|
class = charClassOfAscii(char)
|
||||||
|
if !caseSensitive && class == charUpper {
|
||||||
|
char += 32
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
class = charClassOfNonAscii(char)
|
||||||
|
if !caseSensitive && class == charUpper {
|
||||||
|
char = unicode.To(unicode.LowerCase, char)
|
||||||
|
}
|
||||||
|
if normalize {
|
||||||
|
char = normalizeRune(char)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Tsub[off] = char
|
||||||
|
bonus := bonusFor(prevClass, class)
|
||||||
|
Bsub[off] = bonus
|
||||||
|
prevClass = class
|
||||||
|
|
||||||
|
if char == pchar {
|
||||||
|
if pidx < M {
|
||||||
|
F[pidx] = int32(idx + off)
|
||||||
|
pidx++
|
||||||
|
pchar = pattern[util.Min(pidx, M-1)]
|
||||||
|
}
|
||||||
|
lastIdx = idx + off
|
||||||
|
}
|
||||||
|
|
||||||
|
if char == pchar0 {
|
||||||
|
score := scoreMatch + bonus*bonusFirstCharMultiplier
|
||||||
|
H0sub[off] = score
|
||||||
|
C0sub[off] = 1
|
||||||
|
if M == 1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||||
|
maxScore, maxScorePos = score, idx+off
|
||||||
|
if forward && bonus >= bonusBoundary {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inGap = false
|
||||||
|
} else {
|
||||||
|
if inGap {
|
||||||
|
H0sub[off] = util.Max16(prevH0+scoreGapExtension, 0)
|
||||||
|
} else {
|
||||||
|
H0sub[off] = util.Max16(prevH0+scoreGapStart, 0)
|
||||||
|
}
|
||||||
|
C0sub[off] = 0
|
||||||
|
inGap = true
|
||||||
|
}
|
||||||
|
prevH0 = H0sub[off]
|
||||||
|
}
|
||||||
|
if pidx != M {
|
||||||
|
return Result{-1, -1, 0}, nil
|
||||||
|
}
|
||||||
|
if M == 1 {
|
||||||
|
result := Result{maxScorePos, maxScorePos + 1, int(maxScore)}
|
||||||
|
if !withPos {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
pos := []int{maxScorePos}
|
||||||
|
return result, &pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 3. Fill in score matrix (H)
|
||||||
|
// Unlike the original algorithm, we do not allow omission.
|
||||||
|
f0 := int(F[0])
|
||||||
|
width := lastIdx - f0 + 1
|
||||||
|
offset16, H := alloc16(offset16, slab, width*M)
|
||||||
|
copy(H, H0[f0:lastIdx+1])
|
||||||
|
|
||||||
|
// Possible length of consecutive chunk at each position.
|
||||||
|
_, C := alloc16(offset16, slab, width*M)
|
||||||
|
copy(C, C0[f0:lastIdx+1])
|
||||||
|
|
||||||
|
Fsub := F[1:]
|
||||||
|
Psub := pattern[1:][:len(Fsub)]
|
||||||
|
for off, f := range Fsub {
|
||||||
|
f := int(f)
|
||||||
|
pchar := Psub[off]
|
||||||
|
pidx := off + 1
|
||||||
|
row := pidx * width
|
||||||
|
inGap := false
|
||||||
|
Tsub := T[f : lastIdx+1]
|
||||||
|
Bsub := B[f:][:len(Tsub)]
|
||||||
|
Csub := C[row+f-f0:][:len(Tsub)]
|
||||||
|
Cdiag := C[row+f-f0-1-width:][:len(Tsub)]
|
||||||
|
Hsub := H[row+f-f0:][:len(Tsub)]
|
||||||
|
Hdiag := H[row+f-f0-1-width:][:len(Tsub)]
|
||||||
|
Hleft := H[row+f-f0-1:][:len(Tsub)]
|
||||||
|
Hleft[0] = 0
|
||||||
|
for off, char := range Tsub {
|
||||||
|
col := off + f
|
||||||
|
var s1, s2, consecutive int16
|
||||||
|
|
||||||
|
if inGap {
|
||||||
|
s2 = Hleft[off] + scoreGapExtension
|
||||||
|
} else {
|
||||||
|
s2 = Hleft[off] + scoreGapStart
|
||||||
|
}
|
||||||
|
|
||||||
|
if pchar == char {
|
||||||
|
s1 = Hdiag[off] + scoreMatch
|
||||||
|
b := Bsub[off]
|
||||||
|
consecutive = Cdiag[off] + 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]
|
||||||
|
consecutive = 0
|
||||||
|
} else {
|
||||||
|
s1 += b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Csub[off] = consecutive
|
||||||
|
|
||||||
|
inGap = s1 < s2
|
||||||
|
score := util.Max16(util.Max16(s1, s2), 0)
|
||||||
|
if pidx == M-1 && (forward && score > maxScore || !forward && score >= maxScore) {
|
||||||
|
maxScore, maxScorePos = score, col
|
||||||
|
}
|
||||||
|
Hsub[off] = score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if DEBUG {
|
||||||
|
debugV2(T, pattern, F, lastIdx, H, C)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 4. (Optional) Backtrace to find character positions
|
||||||
pos := posArray(withPos, M)
|
pos := posArray(withPos, M)
|
||||||
j := int(F[0])
|
j := f0
|
||||||
if withPos {
|
if withPos {
|
||||||
i := M - 1
|
i := M - 1
|
||||||
j = maxScorePos
|
j = maxScorePos
|
||||||
preferMatch := true
|
preferMatch := true
|
||||||
for {
|
for {
|
||||||
I := i * width
|
I := i * width
|
||||||
j0 := j - int(F[0])
|
j0 := j - f0
|
||||||
s := H[I+j0]
|
s := H[I+j0]
|
||||||
|
|
||||||
var s1, s2 int16
|
var s1, s2 int16
|
||||||
@@ -451,10 +614,10 @@ func FuzzyMatchV2(caseSensitive bool, normalize bool, forward bool, input util.C
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Implement the same sorting criteria as V2
|
// 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) {
|
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)
|
pidx, score, inGap, consecutive, firstBonus := 0, 0, false, 0, int16(0)
|
||||||
pos := posArray(withPos, len(pattern))
|
pos := posArray(withPos, len(pattern))
|
||||||
prevClass := charNonWord
|
prevClass := initialCharClass
|
||||||
if sidx > 0 {
|
if sidx > 0 {
|
||||||
prevClass = charClassOf(text.Get(sidx - 1))
|
prevClass = charClassOf(text.Get(sidx - 1))
|
||||||
}
|
}
|
||||||
@@ -482,7 +645,7 @@ func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern
|
|||||||
firstBonus = bonus
|
firstBonus = bonus
|
||||||
} else {
|
} else {
|
||||||
// Break consecutive chunk
|
// Break consecutive chunk
|
||||||
if bonus == bonusBoundary {
|
if bonus >= bonusBoundary && bonus > firstBonus {
|
||||||
firstBonus = bonus
|
firstBonus = bonus
|
||||||
}
|
}
|
||||||
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
bonus = util.Max16(util.Max16(bonus, firstBonus), bonusConsecutive)
|
||||||
@@ -497,7 +660,7 @@ func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern
|
|||||||
pidx++
|
pidx++
|
||||||
} else {
|
} else {
|
||||||
if inGap {
|
if inGap {
|
||||||
score += scoreGapExtention
|
score += scoreGapExtension
|
||||||
} else {
|
} else {
|
||||||
score += scoreGapStart
|
score += scoreGapStart
|
||||||
}
|
}
|
||||||
@@ -511,10 +674,13 @@ func calculateScore(caseSensitive bool, normalize bool, text util.Chars, pattern
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FuzzyMatchV1 performs fuzzy-match
|
// FuzzyMatchV1 performs fuzzy-match
|
||||||
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
if len(pattern) == 0 {
|
if len(pattern) == 0 {
|
||||||
return Result{0, 0, 0}, nil
|
return Result{0, 0, 0}, nil
|
||||||
}
|
}
|
||||||
|
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||||
|
return Result{-1, -1, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
pidx := 0
|
pidx := 0
|
||||||
sidx := -1
|
sidx := -1
|
||||||
@@ -594,7 +760,7 @@ func FuzzyMatchV1(caseSensitive bool, normalize bool, forward bool, text util.Ch
|
|||||||
// bonus point, instead of stopping immediately after finding the first match.
|
// bonus point, instead of stopping immediately after finding the first match.
|
||||||
// The solution is much cheaper since there is only one possible alignment of
|
// The solution is much cheaper since there is only one possible alignment of
|
||||||
// the pattern.
|
// the pattern.
|
||||||
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
if len(pattern) == 0 {
|
if len(pattern) == 0 {
|
||||||
return Result{0, 0, 0}, nil
|
return Result{0, 0, 0}, nil
|
||||||
}
|
}
|
||||||
@@ -606,6 +772,10 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util
|
|||||||
return Result{-1, -1, 0}, nil
|
return Result{-1, -1, 0}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if asciiFuzzyIndex(text, pattern, caseSensitive) < 0 {
|
||||||
|
return Result{-1, -1, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
// For simplicity, only look at the bonus at the first character position
|
// For simplicity, only look at the bonus at the first character position
|
||||||
pidx := 0
|
pidx := 0
|
||||||
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
bestPos, bonus, bestBonus := -1, int16(0), int16(-1)
|
||||||
@@ -633,7 +803,7 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util
|
|||||||
if bonus > bestBonus {
|
if bonus > bestBonus {
|
||||||
bestPos, bestBonus = index, bonus
|
bestPos, bestBonus = index, bonus
|
||||||
}
|
}
|
||||||
if bonus == bonusBoundary {
|
if bonus >= bonusBoundary {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
index -= pidx - 1
|
index -= pidx - 1
|
||||||
@@ -660,17 +830,22 @@ func ExactMatchNaive(caseSensitive bool, normalize bool, forward bool, text util
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PrefixMatch performs prefix-match
|
// PrefixMatch performs prefix-match
|
||||||
func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
if len(pattern) == 0 {
|
if len(pattern) == 0 {
|
||||||
return Result{0, 0, 0}, nil
|
return Result{0, 0, 0}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if text.Length() < len(pattern) {
|
trimmedLen := 0
|
||||||
|
if !unicode.IsSpace(pattern[0]) {
|
||||||
|
trimmedLen = text.LeadingWhitespaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
if text.Length()-trimmedLen < len(pattern) {
|
||||||
return Result{-1, -1, 0}, nil
|
return Result{-1, -1, 0}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, r := range pattern {
|
for index, r := range pattern {
|
||||||
char := text.Get(index)
|
char := text.Get(trimmedLen + index)
|
||||||
if !caseSensitive {
|
if !caseSensitive {
|
||||||
char = unicode.ToLower(char)
|
char = unicode.ToLower(char)
|
||||||
}
|
}
|
||||||
@@ -682,14 +857,17 @@ func PrefixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
lenPattern := len(pattern)
|
lenPattern := len(pattern)
|
||||||
score, _ := calculateScore(caseSensitive, normalize, text, pattern, 0, lenPattern, false)
|
score, _ := calculateScore(caseSensitive, normalize, text, pattern, trimmedLen, trimmedLen+lenPattern, false)
|
||||||
return Result{0, lenPattern, score}, nil
|
return Result{trimmedLen, trimmedLen + lenPattern, score}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SuffixMatch performs suffix-match
|
// SuffixMatch performs suffix-match
|
||||||
func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
lenRunes := text.Length()
|
lenRunes := text.Length()
|
||||||
trimmedLen := lenRunes - text.TrailingWhitespaces()
|
trimmedLen := lenRunes
|
||||||
|
if len(pattern) == 0 || !unicode.IsSpace(pattern[len(pattern)-1]) {
|
||||||
|
trimmedLen -= text.TrailingWhitespaces()
|
||||||
|
}
|
||||||
if len(pattern) == 0 {
|
if len(pattern) == 0 {
|
||||||
return Result{trimmedLen, trimmedLen, 0}, nil
|
return Result{trimmedLen, trimmedLen, 0}, nil
|
||||||
}
|
}
|
||||||
@@ -718,16 +896,32 @@ func SuffixMatch(caseSensitive bool, normalize bool, forward bool, text util.Cha
|
|||||||
}
|
}
|
||||||
|
|
||||||
// EqualMatch performs equal-match
|
// EqualMatch performs equal-match
|
||||||
func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
func EqualMatch(caseSensitive bool, normalize bool, forward bool, text *util.Chars, pattern []rune, withPos bool, slab *util.Slab) (Result, *[]int) {
|
||||||
lenPattern := len(pattern)
|
lenPattern := len(pattern)
|
||||||
if text.Length() != lenPattern {
|
if lenPattern == 0 {
|
||||||
|
return Result{-1, -1, 0}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip leading whitespaces
|
||||||
|
trimmedLen := 0
|
||||||
|
if !unicode.IsSpace(pattern[0]) {
|
||||||
|
trimmedLen = text.LeadingWhitespaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip trailing whitespaces
|
||||||
|
trimmedEndLen := 0
|
||||||
|
if !unicode.IsSpace(pattern[lenPattern-1]) {
|
||||||
|
trimmedEndLen = text.TrailingWhitespaces()
|
||||||
|
}
|
||||||
|
|
||||||
|
if text.Length()-trimmedLen-trimmedEndLen != lenPattern {
|
||||||
return Result{-1, -1, 0}, nil
|
return Result{-1, -1, 0}, nil
|
||||||
}
|
}
|
||||||
match := true
|
match := true
|
||||||
if normalize {
|
if normalize {
|
||||||
runes := text.ToRunes()
|
runes := text.ToRunes()
|
||||||
for idx, pchar := range pattern {
|
for idx, pchar := range pattern {
|
||||||
char := runes[idx]
|
char := runes[trimmedLen+idx]
|
||||||
if !caseSensitive {
|
if !caseSensitive {
|
||||||
char = unicode.To(unicode.LowerCase, char)
|
char = unicode.To(unicode.LowerCase, char)
|
||||||
}
|
}
|
||||||
@@ -737,15 +931,16 @@ func EqualMatch(caseSensitive bool, normalize bool, forward bool, text util.Char
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
runesStr := text.ToString()
|
runes := text.ToRunes()
|
||||||
|
runesStr := string(runes[trimmedLen : len(runes)-trimmedEndLen])
|
||||||
if !caseSensitive {
|
if !caseSensitive {
|
||||||
runesStr = strings.ToLower(runesStr)
|
runesStr = strings.ToLower(runesStr)
|
||||||
}
|
}
|
||||||
match = runesStr == string(pattern)
|
match = runesStr == string(pattern)
|
||||||
}
|
}
|
||||||
if match {
|
if match {
|
||||||
return Result{0, lenPattern, (scoreMatch+bonusBoundary)*lenPattern +
|
return Result{trimmedLen, trimmedLen + lenPattern, (scoreMatch+int(bonusBoundaryWhite))*lenPattern +
|
||||||
(bonusFirstCharMultiplier-1)*bonusBoundary}, nil
|
(bonusFirstCharMultiplier-1)*int(bonusBoundaryWhite)}, nil
|
||||||
}
|
}
|
||||||
return Result{-1, -1, 0}, nil
|
return Result{-1, -1, 0}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ func assertMatch2(t *testing.T, fun Algo, caseSensitive, normalize, forward bool
|
|||||||
if !caseSensitive {
|
if !caseSensitive {
|
||||||
pattern = strings.ToLower(pattern)
|
pattern = strings.ToLower(pattern)
|
||||||
}
|
}
|
||||||
res, pos := fun(caseSensitive, normalize, forward, util.RunesToChars([]rune(input)), []rune(pattern), true, nil)
|
chars := util.ToChars([]byte(input))
|
||||||
|
res, pos := fun(caseSensitive, normalize, forward, &chars, []rune(pattern), true, nil)
|
||||||
var start, end int
|
var start, end int
|
||||||
if pos == nil || len(*pos) == 0 {
|
if pos == nil || len(*pos) == 0 {
|
||||||
start = res.Start
|
start = res.Start
|
||||||
@@ -42,46 +43,46 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
|
for _, fn := range []Algo{FuzzyMatchV1, FuzzyMatchV2} {
|
||||||
for _, forward := range []bool{true, false} {
|
for _, forward := range []bool{true, false} {
|
||||||
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
assertMatch(t, fn, false, forward, "fooBarbaz1", "oBZ", 2, 9,
|
||||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||||
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
assertMatch(t, fn, false, forward, "foo bar baz", "fbb", 0, 9,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
int(bonusBoundaryWhite)*2+2*scoreGapStart+4*scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
assertMatch(t, fn, false, forward, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||||
assertMatch(t, fn, false, forward, "/man1/zshcompctl.1", "zshc", 6, 10,
|
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,
|
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,
|
assertMatch(t, fn, false, forward, "ab0123 456", "12356", 3, 10,
|
||||||
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtention)
|
scoreMatch*5+bonusConsecutive*3+scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
assertMatch(t, fn, false, forward, "abc123 456", "12356", 3, 10,
|
||||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtention)
|
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+bonusConsecutive+scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
assertMatch(t, fn, false, forward, "foo/bar/baz", "fbb", 0, 9,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||||
bonusBoundary*2+2*scoreGapStart+4*scoreGapExtention)
|
int(bonusBoundaryDelimiter)*2+2*scoreGapStart+4*scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
assertMatch(t, fn, false, forward, "fooBarBaz", "fbb", 0, 7,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||||
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtention)
|
bonusCamel123*2+2*scoreGapStart+2*scoreGapExtension)
|
||||||
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
assertMatch(t, fn, false, forward, "foo barbaz", "fbb", 0, 8,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary+
|
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)+
|
||||||
scoreGapStart*2+scoreGapExtention*3)
|
scoreGapStart*2+scoreGapExtension*3)
|
||||||
assertMatch(t, fn, false, forward, "fooBar Baz", "foob", 0, 4,
|
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,
|
assertMatch(t, fn, false, forward, "xFoo-Bar Baz", "foo-b", 1, 6,
|
||||||
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
|
scoreMatch*5+bonusCamel123*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||||
bonusNonWord+bonusBoundary)
|
bonusNonWord+bonusBoundary)
|
||||||
|
|
||||||
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
assertMatch(t, fn, true, forward, "fooBarbaz", "oBz", 2, 9,
|
||||||
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtention*3)
|
scoreMatch*3+bonusCamel123+scoreGapStart+scoreGapExtension*3)
|
||||||
assertMatch(t, fn, true, forward, "Foo/Bar/Baz", "FBB", 0, 9,
|
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+scoreGapExtention*4)
|
scoreGapStart*2+scoreGapExtension*4)
|
||||||
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
assertMatch(t, fn, true, forward, "FooBarBaz", "FBB", 0, 7,
|
||||||
scoreMatch*3+bonusBoundary*bonusFirstCharMultiplier+bonusCamel123*2+
|
scoreMatch*3+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+bonusCamel123*2+
|
||||||
scoreGapStart*2+scoreGapExtention*2)
|
scoreGapStart*2+scoreGapExtension*2)
|
||||||
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
assertMatch(t, fn, true, forward, "FooBar Baz", "FooB", 0, 4,
|
||||||
scoreMatch*4+bonusBoundary*bonusFirstCharMultiplier+bonusBoundary*2+
|
scoreMatch*4+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+int(bonusBoundaryWhite)*2+
|
||||||
util.Max(bonusCamel123, bonusBoundary))
|
util.Max(bonusCamel123, int(bonusBoundaryWhite)))
|
||||||
|
|
||||||
// Consecutive bonus updated
|
// Consecutive bonus updated
|
||||||
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
assertMatch(t, fn, true, forward, "foo-bar", "o-ba", 2, 6,
|
||||||
@@ -97,10 +98,10 @@ func TestFuzzyMatch(t *testing.T) {
|
|||||||
|
|
||||||
func TestFuzzyMatchBackward(t *testing.T) {
|
func TestFuzzyMatchBackward(t *testing.T) {
|
||||||
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
assertMatch(t, FuzzyMatchV1, false, true, "foobar fb", "fb", 0, 4,
|
||||||
scoreMatch*2+bonusBoundary*bonusFirstCharMultiplier+
|
scoreMatch*2+int(bonusBoundaryWhite)*bonusFirstCharMultiplier+
|
||||||
scoreGapStart+scoreGapExtention)
|
scoreGapStart+scoreGapExtension)
|
||||||
assertMatch(t, FuzzyMatchV1, false, false, "foobar fb", "fb", 7, 9,
|
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) {
|
func TestExactMatchNaive(t *testing.T) {
|
||||||
@@ -113,9 +114,9 @@ func TestExactMatchNaive(t *testing.T) {
|
|||||||
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
assertMatch(t, ExactMatchNaive, false, dir, "/AutomatorDocument.icns", "rdoc", 9, 13,
|
||||||
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
scoreMatch*4+bonusCamel123+bonusConsecutive*2)
|
||||||
assertMatch(t, ExactMatchNaive, false, dir, "/man1/zshcompctl.1", "zshc", 6, 10,
|
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,
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ func TestExactMatchNaiveBackward(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrefixMatch(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} {
|
for _, dir := range []bool{true, false} {
|
||||||
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
assertMatch(t, PrefixMatch, true, dir, "fooBarbaz", "Foo", -1, -1, 0)
|
||||||
@@ -135,6 +136,10 @@ func TestPrefixMatch(t *testing.T) {
|
|||||||
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score)
|
assertMatch(t, PrefixMatch, false, dir, "fooBarbaz", "Foo", 0, 3, score)
|
||||||
assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score)
|
assertMatch(t, PrefixMatch, false, dir, "foOBarBaZ", "foo", 0, 3, score)
|
||||||
assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score)
|
assertMatch(t, PrefixMatch, false, dir, "f-oBarbaz", "f-o", 0, 3, score)
|
||||||
|
|
||||||
|
assertMatch(t, PrefixMatch, false, dir, " fooBar", "foo", 1, 4, score)
|
||||||
|
assertMatch(t, PrefixMatch, false, dir, " fooBar", " fo", 0, 3, score)
|
||||||
|
assertMatch(t, PrefixMatch, false, dir, " fo", "foo", -1, -1, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +152,14 @@ func TestSuffixMatch(t *testing.T) {
|
|||||||
scoreMatch*3+bonusConsecutive*2)
|
scoreMatch*3+bonusConsecutive*2)
|
||||||
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9,
|
assertMatch(t, SuffixMatch, false, dir, "fooBarBaZ", "baz", 6, 9,
|
||||||
(scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1))
|
(scoreMatch+bonusCamel123)*3+bonusCamel123*(bonusFirstCharMultiplier-1))
|
||||||
|
|
||||||
|
// 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+int(bonusBoundaryWhite))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,9 +183,9 @@ func TestNormalize(t *testing.T) {
|
|||||||
input, pattern, sidx, eidx, score)
|
input, pattern, sidx, eidx, score)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
test("Só Danço Samba", "So", 0, 2, 56, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
test("Só Danço Samba", "So", 0, 2, 62, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, ExactMatchNaive)
|
||||||
test("Só Danço Samba", "sodc", 0, 7, 89, FuzzyMatchV1, FuzzyMatchV2)
|
test("Só Danço Samba", "sodc", 0, 7, 97, FuzzyMatchV1, FuzzyMatchV2)
|
||||||
test("Danço", "danco", 0, 5, 128, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
test("Danço", "danco", 0, 5, 140, FuzzyMatchV1, FuzzyMatchV2, PrefixMatch, SuffixMatch, ExactMatchNaive, EqualMatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLongString(t *testing.T) {
|
func TestLongString(t *testing.T) {
|
||||||
|
|||||||
@@ -405,6 +405,74 @@ var normalized map[rune]rune = map[rune]rune{
|
|||||||
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
|
0x024E: 'Y', // WITH STROKE, LATIN CAPITAL LETTER
|
||||||
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
|
0x028F: 'Y', // , LATIN LETTER SMALL CAPITAL
|
||||||
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
|
0x1D22: 'Z', // , LATIN LETTER SMALL CAPITAL
|
||||||
|
|
||||||
|
'Ắ': 'A',
|
||||||
|
'Ấ': 'A',
|
||||||
|
'Ằ': 'A',
|
||||||
|
'Ầ': 'A',
|
||||||
|
'Ẳ': 'A',
|
||||||
|
'Ẩ': 'A',
|
||||||
|
'Ẵ': 'A',
|
||||||
|
'Ẫ': 'A',
|
||||||
|
'Ặ': 'A',
|
||||||
|
'Ậ': 'A',
|
||||||
|
|
||||||
|
'ắ': 'a',
|
||||||
|
'ấ': 'a',
|
||||||
|
'ằ': 'a',
|
||||||
|
'ầ': 'a',
|
||||||
|
'ẳ': 'a',
|
||||||
|
'ẩ': 'a',
|
||||||
|
'ẵ': 'a',
|
||||||
|
'ẫ': 'a',
|
||||||
|
'ặ': 'a',
|
||||||
|
'ậ': 'a',
|
||||||
|
|
||||||
|
'Ế': 'E',
|
||||||
|
'Ề': 'E',
|
||||||
|
'Ể': 'E',
|
||||||
|
'Ễ': 'E',
|
||||||
|
'Ệ': 'E',
|
||||||
|
|
||||||
|
'ế': 'e',
|
||||||
|
'ề': 'e',
|
||||||
|
'ể': 'e',
|
||||||
|
'ễ': 'e',
|
||||||
|
'ệ': 'e',
|
||||||
|
|
||||||
|
'Ố': 'O',
|
||||||
|
'Ớ': 'O',
|
||||||
|
'Ồ': 'O',
|
||||||
|
'Ờ': 'O',
|
||||||
|
'Ổ': 'O',
|
||||||
|
'Ở': 'O',
|
||||||
|
'Ỗ': 'O',
|
||||||
|
'Ỡ': 'O',
|
||||||
|
'Ộ': 'O',
|
||||||
|
'Ợ': 'O',
|
||||||
|
|
||||||
|
'ố': 'o',
|
||||||
|
'ớ': 'o',
|
||||||
|
'ồ': 'o',
|
||||||
|
'ờ': 'o',
|
||||||
|
'ổ': 'o',
|
||||||
|
'ở': 'o',
|
||||||
|
'ỗ': 'o',
|
||||||
|
'ỡ': 'o',
|
||||||
|
'ộ': 'o',
|
||||||
|
'ợ': 'o',
|
||||||
|
|
||||||
|
'Ứ': 'U',
|
||||||
|
'Ừ': 'U',
|
||||||
|
'Ử': 'U',
|
||||||
|
'Ữ': 'U',
|
||||||
|
'Ự': 'U',
|
||||||
|
|
||||||
|
'ứ': 'u',
|
||||||
|
'ừ': 'u',
|
||||||
|
'ử': 'u',
|
||||||
|
'ữ': 'u',
|
||||||
|
'ự': 'u',
|
||||||
}
|
}
|
||||||
|
|
||||||
// NormalizeRunes normalizes latin script letters
|
// NormalizeRunes normalizes latin script letters
|
||||||
|
|||||||
356
src/ansi.go
356
src/ansi.go
@@ -1,8 +1,6 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
@@ -19,116 +17,343 @@ type ansiState struct {
|
|||||||
fg tui.Color
|
fg tui.Color
|
||||||
bg tui.Color
|
bg tui.Color
|
||||||
attr tui.Attr
|
attr tui.Attr
|
||||||
|
lbg tui.Color
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) colored() bool {
|
func (s *ansiState) colored() bool {
|
||||||
return s.fg != -1 || s.bg != -1 || s.attr > 0
|
return s.fg != -1 || s.bg != -1 || s.attr > 0 || s.lbg >= 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ansiState) equals(t *ansiState) bool {
|
func (s *ansiState) equals(t *ansiState) bool {
|
||||||
if t == nil {
|
if t == nil {
|
||||||
return !s.colored()
|
return !s.colored()
|
||||||
}
|
}
|
||||||
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr
|
return s.fg == t.fg && s.bg == t.bg && s.attr == t.attr && s.lbg == t.lbg
|
||||||
}
|
}
|
||||||
|
|
||||||
var ansiRegex *regexp.Regexp
|
func (s *ansiState) ToString() string {
|
||||||
|
if !s.colored() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
ret := ""
|
||||||
/*
|
if s.attr&tui.Bold > 0 {
|
||||||
References:
|
ret += "1;"
|
||||||
- https://github.com/gnachman/iTerm2
|
}
|
||||||
- http://ascii-table.com/ansi-escape-sequences.php
|
if s.attr&tui.Dim > 0 {
|
||||||
- http://ascii-table.com/ansi-escape-sequences-vt-100.php
|
ret += "2;"
|
||||||
- http://tldp.org/HOWTO/Bash-Prompt-HOWTO/x405.html
|
}
|
||||||
*/
|
if s.attr&tui.Italic > 0 {
|
||||||
// The following regular expression will include not all but most of the
|
ret += "3;"
|
||||||
// frequently used ANSI sequences
|
}
|
||||||
ansiRegex = regexp.MustCompile("\x1b[\\[()][0-9;]*[a-zA-Z@]|\x1b.|[\x0e\x0f]|.\x08")
|
if s.attr&tui.Underline > 0 {
|
||||||
|
ret += "4;"
|
||||||
|
}
|
||||||
|
if s.attr&tui.Blink > 0 {
|
||||||
|
ret += "5;"
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
func toAnsiString(color tui.Color, offset int) string {
|
||||||
|
col := int(color)
|
||||||
|
ret := ""
|
||||||
|
if col == -1 {
|
||||||
|
ret += strconv.Itoa(offset + 9)
|
||||||
|
} else if col < 8 {
|
||||||
|
ret += strconv.Itoa(offset + col)
|
||||||
|
} else if col < 16 {
|
||||||
|
ret += strconv.Itoa(offset - 30 + 90 + col - 8)
|
||||||
|
} else if col < 256 {
|
||||||
|
ret += strconv.Itoa(offset+8) + ";5;" + strconv.Itoa(col)
|
||||||
|
} else if col >= (1 << 24) {
|
||||||
|
r := strconv.Itoa((col >> 16) & 0xff)
|
||||||
|
g := strconv.Itoa((col >> 8) & 0xff)
|
||||||
|
b := strconv.Itoa(col & 0xff)
|
||||||
|
ret += strconv.Itoa(offset+8) + ";2;" + r + ";" + g + ";" + b
|
||||||
|
}
|
||||||
|
return ret + ";"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPrint(c uint8) bool {
|
||||||
|
return '\x20' <= c && c <= '\x7e'
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchOperatingSystemCommand(s string) int {
|
||||||
|
// `\x1b][0-9][;:][[:print:]]+(?:\x1b\\\\|\x07)`
|
||||||
|
// ^ match starting here
|
||||||
|
//
|
||||||
|
i := 5 // prefix matched in nextAnsiEscapeSequence()
|
||||||
|
for ; i < len(s) && isPrint(s[i]); i++ {
|
||||||
|
}
|
||||||
|
if i < len(s) {
|
||||||
|
if s[i] == '\x07' {
|
||||||
|
return i + 1
|
||||||
|
}
|
||||||
|
if s[i] == '\x1b' && i < len(s)-1 && s[i+1] == '\\' {
|
||||||
|
return i + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchControlSequence(s string) int {
|
||||||
|
// `\x1b[\\[()][0-9;:?]*[a-zA-Z@]`
|
||||||
|
// ^ match starting here
|
||||||
|
//
|
||||||
|
i := 2 // prefix matched in nextAnsiEscapeSequence()
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
c := s[i]
|
||||||
|
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 {
|
||||||
|
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)"
|
||||||
|
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':
|
||||||
|
// 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.
|
||||||
|
goto Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, -1
|
||||||
|
|
||||||
|
Loop:
|
||||||
|
for ; i < len(s); i++ {
|
||||||
|
switch s[i] {
|
||||||
|
case '\x08':
|
||||||
|
// backtrack to match: `.\x08`
|
||||||
|
if i > 0 && s[i-1] != '\n' {
|
||||||
|
if s[i-1] < utf8.RuneSelf {
|
||||||
|
return i - 1, i + 1
|
||||||
|
}
|
||||||
|
_, n := utf8.DecodeLastRuneInString(s[:i])
|
||||||
|
return i - n, i + 1
|
||||||
|
}
|
||||||
|
case '\x1b':
|
||||||
|
// 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] == ';' || s[i+3] == ':') && isPrint(s[i+4]) {
|
||||||
|
|
||||||
|
if j := matchOperatingSystemCommand(s[i:]); j != -1 {
|
||||||
|
return i, i + j
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// match: `\x1b.`
|
||||||
|
if i+1 < len(s) && s[i+1] != '\n' {
|
||||||
|
if s[i+1] < utf8.RuneSelf {
|
||||||
|
return i, i + 2
|
||||||
|
}
|
||||||
|
_, n := utf8.DecodeRuneInString(s[i+1:])
|
||||||
|
return i, i + n + 1
|
||||||
|
}
|
||||||
|
case '\x0e', '\x0f':
|
||||||
|
// match: `[\x0e\x0f]`
|
||||||
|
return i, i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
func extractColor(str string, state *ansiState, proc func(string, *ansiState) bool) (string, *[]ansiOffset, *ansiState) {
|
||||||
var offsets []ansiOffset
|
// We append to a stack allocated variable that we'll
|
||||||
var output bytes.Buffer
|
// later copy and return, to save on allocations.
|
||||||
|
offsets := make([]ansiOffset, 0, 32)
|
||||||
|
|
||||||
if state != nil {
|
if state != nil {
|
||||||
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
offsets = append(offsets, ansiOffset{[2]int32{0, 0}, *state})
|
||||||
}
|
}
|
||||||
|
|
||||||
idx := 0
|
var (
|
||||||
for _, offset := range ansiRegex.FindAllStringIndex(str, -1) {
|
pstate *ansiState // lazily allocated
|
||||||
prev := str[idx:offset[0]]
|
output strings.Builder
|
||||||
output.WriteString(prev)
|
prevIdx int
|
||||||
|
runeCount int
|
||||||
|
)
|
||||||
|
for idx := 0; idx < len(str); {
|
||||||
|
// Make sure that we found an ANSI code
|
||||||
|
start, end := nextAnsiEscapeSequence(str[idx:])
|
||||||
|
if start == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start += idx
|
||||||
|
idx += end
|
||||||
|
|
||||||
|
// Check if we should continue
|
||||||
|
prev := str[prevIdx:start]
|
||||||
if proc != nil && !proc(prev, state) {
|
if proc != nil && !proc(prev, state) {
|
||||||
return "", nil, nil
|
return "", nil, nil
|
||||||
}
|
}
|
||||||
newState := interpretCode(str[offset[0]:offset[1]], state)
|
prevIdx = idx
|
||||||
|
|
||||||
|
if len(prev) != 0 {
|
||||||
|
runeCount += utf8.RuneCountInString(prev)
|
||||||
|
// Grow the buffer size to the maximum possible length (string length
|
||||||
|
// containing ansi codes) to avoid repetitive allocation
|
||||||
|
if output.Cap() == 0 {
|
||||||
|
output.Grow(len(str))
|
||||||
|
}
|
||||||
|
output.WriteString(prev)
|
||||||
|
}
|
||||||
|
|
||||||
|
newState := interpretCode(str[start:idx], state)
|
||||||
if !newState.equals(state) {
|
if !newState.equals(state) {
|
||||||
if state != nil {
|
if state != nil {
|
||||||
// Update last offset
|
// Update last offset
|
||||||
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
|
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
if newState.colored() {
|
if newState.colored() {
|
||||||
// Append new offset
|
// Append new offset
|
||||||
state = newState
|
if pstate == nil {
|
||||||
newLen := int32(utf8.RuneCount(output.Bytes()))
|
pstate = &ansiState{}
|
||||||
offsets = append(offsets, ansiOffset{[2]int32{newLen, newLen}, *state})
|
}
|
||||||
|
*pstate = newState
|
||||||
|
state = pstate
|
||||||
|
offsets = append(offsets, ansiOffset{
|
||||||
|
[2]int32{int32(runeCount), int32(runeCount)},
|
||||||
|
newState,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// Discard state
|
// Discard state
|
||||||
state = nil
|
state = nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
idx = offset[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
rest := str[idx:]
|
var rest string
|
||||||
if len(rest) > 0 {
|
var trimmed string
|
||||||
|
if prevIdx == 0 {
|
||||||
|
// No ANSI code found
|
||||||
|
rest = str
|
||||||
|
trimmed = str
|
||||||
|
} else {
|
||||||
|
rest = str[prevIdx:]
|
||||||
output.WriteString(rest)
|
output.WriteString(rest)
|
||||||
if state != nil {
|
trimmed = output.String()
|
||||||
// Update last offset
|
|
||||||
(&offsets[len(offsets)-1]).offset[1] = int32(utf8.RuneCount(output.Bytes()))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if proc != nil {
|
if proc != nil {
|
||||||
proc(rest, state)
|
proc(rest, state)
|
||||||
}
|
}
|
||||||
if len(offsets) == 0 {
|
if len(offsets) > 0 {
|
||||||
return output.String(), nil, state
|
if len(rest) > 0 && state != nil {
|
||||||
|
// Update last offset
|
||||||
|
runeCount += utf8.RuneCountInString(rest)
|
||||||
|
(&offsets[len(offsets)-1]).offset[1] = int32(runeCount)
|
||||||
}
|
}
|
||||||
return output.String(), &offsets, state
|
// Return a copy of the offsets slice
|
||||||
|
a := make([]ansiOffset, len(offsets))
|
||||||
|
copy(a, offsets)
|
||||||
|
return trimmed, &a, state
|
||||||
|
}
|
||||||
|
return trimmed, nil, state
|
||||||
}
|
}
|
||||||
|
|
||||||
func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
func parseAnsiCode(s string, delimiter byte) (int, byte, string) {
|
||||||
// State
|
var remaining string
|
||||||
var state *ansiState
|
i := -1
|
||||||
if prevState == nil {
|
if delimiter == 0 {
|
||||||
state = &ansiState{-1, -1, 0}
|
// Faster than strings.IndexAny(";:")
|
||||||
|
i = strings.IndexByte(s, ';')
|
||||||
|
if i < 0 {
|
||||||
|
i = strings.IndexByte(s, ':')
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
state = &ansiState{prevState.fg, prevState.bg, prevState.attr}
|
i = strings.IndexByte(s, delimiter)
|
||||||
|
}
|
||||||
|
if i >= 0 {
|
||||||
|
delimiter = s[i]
|
||||||
|
remaining = s[i+1:]
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s) > 0 {
|
||||||
|
// Inlined version of strconv.Atoi() that only handles positive
|
||||||
|
// integers and does not allocate on error.
|
||||||
|
code := 0
|
||||||
|
for _, ch := range []byte(s) {
|
||||||
|
ch -= '0'
|
||||||
|
if ch > 9 {
|
||||||
|
return -1, delimiter, remaining
|
||||||
|
}
|
||||||
|
code = code*10 + int(ch)
|
||||||
|
}
|
||||||
|
return code, delimiter, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1, delimiter, remaining
|
||||||
|
}
|
||||||
|
|
||||||
|
func interpretCode(ansiCode string, prevState *ansiState) ansiState {
|
||||||
|
var state ansiState
|
||||||
|
if prevState == nil {
|
||||||
|
state = ansiState{-1, -1, 0, -1}
|
||||||
|
} else {
|
||||||
|
state = ansiState{prevState.fg, prevState.bg, prevState.attr, prevState.lbg}
|
||||||
}
|
}
|
||||||
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
if ansiCode[0] != '\x1b' || ansiCode[1] != '[' || ansiCode[len(ansiCode)-1] != 'm' {
|
||||||
|
if prevState != nil && strings.HasSuffix(ansiCode, "0K") {
|
||||||
|
state.lbg = prevState.bg
|
||||||
|
}
|
||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
ptr := &state.fg
|
if len(ansiCode) <= 3 {
|
||||||
state256 := 0
|
|
||||||
|
|
||||||
init := func() {
|
|
||||||
state.fg = -1
|
state.fg = -1
|
||||||
state.bg = -1
|
state.bg = -1
|
||||||
state.attr = 0
|
state.attr = 0
|
||||||
state256 = 0
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
ansiCode = ansiCode[2 : len(ansiCode)-1]
|
||||||
if len(ansiCode) == 0 {
|
|
||||||
init()
|
state256 := 0
|
||||||
}
|
ptr := &state.fg
|
||||||
for _, code := range strings.Split(ansiCode, ";") {
|
|
||||||
if num, err := strconv.Atoi(code); err == nil {
|
var delimiter byte = 0
|
||||||
|
for len(ansiCode) != 0 {
|
||||||
|
var num int
|
||||||
|
if num, delimiter, ansiCode = parseAnsiCode(ansiCode, delimiter); num != -1 {
|
||||||
switch state256 {
|
switch state256 {
|
||||||
case 0:
|
case 0:
|
||||||
switch num {
|
switch num {
|
||||||
@@ -154,8 +379,26 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
|||||||
state.attr = state.attr | tui.Blink
|
state.attr = state.attr | tui.Blink
|
||||||
case 7:
|
case 7:
|
||||||
state.attr = state.attr | tui.Reverse
|
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:
|
case 0:
|
||||||
init()
|
state.fg = -1
|
||||||
|
state.bg = -1
|
||||||
|
state.attr = 0
|
||||||
|
state256 = 0
|
||||||
default:
|
default:
|
||||||
if num >= 30 && num <= 37 {
|
if num >= 30 && num <= 37 {
|
||||||
state.fg = tui.Color(num - 30)
|
state.fg = tui.Color(num - 30)
|
||||||
@@ -191,6 +434,7 @@ func interpretCode(ansiCode string, prevState *ansiState) *ansiState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state256 > 0 {
|
if state256 > 0 {
|
||||||
*ptr = -1
|
*ptr = -1
|
||||||
}
|
}
|
||||||
|
|||||||
276
src/ansi_test.go
276
src/ansi_test.go
@@ -1,12 +1,192 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"math/rand"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The following regular expression will include not all but most of the
|
||||||
|
// frequently used ANSI sequences. This regex is used as a reference for
|
||||||
|
// 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)")
|
||||||
|
|
||||||
|
func testParserReference(t testing.TB, str string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
toSlice := func(start, end int) []int {
|
||||||
|
if start == -1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return []int{start, end}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := str
|
||||||
|
for i := 0; ; i++ {
|
||||||
|
got := toSlice(nextAnsiEscapeSequence(s))
|
||||||
|
exp := ansiRegexReference.FindStringIndex(s)
|
||||||
|
|
||||||
|
equal := len(got) == len(exp)
|
||||||
|
if equal {
|
||||||
|
for i := 0; i < len(got); i++ {
|
||||||
|
if got[i] != exp[i] {
|
||||||
|
equal = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !equal {
|
||||||
|
var exps, gots []rune
|
||||||
|
if len(got) == 2 {
|
||||||
|
gots = []rune(s[got[0]:got[1]])
|
||||||
|
}
|
||||||
|
if len(exp) == 2 {
|
||||||
|
exps = []rune(s[exp[0]:exp[1]])
|
||||||
|
}
|
||||||
|
t.Errorf("%d: %q: got: %v (%q) want: %v (%q)", i, s, got, gots, exp, exps)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(exp) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s = s[exp[1]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextAnsiEscapeSequence(t *testing.T) {
|
||||||
|
testStrs := []string{
|
||||||
|
"\x1b[0mhello world",
|
||||||
|
"\x1b[1mhello world",
|
||||||
|
"椙\x1b[1m椙",
|
||||||
|
"椙\x1b[1椙m椙",
|
||||||
|
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||||
|
"\x1b[1mhello \x1b[Kworld",
|
||||||
|
"hello \x1b[34;45;1mworld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||||
|
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||||
|
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||||
|
"hello \x1b[32;1mworld",
|
||||||
|
"hello world",
|
||||||
|
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||||
|
"\x1b椙",
|
||||||
|
"椙\x08",
|
||||||
|
"\n\x08",
|
||||||
|
"X\x08",
|
||||||
|
"",
|
||||||
|
"\x1b]4;3;rgb:aa/bb/cc\x07 ",
|
||||||
|
"\x1b]4;3;rgb:aa/bb/cc\x1b\\ ",
|
||||||
|
ansiBenchmarkString,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, s := range testStrs {
|
||||||
|
testParserReference(t, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextAnsiEscapeSequence_Fuzz_Modified(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("short test")
|
||||||
|
}
|
||||||
|
|
||||||
|
testStrs := []string{
|
||||||
|
"\x1b[0mhello world",
|
||||||
|
"\x1b[1mhello world",
|
||||||
|
"椙\x1b[1m椙",
|
||||||
|
"椙\x1b[1椙m椙",
|
||||||
|
"\x1b[1mhello \x1b[mw\x1b7o\x1b8r\x1b(Bl\x1b[2@d",
|
||||||
|
"\x1b[1mhello \x1b[Kworld",
|
||||||
|
"hello \x1b[34;45;1mworld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[34;45;1mld",
|
||||||
|
"hello \x1b[34;45;1mwor\x1b[0mld",
|
||||||
|
"hello \x1b[34;48;5;233;1mwo\x1b[38;5;161mr\x1b[0ml\x1b[38;5;161md",
|
||||||
|
"hello \x1b[38;5;38;48;5;48;1mwor\x1b[38;5;48;48;5;38ml\x1b[0md",
|
||||||
|
"hello \x1b[32;1mworld",
|
||||||
|
"hello world",
|
||||||
|
"hello \x1b[0;38;5;200;48;5;100mworld",
|
||||||
|
ansiBenchmarkString,
|
||||||
|
}
|
||||||
|
|
||||||
|
replacementBytes := [...]rune{'\x0e', '\x0f', '\x1b', '\x08'}
|
||||||
|
|
||||||
|
modifyString := func(s string, rr *rand.Rand) string {
|
||||||
|
n := rr.Intn(len(s))
|
||||||
|
b := []rune(s)
|
||||||
|
for ; n >= 0 && len(b) != 0; n-- {
|
||||||
|
i := rr.Intn(len(b))
|
||||||
|
switch x := rr.Intn(4); x {
|
||||||
|
case 0:
|
||||||
|
b = append(b[:i], b[i+1:]...)
|
||||||
|
case 1:
|
||||||
|
j := rr.Intn(len(replacementBytes) - 1)
|
||||||
|
b[i] = replacementBytes[j]
|
||||||
|
case 2:
|
||||||
|
x := rune(rr.Intn(utf8.MaxRune))
|
||||||
|
for !utf8.ValidRune(x) {
|
||||||
|
x = rune(rr.Intn(utf8.MaxRune))
|
||||||
|
}
|
||||||
|
b[i] = x
|
||||||
|
case 3:
|
||||||
|
b[i] = rune(rr.Intn(utf8.MaxRune)) // potentially invalid
|
||||||
|
default:
|
||||||
|
t.Fatalf("unsupported value: %d", x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := rand.New(rand.NewSource(1))
|
||||||
|
for _, s := range testStrs {
|
||||||
|
for i := 1_000; i >= 0; i-- {
|
||||||
|
testParserReference(t, modifyString(s, rr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNextAnsiEscapeSequence_Fuzz_Random(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("short test")
|
||||||
|
}
|
||||||
|
|
||||||
|
randomString := func(rr *rand.Rand) string {
|
||||||
|
numChars := rand.Intn(50)
|
||||||
|
codePoints := make([]rune, numChars)
|
||||||
|
for i := 0; i < len(codePoints); i++ {
|
||||||
|
var r rune
|
||||||
|
for n := 0; n < 1000; n++ {
|
||||||
|
r = rune(rr.Intn(utf8.MaxRune))
|
||||||
|
// Allow 10% of runes to be invalid
|
||||||
|
if utf8.ValidRune(r) || rr.Float64() < 0.10 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
codePoints[i] = r
|
||||||
|
}
|
||||||
|
return string(codePoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := rand.New(rand.NewSource(1))
|
||||||
|
for i := 0; i < 100_000; i++ {
|
||||||
|
testParserReference(t, randomString(rr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExtractColor(t *testing.T) {
|
func TestExtractColor(t *testing.T) {
|
||||||
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
assert := func(offset ansiOffset, b int32, e int32, fg tui.Color, bg tui.Color, bold bool) {
|
||||||
var attr tui.Attr
|
var attr tui.Attr
|
||||||
@@ -26,9 +206,9 @@ func TestExtractColor(t *testing.T) {
|
|||||||
output, ansiOffsets, newState := extractColor(src, state, nil)
|
output, ansiOffsets, newState := extractColor(src, state, nil)
|
||||||
state = newState
|
state = newState
|
||||||
if output != "hello world" {
|
if output != "hello world" {
|
||||||
t.Errorf("Invalid output: %s %s", output, []rune(output))
|
t.Errorf("Invalid output: %s %v", output, []rune(output))
|
||||||
}
|
}
|
||||||
fmt.Println(src, ansiOffsets, clean)
|
t.Log(src, ansiOffsets, clean)
|
||||||
assertion(ansiOffsets, state)
|
assertion(ansiOffsets, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,3 +336,93 @@ func TestExtractColor(t *testing.T) {
|
|||||||
assert((*offsets)[1], 6, 11, 200, 100, false)
|
assert((*offsets)[1], 6, 11, 200, 100, false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAnsiCodeStringConversion(t *testing.T) {
|
||||||
|
assert := func(code string, prevState *ansiState, expected string) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert("\x1b[m", nil, "")
|
||||||
|
assert("\x1b[m", &ansiState{attr: tui.Blink, lbg: -1}, "")
|
||||||
|
|
||||||
|
assert("\x1b[31m", nil, "\x1b[31;49m")
|
||||||
|
assert("\x1b[41m", nil, "\x1b[39;41m")
|
||||||
|
|
||||||
|
assert("\x1b[92m", nil, "\x1b[92;49m")
|
||||||
|
assert("\x1b[102m", nil, "\x1b[39;102m")
|
||||||
|
|
||||||
|
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",
|
||||||
|
&ansiState{attr: tui.Dim | tui.Italic, fg: 1, bg: 1},
|
||||||
|
"\x1b[2;3;7;38;2;10;20;30;48;5;100m")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseAnsiCode(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
In, Exp string
|
||||||
|
N int
|
||||||
|
}{
|
||||||
|
{"123", "", 123},
|
||||||
|
{"1a", "", -1},
|
||||||
|
{"1a;12", "12", -1},
|
||||||
|
{"12;a", "a", 12},
|
||||||
|
{"-2", "", -1},
|
||||||
|
}
|
||||||
|
for _, x := range tests {
|
||||||
|
n, _, s := parseAnsiCode(x.In, 0)
|
||||||
|
if n != x.N || s != x.Exp {
|
||||||
|
t.Fatalf("%q: got: (%d %q) want: (%d %q)", x.In, n, s, x.N, x.Exp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
func BenchmarkNextAnsiEscapeSequence(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := ansiBenchmarkString
|
||||||
|
for {
|
||||||
|
_, o := nextAnsiEscapeSequence(s)
|
||||||
|
if o == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s = s[o:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Baseline test to compare the speed of nextAnsiEscapeSequence() to the
|
||||||
|
// previously used regex based implementation.
|
||||||
|
func BenchmarkNextAnsiEscapeSequence_Regex(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := ansiBenchmarkString
|
||||||
|
for {
|
||||||
|
a := ansiRegexReference.FindStringIndex(s)
|
||||||
|
if len(a) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
s = s[a[1]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkExtractColor(b *testing.B) {
|
||||||
|
b.SetBytes(int64(len(ansiBenchmarkString)))
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
extractColor(ansiBenchmarkString, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
42
src/cache.go
42
src/cache.go
@@ -3,7 +3,7 @@ package fzf
|
|||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
// queryCache associates strings to lists of items
|
// queryCache associates strings to lists of items
|
||||||
type queryCache map[string][]*Result
|
type queryCache map[string][]Result
|
||||||
|
|
||||||
// ChunkCache associates Chunk and query string to lists of items
|
// ChunkCache associates Chunk and query string to lists of items
|
||||||
type ChunkCache struct {
|
type ChunkCache struct {
|
||||||
@@ -17,7 +17,7 @@ func NewChunkCache() ChunkCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add adds the list to the cache
|
// Add adds the list to the cache
|
||||||
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
func (cc *ChunkCache) Add(chunk *Chunk, key string, list []Result) {
|
||||||
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
if len(key) == 0 || !chunk.IsFull() || len(list) > queryCacheMax {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -33,10 +33,10 @@ func (cc *ChunkCache) Add(chunk *Chunk, key string, list []*Result) {
|
|||||||
(*qc)[key] = list
|
(*qc)[key] = list
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find is called to lookup ChunkCache
|
// Lookup is called to lookup ChunkCache
|
||||||
func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
|
func (cc *ChunkCache) Lookup(chunk *Chunk, key string) []Result {
|
||||||
if len(key) == 0 || !chunk.IsFull() {
|
if len(key) == 0 || !chunk.IsFull() {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cc.mutex.Lock()
|
cc.mutex.Lock()
|
||||||
@@ -46,8 +46,36 @@ func (cc *ChunkCache) Find(chunk *Chunk, key string) ([]*Result, bool) {
|
|||||||
if ok {
|
if ok {
|
||||||
list, ok := (*qc)[key]
|
list, ok := (*qc)[key]
|
||||||
if ok {
|
if ok {
|
||||||
return list, true
|
return list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, false
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cc *ChunkCache) Search(chunk *Chunk, key string) []Result {
|
||||||
|
if len(key) == 0 || !chunk.IsFull() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cc.mutex.Lock()
|
||||||
|
defer cc.mutex.Unlock()
|
||||||
|
|
||||||
|
qc, ok := cc.cache[chunk]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 1; idx < len(key); idx++ {
|
||||||
|
// [---------| ] | [ |---------]
|
||||||
|
// [--------| ] | [ |--------]
|
||||||
|
// [-------| ] | [ |-------]
|
||||||
|
prefix := key[:len(key)-idx]
|
||||||
|
suffix := key[idx:]
|
||||||
|
for _, substr := range [2]string{prefix, suffix} {
|
||||||
|
if cached, found := (*qc)[substr]; found {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,37 +4,36 @@ import "testing"
|
|||||||
|
|
||||||
func TestChunkCache(t *testing.T) {
|
func TestChunkCache(t *testing.T) {
|
||||||
cache := NewChunkCache()
|
cache := NewChunkCache()
|
||||||
chunk2 := make(Chunk, chunkSize)
|
|
||||||
chunk1p := &Chunk{}
|
chunk1p := &Chunk{}
|
||||||
chunk2p := &chunk2
|
chunk2p := &Chunk{count: chunkSize}
|
||||||
items1 := []*Result{&Result{}}
|
items1 := []Result{{}}
|
||||||
items2 := []*Result{&Result{}, &Result{}}
|
items2 := []Result{{}, {}}
|
||||||
cache.Add(chunk1p, "foo", items1)
|
cache.Add(chunk1p, "foo", items1)
|
||||||
cache.Add(chunk2p, "foo", items1)
|
cache.Add(chunk2p, "foo", items1)
|
||||||
cache.Add(chunk2p, "bar", items2)
|
cache.Add(chunk2p, "bar", items2)
|
||||||
|
|
||||||
{ // chunk1 is not full
|
{ // chunk1 is not full
|
||||||
cached, found := cache.Find(chunk1p, "foo")
|
cached := cache.Lookup(chunk1p, "foo")
|
||||||
if found {
|
if cached != nil {
|
||||||
t.Error("Cached disabled for non-empty chunks", found, cached)
|
t.Error("Cached disabled for non-empty chunks", cached)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cached, found := cache.Find(chunk2p, "foo")
|
cached := cache.Lookup(chunk2p, "foo")
|
||||||
if !found || len(cached) != 1 {
|
if cached == nil || len(cached) != 1 {
|
||||||
t.Error("Expected 1 item cached", found, cached)
|
t.Error("Expected 1 item cached", cached)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cached, found := cache.Find(chunk2p, "bar")
|
cached := cache.Lookup(chunk2p, "bar")
|
||||||
if !found || len(cached) != 2 {
|
if cached == nil || len(cached) != 2 {
|
||||||
t.Error("Expected 2 items cached", found, cached)
|
t.Error("Expected 2 items cached", cached)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
cached, found := cache.Find(chunk1p, "foobar")
|
cached := cache.Lookup(chunk1p, "foobar")
|
||||||
if found {
|
if cached != nil {
|
||||||
t.Error("Expected 0 item cached", found, cached)
|
t.Error("Expected 0 item cached", cached)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,17 +2,18 @@ package fzf
|
|||||||
|
|
||||||
import "sync"
|
import "sync"
|
||||||
|
|
||||||
// Chunk is a list of Item pointers whose size has the upper limit of chunkSize
|
// Chunk is a list of Items whose size has the upper limit of chunkSize
|
||||||
type Chunk []*Item // >>> []Item
|
type Chunk struct {
|
||||||
|
items [chunkSize]Item
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
// ItemBuilder is a closure type that builds Item object from a pointer to a
|
// ItemBuilder is a closure type that builds Item object from byte array
|
||||||
// string and an integer
|
type ItemBuilder func(*Item, []byte) bool
|
||||||
type ItemBuilder func([]byte, int) *Item
|
|
||||||
|
|
||||||
// ChunkList is a list of Chunks
|
// ChunkList is a list of Chunks
|
||||||
type ChunkList struct {
|
type ChunkList struct {
|
||||||
chunks []*Chunk
|
chunks []*Chunk
|
||||||
count int
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
trans ItemBuilder
|
trans ItemBuilder
|
||||||
}
|
}
|
||||||
@@ -21,15 +22,13 @@ type ChunkList struct {
|
|||||||
func NewChunkList(trans ItemBuilder) *ChunkList {
|
func NewChunkList(trans ItemBuilder) *ChunkList {
|
||||||
return &ChunkList{
|
return &ChunkList{
|
||||||
chunks: []*Chunk{},
|
chunks: []*Chunk{},
|
||||||
count: 0,
|
|
||||||
mutex: sync.Mutex{},
|
mutex: sync.Mutex{},
|
||||||
trans: trans}
|
trans: trans}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
|
func (c *Chunk) push(trans ItemBuilder, data []byte) bool {
|
||||||
item := trans(data, index)
|
if trans(&c.items[c.count], data) {
|
||||||
if item != nil {
|
c.count++
|
||||||
*c = append(*c, item)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -37,7 +36,7 @@ func (c *Chunk) push(trans ItemBuilder, data []byte, index int) bool {
|
|||||||
|
|
||||||
// IsFull returns true if the Chunk is full
|
// IsFull returns true if the Chunk is full
|
||||||
func (c *Chunk) IsFull() bool {
|
func (c *Chunk) IsFull() bool {
|
||||||
return len(*c) == chunkSize
|
return c.count == chunkSize
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cl *ChunkList) lastChunk() *Chunk {
|
func (cl *ChunkList) lastChunk() *Chunk {
|
||||||
@@ -49,45 +48,42 @@ func CountItems(cs []*Chunk) int {
|
|||||||
if len(cs) == 0 {
|
if len(cs) == 0 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return chunkSize*(len(cs)-1) + len(*(cs[len(cs)-1]))
|
return chunkSize*(len(cs)-1) + cs[len(cs)-1].count
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push adds the item to the list
|
// Push adds the item to the list
|
||||||
func (cl *ChunkList) Push(data []byte) bool {
|
func (cl *ChunkList) Push(data []byte) bool {
|
||||||
cl.mutex.Lock()
|
cl.mutex.Lock()
|
||||||
defer cl.mutex.Unlock()
|
|
||||||
|
|
||||||
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
|
if len(cl.chunks) == 0 || cl.lastChunk().IsFull() {
|
||||||
newChunk := Chunk(make([]*Item, 0, chunkSize))
|
cl.chunks = append(cl.chunks, &Chunk{})
|
||||||
cl.chunks = append(cl.chunks, &newChunk)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if cl.lastChunk().push(cl.trans, data, cl.count) {
|
ret := cl.lastChunk().push(cl.trans, data)
|
||||||
cl.count++
|
cl.mutex.Unlock()
|
||||||
return true
|
return ret
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
// Clear clears the data
|
||||||
|
func (cl *ChunkList) Clear() {
|
||||||
|
cl.mutex.Lock()
|
||||||
|
cl.chunks = nil
|
||||||
|
cl.mutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot returns immutable snapshot of the ChunkList
|
// Snapshot returns immutable snapshot of the ChunkList
|
||||||
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
func (cl *ChunkList) Snapshot() ([]*Chunk, int) {
|
||||||
cl.mutex.Lock()
|
cl.mutex.Lock()
|
||||||
defer cl.mutex.Unlock()
|
|
||||||
|
|
||||||
ret := make([]*Chunk, len(cl.chunks))
|
ret := make([]*Chunk, len(cl.chunks))
|
||||||
copy(ret, cl.chunks)
|
copy(ret, cl.chunks)
|
||||||
|
|
||||||
// Duplicate the last chunk
|
// Duplicate the last chunk
|
||||||
if cnt := len(ret); cnt > 0 {
|
if cnt := len(ret); cnt > 0 {
|
||||||
ret[cnt-1] = ret[cnt-1].dupe()
|
newChunk := *ret[cnt-1]
|
||||||
}
|
ret[cnt-1] = &newChunk
|
||||||
return ret, cl.count
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Chunk) dupe() *Chunk {
|
cl.mutex.Unlock()
|
||||||
newChunk := make(Chunk, len(*c))
|
return ret, CountItems(ret)
|
||||||
for idx, ptr := range *c {
|
|
||||||
newChunk[idx] = ptr
|
|
||||||
}
|
|
||||||
return &newChunk
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ func TestChunkList(t *testing.T) {
|
|||||||
// FIXME global
|
// FIXME global
|
||||||
sortCriteria = []criterion{byScore, byLength}
|
sortCriteria = []criterion{byScore, byLength}
|
||||||
|
|
||||||
cl := NewChunkList(func(s []byte, i int) *Item {
|
cl := NewChunkList(func(item *Item, s []byte) bool {
|
||||||
return &Item{text: util.ToChars(s), index: int32(i * 2)}
|
item.text = util.ToChars(s)
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Snapshot
|
// Snapshot
|
||||||
@@ -38,11 +39,11 @@ func TestChunkList(t *testing.T) {
|
|||||||
|
|
||||||
// Check the content of the ChunkList
|
// Check the content of the ChunkList
|
||||||
chunk1 := snapshot[0]
|
chunk1 := snapshot[0]
|
||||||
if len(*chunk1) != 2 {
|
if chunk1.count != 2 {
|
||||||
t.Error("Snapshot should contain only two items")
|
t.Error("Snapshot should contain only two items")
|
||||||
}
|
}
|
||||||
if (*chunk1)[0].text.ToString() != "hello" || (*chunk1)[0].index != 0 ||
|
if chunk1.items[0].text.ToString() != "hello" ||
|
||||||
(*chunk1)[1].text.ToString() != "world" || (*chunk1)[1].index != 2 {
|
chunk1.items[1].text.ToString() != "world" {
|
||||||
t.Error("Invalid data")
|
t.Error("Invalid data")
|
||||||
}
|
}
|
||||||
if chunk1.IsFull() {
|
if chunk1.IsFull() {
|
||||||
@@ -65,14 +66,14 @@ func TestChunkList(t *testing.T) {
|
|||||||
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
!snapshot[1].IsFull() || snapshot[2].IsFull() || count != chunkSize*2+2 {
|
||||||
t.Error("Expected two full chunks and one more chunk")
|
t.Error("Expected two full chunks and one more chunk")
|
||||||
}
|
}
|
||||||
if len(*snapshot[2]) != 2 {
|
if snapshot[2].count != 2 {
|
||||||
t.Error("Unexpected number of items")
|
t.Error("Unexpected number of items")
|
||||||
}
|
}
|
||||||
|
|
||||||
cl.Push([]byte("hello"))
|
cl.Push([]byte("hello"))
|
||||||
cl.Push([]byte("world"))
|
cl.Push([]byte("world"))
|
||||||
|
|
||||||
lastChunkCount := len(*snapshot[len(snapshot)-1])
|
lastChunkCount := snapshot[len(snapshot)-1].count
|
||||||
if lastChunkCount != 2 {
|
if lastChunkCount != 2 {
|
||||||
t.Error("Unexpected number of items:", lastChunkCount)
|
t.Error("Unexpected number of items:", lastChunkCount)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -8,20 +9,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Current version
|
|
||||||
version = "0.16.8"
|
|
||||||
|
|
||||||
// Core
|
// Core
|
||||||
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
coordinatorDelayMax time.Duration = 100 * time.Millisecond
|
||||||
coordinatorDelayStep time.Duration = 10 * time.Millisecond
|
coordinatorDelayStep time.Duration = 10 * time.Millisecond
|
||||||
|
|
||||||
// Reader
|
// Reader
|
||||||
readerBufferSize = 64 * 1024
|
readerBufferSize = 64 * 1024
|
||||||
|
readerPollIntervalMin = 10 * time.Millisecond
|
||||||
|
readerPollIntervalStep = 5 * time.Millisecond
|
||||||
|
readerPollIntervalMax = 50 * time.Millisecond
|
||||||
|
|
||||||
// Terminal
|
// Terminal
|
||||||
initialDelay = 20 * time.Millisecond
|
initialDelay = 20 * time.Millisecond
|
||||||
initialDelayTac = 100 * time.Millisecond
|
initialDelayTac = 100 * time.Millisecond
|
||||||
spinnerDuration = 200 * time.Millisecond
|
spinnerDuration = 100 * time.Millisecond
|
||||||
|
previewCancelWait = 500 * time.Millisecond
|
||||||
|
previewChunkDelay = 100 * time.Millisecond
|
||||||
|
previewDelayed = 500 * time.Millisecond
|
||||||
|
maxPatternLength = 300
|
||||||
|
maxMulti = math.MaxInt32
|
||||||
|
|
||||||
// Matcher
|
// Matcher
|
||||||
numPartitionsMultiplier = 8
|
numPartitionsMultiplier = 8
|
||||||
@@ -52,11 +58,9 @@ var defaultCommand string
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
if !util.IsWindows() {
|
if !util.IsWindows() {
|
||||||
defaultCommand = `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-`
|
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" {
|
} 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-"`
|
defaultCommand = `sh -c "command find -L . -mindepth 1 -path '*/.*' -prune -o -type f -print -o -type l -print 2> /dev/null | cut -b3-"`
|
||||||
} else {
|
|
||||||
defaultCommand = `dir /s/b`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,10 +72,12 @@ const (
|
|||||||
EvtSearchProgress
|
EvtSearchProgress
|
||||||
EvtSearchFin
|
EvtSearchFin
|
||||||
EvtHeader
|
EvtHeader
|
||||||
EvtClose
|
EvtReady
|
||||||
|
EvtQuit
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
exitCancel = -1
|
||||||
exitOk = 0
|
exitOk = 0
|
||||||
exitNoMatch = 1
|
exitNoMatch = 1
|
||||||
exitError = 2
|
exitError = 2
|
||||||
|
|||||||
276
src/core.go
276
src/core.go
@@ -1,28 +1,4 @@
|
|||||||
/*
|
// Package fzf implements fzf, a command-line fuzzy finder.
|
||||||
Package fzf implements fzf, a command-line fuzzy finder.
|
|
||||||
|
|
||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2017 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
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -43,7 +19,7 @@ Matcher -> EvtHeader -> Terminal (update header)
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Run starts fzf
|
// Run starts fzf
|
||||||
func Run(opts *Options, revision string) {
|
func Run(opts *Options, version string, revision string) {
|
||||||
sort := opts.Sort > 0
|
sort := opts.Sort > 0
|
||||||
sortCriteria = opts.Criteria
|
sortCriteria = opts.Criteria
|
||||||
|
|
||||||
@@ -63,96 +39,108 @@ func Run(opts *Options, revision string) {
|
|||||||
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
|
ansiProcessor := func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||||
return util.ToChars(data), nil
|
return util.ToChars(data), nil
|
||||||
}
|
}
|
||||||
ansiProcessorRunes := func(data []rune) (util.Chars, *[]ansiOffset) {
|
|
||||||
return util.RunesToChars(data), nil
|
var lineAnsiState, prevLineAnsiState *ansiState
|
||||||
}
|
|
||||||
if opts.Ansi {
|
if opts.Ansi {
|
||||||
if opts.Theme != nil {
|
if opts.Theme.Colored {
|
||||||
var state *ansiState
|
|
||||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||||
trimmed, offsets, newState := extractColor(string(data), state, nil)
|
prevLineAnsiState = lineAnsiState
|
||||||
state = newState
|
trimmed, offsets, newState := extractColor(string(data), lineAnsiState, nil)
|
||||||
return util.RunesToChars([]rune(trimmed)), offsets
|
lineAnsiState = newState
|
||||||
|
return util.ToChars([]byte(trimmed)), offsets
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When color is disabled but ansi option is given,
|
// When color is disabled but ansi option is given,
|
||||||
// we simply strip out ANSI codes from the input
|
// we simply strip out ANSI codes from the input
|
||||||
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
ansiProcessor = func(data []byte) (util.Chars, *[]ansiOffset) {
|
||||||
trimmed, _, _ := extractColor(string(data), nil, nil)
|
trimmed, _, _ := extractColor(string(data), nil, nil)
|
||||||
return util.RunesToChars([]rune(trimmed)), nil
|
return util.ToChars([]byte(trimmed)), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ansiProcessorRunes = func(data []rune) (util.Chars, *[]ansiOffset) {
|
|
||||||
return ansiProcessor([]byte(string(data)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk list
|
// Chunk list
|
||||||
var chunkList *ChunkList
|
var chunkList *ChunkList
|
||||||
|
var itemIndex int32
|
||||||
header := make([]string, 0, opts.HeaderLines)
|
header := make([]string, 0, opts.HeaderLines)
|
||||||
if len(opts.WithNth) == 0 {
|
if len(opts.WithNth) == 0 {
|
||||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||||
if len(header) < opts.HeaderLines {
|
if len(header) < opts.HeaderLines {
|
||||||
header = append(header, string(data))
|
header = append(header, string(data))
|
||||||
eventBox.Set(EvtHeader, header)
|
eventBox.Set(EvtHeader, header)
|
||||||
return nil
|
return false
|
||||||
}
|
}
|
||||||
chars, colors := ansiProcessor(data)
|
item.text, item.colors = ansiProcessor(data)
|
||||||
return &Item{
|
item.text.Index = itemIndex
|
||||||
index: int32(index),
|
itemIndex++
|
||||||
trimLength: -1,
|
return true
|
||||||
text: chars,
|
|
||||||
colors: colors}
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
chunkList = NewChunkList(func(data []byte, index int) *Item {
|
chunkList = NewChunkList(func(item *Item, data []byte) bool {
|
||||||
tokens := Tokenize(util.ToChars(data), opts.Delimiter)
|
tokens := Tokenize(string(data), opts.Delimiter)
|
||||||
trans := Transform(tokens, opts.WithNth)
|
if opts.Ansi && opts.Theme.Colored && len(tokens) > 1 {
|
||||||
if len(header) < opts.HeaderLines {
|
var ansiState *ansiState
|
||||||
header = append(header, string(joinTokens(trans)))
|
if prevLineAnsiState != nil {
|
||||||
eventBox.Set(EvtHeader, header)
|
ansiStateDup := *prevLineAnsiState
|
||||||
return nil
|
ansiState = &ansiStateDup
|
||||||
}
|
}
|
||||||
textRunes := joinTokens(trans)
|
for _, token := range tokens {
|
||||||
item := Item{
|
prevAnsiState := ansiState
|
||||||
index: int32(index),
|
_, _, ansiState = extractColor(token.text.ToString(), ansiState, nil)
|
||||||
trimLength: -1,
|
if prevAnsiState != nil {
|
||||||
origText: &data,
|
token.text.Prepend("\x1b[m" + prevAnsiState.ToString())
|
||||||
colors: nil}
|
} else {
|
||||||
|
token.text.Prepend("\x1b[m")
|
||||||
trimmed, colors := ansiProcessorRunes(textRunes)
|
}
|
||||||
item.text = trimmed
|
}
|
||||||
item.colors = colors
|
}
|
||||||
return &item
|
trans := Transform(tokens, opts.WithNth)
|
||||||
|
transformed := joinTokens(trans)
|
||||||
|
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.Index = itemIndex
|
||||||
|
item.origText = &data
|
||||||
|
itemIndex++
|
||||||
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reader
|
// Reader
|
||||||
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
streamingFilter := opts.Filter != nil && !sort && !opts.Tac && !opts.Sync
|
||||||
|
var reader *Reader
|
||||||
if !streamingFilter {
|
if !streamingFilter {
|
||||||
reader := Reader{func(data []byte) bool {
|
reader = NewReader(func(data []byte) bool {
|
||||||
return chunkList.Push(data)
|
return chunkList.Push(data)
|
||||||
}, eventBox, opts.ReadZero}
|
}, eventBox, opts.ReadZero, opts.Filter == nil)
|
||||||
go reader.ReadSource()
|
go reader.ReadSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matcher
|
// Matcher
|
||||||
forward := true
|
forward := true
|
||||||
for _, cri := range opts.Criteria[1:] {
|
withPos := false
|
||||||
if cri == byEnd {
|
for idx := len(opts.Criteria) - 1; idx > 0; idx-- {
|
||||||
|
switch opts.Criteria[idx] {
|
||||||
|
case byChunk:
|
||||||
|
withPos = true
|
||||||
|
case byEnd:
|
||||||
forward = false
|
forward = false
|
||||||
break
|
case byBegin:
|
||||||
}
|
forward = true
|
||||||
if cri == byBegin {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
patternBuilder := func(runes []rune) *Pattern {
|
patternBuilder := func(runes []rune) *Pattern {
|
||||||
return BuildPattern(
|
return BuildPattern(
|
||||||
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward,
|
opts.Fuzzy, opts.FuzzyAlgo, opts.Extended, opts.Case, opts.Normalize, forward, withPos,
|
||||||
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
opts.Filter == nil, opts.Nth, opts.Delimiter, runes)
|
||||||
}
|
}
|
||||||
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox)
|
inputRevision := 0
|
||||||
|
snapshotRevision := 0
|
||||||
|
matcher := NewMatcher(patternBuilder, sort, opts.Tac, eventBox, inputRevision)
|
||||||
|
|
||||||
// Filtering mode
|
// Filtering mode
|
||||||
if opts.Filter != nil {
|
if opts.Filter != nil {
|
||||||
@@ -161,21 +149,22 @@ func Run(opts *Options, revision string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pattern := patternBuilder([]rune(*opts.Filter))
|
pattern := patternBuilder([]rune(*opts.Filter))
|
||||||
|
matcher.sort = pattern.sortable
|
||||||
|
|
||||||
found := false
|
found := false
|
||||||
if streamingFilter {
|
if streamingFilter {
|
||||||
slab := util.MakeSlab(slab16Size, slab32Size)
|
slab := util.MakeSlab(slab16Size, slab32Size)
|
||||||
reader := Reader{
|
reader := NewReader(
|
||||||
func(runes []byte) bool {
|
func(runes []byte) bool {
|
||||||
item := chunkList.trans(runes, 0)
|
item := Item{}
|
||||||
if item != nil {
|
if chunkList.trans(&item, runes) {
|
||||||
if result, _, _ := pattern.MatchItem(item, false, slab); result != nil {
|
if result, _, _ := pattern.MatchItem(&item, false, slab); result != nil {
|
||||||
opts.Printer(item.text.ToString())
|
opts.Printer(item.text.ToString())
|
||||||
found = true
|
found = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}, eventBox, opts.ReadZero}
|
}, eventBox, opts.ReadZero, false)
|
||||||
reader.ReadSource()
|
reader.ReadSource()
|
||||||
} else {
|
} else {
|
||||||
eventBox.Unwatch(EvtReadNew)
|
eventBox.Unwatch(EvtReadNew)
|
||||||
@@ -207,37 +196,133 @@ func Run(opts *Options, revision string) {
|
|||||||
|
|
||||||
// Terminal I/O
|
// Terminal I/O
|
||||||
terminal := NewTerminal(opts, eventBox)
|
terminal := NewTerminal(opts, eventBox)
|
||||||
|
maxFit := 0 // Maximum number of items that can fit on screen
|
||||||
|
padHeight := 0
|
||||||
|
heightUnknown := opts.Height.auto
|
||||||
|
if heightUnknown {
|
||||||
|
maxFit, padHeight = terminal.MaxFitAndPad(opts)
|
||||||
|
}
|
||||||
deferred := opts.Select1 || opts.Exit0
|
deferred := opts.Select1 || opts.Exit0
|
||||||
go terminal.Loop()
|
go terminal.Loop()
|
||||||
if !deferred {
|
if !deferred && !heightUnknown {
|
||||||
terminal.startChan <- true
|
// Start right away
|
||||||
|
terminal.startChan <- fitpad{-1, -1}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Event coordination
|
// Event coordination
|
||||||
reading := true
|
reading := true
|
||||||
ticks := 0
|
ticks := 0
|
||||||
|
var nextCommand *string
|
||||||
eventBox.Watch(EvtReadNew)
|
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 string) {
|
||||||
|
reading = true
|
||||||
|
chunkList.Clear()
|
||||||
|
itemIndex = 0
|
||||||
|
inputRevision++
|
||||||
|
header = make([]string, 0, opts.HeaderLines)
|
||||||
|
go reader.restart(command)
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
delay := true
|
delay := true
|
||||||
ticks++
|
ticks++
|
||||||
|
input := func() []rune {
|
||||||
|
reloaded := snapshotRevision != inputRevision
|
||||||
|
paused, input := terminal.Input()
|
||||||
|
if reloaded && paused {
|
||||||
|
query = []rune{}
|
||||||
|
} else if !paused {
|
||||||
|
query = input
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
eventBox.Wait(func(events *util.Events) {
|
eventBox.Wait(func(events *util.Events) {
|
||||||
defer events.Clear()
|
if _, fin := (*events)[EvtReadFin]; fin {
|
||||||
|
delete(*events, EvtReadNew)
|
||||||
|
}
|
||||||
for evt, value := range *events {
|
for evt, value := range *events {
|
||||||
switch evt {
|
switch evt {
|
||||||
|
case EvtQuit:
|
||||||
|
if reading {
|
||||||
|
reader.terminate()
|
||||||
|
}
|
||||||
|
os.Exit(value.(int))
|
||||||
case EvtReadNew, EvtReadFin:
|
case EvtReadNew, EvtReadFin:
|
||||||
|
if evt == EvtReadFin && nextCommand != nil {
|
||||||
|
restart(*nextCommand)
|
||||||
|
nextCommand = nil
|
||||||
|
break
|
||||||
|
} else {
|
||||||
reading = reading && evt == EvtReadNew
|
reading = reading && evt == EvtReadNew
|
||||||
snapshot, count := chunkList.Snapshot()
|
}
|
||||||
terminal.UpdateCount(count, !reading)
|
if useSnapshot && evt == EvtReadFin {
|
||||||
matcher.Reset(snapshot, terminal.Input(), false, !reading, sort)
|
useSnapshot = false
|
||||||
|
}
|
||||||
|
if !useSnapshot {
|
||||||
|
snapshot, count = chunkList.Snapshot()
|
||||||
|
snapshotRevision = inputRevision
|
||||||
|
}
|
||||||
|
total = count
|
||||||
|
terminal.UpdateCount(total, !reading, value.(*string))
|
||||||
|
if opts.Sync {
|
||||||
|
opts.Sync = false
|
||||||
|
terminal.UpdateList(PassMerger(&snapshot, opts.Tac, snapshotRevision))
|
||||||
|
}
|
||||||
|
if heightUnknown && !deferred {
|
||||||
|
determine(!reading)
|
||||||
|
}
|
||||||
|
matcher.Reset(snapshot, input(), false, !reading, sort, snapshotRevision)
|
||||||
|
|
||||||
case EvtSearchNew:
|
case EvtSearchNew:
|
||||||
|
var command *string
|
||||||
|
var changed bool
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
case bool:
|
case searchRequest:
|
||||||
sort = val
|
sort = val.sort
|
||||||
|
command = val.command
|
||||||
|
changed = val.changed
|
||||||
|
if command != nil {
|
||||||
|
useSnapshot = val.sync
|
||||||
}
|
}
|
||||||
snapshot, _ := chunkList.Snapshot()
|
}
|
||||||
matcher.Reset(snapshot, terminal.Input(), true, !reading, sort)
|
if command != nil {
|
||||||
|
if reading {
|
||||||
|
reader.terminate()
|
||||||
|
nextCommand = command
|
||||||
|
} else {
|
||||||
|
restart(*command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !changed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !useSnapshot {
|
||||||
|
newSnapshot, _ := chunkList.Snapshot()
|
||||||
|
// 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 || len(newSnapshot) > 0 {
|
||||||
|
snapshot = newSnapshot
|
||||||
|
snapshotRevision = inputRevision
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher.Reset(snapshot, input(), true, !reading, sort, snapshotRevision)
|
||||||
delay = false
|
delay = false
|
||||||
|
|
||||||
case EvtSearchProgress:
|
case EvtSearchProgress:
|
||||||
@@ -247,7 +332,9 @@ func Run(opts *Options, revision string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case EvtHeader:
|
case EvtHeader:
|
||||||
terminal.UpdateHeader(value.([]string))
|
headerPadded := make([]string, opts.HeaderLines)
|
||||||
|
copy(headerPadded, value.([]string))
|
||||||
|
terminal.UpdateHeader(headerPadded)
|
||||||
|
|
||||||
case EvtSearchFin:
|
case EvtSearchFin:
|
||||||
switch val := value.(type) {
|
switch val := value.(type) {
|
||||||
@@ -255,8 +342,7 @@ func Run(opts *Options, revision string) {
|
|||||||
if deferred {
|
if deferred {
|
||||||
count := val.Length()
|
count := val.Length()
|
||||||
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
if opts.Select1 && count > 1 || opts.Exit0 && !opts.Select1 && count > 0 {
|
||||||
deferred = false
|
determine(val.final)
|
||||||
terminal.startChan <- true
|
|
||||||
} else if val.final {
|
} else if val.final {
|
||||||
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
if opts.Exit0 && count == 0 || opts.Select1 && count == 1 {
|
||||||
if opts.PrintQuery {
|
if opts.PrintQuery {
|
||||||
@@ -273,14 +359,14 @@ func Run(opts *Options, revision string) {
|
|||||||
}
|
}
|
||||||
os.Exit(exitNoMatch)
|
os.Exit(exitNoMatch)
|
||||||
}
|
}
|
||||||
deferred = false
|
determine(val.final)
|
||||||
terminal.startChan <- true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.UpdateList(val)
|
terminal.UpdateList(val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
events.Clear()
|
||||||
})
|
})
|
||||||
if delay && reading {
|
if delay && reading {
|
||||||
dur := util.DurWithin(
|
dur := util.DurWithin(
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -26,12 +25,12 @@ func NewHistory(path string, maxSize int) (*History, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read history file
|
// Read history file
|
||||||
data, err := ioutil.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If it doesn't exist, check if we can create a file with the name
|
// If it doesn't exist, check if we can create a file with the name
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
data = []byte{}
|
data = []byte{}
|
||||||
if err := ioutil.WriteFile(path, data, 0600); err != nil {
|
if err := os.WriteFile(path, data, 0600); err != nil {
|
||||||
return nil, fmtError(err)
|
return nil, fmtError(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -59,14 +58,14 @@ func (h *History) append(line string) error {
|
|||||||
|
|
||||||
lines := append(h.lines[:len(h.lines)-1], line)
|
lines := append(h.lines[:len(h.lines)-1], line)
|
||||||
if len(lines) > h.maxSize {
|
if len(lines) > h.maxSize {
|
||||||
lines = lines[len(lines)-h.maxSize : len(lines)]
|
lines = lines[len(lines)-h.maxSize:]
|
||||||
}
|
}
|
||||||
h.lines = append(lines, "")
|
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) {
|
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 {
|
if h.cursor == len(h.lines)-1 {
|
||||||
h.lines[h.cursor] = str
|
h.lines[h.cursor] = str
|
||||||
} else if h.cursor < len(h.lines)-1 {
|
} else if h.cursor < len(h.lines)-1 {
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -12,16 +10,12 @@ func TestHistory(t *testing.T) {
|
|||||||
maxHistory := 50
|
maxHistory := 50
|
||||||
|
|
||||||
// Invalid arguments
|
// Invalid arguments
|
||||||
user, _ := user.Current()
|
|
||||||
var paths []string
|
var paths []string
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
// GOPATH should exist, so we shouldn't be able to override it
|
// GOPATH should exist, so we shouldn't be able to override it
|
||||||
paths = []string{os.Getenv("GOPATH")}
|
paths = []string{os.Getenv("GOPATH")}
|
||||||
} else {
|
} else {
|
||||||
paths = []string{"/etc", "/proc"}
|
paths = []string{"/etc", "/proc"}
|
||||||
if user.Name != "root" {
|
|
||||||
paths = append(paths, "/etc/sudoers")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range paths {
|
||||||
@@ -30,7 +24,7 @@ func TestHistory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f, _ := ioutil.TempFile("", "fzf-history")
|
f, _ := os.CreateTemp("", "fzf-history")
|
||||||
f.Close()
|
f.Close()
|
||||||
|
|
||||||
{ // Append lines
|
{ // Append lines
|
||||||
|
|||||||
24
src/item.go
24
src/item.go
@@ -4,27 +4,23 @@ import (
|
|||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Item represents each input line
|
// Item represents each input line. 56 bytes.
|
||||||
type Item struct {
|
type Item struct {
|
||||||
index int32
|
text util.Chars // 32 = 24 + 1 + 1 + 2 + 4
|
||||||
trimLength int32
|
transformed *[]Token // 8
|
||||||
text util.Chars
|
origText *[]byte // 8
|
||||||
origText *[]byte
|
colors *[]ansiOffset // 8
|
||||||
colors *[]ansiOffset
|
|
||||||
transformed []Token
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Index returns ordinal index of the Item
|
// Index returns ordinal index of the Item
|
||||||
func (item *Item) Index() int32 {
|
func (item *Item) Index() int32 {
|
||||||
return item.index
|
return item.text.Index
|
||||||
}
|
}
|
||||||
|
|
||||||
func (item *Item) TrimLength() int32 {
|
var minItem = Item{text: util.Chars{Index: -1}}
|
||||||
if item.trimLength >= 0 {
|
|
||||||
return item.trimLength
|
func (item *Item) TrimLength() uint16 {
|
||||||
}
|
return item.text.TrimLength()
|
||||||
item.trimLength = int32(item.text.TrimLength())
|
|
||||||
return item.trimLength
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Colors returns ansiOffsets of the Item
|
// Colors returns ansiOffsets of the Item
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type MatchRequest struct {
|
|||||||
pattern *Pattern
|
pattern *Pattern
|
||||||
final bool
|
final bool
|
||||||
sort bool
|
sort bool
|
||||||
|
revision int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matcher is responsible for performing search
|
// Matcher is responsible for performing search
|
||||||
@@ -28,6 +29,7 @@ type Matcher struct {
|
|||||||
partitions int
|
partitions int
|
||||||
slab []*util.Slab
|
slab []*util.Slab
|
||||||
mergerCache map[string]*Merger
|
mergerCache map[string]*Merger
|
||||||
|
revision int
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -37,7 +39,7 @@ const (
|
|||||||
|
|
||||||
// NewMatcher returns a new Matcher
|
// NewMatcher returns a new Matcher
|
||||||
func NewMatcher(patternBuilder func([]rune) *Pattern,
|
func NewMatcher(patternBuilder func([]rune) *Pattern,
|
||||||
sort bool, tac bool, eventBox *util.EventBox) *Matcher {
|
sort bool, tac bool, eventBox *util.EventBox, revision int) *Matcher {
|
||||||
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
partitions := util.Min(numPartitionsMultiplier*runtime.NumCPU(), maxPartitions)
|
||||||
return &Matcher{
|
return &Matcher{
|
||||||
patternBuilder: patternBuilder,
|
patternBuilder: patternBuilder,
|
||||||
@@ -47,7 +49,8 @@ func NewMatcher(patternBuilder func([]rune) *Pattern,
|
|||||||
reqBox: util.NewEventBox(),
|
reqBox: util.NewEventBox(),
|
||||||
partitions: partitions,
|
partitions: partitions,
|
||||||
slab: make([]*util.Slab, partitions),
|
slab: make([]*util.Slab, partitions),
|
||||||
mergerCache: make(map[string]*Merger)}
|
mergerCache: make(map[string]*Merger),
|
||||||
|
revision: revision}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop puts Matcher in action
|
// Loop puts Matcher in action
|
||||||
@@ -69,8 +72,9 @@ func (m *Matcher) Loop() {
|
|||||||
events.Clear()
|
events.Clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
if request.sort != m.sort {
|
if request.sort != m.sort || request.revision != m.revision {
|
||||||
m.sort = request.sort
|
m.sort = request.sort
|
||||||
|
m.revision = request.revision
|
||||||
m.mergerCache = make(map[string]*Merger)
|
m.mergerCache = make(map[string]*Merger)
|
||||||
clearChunkCache()
|
clearChunkCache()
|
||||||
}
|
}
|
||||||
@@ -131,7 +135,7 @@ func (m *Matcher) sliceChunks(chunks []*Chunk) [][]*Chunk {
|
|||||||
|
|
||||||
type partialResult struct {
|
type partialResult struct {
|
||||||
index int
|
index int
|
||||||
matches []*Result
|
matches []Result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
||||||
@@ -139,11 +143,11 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
|
|
||||||
numChunks := len(request.chunks)
|
numChunks := len(request.chunks)
|
||||||
if numChunks == 0 {
|
if numChunks == 0 {
|
||||||
return EmptyMerger, false
|
return EmptyMerger(request.revision), false
|
||||||
}
|
}
|
||||||
pattern := request.pattern
|
pattern := request.pattern
|
||||||
if pattern.IsEmpty() {
|
if pattern.IsEmpty() {
|
||||||
return PassMerger(&request.chunks, m.tac), false
|
return PassMerger(&request.chunks, m.tac, request.revision), false
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelled := util.NewAtomicBool(false)
|
cancelled := util.NewAtomicBool(false)
|
||||||
@@ -162,7 +166,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
go func(idx int, slab *util.Slab, chunks []*Chunk) {
|
go func(idx int, slab *util.Slab, chunks []*Chunk) {
|
||||||
defer func() { waitGroup.Done() }()
|
defer func() { waitGroup.Done() }()
|
||||||
count := 0
|
count := 0
|
||||||
allMatches := make([][]*Result, len(chunks))
|
allMatches := make([][]Result, len(chunks))
|
||||||
for idx, chunk := range chunks {
|
for idx, chunk := range chunks {
|
||||||
matches := request.pattern.Match(chunk, slab)
|
matches := request.pattern.Match(chunk, slab)
|
||||||
allMatches[idx] = matches
|
allMatches[idx] = matches
|
||||||
@@ -172,7 +176,7 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
}
|
}
|
||||||
countChan <- len(matches)
|
countChan <- len(matches)
|
||||||
}
|
}
|
||||||
sliceMatches := make([]*Result, 0, count)
|
sliceMatches := make([]Result, 0, count)
|
||||||
for _, matches := range allMatches {
|
for _, matches := range allMatches {
|
||||||
sliceMatches = append(sliceMatches, matches...)
|
sliceMatches = append(sliceMatches, matches...)
|
||||||
}
|
}
|
||||||
@@ -207,21 +211,21 @@ func (m *Matcher) scan(request MatchRequest) (*Merger, bool) {
|
|||||||
return nil, wait()
|
return nil, wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
if time.Now().Sub(startedAt) > progressMinDuration {
|
if time.Since(startedAt) > progressMinDuration {
|
||||||
m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
|
m.eventBox.Set(EvtSearchProgress, float32(count)/float32(numChunks))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
partialResults := make([][]*Result, numSlices)
|
partialResults := make([][]Result, numSlices)
|
||||||
for _ = range slices {
|
for range slices {
|
||||||
partialResult := <-resultChan
|
partialResult := <-resultChan
|
||||||
partialResults[partialResult.index] = partialResult.matches
|
partialResults[partialResult.index] = partialResult.matches
|
||||||
}
|
}
|
||||||
return NewMerger(pattern, partialResults, m.sort, m.tac), false
|
return NewMerger(pattern, partialResults, m.sort, m.tac, request.revision), false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset is called to interrupt/signal the ongoing search
|
// Reset is called to interrupt/signal the ongoing search
|
||||||
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool) {
|
func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final bool, sort bool, revision int) {
|
||||||
pattern := m.patternBuilder(patternRunes)
|
pattern := m.patternBuilder(patternRunes)
|
||||||
|
|
||||||
var event util.EventType
|
var event util.EventType
|
||||||
@@ -230,5 +234,5 @@ func (m *Matcher) Reset(chunks []*Chunk, patternRunes []rune, cancel bool, final
|
|||||||
} else {
|
} else {
|
||||||
event = reqRetry
|
event = reqRetry
|
||||||
}
|
}
|
||||||
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort})
|
m.reqBox.Set(event, MatchRequest{chunks, pattern, final, sort && pattern.sortable, revision})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,49 +3,56 @@ package fzf
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
// EmptyMerger is a Merger with no data
|
// EmptyMerger is a Merger with no data
|
||||||
var EmptyMerger = NewMerger(nil, [][]*Result{}, false, false)
|
func EmptyMerger(revision int) *Merger {
|
||||||
|
return NewMerger(nil, [][]Result{}, false, false, revision)
|
||||||
|
}
|
||||||
|
|
||||||
// Merger holds a set of locally sorted lists of items and provides the view of
|
// Merger holds a set of locally sorted lists of items and provides the view of
|
||||||
// a single, globally-sorted list
|
// a single, globally-sorted list
|
||||||
type Merger struct {
|
type Merger struct {
|
||||||
pattern *Pattern
|
pattern *Pattern
|
||||||
lists [][]*Result
|
lists [][]Result
|
||||||
merged []*Result
|
merged []Result
|
||||||
chunks *[]*Chunk
|
chunks *[]*Chunk
|
||||||
cursors []int
|
cursors []int
|
||||||
sorted bool
|
sorted bool
|
||||||
tac bool
|
tac bool
|
||||||
final bool
|
final bool
|
||||||
count int
|
count int
|
||||||
|
pass bool
|
||||||
|
revision int
|
||||||
}
|
}
|
||||||
|
|
||||||
// PassMerger returns a new Merger that simply returns the items in the
|
// PassMerger returns a new Merger that simply returns the items in the
|
||||||
// original order
|
// original order
|
||||||
func PassMerger(chunks *[]*Chunk, tac bool) *Merger {
|
func PassMerger(chunks *[]*Chunk, tac bool, revision int) *Merger {
|
||||||
mg := Merger{
|
mg := Merger{
|
||||||
pattern: nil,
|
pattern: nil,
|
||||||
chunks: chunks,
|
chunks: chunks,
|
||||||
tac: tac,
|
tac: tac,
|
||||||
count: 0}
|
count: 0,
|
||||||
|
pass: true,
|
||||||
|
revision: revision}
|
||||||
|
|
||||||
for _, chunk := range *mg.chunks {
|
for _, chunk := range *mg.chunks {
|
||||||
mg.count += len(*chunk)
|
mg.count += chunk.count
|
||||||
}
|
}
|
||||||
return &mg
|
return &mg
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMerger returns a new 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 int) *Merger {
|
||||||
mg := Merger{
|
mg := Merger{
|
||||||
pattern: pattern,
|
pattern: pattern,
|
||||||
lists: lists,
|
lists: lists,
|
||||||
merged: []*Result{},
|
merged: []Result{},
|
||||||
chunks: nil,
|
chunks: nil,
|
||||||
cursors: make([]int, len(lists)),
|
cursors: make([]int, len(lists)),
|
||||||
sorted: sorted,
|
sorted: sorted,
|
||||||
tac: tac,
|
tac: tac,
|
||||||
final: false,
|
final: false,
|
||||||
count: 0}
|
count: 0,
|
||||||
|
revision: revision}
|
||||||
|
|
||||||
for _, list := range mg.lists {
|
for _, list := range mg.lists {
|
||||||
mg.count += len(list)
|
mg.count += len(list)
|
||||||
@@ -53,19 +60,50 @@ func NewMerger(pattern *Pattern, lists [][]*Result, sorted bool, tac bool) *Merg
|
|||||||
return &mg
|
return &mg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Revision returns revision number
|
||||||
|
func (mg *Merger) Revision() int {
|
||||||
|
return mg.revision
|
||||||
|
}
|
||||||
|
|
||||||
// Length returns the number of items
|
// Length returns the number of items
|
||||||
func (mg *Merger) Length() int {
|
func (mg *Merger) Length() int {
|
||||||
return mg.count
|
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)
|
||||||
|
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
|
// Get returns the pointer to the Result object indexed by the given integer
|
||||||
func (mg *Merger) Get(idx int) *Result {
|
func (mg *Merger) Get(idx int) Result {
|
||||||
if mg.chunks != nil {
|
if mg.chunks != nil {
|
||||||
if mg.tac {
|
if mg.tac {
|
||||||
idx = mg.count - idx - 1
|
idx = mg.count - idx - 1
|
||||||
}
|
}
|
||||||
chunk := (*mg.chunks)[idx/chunkSize]
|
chunk := (*mg.chunks)[idx/chunkSize]
|
||||||
return &Result{item: (*chunk)[idx%chunkSize]}
|
return Result{item: &chunk.items[idx%chunkSize]}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mg.sorted {
|
if mg.sorted {
|
||||||
@@ -89,7 +127,7 @@ func (mg *Merger) cacheable() bool {
|
|||||||
return mg.count < mergerCacheMax
|
return mg.count < mergerCacheMax
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mg *Merger) mergedGet(idx int) *Result {
|
func (mg *Merger) mergedGet(idx int) Result {
|
||||||
for i := len(mg.merged); i <= idx; i++ {
|
for i := len(mg.merged); i <= idx; i++ {
|
||||||
minRank := minRank()
|
minRank := minRank()
|
||||||
minIdx := -1
|
minIdx := -1
|
||||||
@@ -100,13 +138,12 @@ func (mg *Merger) mergedGet(idx int) *Result {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if cursor >= 0 {
|
if cursor >= 0 {
|
||||||
rank := list[cursor].rank
|
rank := list[cursor]
|
||||||
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
if minIdx < 0 || compareRanks(rank, minRank, mg.tac) {
|
||||||
minRank = rank
|
minRank = rank
|
||||||
minIdx = listIdx
|
minIdx = listIdx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mg.cursors[listIdx] = cursor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if minIdx >= 0 {
|
if minIdx >= 0 {
|
||||||
|
|||||||
@@ -15,28 +15,28 @@ func assert(t *testing.T, cond bool, msg ...string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func randResult() *Result {
|
func randResult() Result {
|
||||||
str := fmt.Sprintf("%d", rand.Uint32())
|
str := fmt.Sprintf("%d", rand.Uint32())
|
||||||
return &Result{
|
chars := util.ToChars([]byte(str))
|
||||||
item: &Item{text: util.RunesToChars([]rune(str))},
|
chars.Index = rand.Int31()
|
||||||
rank: rank{index: rand.Int31()}}
|
return Result{item: &Item{text: chars}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmptyMerger(t *testing.T) {
|
func TestEmptyMerger(t *testing.T) {
|
||||||
assert(t, EmptyMerger.Length() == 0, "Not empty")
|
assert(t, EmptyMerger(0).Length() == 0, "Not empty")
|
||||||
assert(t, EmptyMerger.count == 0, "Invalid count")
|
assert(t, EmptyMerger(0).count == 0, "Invalid count")
|
||||||
assert(t, len(EmptyMerger.lists) == 0, "Invalid lists")
|
assert(t, len(EmptyMerger(0).lists) == 0, "Invalid lists")
|
||||||
assert(t, len(EmptyMerger.merged) == 0, "Invalid merged list")
|
assert(t, len(EmptyMerger(0).merged) == 0, "Invalid merged list")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
func buildLists(partiallySorted bool) ([][]Result, []Result) {
|
||||||
numLists := 4
|
numLists := 4
|
||||||
lists := make([][]*Result, numLists)
|
lists := make([][]Result, numLists)
|
||||||
cnt := 0
|
cnt := 0
|
||||||
for i := 0; i < numLists; i++ {
|
for i := 0; i < numLists; i++ {
|
||||||
numResults := rand.Int() % 20
|
numResults := rand.Int() % 20
|
||||||
cnt += numResults
|
cnt += numResults
|
||||||
lists[i] = make([]*Result, numResults)
|
lists[i] = make([]Result, numResults)
|
||||||
for j := 0; j < numResults; j++ {
|
for j := 0; j < numResults; j++ {
|
||||||
item := randResult()
|
item := randResult()
|
||||||
lists[i][j] = item
|
lists[i][j] = item
|
||||||
@@ -45,7 +45,7 @@ func buildLists(partiallySorted bool) ([][]*Result, []*Result) {
|
|||||||
sort.Sort(ByRelevance(lists[i]))
|
sort.Sort(ByRelevance(lists[i]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items := []*Result{}
|
items := []Result{}
|
||||||
for _, list := range lists {
|
for _, list := range lists {
|
||||||
items = append(items, list...)
|
items = append(items, list...)
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ func TestMergerUnsorted(t *testing.T) {
|
|||||||
cnt := len(items)
|
cnt := len(items)
|
||||||
|
|
||||||
// Not sorted: same order
|
// Not sorted: same order
|
||||||
mg := NewMerger(nil, lists, false, false)
|
mg := NewMerger(nil, lists, false, false, 0)
|
||||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||||
for i := 0; i < cnt; i++ {
|
for i := 0; i < cnt; i++ {
|
||||||
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
assert(t, items[i] == mg.Get(i), "Invalid Get")
|
||||||
@@ -69,7 +69,7 @@ func TestMergerSorted(t *testing.T) {
|
|||||||
cnt := len(items)
|
cnt := len(items)
|
||||||
|
|
||||||
// Sorted sorted order
|
// Sorted sorted order
|
||||||
mg := NewMerger(nil, lists, true, false)
|
mg := NewMerger(nil, lists, true, false, 0)
|
||||||
assert(t, cnt == mg.Length(), "Invalid Length")
|
assert(t, cnt == mg.Length(), "Invalid Length")
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
for i := 0; i < cnt; i++ {
|
for i := 0; i < cnt; i++ {
|
||||||
@@ -79,7 +79,7 @@ func TestMergerSorted(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inverse order
|
// Inverse order
|
||||||
mg2 := NewMerger(nil, lists, true, false)
|
mg2 := NewMerger(nil, lists, true, false, 0)
|
||||||
for i := cnt - 1; i >= 0; i-- {
|
for i := cnt - 1; i >= 0; i-- {
|
||||||
if items[i] != mg2.Get(i) {
|
if items[i] != mg2.Get(i) {
|
||||||
t.Error("Not sorted", items[i], mg2.Get(i))
|
t.Error("Not sorted", items[i], mg2.Get(i))
|
||||||
|
|||||||
1346
src/options.go
1346
src/options.go
File diff suppressed because it is too large
Load Diff
@@ -2,11 +2,10 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/tui"
|
"github.com/junegunn/fzf/src/tui"
|
||||||
"github.com/junegunn/fzf/src/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDelimiterRegex(t *testing.T) {
|
func TestDelimiterRegex(t *testing.T) {
|
||||||
@@ -44,20 +43,20 @@ func TestDelimiterRegex(t *testing.T) {
|
|||||||
|
|
||||||
func TestDelimiterRegexString(t *testing.T) {
|
func TestDelimiterRegexString(t *testing.T) {
|
||||||
delim := delimiterRegexp("*")
|
delim := delimiterRegexp("*")
|
||||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
tokens := Tokenize("-*--*---**---", delim)
|
||||||
if delim.regex != nil ||
|
if delim.regex != nil ||
|
||||||
tokens[0].text.ToString() != "-*" ||
|
tokens[0].text.ToString() != "-*" ||
|
||||||
tokens[1].text.ToString() != "--*" ||
|
tokens[1].text.ToString() != "--*" ||
|
||||||
tokens[2].text.ToString() != "---*" ||
|
tokens[2].text.ToString() != "---*" ||
|
||||||
tokens[3].text.ToString() != "*" ||
|
tokens[3].text.ToString() != "*" ||
|
||||||
tokens[4].text.ToString() != "---" {
|
tokens[4].text.ToString() != "---" {
|
||||||
t.Errorf("%s %s %d", delim, tokens, len(tokens))
|
t.Errorf("%s %v %d", delim, tokens, len(tokens))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDelimiterRegexRegex(t *testing.T) {
|
func TestDelimiterRegexRegex(t *testing.T) {
|
||||||
delim := delimiterRegexp("--\\*")
|
delim := delimiterRegexp("--\\*")
|
||||||
tokens := Tokenize(util.RunesToChars([]rune("-*--*---**---")), delim)
|
tokens := Tokenize("-*--*---**---", delim)
|
||||||
if delim.str != nil ||
|
if delim.str != nil ||
|
||||||
tokens[0].text.ToString() != "-*--*" ||
|
tokens[0].text.ToString() != "-*--*" ||
|
||||||
tokens[1].text.ToString() != "---*" ||
|
tokens[1].text.ToString() != "---*" ||
|
||||||
@@ -66,13 +65,26 @@ 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) {
|
func TestSplitNth(t *testing.T) {
|
||||||
{
|
{
|
||||||
ranges := splitNth("..")
|
ranges := splitNth("..")
|
||||||
if len(ranges) != 1 ||
|
if len(ranges) != 1 ||
|
||||||
ranges[0].begin != rangeEllipsis ||
|
ranges[0].begin != rangeEllipsis ||
|
||||||
ranges[0].end != rangeEllipsis {
|
ranges[0].end != rangeEllipsis {
|
||||||
t.Errorf("%s", ranges)
|
t.Errorf("%v", ranges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -88,7 +100,7 @@ func TestSplitNth(t *testing.T) {
|
|||||||
ranges[7].begin != -2 || ranges[7].end != -2 ||
|
ranges[7].begin != -2 || ranges[7].end != -2 ||
|
||||||
ranges[8].begin != 2 || ranges[8].end != -2 ||
|
ranges[8].begin != 2 || ranges[8].end != -2 ||
|
||||||
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
|
ranges[9].begin != rangeEllipsis || ranges[9].end != rangeEllipsis {
|
||||||
t.Errorf("%s", ranges)
|
t.Errorf("%v", ranges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,16 +112,16 @@ func TestIrrelevantNth(t *testing.T) {
|
|||||||
parseOptions(opts, words)
|
parseOptions(opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if len(opts.Nth) != 0 {
|
if len(opts.Nth) != 0 {
|
||||||
t.Errorf("nth should be empty: %s", opts.Nth)
|
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, words := range [][]string{[]string{"--nth", "..,3", "+x"}, []string{"--nth", "3,1..", "+x"}, []string{"--nth", "..-1,1", "+x"}} {
|
for _, words := range [][]string{{"--nth", "..,3", "+x"}, {"--nth", "3,1..", "+x"}, {"--nth", "..-1,1", "+x"}} {
|
||||||
{
|
{
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
parseOptions(opts, words)
|
parseOptions(opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if len(opts.Nth) != 0 {
|
if len(opts.Nth) != 0 {
|
||||||
t.Errorf("nth should be empty: %s", opts.Nth)
|
t.Errorf("nth should be empty: %v", opts.Nth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -118,7 +130,7 @@ func TestIrrelevantNth(t *testing.T) {
|
|||||||
parseOptions(opts, words)
|
parseOptions(opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if len(opts.Nth) != 2 {
|
if len(opts.Nth) != 2 {
|
||||||
t.Errorf("nth should not be empty: %s", opts.Nth)
|
t.Errorf("nth should not be empty: %v", opts.Nth)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,26 +138,29 @@ func TestIrrelevantNth(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseKeys(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", "")
|
||||||
check := func(i int, s string) {
|
checkEvent := func(e tui.Event, s string) {
|
||||||
if pairs[i] != s {
|
if pairs[e] != s {
|
||||||
t.Errorf("%s != %s", pairs[i], s)
|
t.Errorf("%s != %s", pairs[e], s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
check := func(et tui.EventType, s string) {
|
||||||
|
checkEvent(et.AsEvent(), s)
|
||||||
|
}
|
||||||
if len(pairs) != 12 {
|
if len(pairs) != 12 {
|
||||||
t.Error(12)
|
t.Error(12)
|
||||||
}
|
}
|
||||||
check(tui.CtrlZ, "ctrl-z")
|
check(tui.CtrlZ, "ctrl-z")
|
||||||
check(tui.AltZ, "alt-z")
|
|
||||||
check(tui.F2, "f2")
|
check(tui.F2, "f2")
|
||||||
check(tui.AltZ+'@', "@")
|
check(tui.CtrlG, "ctrl-G")
|
||||||
check(tui.AltA, "Alt-a")
|
checkEvent(tui.AltKey('z'), "alt-z")
|
||||||
check(tui.AltZ+'!', "!")
|
checkEvent(tui.Key('@'), "@")
|
||||||
check(tui.CtrlA+'g'-'a', "ctrl-G")
|
checkEvent(tui.AltKey('a'), "Alt-a")
|
||||||
check(tui.AltZ+'J', "J")
|
checkEvent(tui.Key('!'), "!")
|
||||||
check(tui.AltZ+'g', "g")
|
checkEvent(tui.Key('J'), "J")
|
||||||
check(tui.CtrlAltA, "ctrl-alt-a")
|
checkEvent(tui.Key('g'), "g")
|
||||||
check(tui.CtrlAltM, "ALT-enter")
|
checkEvent(tui.CtrlAltKey('a'), "ctrl-alt-a")
|
||||||
check(tui.AltSpace, "alt-SPACE")
|
checkEvent(tui.CtrlAltKey('m'), "ALT-enter")
|
||||||
|
checkEvent(tui.AltKey(' '), "alt-SPACE")
|
||||||
|
|
||||||
// Synonyms
|
// 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", "")
|
||||||
@@ -153,7 +168,7 @@ func TestParseKeys(t *testing.T) {
|
|||||||
t.Error(9)
|
t.Error(9)
|
||||||
}
|
}
|
||||||
check(tui.CtrlM, "Return")
|
check(tui.CtrlM, "Return")
|
||||||
check(tui.AltZ+' ', "space")
|
checkEvent(tui.Key(' '), "space")
|
||||||
check(tui.Tab, "tab")
|
check(tui.Tab, "tab")
|
||||||
check(tui.BTab, "btab")
|
check(tui.BTab, "btab")
|
||||||
check(tui.ESC, "esc")
|
check(tui.ESC, "esc")
|
||||||
@@ -185,92 +200,105 @@ func TestParseKeysWithComma(t *testing.T) {
|
|||||||
t.Errorf("%d != %d", a, b)
|
t.Errorf("%d != %d", a, b)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
check := func(pairs map[int]string, i int, s string) {
|
check := func(pairs map[tui.Event]string, e tui.Event, s string) {
|
||||||
if pairs[i] != s {
|
if pairs[e] != s {
|
||||||
t.Errorf("%s != %s", pairs[i], s)
|
t.Errorf("%s != %s", pairs[e], s)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pairs := parseKeyChords(",", "")
|
pairs := parseKeyChords(",", "")
|
||||||
checkN(len(pairs), 1)
|
checkN(len(pairs), 1)
|
||||||
check(pairs, tui.AltZ+',', ",")
|
check(pairs, tui.Key(','), ",")
|
||||||
|
|
||||||
pairs = parseKeyChords(",,a,b", "")
|
pairs = parseKeyChords(",,a,b", "")
|
||||||
checkN(len(pairs), 3)
|
checkN(len(pairs), 3)
|
||||||
check(pairs, tui.AltZ+'a', "a")
|
check(pairs, tui.Key('a'), "a")
|
||||||
check(pairs, tui.AltZ+'b', "b")
|
check(pairs, tui.Key('b'), "b")
|
||||||
check(pairs, tui.AltZ+',', ",")
|
check(pairs, tui.Key(','), ",")
|
||||||
|
|
||||||
pairs = parseKeyChords("a,b,,", "")
|
pairs = parseKeyChords("a,b,,", "")
|
||||||
checkN(len(pairs), 3)
|
checkN(len(pairs), 3)
|
||||||
check(pairs, tui.AltZ+'a', "a")
|
check(pairs, tui.Key('a'), "a")
|
||||||
check(pairs, tui.AltZ+'b', "b")
|
check(pairs, tui.Key('b'), "b")
|
||||||
check(pairs, tui.AltZ+',', ",")
|
check(pairs, tui.Key(','), ",")
|
||||||
|
|
||||||
pairs = parseKeyChords("a,,,b", "")
|
pairs = parseKeyChords("a,,,b", "")
|
||||||
checkN(len(pairs), 3)
|
checkN(len(pairs), 3)
|
||||||
check(pairs, tui.AltZ+'a', "a")
|
check(pairs, tui.Key('a'), "a")
|
||||||
check(pairs, tui.AltZ+'b', "b")
|
check(pairs, tui.Key('b'), "b")
|
||||||
check(pairs, tui.AltZ+',', ",")
|
check(pairs, tui.Key(','), ",")
|
||||||
|
|
||||||
pairs = parseKeyChords("a,,,b,c", "")
|
pairs = parseKeyChords("a,,,b,c", "")
|
||||||
checkN(len(pairs), 4)
|
checkN(len(pairs), 4)
|
||||||
check(pairs, tui.AltZ+'a', "a")
|
check(pairs, tui.Key('a'), "a")
|
||||||
check(pairs, tui.AltZ+'b', "b")
|
check(pairs, tui.Key('b'), "b")
|
||||||
check(pairs, tui.AltZ+'c', "c")
|
check(pairs, tui.Key('c'), "c")
|
||||||
check(pairs, tui.AltZ+',', ",")
|
check(pairs, tui.Key(','), ",")
|
||||||
|
|
||||||
pairs = parseKeyChords(",,,", "")
|
pairs = parseKeyChords(",,,", "")
|
||||||
checkN(len(pairs), 1)
|
checkN(len(pairs), 1)
|
||||||
check(pairs, tui.AltZ+',', ",")
|
check(pairs, tui.Key(','), ",")
|
||||||
|
|
||||||
|
pairs = parseKeyChords(",ALT-,,", "")
|
||||||
|
checkN(len(pairs), 1)
|
||||||
|
check(pairs, tui.AltKey(','), "ALT-,")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBind(t *testing.T) {
|
func TestBind(t *testing.T) {
|
||||||
keymap := defaultKeymap()
|
keymap := defaultKeymap()
|
||||||
check := func(keyName int, arg1 string, types ...actionType) {
|
check := func(event tui.Event, arg1 string, types ...actionType) {
|
||||||
if len(keymap[keyName]) != len(types) {
|
if len(keymap[event]) != len(types) {
|
||||||
t.Errorf("invalid number of actions (%d != %d)", len(types), len(keymap[keyName]))
|
t.Errorf("invalid number of actions for %v (%d != %d)",
|
||||||
|
event, len(types), len(keymap[event]))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for idx, action := range keymap[keyName] {
|
for idx, action := range keymap[event] {
|
||||||
if types[idx] != action.t {
|
if types[idx] != action.t {
|
||||||
t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
|
t.Errorf("invalid action type (%d != %d)", types[idx], action.t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(arg1) > 0 && keymap[keyName][0].a != arg1 {
|
if len(arg1) > 0 && keymap[event][0].a != arg1 {
|
||||||
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[keyName][0].a)
|
t.Errorf("invalid action argument: (%s != %s)", arg1, keymap[event][0].a)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
check(tui.CtrlA, "", actBeginningOfLine)
|
check(tui.CtrlA.AsEvent(), "", actBeginningOfLine)
|
||||||
|
errorString := ""
|
||||||
|
errorFn := func(e string) {
|
||||||
|
errorString = e
|
||||||
|
}
|
||||||
parseKeymap(keymap,
|
parseKeymap(keymap,
|
||||||
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
"ctrl-a:kill-line,ctrl-b:toggle-sort+up+down,c:page-up,alt-z:page-down,"+
|
||||||
"f1:execute(ls {})+abort,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 (,),[,],/,:,@,%,{};,"+
|
"alt-a:execute-Multi@echo (,),[,],/,:,;,%,{}@,alt-b:execute;echo (,),[,],/,:,@,%,{};,"+
|
||||||
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
"x:Execute(foo+bar),X:execute/bar+baz/"+
|
||||||
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up")
|
",f1:+first,f1:+top"+
|
||||||
check(tui.CtrlA, "", actKillLine)
|
",,:abort,::accept,+:execute:++\nfoobar,Y:execute(baz)+up", errorFn)
|
||||||
check(tui.CtrlB, "", actToggleSort, actUp, actDown)
|
check(tui.CtrlA.AsEvent(), "", actKillLine)
|
||||||
check(tui.AltZ+'c', "", actPageUp)
|
check(tui.CtrlB.AsEvent(), "", actToggleSort, actUp, actDown)
|
||||||
check(tui.AltZ+',', "", actAbort)
|
check(tui.Key('c'), "", actPageUp)
|
||||||
check(tui.AltZ+':', "", actAccept)
|
check(tui.Key(','), "", actAbort)
|
||||||
check(tui.AltZ, "", actPageDown)
|
check(tui.Key(':'), "", actAccept)
|
||||||
check(tui.F1, "ls {}", actExecute, actAbort)
|
check(tui.AltKey('z'), "", actPageDown)
|
||||||
check(tui.F2, "echo {}, {}, {}", actExecute)
|
check(tui.F1.AsEvent(), "ls {+}", actExecute, actAbort, actExecute, actSelectAll, actFirst, actFirst)
|
||||||
check(tui.F3, "echo '({})'", actExecute)
|
check(tui.F2.AsEvent(), "echo {}, {}, {}", actExecute)
|
||||||
check(tui.F4, "less {}", actExecute)
|
check(tui.F3.AsEvent(), "echo '({})'", actExecute)
|
||||||
check(tui.AltZ+'x', "foo+bar", actExecute)
|
check(tui.F4.AsEvent(), "less {}", actExecute)
|
||||||
check(tui.AltZ+'X', "bar+baz", actExecute)
|
check(tui.Key('x'), "foo+bar", actExecute)
|
||||||
check(tui.AltA, "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
|
check(tui.Key('X'), "bar+baz", actExecute)
|
||||||
check(tui.AltB, "echo (,),[,],/,:,@,%,{}", actExecute)
|
check(tui.AltKey('a'), "echo (,),[,],/,:,;,%,{}", actExecuteMulti)
|
||||||
check(tui.AltZ+'+', "++\nfoobar,Y:execute(baz)+up", actExecute)
|
check(tui.AltKey('b'), "echo (,),[,],/,:,@,%,{}", actExecute)
|
||||||
|
check(tui.Key('+'), "++\nfoobar,Y:execute(baz)+up", actExecute)
|
||||||
|
|
||||||
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
for idx, char := range []rune{'~', '!', '@', '#', '$', '%', '^', '&', '*', '|', ';', '/'} {
|
||||||
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char))
|
parseKeymap(keymap, fmt.Sprintf("%d:execute%cfoobar%c", idx%10, char, char), errorFn)
|
||||||
check(tui.AltZ+int([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
check(tui.Key([]rune(fmt.Sprintf("%d", idx%10))[0]), "foobar", actExecute)
|
||||||
}
|
}
|
||||||
|
|
||||||
parseKeymap(keymap, "f1:abort")
|
parseKeymap(keymap, "f1:abort", errorFn)
|
||||||
check(tui.F1, "", actAbort)
|
check(tui.F1.AsEvent(), "", actAbort)
|
||||||
|
if len(errorString) > 0 {
|
||||||
|
t.Errorf("error parsing keymap: %s", errorString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestColorSpec(t *testing.T) {
|
func TestColorSpec(t *testing.T) {
|
||||||
@@ -295,7 +323,7 @@ func TestColorSpec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
customized := parseTheme(theme, "fg:231,bg:232")
|
customized := parseTheme(theme, "fg:231,bg:232")
|
||||||
if customized.Fg != 231 || customized.Bg != 232 {
|
if customized.Fg.Color != 231 || customized.Bg.Color != 232 {
|
||||||
t.Errorf("color not customized")
|
t.Errorf("color not customized")
|
||||||
}
|
}
|
||||||
if *tui.Dark256 == *customized {
|
if *tui.Dark256 == *customized {
|
||||||
@@ -313,24 +341,13 @@ func TestColorSpec(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseNilTheme(t *testing.T) {
|
|
||||||
var theme *tui.ColorTheme
|
|
||||||
newTheme := parseTheme(theme, "prompt:12")
|
|
||||||
if newTheme != nil {
|
|
||||||
t.Errorf("color is disabled. keep it that way.")
|
|
||||||
}
|
|
||||||
newTheme = parseTheme(theme, "prompt:12,dark,prompt:13")
|
|
||||||
if newTheme.Prompt != 13 {
|
|
||||||
t.Errorf("color should now be enabled and customized")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDefaultCtrlNP(t *testing.T) {
|
func TestDefaultCtrlNP(t *testing.T) {
|
||||||
check := func(words []string, key int, expected actionType) {
|
check := func(words []string, et tui.EventType, expected actionType) {
|
||||||
|
e := et.AsEvent()
|
||||||
opts := defaultOptions()
|
opts := defaultOptions()
|
||||||
parseOptions(opts, words)
|
parseOptions(opts, words)
|
||||||
postProcessOptions(opts)
|
postProcessOptions(opts)
|
||||||
if opts.Keymap[key][0].t != expected {
|
if opts.Keymap[e][0].t != expected {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,14 +357,14 @@ func TestDefaultCtrlNP(t *testing.T) {
|
|||||||
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
check([]string{"--bind=ctrl-n:accept"}, tui.CtrlN, actAccept)
|
||||||
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
check([]string{"--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||||
|
|
||||||
f, _ := ioutil.TempFile("", "fzf-history")
|
f, _ := os.CreateTemp("", "fzf-history")
|
||||||
f.Close()
|
f.Close()
|
||||||
hist := "--history=" + f.Name()
|
hist := "--history=" + f.Name()
|
||||||
check([]string{hist}, tui.CtrlN, actNextHistory)
|
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.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.CtrlN, actNextHistory)
|
||||||
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
check([]string{hist, "--bind=ctrl-p:accept"}, tui.CtrlP, actAccept)
|
||||||
@@ -387,23 +404,26 @@ func TestPreviewOpts(t *testing.T) {
|
|||||||
opts.Preview.size.size == 50) {
|
opts.Preview.size.size == 50) {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
opts = optsFor("--preview", "cat {}", "--preview-window=left:15:hidden:wrap")
|
opts = optsFor("--preview", "cat {}", "--preview-window=left:15,hidden,wrap:+{1}-/2")
|
||||||
if !(opts.Preview.command == "cat {}" &&
|
if !(opts.Preview.command == "cat {}" &&
|
||||||
opts.Preview.hidden == true &&
|
opts.Preview.hidden == true &&
|
||||||
opts.Preview.wrap == true &&
|
opts.Preview.wrap == true &&
|
||||||
opts.Preview.position == posLeft &&
|
opts.Preview.position == posLeft &&
|
||||||
|
opts.Preview.scroll == "+{1}-/2" &&
|
||||||
opts.Preview.size.percent == false &&
|
opts.Preview.size.percent == false &&
|
||||||
opts.Preview.size.size == 15+2+2) {
|
opts.Preview.size.size == 15) {
|
||||||
t.Error(opts.Preview)
|
t.Error(opts.Preview)
|
||||||
}
|
}
|
||||||
opts = optsFor("--preview-window=up:15:wrap:hidden", "--preview-window=down")
|
opts = optsFor("--preview-window=up,15,wrap,hidden,+{1}+3-1-2/2", "--preview-window=down", "--preview-window=cycle")
|
||||||
if !(opts.Preview.command == "" &&
|
if !(opts.Preview.command == "" &&
|
||||||
opts.Preview.hidden == false &&
|
opts.Preview.hidden == true &&
|
||||||
opts.Preview.wrap == false &&
|
opts.Preview.wrap == true &&
|
||||||
|
opts.Preview.cycle == true &&
|
||||||
opts.Preview.position == posDown &&
|
opts.Preview.position == posDown &&
|
||||||
opts.Preview.size.percent == true &&
|
opts.Preview.scroll == "+{1}+3-1-2/2" &&
|
||||||
opts.Preview.size.size == 50) {
|
opts.Preview.size.percent == false &&
|
||||||
t.Error(opts.Preview)
|
opts.Preview.size.size == 15) {
|
||||||
|
t.Error(opts.Preview.size.size)
|
||||||
}
|
}
|
||||||
opts = optsFor("--preview-window=up:15:wrap:hidden")
|
opts = optsFor("--preview-window=up:15:wrap:hidden")
|
||||||
if !(opts.Preview.command == "" &&
|
if !(opts.Preview.command == "" &&
|
||||||
@@ -411,7 +431,80 @@ func TestPreviewOpts(t *testing.T) {
|
|||||||
opts.Preview.wrap == true &&
|
opts.Preview.wrap == true &&
|
||||||
opts.Preview.position == posUp &&
|
opts.Preview.position == posUp &&
|
||||||
opts.Preview.size.percent == false &&
|
opts.Preview.size.percent == false &&
|
||||||
opts.Preview.size.size == 15+2) {
|
opts.Preview.size.size == 15) {
|
||||||
|
t.Error(opts.Preview)
|
||||||
|
}
|
||||||
|
opts = optsFor("--preview=foo", "--preview-window=up", "--preview-window=default:70%")
|
||||||
|
if !(opts.Preview.command == "foo" &&
|
||||||
|
opts.Preview.position == posRight &&
|
||||||
|
opts.Preview.size.percent == true &&
|
||||||
|
opts.Preview.size.size == 70) {
|
||||||
t.Error(opts.Preview)
|
t.Error(opts.Preview)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAdditiveExpect(t *testing.T) {
|
||||||
|
opts := optsFor("--expect=a", "--expect", "b", "--expect=c")
|
||||||
|
if len(opts.Expect) != 3 {
|
||||||
|
t.Error(opts.Expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateSign(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
inputSign string
|
||||||
|
isValid bool
|
||||||
|
}{
|
||||||
|
{"> ", true},
|
||||||
|
{"아", true},
|
||||||
|
{"😀", true},
|
||||||
|
{"", false},
|
||||||
|
{">>>", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
err := validateSign(testCase.inputSign, "")
|
||||||
|
if testCase.isValid && err != nil {
|
||||||
|
t.Errorf("Input sign `%s` caused error", testCase.inputSign)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.isValid && err == nil {
|
||||||
|
t.Errorf("Input sign `%s` did not cause error", testCase.inputSign)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseSingleActionList(t *testing.T) {
|
||||||
|
actions := parseSingleActionList("Execute@foo+bar,baz@+up+up+reload:down+down", func(string) {})
|
||||||
|
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", func(e string) {
|
||||||
|
err = e
|
||||||
|
})
|
||||||
|
if len(err) == 0 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
161
src/pattern.go
161
src/pattern.go
@@ -1,6 +1,7 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -10,12 +11,12 @@ import (
|
|||||||
|
|
||||||
// fuzzy
|
// fuzzy
|
||||||
// 'exact
|
// 'exact
|
||||||
// ^exact-prefix
|
// ^prefix-exact
|
||||||
// exact-suffix$
|
// suffix-exact$
|
||||||
// !not-fuzzy
|
// !inverse-exact
|
||||||
// !'not-exact
|
// !'inverse-fuzzy
|
||||||
// !^not-exact-prefix
|
// !^inverse-prefix-exact
|
||||||
// !not-exact-suffix$
|
// !inverse-suffix-exact$
|
||||||
|
|
||||||
type termType int
|
type termType int
|
||||||
|
|
||||||
@@ -32,7 +33,12 @@ type term struct {
|
|||||||
inv bool
|
inv bool
|
||||||
text []rune
|
text []rune
|
||||||
caseSensitive bool
|
caseSensitive bool
|
||||||
origText []rune
|
normalize bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of a term.
|
||||||
|
func (t term) String() string {
|
||||||
|
return fmt.Sprintf("term{typ: %d, inv: %v, text: []rune(%q), caseSensitive: %v}", t.typ, t.inv, string(t.text), t.caseSensitive)
|
||||||
}
|
}
|
||||||
|
|
||||||
type termSet []term
|
type termSet []term
|
||||||
@@ -45,9 +51,12 @@ type Pattern struct {
|
|||||||
caseSensitive bool
|
caseSensitive bool
|
||||||
normalize bool
|
normalize bool
|
||||||
forward bool
|
forward bool
|
||||||
|
withPos bool
|
||||||
text []rune
|
text []rune
|
||||||
termSets []termSet
|
termSets []termSet
|
||||||
|
sortable bool
|
||||||
cacheable bool
|
cacheable bool
|
||||||
|
cacheKey string
|
||||||
delimiter Delimiter
|
delimiter Delimiter
|
||||||
nth []Range
|
nth []Range
|
||||||
procFun map[termType]algo.Algo
|
procFun map[termType]algo.Algo
|
||||||
@@ -60,7 +69,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
_splitRegex = regexp.MustCompile("\\s+")
|
_splitRegex = regexp.MustCompile(" +")
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
clearChunkCache()
|
clearChunkCache()
|
||||||
}
|
}
|
||||||
@@ -77,11 +86,14 @@ func clearChunkCache() {
|
|||||||
|
|
||||||
// BuildPattern builds Pattern object from the given arguments
|
// BuildPattern builds Pattern object from the given arguments
|
||||||
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case, normalize bool, forward bool,
|
||||||
cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
withPos bool, cacheable bool, nth []Range, delimiter Delimiter, runes []rune) *Pattern {
|
||||||
|
|
||||||
var asString string
|
var asString string
|
||||||
if extended {
|
if extended {
|
||||||
asString = strings.Trim(string(runes), " ")
|
asString = strings.TrimLeft(string(runes), " ")
|
||||||
|
for strings.HasSuffix(asString, " ") && !strings.HasSuffix(asString, "\\ ") {
|
||||||
|
asString = asString[:len(asString)-1]
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
asString = string(runes)
|
asString = string(runes)
|
||||||
}
|
}
|
||||||
@@ -92,23 +104,34 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
|||||||
}
|
}
|
||||||
|
|
||||||
caseSensitive := true
|
caseSensitive := true
|
||||||
|
sortable := true
|
||||||
termSets := []termSet{}
|
termSets := []termSet{}
|
||||||
|
|
||||||
if extended {
|
if extended {
|
||||||
termSets = parseTerms(fuzzy, caseMode, normalize, asString)
|
termSets = parseTerms(fuzzy, caseMode, normalize, asString)
|
||||||
|
// We should not sort the result if there are only inverse search terms
|
||||||
|
sortable = false
|
||||||
Loop:
|
Loop:
|
||||||
for _, termSet := range termSets {
|
for _, termSet := range termSets {
|
||||||
for idx, term := range termSet {
|
for idx, term := range termSet {
|
||||||
|
if !term.inv {
|
||||||
|
sortable = true
|
||||||
|
}
|
||||||
// If the query contains inverse search terms or OR operators,
|
// If the query contains inverse search terms or OR operators,
|
||||||
// we cannot cache the search scope
|
// we cannot cache the search scope
|
||||||
if !cacheable || idx > 0 || term.inv || !fuzzy && term.typ != termExact {
|
if !cacheable || idx > 0 || term.inv || fuzzy && term.typ != termFuzzy || !fuzzy && term.typ != termExact {
|
||||||
cacheable = false
|
cacheable = false
|
||||||
|
if sortable {
|
||||||
|
// Can't break until we see at least one non-inverse term
|
||||||
break Loop
|
break Loop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lowerString := strings.ToLower(asString)
|
lowerString := strings.ToLower(asString)
|
||||||
|
normalize = normalize &&
|
||||||
|
lowerString == string(algo.NormalizeRunes([]rune(lowerString)))
|
||||||
caseSensitive = caseMode == CaseRespect ||
|
caseSensitive = caseMode == CaseRespect ||
|
||||||
caseMode == CaseSmart && lowerString != asString
|
caseMode == CaseSmart && lowerString != asString
|
||||||
if !caseSensitive {
|
if !caseSensitive {
|
||||||
@@ -123,13 +146,16 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
|||||||
caseSensitive: caseSensitive,
|
caseSensitive: caseSensitive,
|
||||||
normalize: normalize,
|
normalize: normalize,
|
||||||
forward: forward,
|
forward: forward,
|
||||||
|
withPos: withPos,
|
||||||
text: []rune(asString),
|
text: []rune(asString),
|
||||||
termSets: termSets,
|
termSets: termSets,
|
||||||
|
sortable: sortable,
|
||||||
cacheable: cacheable,
|
cacheable: cacheable,
|
||||||
nth: nth,
|
nth: nth,
|
||||||
delimiter: delimiter,
|
delimiter: delimiter,
|
||||||
procFun: make(map[termType]algo.Algo)}
|
procFun: make(map[termType]algo.Algo)}
|
||||||
|
|
||||||
|
ptr.cacheKey = ptr.buildCacheKey()
|
||||||
ptr.procFun[termFuzzy] = fuzzyAlgo
|
ptr.procFun[termFuzzy] = fuzzyAlgo
|
||||||
ptr.procFun[termEqual] = algo.EqualMatch
|
ptr.procFun[termEqual] = algo.EqualMatch
|
||||||
ptr.procFun[termExact] = algo.ExactMatchNaive
|
ptr.procFun[termExact] = algo.ExactMatchNaive
|
||||||
@@ -141,27 +167,32 @@ func BuildPattern(fuzzy bool, fuzzyAlgo algo.Algo, extended bool, caseMode Case,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet {
|
||||||
|
str = strings.Replace(str, "\\ ", "\t", -1)
|
||||||
tokens := _splitRegex.Split(str, -1)
|
tokens := _splitRegex.Split(str, -1)
|
||||||
sets := []termSet{}
|
sets := []termSet{}
|
||||||
set := termSet{}
|
set := termSet{}
|
||||||
switchSet := false
|
switchSet := false
|
||||||
|
afterBar := false
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
typ, inv, text := termFuzzy, false, token
|
typ, inv, text := termFuzzy, false, strings.Replace(token, "\t", " ", -1)
|
||||||
lowerText := strings.ToLower(text)
|
lowerText := strings.ToLower(text)
|
||||||
caseSensitive := caseMode == CaseRespect ||
|
caseSensitive := caseMode == CaseRespect ||
|
||||||
caseMode == CaseSmart && text != lowerText
|
caseMode == CaseSmart && text != lowerText
|
||||||
|
normalizeTerm := normalize &&
|
||||||
|
lowerText == string(algo.NormalizeRunes([]rune(lowerText)))
|
||||||
if !caseSensitive {
|
if !caseSensitive {
|
||||||
text = lowerText
|
text = lowerText
|
||||||
}
|
}
|
||||||
origText := []rune(text)
|
|
||||||
if !fuzzy {
|
if !fuzzy {
|
||||||
typ = termExact
|
typ = termExact
|
||||||
}
|
}
|
||||||
|
|
||||||
if text == "|" {
|
if len(set) > 0 && !afterBar && text == "|" {
|
||||||
switchSet = false
|
switchSet = false
|
||||||
|
afterBar = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
afterBar = false
|
||||||
|
|
||||||
if strings.HasPrefix(text, "!") {
|
if strings.HasPrefix(text, "!") {
|
||||||
inv = true
|
inv = true
|
||||||
@@ -169,6 +200,11 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
text = text[1:]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if text != "$" && strings.HasSuffix(text, "$") {
|
||||||
|
typ = termSuffix
|
||||||
|
text = text[:len(text)-1]
|
||||||
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(text, "'") {
|
if strings.HasPrefix(text, "'") {
|
||||||
// Flip exactness
|
// Flip exactness
|
||||||
if fuzzy && !inv {
|
if fuzzy && !inv {
|
||||||
@@ -179,16 +215,12 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
text = text[1:]
|
text = text[1:]
|
||||||
}
|
}
|
||||||
} else if strings.HasPrefix(text, "^") {
|
} else if strings.HasPrefix(text, "^") {
|
||||||
if strings.HasSuffix(text, "$") {
|
if typ == termSuffix {
|
||||||
typ = termEqual
|
typ = termEqual
|
||||||
text = text[1 : len(text)-1]
|
|
||||||
} else {
|
} else {
|
||||||
typ = termPrefix
|
typ = termPrefix
|
||||||
text = text[1:]
|
|
||||||
}
|
}
|
||||||
} else if strings.HasSuffix(text, "$") {
|
text = text[1:]
|
||||||
typ = termSuffix
|
|
||||||
text = text[:len(text)-1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(text) > 0 {
|
if len(text) > 0 {
|
||||||
@@ -197,7 +229,7 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
set = termSet{}
|
set = termSet{}
|
||||||
}
|
}
|
||||||
textRunes := []rune(text)
|
textRunes := []rune(text)
|
||||||
if normalize {
|
if normalizeTerm {
|
||||||
textRunes = algo.NormalizeRunes(textRunes)
|
textRunes = algo.NormalizeRunes(textRunes)
|
||||||
}
|
}
|
||||||
set = append(set, term{
|
set = append(set, term{
|
||||||
@@ -205,7 +237,7 @@ func parseTerms(fuzzy bool, caseMode Case, normalize bool, str string) []termSet
|
|||||||
inv: inv,
|
inv: inv,
|
||||||
text: textRunes,
|
text: textRunes,
|
||||||
caseSensitive: caseSensitive,
|
caseSensitive: caseSensitive,
|
||||||
origText: origText})
|
normalize: normalizeTerm})
|
||||||
switchSet = true
|
switchSet = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,46 +260,36 @@ func (p *Pattern) AsString() string {
|
|||||||
return string(p.text)
|
return string(p.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheKey is used to build string to be used as the key of result cache
|
func (p *Pattern) buildCacheKey() string {
|
||||||
func (p *Pattern) CacheKey() string {
|
|
||||||
if !p.extended {
|
if !p.extended {
|
||||||
return p.AsString()
|
return p.AsString()
|
||||||
}
|
}
|
||||||
cacheableTerms := []string{}
|
cacheableTerms := []string{}
|
||||||
for _, termSet := range p.termSets {
|
for _, termSet := range p.termSets {
|
||||||
if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) {
|
if len(termSet) == 1 && !termSet[0].inv && (p.fuzzy || termSet[0].typ == termExact) {
|
||||||
cacheableTerms = append(cacheableTerms, string(termSet[0].origText))
|
cacheableTerms = append(cacheableTerms, string(termSet[0].text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(cacheableTerms, " ")
|
return strings.Join(cacheableTerms, "\t")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheKey is used to build string to be used as the key of result cache
|
||||||
|
func (p *Pattern) CacheKey() string {
|
||||||
|
return p.cacheKey
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match returns the list of matches Items in the given Chunk
|
// Match returns the list of matches Items in the given Chunk
|
||||||
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []*Result {
|
func (p *Pattern) Match(chunk *Chunk, slab *util.Slab) []Result {
|
||||||
// ChunkCache: Exact match
|
// ChunkCache: Exact match
|
||||||
cacheKey := p.CacheKey()
|
cacheKey := p.CacheKey()
|
||||||
if p.cacheable {
|
if p.cacheable {
|
||||||
if cached, found := _cache.Find(chunk, cacheKey); found {
|
if cached := _cache.Lookup(chunk, cacheKey); cached != nil {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefix/suffix cache
|
// Prefix/suffix cache
|
||||||
var space []*Result
|
space := _cache.Search(chunk, cacheKey)
|
||||||
Loop:
|
|
||||||
for idx := 1; idx < len(cacheKey); idx++ {
|
|
||||||
// [---------| ] | [ |---------]
|
|
||||||
// [--------| ] | [ |--------]
|
|
||||||
// [-------| ] | [ |-------]
|
|
||||||
prefix := cacheKey[:len(cacheKey)-idx]
|
|
||||||
suffix := cacheKey[idx:]
|
|
||||||
for _, substr := range [2]*string{&prefix, &suffix} {
|
|
||||||
if cached, found := _cache.Find(chunk, *substr); found {
|
|
||||||
space = cached
|
|
||||||
break Loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
matches := p.matchChunk(chunk, space, slab)
|
matches := p.matchChunk(chunk, space, slab)
|
||||||
|
|
||||||
@@ -277,19 +299,19 @@ Loop:
|
|||||||
return matches
|
return matches
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*Result {
|
func (p *Pattern) matchChunk(chunk *Chunk, space []Result, slab *util.Slab) []Result {
|
||||||
matches := []*Result{}
|
matches := []Result{}
|
||||||
|
|
||||||
if space == nil {
|
if space == nil {
|
||||||
for _, item := range *chunk {
|
for idx := 0; idx < chunk.count; idx++ {
|
||||||
if match, _, _ := p.MatchItem(item, false, slab); match != nil {
|
if match, _, _ := p.MatchItem(&chunk.items[idx], p.withPos, slab); match != nil {
|
||||||
matches = append(matches, match)
|
matches = append(matches, *match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, result := range space {
|
for _, result := range space {
|
||||||
if match, _, _ := p.MatchItem(result.item, false, slab); match != nil {
|
if match, _, _ := p.MatchItem(result.item, p.withPos, slab); match != nil {
|
||||||
matches = append(matches, match)
|
matches = append(matches, *match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,20 +322,27 @@ func (p *Pattern) matchChunk(chunk *Chunk, space []*Result, slab *util.Slab) []*
|
|||||||
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
func (p *Pattern) MatchItem(item *Item, withPos bool, slab *util.Slab) (*Result, []Offset, *[]int) {
|
||||||
if p.extended {
|
if p.extended {
|
||||||
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
if offsets, bonus, pos := p.extendedMatch(item, withPos, slab); len(offsets) == len(p.termSets) {
|
||||||
return buildResult(item, offsets, bonus), offsets, pos
|
result := buildResult(item, offsets, bonus)
|
||||||
|
return &result, offsets, pos
|
||||||
}
|
}
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
offset, bonus, pos := p.basicMatch(item, withPos, slab)
|
||||||
if sidx := offset[0]; sidx >= 0 {
|
if sidx := offset[0]; sidx >= 0 {
|
||||||
offsets := []Offset{offset}
|
offsets := []Offset{offset}
|
||||||
return buildResult(item, offsets, bonus), offsets, pos
|
result := buildResult(item, offsets, bonus)
|
||||||
|
return &result, offsets, pos
|
||||||
}
|
}
|
||||||
return nil, nil, nil
|
return nil, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||||
input := p.prepareInput(item)
|
var input []Token
|
||||||
|
if len(p.nth) == 0 {
|
||||||
|
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||||
|
} else {
|
||||||
|
input = p.transformInput(item)
|
||||||
|
}
|
||||||
if p.fuzzy {
|
if p.fuzzy {
|
||||||
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
return p.iter(p.fuzzyAlgo, input, p.caseSensitive, p.normalize, p.forward, p.text, withPos, slab)
|
||||||
}
|
}
|
||||||
@@ -321,7 +350,12 @@ func (p *Pattern) basicMatch(item *Item, withPos bool, slab *util.Slab) (Offset,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Offset, int, *[]int) {
|
||||||
input := p.prepareInput(item)
|
var input []Token
|
||||||
|
if len(p.nth) == 0 {
|
||||||
|
input = []Token{{text: &item.text, prefixLength: 0}}
|
||||||
|
} else {
|
||||||
|
input = p.transformInput(item)
|
||||||
|
}
|
||||||
offsets := []Offset{}
|
offsets := []Offset{}
|
||||||
var totalScore int
|
var totalScore int
|
||||||
var allPos *[]int
|
var allPos *[]int
|
||||||
@@ -334,7 +368,7 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
|||||||
matched := false
|
matched := false
|
||||||
for _, term := range termSet {
|
for _, term := range termSet {
|
||||||
pfun := p.procFun[term.typ]
|
pfun := p.procFun[term.typ]
|
||||||
off, score, pos := p.iter(pfun, input, term.caseSensitive, p.normalize, p.forward, term.text, withPos, slab)
|
off, score, pos := p.iter(pfun, input, term.caseSensitive, term.normalize, p.forward, term.text, withPos, slab)
|
||||||
if sidx := off[0]; sidx >= 0 {
|
if sidx := off[0]; sidx >= 0 {
|
||||||
if term.inv {
|
if term.inv {
|
||||||
continue
|
continue
|
||||||
@@ -365,25 +399,20 @@ func (p *Pattern) extendedMatch(item *Item, withPos bool, slab *util.Slab) ([]Of
|
|||||||
return offsets, totalScore, allPos
|
return offsets, totalScore, allPos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) prepareInput(item *Item) []Token {
|
func (p *Pattern) transformInput(item *Item) []Token {
|
||||||
if item.transformed != nil {
|
if item.transformed != nil {
|
||||||
return item.transformed
|
return *item.transformed
|
||||||
}
|
}
|
||||||
|
|
||||||
var ret []Token
|
tokens := Tokenize(item.text.ToString(), p.delimiter)
|
||||||
if len(p.nth) == 0 {
|
ret := Transform(tokens, p.nth)
|
||||||
ret = []Token{Token{text: &item.text, prefixLength: 0}}
|
item.transformed = &ret
|
||||||
} else {
|
|
||||||
tokens := Tokenize(item.text, p.delimiter)
|
|
||||||
ret = Transform(tokens, p.nth)
|
|
||||||
}
|
|
||||||
item.transformed = ret
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
func (p *Pattern) iter(pfun algo.Algo, tokens []Token, caseSensitive bool, normalize bool, forward bool, pattern []rune, withPos bool, slab *util.Slab) (Offset, int, *[]int) {
|
||||||
for _, part := range tokens {
|
for _, part := range tokens {
|
||||||
if res, pos := pfun(caseSensitive, normalize, forward, *part.text, pattern, withPos, slab); res.Start >= 0 {
|
if res, pos := pfun(caseSensitive, normalize, forward, part.text, pattern, withPos, slab); res.Start >= 0 {
|
||||||
sidx := int32(res.Start) + part.prefixLength
|
sidx := int32(res.Start) + part.prefixLength
|
||||||
eidx := int32(res.End) + part.prefixLength
|
eidx := int32(res.End) + part.prefixLength
|
||||||
if pos != nil {
|
if pos != nil {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func init() {
|
|||||||
|
|
||||||
func TestParseTermsExtended(t *testing.T) {
|
func TestParseTermsExtended(t *testing.T) {
|
||||||
terms := parseTerms(true, CaseSmart, false,
|
terms := parseTerms(true, CaseSmart, false,
|
||||||
"| aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | | zzz$ | !ZZZ |")
|
"aaa 'bbb ^ccc ddd$ !eee !'fff !^ggg !hhh$ | ^iii$ ^xxx | 'yyy | zzz$ | !ZZZ |")
|
||||||
if len(terms) != 9 ||
|
if len(terms) != 9 ||
|
||||||
terms[0][0].typ != termFuzzy || terms[0][0].inv ||
|
terms[0][0].typ != termFuzzy || terms[0][0].inv ||
|
||||||
terms[1][0].typ != termExact || terms[1][0].inv ||
|
terms[1][0].typ != termExact || terms[1][0].inv ||
|
||||||
@@ -31,20 +31,12 @@ func TestParseTermsExtended(t *testing.T) {
|
|||||||
terms[8][1].typ != termExact || terms[8][1].inv ||
|
terms[8][1].typ != termExact || terms[8][1].inv ||
|
||||||
terms[8][2].typ != termSuffix || terms[8][2].inv ||
|
terms[8][2].typ != termSuffix || terms[8][2].inv ||
|
||||||
terms[8][3].typ != termExact || !terms[8][3].inv {
|
terms[8][3].typ != termExact || !terms[8][3].inv {
|
||||||
t.Errorf("%s", terms)
|
t.Errorf("%v", terms)
|
||||||
}
|
}
|
||||||
for idx, termSet := range terms[:8] {
|
for _, termSet := range terms[:8] {
|
||||||
term := termSet[0]
|
term := termSet[0]
|
||||||
if len(term.text) != 3 {
|
if len(term.text) != 3 {
|
||||||
t.Errorf("%s", term)
|
t.Errorf("%v", term)
|
||||||
}
|
|
||||||
if idx > 0 && len(term.origText) != 4+idx/5 {
|
|
||||||
t.Errorf("%s", term)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, term := range terms[8] {
|
|
||||||
if len(term.origText) != 4 {
|
|
||||||
t.Errorf("%s", term)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,26 +53,27 @@ func TestParseTermsExtendedExact(t *testing.T) {
|
|||||||
terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 ||
|
terms[5][0].typ != termFuzzy || !terms[5][0].inv || len(terms[5][0].text) != 3 ||
|
||||||
terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 ||
|
terms[6][0].typ != termPrefix || !terms[6][0].inv || len(terms[6][0].text) != 3 ||
|
||||||
terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 {
|
terms[7][0].typ != termSuffix || !terms[7][0].inv || len(terms[7][0].text) != 3 {
|
||||||
t.Errorf("%s", terms)
|
t.Errorf("%v", terms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTermsEmpty(t *testing.T) {
|
func TestParseTermsEmpty(t *testing.T) {
|
||||||
terms := parseTerms(true, CaseSmart, false, "' $ ^ !' !^ !$")
|
terms := parseTerms(true, CaseSmart, false, "' ^ !' !^")
|
||||||
if len(terms) != 0 {
|
if len(terms) != 0 {
|
||||||
t.Errorf("%s", terms)
|
t.Errorf("%v", terms)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExact(t *testing.T) {
|
func TestExact(t *testing.T) {
|
||||||
defer clearPatternCache()
|
defer clearPatternCache()
|
||||||
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"))
|
[]Range{}, Delimiter{}, []rune("'abc"))
|
||||||
|
chars := util.ToChars([]byte("aabbcc abc"))
|
||||||
res, pos := algo.ExactMatchNaive(
|
res, pos := algo.ExactMatchNaive(
|
||||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune("aabbcc abc")), pattern.termSets[0][0].text, true, nil)
|
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
|
||||||
if res.Start != 7 || res.End != 10 {
|
if res.Start != 7 || res.End != 10 {
|
||||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End)
|
||||||
}
|
}
|
||||||
if pos != nil {
|
if pos != nil {
|
||||||
t.Errorf("pos is expected to be nil")
|
t.Errorf("pos is expected to be nil")
|
||||||
@@ -90,13 +83,14 @@ func TestExact(t *testing.T) {
|
|||||||
func TestEqual(t *testing.T) {
|
func TestEqual(t *testing.T) {
|
||||||
defer clearPatternCache()
|
defer clearPatternCache()
|
||||||
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) {
|
match := func(str string, sidxExpected int, eidxExpected int) {
|
||||||
|
chars := util.ToChars([]byte(str))
|
||||||
res, pos := algo.EqualMatch(
|
res, pos := algo.EqualMatch(
|
||||||
pattern.caseSensitive, pattern.normalize, pattern.forward, util.RunesToChars([]rune(str)), pattern.termSets[0][0].text, true, nil)
|
pattern.caseSensitive, pattern.normalize, pattern.forward, &chars, pattern.termSets[0][0].text, true, nil)
|
||||||
if res.Start != sidxExpected || res.End != eidxExpected {
|
if res.Start != sidxExpected || res.End != eidxExpected {
|
||||||
t.Errorf("%s / %d / %d", pattern.termSets, res.Start, res.End)
|
t.Errorf("%v / %d / %d", pattern.termSets, res.Start, res.End)
|
||||||
}
|
}
|
||||||
if pos != nil {
|
if pos != nil {
|
||||||
t.Errorf("pos is expected to be nil")
|
t.Errorf("pos is expected to be nil")
|
||||||
@@ -104,22 +98,25 @@ func TestEqual(t *testing.T) {
|
|||||||
}
|
}
|
||||||
match("ABC", -1, -1)
|
match("ABC", -1, -1)
|
||||||
match("AbC", 0, 3)
|
match("AbC", 0, 3)
|
||||||
|
match("AbC ", 0, 3)
|
||||||
|
match(" AbC ", 1, 4)
|
||||||
|
match(" AbC", 2, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCaseSensitivity(t *testing.T) {
|
func TestCaseSensitivity(t *testing.T) {
|
||||||
defer clearPatternCache()
|
defer clearPatternCache()
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
pat1 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
pat2 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
pat3 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, true, []Range{}, Delimiter{}, []rune("Abc"))
|
pat4 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseIgnore, false, true, false, true, []Range{}, Delimiter{}, []rune("Abc"))
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, true, []Range{}, Delimiter{}, []rune("abc"))
|
pat5 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, false, true, []Range{}, Delimiter{}, []rune("abc"))
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
pat6 := BuildPattern(true, algo.FuzzyMatchV2, false, CaseRespect, false, true, 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 ||
|
if string(pat1.text) != "abc" || pat1.caseSensitive != false ||
|
||||||
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
string(pat2.text) != "Abc" || pat2.caseSensitive != true ||
|
||||||
@@ -132,31 +129,30 @@ func TestCaseSensitivity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestOrigTextAndTransformed(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(util.RunesToChars([]rune("junegunn")), Delimiter{})
|
tokens := Tokenize("junegunn", Delimiter{})
|
||||||
trans := Transform(tokens, []Range{Range{1, 1}})
|
trans := Transform(tokens, []Range{{1, 1}})
|
||||||
|
|
||||||
origBytes := []byte("junegunn.choi")
|
origBytes := []byte("junegunn.choi")
|
||||||
for _, extended := range []bool{false, true} {
|
for _, extended := range []bool{false, true} {
|
||||||
chunk := Chunk{
|
chunk := Chunk{count: 1}
|
||||||
&Item{
|
chunk.items[0] = Item{
|
||||||
text: util.RunesToChars([]rune("junegunn")),
|
text: util.ToChars([]byte("junegunn")),
|
||||||
origText: &origBytes,
|
origText: &origBytes,
|
||||||
transformed: trans},
|
transformed: &trans}
|
||||||
}
|
|
||||||
pattern.extended = extended
|
pattern.extended = extended
|
||||||
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
matches := pattern.matchChunk(&chunk, nil, slab) // No cache
|
||||||
if !(matches[0].item.text.ToString() == "junegunn" &&
|
if !(matches[0].item.text.ToString() == "junegunn" &&
|
||||||
string(*matches[0].item.origText) == "junegunn.choi" &&
|
string(*matches[0].item.origText) == "junegunn.choi" &&
|
||||||
reflect.DeepEqual(matches[0].item.transformed, trans)) {
|
reflect.DeepEqual(*matches[0].item.transformed, trans)) {
|
||||||
t.Error("Invalid match result", matches)
|
t.Error("Invalid match result", matches)
|
||||||
}
|
}
|
||||||
|
|
||||||
match, offsets, pos := pattern.MatchItem(chunk[0], true, slab)
|
match, offsets, pos := pattern.MatchItem(&chunk.items[0], true, slab)
|
||||||
if !(match.item.text.ToString() == "junegunn" &&
|
if !(match.item.text.ToString() == "junegunn" &&
|
||||||
string(*match.item.origText) == "junegunn.choi" &&
|
string(*match.item.origText) == "junegunn.choi" &&
|
||||||
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
offsets[0][0] == 0 && offsets[0][1] == 5 &&
|
||||||
reflect.DeepEqual(match.item.transformed, trans)) {
|
reflect.DeepEqual(*match.item.transformed, trans)) {
|
||||||
t.Error("Invalid match result", match, offsets, extended)
|
t.Error("Invalid match result", match, offsets, extended)
|
||||||
}
|
}
|
||||||
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
if !((*pos)[0] == 4 && (*pos)[1] == 0) {
|
||||||
@@ -167,40 +163,47 @@ func TestOrigTextAndTransformed(t *testing.T) {
|
|||||||
|
|
||||||
func TestCacheKey(t *testing.T) {
|
func TestCacheKey(t *testing.T) {
|
||||||
test := func(extended bool, patStr string, expected string, cacheable bool) {
|
test := func(extended bool, patStr string, expected string, cacheable bool) {
|
||||||
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, true, []Range{}, Delimiter{}, []rune(patStr))
|
clearPatternCache()
|
||||||
|
pat := BuildPattern(true, algo.FuzzyMatchV2, extended, CaseSmart, false, true, false, true, []Range{}, Delimiter{}, []rune(patStr))
|
||||||
if pat.CacheKey() != expected {
|
if pat.CacheKey() != expected {
|
||||||
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
t.Errorf("Expected: %s, actual: %s", expected, pat.CacheKey())
|
||||||
}
|
}
|
||||||
if pat.cacheable != cacheable {
|
if pat.cacheable != cacheable {
|
||||||
t.Errorf("Expected: %s, actual: %s (%s)", cacheable, pat.cacheable, patStr)
|
t.Errorf("Expected: %t, actual: %t (%s)", cacheable, pat.cacheable, patStr)
|
||||||
}
|
}
|
||||||
clearPatternCache()
|
clearPatternCache()
|
||||||
}
|
}
|
||||||
test(false, "foo !bar", "foo !bar", true)
|
test(false, "foo !bar", "foo !bar", true)
|
||||||
test(false, "foo | bar !baz", "foo | bar !baz", true)
|
test(false, "foo | bar !baz", "foo | bar !baz", true)
|
||||||
test(true, "foo bar baz", "foo bar baz", true)
|
test(true, "foo bar baz", "foo\tbar\tbaz", true)
|
||||||
test(true, "foo !bar", "foo", false)
|
test(true, "foo !bar", "foo", false)
|
||||||
test(true, "foo !bar baz", "foo baz", false)
|
test(true, "foo !bar baz", "foo\tbaz", false)
|
||||||
test(true, "foo | bar baz", "baz", false)
|
test(true, "foo | bar baz", "baz", false)
|
||||||
test(true, "foo | bar | baz", "", false)
|
test(true, "foo | bar | baz", "", false)
|
||||||
test(true, "foo | bar !baz", "", false)
|
test(true, "foo | bar !baz", "", false)
|
||||||
test(true, "| | | foo", "foo", true)
|
test(true, "| | foo", "", false)
|
||||||
|
test(true, "| | | foo", "foo", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCacheable(t *testing.T) {
|
func TestCacheable(t *testing.T) {
|
||||||
test := func(fuzzy bool, str string, cacheable bool) {
|
test := func(fuzzy bool, str string, expected string, cacheable bool) {
|
||||||
clearPatternCache()
|
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 {
|
if cacheable != pat.cacheable {
|
||||||
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
|
t.Errorf("Invalid Pattern.cacheable for \"%s\": %v (expected: %v)", str, pat.cacheable, cacheable)
|
||||||
}
|
}
|
||||||
|
clearPatternCache()
|
||||||
}
|
}
|
||||||
test(true, "foo bar", true)
|
test(true, "foo bar", "foo\tbar", true)
|
||||||
test(true, "foo 'bar", true)
|
test(true, "foo 'bar", "foo\tbar", false)
|
||||||
test(true, "foo !bar", false)
|
test(true, "foo !bar", "foo", false)
|
||||||
|
|
||||||
test(false, "foo bar", true)
|
test(false, "foo bar", "foo\tbar", true)
|
||||||
test(false, "foo '", true)
|
test(false, "foo 'bar", "foo", false)
|
||||||
test(false, "foo 'bar", false)
|
test(false, "foo '", "foo", true)
|
||||||
test(false, "foo !bar", false)
|
test(false, "foo 'bar", "foo", false)
|
||||||
|
test(false, "foo !bar", "foo", false)
|
||||||
}
|
}
|
||||||
|
|||||||
8
src/protector/protector.go
Normal file
8
src/protector/protector.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
//go:build !openbsd
|
||||||
|
|
||||||
|
package protector
|
||||||
|
|
||||||
|
// Protect calls OS specific protections like pledge on OpenBSD
|
||||||
|
func Protect() {
|
||||||
|
return
|
||||||
|
}
|
||||||
10
src/protector/protector_openbsd.go
Normal file
10
src/protector/protector_openbsd.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
//go:build openbsd
|
||||||
|
|
||||||
|
package protector
|
||||||
|
|
||||||
|
import "golang.org/x/sys/unix"
|
||||||
|
|
||||||
|
// Protect calls OS specific protections like pledge on OpenBSD
|
||||||
|
func Protect() {
|
||||||
|
unix.PledgePromises("stdio rpath tty proc exec inet")
|
||||||
|
}
|
||||||
167
src/reader.go
167
src/reader.go
@@ -2,10 +2,18 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
"github.com/saracen/walker"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reader reads from command or standard input
|
// Reader reads from command or standard input
|
||||||
@@ -13,20 +21,109 @@ type Reader struct {
|
|||||||
pusher func([]byte) bool
|
pusher func([]byte) bool
|
||||||
eventBox *util.EventBox
|
eventBox *util.EventBox
|
||||||
delimNil bool
|
delimNil bool
|
||||||
|
event int32
|
||||||
|
finChan chan bool
|
||||||
|
mutex sync.Mutex
|
||||||
|
exec *exec.Cmd
|
||||||
|
command *string
|
||||||
|
killed bool
|
||||||
|
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 (r *Reader) startEventPoller() {
|
||||||
|
go func() {
|
||||||
|
ptr := &r.event
|
||||||
|
pollInterval := readerPollIntervalMin
|
||||||
|
for {
|
||||||
|
if atomic.CompareAndSwapInt32(ptr, int32(EvtReadNew), int32(EvtReady)) {
|
||||||
|
r.eventBox.Set(EvtReadNew, (*string)(nil))
|
||||||
|
pollInterval = readerPollIntervalMin
|
||||||
|
} else if atomic.LoadInt32(ptr) == int32(EvtReadFin) {
|
||||||
|
if r.wait {
|
||||||
|
r.finChan <- true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
pollInterval += readerPollIntervalStep
|
||||||
|
if pollInterval > readerPollIntervalMax {
|
||||||
|
pollInterval = readerPollIntervalMax
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(pollInterval)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) fin(success bool) {
|
||||||
|
atomic.StoreInt32(&r.event, int32(EvtReadFin))
|
||||||
|
if r.wait {
|
||||||
|
<-r.finChan
|
||||||
|
}
|
||||||
|
|
||||||
|
r.mutex.Lock()
|
||||||
|
ret := r.command
|
||||||
|
if success || r.killed {
|
||||||
|
ret = nil
|
||||||
|
}
|
||||||
|
r.mutex.Unlock()
|
||||||
|
|
||||||
|
r.eventBox.Set(EvtReadFin, ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) restart(command string) {
|
||||||
|
r.event = int32(EvtReady)
|
||||||
|
r.startEventPoller()
|
||||||
|
success := r.readFromCommand(nil, command)
|
||||||
|
r.fin(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadSource reads data from the default command or from standard input
|
// ReadSource reads data from the default command or from standard input
|
||||||
func (r *Reader) ReadSource() {
|
func (r *Reader) ReadSource() {
|
||||||
|
r.startEventPoller()
|
||||||
|
var success bool
|
||||||
if util.IsTty() {
|
if util.IsTty() {
|
||||||
|
// The default command for *nix requires a shell that supports "pipefail"
|
||||||
|
// https://unix.stackexchange.com/a/654932/62171
|
||||||
|
shell := "bash"
|
||||||
|
currentShell := os.Getenv("SHELL")
|
||||||
|
currentShellName := path.Base(currentShell)
|
||||||
|
for _, shellName := range []string{"bash", "zsh", "ksh", "ash", "hush", "mksh", "yash"} {
|
||||||
|
if currentShellName == shellName {
|
||||||
|
shell = currentShell
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
cmd := os.Getenv("FZF_DEFAULT_COMMAND")
|
||||||
if len(cmd) == 0 {
|
if len(cmd) == 0 {
|
||||||
cmd = defaultCommand
|
if defaultCommand != "" {
|
||||||
}
|
success = r.readFromCommand(&shell, defaultCommand)
|
||||||
r.readFromCommand(cmd)
|
|
||||||
} else {
|
} else {
|
||||||
r.readFromStdin()
|
success = r.readFiles()
|
||||||
}
|
}
|
||||||
r.eventBox.Set(EvtReadFin, nil)
|
} else {
|
||||||
|
success = r.readFromCommand(nil, cmd)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
success = r.readFromStdin()
|
||||||
|
}
|
||||||
|
r.fin(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) feed(src io.Reader) {
|
func (r *Reader) feed(src io.Reader) {
|
||||||
@@ -40,7 +137,7 @@ func (r *Reader) feed(src io.Reader) {
|
|||||||
// end in delim.
|
// end in delim.
|
||||||
bytea, err := reader.ReadBytes(delim)
|
bytea, err := reader.ReadBytes(delim)
|
||||||
byteaLen := len(bytea)
|
byteaLen := len(bytea)
|
||||||
if len(bytea) > 0 {
|
if byteaLen > 0 {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// get rid of carriage return if under Windows:
|
// get rid of carriage return if under Windows:
|
||||||
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
|
if util.IsWindows() && byteaLen >= 2 && bytea[byteaLen-2] == byte('\r') {
|
||||||
@@ -50,7 +147,7 @@ func (r *Reader) feed(src io.Reader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if r.pusher(bytea) {
|
if r.pusher(bytea) {
|
||||||
r.eventBox.Set(EvtReadNew, nil)
|
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,20 +156,56 @@ func (r *Reader) feed(src io.Reader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readFromStdin() {
|
func (r *Reader) readFromStdin() bool {
|
||||||
r.feed(os.Stdin)
|
r.feed(os.Stdin)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reader) readFromCommand(cmd string) {
|
func (r *Reader) readFiles() bool {
|
||||||
listCommand := util.ExecCommand(cmd)
|
r.killed = false
|
||||||
out, err := listCommand.StdoutPipe()
|
fn := func(path string, mode os.FileInfo) error {
|
||||||
if err != nil {
|
path = filepath.Clean(path)
|
||||||
return
|
if path != "." {
|
||||||
|
isDir := mode.Mode().IsDir()
|
||||||
|
if isDir && filepath.Base(path)[0] == '.' {
|
||||||
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
err = listCommand.Start()
|
if !isDir && r.pusher([]byte(path)) {
|
||||||
if err != nil {
|
atomic.StoreInt32(&r.event, int32(EvtReadNew))
|
||||||
return
|
}
|
||||||
|
}
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
if r.killed {
|
||||||
|
return context.Canceled
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
cb := walker.WithErrorCallback(func(pathname string, err error) error {
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return walker.Walk(".", fn, cb) == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Reader) readFromCommand(shell *string, command string) bool {
|
||||||
|
r.mutex.Lock()
|
||||||
|
r.killed = false
|
||||||
|
r.command = &command
|
||||||
|
if shell != nil {
|
||||||
|
r.exec = util.ExecCommandWith(*shell, command, true)
|
||||||
|
} else {
|
||||||
|
r.exec = util.ExecCommand(command, true)
|
||||||
|
}
|
||||||
|
out, err := r.exec.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
r.mutex.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
err = r.exec.Start()
|
||||||
|
r.mutex.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
defer listCommand.Wait()
|
|
||||||
r.feed(out)
|
r.feed(out)
|
||||||
|
return r.exec.Wait() == nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
@@ -9,9 +10,11 @@ import (
|
|||||||
func TestReadFromCommand(t *testing.T) {
|
func TestReadFromCommand(t *testing.T) {
|
||||||
strs := []string{}
|
strs := []string{}
|
||||||
eb := util.NewEventBox()
|
eb := util.NewEventBox()
|
||||||
reader := Reader{
|
reader := NewReader(
|
||||||
pusher: func(s []byte) bool { strs = append(strs, string(s)); return true },
|
func(s []byte) bool { strs = append(strs, string(s)); return true },
|
||||||
eventBox: eb}
|
eb, false, true)
|
||||||
|
|
||||||
|
reader.startEventPoller()
|
||||||
|
|
||||||
// Check EventBox
|
// Check EventBox
|
||||||
if eb.Peek(EvtReadNew) {
|
if eb.Peek(EvtReadNew) {
|
||||||
@@ -19,21 +22,16 @@ func TestReadFromCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normal command
|
// Normal command
|
||||||
reader.readFromCommand(`echo abc && echo def`)
|
reader.fin(reader.readFromCommand(nil, `echo abc&&echo def`))
|
||||||
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
if len(strs) != 2 || strs[0] != "abc" || strs[1] != "def" {
|
||||||
t.Errorf("%s", strs)
|
t.Errorf("%s", strs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check EventBox again
|
// Check EventBox again
|
||||||
if !eb.Peek(EvtReadNew) {
|
eb.WaitFor(EvtReadFin)
|
||||||
t.Error("EvtReadNew should be set yet")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait should return immediately
|
// Wait should return immediately
|
||||||
eb.Wait(func(events *util.Events) {
|
eb.Wait(func(events *util.Events) {
|
||||||
if _, found := (*events)[EvtReadNew]; !found {
|
|
||||||
t.Errorf("%s", events)
|
|
||||||
}
|
|
||||||
events.Clear()
|
events.Clear()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -42,8 +40,14 @@ func TestReadFromCommand(t *testing.T) {
|
|||||||
t.Error("EvtReadNew should not be set yet")
|
t.Error("EvtReadNew should not be set yet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Make sure that event poller is finished
|
||||||
|
time.Sleep(readerPollIntervalMax)
|
||||||
|
|
||||||
|
// Restart event poller
|
||||||
|
reader.startEventPoller()
|
||||||
|
|
||||||
// Failing command
|
// Failing command
|
||||||
reader.readFromCommand(`no-such-command`)
|
reader.fin(reader.readFromCommand(nil, `no-such-command`))
|
||||||
strs = []string{}
|
strs = []string{}
|
||||||
if len(strs) > 0 {
|
if len(strs) > 0 {
|
||||||
t.Errorf("%s", strs)
|
t.Errorf("%s", strs)
|
||||||
@@ -51,6 +55,9 @@ func TestReadFromCommand(t *testing.T) {
|
|||||||
|
|
||||||
// Check EventBox again
|
// Check EventBox again
|
||||||
if eb.Peek(EvtReadNew) {
|
if eb.Peek(EvtReadNew) {
|
||||||
t.Error("Command failed. EvtReadNew should be set")
|
t.Error("Command failed. EvtReadNew should not be set")
|
||||||
|
}
|
||||||
|
if !eb.Peek(EvtReadFin) {
|
||||||
|
t.Error("EvtReadFin should be set")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
123
src/result.go
123
src/result.go
@@ -15,26 +15,19 @@ type Offset [2]int32
|
|||||||
type colorOffset struct {
|
type colorOffset struct {
|
||||||
offset [2]int32
|
offset [2]int32
|
||||||
color tui.ColorPair
|
color tui.ColorPair
|
||||||
attr tui.Attr
|
|
||||||
index int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type rank struct {
|
|
||||||
points [4]uint16
|
|
||||||
index int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
item *Item
|
item *Item
|
||||||
rank rank
|
points [4]uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildResult(item *Item, offsets []Offset, score int) *Result {
|
func buildResult(item *Item, offsets []Offset, score int) Result {
|
||||||
if len(offsets) > 1 {
|
if len(offsets) > 1 {
|
||||||
sort.Sort(ByOrder(offsets))
|
sort.Sort(ByOrder(offsets))
|
||||||
}
|
}
|
||||||
|
|
||||||
result := Result{item: item, rank: rank{index: item.index}}
|
result := Result{item: item}
|
||||||
numChars := item.text.Length()
|
numChars := item.text.Length()
|
||||||
minBegin := math.MaxUint16
|
minBegin := math.MaxUint16
|
||||||
minEnd := math.MaxUint16
|
minEnd := math.MaxUint16
|
||||||
@@ -56,8 +49,24 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
|
|||||||
case byScore:
|
case byScore:
|
||||||
// Higher is better
|
// Higher is better
|
||||||
val = math.MaxUint16 - util.AsUint16(score)
|
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:
|
case byLength:
|
||||||
val = util.AsUint16(int(item.TrimLength()))
|
val = item.TrimLength()
|
||||||
case byBegin, byEnd:
|
case byBegin, byEnd:
|
||||||
if validOffsetFound {
|
if validOffsetFound {
|
||||||
whitePrefixLen := 0
|
whitePrefixLen := 0
|
||||||
@@ -75,10 +84,10 @@ func buildResult(item *Item, offsets []Offset, score int) *Result {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.rank.points[idx] = val
|
result.points[3-idx] = val
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort criteria to use. Never changes once fzf is started.
|
// Sort criteria to use. Never changes once fzf is started.
|
||||||
@@ -86,21 +95,21 @@ var sortCriteria []criterion
|
|||||||
|
|
||||||
// Index returns ordinal index of the Item
|
// Index returns ordinal index of the Item
|
||||||
func (result *Result) Index() int32 {
|
func (result *Result) Index() int32 {
|
||||||
return result.item.index
|
return result.item.Index()
|
||||||
}
|
}
|
||||||
|
|
||||||
func minRank() rank {
|
func minRank() Result {
|
||||||
return rank{index: 0, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
return Result{item: &minItem, points: [4]uint16{math.MaxUint16, 0, 0, 0}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, color tui.ColorPair, attr tui.Attr, current bool) []colorOffset {
|
func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme, colBase tui.ColorPair, colMatch tui.ColorPair, current bool) []colorOffset {
|
||||||
itemColors := result.item.Colors()
|
itemColors := result.item.Colors()
|
||||||
|
|
||||||
// No ANSI code, or --color=no
|
// No ANSI codes
|
||||||
if len(itemColors) == 0 {
|
if len(itemColors) == 0 {
|
||||||
var offsets []colorOffset
|
var offsets []colorOffset
|
||||||
for _, off := range matchOffsets {
|
for _, off := range matchOffsets {
|
||||||
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: color, attr: attr})
|
offsets = append(offsets, colorOffset{offset: [2]int32{off[0], off[1]}, color: colMatch})
|
||||||
}
|
}
|
||||||
return offsets
|
return offsets
|
||||||
}
|
}
|
||||||
@@ -117,17 +126,19 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
|||||||
maxCol = ansi.offset[1]
|
maxCol = ansi.offset[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cols := make([]int, maxCol)
|
|
||||||
|
|
||||||
|
cols := make([]int, maxCol)
|
||||||
for colorIndex, ansi := range itemColors {
|
for colorIndex, ansi := range itemColors {
|
||||||
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
for i := ansi.offset[0]; i < ansi.offset[1]; i++ {
|
||||||
cols[i] = colorIndex + 1 // XXX
|
cols[i] = colorIndex + 1 // 1-based index of itemColors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, off := range matchOffsets {
|
for _, off := range matchOffsets {
|
||||||
for i := off[0]; i < off[1]; i++ {
|
for i := off[0]; i < off[1]; i++ {
|
||||||
cols[i] = -1
|
// Negative of 1-based index of itemColors
|
||||||
|
// - The extra -1 means highlighted
|
||||||
|
cols[i] = cols[i]*-1 - 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,36 +150,53 @@ func (result *Result) colorOffsets(matchOffsets []Offset, theme *tui.ColorTheme,
|
|||||||
// --++++++++-- --++++++++++---
|
// --++++++++-- --++++++++++---
|
||||||
curr := 0
|
curr := 0
|
||||||
start := 0
|
start := 0
|
||||||
var colors []colorOffset
|
ansiToColorPair := func(ansi ansiOffset, base tui.ColorPair) tui.ColorPair {
|
||||||
add := func(idx int) {
|
|
||||||
if curr != 0 && idx > start {
|
|
||||||
if curr == -1 {
|
|
||||||
colors = append(colors, colorOffset{
|
|
||||||
offset: [2]int32{int32(start), int32(idx)}, color: color, attr: attr})
|
|
||||||
} else {
|
|
||||||
ansi := itemColors[curr-1]
|
|
||||||
fg := ansi.color.fg
|
fg := ansi.color.fg
|
||||||
bg := ansi.color.bg
|
bg := ansi.color.bg
|
||||||
if theme != nil {
|
|
||||||
if fg == -1 {
|
if fg == -1 {
|
||||||
if current {
|
if current {
|
||||||
fg = theme.Current
|
fg = theme.Current.Color
|
||||||
} else {
|
} else {
|
||||||
fg = theme.Fg
|
fg = theme.Fg.Color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if bg == -1 {
|
if bg == -1 {
|
||||||
if current {
|
if current {
|
||||||
bg = theme.DarkBg
|
bg = theme.DarkBg.Color
|
||||||
} else {
|
} else {
|
||||||
bg = theme.Bg
|
bg = theme.Bg.Color
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
// hl or hl+ only sets the foreground color, so colMatch is the
|
||||||
|
// combination of either [hl and bg] or [hl+ and bg+].
|
||||||
|
//
|
||||||
|
// If the original text already has background color, and the
|
||||||
|
// foreground color of colMatch is -1, we shouldn't only apply the
|
||||||
|
// background color of colMatch.
|
||||||
|
// e.g. echo -e "\x1b[32;7mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||||
|
// echo -e "\x1b[42mfoo\x1b[mbar" | fzf --ansi --color bg+:1,hl+:-1:underline
|
||||||
|
if color.Fg().IsDefault() && origColor.HasBg() {
|
||||||
|
color = origColor
|
||||||
|
} else {
|
||||||
|
color = origColor.MergeNonDefault(color)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
colors = append(colors, colorOffset{
|
||||||
|
offset: [2]int32{int32(start), int32(idx)}, color: color})
|
||||||
|
} else {
|
||||||
|
ansi := itemColors[curr-1]
|
||||||
colors = append(colors, colorOffset{
|
colors = append(colors, colorOffset{
|
||||||
offset: [2]int32{int32(start), int32(idx)},
|
offset: [2]int32{int32(start), int32(idx)},
|
||||||
color: tui.NewColorPair(fg, bg),
|
color: ansiToColorPair(ansi, colBase)})
|
||||||
attr: ansi.color.attr.Merge(attr)})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +229,7 @@ func (a ByOrder) Less(i, j int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ByRelevance is for sorting Items
|
// ByRelevance is for sorting Items
|
||||||
type ByRelevance []*Result
|
type ByRelevance []Result
|
||||||
|
|
||||||
func (a ByRelevance) Len() int {
|
func (a ByRelevance) Len() int {
|
||||||
return len(a)
|
return len(a)
|
||||||
@@ -212,11 +240,11 @@ func (a ByRelevance) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a ByRelevance) Less(i, j int) bool {
|
func (a ByRelevance) Less(i, j int) bool {
|
||||||
return compareRanks((*a[i]).rank, (*a[j]).rank, false)
|
return compareRanks(a[i], a[j], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ByRelevanceTac is for sorting Items
|
// ByRelevanceTac is for sorting Items
|
||||||
type ByRelevanceTac []*Result
|
type ByRelevanceTac []Result
|
||||||
|
|
||||||
func (a ByRelevanceTac) Len() int {
|
func (a ByRelevanceTac) Len() int {
|
||||||
return len(a)
|
return len(a)
|
||||||
@@ -227,18 +255,5 @@ func (a ByRelevanceTac) Swap(i, j int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a ByRelevanceTac) Less(i, j int) bool {
|
func (a ByRelevanceTac) Less(i, j int) bool {
|
||||||
return compareRanks((*a[i]).rank, (*a[j]).rank, true)
|
return compareRanks(a[i], a[j], true)
|
||||||
}
|
|
||||||
|
|
||||||
func compareRanks(irank rank, jrank rank, tac bool) bool {
|
|
||||||
for idx := 0; idx < 4; idx++ {
|
|
||||||
left := irank.points[idx]
|
|
||||||
right := jrank.points[idx]
|
|
||||||
if left < right {
|
|
||||||
return true
|
|
||||||
} else if left > right {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (irank.index <= jrank.index) != tac
|
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/result_others.go
Normal file
16
src/result_others.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build !386 && !amd64
|
||||||
|
|
||||||
|
package fzf
|
||||||
|
|
||||||
|
func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||||
|
for idx := 3; idx >= 0; idx-- {
|
||||||
|
left := irank.points[idx]
|
||||||
|
right := jrank.points[idx]
|
||||||
|
if left < right {
|
||||||
|
return true
|
||||||
|
} else if left > right {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// +build !tcell
|
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -11,10 +9,15 @@ import (
|
|||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func withIndex(i *Item, index int) *Item {
|
||||||
|
(*i).text.Index = int32(index)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
func TestOffsetSort(t *testing.T) {
|
func TestOffsetSort(t *testing.T) {
|
||||||
offsets := []Offset{
|
offsets := []Offset{
|
||||||
Offset{3, 5}, Offset{2, 7},
|
{3, 5}, {2, 7},
|
||||||
Offset{1, 3}, Offset{2, 9}}
|
{1, 3}, {2, 9}}
|
||||||
sort.Sort(ByOrder(offsets))
|
sort.Sort(ByOrder(offsets))
|
||||||
|
|
||||||
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
if offsets[0][0] != 1 || offsets[0][1] != 3 ||
|
||||||
@@ -26,10 +29,10 @@ func TestOffsetSort(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRankComparison(t *testing.T) {
|
func TestRankComparison(t *testing.T) {
|
||||||
rank := func(vals ...uint16) rank {
|
rank := func(vals ...uint16) Result {
|
||||||
return rank{
|
return Result{
|
||||||
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
|
points: [4]uint16{vals[0], vals[1], vals[2], vals[3]},
|
||||||
index: int32(vals[4])}
|
item: &Item{text: util.Chars{Index: int32(vals[4])}}}
|
||||||
}
|
}
|
||||||
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
|
if compareRanks(rank(3, 0, 0, 0, 5), rank(2, 0, 0, 0, 7), false) ||
|
||||||
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
!compareRanks(rank(3, 0, 0, 0, 5), rank(3, 0, 0, 0, 6), false) ||
|
||||||
@@ -51,37 +54,42 @@ func TestResultRank(t *testing.T) {
|
|||||||
// FIXME global
|
// FIXME global
|
||||||
sortCriteria = []criterion{byScore, byLength}
|
sortCriteria = []criterion{byScore, byLength}
|
||||||
|
|
||||||
strs := [][]rune{[]rune("foo"), []rune("foobar"), []rune("bar"), []rune("baz")}
|
str := []rune("foo")
|
||||||
item1 := buildResult(&Item{text: util.RunesToChars(strs[0]), index: 1, trimLength: -1}, []Offset{}, 2)
|
item1 := buildResult(
|
||||||
if item1.rank.points[0] != math.MaxUint16-2 || // Bonus
|
withIndex(&Item{text: util.RunesToChars(str)}, 1), []Offset{}, 2)
|
||||||
item1.rank.points[1] != 3 || // Length
|
if item1.points[3] != math.MaxUint16-2 || // Bonus
|
||||||
item1.rank.points[2] != 0 || // Unused
|
item1.points[2] != 3 || // Length
|
||||||
item1.rank.points[3] != 0 || // Unused
|
item1.points[1] != 0 || // Unused
|
||||||
item1.item.index != 1 {
|
item1.points[0] != 0 || // Unused
|
||||||
t.Error(item1.rank)
|
item1.item.Index() != 1 {
|
||||||
|
t.Error(item1)
|
||||||
}
|
}
|
||||||
// Only differ in index
|
// 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}
|
items := []Result{item1, item2}
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
if items[0] != item2 || items[1] != item1 {
|
if items[0] != item2 || items[1] != item1 {
|
||||||
t.Error(items)
|
t.Error(items)
|
||||||
}
|
}
|
||||||
|
|
||||||
items = []*Result{item2, item1, item1, item2}
|
items = []Result{item2, item1, item1, item2}
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
if items[0] != item2 || items[1] != item2 ||
|
if items[0] != item2 || items[1] != item2 ||
|
||||||
items[2] != item1 || items[3] != item1 {
|
items[2] != item1 || items[3] != item1 {
|
||||||
t.Error(items, item1, item1.item.index, item2, item2.item.index)
|
t.Error(items, item1, item1.item.Index(), item2, item2.item.Index())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by relevance
|
// Sort by relevance
|
||||||
item3 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 3)
|
item3 := buildResult(
|
||||||
item4 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 4)
|
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 3)
|
||||||
item5 := buildResult(&Item{index: 2}, []Offset{Offset{1, 3}, Offset{5, 7}}, 5)
|
item4 := buildResult(
|
||||||
item6 := buildResult(&Item{index: 2}, []Offset{Offset{1, 2}, Offset{6, 7}}, 6)
|
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 4)
|
||||||
items = []*Result{item1, item2, item3, item4, item5, item6}
|
item5 := buildResult(
|
||||||
|
withIndex(&Item{}, 2), []Offset{{1, 3}, {5, 7}}, 5)
|
||||||
|
item6 := buildResult(
|
||||||
|
withIndex(&Item{}, 2), []Offset{{1, 2}, {6, 7}}, 6)
|
||||||
|
items = []Result{item1, item2, item3, item4, item5, item6}
|
||||||
sort.Sort(ByRelevance(items))
|
sort.Sort(ByRelevance(items))
|
||||||
if !(items[0] == item6 && items[1] == item5 &&
|
if !(items[0] == item6 && items[1] == item5 &&
|
||||||
items[2] == item4 && items[3] == item3 &&
|
items[2] == item4 && items[3] == item3 &&
|
||||||
@@ -90,37 +98,77 @@ 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) {
|
func TestColorOffset(t *testing.T) {
|
||||||
// ------------ 20 ---- -- ----
|
// ------------ 20 ---- -- ----
|
||||||
// ++++++++ ++++++++++
|
// ++++++++ ++++++++++
|
||||||
// --++++++++-- --++++++++++---
|
// --++++++++-- --++++++++++---
|
||||||
|
|
||||||
offsets := []Offset{Offset{5, 15}, Offset{25, 35}}
|
offsets := []Offset{{5, 15}, {25, 35}}
|
||||||
item := Result{
|
item := Result{
|
||||||
item: &Item{
|
item: &Item{
|
||||||
colors: &[]ansiOffset{
|
colors: &[]ansiOffset{
|
||||||
ansiOffset{[2]int32{0, 20}, ansiState{1, 5, 0}},
|
{[2]int32{0, 20}, ansiState{1, 5, 0, -1}},
|
||||||
ansiOffset{[2]int32{22, 27}, ansiState{2, 6, tui.Bold}},
|
{[2]int32{22, 27}, ansiState{2, 6, tui.Bold, -1}},
|
||||||
ansiOffset{[2]int32{30, 32}, ansiState{3, 7, 0}},
|
{[2]int32{30, 32}, ansiState{3, 7, 0, -1}},
|
||||||
ansiOffset{[2]int32{33, 40}, ansiState{4, 8, tui.Bold}}}}}
|
{[2]int32{33, 40}, ansiState{4, 8, tui.Bold, -1}}}}}
|
||||||
// [{[0 5] 9 false} {[5 15] 99 false} {[15 20] 9 false} {[22 25] 10 true} {[25 35] 99 false} {[35 40] 11 true}]
|
|
||||||
|
|
||||||
pair := tui.NewColorPair(99, 199)
|
colBase := tui.NewColorPair(89, 189, tui.AttrUndefined)
|
||||||
colors := item.colorOffsets(offsets, tui.Dark256, pair, tui.AttrRegular, true)
|
colMatch := tui.NewColorPair(99, 199, tui.AttrUndefined)
|
||||||
assert := func(idx int, b int32, e int32, c tui.ColorPair, bold bool) {
|
colors := item.colorOffsets(offsets, tui.Dark256, colBase, colMatch, true)
|
||||||
var attr tui.Attr
|
assert := func(idx int, b int32, e int32, c tui.ColorPair) {
|
||||||
if bold {
|
|
||||||
attr = tui.Bold
|
|
||||||
}
|
|
||||||
o := colors[idx]
|
o := colors[idx]
|
||||||
if o.offset[0] != b || o.offset[1] != e || o.color != c || o.attr != attr {
|
if o.offset[0] != b || o.offset[1] != e || o.color != c {
|
||||||
t.Error(o)
|
t.Error(o, b, e, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert(0, 0, 5, tui.NewColorPair(1, 5), false)
|
// [{[0 5] {1 5 0}} {[5 15] {99 199 0}} {[15 20] {1 5 0}}
|
||||||
assert(1, 5, 15, pair, false)
|
// {[22 25] {2 6 1}} {[25 27] {99 199 1}} {[27 30] {99 199 0}}
|
||||||
assert(2, 15, 20, tui.NewColorPair(1, 5), false)
|
// {[30 32] {99 199 0}} {[32 33] {99 199 0}} {[33 35] {99 199 1}}
|
||||||
assert(3, 22, 25, tui.NewColorPair(2, 6), true)
|
// {[35 40] {4 8 1}}]
|
||||||
assert(4, 25, 35, pair, false)
|
assert(0, 0, 5, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
assert(5, 35, 40, tui.NewColorPair(4, 8), true)
|
assert(1, 5, 15, colMatch)
|
||||||
|
assert(2, 15, 20, tui.NewColorPair(1, 5, tui.AttrUndefined))
|
||||||
|
assert(3, 22, 25, tui.NewColorPair(2, 6, tui.Bold))
|
||||||
|
assert(4, 25, 27, colMatch.WithAttr(tui.Bold))
|
||||||
|
assert(5, 27, 30, colMatch)
|
||||||
|
assert(6, 30, 32, colMatch)
|
||||||
|
assert(7, 32, 33, colMatch) // TODO: Should we merge consecutive blocks?
|
||||||
|
assert(8, 33, 35, colMatch.WithAttr(tui.Bold))
|
||||||
|
assert(9, 35, 40, tui.NewColorPair(4, 8, tui.Bold))
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/result_x86.go
Normal file
16
src/result_x86.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
//go:build 386 || amd64
|
||||||
|
|
||||||
|
package fzf
|
||||||
|
|
||||||
|
import "unsafe"
|
||||||
|
|
||||||
|
func compareRanks(irank Result, jrank Result, tac bool) bool {
|
||||||
|
left := *(*uint64)(unsafe.Pointer(&irank.points[0]))
|
||||||
|
right := *(*uint64)(unsafe.Pointer(&jrank.points[0]))
|
||||||
|
if left < right {
|
||||||
|
return true
|
||||||
|
} else if left > right {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (irank.item.Index() <= jrank.item.Index()) != tac
|
||||||
|
}
|
||||||
244
src/server.go
Normal file
244
src/server.go
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
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
|
||||||
|
httpReadTimeout = 10 * time.Second
|
||||||
|
maxContentLength = 1024 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpServer struct {
|
||||||
|
apiKey []byte
|
||||||
|
actionChannel chan []*action
|
||||||
|
responseChannel chan 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) (error, listenAddress) {
|
||||||
|
parts := strings.SplitN(address, ":", 3)
|
||||||
|
if len(parts) == 1 {
|
||||||
|
parts = []string{"localhost", parts[0]}
|
||||||
|
}
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return fmt.Errorf("invalid listen address: %s", address), defaultListenAddr
|
||||||
|
}
|
||||||
|
portStr := parts[len(parts)-1]
|
||||||
|
port, err := strconv.Atoi(portStr)
|
||||||
|
if err != nil || port < 0 || port > 65535 {
|
||||||
|
return fmt.Errorf("invalid listen port: %s", portStr), defaultListenAddr
|
||||||
|
}
|
||||||
|
if len(parts[0]) == 0 {
|
||||||
|
parts[0] = "localhost"
|
||||||
|
}
|
||||||
|
return nil, listenAddress{parts[0], port}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startHttpServer(address listenAddress, actionChannel chan []*action, responseChannel chan string) (error, int) {
|
||||||
|
host := address.host
|
||||||
|
port := address.port
|
||||||
|
apiKey := os.Getenv("FZF_API_KEY")
|
||||||
|
if !address.IsLocal() && len(apiKey) == 0 {
|
||||||
|
return fmt.Errorf("FZF_API_KEY is required to allow remote access"), port
|
||||||
|
}
|
||||||
|
addrStr := fmt.Sprintf("%s:%d", host, port)
|
||||||
|
listener, err := net.Listen("tcp", addrStr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to listen on %s", addrStr), port
|
||||||
|
}
|
||||||
|
if port == 0 {
|
||||||
|
addr := listener.Addr().String()
|
||||||
|
parts := strings.Split(addr, ":")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return fmt.Errorf("cannot extract port: %s", addr), port
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
port, err = strconv.Atoi(parts[len(parts)-1])
|
||||||
|
if err != nil {
|
||||||
|
return err, port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httpServer{
|
||||||
|
apiKey: []byte(apiKey),
|
||||||
|
actionChannel: actionChannel,
|
||||||
|
responseChannel: responseChannel,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, net.ErrClosed) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
conn.Write([]byte(server.handleHttpRequest(conn)))
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
listener.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil, port
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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+"Content-Type: application/json"+crlf, 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 {
|
||||||
|
server.actionChannel <- []*action{{t: actResponse, a: getMatch[1]}}
|
||||||
|
response := <-server.responseChannel
|
||||||
|
return good(response)
|
||||||
|
} 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]
|
||||||
|
|
||||||
|
errorMessage := ""
|
||||||
|
actions := parseSingleActionList(strings.Trim(string(body), "\r\n"), func(message string) {
|
||||||
|
errorMessage = message
|
||||||
|
})
|
||||||
|
if len(errorMessage) > 0 {
|
||||||
|
return bad(errorMessage)
|
||||||
|
}
|
||||||
|
if len(actions) == 0 {
|
||||||
|
return bad("no action specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
server.actionChannel <- actions
|
||||||
|
return httpOk
|
||||||
|
}
|
||||||
|
|
||||||
|
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":
|
||||||
|
val, err := strconv.Atoi(parts[1])
|
||||||
|
if err == nil {
|
||||||
|
params.limit = val
|
||||||
|
}
|
||||||
|
case "offset":
|
||||||
|
val, err := strconv.Atoi(parts[1])
|
||||||
|
if err == nil {
|
||||||
|
params.offset = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
3446
src/terminal.go
3446
src/terminal.go
File diff suppressed because it is too large
Load Diff
@@ -1,18 +1,17 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
"github.com/junegunn/fzf/src/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newItem(str string) *Item {
|
|
||||||
bytes := []byte(str)
|
|
||||||
trimmed, _, _ := extractColor(str, nil, nil)
|
|
||||||
return &Item{origText: &bytes, text: util.RunesToChars([]rune(trimmed))}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReplacePlaceholder(t *testing.T) {
|
func TestReplacePlaceholder(t *testing.T) {
|
||||||
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
item1 := newItem(" foo'bar \x1b[31mbaz\x1b[m")
|
||||||
items1 := []*Item{item1, item1}
|
items1 := []*Item{item1, item1}
|
||||||
@@ -21,73 +20,619 @@ func TestReplacePlaceholder(t *testing.T) {
|
|||||||
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
newItem("foo'bar \x1b[31mbaz\x1b[m"),
|
||||||
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
newItem("FOO'BAR \x1b[31mBAZ\x1b[m")}
|
||||||
|
|
||||||
|
delim := "'"
|
||||||
|
var regex *regexp.Regexp
|
||||||
|
|
||||||
var result string
|
var result string
|
||||||
check := func(expected string) {
|
check := func(expected string) {
|
||||||
if result != expected {
|
if result != expected {
|
||||||
t.Errorf("expected: %s, actual: %s", expected, result)
|
t.Errorf("expected: %s, actual: %s", expected, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// helper function that converts template format into string and carries out the check()
|
||||||
|
checkFormat := func(format string) {
|
||||||
|
type quotes struct{ O, I, S string } // outer, inner quotes, print separator
|
||||||
|
unixStyle := quotes{`'`, `'\''`, "\n"}
|
||||||
|
windowsStyle := quotes{`^"`, `'`, "\n"}
|
||||||
|
var effectiveStyle quotes
|
||||||
|
|
||||||
|
if util.IsWindows() {
|
||||||
|
effectiveStyle = windowsStyle
|
||||||
|
} else {
|
||||||
|
effectiveStyle = unixStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := templateToString(format, effectiveStyle)
|
||||||
|
check(expected)
|
||||||
|
}
|
||||||
|
printsep := "\n"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Test multiple placeholders and the function parameters.
|
||||||
|
*/
|
||||||
|
|
||||||
// {}, preserve ansi
|
// {}, preserve ansi
|
||||||
result = replacePlaceholder("echo {}", false, Delimiter{}, false, "query", items1)
|
result = replacePlaceholder("echo {}", false, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar \x1b[31mbaz\x1b[m'")
|
checkFormat("echo {{.O}} foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||||
|
|
||||||
// {}, strip ansi
|
// {}, strip ansi
|
||||||
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items1)
|
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// {}, with multiple items
|
// {}, with multiple items
|
||||||
result = replacePlaceholder("echo {}", true, Delimiter{}, false, "query", items2)
|
result = replacePlaceholder("echo {}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
check("echo 'foo'\\''bar baz'")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// {..}, strip leading whitespaces, preserve ansi
|
// {..}, strip leading whitespaces, preserve ansi
|
||||||
result = replacePlaceholder("echo {..}", false, Delimiter{}, false, "query", items1)
|
result = replacePlaceholder("echo {..}", false, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo 'foo'\\''bar \x1b[31mbaz\x1b[m'")
|
checkFormat("echo {{.O}}foo{{.I}}bar \x1b[31mbaz\x1b[m{{.O}}")
|
||||||
|
|
||||||
// {..}, strip leading whitespaces, strip ansi
|
// {..}, strip leading whitespaces, strip ansi
|
||||||
result = replacePlaceholder("echo {..}", true, Delimiter{}, false, "query", items1)
|
result = replacePlaceholder("echo {..}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo 'foo'\\''bar baz'")
|
checkFormat("echo {{.O}}foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// {q}
|
// {q}
|
||||||
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, false, "query", items1)
|
result = replacePlaceholder("echo {} {q}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz' 'query'")
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}} {{.O}}query{{.O}}")
|
||||||
|
|
||||||
// {q}, multiple items
|
// {q}, multiple items
|
||||||
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, false, "query 'string'", items2)
|
result = replacePlaceholder("echo {+}{q}{+}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||||
check("echo 'foo'\\''bar baz' 'FOO'\\''BAR BAZ''query '\\''string'\\''''foo'\\''bar baz' 'FOO'\\''BAR BAZ'")
|
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{}, false, "query 'string'", items2)
|
result = replacePlaceholder("echo {}{q}{}", true, Delimiter{}, printsep, false, "query 'string'", items2)
|
||||||
check("echo 'foo'\\''bar baz''query '\\''string'\\''''foo'\\''bar baz'")
|
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{}, false, "query", items1)
|
result = replacePlaceholder("echo {1}/{2}/{2,1}/{-1}/{-2}/{}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items1)
|
||||||
check("echo 'foo'\\''bar'/'baz'/'bazfoo'\\''bar'/'baz'/'foo'\\''bar'/' foo'\\''bar baz'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
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{}, false, "query", items2)
|
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
check("echo 'foo'\\''bar'/'baz'/'baz'/'foo'\\''bar'/'foo'\\''bar baz'/{n.t}/{}/{1}/{q}/''")
|
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{}, false, "query", items2)
|
result = replacePlaceholder("echo {+1}/{+2}/{+-1}/{+-2}/{+..}/{n.t}/\\{}/\\{1}/\\{q}/{+3}", true, Delimiter{}, printsep, false, "query", items2)
|
||||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
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
|
// forcePlus
|
||||||
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, true, "query", items2)
|
result = replacePlaceholder("echo {1}/{2}/{-1}/{-2}/{..}/{n.t}/\\{}/\\{1}/\\{q}/{3}", true, Delimiter{}, printsep, true, "query", items2)
|
||||||
check("echo 'foo'\\''bar' 'FOO'\\''BAR'/'baz' 'BAZ'/'baz' 'BAZ'/'foo'\\''bar' 'FOO'\\''BAR'/'foo'\\''bar baz' 'FOO'\\''BAR BAZ'/{n.t}/{}/{1}/{q}/'' ''")
|
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)
|
||||||
|
checkFormat("echo {{.O}} foo{{.O}}")
|
||||||
|
|
||||||
|
result = replacePlaceholder("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)
|
||||||
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
|
result = replacePlaceholder("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)
|
||||||
|
checkFormat("echo {{.O}} {{.O}}")
|
||||||
|
|
||||||
|
result = replacePlaceholder("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)
|
||||||
|
checkFormat("echo {{.O}} {{.O}}")
|
||||||
|
|
||||||
// No match
|
// No match
|
||||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, nil})
|
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, nil})
|
||||||
check("echo /")
|
check("echo /")
|
||||||
|
|
||||||
// No match, but with selections
|
// No match, but with selections
|
||||||
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, false, "query", []*Item{nil, item1})
|
result = replacePlaceholder("echo {}/{+}", true, Delimiter{}, printsep, false, "query", []*Item{nil, item1})
|
||||||
check("echo /' foo'\\''bar baz'")
|
checkFormat("echo /{{.O}} foo{{.I}}bar baz{{.O}}")
|
||||||
|
|
||||||
// String delimiter
|
// String delimiter
|
||||||
delim := "'"
|
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, printsep, false, "query", items1)
|
||||||
result = replacePlaceholder("echo {}/{1}/{2}", true, Delimiter{str: &delim}, false, "query", items1)
|
checkFormat("echo {{.O}} foo{{.I}}bar baz{{.O}}/{{.O}}foo{{.O}}/{{.O}}bar baz{{.O}}")
|
||||||
check("echo ' foo'\\''bar baz'/'foo'/'bar baz'")
|
|
||||||
|
|
||||||
// Regex delimiter
|
// Regex delimiter
|
||||||
regex := regexp.MustCompile("[oa]+")
|
regex = regexp.MustCompile("[oa]+")
|
||||||
// foo'bar baz
|
// foo'bar baz
|
||||||
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, false, "query", items1)
|
result = replacePlaceholder("echo {}/{1}/{3}/{2..3}", true, Delimiter{regex: regex}, printsep, false, "query", items1)
|
||||||
check("echo ' foo'\\''bar baz'/'f'/'r b'/''\\''bar b'")
|
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{
|
||||||
|
// single line
|
||||||
|
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"),
|
||||||
|
}
|
||||||
|
stripAnsi := false
|
||||||
|
printsep = "\n"
|
||||||
|
forcePlus := false
|
||||||
|
query := "sample query"
|
||||||
|
|
||||||
|
templateToOutput := make(map[string]string)
|
||||||
|
templateToFile := make(map[string]string) // same as above, but the file contents will be matched
|
||||||
|
// I. item type placeholder
|
||||||
|
templateToOutput[`{}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}}`
|
||||||
|
templateToOutput[`{+}`] = `{{.O}}1a 1b 1c 1d 1e 1f{{.O}} {{.O}}2a 2b 2c 2d 2e 2f{{.O}} {{.O}}3a 3b 3c 3d 3e 3f{{.O}} {{.O}}4a 4b 4c 4d 4e 4f{{.O}} {{.O}}5a 5b 5c 5d 5e 5f{{.O}} {{.O}}6a 6b 6c 6d 6e 6f{{.O}} {{.O}}7a 7b 7c 7d 7e 7f{{.O}}`
|
||||||
|
templateToOutput[`{n}`] = `0`
|
||||||
|
templateToOutput[`{+n}`] = `0 0 0 0 0 0 0`
|
||||||
|
templateToFile[`{f}`] = `1a 1b 1c 1d 1e 1f{{.S}}`
|
||||||
|
templateToFile[`{+f}`] = `1a 1b 1c 1d 1e 1f{{.S}}2a 2b 2c 2d 2e 2f{{.S}}3a 3b 3c 3d 3e 3f{{.S}}4a 4b 4c 4d 4e 4f{{.S}}5a 5b 5c 5d 5e 5f{{.S}}6a 6b 6c 6d 6e 6f{{.S}}7a 7b 7c 7d 7e 7f{{.S}}`
|
||||||
|
templateToFile[`{nf}`] = `0{{.S}}`
|
||||||
|
templateToFile[`{+nf}`] = `0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}0{{.S}}`
|
||||||
|
|
||||||
|
// II. token type placeholders
|
||||||
|
templateToOutput[`{..}`] = templateToOutput[`{}`]
|
||||||
|
templateToOutput[`{1..}`] = templateToOutput[`{}`]
|
||||||
|
templateToOutput[`{..2}`] = `{{.O}}1a 1b{{.O}}`
|
||||||
|
templateToOutput[`{1..2}`] = templateToOutput[`{..2}`]
|
||||||
|
templateToOutput[`{-2..-1}`] = `{{.O}}1e 1f{{.O}}`
|
||||||
|
// shorthand for x..x range
|
||||||
|
templateToOutput[`{1}`] = `{{.O}}1a{{.O}}`
|
||||||
|
templateToOutput[`{1..1}`] = templateToOutput[`{1}`]
|
||||||
|
templateToOutput[`{-6}`] = templateToOutput[`{1}`]
|
||||||
|
// multiple ranges
|
||||||
|
templateToOutput[`{1,2}`] = templateToOutput[`{1..2}`]
|
||||||
|
templateToOutput[`{1,2,4}`] = `{{.O}}1a 1b 1d{{.O}}`
|
||||||
|
templateToOutput[`{1,2..4}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
|
||||||
|
templateToOutput[`{1..2,-4..-3}`] = `{{.O}}1a 1b 1c 1d{{.O}}`
|
||||||
|
// flags
|
||||||
|
templateToOutput[`{+1}`] = `{{.O}}1a{{.O}} {{.O}}2a{{.O}} {{.O}}3a{{.O}} {{.O}}4a{{.O}} {{.O}}5a{{.O}} {{.O}}6a{{.O}} {{.O}}7a{{.O}}`
|
||||||
|
templateToOutput[`{+-1}`] = `{{.O}}1f{{.O}} {{.O}}2f{{.O}} {{.O}}3f{{.O}} {{.O}}4f{{.O}} {{.O}}5f{{.O}} {{.O}}6f{{.O}} {{.O}}7f{{.O}}`
|
||||||
|
templateToOutput[`{s1}`] = `{{.O}}1a {{.O}}`
|
||||||
|
templateToFile[`{f1}`] = `1a{{.S}}`
|
||||||
|
templateToOutput[`{+s1..2}`] = `{{.O}}1a 1b {{.O}} {{.O}}2a 2b {{.O}} {{.O}}3a 3b {{.O}} {{.O}}4a 4b {{.O}} {{.O}}5a 5b {{.O}} {{.O}}6a 6b {{.O}} {{.O}}7a 7b {{.O}}`
|
||||||
|
templateToFile[`{+sf1..2}`] = `1a 1b {{.S}}2a 2b {{.S}}3a 3b {{.S}}4a 4b {{.S}}5a 5b {{.S}}6a 6b {{.S}}7a 7b {{.S}}`
|
||||||
|
|
||||||
|
// 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
|
||||||
|
templateToOutput[`{q}`] = "{{.O}}" + query + "{{.O}}"
|
||||||
|
|
||||||
|
// IV. escaping placeholder
|
||||||
|
templateToOutput[`\{}`] = `{}`
|
||||||
|
templateToOutput[`\{++}`] = `{++}`
|
||||||
|
templateToOutput[`{++}`] = templateToOutput[`{+}`]
|
||||||
|
|
||||||
|
for giveTemplate, wantOutput := range templateToOutput {
|
||||||
|
result = replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||||
|
checkFormat(wantOutput)
|
||||||
|
}
|
||||||
|
for giveTemplate, wantOutput := range templateToFile {
|
||||||
|
path := replacePlaceholder(giveTemplate, stripAnsi, Delimiter{}, printsep, forcePlus, query, items3)
|
||||||
|
|
||||||
|
data, err := readFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Cannot read the content of the temp file %s.", path)
|
||||||
|
}
|
||||||
|
result = string(data)
|
||||||
|
|
||||||
|
checkFormat(wantOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuoteEntry(t *testing.T) {
|
||||||
|
type quotes struct{ E, O, SQ, DQ, BS string } // standalone escape, outer, single and double quotes, backslash
|
||||||
|
unixStyle := quotes{``, `'`, `'\''`, `"`, `\`}
|
||||||
|
windowsStyle := quotes{`^`, `^"`, `'`, `\^"`, `\\`}
|
||||||
|
var effectiveStyle quotes
|
||||||
|
|
||||||
|
if util.IsWindows() {
|
||||||
|
effectiveStyle = windowsStyle
|
||||||
|
} else {
|
||||||
|
effectiveStyle = unixStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := map[string]string{
|
||||||
|
`'`: `{{.O}}{{.SQ}}{{.O}}`,
|
||||||
|
`"`: `{{.O}}{{.DQ}}{{.O}}`,
|
||||||
|
`\`: `{{.O}}{{.BS}}{{.O}}`,
|
||||||
|
`\"`: `{{.O}}{{.BS}}{{.DQ}}{{.O}}`,
|
||||||
|
`"\\\"`: `{{.O}}{{.DQ}}{{.BS}}{{.BS}}{{.BS}}{{.DQ}}{{.O}}`,
|
||||||
|
|
||||||
|
`$`: `{{.O}}${{.O}}`,
|
||||||
|
`$HOME`: `{{.O}}$HOME{{.O}}`,
|
||||||
|
`'$HOME'`: `{{.O}}{{.SQ}}$HOME{{.SQ}}{{.O}}`,
|
||||||
|
|
||||||
|
`&`: `{{.O}}{{.E}}&{{.O}}`,
|
||||||
|
`|`: `{{.O}}{{.E}}|{{.O}}`,
|
||||||
|
`<`: `{{.O}}{{.E}}<{{.O}}`,
|
||||||
|
`>`: `{{.O}}{{.E}}>{{.O}}`,
|
||||||
|
`(`: `{{.O}}{{.E}}({{.O}}`,
|
||||||
|
`)`: `{{.O}}{{.E}}){{.O}}`,
|
||||||
|
`@`: `{{.O}}{{.E}}@{{.O}}`,
|
||||||
|
`^`: `{{.O}}{{.E}}^{{.O}}`,
|
||||||
|
`%`: `{{.O}}{{.E}}%{{.O}}`,
|
||||||
|
`!`: `{{.O}}{{.E}}!{{.O}}`,
|
||||||
|
`%USERPROFILE%`: `{{.O}}{{.E}}%USERPROFILE{{.E}}%{{.O}}`,
|
||||||
|
`C:\Program Files (x86)\`: `{{.O}}C:{{.BS}}Program Files {{.E}}(x86{{.E}}){{.BS}}{{.O}}`,
|
||||||
|
`"C:\Program Files"`: `{{.O}}{{.DQ}}C:{{.BS}}Program Files{{.DQ}}{{.O}}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for input, expected := range tests {
|
||||||
|
escaped := quoteEntry(input)
|
||||||
|
expected = templateToString(expected, effectiveStyle)
|
||||||
|
if escaped != expected {
|
||||||
|
t.Errorf("Input: %s, expected: %s, actual %s", input, expected, escaped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Unix
|
||||||
|
func TestUnixCommands(t *testing.T) {
|
||||||
|
if util.IsWindows() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
// paths that does not have to evaluated will work fine, when quoted
|
||||||
|
{give{`grep foo {}`, ``, newItems(`test`)}, want{output: `grep foo 'test'`}},
|
||||||
|
{give{`grep foo {}`, ``, newItems(`/home/user/test`)}, want{output: `grep foo '/home/user/test'`}},
|
||||||
|
{give{`grep foo {}`, ``, newItems(`./test`)}, want{output: `grep foo './test'`}},
|
||||||
|
|
||||||
|
// only placeholders are escaped as data, this will lookup tilde character in a test file in your home directory
|
||||||
|
// quoting the tilde is required (to be treated as string)
|
||||||
|
{give{`grep {} ~/test`, ``, newItems(`~`)}, want{output: `grep '~' ~/test`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// paths that need to expand some part of it won't work (special characters and variables)
|
||||||
|
{give{`cat {}`, ``, newItems(`~/test`)}, want{output: `cat '~/test'`}},
|
||||||
|
{give{`cat {}`, ``, newItems(`$HOME/test`)}, want{output: `cat '$HOME/test'`}},
|
||||||
|
}
|
||||||
|
testCommands(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
// example of redundantly escaped backslash in the output, besides looking bit ugly, it won't cause any issue
|
||||||
|
{give{`type {}`, ``, newItems(`C:\test.txt`)}, want{output: `type ^"C:\\test.txt^"`}},
|
||||||
|
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" ^".\\test.go^"`}},
|
||||||
|
// example of mandatorily escaped backslash in the output, otherwise `rg -- "C:\test.txt"` is matching for tabulator
|
||||||
|
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- ^"C:\\test.txt^"`}},
|
||||||
|
// example of mandatorily escaped double quote in the output, otherwise `rg -- ""C:\\test.txt""` is not matching for the double quotes around the path
|
||||||
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- ^"\^"C:\\test.txt\^"^"`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// notepad++'s parser can't handle `-n"12"` generate by fzf, expects `-n12`
|
||||||
|
{give{`notepad++ -n{1} {2}`, ``, newItems(`12 C:\Work\Test Folder\File.txt`)}, want{output: `notepad++ -n^"12^" ^"C:\\Work\\Test Folder\\File.txt^"`}},
|
||||||
|
|
||||||
|
// cat is parsing `\"` as a part of the file path, double quote is illegal character for paths on Windows
|
||||||
|
// cat: "C:\\test.txt: Invalid argument
|
||||||
|
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat ^"\^"C:\\test.txt\^"^"`}},
|
||||||
|
// cat: "C:\\test.txt": Invalid argument
|
||||||
|
{give{`cmd /c {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `cmd /c ^"cat \^"C:\\test.txt\^"^"`}},
|
||||||
|
|
||||||
|
// the "file" flag in the pattern won't create *.bat or *.cmd file so the command in the output tries to edit the file, instead of executing it
|
||||||
|
// the temp file contains: `cat "C:\test.txt"`
|
||||||
|
// TODO this should actually work
|
||||||
|
{give{`cmd /c {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^cmd /c .*\fzf-preview-[0-9]{9}$`}},
|
||||||
|
}
|
||||||
|
testCommands(t, tests)
|
||||||
|
}
|
||||||
|
|
||||||
|
// purpose of this test is to demonstrate some shortcomings of fzf's templating system on Windows in Powershell
|
||||||
|
func TestPowershellCommands(t *testing.T) {
|
||||||
|
if !util.IsWindows() {
|
||||||
|
t.SkipNow()
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []testCase{
|
||||||
|
// reference: give{template, query, items}, want{output OR match}
|
||||||
|
|
||||||
|
/*
|
||||||
|
You can read each line in the following table as a pipeline that
|
||||||
|
consist of series of parsers that act upon your input (col. 1) and
|
||||||
|
each cell represents the output value.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
- exec.Command("program.exe", `\''`)
|
||||||
|
- goes to win32 api which will process it transparently as it contains no special characters, see [CommandLineToArgvW][].
|
||||||
|
- powershell command will receive it as is, that is two arguments: a literal backslash and empty string in single quotes
|
||||||
|
- native command run via/from powershell will receive only one argument: a literal backslash. Because extra parsing rules apply, see [NativeCallsFromPowershell][].
|
||||||
|
- some¹ apps have internal parser, that requires one more level of escaping (yes, this is completely application-specific, but see terminal_test.go#TestWindowsCommands)
|
||||||
|
|
||||||
|
Character⁰ CommandLineToArgvW Powershell commands Native commands from Powershell Apps requiring escapes¹ | Being tested below
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
" empty string² missing argument error ... ... |
|
||||||
|
\" literal " unbalanced quote error ... ... |
|
||||||
|
'\"' literal '"' literal " empty string empty string (match all) | yes
|
||||||
|
'\\\"' literal '\"' literal \" literal " literal " |
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
\ transparent transparent transparent regex error |
|
||||||
|
'\' transparent literal \ literal \ regex error | yes
|
||||||
|
\\ transparent transparent transparent literal \ |
|
||||||
|
'\\' transparent literal \\ literal \\ literal \ |
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
' transparent unbalanced quote error ... ... |
|
||||||
|
\' transparent literal \ and unb. quote error ... ... |
|
||||||
|
\'' transparent literal \ and empty string literal \ regex error | no, but given as example above
|
||||||
|
''' transparent unbalanced quote error ... ... |
|
||||||
|
'''' transparent literal ' literal ' literal ' | yes
|
||||||
|
---------- ------------------ ------------------------------ ------------------------------- -------------------------- | ------------------
|
||||||
|
|
||||||
|
⁰: charatecter or characters 'x' as an argument to a program in go's call: exec.Command("program.exe", `x`)
|
||||||
|
¹: native commands like grep, git grep, ripgrep
|
||||||
|
²: interpreted as a grouping quote, affects argument parser and gets removed from the result
|
||||||
|
|
||||||
|
[CommandLineToArgvW]: https://docs.microsoft.com/en-gb/windows/win32/api/shellapi/nf-shellapi-commandlinetoargvw#remarks
|
||||||
|
[NativeCallsFromPowershell]: https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing?view=powershell-7.1#passing-arguments-that-contain-quote-characters
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 1) working examples
|
||||||
|
|
||||||
|
{give{`Get-Content {}`, ``, newItems(`C:\test.txt`)}, want{output: `Get-Content 'C:\test.txt'`}},
|
||||||
|
{give{`rg -- "package" {}`, ``, newItems(`.\test.go`)}, want{output: `rg -- "package" '.\test.go'`}},
|
||||||
|
|
||||||
|
// example of escaping single quotes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`'foobar'`)}, want{output: `rg -- '''foobar'''`}},
|
||||||
|
|
||||||
|
// chaining powershells
|
||||||
|
{give{`powershell -NoProfile -Command {}`, ``, newItems(`cat "C:\test.txt"`)}, want{output: `powershell -NoProfile -Command 'cat \"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// 2) problematic examples
|
||||||
|
// (not necessarily unexpected)
|
||||||
|
|
||||||
|
// looking for a path string will only work with escaped backslashes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`C:\test.txt`)}, want{output: `rg -- 'C:\test.txt'`}},
|
||||||
|
// looking for a literal double quote will only work with triple escaped double quotes
|
||||||
|
{give{`rg -- {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `rg -- '\"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// Get-Content (i.e. cat alias) is parsing `"` as a part of the file path, returns an error:
|
||||||
|
// Get-Content : Cannot find drive. A drive with the name '"C:' does not exist.
|
||||||
|
{give{`cat {}`, ``, newItems(`"C:\test.txt"`)}, want{output: `cat '\"C:\test.txt\"'`}},
|
||||||
|
|
||||||
|
// the "file" flag in the pattern won't create *.ps1 file so the powershell will offload this "unknown" filetype
|
||||||
|
// to explorer, which will prompt user to pick editing program for the fzf-preview file
|
||||||
|
// the temp file contains: `cat "C:\test.txt"`
|
||||||
|
// TODO this should actually work
|
||||||
|
{give{`powershell -NoProfile -Command {f}`, ``, newItems(`cat "C:\test.txt"`)}, want{match: `^powershell -NoProfile -Command .*\fzf-preview-[0-9]{9}$`}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// to force powershell-style escaping we temporarily set environment variable that fzf honors
|
||||||
|
shellBackup := os.Getenv("SHELL")
|
||||||
|
os.Setenv("SHELL", "powershell")
|
||||||
|
testCommands(t, tests)
|
||||||
|
os.Setenv("SHELL", shellBackup)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
func TestParsePlaceholder(t *testing.T) {
|
||||||
|
// give, want pairs
|
||||||
|
templates := map[string]string{
|
||||||
|
// I. item type placeholder
|
||||||
|
`{}`: `{}`,
|
||||||
|
`{+}`: `{+}`,
|
||||||
|
`{n}`: `{n}`,
|
||||||
|
`{+n}`: `{+n}`,
|
||||||
|
`{f}`: `{f}`,
|
||||||
|
`{+nf}`: `{+nf}`,
|
||||||
|
|
||||||
|
// II. token type placeholders
|
||||||
|
`{..}`: `{..}`,
|
||||||
|
`{1..}`: `{1..}`,
|
||||||
|
`{..2}`: `{..2}`,
|
||||||
|
`{1..2}`: `{1..2}`,
|
||||||
|
`{-2..-1}`: `{-2..-1}`,
|
||||||
|
// shorthand for x..x range
|
||||||
|
`{1}`: `{1}`,
|
||||||
|
`{1..1}`: `{1..1}`,
|
||||||
|
`{-6}`: `{-6}`,
|
||||||
|
// multiple ranges
|
||||||
|
`{1,2}`: `{1,2}`,
|
||||||
|
`{1,2,4}`: `{1,2,4}`,
|
||||||
|
`{1,2..4}`: `{1,2..4}`,
|
||||||
|
`{1..2,-4..-3}`: `{1..2,-4..-3}`,
|
||||||
|
// flags
|
||||||
|
`{+1}`: `{+1}`,
|
||||||
|
`{+-1}`: `{+-1}`,
|
||||||
|
`{s1}`: `{s1}`,
|
||||||
|
`{f1}`: `{f1}`,
|
||||||
|
`{+s1..2}`: `{+s1..2}`,
|
||||||
|
`{+sf1..2}`: `{+sf1..2}`,
|
||||||
|
|
||||||
|
// 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}`,
|
||||||
|
|
||||||
|
// IV. escaping placeholder
|
||||||
|
`\{}`: `{}`,
|
||||||
|
`\{++}`: `{++}`,
|
||||||
|
`{++}`: `{+}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for giveTemplate, wantTemplate := range templates {
|
||||||
|
if !placeholder.MatchString(giveTemplate) {
|
||||||
|
t.Errorf(`given placeholder %s does not match placeholder regex, so attempt to parse it is unexpected`, giveTemplate)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
_, placeholderWithoutFlags, flags := parsePlaceholder(giveTemplate)
|
||||||
|
gotTemplate := placeholderWithoutFlags[:1] + flags.encodePlaceholder() + placeholderWithoutFlags[1:]
|
||||||
|
|
||||||
|
if gotTemplate != wantTemplate {
|
||||||
|
t.Errorf(`parsed placeholder "%s" into "%s", but want "%s"`, giveTemplate, gotTemplate, wantTemplate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* utilities section */
|
||||||
|
|
||||||
|
// Item represents one line in fzf UI. Usually it is relative path to files and folders.
|
||||||
|
func newItem(str string) *Item {
|
||||||
|
bytes := []byte(str)
|
||||||
|
trimmed, _, _ := extractColor(str, nil, nil)
|
||||||
|
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))
|
||||||
|
for i, s := range str {
|
||||||
|
result[i] = newItem(s)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// (for logging purposes)
|
||||||
|
func (item *Item) String() string {
|
||||||
|
return item.AsString(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to parse, execute and convert "text/template" to string. Panics on error.
|
||||||
|
func templateToString(format string, data interface{}) string {
|
||||||
|
bb := &bytes.Buffer{}
|
||||||
|
|
||||||
|
err := template.Must(template.New("").Parse(format)).Execute(bb, data)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ad hoc types for test cases
|
||||||
|
type give struct {
|
||||||
|
template string
|
||||||
|
query string
|
||||||
|
allItems []*Item
|
||||||
|
}
|
||||||
|
type want struct {
|
||||||
|
/*
|
||||||
|
Unix:
|
||||||
|
The `want.output` string is supposed to be formatted for evaluation by
|
||||||
|
`sh -c command` system call.
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
The `want.output` string is supposed to be formatted for evaluation by
|
||||||
|
`cmd.exe /s /c "command"` system call. The `/s` switch enables so called old
|
||||||
|
behaviour, which is more favourable for nesting (possibly escaped)
|
||||||
|
special characters. This is the relevant section of `help cmd`:
|
||||||
|
|
||||||
|
...old behavior is to see if the first character is
|
||||||
|
a quote character and if so, strip the leading character and
|
||||||
|
remove the last quote character on the command line, preserving
|
||||||
|
any text after the last quote character.
|
||||||
|
*/
|
||||||
|
output string // literal output
|
||||||
|
match string // output is matched against this regex (when output is empty string)
|
||||||
|
}
|
||||||
|
type testCase struct {
|
||||||
|
give
|
||||||
|
want
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCommands(t *testing.T, tests []testCase) {
|
||||||
|
// common test parameters
|
||||||
|
delim := "\t"
|
||||||
|
delimiter := Delimiter{str: &delim}
|
||||||
|
printsep := ""
|
||||||
|
stripAnsi := false
|
||||||
|
forcePlus := false
|
||||||
|
|
||||||
|
// evaluate the test cases
|
||||||
|
for idx, test := range tests {
|
||||||
|
gotOutput := replacePlaceholder(
|
||||||
|
test.give.template, stripAnsi, delimiter, printsep, forcePlus,
|
||||||
|
test.give.query,
|
||||||
|
test.give.allItems)
|
||||||
|
switch {
|
||||||
|
case test.want.output != "":
|
||||||
|
if gotOutput != test.want.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)
|
||||||
|
}
|
||||||
|
case test.want.match != "":
|
||||||
|
wantMatch := strings.ReplaceAll(test.want.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)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
t.Errorf("tests[%v]: test case does not describe 'want' property", idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// naive encoder of placeholder flags
|
||||||
|
func (flags placeholderFlags) encodePlaceholder() string {
|
||||||
|
encoded := ""
|
||||||
|
if flags.plus {
|
||||||
|
encoded += "+"
|
||||||
|
}
|
||||||
|
if flags.preserveSpace {
|
||||||
|
encoded += "s"
|
||||||
|
}
|
||||||
|
if flags.number {
|
||||||
|
encoded += "n"
|
||||||
|
}
|
||||||
|
if flags.file {
|
||||||
|
encoded += "f"
|
||||||
|
}
|
||||||
|
if flags.query {
|
||||||
|
encoded += "q"
|
||||||
|
}
|
||||||
|
return encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
// can be replaced with os.ReadFile() in go 1.16+
|
||||||
|
func readFile(path string) ([]byte, error) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
data := make([]byte, 0, 128)
|
||||||
|
for {
|
||||||
|
if len(data) >= cap(data) {
|
||||||
|
d := append(data[:cap(data)], 0)
|
||||||
|
data = d[:len(data)]
|
||||||
|
}
|
||||||
|
|
||||||
|
n, err := file.Read(data[len(data):cap(data)])
|
||||||
|
data = data[:len(data)+n]
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// +build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||||
@@ -13,9 +16,18 @@ func notifyOnResize(resizeChan chan<- os.Signal) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func notifyStop(p *os.Process) {
|
func notifyStop(p *os.Process) {
|
||||||
p.Signal(syscall.SIGSTOP)
|
pid := p.Pid
|
||||||
|
pgid, err := unix.Getpgid(pid)
|
||||||
|
if err == nil {
|
||||||
|
pid = pgid * -1
|
||||||
|
}
|
||||||
|
unix.Kill(pid, syscall.SIGSTOP)
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||||
signal.Notify(resizeChan, syscall.SIGCONT)
|
signal.Notify(resizeChan, syscall.SIGCONT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func quoteEntry(entry string) string {
|
||||||
|
return "'" + strings.Replace(entry, "'", "'\\''", -1) + "'"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// +build windows
|
//go:build windows
|
||||||
|
|
||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func notifyOnResize(resizeChan chan<- os.Signal) {
|
func notifyOnResize(resizeChan chan<- os.Signal) {
|
||||||
@@ -17,3 +19,27 @@ func notifyStop(p *os.Process) {
|
|||||||
func notifyOnCont(resizeChan chan<- os.Signal) {
|
func notifyOnCont(resizeChan chan<- os.Signal) {
|
||||||
// NOOP
|
// 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) + "'"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package fzf
|
package fzf
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -22,12 +24,22 @@ type Token struct {
|
|||||||
prefixLength int32
|
prefixLength int32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of a Token.
|
||||||
|
func (t Token) String() string {
|
||||||
|
return fmt.Sprintf("Token{text: %s, prefixLength: %d}", t.text, t.prefixLength)
|
||||||
|
}
|
||||||
|
|
||||||
// Delimiter for tokenizing the input
|
// Delimiter for tokenizing the input
|
||||||
type Delimiter struct {
|
type Delimiter struct {
|
||||||
regex *regexp.Regexp
|
regex *regexp.Regexp
|
||||||
str *string
|
str *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
func newRange(begin int, end int) Range {
|
||||||
if begin == 1 {
|
if begin == 1 {
|
||||||
begin = rangeEllipsis
|
begin = rangeEllipsis
|
||||||
@@ -74,14 +86,14 @@ func ParseRange(str *string) (Range, bool) {
|
|||||||
return newRange(n, n), true
|
return newRange(n, n), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func withPrefixLengths(tokens []util.Chars, begin int) []Token {
|
func withPrefixLengths(tokens []string, begin int) []Token {
|
||||||
ret := make([]Token, len(tokens))
|
ret := make([]Token, len(tokens))
|
||||||
|
|
||||||
prefixLength := begin
|
prefixLength := begin
|
||||||
for idx, token := range tokens {
|
for idx := range tokens {
|
||||||
// NOTE: &tokens[idx] instead of &tokens
|
chars := util.ToChars([]byte(tokens[idx]))
|
||||||
ret[idx] = Token{&tokens[idx], int32(prefixLength)}
|
ret[idx] = Token{&chars, int32(prefixLength)}
|
||||||
prefixLength += token.Length()
|
prefixLength += chars.Length()
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@@ -92,16 +104,15 @@ const (
|
|||||||
awkWhite
|
awkWhite
|
||||||
)
|
)
|
||||||
|
|
||||||
func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
func awkTokenizer(input string) ([]string, int) {
|
||||||
// 9, 32
|
// 9, 32
|
||||||
ret := []util.Chars{}
|
ret := []string{}
|
||||||
prefixLength := 0
|
prefixLength := 0
|
||||||
state := awkNil
|
state := awkNil
|
||||||
numChars := input.Length()
|
|
||||||
begin := 0
|
begin := 0
|
||||||
end := 0
|
end := 0
|
||||||
for idx := 0; idx < numChars; idx++ {
|
for idx := 0; idx < len(input); idx++ {
|
||||||
r := input.Get(idx)
|
r := input[idx]
|
||||||
white := r == 9 || r == 32
|
white := r == 9 || r == 32
|
||||||
switch state {
|
switch state {
|
||||||
case awkNil:
|
case awkNil:
|
||||||
@@ -119,19 +130,19 @@ func awkTokenizer(input util.Chars) ([]util.Chars, int) {
|
|||||||
if white {
|
if white {
|
||||||
end = idx + 1
|
end = idx + 1
|
||||||
} else {
|
} else {
|
||||||
ret = append(ret, input.Slice(begin, end))
|
ret = append(ret, input[begin:end])
|
||||||
state, begin, end = awkBlack, idx, idx+1
|
state, begin, end = awkBlack, idx, idx+1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if begin < end {
|
if begin < end {
|
||||||
ret = append(ret, input.Slice(begin, end))
|
ret = append(ret, input[begin:end])
|
||||||
}
|
}
|
||||||
return ret, prefixLength
|
return ret, prefixLength
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tokenize tokenizes the given string with the delimiter
|
// Tokenize tokenizes the given string with the delimiter
|
||||||
func Tokenize(text util.Chars, delimiter Delimiter) []Token {
|
func Tokenize(text string, delimiter Delimiter) []Token {
|
||||||
if delimiter.str == nil && delimiter.regex == nil {
|
if delimiter.str == nil && delimiter.regex == nil {
|
||||||
// AWK-style (\S+\s*)
|
// AWK-style (\S+\s*)
|
||||||
tokens, prefixLength := awkTokenizer(text)
|
tokens, prefixLength := awkTokenizer(text)
|
||||||
@@ -139,36 +150,31 @@ func Tokenize(text util.Chars, delimiter Delimiter) []Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if delimiter.str != nil {
|
if delimiter.str != nil {
|
||||||
return withPrefixLengths(text.Split(*delimiter.str), 0)
|
return withPrefixLengths(strings.SplitAfter(text, *delimiter.str), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME performance
|
// FIXME performance
|
||||||
var tokens []string
|
var tokens []string
|
||||||
if delimiter.regex != nil {
|
if delimiter.regex != nil {
|
||||||
str := text.ToString()
|
locs := delimiter.regex.FindAllStringIndex(text, -1)
|
||||||
for len(str) > 0 {
|
begin := 0
|
||||||
loc := delimiter.regex.FindStringIndex(str)
|
for _, loc := range locs {
|
||||||
if loc == nil {
|
tokens = append(tokens, text[begin:loc[1]])
|
||||||
loc = []int{0, len(str)}
|
begin = loc[1]
|
||||||
}
|
}
|
||||||
last := util.Max(loc[1], 1)
|
if begin < len(text) {
|
||||||
tokens = append(tokens, str[:last])
|
tokens = append(tokens, text[begin:])
|
||||||
str = str[last:]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
asRunes := make([]util.Chars, len(tokens))
|
return withPrefixLengths(tokens, 0)
|
||||||
for i, token := range tokens {
|
|
||||||
asRunes[i] = util.RunesToChars([]rune(token))
|
|
||||||
}
|
|
||||||
return withPrefixLengths(asRunes, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinTokens(tokens []Token) []rune {
|
func joinTokens(tokens []Token) string {
|
||||||
ret := []rune{}
|
var output bytes.Buffer
|
||||||
for _, token := range tokens {
|
for _, token := range tokens {
|
||||||
ret = append(ret, token.text.ToRunes()...)
|
output.WriteString(token.text.ToString())
|
||||||
}
|
}
|
||||||
return ret
|
return output.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transform is used to transform the input when --with-nth option is given
|
// Transform is used to transform the input when --with-nth option is given
|
||||||
@@ -181,7 +187,7 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
|||||||
if r.begin == r.end {
|
if r.begin == r.end {
|
||||||
idx := r.begin
|
idx := r.begin
|
||||||
if idx == rangeEllipsis {
|
if idx == rangeEllipsis {
|
||||||
chars := util.RunesToChars(joinTokens(tokens))
|
chars := util.ToChars([]byte(joinTokens(tokens)))
|
||||||
parts = append(parts, &chars)
|
parts = append(parts, &chars)
|
||||||
} else {
|
} else {
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
@@ -224,15 +230,15 @@ func Transform(tokens []Token, withNth []Range) []Token {
|
|||||||
var merged util.Chars
|
var merged util.Chars
|
||||||
switch len(parts) {
|
switch len(parts) {
|
||||||
case 0:
|
case 0:
|
||||||
merged = util.RunesToChars([]rune{})
|
merged = util.ToChars([]byte{})
|
||||||
case 1:
|
case 1:
|
||||||
merged = *parts[0]
|
merged = *parts[0]
|
||||||
default:
|
default:
|
||||||
runes := []rune{}
|
var output bytes.Buffer
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
runes = append(runes, part.ToRunes()...)
|
output.WriteString(part.ToString())
|
||||||
}
|
}
|
||||||
merged = util.RunesToChars(runes)
|
merged = util.ToChars(output.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefixLength int32
|
var prefixLength int32
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package fzf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/junegunn/fzf/src/util"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseRange(t *testing.T) {
|
func TestParseRange(t *testing.T) {
|
||||||
@@ -11,35 +9,35 @@ func TestParseRange(t *testing.T) {
|
|||||||
i := ".."
|
i := ".."
|
||||||
r, _ := ParseRange(&i)
|
r, _ := ParseRange(&i)
|
||||||
if r.begin != rangeEllipsis || r.end != rangeEllipsis {
|
if r.begin != rangeEllipsis || r.end != rangeEllipsis {
|
||||||
t.Errorf("%s", r)
|
t.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
i := "3.."
|
i := "3.."
|
||||||
r, _ := ParseRange(&i)
|
r, _ := ParseRange(&i)
|
||||||
if r.begin != 3 || r.end != rangeEllipsis {
|
if r.begin != 3 || r.end != rangeEllipsis {
|
||||||
t.Errorf("%s", r)
|
t.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
i := "3..5"
|
i := "3..5"
|
||||||
r, _ := ParseRange(&i)
|
r, _ := ParseRange(&i)
|
||||||
if r.begin != 3 || r.end != 5 {
|
if r.begin != 3 || r.end != 5 {
|
||||||
t.Errorf("%s", r)
|
t.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
i := "-3..-5"
|
i := "-3..-5"
|
||||||
r, _ := ParseRange(&i)
|
r, _ := ParseRange(&i)
|
||||||
if r.begin != -3 || r.end != -5 {
|
if r.begin != -3 || r.end != -5 {
|
||||||
t.Errorf("%s", r)
|
t.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
i := "3"
|
i := "3"
|
||||||
r, _ := ParseRange(&i)
|
r, _ := ParseRange(&i)
|
||||||
if r.begin != 3 || r.end != 3 {
|
if r.begin != 3 || r.end != 3 {
|
||||||
t.Errorf("%s", r)
|
t.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,19 +45,19 @@ func TestParseRange(t *testing.T) {
|
|||||||
func TestTokenize(t *testing.T) {
|
func TestTokenize(t *testing.T) {
|
||||||
// AWK-style
|
// AWK-style
|
||||||
input := " abc: def: ghi "
|
input := " abc: def: ghi "
|
||||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
tokens := Tokenize(input, Delimiter{})
|
||||||
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
|
if tokens[0].text.ToString() != "abc: " || tokens[0].prefixLength != 2 {
|
||||||
t.Errorf("%s", tokens)
|
t.Errorf("%s", tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With delimiter
|
// With delimiter
|
||||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
tokens = Tokenize(input, delimiterRegexp(":"))
|
||||||
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
|
if tokens[0].text.ToString() != " abc:" || tokens[0].prefixLength != 0 {
|
||||||
t.Errorf("%s", tokens)
|
t.Error(tokens[0].text.ToString(), tokens[0].prefixLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
// With delimiter regex
|
// With delimiter regex
|
||||||
tokens = Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp("\\s+"))
|
tokens = Tokenize(input, delimiterRegexp("\\s+"))
|
||||||
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
|
if tokens[0].text.ToString() != " " || tokens[0].prefixLength != 0 ||
|
||||||
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
|
tokens[1].text.ToString() != "abc: " || tokens[1].prefixLength != 2 ||
|
||||||
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
|
tokens[2].text.ToString() != "def: " || tokens[2].prefixLength != 8 ||
|
||||||
@@ -71,11 +69,11 @@ func TestTokenize(t *testing.T) {
|
|||||||
func TestTransform(t *testing.T) {
|
func TestTransform(t *testing.T) {
|
||||||
input := " abc: def: ghi: jkl"
|
input := " abc: def: ghi: jkl"
|
||||||
{
|
{
|
||||||
tokens := Tokenize(util.RunesToChars([]rune(input)), Delimiter{})
|
tokens := Tokenize(input, Delimiter{})
|
||||||
{
|
{
|
||||||
ranges := splitNth("1,2,3")
|
ranges := splitNth("1,2,3")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if string(joinTokens(tx)) != "abc: def: ghi: " {
|
if joinTokens(tx) != "abc: def: ghi: " {
|
||||||
t.Errorf("%s", tx)
|
t.Errorf("%s", tx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,11 +91,11 @@ func TestTransform(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
tokens := Tokenize(util.RunesToChars([]rune(input)), delimiterRegexp(":"))
|
tokens := Tokenize(input, delimiterRegexp(":"))
|
||||||
{
|
{
|
||||||
ranges := splitNth("1..2,3,2..,1")
|
ranges := splitNth("1..2,3,2..,1")
|
||||||
tx := Transform(tokens, ranges)
|
tx := Transform(tokens, ranges)
|
||||||
if string(joinTokens(tx)) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
if joinTokens(tx) != " abc: def: ghi: def: ghi: jkl abc:" ||
|
||||||
len(tx) != 4 ||
|
len(tx) != 4 ||
|
||||||
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
tx[0].text.ToString() != " abc: def:" || tx[0].prefixLength != 0 ||
|
||||||
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
tx[1].text.ToString() != " ghi:" || tx[1].prefixLength != 12 ||
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
// +build !ncurses
|
//go:build !tcell && !windows
|
||||||
// +build !tcell
|
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
type Attr int
|
type Attr int32
|
||||||
|
|
||||||
func HasFullscreenRenderer() bool {
|
func HasFullscreenRenderer() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var DefaultBorderShape BorderShape = BorderRounded
|
||||||
|
|
||||||
func (a Attr) Merge(b Attr) Attr {
|
func (a Attr) Merge(b Attr) Attr {
|
||||||
return a | b
|
return a | b
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttrRegular Attr = Attr(0)
|
AttrUndefined = Attr(0)
|
||||||
|
AttrRegular = Attr(1 << 8)
|
||||||
|
AttrClear = Attr(1 << 9)
|
||||||
|
|
||||||
Bold = Attr(1)
|
Bold = Attr(1)
|
||||||
Dim = Attr(1 << 1)
|
Dim = Attr(1 << 1)
|
||||||
Italic = Attr(1 << 2)
|
Italic = Attr(1 << 2)
|
||||||
@@ -23,23 +26,27 @@ const (
|
|||||||
Blink = Attr(1 << 4)
|
Blink = Attr(1 << 4)
|
||||||
Blink2 = Attr(1 << 5)
|
Blink2 = Attr(1 << 5)
|
||||||
Reverse = Attr(1 << 6)
|
Reverse = Attr(1 << 6)
|
||||||
|
StrikeThrough = Attr(1 << 7)
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Init() {}
|
func (r *FullscreenRenderer) Init() {}
|
||||||
|
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||||
func (r *FullscreenRenderer) Pause(bool) {}
|
func (r *FullscreenRenderer) Pause(bool) {}
|
||||||
func (r *FullscreenRenderer) Resume(bool) {}
|
func (r *FullscreenRenderer) Resume(bool, bool) {}
|
||||||
|
func (r *FullscreenRenderer) PassThrough(string) {}
|
||||||
func (r *FullscreenRenderer) Clear() {}
|
func (r *FullscreenRenderer) Clear() {}
|
||||||
|
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool { return false }
|
||||||
func (r *FullscreenRenderer) Refresh() {}
|
func (r *FullscreenRenderer) Refresh() {}
|
||||||
func (r *FullscreenRenderer) Close() {}
|
func (r *FullscreenRenderer) Close() {}
|
||||||
|
func (r *FullscreenRenderer) Size() TermSize { return TermSize{} }
|
||||||
|
|
||||||
func (r *FullscreenRenderer) DoesAutoWrap() bool { return false }
|
|
||||||
func (r *FullscreenRenderer) IsOptimized() bool { return false }
|
|
||||||
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
func (r *FullscreenRenderer) GetChar() Event { return Event{} }
|
||||||
|
func (r *FullscreenRenderer) Top() int { return 0 }
|
||||||
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
func (r *FullscreenRenderer) MaxX() int { return 0 }
|
||||||
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
func (r *FullscreenRenderer) MaxY() int { return 0 }
|
||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
793
src/tui/light.go
793
src/tui/light.go
File diff suppressed because it is too large
Load Diff
119
src/tui/light_unix.go
Normal file
119
src/tui/light_unix.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
"golang.org/x/term"
|
||||||
|
)
|
||||||
|
|
||||||
|
func IsLightRendererSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) defaultTheme() *ColorTheme {
|
||||||
|
if strings.Contains(os.Getenv("TERM"), "256") {
|
||||||
|
return Dark256
|
||||||
|
}
|
||||||
|
colors, err := exec.Command("tput", "colors").Output()
|
||||||
|
if err == nil && atoi(strings.TrimSpace(string(colors)), 16) > 16 {
|
||||||
|
return Dark256
|
||||||
|
}
|
||||||
|
return Default16
|
||||||
|
}
|
||||||
|
|
||||||
|
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) closePlatform() {
|
||||||
|
// NOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTtyIn() *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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, "Failed to open "+consoleDevice)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
return in
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) setupTerminal() {
|
||||||
|
term.MakeRaw(r.fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) restoreTerminal() {
|
||||||
|
term.Restore(r.fd(), r.origState)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) updateTerminalSize() {
|
||||||
|
width, height, err := term.GetSize(r.fd())
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
r.width = width
|
||||||
|
r.height = r.maxHeightFunc(height)
|
||||||
|
} else {
|
||||||
|
r.width = getEnv("COLUMNS", defaultWidth)
|
||||||
|
r.height = r.maxHeightFunc(getEnv("LINES", defaultHeight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||||
|
r.csi("6n")
|
||||||
|
r.flush()
|
||||||
|
bytes := []byte{}
|
||||||
|
for tries := 0; tries < offsetPollTries; tries++ {
|
||||||
|
bytes = r.getBytesInternal(bytes, tries > 0)
|
||||||
|
offsets := offsetRegexp.FindSubmatch(bytes)
|
||||||
|
if len(offsets) > 3 {
|
||||||
|
// Add anything we skipped over to the input buffer
|
||||||
|
r.buffer = append(r.buffer, offsets[1]...)
|
||||||
|
return atoi(string(offsets[2]), 0) - 1, atoi(string(offsets[3]), 0) - 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||||
|
b := make([]byte, 1)
|
||||||
|
fd := r.fd()
|
||||||
|
util.SetNonblock(r.ttyin, nonblock)
|
||||||
|
_, err := util.Read(fd, b)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
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)}
|
||||||
|
}
|
||||||
153
src/tui/light_windows.go
Normal file
153
src/tui/light_windows.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
timeoutInterval = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsLightRendererSupported checks to see if the Light renderer is supported
|
||||||
|
func IsLightRendererSupported() bool {
|
||||||
|
var oldState uint32
|
||||||
|
// enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
|
||||||
|
if windows.GetConsoleMode(windows.Stderr, &oldState) != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// attempt to set mode to determine if we support VT 100 codes. This will work on newer Windows 10
|
||||||
|
// version:
|
||||||
|
canSetVt100 := windows.SetConsoleMode(windows.Stderr, oldState|windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == nil
|
||||||
|
var checkState uint32
|
||||||
|
if windows.GetConsoleMode(windows.Stderr, &checkState) != nil ||
|
||||||
|
(checkState&windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING) != windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
windows.SetConsoleMode(windows.Stderr, oldState)
|
||||||
|
return canSetVt100
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return Dark256
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) initPlatform() error {
|
||||||
|
//outHandle := windows.Stdout
|
||||||
|
outHandle, _ := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
|
||||||
|
// enable vt100 emulation (https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences)
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(outHandle), &r.origStateOutput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.outHandle = uintptr(outHandle)
|
||||||
|
inHandle, _ := syscall.Open("CONIN$", syscall.O_RDWR, 0)
|
||||||
|
if err := windows.GetConsoleMode(windows.Handle(inHandle), &r.origStateInput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) closePlatform() {
|
||||||
|
windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||||
|
windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
func openTtyIn() *os.File {
|
||||||
|
// not used
|
||||||
|
return 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 (r *LightRenderer) restoreTerminal() error {
|
||||||
|
if err := windows.SetConsoleMode(windows.Handle(r.inHandle), r.origStateInput); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return windows.SetConsoleMode(windows.Handle(r.outHandle), r.origStateOutput)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) 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 TermSize{h, w, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) updateTerminalSize() {
|
||||||
|
size := r.Size()
|
||||||
|
r.width = size.Columns
|
||||||
|
r.height = size.Lines
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) findOffset() (row int, col int) {
|
||||||
|
var bufferInfo windows.ConsoleScreenBufferInfo
|
||||||
|
if err := windows.GetConsoleScreenBufferInfo(windows.Handle(r.outHandle), &bufferInfo); err != nil {
|
||||||
|
return -1, -1
|
||||||
|
}
|
||||||
|
return int(bufferInfo.CursorPosition.X), int(bufferInfo.CursorPosition.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *LightRenderer) getch(nonblock bool) (int, bool) {
|
||||||
|
if nonblock {
|
||||||
|
select {
|
||||||
|
case bc := <-r.ttyinChannel:
|
||||||
|
return int(bc), true
|
||||||
|
case <-time.After(timeoutInterval * time.Millisecond):
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bc := <-r.ttyinChannel
|
||||||
|
return int(bc), true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
// +build ncurses
|
|
||||||
// +build !windows
|
|
||||||
// +build !tcell
|
|
||||||
|
|
||||||
package tui
|
|
||||||
|
|
||||||
/*
|
|
||||||
#include <ncurses.h>
|
|
||||||
#include <locale.h>
|
|
||||||
#cgo !static LDFLAGS: -lncurses
|
|
||||||
#cgo static LDFLAGS: -l:libncursesw.a -l:libtinfo.a -l:libgpm.a -ldl
|
|
||||||
#cgo android static LDFLAGS: -l:libncurses.a -fPIE -march=armv7-a -mfpu=neon -mhard-float -Wl,--no-warn-mismatch
|
|
||||||
|
|
||||||
FILE* c_tty() {
|
|
||||||
return fopen("/dev/tty", "r");
|
|
||||||
}
|
|
||||||
|
|
||||||
SCREEN* c_newterm(FILE* tty) {
|
|
||||||
return newterm(NULL, stderr, tty);
|
|
||||||
}
|
|
||||||
|
|
||||||
int c_getcurx(WINDOW* win) {
|
|
||||||
return getcurx(win);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
func HasFullscreenRenderer() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
type Attr C.uint
|
|
||||||
|
|
||||||
type CursesWindow struct {
|
|
||||||
impl *C.WINDOW
|
|
||||||
top int
|
|
||||||
left int
|
|
||||||
width int
|
|
||||||
height int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Top() int {
|
|
||||||
return w.top
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Left() int {
|
|
||||||
return w.left
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Width() int {
|
|
||||||
return w.width
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Height() int {
|
|
||||||
return w.height
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Refresh() {
|
|
||||||
C.wnoutrefresh(w.impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) FinishFill() {
|
|
||||||
// NO-OP
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
Bold Attr = C.A_BOLD
|
|
||||||
Dim = C.A_DIM
|
|
||||||
Blink = C.A_BLINK
|
|
||||||
Reverse = C.A_REVERSE
|
|
||||||
Underline = C.A_UNDERLINE
|
|
||||||
)
|
|
||||||
|
|
||||||
var Italic Attr = C.A_VERTICAL << 1 // FIXME
|
|
||||||
|
|
||||||
const (
|
|
||||||
AttrRegular Attr = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_screen *C.SCREEN
|
|
||||||
_colorMap map[int]int16
|
|
||||||
_colorFn func(ColorPair, Attr) (C.short, C.int)
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
_colorMap = make(map[int]int16)
|
|
||||||
if strings.HasPrefix(C.GoString(C.curses_version()), "ncurses 5") {
|
|
||||||
Italic = C.A_NORMAL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a Attr) Merge(b Attr) Attr {
|
|
||||||
return a | b
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
|
||||||
if C.tigetnum(C.CString("colors")) >= 256 {
|
|
||||||
return Dark256
|
|
||||||
}
|
|
||||||
return Default16
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Init() {
|
|
||||||
C.setlocale(C.LC_ALL, C.CString(""))
|
|
||||||
tty := C.c_tty()
|
|
||||||
if tty == nil {
|
|
||||||
errorExit("Failed to open /dev/tty")
|
|
||||||
}
|
|
||||||
_screen = C.c_newterm(tty)
|
|
||||||
if _screen == nil {
|
|
||||||
errorExit("Invalid $TERM: " + os.Getenv("TERM"))
|
|
||||||
}
|
|
||||||
C.set_term(_screen)
|
|
||||||
if r.mouse {
|
|
||||||
C.mousemask(C.ALL_MOUSE_EVENTS, nil)
|
|
||||||
C.mouseinterval(0)
|
|
||||||
}
|
|
||||||
C.noecho()
|
|
||||||
C.raw() // stty dsusp undef
|
|
||||||
C.nonl()
|
|
||||||
C.keypad(C.stdscr, true)
|
|
||||||
|
|
||||||
delay := 50
|
|
||||||
delayEnv := os.Getenv("ESCDELAY")
|
|
||||||
if len(delayEnv) > 0 {
|
|
||||||
num, err := strconv.Atoi(delayEnv)
|
|
||||||
if err == nil && num >= 0 {
|
|
||||||
delay = num
|
|
||||||
}
|
|
||||||
}
|
|
||||||
C.set_escdelay(C.int(delay))
|
|
||||||
|
|
||||||
if r.theme != nil {
|
|
||||||
C.start_color()
|
|
||||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
|
||||||
initPairs(r.theme)
|
|
||||||
C.bkgd(C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
|
|
||||||
_colorFn = attrColored
|
|
||||||
} else {
|
|
||||||
initTheme(r.theme, nil, r.forceBlack)
|
|
||||||
_colorFn = attrMono
|
|
||||||
}
|
|
||||||
|
|
||||||
C.nodelay(C.stdscr, true)
|
|
||||||
ch := C.getch()
|
|
||||||
if ch != C.ERR {
|
|
||||||
C.ungetch(ch)
|
|
||||||
}
|
|
||||||
C.nodelay(C.stdscr, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func initPairs(theme *ColorTheme) {
|
|
||||||
C.assume_default_colors(C.int(theme.Fg), C.int(theme.Bg))
|
|
||||||
for _, pair := range []ColorPair{
|
|
||||||
ColNormal,
|
|
||||||
ColPrompt,
|
|
||||||
ColMatch,
|
|
||||||
ColCurrent,
|
|
||||||
ColCurrentMatch,
|
|
||||||
ColSpinner,
|
|
||||||
ColInfo,
|
|
||||||
ColCursor,
|
|
||||||
ColSelected,
|
|
||||||
ColHeader,
|
|
||||||
ColBorder} {
|
|
||||||
C.init_pair(C.short(pair.index()), C.short(pair.Fg()), C.short(pair.Bg()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Pause(bool) {
|
|
||||||
C.endwin()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Resume(bool) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Close() {
|
|
||||||
C.endwin()
|
|
||||||
C.delscreen(_screen)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
|
||||||
win := C.newwin(C.int(height), C.int(width), C.int(top), C.int(left))
|
|
||||||
if r.theme != nil {
|
|
||||||
C.wbkgd(win, C.chtype(C.COLOR_PAIR(C.int(ColNormal.index()))))
|
|
||||||
}
|
|
||||||
// FIXME Does not implement BorderHorizontal
|
|
||||||
if borderStyle != BorderNone {
|
|
||||||
pair, attr := _colorFn(ColBorder, 0)
|
|
||||||
C.wcolor_set(win, pair, nil)
|
|
||||||
C.wattron(win, attr)
|
|
||||||
C.box(win, 0, 0)
|
|
||||||
C.wattroff(win, attr)
|
|
||||||
C.wcolor_set(win, 0, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CursesWindow{
|
|
||||||
impl: win,
|
|
||||||
top: top,
|
|
||||||
left: left,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func attrColored(color ColorPair, a Attr) (C.short, C.int) {
|
|
||||||
return C.short(color.index()), C.int(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
func attrMono(color ColorPair, a Attr) (C.short, C.int) {
|
|
||||||
return 0, C.int(attrFor(color, a))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) MaxX() int {
|
|
||||||
return int(C.COLS)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) MaxY() int {
|
|
||||||
return int(C.LINES)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Close() {
|
|
||||||
C.delwin(w.impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Enclose(y int, x int) bool {
|
|
||||||
return bool(C.wenclose(w.impl, C.int(y), C.int(x)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Move(y int, x int) {
|
|
||||||
C.wmove(w.impl, C.int(y), C.int(x))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) MoveAndClear(y int, x int) {
|
|
||||||
w.Move(y, x)
|
|
||||||
C.wclrtoeol(w.impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Print(text string) {
|
|
||||||
C.waddstr(w.impl, C.CString(strings.Map(func(r rune) rune {
|
|
||||||
if r < 32 {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}, text)))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) CPrint(color ColorPair, attr Attr, text string) {
|
|
||||||
p, a := _colorFn(color, attr)
|
|
||||||
C.wcolor_set(w.impl, p, nil)
|
|
||||||
C.wattron(w.impl, a)
|
|
||||||
w.Print(text)
|
|
||||||
C.wattroff(w.impl, a)
|
|
||||||
C.wcolor_set(w.impl, 0, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Clear() {
|
|
||||||
C.clear()
|
|
||||||
C.endwin()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Refresh() {
|
|
||||||
C.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Erase() {
|
|
||||||
C.werase(w.impl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) X() int {
|
|
||||||
return int(C.c_getcurx(w.impl))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) IsOptimized() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) Fill(str string) FillReturn {
|
|
||||||
if C.waddstr(w.impl, C.CString(str)) == C.OK {
|
|
||||||
return FillContinue
|
|
||||||
}
|
|
||||||
return FillSuspend
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *CursesWindow) CFill(fg Color, bg Color, attr Attr, str string) FillReturn {
|
|
||||||
index := ColorPair{fg, bg, -1}.index()
|
|
||||||
C.wcolor_set(w.impl, C.short(index), nil)
|
|
||||||
C.wattron(w.impl, C.int(attr))
|
|
||||||
ret := w.Fill(str)
|
|
||||||
C.wattroff(w.impl, C.int(attr))
|
|
||||||
C.wcolor_set(w.impl, 0, nil)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
|
||||||
for _, w := range windows {
|
|
||||||
w.Refresh()
|
|
||||||
}
|
|
||||||
C.doupdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p ColorPair) index() int16 {
|
|
||||||
if p.id >= 0 {
|
|
||||||
return p.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// ncurses does not support 24-bit colors
|
|
||||||
if p.is24() {
|
|
||||||
return ColDefault.index()
|
|
||||||
}
|
|
||||||
|
|
||||||
key := p.key()
|
|
||||||
if found, prs := _colorMap[key]; prs {
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
id := int16(len(_colorMap)) + ColUser.id
|
|
||||||
C.init_pair(C.short(id), C.short(p.Fg()), C.short(p.Bg()))
|
|
||||||
_colorMap[key] = id
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func consume(expects ...rune) bool {
|
|
||||||
for _, r := range expects {
|
|
||||||
if int(C.getch()) != int(r) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func escSequence() Event {
|
|
||||||
C.nodelay(C.stdscr, true)
|
|
||||||
defer func() {
|
|
||||||
C.nodelay(C.stdscr, false)
|
|
||||||
}()
|
|
||||||
c := C.getch()
|
|
||||||
switch c {
|
|
||||||
case C.ERR:
|
|
||||||
return Event{ESC, 0, nil}
|
|
||||||
case CtrlM:
|
|
||||||
return Event{CtrlAltM, 0, nil}
|
|
||||||
case '/':
|
|
||||||
return Event{AltSlash, 0, nil}
|
|
||||||
case ' ':
|
|
||||||
return Event{AltSpace, 0, nil}
|
|
||||||
case 127, C.KEY_BACKSPACE:
|
|
||||||
return Event{AltBS, 0, nil}
|
|
||||||
case '[':
|
|
||||||
// Bracketed paste mode (printf "\e[?2004h")
|
|
||||||
// \e[200~ TEXT \e[201~
|
|
||||||
if consume('2', '0', '0', '~') {
|
|
||||||
return Event{Invalid, 0, nil}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if c >= 'a' && c <= 'z' {
|
|
||||||
return Event{AltA + int(c) - 'a', 0, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c >= '0' && c <= '9' {
|
|
||||||
return Event{Alt0 + int(c) - '0', 0, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't care. Ignore the rest.
|
|
||||||
for ; c != C.ERR; c = C.getch() {
|
|
||||||
}
|
|
||||||
return Event{Invalid, 0, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) GetChar() Event {
|
|
||||||
c := C.getch()
|
|
||||||
switch c {
|
|
||||||
case C.ERR:
|
|
||||||
// Unexpected error from blocking read
|
|
||||||
r.Close()
|
|
||||||
errorExit("Failed to read /dev/tty")
|
|
||||||
case C.KEY_UP:
|
|
||||||
return Event{Up, 0, nil}
|
|
||||||
case C.KEY_DOWN:
|
|
||||||
return Event{Down, 0, nil}
|
|
||||||
case C.KEY_LEFT:
|
|
||||||
return Event{Left, 0, nil}
|
|
||||||
case C.KEY_RIGHT:
|
|
||||||
return Event{Right, 0, nil}
|
|
||||||
case C.KEY_HOME:
|
|
||||||
return Event{Home, 0, nil}
|
|
||||||
case C.KEY_END:
|
|
||||||
return Event{End, 0, nil}
|
|
||||||
case C.KEY_BACKSPACE:
|
|
||||||
return Event{BSpace, 0, nil}
|
|
||||||
case C.KEY_F0 + 1:
|
|
||||||
return Event{F1, 0, nil}
|
|
||||||
case C.KEY_F0 + 2:
|
|
||||||
return Event{F2, 0, nil}
|
|
||||||
case C.KEY_F0 + 3:
|
|
||||||
return Event{F3, 0, nil}
|
|
||||||
case C.KEY_F0 + 4:
|
|
||||||
return Event{F4, 0, nil}
|
|
||||||
case C.KEY_F0 + 5:
|
|
||||||
return Event{F5, 0, nil}
|
|
||||||
case C.KEY_F0 + 6:
|
|
||||||
return Event{F6, 0, nil}
|
|
||||||
case C.KEY_F0 + 7:
|
|
||||||
return Event{F7, 0, nil}
|
|
||||||
case C.KEY_F0 + 8:
|
|
||||||
return Event{F8, 0, nil}
|
|
||||||
case C.KEY_F0 + 9:
|
|
||||||
return Event{F9, 0, nil}
|
|
||||||
case C.KEY_F0 + 10:
|
|
||||||
return Event{F10, 0, nil}
|
|
||||||
case C.KEY_F0 + 11:
|
|
||||||
return Event{F11, 0, nil}
|
|
||||||
case C.KEY_F0 + 12:
|
|
||||||
return Event{F12, 0, nil}
|
|
||||||
case C.KEY_DC:
|
|
||||||
return Event{Del, 0, nil}
|
|
||||||
case C.KEY_PPAGE:
|
|
||||||
return Event{PgUp, 0, nil}
|
|
||||||
case C.KEY_NPAGE:
|
|
||||||
return Event{PgDn, 0, nil}
|
|
||||||
case C.KEY_BTAB:
|
|
||||||
return Event{BTab, 0, nil}
|
|
||||||
case C.KEY_ENTER:
|
|
||||||
return Event{CtrlM, 0, nil}
|
|
||||||
case C.KEY_SLEFT:
|
|
||||||
return Event{SLeft, 0, nil}
|
|
||||||
case C.KEY_SRIGHT:
|
|
||||||
return Event{SRight, 0, nil}
|
|
||||||
case C.KEY_MOUSE:
|
|
||||||
var me C.MEVENT
|
|
||||||
if C.getmouse(&me) != C.ERR {
|
|
||||||
mod := ((me.bstate & C.BUTTON_SHIFT) | (me.bstate & C.BUTTON_CTRL) | (me.bstate & C.BUTTON_ALT)) > 0
|
|
||||||
x := int(me.x)
|
|
||||||
y := int(me.y)
|
|
||||||
/* Cannot use BUTTON1_DOUBLE_CLICKED due to mouseinterval(0) */
|
|
||||||
if (me.bstate & C.BUTTON1_PRESSED) > 0 {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
|
||||||
r.clickY = append(r.clickY, y)
|
|
||||||
} else {
|
|
||||||
r.clickY = []int{y}
|
|
||||||
r.prevDownTime = now
|
|
||||||
}
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, false, mod}}
|
|
||||||
} else if (me.bstate & C.BUTTON1_RELEASED) > 0 {
|
|
||||||
double := false
|
|
||||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
|
||||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
|
||||||
double = true
|
|
||||||
}
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, double, mod}}
|
|
||||||
} else if (me.bstate&0x8000000) > 0 || (me.bstate&0x80) > 0 {
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
|
|
||||||
} else if (me.bstate & C.BUTTON4_PRESSED) > 0 {
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 1, false, false, mod}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Event{Invalid, 0, nil}
|
|
||||||
case C.KEY_RESIZE:
|
|
||||||
return Event{Resize, 0, nil}
|
|
||||||
case ESC:
|
|
||||||
return escSequence()
|
|
||||||
case 127:
|
|
||||||
return Event{BSpace, 0, nil}
|
|
||||||
case 0:
|
|
||||||
return Event{CtrlSpace, 0, nil}
|
|
||||||
}
|
|
||||||
// CTRL-A ~ CTRL-Z
|
|
||||||
if c >= CtrlA && c <= CtrlZ {
|
|
||||||
return Event{int(c), 0, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Multi-byte character
|
|
||||||
buffer := []byte{byte(c)}
|
|
||||||
for {
|
|
||||||
r, _ := utf8.DecodeRune(buffer)
|
|
||||||
if r != utf8.RuneError {
|
|
||||||
return Event{Rune, r, nil}
|
|
||||||
}
|
|
||||||
|
|
||||||
c := C.getch()
|
|
||||||
if c == C.ERR {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if c >= C.KEY_CODE_YES {
|
|
||||||
C.ungetch(c)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
buffer = append(buffer, byte(c))
|
|
||||||
}
|
|
||||||
return Event{Invalid, 0, nil}
|
|
||||||
}
|
|
||||||
553
src/tui/tcell.go
553
src/tui/tcell.go
@@ -1,37 +1,52 @@
|
|||||||
// +build tcell windows
|
//go:build tcell || windows
|
||||||
|
|
||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
|
||||||
|
|
||||||
"runtime"
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/gdamore/tcell/v2/encoding"
|
||||||
"github.com/gdamore/tcell"
|
"github.com/junegunn/fzf/src/util"
|
||||||
"github.com/gdamore/tcell/encoding"
|
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HasFullscreenRenderer() bool {
|
func HasFullscreenRenderer() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ColorPair) style() tcell.Style {
|
var DefaultBorderShape BorderShape = BorderSharp
|
||||||
style := tcell.StyleDefault
|
|
||||||
return style.Foreground(tcell.Color(p.Fg())).Background(tcell.Color(p.Bg()))
|
func asTcellColor(color Color) tcell.Color {
|
||||||
|
if color == colDefault {
|
||||||
|
return tcell.ColorDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
type Attr tcell.Style
|
value := uint64(tcell.ColorValid) + uint64(color)
|
||||||
|
if color.is24() {
|
||||||
|
value = value | uint64(tcell.ColorIsRGB)
|
||||||
|
}
|
||||||
|
return tcell.Color(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) style() tcell.Style {
|
||||||
|
style := tcell.StyleDefault
|
||||||
|
return style.Foreground(asTcellColor(p.Fg())).Background(asTcellColor(p.Bg()))
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attr int32
|
||||||
|
|
||||||
type TcellWindow struct {
|
type TcellWindow struct {
|
||||||
color bool
|
color bool
|
||||||
|
preview bool
|
||||||
top int
|
top int
|
||||||
left int
|
left int
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
|
normal ColorPair
|
||||||
lastX int
|
lastX int
|
||||||
lastY int
|
lastY int
|
||||||
moveCursor bool
|
moveCursor bool
|
||||||
@@ -61,12 +76,6 @@ func (w *TcellWindow) Refresh() {
|
|||||||
}
|
}
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
w.lastY = 0
|
w.lastY = 0
|
||||||
switch w.borderStyle {
|
|
||||||
case BorderAround:
|
|
||||||
w.drawBorder(true)
|
|
||||||
case BorderHorizontal:
|
|
||||||
w.drawBorder(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) FinishFill() {
|
func (w *TcellWindow) FinishFill() {
|
||||||
@@ -79,13 +88,23 @@ const (
|
|||||||
Blink = Attr(tcell.AttrBlink)
|
Blink = Attr(tcell.AttrBlink)
|
||||||
Reverse = Attr(tcell.AttrReverse)
|
Reverse = Attr(tcell.AttrReverse)
|
||||||
Underline = Attr(tcell.AttrUnderline)
|
Underline = Attr(tcell.AttrUnderline)
|
||||||
Italic = Attr(tcell.AttrNone) // Not supported
|
StrikeThrough = Attr(tcell.AttrStrikeThrough)
|
||||||
|
Italic = Attr(tcell.AttrItalic)
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AttrRegular Attr = 0
|
AttrUndefined = Attr(0)
|
||||||
|
AttrRegular = Attr(1 << 7)
|
||||||
|
AttrClear = Attr(1 << 8)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) PassThrough(str string) {
|
||||||
|
// No-op
|
||||||
|
// https://github.com/gdamore/tcell/pull/650#issuecomment-1806442846
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) Resize(maxHeightFunc func(int) int) {}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
func (r *FullscreenRenderer) defaultTheme() *ColorTheme {
|
||||||
if _screen.Colors() >= 256 {
|
if _screen.Colors() >= 256 {
|
||||||
return Dark256
|
return Dark256
|
||||||
@@ -120,8 +139,11 @@ func (a Attr) Merge(b Attr) Attr {
|
|||||||
return a | b
|
return a | b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handle the following as private members of FullscreenRenderer instance
|
||||||
|
// they are declared here to prevent introducing tcell library in non-windows builds
|
||||||
var (
|
var (
|
||||||
_screen tcell.Screen
|
_screen tcell.Screen
|
||||||
|
_prevMouseButton tcell.ButtonMask
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *FullscreenRenderer) initScreen() {
|
func (r *FullscreenRenderer) initScreen() {
|
||||||
@@ -150,6 +172,10 @@ func (r *FullscreenRenderer) Init() {
|
|||||||
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
initTheme(r.theme, r.defaultTheme(), r.forceBlack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) Top() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) MaxX() int {
|
func (r *FullscreenRenderer) MaxX() int {
|
||||||
ncols, _ := _screen.Size()
|
ncols, _ := _screen.Size()
|
||||||
return int(ncols)
|
return int(ncols)
|
||||||
@@ -164,12 +190,8 @@ func (w *TcellWindow) X() int {
|
|||||||
return w.lastX
|
return w.lastX
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) DoesAutoWrap() bool {
|
func (w *TcellWindow) Y() int {
|
||||||
return false
|
return w.lastY
|
||||||
}
|
|
||||||
|
|
||||||
func (r *FullscreenRenderer) IsOptimized() bool {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Clear() {
|
func (r *FullscreenRenderer) Clear() {
|
||||||
@@ -177,10 +199,20 @@ func (r *FullscreenRenderer) Clear() {
|
|||||||
_screen.Clear()
|
_screen.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *FullscreenRenderer) NeedScrollbarRedraw() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Refresh() {
|
func (r *FullscreenRenderer) Refresh() {
|
||||||
// noop
|
// noop
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Pixel width and height not implemented
|
||||||
|
func (r *FullscreenRenderer) Size() TermSize {
|
||||||
|
cols, lines := _screen.Size()
|
||||||
|
return TermSize{lines, cols, 0, 0}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) GetChar() Event {
|
func (r *FullscreenRenderer) GetChar() Event {
|
||||||
ev := _screen.PollEvent()
|
ev := _screen.PollEvent()
|
||||||
switch ev := ev.(type) {
|
switch ev := ev.(type) {
|
||||||
@@ -189,120 +221,219 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
|
|
||||||
// process mouse events:
|
// process mouse events:
|
||||||
case *tcell.EventMouse:
|
case *tcell.EventMouse:
|
||||||
|
// mouse down events have zeroed buttons, so we can't use them
|
||||||
|
// mouse up event consists of two events, 1. (main) event with modifier and other metadata, 2. event with zeroed buttons
|
||||||
|
// so mouse click is three consecutive events, but the first and last are indistinguishable from movement events (with released buttons)
|
||||||
|
// dragging has same structure, it only repeats the middle (main) event appropriately
|
||||||
x, y := ev.Position()
|
x, y := ev.Position()
|
||||||
button := ev.Buttons()
|
|
||||||
mod := ev.Modifiers() != 0
|
mod := ev.Modifiers() != 0
|
||||||
if button&tcell.WheelDown != 0 {
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, mod}}
|
// since we dont have mouse down events (unlike LightRenderer), we need to track state in prevButton
|
||||||
} else if button&tcell.WheelUp != 0 {
|
prevButton, button := _prevMouseButton, ev.Buttons()
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, mod}}
|
_prevMouseButton = button
|
||||||
} else if runtime.GOOS != "windows" {
|
drag := prevButton == button
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case button&tcell.WheelDown != 0:
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, -1, false, false, false, mod}}
|
||||||
|
case button&tcell.WheelUp != 0:
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, +1, false, false, false, mod}}
|
||||||
|
case button&tcell.Button1 != 0:
|
||||||
|
double := false
|
||||||
|
if !drag {
|
||||||
|
// all potential double click events put their coordinates in the clicks array
|
||||||
|
// double click event has two conditions, temporal and spatial, the first is checked here
|
||||||
|
now := time.Now()
|
||||||
|
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
||||||
|
r.clicks = append(r.clicks, [2]int{x, y})
|
||||||
|
} else {
|
||||||
|
r.clicks = [][2]int{{x, y}}
|
||||||
|
}
|
||||||
|
r.prevDownTime = now
|
||||||
|
|
||||||
|
// detect double clicks (also check for spatial condition)
|
||||||
|
n := len(r.clicks)
|
||||||
|
double = n > 1 && r.clicks[n-2][0] == r.clicks[n-1][0] && r.clicks[n-2][1] == r.clicks[n-1][1]
|
||||||
|
if double {
|
||||||
|
// make sure two consecutive double clicks require four clicks
|
||||||
|
r.clicks = [][2]int{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fire single or double click event
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, true, !double, double, mod}}
|
||||||
|
case button&tcell.Button2 != 0:
|
||||||
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, false, true, false, mod}}
|
||||||
|
default:
|
||||||
// double and single taps on Windows don't quite work due to
|
// double and single taps on Windows don't quite work due to
|
||||||
// the console acting on the events and not allowing us
|
// the console acting on the events and not allowing us
|
||||||
// to consume them.
|
// to consume them.
|
||||||
|
left := button&tcell.Button1 != 0
|
||||||
down := button&tcell.Button1 != 0 // left
|
down := left || button&tcell.Button3 != 0
|
||||||
double := false
|
double := false
|
||||||
if down {
|
|
||||||
now := time.Now()
|
|
||||||
if now.Sub(r.prevDownTime) < doubleClickDuration {
|
|
||||||
r.clickY = append(r.clickY, x)
|
|
||||||
} else {
|
|
||||||
r.clickY = []int{x}
|
|
||||||
r.prevDownTime = now
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if len(r.clickY) > 1 && r.clickY[0] == r.clickY[1] &&
|
|
||||||
time.Now().Sub(r.prevDownTime) < doubleClickDuration {
|
|
||||||
double = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Event{Mouse, 0, &MouseEvent{y, x, 0, down, double, mod}}
|
return Event{Mouse, 0, &MouseEvent{y, x, 0, left, down, double, mod}}
|
||||||
}
|
}
|
||||||
|
|
||||||
// process keyboard:
|
// process keyboard:
|
||||||
case *tcell.EventKey:
|
case *tcell.EventKey:
|
||||||
alt := (ev.Modifiers() & tcell.ModAlt) > 0
|
mods := ev.Modifiers()
|
||||||
keyfn := func(r rune) int {
|
none := mods == tcell.ModNone
|
||||||
|
alt := (mods & tcell.ModAlt) > 0
|
||||||
|
ctrl := (mods & tcell.ModCtrl) > 0
|
||||||
|
shift := (mods & tcell.ModShift) > 0
|
||||||
|
ctrlAlt := ctrl && alt
|
||||||
|
altShift := alt && shift
|
||||||
|
|
||||||
|
keyfn := func(r rune) Event {
|
||||||
if alt {
|
if alt {
|
||||||
return CtrlAltA - 'a' + int(r)
|
return CtrlAltKey(r)
|
||||||
}
|
}
|
||||||
return CtrlA - 'a' + int(r)
|
return EventType(CtrlA.Int() - 'a' + int(r)).AsEvent()
|
||||||
}
|
}
|
||||||
switch ev.Key() {
|
switch ev.Key() {
|
||||||
|
// section 1: Ctrl+(Alt)+[a-z]
|
||||||
case tcell.KeyCtrlA:
|
case tcell.KeyCtrlA:
|
||||||
return Event{keyfn('a'), 0, nil}
|
return keyfn('a')
|
||||||
case tcell.KeyCtrlB:
|
case tcell.KeyCtrlB:
|
||||||
return Event{keyfn('b'), 0, nil}
|
return keyfn('b')
|
||||||
case tcell.KeyCtrlC:
|
case tcell.KeyCtrlC:
|
||||||
return Event{keyfn('c'), 0, nil}
|
return keyfn('c')
|
||||||
case tcell.KeyCtrlD:
|
case tcell.KeyCtrlD:
|
||||||
return Event{keyfn('d'), 0, nil}
|
return keyfn('d')
|
||||||
case tcell.KeyCtrlE:
|
case tcell.KeyCtrlE:
|
||||||
return Event{keyfn('e'), 0, nil}
|
return keyfn('e')
|
||||||
case tcell.KeyCtrlF:
|
case tcell.KeyCtrlF:
|
||||||
return Event{keyfn('f'), 0, nil}
|
return keyfn('f')
|
||||||
case tcell.KeyCtrlG:
|
case tcell.KeyCtrlG:
|
||||||
return Event{keyfn('g'), 0, nil}
|
return keyfn('g')
|
||||||
case tcell.KeyCtrlH:
|
case tcell.KeyCtrlH:
|
||||||
return Event{keyfn('h'), 0, nil}
|
switch ev.Rune() {
|
||||||
|
case 0:
|
||||||
|
if ctrl {
|
||||||
|
return Event{BSpace, 0, nil}
|
||||||
|
}
|
||||||
|
case rune(tcell.KeyCtrlH):
|
||||||
|
switch {
|
||||||
|
case ctrl:
|
||||||
|
return keyfn('h')
|
||||||
|
case alt:
|
||||||
|
return Event{AltBS, 0, nil}
|
||||||
|
case none, shift:
|
||||||
|
return Event{BSpace, 0, nil}
|
||||||
|
}
|
||||||
|
}
|
||||||
case tcell.KeyCtrlI:
|
case tcell.KeyCtrlI:
|
||||||
return Event{keyfn('i'), 0, nil}
|
return keyfn('i')
|
||||||
case tcell.KeyCtrlJ:
|
case tcell.KeyCtrlJ:
|
||||||
return Event{keyfn('j'), 0, nil}
|
return keyfn('j')
|
||||||
case tcell.KeyCtrlK:
|
case tcell.KeyCtrlK:
|
||||||
return Event{keyfn('k'), 0, nil}
|
return keyfn('k')
|
||||||
case tcell.KeyCtrlL:
|
case tcell.KeyCtrlL:
|
||||||
return Event{keyfn('l'), 0, nil}
|
return keyfn('l')
|
||||||
case tcell.KeyCtrlM:
|
case tcell.KeyCtrlM:
|
||||||
return Event{keyfn('m'), 0, nil}
|
return keyfn('m')
|
||||||
case tcell.KeyCtrlN:
|
case tcell.KeyCtrlN:
|
||||||
return Event{keyfn('n'), 0, nil}
|
return keyfn('n')
|
||||||
case tcell.KeyCtrlO:
|
case tcell.KeyCtrlO:
|
||||||
return Event{keyfn('o'), 0, nil}
|
return keyfn('o')
|
||||||
case tcell.KeyCtrlP:
|
case tcell.KeyCtrlP:
|
||||||
return Event{keyfn('p'), 0, nil}
|
return keyfn('p')
|
||||||
case tcell.KeyCtrlQ:
|
case tcell.KeyCtrlQ:
|
||||||
return Event{keyfn('q'), 0, nil}
|
return keyfn('q')
|
||||||
case tcell.KeyCtrlR:
|
case tcell.KeyCtrlR:
|
||||||
return Event{keyfn('r'), 0, nil}
|
return keyfn('r')
|
||||||
case tcell.KeyCtrlS:
|
case tcell.KeyCtrlS:
|
||||||
return Event{keyfn('s'), 0, nil}
|
return keyfn('s')
|
||||||
case tcell.KeyCtrlT:
|
case tcell.KeyCtrlT:
|
||||||
return Event{keyfn('t'), 0, nil}
|
return keyfn('t')
|
||||||
case tcell.KeyCtrlU:
|
case tcell.KeyCtrlU:
|
||||||
return Event{keyfn('u'), 0, nil}
|
return keyfn('u')
|
||||||
case tcell.KeyCtrlV:
|
case tcell.KeyCtrlV:
|
||||||
return Event{keyfn('v'), 0, nil}
|
return keyfn('v')
|
||||||
case tcell.KeyCtrlW:
|
case tcell.KeyCtrlW:
|
||||||
return Event{keyfn('w'), 0, nil}
|
return keyfn('w')
|
||||||
case tcell.KeyCtrlX:
|
case tcell.KeyCtrlX:
|
||||||
return Event{keyfn('x'), 0, nil}
|
return keyfn('x')
|
||||||
case tcell.KeyCtrlY:
|
case tcell.KeyCtrlY:
|
||||||
return Event{keyfn('y'), 0, nil}
|
return keyfn('y')
|
||||||
case tcell.KeyCtrlZ:
|
case tcell.KeyCtrlZ:
|
||||||
return Event{keyfn('z'), 0, nil}
|
return keyfn('z')
|
||||||
|
// section 2: Ctrl+[ \]_]
|
||||||
case tcell.KeyCtrlSpace:
|
case tcell.KeyCtrlSpace:
|
||||||
return Event{CtrlSpace, 0, nil}
|
return Event{CtrlSpace, 0, nil}
|
||||||
|
case tcell.KeyCtrlBackslash:
|
||||||
|
return Event{CtrlBackSlash, 0, nil}
|
||||||
|
case tcell.KeyCtrlRightSq:
|
||||||
|
return Event{CtrlRightBracket, 0, nil}
|
||||||
|
case tcell.KeyCtrlCarat:
|
||||||
|
return Event{CtrlCaret, 0, nil}
|
||||||
|
case tcell.KeyCtrlUnderscore:
|
||||||
|
return Event{CtrlSlash, 0, nil}
|
||||||
|
// section 3: (Alt)+Backspace2
|
||||||
case tcell.KeyBackspace2:
|
case tcell.KeyBackspace2:
|
||||||
if alt {
|
if alt {
|
||||||
return Event{AltBS, 0, nil}
|
return Event{AltBS, 0, nil}
|
||||||
}
|
}
|
||||||
return Event{BSpace, 0, nil}
|
return Event{BSpace, 0, nil}
|
||||||
|
|
||||||
|
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||||
case tcell.KeyUp:
|
case tcell.KeyUp:
|
||||||
|
if altShift {
|
||||||
|
return Event{AltSUp, 0, nil}
|
||||||
|
}
|
||||||
|
if shift {
|
||||||
|
return Event{SUp, 0, nil}
|
||||||
|
}
|
||||||
|
if alt {
|
||||||
|
return Event{AltUp, 0, nil}
|
||||||
|
}
|
||||||
return Event{Up, 0, nil}
|
return Event{Up, 0, nil}
|
||||||
case tcell.KeyDown:
|
case tcell.KeyDown:
|
||||||
|
if altShift {
|
||||||
|
return Event{AltSDown, 0, nil}
|
||||||
|
}
|
||||||
|
if shift {
|
||||||
|
return Event{SDown, 0, nil}
|
||||||
|
}
|
||||||
|
if alt {
|
||||||
|
return Event{AltDown, 0, nil}
|
||||||
|
}
|
||||||
return Event{Down, 0, nil}
|
return Event{Down, 0, nil}
|
||||||
case tcell.KeyLeft:
|
case tcell.KeyLeft:
|
||||||
|
if altShift {
|
||||||
|
return Event{AltSLeft, 0, nil}
|
||||||
|
}
|
||||||
|
if shift {
|
||||||
|
return Event{SLeft, 0, nil}
|
||||||
|
}
|
||||||
|
if alt {
|
||||||
|
return Event{AltLeft, 0, nil}
|
||||||
|
}
|
||||||
return Event{Left, 0, nil}
|
return Event{Left, 0, nil}
|
||||||
case tcell.KeyRight:
|
case tcell.KeyRight:
|
||||||
|
if altShift {
|
||||||
|
return Event{AltSRight, 0, nil}
|
||||||
|
}
|
||||||
|
if shift {
|
||||||
|
return Event{SRight, 0, nil}
|
||||||
|
}
|
||||||
|
if alt {
|
||||||
|
return Event{AltRight, 0, nil}
|
||||||
|
}
|
||||||
return Event{Right, 0, nil}
|
return Event{Right, 0, nil}
|
||||||
|
|
||||||
|
// section 5: (Insert|Home|Delete|End|PgUp|PgDn|BackTab|F1-F12)
|
||||||
|
case tcell.KeyInsert:
|
||||||
|
return Event{Insert, 0, nil}
|
||||||
case tcell.KeyHome:
|
case tcell.KeyHome:
|
||||||
return Event{Home, 0, nil}
|
return Event{Home, 0, nil}
|
||||||
case tcell.KeyDelete:
|
case tcell.KeyDelete:
|
||||||
|
if ctrl {
|
||||||
|
return Event{CtrlDelete, 0, nil}
|
||||||
|
}
|
||||||
|
if shift {
|
||||||
|
return Event{SDelete, 0, nil}
|
||||||
|
}
|
||||||
return Event{Del, 0, nil}
|
return Event{Del, 0, nil}
|
||||||
case tcell.KeyEnd:
|
case tcell.KeyEnd:
|
||||||
return Event{End, 0, nil}
|
return Event{End, 0, nil}
|
||||||
@@ -310,10 +441,8 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
return Event{PgUp, 0, nil}
|
return Event{PgUp, 0, nil}
|
||||||
case tcell.KeyPgDn:
|
case tcell.KeyPgDn:
|
||||||
return Event{PgDn, 0, nil}
|
return Event{PgDn, 0, nil}
|
||||||
|
|
||||||
case tcell.KeyBacktab:
|
case tcell.KeyBacktab:
|
||||||
return Event{BTab, 0, nil}
|
return Event{BTab, 0, nil}
|
||||||
|
|
||||||
case tcell.KeyF1:
|
case tcell.KeyF1:
|
||||||
return Event{F1, 0, nil}
|
return Event{F1, 0, nil}
|
||||||
case tcell.KeyF2:
|
case tcell.KeyF2:
|
||||||
@@ -339,41 +468,45 @@ func (r *FullscreenRenderer) GetChar() Event {
|
|||||||
case tcell.KeyF12:
|
case tcell.KeyF12:
|
||||||
return Event{F12, 0, nil}
|
return Event{F12, 0, nil}
|
||||||
|
|
||||||
// ev.Ch doesn't work for some reason for space:
|
// section 6: (Ctrl+Alt)+'rune'
|
||||||
case tcell.KeyRune:
|
case tcell.KeyRune:
|
||||||
r := ev.Rune()
|
r := ev.Rune()
|
||||||
if alt {
|
|
||||||
switch r {
|
|
||||||
case ' ':
|
|
||||||
return Event{AltSpace, 0, nil}
|
|
||||||
case '/':
|
|
||||||
return Event{AltSlash, 0, nil}
|
|
||||||
}
|
|
||||||
if r >= 'a' && r <= 'z' {
|
|
||||||
return Event{AltA + int(r) - 'a', 0, nil}
|
|
||||||
}
|
|
||||||
if r >= '0' && r <= '9' {
|
|
||||||
return Event{Alt0 + int(r) - '0', 0, nil}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Event{Rune, r, nil}
|
|
||||||
|
|
||||||
|
switch {
|
||||||
|
// translate native key events to ascii control characters
|
||||||
|
case r == ' ' && ctrl:
|
||||||
|
return Event{CtrlSpace, 0, nil}
|
||||||
|
// handle AltGr characters
|
||||||
|
case ctrlAlt:
|
||||||
|
return Event{Rune, r, nil} // dropping modifiers
|
||||||
|
// simple characters (possibly with modifier)
|
||||||
|
case alt:
|
||||||
|
return AltKey(r)
|
||||||
|
default:
|
||||||
|
return Event{Rune, r, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
// section 7: Esc
|
||||||
case tcell.KeyEsc:
|
case tcell.KeyEsc:
|
||||||
return Event{ESC, 0, nil}
|
return Event{ESC, 0, nil}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// section 8: Invalid
|
||||||
return Event{Invalid, 0, nil}
|
return Event{Invalid, 0, nil}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Pause(bool) {
|
func (r *FullscreenRenderer) Pause(clear bool) {
|
||||||
|
if clear {
|
||||||
_screen.Fini()
|
_screen.Fini()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Resume(bool) {
|
func (r *FullscreenRenderer) Resume(clear bool, sigcont bool) {
|
||||||
|
if clear {
|
||||||
r.initScreen()
|
r.initScreen()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) Close() {
|
func (r *FullscreenRenderer) Close() {
|
||||||
_screen.Fini()
|
_screen.Fini()
|
||||||
@@ -387,32 +520,44 @@ func (r *FullscreenRenderer) RefreshWindows(windows []Window) {
|
|||||||
_screen.Show()
|
_screen.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window {
|
func (r *FullscreenRenderer) NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window {
|
||||||
// TODO
|
normal := ColNormal
|
||||||
return &TcellWindow{
|
if preview {
|
||||||
color: r.theme != nil,
|
normal = ColPreview
|
||||||
|
}
|
||||||
|
w := &TcellWindow{
|
||||||
|
color: r.theme.Colored,
|
||||||
|
preview: preview,
|
||||||
top: top,
|
top: top,
|
||||||
left: left,
|
left: left,
|
||||||
width: width,
|
width: width,
|
||||||
height: height,
|
height: height,
|
||||||
|
normal: normal,
|
||||||
borderStyle: borderStyle}
|
borderStyle: borderStyle}
|
||||||
|
w.drawBorder(false)
|
||||||
|
return w
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Close() {
|
func (w *TcellWindow) Close() {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
func fill(x, y, w, h int, r rune) {
|
func fill(x, y, w, h int, n ColorPair, r rune) {
|
||||||
for ly := 0; ly <= h; ly++ {
|
for ly := 0; ly <= h; ly++ {
|
||||||
for lx := 0; lx <= w; lx++ {
|
for lx := 0; lx <= w; lx++ {
|
||||||
_screen.SetContent(x+lx, y+ly, r, nil, ColDefault.style())
|
_screen.SetContent(x+lx, y+ly, r, nil, n.style())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Erase() {
|
func (w *TcellWindow) Erase() {
|
||||||
// TODO
|
w.drawBorder(false)
|
||||||
fill(w.left, w.top, w.width, w.height, ' ')
|
fill(w.left-1, w.top, w.width+1, w.height-1, w.normal, ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) EraseMaybe() bool {
|
||||||
|
w.Erase()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Enclose(y int, x int) bool {
|
func (w *TcellWindow) Enclose(y int, x int) bool {
|
||||||
@@ -429,124 +574,154 @@ func (w *TcellWindow) Move(y int, x int) {
|
|||||||
func (w *TcellWindow) MoveAndClear(y int, x int) {
|
func (w *TcellWindow) MoveAndClear(y int, x int) {
|
||||||
w.Move(y, x)
|
w.Move(y, x)
|
||||||
for i := w.lastX; i < w.width; i++ {
|
for i := w.lastX; i < w.width; i++ {
|
||||||
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, ColDefault.style())
|
_screen.SetContent(i+w.left, w.lastY+w.top, rune(' '), nil, w.normal.style())
|
||||||
}
|
}
|
||||||
w.lastX = x
|
w.lastX = x
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Print(text string) {
|
func (w *TcellWindow) Print(text string) {
|
||||||
w.printString(text, ColDefault, 0)
|
w.printString(text, w.normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) printString(text string, pair ColorPair, a Attr) {
|
func (w *TcellWindow) printString(text string, pair ColorPair) {
|
||||||
t := text
|
|
||||||
lx := 0
|
lx := 0
|
||||||
|
a := pair.Attr()
|
||||||
|
|
||||||
var style tcell.Style
|
style := pair.style()
|
||||||
if w.color {
|
if a&AttrClear == 0 {
|
||||||
style = pair.style().
|
|
||||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
|
||||||
Underline(a&Attr(tcell.AttrUnderline) != 0)
|
|
||||||
} else {
|
|
||||||
style = ColDefault.style().
|
|
||||||
Reverse(a&Attr(tcell.AttrReverse) != 0 || pair == ColCurrent || pair == ColCurrentMatch).
|
|
||||||
Underline(a&Attr(tcell.AttrUnderline) != 0 || pair == ColMatch || pair == ColCurrentMatch)
|
|
||||||
}
|
|
||||||
style = style.
|
style = style.
|
||||||
|
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||||
|
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||||
|
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||||
|
Italic(a&Attr(tcell.AttrItalic) != 0).
|
||||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||||
Bold(a&Attr(tcell.AttrBold) != 0).
|
|
||||||
Dim(a&Attr(tcell.AttrDim) != 0)
|
Dim(a&Attr(tcell.AttrDim) != 0)
|
||||||
|
|
||||||
for {
|
|
||||||
if len(t) == 0 {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
r, size := utf8.DecodeRuneInString(t)
|
|
||||||
t = t[size:]
|
|
||||||
|
|
||||||
if r < rune(' ') { // ignore control characters
|
gr := uniseg.NewGraphemes(text)
|
||||||
|
for gr.Next() {
|
||||||
|
st := style
|
||||||
|
rs := gr.Runes()
|
||||||
|
|
||||||
|
if len(rs) == 1 {
|
||||||
|
r := rs[0]
|
||||||
|
if r == '\r' {
|
||||||
|
st = style.Dim(true)
|
||||||
|
rs[0] = '␍'
|
||||||
|
} else if r == '\n' {
|
||||||
|
st = style.Dim(true)
|
||||||
|
rs[0] = '␊'
|
||||||
|
} else if r < rune(' ') { // ignore control characters
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if r == '\n' {
|
|
||||||
w.lastY++
|
|
||||||
lx = 0
|
|
||||||
} else {
|
|
||||||
|
|
||||||
if r == '\u000D' { // skip carriage return
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var xPos = w.left + w.lastX + lx
|
var xPos = w.left + w.lastX + lx
|
||||||
var yPos = w.top + w.lastY
|
var yPos = w.top + w.lastY
|
||||||
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
if xPos < (w.left+w.width) && yPos < (w.top+w.height) {
|
||||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||||
}
|
|
||||||
lx += runewidth.RuneWidth(r)
|
|
||||||
}
|
}
|
||||||
|
lx += util.StringWidth(string(rs))
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
w.lastX += lx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) CPrint(pair ColorPair, attr Attr, text string) {
|
func (w *TcellWindow) CPrint(pair ColorPair, text string) {
|
||||||
w.printString(text, pair, attr)
|
w.printString(text, pair)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) fillString(text string, pair ColorPair, a Attr) FillReturn {
|
func (w *TcellWindow) fillString(text string, pair ColorPair) FillReturn {
|
||||||
lx := 0
|
lx := 0
|
||||||
|
a := pair.Attr()
|
||||||
|
|
||||||
var style tcell.Style
|
var style tcell.Style
|
||||||
if w.color {
|
if w.color {
|
||||||
style = pair.style()
|
style = pair.style()
|
||||||
} else {
|
} else {
|
||||||
style = ColDefault.style()
|
style = w.normal.style()
|
||||||
}
|
}
|
||||||
style = style.
|
style = style.
|
||||||
Blink(a&Attr(tcell.AttrBlink) != 0).
|
Blink(a&Attr(tcell.AttrBlink) != 0).
|
||||||
Bold(a&Attr(tcell.AttrBold) != 0).
|
Bold(a&Attr(tcell.AttrBold) != 0).
|
||||||
Dim(a&Attr(tcell.AttrDim) != 0).
|
Dim(a&Attr(tcell.AttrDim) != 0).
|
||||||
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
Reverse(a&Attr(tcell.AttrReverse) != 0).
|
||||||
Underline(a&Attr(tcell.AttrUnderline) != 0)
|
Underline(a&Attr(tcell.AttrUnderline) != 0).
|
||||||
|
StrikeThrough(a&Attr(tcell.AttrStrikeThrough) != 0).
|
||||||
|
Italic(a&Attr(tcell.AttrItalic) != 0)
|
||||||
|
|
||||||
for _, r := range text {
|
gr := uniseg.NewGraphemes(text)
|
||||||
if r == '\n' {
|
Loop:
|
||||||
|
for gr.Next() {
|
||||||
|
st := style
|
||||||
|
rs := gr.Runes()
|
||||||
|
if len(rs) == 1 {
|
||||||
|
r := rs[0]
|
||||||
|
switch r {
|
||||||
|
case '\r':
|
||||||
|
st = style.Dim(true)
|
||||||
|
rs[0] = '␍'
|
||||||
|
case '\n':
|
||||||
w.lastY++
|
w.lastY++
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
} else {
|
continue Loop
|
||||||
var xPos = w.left + w.lastX + lx
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// word wrap:
|
// word wrap:
|
||||||
|
xPos := w.left + w.lastX + lx
|
||||||
if xPos >= (w.left + w.width) {
|
if xPos >= (w.left + w.width) {
|
||||||
w.lastY++
|
w.lastY++
|
||||||
w.lastX = 0
|
w.lastX = 0
|
||||||
lx = 0
|
lx = 0
|
||||||
xPos = w.left
|
xPos = w.left
|
||||||
}
|
}
|
||||||
var yPos = w.top + w.lastY
|
|
||||||
|
|
||||||
|
yPos := w.top + w.lastY
|
||||||
if yPos >= (w.top + w.height) {
|
if yPos >= (w.top + w.height) {
|
||||||
return FillSuspend
|
return FillSuspend
|
||||||
}
|
}
|
||||||
|
|
||||||
_screen.SetContent(xPos, yPos, r, nil, style)
|
_screen.SetContent(xPos, yPos, rs[0], rs[1:], st)
|
||||||
lx += runewidth.RuneWidth(r)
|
lx += util.StringWidth(string(rs))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
w.lastX += lx
|
w.lastX += lx
|
||||||
|
if w.lastX == w.width {
|
||||||
|
w.lastY++
|
||||||
|
w.lastX = 0
|
||||||
|
return FillNextLine
|
||||||
|
}
|
||||||
|
|
||||||
return FillContinue
|
return FillContinue
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) Fill(str string) FillReturn {
|
func (w *TcellWindow) Fill(str string) FillReturn {
|
||||||
return w.fillString(str, ColDefault, 0)
|
return w.fillString(str, w.normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
func (w *TcellWindow) CFill(fg Color, bg Color, a Attr, str string) FillReturn {
|
||||||
return w.fillString(str, ColorPair{fg, bg, -1}, a)
|
if fg == colDefault {
|
||||||
|
fg = w.normal.Fg()
|
||||||
|
}
|
||||||
|
if bg == colDefault {
|
||||||
|
bg = w.normal.Bg()
|
||||||
|
}
|
||||||
|
return w.fillString(str, NewColorPair(fg, bg, a))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) DrawBorder() {
|
||||||
|
w.drawBorder(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) DrawHBorder() {
|
||||||
|
w.drawBorder(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *TcellWindow) drawBorder(onlyHorizontal bool) {
|
||||||
|
shape := w.borderStyle.shape
|
||||||
|
if shape == BorderNone {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *TcellWindow) drawBorder(around bool) {
|
|
||||||
left := w.left
|
left := w.left
|
||||||
right := left + w.width
|
right := left + w.width
|
||||||
top := w.top
|
top := w.top
|
||||||
@@ -554,25 +729,61 @@ func (w *TcellWindow) drawBorder(around bool) {
|
|||||||
|
|
||||||
var style tcell.Style
|
var style tcell.Style
|
||||||
if w.color {
|
if w.color {
|
||||||
style = ColBorder.style()
|
if w.preview {
|
||||||
|
style = ColPreviewBorder.style()
|
||||||
} else {
|
} else {
|
||||||
style = ColDefault.style()
|
style = ColBorder.style()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
style = w.normal.style()
|
||||||
}
|
}
|
||||||
|
|
||||||
for x := left; x < right; x++ {
|
hw := runewidth.RuneWidth(w.borderStyle.top)
|
||||||
_screen.SetContent(x, top, tcell.RuneHLine, nil, style)
|
switch shape {
|
||||||
_screen.SetContent(x, bot-1, tcell.RuneHLine, nil, style)
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderTop:
|
||||||
|
max := right - 2*hw
|
||||||
|
if shape == BorderHorizontal || shape == BorderTop {
|
||||||
|
max = right - hw
|
||||||
}
|
}
|
||||||
|
// tcell has an issue displaying two overlapping wide runes
|
||||||
if around {
|
// e.g. SetContent( HH )
|
||||||
|
// SetContent( TR )
|
||||||
|
// ==================
|
||||||
|
// ( HH ) => TR is ignored
|
||||||
|
for x := left; x <= max; x += hw {
|
||||||
|
_screen.SetContent(x, top, w.borderStyle.top, nil, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch shape {
|
||||||
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderHorizontal, BorderBottom:
|
||||||
|
max := right - 2*hw
|
||||||
|
if shape == BorderHorizontal || shape == BorderBottom {
|
||||||
|
max = right - hw
|
||||||
|
}
|
||||||
|
for x := left; x <= max; x += hw {
|
||||||
|
_screen.SetContent(x, bot-1, w.borderStyle.bottom, nil, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !onlyHorizontal {
|
||||||
|
switch shape {
|
||||||
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderLeft:
|
||||||
for y := top; y < bot; y++ {
|
for y := top; y < bot; y++ {
|
||||||
_screen.SetContent(left, y, tcell.RuneVLine, nil, style)
|
_screen.SetContent(left, y, w.borderStyle.left, nil, style)
|
||||||
_screen.SetContent(right-1, y, tcell.RuneVLine, nil, style)
|
}
|
||||||
}
|
}
|
||||||
|
switch shape {
|
||||||
_screen.SetContent(left, top, tcell.RuneULCorner, nil, style)
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble, BorderVertical, BorderRight:
|
||||||
_screen.SetContent(right-1, top, tcell.RuneURCorner, nil, style)
|
vw := runewidth.RuneWidth(w.borderStyle.right)
|
||||||
_screen.SetContent(left, bot-1, tcell.RuneLLCorner, nil, style)
|
for y := top; y < bot; y++ {
|
||||||
_screen.SetContent(right-1, bot-1, tcell.RuneLRCorner, nil, style)
|
_screen.SetContent(right-vw, y, w.borderStyle.right, nil, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch shape {
|
||||||
|
case BorderRounded, BorderSharp, BorderBold, BorderBlock, BorderThinBlock, BorderDouble:
|
||||||
|
_screen.SetContent(left, top, w.borderStyle.topLeft, nil, style)
|
||||||
|
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.topRight), top, w.borderStyle.topRight, nil, style)
|
||||||
|
_screen.SetContent(left, bot-1, w.borderStyle.bottomLeft, nil, style)
|
||||||
|
_screen.SetContent(right-runewidth.RuneWidth(w.borderStyle.bottomRight), bot-1, w.borderStyle.bottomRight, nil, style)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
392
src/tui/tcell_test.go
Normal file
392
src/tui/tcell_test.go
Normal file
@@ -0,0 +1,392 @@
|
|||||||
|
//go:build tcell || windows
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gdamore/tcell/v2"
|
||||||
|
"github.com/junegunn/fzf/src/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func assert(t *testing.T, context string, got interface{}, want interface{}) bool {
|
||||||
|
if got == want {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
t.Errorf("%s = (%T)%v, want (%T)%v", context, got, got, want, want)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the handling of the tcell keyboard events.
|
||||||
|
func TestGetCharEventKey(t *testing.T) {
|
||||||
|
if util.ToTty() {
|
||||||
|
// 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
|
||||||
|
// the output unreadable
|
||||||
|
// - terminal may get cleared which prevents you from seeing results of
|
||||||
|
// previous tests
|
||||||
|
// Good ways to prevent the glitches are piping the output to a pager
|
||||||
|
// or redirecting to a file. I've found `less +G` to be trouble-free.
|
||||||
|
t.Skip("Skipped because this test misbehaves in terminal, pipe to a pager or redirect output to a file to run it safely.")
|
||||||
|
} else if testing.Verbose() {
|
||||||
|
// I have observed a behaviour when this test outputted more than 8192
|
||||||
|
// bytes (32*256) into the 'less' pager, both the go's test executable
|
||||||
|
// and the pager hanged. The go's executable was blocking on printing.
|
||||||
|
// I was able to create minimal working example of that behaviour, but
|
||||||
|
// that example hanged after 12256 bytes (32*(256+127)).
|
||||||
|
t.Log("If you are piping this test to a pager and it hangs, make the pager greedy for input, e.g. 'less +G'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !HasFullscreenRenderer() {
|
||||||
|
t.Skip("Can't test FullscreenRenderer.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// construct test cases
|
||||||
|
type giveKey struct {
|
||||||
|
Type tcell.Key
|
||||||
|
Char rune
|
||||||
|
Mods tcell.ModMask
|
||||||
|
}
|
||||||
|
type wantKey = Event
|
||||||
|
type testCase struct {
|
||||||
|
giveKey
|
||||||
|
wantKey
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Some test cases are marked "fabricated". It means that giveKey value
|
||||||
|
is valid, but it is not what you get when you press the keys. For
|
||||||
|
example Ctrl+C will NOT give you tcell.KeyCtrlC, but tcell.KeyETX
|
||||||
|
(End-Of-Text character, causing SIGINT).
|
||||||
|
I was trying to accompany the fabricated test cases with real ones.
|
||||||
|
|
||||||
|
Some test cases are marked "unhandled". It means that giveKey.Type
|
||||||
|
is not present in tcell.go source code. It can still be handled via
|
||||||
|
implicit or explicit alias.
|
||||||
|
|
||||||
|
If not said otherwise, test cases are for US keyboard.
|
||||||
|
|
||||||
|
(tabstop=44)
|
||||||
|
*/
|
||||||
|
tests := []testCase{
|
||||||
|
|
||||||
|
// section 1: Ctrl+(Alt)+[a-z]
|
||||||
|
{giveKey{tcell.KeyCtrlA, rune(tcell.KeyCtrlA), tcell.ModCtrl}, wantKey{CtrlA, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyCtrlC, rune(tcell.KeyCtrlC), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyETX, rune(tcell.KeyETX), tcell.ModCtrl}, wantKey{CtrlC, 0, nil}}, // this is SIGINT (Ctrl+C)
|
||||||
|
{giveKey{tcell.KeyCtrlZ, rune(tcell.KeyCtrlZ), tcell.ModCtrl}, wantKey{CtrlZ, 0, nil}}, // fabricated
|
||||||
|
// KeyTab is alias for KeyTAB
|
||||||
|
{giveKey{tcell.KeyCtrlI, rune(tcell.KeyCtrlI), tcell.ModCtrl}, wantKey{Tab, 0, nil}}, // fabricated
|
||||||
|
{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
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// section 2: Ctrl+[ \]_]
|
||||||
|
{giveKey{tcell.KeyCtrlSpace, rune(tcell.KeyCtrlSpace), tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyNUL, rune(tcell.KeyNUL), tcell.ModNone}, wantKey{CtrlSpace, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyRune, ' ', tcell.ModCtrl}, wantKey{CtrlSpace, 0, nil}}, // actual Ctrl+' '
|
||||||
|
{giveKey{tcell.KeyCtrlBackslash, rune(tcell.KeyCtrlBackslash), tcell.ModCtrl}, wantKey{CtrlBackSlash, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyCtrlRightSq, rune(tcell.KeyCtrlRightSq), tcell.ModCtrl}, wantKey{CtrlRightBracket, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyCtrlCarat, rune(tcell.KeyCtrlCarat), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyRS, rune(tcell.KeyRS), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlCaret, 0, nil}}, // actual Ctrl+Shift+6 (i.e. Ctrl+^) keystroke
|
||||||
|
{giveKey{tcell.KeyCtrlUnderscore, rune(tcell.KeyCtrlUnderscore), tcell.ModShift | tcell.ModCtrl}, wantKey{CtrlSlash, 0, nil}},
|
||||||
|
|
||||||
|
// section 3: (Alt)+Backspace2
|
||||||
|
// 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.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.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
|
||||||
|
{giveKey{tcell.KeyCtrlH, rune(tcell.KeyCtrlH), tcell.ModCtrl | tcell.ModAlt | tcell.ModShift}, wantKey{CtrlAlt, 'h', nil}}, // fabricated "Ctrl+Shift+Alt+H" keystroke
|
||||||
|
|
||||||
|
// section 4: (Alt+Shift)+Key(Up|Down|Left|Right)
|
||||||
|
{giveKey{tcell.KeyUp, 0, tcell.ModNone}, wantKey{Up, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyDown, 0, tcell.ModAlt}, wantKey{AltDown, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyLeft, 0, tcell.ModShift}, wantKey{SLeft, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyRight, 0, tcell.ModShift | tcell.ModAlt}, wantKey{AltSRight, 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
|
||||||
|
{giveKey{tcell.KeyDownRight, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyCenter, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
// 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}},
|
||||||
|
// 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
|
||||||
|
{giveKey{tcell.KeyRune, 'a', tcell.ModAlt}, wantKey{Alt, 'a', nil}},
|
||||||
|
{giveKey{tcell.KeyRune, 'A', tcell.ModAlt}, wantKey{Alt, 'A', nil}},
|
||||||
|
{giveKey{tcell.KeyRune, '`', tcell.ModAlt}, wantKey{Alt, '`', nil}},
|
||||||
|
/*
|
||||||
|
"Input method" in Windows Language options:
|
||||||
|
US: "US Keyboard" does not generate any characters (and thus any events) in Ctrl+Alt+[a-z] range
|
||||||
|
CS: "Czech keyboard"
|
||||||
|
DE: "German keyboard"
|
||||||
|
|
||||||
|
Note that right Alt is not just `tcell.ModAlt` on foreign language keyboards, but it is the AltGr `tcell.ModCtrl|tcell.ModAlt`.
|
||||||
|
*/
|
||||||
|
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // CS: Ctrl+Alt+b = "{" // Note that this does not interfere with CtrlB, since the "b" is replaced with "{" on OS level
|
||||||
|
{giveKey{tcell.KeyRune, '$', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '$', nil}}, // CS: Ctrl+Alt+ů = "$"
|
||||||
|
{giveKey{tcell.KeyRune, '~', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '~', nil}}, // CS: Ctrl+Alt++ = "~"
|
||||||
|
{giveKey{tcell.KeyRune, '`', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '`', nil}}, // CS: Ctrl+Alt+ý,Space = "`" // this is dead key, space is required to emit the char
|
||||||
|
|
||||||
|
{giveKey{tcell.KeyRune, '{', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '{', nil}}, // DE: Ctrl+Alt+7 = "{"
|
||||||
|
{giveKey{tcell.KeyRune, '@', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, '@', nil}}, // DE: Ctrl+Alt+q = "@"
|
||||||
|
{giveKey{tcell.KeyRune, 'µ', tcell.ModCtrl | tcell.ModAlt}, wantKey{Rune, 'µ', nil}}, // DE: Ctrl+Alt+m = "µ"
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// section 8: Invalid
|
||||||
|
{giveKey{tcell.KeyRune, 'a', tcell.ModMeta}, wantKey{Rune, 'a', nil}}, // fabricated
|
||||||
|
{giveKey{tcell.KeyF24, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}},
|
||||||
|
{giveKey{tcell.KeyHelp, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyExit, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyClear, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled, actual keystroke Numpad_5 with Numlock OFF
|
||||||
|
{giveKey{tcell.KeyCancel, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyPrint, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // fabricated, unhandled
|
||||||
|
{giveKey{tcell.KeyPause, 0, tcell.ModNone}, wantKey{Invalid, 0, nil}}, // unhandled
|
||||||
|
|
||||||
|
}
|
||||||
|
r := NewFullscreenRenderer(&ColorTheme{}, false, false)
|
||||||
|
r.Init()
|
||||||
|
|
||||||
|
// run and evaluate the tests
|
||||||
|
for _, test := range tests {
|
||||||
|
// generate key event
|
||||||
|
giveEvent := tcell.NewEventKey(test.giveKey.Type, test.giveKey.Char, test.giveKey.Mods)
|
||||||
|
_screen.PostEventWait(giveEvent)
|
||||||
|
t.Logf("giveEvent = %T{key: %v, ch: %q (%[3]v), mod: %#04b}\n", giveEvent, giveEvent.Key(), giveEvent.Rune(), giveEvent.Modifiers())
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
t.Logf("Resize swallowed")
|
||||||
|
gotEvent = r.GetChar()
|
||||||
|
}
|
||||||
|
t.Logf("wantEvent = %T{Type: %v, Char: %q (%[3]v)}\n", test.wantKey, test.wantKey.Type, test.wantKey.Char)
|
||||||
|
t.Logf("gotEvent = %T{Type: %v, Char: %q (%[3]v)}\n", gotEvent, gotEvent.Type, gotEvent.Char)
|
||||||
|
|
||||||
|
assert(t, "r.GetChar().Type", gotEvent.Type, test.wantKey.Type)
|
||||||
|
assert(t, "r.GetChar().Char", gotEvent.Char, test.wantKey.Char)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Quick reference
|
||||||
|
---------------
|
||||||
|
|
||||||
|
(tabstop=18)
|
||||||
|
(this is not mapping table, it merely puts multiple constants ranges in one table)
|
||||||
|
|
||||||
|
¹) the two columns are each other implicit alias
|
||||||
|
²) explicit aliases here
|
||||||
|
|
||||||
|
%v section # tcell ctrl key¹ tcell ctrl char¹ tcell alias² tui constants tcell named keys tcell mods
|
||||||
|
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
|
||||||
|
0 2 KeyCtrlSpace KeyNUL = ^@ Rune ModNone
|
||||||
|
1 1 KeyCtrlA KeySOH = ^A CtrlA ModShift
|
||||||
|
2 1 KeyCtrlB KeySTX = ^B CtrlB ModCtrl
|
||||||
|
3 1 KeyCtrlC KeyETX = ^C CtrlC
|
||||||
|
4 1 KeyCtrlD KeyEOT = ^D CtrlD ModAlt
|
||||||
|
5 1 KeyCtrlE KeyENQ = ^E CtrlE
|
||||||
|
6 1 KeyCtrlF KeyACK = ^F CtrlF
|
||||||
|
7 1 KeyCtrlG KeyBEL = ^G CtrlG
|
||||||
|
8 1 KeyCtrlH KeyBS = ^H KeyBackspace CtrlH ModMeta
|
||||||
|
9 1 KeyCtrlI KeyTAB = ^I KeyTab Tab
|
||||||
|
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
|
||||||
|
14 1 KeyCtrlN KeySO = ^N CtrlN
|
||||||
|
15 1 KeyCtrlO KeySI = ^O CtrlO
|
||||||
|
16 1 KeyCtrlP KeyDLE = ^P CtrlP
|
||||||
|
17 1 KeyCtrlQ KeyDC1 = ^Q CtrlQ
|
||||||
|
18 1 KeyCtrlR KeyDC2 = ^R CtrlR
|
||||||
|
19 1 KeyCtrlS KeyDC3 = ^S CtrlS
|
||||||
|
20 1 KeyCtrlT KeyDC4 = ^T CtrlT
|
||||||
|
21 1 KeyCtrlU KeyNAK = ^U CtrlU
|
||||||
|
22 1 KeyCtrlV KeySYN = ^V CtrlV
|
||||||
|
23 1 KeyCtrlW KeyETB = ^W CtrlW
|
||||||
|
24 1 KeyCtrlX KeyCAN = ^X CtrlX
|
||||||
|
25 1 KeyCtrlY KeyEM = ^Y CtrlY
|
||||||
|
26 1 KeyCtrlZ KeySUB = ^Z CtrlZ
|
||||||
|
27 7 KeyCtrlLeftSq KeyESC = ^[ KeyEsc, KeyEscape ESC
|
||||||
|
28 2 KeyCtrlBackslash KeyFS = ^\ CtrlSpace
|
||||||
|
29 2 KeyCtrlRightSq KeyGS = ^] CtrlBackSlash
|
||||||
|
30 2 KeyCtrlCarat KeyRS = ^^ CtrlRightBracket
|
||||||
|
31 2 KeyCtrlUnderscore KeyUS = ^_ CtrlCaret
|
||||||
|
32 CtrlSlash
|
||||||
|
33 Invalid
|
||||||
|
34 Resize
|
||||||
|
35 Mouse
|
||||||
|
36 DoubleClick
|
||||||
|
37 LeftClick
|
||||||
|
38 RightClick
|
||||||
|
39 BTab
|
||||||
|
40 BSpace
|
||||||
|
41 Del
|
||||||
|
42 PgUp
|
||||||
|
43 PgDn
|
||||||
|
44 Up
|
||||||
|
45 Down
|
||||||
|
46 Left
|
||||||
|
47 Right
|
||||||
|
48 Home
|
||||||
|
49 End
|
||||||
|
50 Insert
|
||||||
|
51 SUp
|
||||||
|
52 SDown
|
||||||
|
53 SLeft
|
||||||
|
54 SRight
|
||||||
|
55 F1
|
||||||
|
56 F2
|
||||||
|
57 F3
|
||||||
|
58 F4
|
||||||
|
59 F5
|
||||||
|
60 F6
|
||||||
|
61 F7
|
||||||
|
62 F8
|
||||||
|
63 F9
|
||||||
|
64 F10
|
||||||
|
65 F11
|
||||||
|
66 F12
|
||||||
|
67 Change
|
||||||
|
68 BackwardEOF
|
||||||
|
69 AltBS
|
||||||
|
70 AltUp
|
||||||
|
71 AltDown
|
||||||
|
72 AltLeft
|
||||||
|
73 AltRight
|
||||||
|
74 AltSUp
|
||||||
|
75 AltSDown
|
||||||
|
76 AltSLeft
|
||||||
|
77 AltSRight
|
||||||
|
78 Alt
|
||||||
|
79 CtrlAlt
|
||||||
|
..
|
||||||
|
127 3 KeyDEL KeyBackspace2
|
||||||
|
..
|
||||||
|
256 6 KeyRune
|
||||||
|
257 4 KeyUp
|
||||||
|
258 4 KeyDown
|
||||||
|
259 4 KeyRight
|
||||||
|
260 4 KeyLeft
|
||||||
|
261 8 KeyUpLeft
|
||||||
|
262 8 KeyUpRight
|
||||||
|
263 8 KeyDownLeft
|
||||||
|
264 8 KeyDownRight
|
||||||
|
265 8 KeyCenter
|
||||||
|
266 5 KeyPgUp
|
||||||
|
267 5 KeyPgDn
|
||||||
|
268 5 KeyHome
|
||||||
|
269 5 KeyEnd
|
||||||
|
270 5 KeyInsert
|
||||||
|
271 5 KeyDelete
|
||||||
|
272 8 KeyHelp
|
||||||
|
273 8 KeyExit
|
||||||
|
274 8 KeyClear
|
||||||
|
275 8 KeyCancel
|
||||||
|
276 8 KeyPrint
|
||||||
|
277 8 KeyPause
|
||||||
|
278 5 KeyBacktab
|
||||||
|
279 5 KeyF1
|
||||||
|
280 5 KeyF2
|
||||||
|
281 5 KeyF3
|
||||||
|
282 5 KeyF4
|
||||||
|
283 5 KeyF5
|
||||||
|
284 5 KeyF6
|
||||||
|
285 5 KeyF7
|
||||||
|
286 5 KeyF8
|
||||||
|
287 5 KeyF9
|
||||||
|
288 5 KeyF10
|
||||||
|
289 5 KeyF11
|
||||||
|
290 5 KeyF12
|
||||||
|
291 8 KeyF13
|
||||||
|
292 8 KeyF14
|
||||||
|
293 8 KeyF15
|
||||||
|
294 8 KeyF16
|
||||||
|
295 8 KeyF17
|
||||||
|
296 8 KeyF18
|
||||||
|
297 8 KeyF19
|
||||||
|
298 8 KeyF20
|
||||||
|
299 8 KeyF21
|
||||||
|
300 8 KeyF22
|
||||||
|
301 8 KeyF23
|
||||||
|
302 8 KeyF24
|
||||||
|
303 8 KeyF25
|
||||||
|
304 8 KeyF26
|
||||||
|
305 8 KeyF27
|
||||||
|
306 8 KeyF28
|
||||||
|
307 8 KeyF29
|
||||||
|
308 8 KeyF30
|
||||||
|
309 8 KeyF31
|
||||||
|
310 8 KeyF32
|
||||||
|
311 8 KeyF33
|
||||||
|
312 8 KeyF34
|
||||||
|
313 8 KeyF35
|
||||||
|
314 8 KeyF36
|
||||||
|
315 8 KeyF37
|
||||||
|
316 8 KeyF38
|
||||||
|
317 8 KeyF39
|
||||||
|
318 8 KeyF40
|
||||||
|
319 8 KeyF41
|
||||||
|
320 8 KeyF42
|
||||||
|
321 8 KeyF43
|
||||||
|
322 8 KeyF44
|
||||||
|
323 8 KeyF45
|
||||||
|
324 8 KeyF46
|
||||||
|
325 8 KeyF47
|
||||||
|
326 8 KeyF48
|
||||||
|
327 8 KeyF49
|
||||||
|
328 8 KeyF50
|
||||||
|
329 8 KeyF51
|
||||||
|
330 8 KeyF52
|
||||||
|
331 8 KeyF53
|
||||||
|
332 8 KeyF54
|
||||||
|
333 8 KeyF55
|
||||||
|
334 8 KeyF56
|
||||||
|
335 8 KeyF57
|
||||||
|
336 8 KeyF58
|
||||||
|
337 8 KeyF59
|
||||||
|
338 8 KeyF60
|
||||||
|
339 8 KeyF61
|
||||||
|
340 8 KeyF62
|
||||||
|
341 8 KeyF63
|
||||||
|
342 8 KeyF64
|
||||||
|
-- --------- -------------- --------------- ----------- ------------- ---------------- ----------
|
||||||
|
%v section # tcell ctrl key tcell ctrl char tcell alias tui constants tcell named keys tcell mods
|
||||||
|
*/
|
||||||
50
src/tui/ttyname_unix.go
Normal file
50
src/tui/ttyname_unix.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
var devPrefixes = [...]string{"/dev/pts/", "/dev/"}
|
||||||
|
|
||||||
|
func ttyname() string {
|
||||||
|
var stderr syscall.Stat_t
|
||||||
|
if syscall.Fstat(2, &stderr) != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, prefix := range devPrefixes {
|
||||||
|
files, err := os.ReadDir(prefix)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
info, err := file.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if stat, ok := info.Sys().(*syscall.Stat_t); ok && stat.Rdev == stderr.Rdev {
|
||||||
|
return prefix + file.Name()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
14
src/tui/ttyname_windows.go
Normal file
14
src/tui/ttyname_windows.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package tui
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func ttyname() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// TtyIn on Windows returns os.Stdin
|
||||||
|
func TtyIn() *os.File {
|
||||||
|
return os.Stdin
|
||||||
|
}
|
||||||
693
src/tui/tui.go
693
src/tui/tui.go
@@ -8,8 +8,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Types of user action
|
// Types of user action
|
||||||
|
type EventType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Rune = iota
|
Rune EventType = iota
|
||||||
|
|
||||||
CtrlA
|
CtrlA
|
||||||
CtrlB
|
CtrlB
|
||||||
@@ -39,11 +41,28 @@ const (
|
|||||||
CtrlZ
|
CtrlZ
|
||||||
ESC
|
ESC
|
||||||
CtrlSpace
|
CtrlSpace
|
||||||
|
CtrlDelete
|
||||||
|
|
||||||
|
// https://apple.stackexchange.com/questions/24261/how-do-i-send-c-that-is-control-slash-to-the-terminal
|
||||||
|
CtrlBackSlash
|
||||||
|
CtrlRightBracket
|
||||||
|
CtrlCaret
|
||||||
|
CtrlSlash
|
||||||
|
|
||||||
Invalid
|
Invalid
|
||||||
Resize
|
Resize
|
||||||
Mouse
|
Mouse
|
||||||
DoubleClick
|
DoubleClick
|
||||||
|
LeftClick
|
||||||
|
RightClick
|
||||||
|
SLeftClick
|
||||||
|
SRightClick
|
||||||
|
ScrollUp
|
||||||
|
ScrollDown
|
||||||
|
SScrollUp
|
||||||
|
SScrollDown
|
||||||
|
PreviewScrollUp
|
||||||
|
PreviewScrollDown
|
||||||
|
|
||||||
BTab
|
BTab
|
||||||
BSpace
|
BSpace
|
||||||
@@ -58,9 +77,13 @@ const (
|
|||||||
Right
|
Right
|
||||||
Home
|
Home
|
||||||
End
|
End
|
||||||
|
Insert
|
||||||
|
|
||||||
|
SUp
|
||||||
|
SDown
|
||||||
SLeft
|
SLeft
|
||||||
SRight
|
SRight
|
||||||
|
SDelete
|
||||||
|
|
||||||
F1
|
F1
|
||||||
F2
|
F2
|
||||||
@@ -76,25 +99,57 @@ const (
|
|||||||
F12
|
F12
|
||||||
|
|
||||||
Change
|
Change
|
||||||
|
BackwardEOF
|
||||||
|
Start
|
||||||
|
Load
|
||||||
|
Focus
|
||||||
|
One
|
||||||
|
Zero
|
||||||
|
|
||||||
AltSpace
|
|
||||||
AltSlash
|
|
||||||
AltBS
|
AltBS
|
||||||
|
|
||||||
Alt0
|
AltUp
|
||||||
|
AltDown
|
||||||
|
AltLeft
|
||||||
|
AltRight
|
||||||
|
|
||||||
|
AltSUp
|
||||||
|
AltSDown
|
||||||
|
AltSLeft
|
||||||
|
AltSRight
|
||||||
|
|
||||||
|
Alt
|
||||||
|
CtrlAlt
|
||||||
)
|
)
|
||||||
|
|
||||||
const ( // Reset iota
|
func (t EventType) AsEvent() Event {
|
||||||
AltA = Alt0 + 'a' - '0' + iota
|
return Event{t, 0, nil}
|
||||||
AltB
|
}
|
||||||
AltC
|
|
||||||
AltD
|
func (t EventType) Int() int {
|
||||||
AltE
|
return int(t)
|
||||||
AltF
|
}
|
||||||
AltZ = AltA + 'z' - 'a'
|
|
||||||
CtrlAltA = AltZ + 1
|
func (t EventType) Byte() byte {
|
||||||
CtrlAltM = CtrlAltA + 'm' - 'a'
|
return byte(t)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
func (e Event) Comparable() Event {
|
||||||
|
// Ignore MouseEvent pointer
|
||||||
|
return Event{e.Type, e.Char, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Key(r rune) Event {
|
||||||
|
return Event{Rune, r, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AltKey(r rune) Event {
|
||||||
|
return Event{Alt, r, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CtrlAltKey(r rune) Event {
|
||||||
|
return Event{CtrlAlt, r, nil}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
doubleClickDuration = 500 * time.Millisecond
|
doubleClickDuration = 500 * time.Millisecond
|
||||||
@@ -102,13 +157,26 @@ const (
|
|||||||
|
|
||||||
type Color int32
|
type Color int32
|
||||||
|
|
||||||
|
func (c Color) IsDefault() bool {
|
||||||
|
return c == colDefault
|
||||||
|
}
|
||||||
|
|
||||||
func (c Color) is24() bool {
|
func (c Color) is24() bool {
|
||||||
return c > 0 && (c&(1<<24)) > 0
|
return c > 0 && (c&(1<<24)) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ColorAttr struct {
|
||||||
|
Color Color
|
||||||
|
Attr Attr
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewColorAttr() ColorAttr {
|
||||||
|
return ColorAttr{Color: colUndefined, Attr: AttrUndefined}
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
colUndefined Color = -2
|
colUndefined Color = -2
|
||||||
colDefault = -1
|
colDefault Color = -1
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -133,7 +201,7 @@ const (
|
|||||||
type ColorPair struct {
|
type ColorPair struct {
|
||||||
fg Color
|
fg Color
|
||||||
bg Color
|
bg Color
|
||||||
id int16
|
attr Attr
|
||||||
}
|
}
|
||||||
|
|
||||||
func HexToColor(rrggbb string) Color {
|
func HexToColor(rrggbb string) Color {
|
||||||
@@ -143,8 +211,8 @@ func HexToColor(rrggbb string) Color {
|
|||||||
return Color((1 << 24) + (r << 16) + (g << 8) + b)
|
return Color((1 << 24) + (r << 16) + (g << 8) + b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewColorPair(fg Color, bg Color) ColorPair {
|
func NewColorPair(fg Color, bg Color, attr Attr) ColorPair {
|
||||||
return ColorPair{fg, bg, -1}
|
return ColorPair{fg, bg, attr}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ColorPair) Fg() Color {
|
func (p ColorPair) Fg() Color {
|
||||||
@@ -155,74 +223,284 @@ func (p ColorPair) Bg() Color {
|
|||||||
return p.bg
|
return p.bg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ColorPair) key() int {
|
func (p ColorPair) Attr() Attr {
|
||||||
return (int(p.Fg()) << 8) + int(p.Bg())
|
return p.attr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p ColorPair) is24() bool {
|
func (p ColorPair) HasBg() bool {
|
||||||
return p.Fg().is24() || p.Bg().is24()
|
return p.attr&Reverse == 0 && p.bg != colDefault ||
|
||||||
|
p.attr&Reverse > 0 && p.fg != colDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) merge(other ColorPair, except Color) ColorPair {
|
||||||
|
dup := p
|
||||||
|
dup.attr = dup.attr.Merge(other.attr)
|
||||||
|
if other.fg != except {
|
||||||
|
dup.fg = other.fg
|
||||||
|
}
|
||||||
|
if other.bg != except {
|
||||||
|
dup.bg = other.bg
|
||||||
|
}
|
||||||
|
return dup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) WithAttr(attr Attr) ColorPair {
|
||||||
|
dup := p
|
||||||
|
dup.attr = dup.attr.Merge(attr)
|
||||||
|
return dup
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) MergeAttr(other ColorPair) ColorPair {
|
||||||
|
return p.WithAttr(other.attr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) Merge(other ColorPair) ColorPair {
|
||||||
|
return p.merge(other, colUndefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p ColorPair) MergeNonDefault(other ColorPair) ColorPair {
|
||||||
|
return p.merge(other, colDefault)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ColorTheme struct {
|
type ColorTheme struct {
|
||||||
Fg Color
|
Colored bool
|
||||||
Bg Color
|
Input ColorAttr
|
||||||
DarkBg Color
|
Disabled ColorAttr
|
||||||
Prompt Color
|
Fg ColorAttr
|
||||||
Match Color
|
Bg ColorAttr
|
||||||
Current Color
|
PreviewFg ColorAttr
|
||||||
CurrentMatch Color
|
PreviewBg ColorAttr
|
||||||
Spinner Color
|
DarkBg ColorAttr
|
||||||
Info Color
|
Gutter ColorAttr
|
||||||
Cursor Color
|
Prompt ColorAttr
|
||||||
Selected Color
|
Match ColorAttr
|
||||||
Header Color
|
Current ColorAttr
|
||||||
Border Color
|
CurrentMatch ColorAttr
|
||||||
}
|
Spinner ColorAttr
|
||||||
|
Info ColorAttr
|
||||||
func (t *ColorTheme) HasBg() bool {
|
Cursor ColorAttr
|
||||||
return t.Bg != colDefault
|
Selected ColorAttr
|
||||||
|
Header ColorAttr
|
||||||
|
Separator ColorAttr
|
||||||
|
Scrollbar ColorAttr
|
||||||
|
Border ColorAttr
|
||||||
|
PreviewBorder ColorAttr
|
||||||
|
PreviewScrollbar ColorAttr
|
||||||
|
BorderLabel ColorAttr
|
||||||
|
PreviewLabel ColorAttr
|
||||||
}
|
}
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
Type int
|
Type EventType
|
||||||
Char rune
|
Char rune
|
||||||
MouseEvent *MouseEvent
|
MouseEvent *MouseEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e Event) Is(types ...EventType) bool {
|
||||||
|
for _, t := range types {
|
||||||
|
if e.Type == t {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type MouseEvent struct {
|
type MouseEvent struct {
|
||||||
Y int
|
Y int
|
||||||
X int
|
X int
|
||||||
S int
|
S int
|
||||||
|
Left bool
|
||||||
Down bool
|
Down bool
|
||||||
Double bool
|
Double bool
|
||||||
Mod bool
|
Mod bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type BorderStyle int
|
type BorderShape int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
BorderNone BorderStyle = iota
|
BorderNone BorderShape = iota
|
||||||
BorderAround
|
BorderRounded
|
||||||
|
BorderSharp
|
||||||
|
BorderBold
|
||||||
|
BorderBlock
|
||||||
|
BorderThinBlock
|
||||||
|
BorderDouble
|
||||||
BorderHorizontal
|
BorderHorizontal
|
||||||
|
BorderVertical
|
||||||
|
BorderTop
|
||||||
|
BorderBottom
|
||||||
|
BorderLeft
|
||||||
|
BorderRight
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (s BorderShape) HasRight() bool {
|
||||||
|
switch s {
|
||||||
|
case BorderNone, BorderLeft, BorderTop, BorderBottom, BorderHorizontal: // No right
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s BorderShape) HasTop() bool {
|
||||||
|
switch s {
|
||||||
|
case BorderNone, BorderLeft, BorderRight, BorderBottom, BorderVertical: // No top
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
type BorderStyle struct {
|
||||||
|
shape BorderShape
|
||||||
|
top rune
|
||||||
|
bottom rune
|
||||||
|
left rune
|
||||||
|
right rune
|
||||||
|
topLeft rune
|
||||||
|
topRight rune
|
||||||
|
bottomLeft rune
|
||||||
|
bottomRight rune
|
||||||
|
}
|
||||||
|
|
||||||
|
type BorderCharacter int
|
||||||
|
|
||||||
|
func MakeBorderStyle(shape BorderShape, unicode bool) BorderStyle {
|
||||||
|
if !unicode {
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '-',
|
||||||
|
bottom: '-',
|
||||||
|
left: '|',
|
||||||
|
right: '|',
|
||||||
|
topLeft: '+',
|
||||||
|
topRight: '+',
|
||||||
|
bottomLeft: '+',
|
||||||
|
bottomRight: '+',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch shape {
|
||||||
|
case BorderSharp:
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '─',
|
||||||
|
bottom: '─',
|
||||||
|
left: '│',
|
||||||
|
right: '│',
|
||||||
|
topLeft: '┌',
|
||||||
|
topRight: '┐',
|
||||||
|
bottomLeft: '└',
|
||||||
|
bottomRight: '┘',
|
||||||
|
}
|
||||||
|
case BorderBold:
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '━',
|
||||||
|
bottom: '━',
|
||||||
|
left: '┃',
|
||||||
|
right: '┃',
|
||||||
|
topLeft: '┏',
|
||||||
|
topRight: '┓',
|
||||||
|
bottomLeft: '┗',
|
||||||
|
bottomRight: '┛',
|
||||||
|
}
|
||||||
|
case BorderBlock:
|
||||||
|
// ▛▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▜
|
||||||
|
// ▌ ▐
|
||||||
|
// ▙▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▟
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '▀',
|
||||||
|
bottom: '▄',
|
||||||
|
left: '▌',
|
||||||
|
right: '▐',
|
||||||
|
topLeft: '▛',
|
||||||
|
topRight: '▜',
|
||||||
|
bottomLeft: '▙',
|
||||||
|
bottomRight: '▟',
|
||||||
|
}
|
||||||
|
|
||||||
|
case BorderThinBlock:
|
||||||
|
// 🭽▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔▔🭾
|
||||||
|
// ▏ ▕
|
||||||
|
// 🭼▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁🭿
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '▔',
|
||||||
|
bottom: '▁',
|
||||||
|
left: '▏',
|
||||||
|
right: '▕',
|
||||||
|
topLeft: '🭽',
|
||||||
|
topRight: '🭾',
|
||||||
|
bottomLeft: '🭼',
|
||||||
|
bottomRight: '🭿',
|
||||||
|
}
|
||||||
|
|
||||||
|
case BorderDouble:
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '═',
|
||||||
|
bottom: '═',
|
||||||
|
left: '║',
|
||||||
|
right: '║',
|
||||||
|
topLeft: '╔',
|
||||||
|
topRight: '╗',
|
||||||
|
bottomLeft: '╚',
|
||||||
|
bottomRight: '╝',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return BorderStyle{
|
||||||
|
shape: shape,
|
||||||
|
top: '─',
|
||||||
|
bottom: '─',
|
||||||
|
left: '│',
|
||||||
|
right: '│',
|
||||||
|
topLeft: '╭',
|
||||||
|
topRight: '╮',
|
||||||
|
bottomLeft: '╰',
|
||||||
|
bottomRight: '╯',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeTransparentBorder() BorderStyle {
|
||||||
|
return BorderStyle{
|
||||||
|
shape: BorderRounded,
|
||||||
|
top: ' ',
|
||||||
|
bottom: ' ',
|
||||||
|
left: ' ',
|
||||||
|
right: ' ',
|
||||||
|
topLeft: ' ',
|
||||||
|
topRight: ' ',
|
||||||
|
bottomLeft: ' ',
|
||||||
|
bottomRight: ' '}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TermSize struct {
|
||||||
|
Lines int
|
||||||
|
Columns int
|
||||||
|
PxWidth int
|
||||||
|
PxHeight int
|
||||||
|
}
|
||||||
|
|
||||||
type Renderer interface {
|
type Renderer interface {
|
||||||
Init()
|
Init()
|
||||||
|
Resize(maxHeightFunc func(int) int)
|
||||||
Pause(clear bool)
|
Pause(clear bool)
|
||||||
Resume(clear bool)
|
Resume(clear bool, sigcont bool)
|
||||||
Clear()
|
Clear()
|
||||||
RefreshWindows(windows []Window)
|
RefreshWindows(windows []Window)
|
||||||
Refresh()
|
Refresh()
|
||||||
Close()
|
Close()
|
||||||
|
PassThrough(string)
|
||||||
|
NeedScrollbarRedraw() bool
|
||||||
|
|
||||||
GetChar() Event
|
GetChar() Event
|
||||||
|
|
||||||
|
Top() int
|
||||||
MaxX() int
|
MaxX() int
|
||||||
MaxY() int
|
MaxY() int
|
||||||
DoesAutoWrap() bool
|
|
||||||
IsOptimized() bool
|
|
||||||
|
|
||||||
NewWindow(top int, left int, width int, height int, borderStyle BorderStyle) Window
|
Size() TermSize
|
||||||
|
|
||||||
|
NewWindow(top int, left int, width int, height int, preview bool, borderStyle BorderStyle) Window
|
||||||
}
|
}
|
||||||
|
|
||||||
type Window interface {
|
type Window interface {
|
||||||
@@ -231,20 +509,24 @@ type Window interface {
|
|||||||
Width() int
|
Width() int
|
||||||
Height() int
|
Height() int
|
||||||
|
|
||||||
|
DrawBorder()
|
||||||
|
DrawHBorder()
|
||||||
Refresh()
|
Refresh()
|
||||||
FinishFill()
|
FinishFill()
|
||||||
Close()
|
Close()
|
||||||
|
|
||||||
X() int
|
X() int
|
||||||
|
Y() int
|
||||||
Enclose(y int, x int) bool
|
Enclose(y int, x int) bool
|
||||||
|
|
||||||
Move(y int, x int)
|
Move(y int, x int)
|
||||||
MoveAndClear(y int, x int)
|
MoveAndClear(y int, x int)
|
||||||
Print(text string)
|
Print(text string)
|
||||||
CPrint(color ColorPair, attr Attr, text string)
|
CPrint(color ColorPair, text string)
|
||||||
Fill(text string) FillReturn
|
Fill(text string) FillReturn
|
||||||
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
CFill(fg Color, bg Color, attr Attr, text string) FillReturn
|
||||||
Erase()
|
Erase()
|
||||||
|
EraseMaybe() bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type FullscreenRenderer struct {
|
type FullscreenRenderer struct {
|
||||||
@@ -252,7 +534,7 @@ type FullscreenRenderer struct {
|
|||||||
mouse bool
|
mouse bool
|
||||||
forceBlack bool
|
forceBlack bool
|
||||||
prevDownTime time.Time
|
prevDownTime time.Time
|
||||||
clickY []int
|
clicks [][2]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Renderer {
|
||||||
@@ -261,7 +543,7 @@ func NewFullscreenRenderer(theme *ColorTheme, forceBlack bool, mouse bool) Rende
|
|||||||
mouse: mouse,
|
mouse: mouse,
|
||||||
forceBlack: forceBlack,
|
forceBlack: forceBlack,
|
||||||
prevDownTime: time.Unix(0, 0),
|
prevDownTime: time.Unix(0, 0),
|
||||||
clickY: []int{}}
|
clicks: [][2]int{}}
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,36 +552,92 @@ var (
|
|||||||
Dark256 *ColorTheme
|
Dark256 *ColorTheme
|
||||||
Light256 *ColorTheme
|
Light256 *ColorTheme
|
||||||
|
|
||||||
ColDefault ColorPair
|
|
||||||
ColNormal ColorPair
|
|
||||||
ColPrompt ColorPair
|
ColPrompt ColorPair
|
||||||
|
ColNormal ColorPair
|
||||||
|
ColInput ColorPair
|
||||||
|
ColDisabled ColorPair
|
||||||
ColMatch ColorPair
|
ColMatch ColorPair
|
||||||
|
ColCursor ColorPair
|
||||||
|
ColCursorEmpty ColorPair
|
||||||
|
ColSelected ColorPair
|
||||||
ColCurrent ColorPair
|
ColCurrent ColorPair
|
||||||
ColCurrentMatch ColorPair
|
ColCurrentMatch ColorPair
|
||||||
|
ColCurrentCursor ColorPair
|
||||||
|
ColCurrentCursorEmpty ColorPair
|
||||||
|
ColCurrentSelected ColorPair
|
||||||
|
ColCurrentSelectedEmpty ColorPair
|
||||||
ColSpinner ColorPair
|
ColSpinner ColorPair
|
||||||
ColInfo ColorPair
|
ColInfo ColorPair
|
||||||
ColCursor ColorPair
|
|
||||||
ColSelected ColorPair
|
|
||||||
ColHeader ColorPair
|
ColHeader ColorPair
|
||||||
|
ColSeparator ColorPair
|
||||||
|
ColScrollbar ColorPair
|
||||||
ColBorder ColorPair
|
ColBorder ColorPair
|
||||||
ColUser ColorPair
|
ColPreview ColorPair
|
||||||
|
ColPreviewBorder ColorPair
|
||||||
|
ColBorderLabel ColorPair
|
||||||
|
ColPreviewLabel ColorPair
|
||||||
|
ColPreviewScrollbar ColorPair
|
||||||
|
ColPreviewSpinner ColorPair
|
||||||
)
|
)
|
||||||
|
|
||||||
func EmptyTheme() *ColorTheme {
|
func EmptyTheme() *ColorTheme {
|
||||||
return &ColorTheme{
|
return &ColorTheme{
|
||||||
Fg: colUndefined,
|
Colored: true,
|
||||||
Bg: colUndefined,
|
Input: ColorAttr{colUndefined, AttrUndefined},
|
||||||
DarkBg: colUndefined,
|
Fg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Prompt: colUndefined,
|
Bg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Match: colUndefined,
|
DarkBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Current: colUndefined,
|
Prompt: ColorAttr{colUndefined, AttrUndefined},
|
||||||
CurrentMatch: colUndefined,
|
Match: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Spinner: colUndefined,
|
Current: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Info: colUndefined,
|
CurrentMatch: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Cursor: colUndefined,
|
Spinner: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Selected: colUndefined,
|
Info: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Header: colUndefined,
|
Cursor: ColorAttr{colUndefined, AttrUndefined},
|
||||||
Border: colUndefined}
|
Selected: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Header: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Border: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
BorderLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NoColorTheme() *ColorTheme {
|
||||||
|
return &ColorTheme{
|
||||||
|
Colored: false,
|
||||||
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
DarkBg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Prompt: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Match: ColorAttr{colDefault, Underline},
|
||||||
|
Current: ColorAttr{colDefault, Reverse},
|
||||||
|
CurrentMatch: ColorAttr{colDefault, Reverse | Underline},
|
||||||
|
Spinner: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Info: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Cursor: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Selected: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Header: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Border: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
BorderLabel: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Disabled: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
PreviewFg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
PreviewBg: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Gutter: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
PreviewBorder: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
PreviewScrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
PreviewLabel: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Separator: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
Scrollbar: ColorAttr{colDefault, AttrUndefined},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorExit(message string) {
|
func errorExit(message string) {
|
||||||
@@ -309,65 +647,104 @@ func errorExit(message string) {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Default16 = &ColorTheme{
|
Default16 = &ColorTheme{
|
||||||
Fg: colDefault,
|
Colored: true,
|
||||||
Bg: colDefault,
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
DarkBg: colBlack,
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Prompt: colBlue,
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Match: colGreen,
|
DarkBg: ColorAttr{colBlack, AttrUndefined},
|
||||||
Current: colYellow,
|
Prompt: ColorAttr{colBlue, AttrUndefined},
|
||||||
CurrentMatch: colGreen,
|
Match: ColorAttr{colGreen, AttrUndefined},
|
||||||
Spinner: colGreen,
|
Current: ColorAttr{colYellow, AttrUndefined},
|
||||||
Info: colWhite,
|
CurrentMatch: ColorAttr{colGreen, AttrUndefined},
|
||||||
Cursor: colRed,
|
Spinner: ColorAttr{colGreen, AttrUndefined},
|
||||||
Selected: colMagenta,
|
Info: ColorAttr{colWhite, AttrUndefined},
|
||||||
Header: colCyan,
|
Cursor: ColorAttr{colRed, AttrUndefined},
|
||||||
Border: colBlack}
|
Selected: ColorAttr{colMagenta, AttrUndefined},
|
||||||
|
Header: ColorAttr{colCyan, AttrUndefined},
|
||||||
|
Border: ColorAttr{colBlack, AttrUndefined},
|
||||||
|
BorderLabel: ColorAttr{colWhite, AttrUndefined},
|
||||||
|
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
}
|
||||||
Dark256 = &ColorTheme{
|
Dark256 = &ColorTheme{
|
||||||
Fg: colDefault,
|
Colored: true,
|
||||||
Bg: colDefault,
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
DarkBg: 236,
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Prompt: 110,
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Match: 108,
|
DarkBg: ColorAttr{236, AttrUndefined},
|
||||||
Current: 254,
|
Prompt: ColorAttr{110, AttrUndefined},
|
||||||
CurrentMatch: 151,
|
Match: ColorAttr{108, AttrUndefined},
|
||||||
Spinner: 148,
|
Current: ColorAttr{254, AttrUndefined},
|
||||||
Info: 144,
|
CurrentMatch: ColorAttr{151, AttrUndefined},
|
||||||
Cursor: 161,
|
Spinner: ColorAttr{148, AttrUndefined},
|
||||||
Selected: 168,
|
Info: ColorAttr{144, AttrUndefined},
|
||||||
Header: 109,
|
Cursor: ColorAttr{161, AttrUndefined},
|
||||||
Border: 59}
|
Selected: ColorAttr{168, AttrUndefined},
|
||||||
|
Header: ColorAttr{109, AttrUndefined},
|
||||||
|
Border: ColorAttr{59, AttrUndefined},
|
||||||
|
BorderLabel: ColorAttr{145, AttrUndefined},
|
||||||
|
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
}
|
||||||
Light256 = &ColorTheme{
|
Light256 = &ColorTheme{
|
||||||
Fg: colDefault,
|
Colored: true,
|
||||||
Bg: colDefault,
|
Input: ColorAttr{colDefault, AttrUndefined},
|
||||||
DarkBg: 251,
|
Fg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Prompt: 25,
|
Bg: ColorAttr{colDefault, AttrUndefined},
|
||||||
Match: 66,
|
DarkBg: ColorAttr{251, AttrUndefined},
|
||||||
Current: 237,
|
Prompt: ColorAttr{25, AttrUndefined},
|
||||||
CurrentMatch: 23,
|
Match: ColorAttr{66, AttrUndefined},
|
||||||
Spinner: 65,
|
Current: ColorAttr{237, AttrUndefined},
|
||||||
Info: 101,
|
CurrentMatch: ColorAttr{23, AttrUndefined},
|
||||||
Cursor: 161,
|
Spinner: ColorAttr{65, AttrUndefined},
|
||||||
Selected: 168,
|
Info: ColorAttr{101, AttrUndefined},
|
||||||
Header: 31,
|
Cursor: ColorAttr{161, AttrUndefined},
|
||||||
Border: 145}
|
Selected: ColorAttr{168, AttrUndefined},
|
||||||
|
Header: ColorAttr{31, AttrUndefined},
|
||||||
|
Border: ColorAttr{145, AttrUndefined},
|
||||||
|
BorderLabel: ColorAttr{59, AttrUndefined},
|
||||||
|
Disabled: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewFg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBg: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Gutter: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewBorder: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewScrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
PreviewLabel: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Separator: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
Scrollbar: ColorAttr{colUndefined, AttrUndefined},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
||||||
if theme == nil {
|
|
||||||
initPalette(theme)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if forceBlack {
|
if forceBlack {
|
||||||
theme.Bg = colBlack
|
theme.Bg = ColorAttr{colBlack, AttrUndefined}
|
||||||
}
|
}
|
||||||
|
|
||||||
o := func(a Color, b Color) Color {
|
o := func(a ColorAttr, b ColorAttr) ColorAttr {
|
||||||
if b == colUndefined {
|
c := a
|
||||||
return a
|
if b.Color != colUndefined {
|
||||||
|
c.Color = b.Color
|
||||||
}
|
}
|
||||||
return b
|
if b.Attr != AttrUndefined {
|
||||||
|
c.Attr = b.Attr
|
||||||
}
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
theme.Input = o(baseTheme.Input, theme.Input)
|
||||||
theme.Fg = o(baseTheme.Fg, theme.Fg)
|
theme.Fg = o(baseTheme.Fg, theme.Fg)
|
||||||
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
theme.Bg = o(baseTheme.Bg, theme.Bg)
|
||||||
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
theme.DarkBg = o(baseTheme.DarkBg, theme.DarkBg)
|
||||||
@@ -381,48 +758,56 @@ func initTheme(theme *ColorTheme, baseTheme *ColorTheme, forceBlack bool) {
|
|||||||
theme.Selected = o(baseTheme.Selected, theme.Selected)
|
theme.Selected = o(baseTheme.Selected, theme.Selected)
|
||||||
theme.Header = o(baseTheme.Header, theme.Header)
|
theme.Header = o(baseTheme.Header, theme.Header)
|
||||||
theme.Border = o(baseTheme.Border, theme.Border)
|
theme.Border = o(baseTheme.Border, theme.Border)
|
||||||
|
theme.BorderLabel = o(baseTheme.BorderLabel, theme.BorderLabel)
|
||||||
|
|
||||||
|
// These colors are not defined in the base themes
|
||||||
|
theme.Disabled = o(theme.Input, theme.Disabled)
|
||||||
|
theme.Gutter = o(theme.DarkBg, theme.Gutter)
|
||||||
|
theme.PreviewFg = o(theme.Fg, theme.PreviewFg)
|
||||||
|
theme.PreviewBg = o(theme.Bg, theme.PreviewBg)
|
||||||
|
theme.PreviewLabel = o(theme.BorderLabel, theme.PreviewLabel)
|
||||||
|
theme.PreviewBorder = o(theme.Border, theme.PreviewBorder)
|
||||||
|
theme.Separator = o(theme.Border, theme.Separator)
|
||||||
|
theme.Scrollbar = o(theme.Border, theme.Scrollbar)
|
||||||
|
theme.PreviewScrollbar = o(theme.PreviewBorder, theme.PreviewScrollbar)
|
||||||
|
|
||||||
initPalette(theme)
|
initPalette(theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initPalette(theme *ColorTheme) {
|
func initPalette(theme *ColorTheme) {
|
||||||
ColDefault = ColorPair{colDefault, colDefault, 0}
|
pair := func(fg, bg ColorAttr) ColorPair {
|
||||||
if theme != nil {
|
if fg.Color == colDefault && (fg.Attr&Reverse) > 0 {
|
||||||
ColNormal = ColorPair{theme.Fg, theme.Bg, 1}
|
bg.Color = colDefault
|
||||||
ColPrompt = ColorPair{theme.Prompt, theme.Bg, 2}
|
|
||||||
ColMatch = ColorPair{theme.Match, theme.Bg, 3}
|
|
||||||
ColCurrent = ColorPair{theme.Current, theme.DarkBg, 4}
|
|
||||||
ColCurrentMatch = ColorPair{theme.CurrentMatch, theme.DarkBg, 5}
|
|
||||||
ColSpinner = ColorPair{theme.Spinner, theme.Bg, 6}
|
|
||||||
ColInfo = ColorPair{theme.Info, theme.Bg, 7}
|
|
||||||
ColCursor = ColorPair{theme.Cursor, theme.DarkBg, 8}
|
|
||||||
ColSelected = ColorPair{theme.Selected, theme.DarkBg, 9}
|
|
||||||
ColHeader = ColorPair{theme.Header, theme.Bg, 10}
|
|
||||||
ColBorder = ColorPair{theme.Border, theme.Bg, 11}
|
|
||||||
} else {
|
|
||||||
ColNormal = ColorPair{colDefault, colDefault, 1}
|
|
||||||
ColPrompt = ColorPair{colDefault, colDefault, 2}
|
|
||||||
ColMatch = ColorPair{colDefault, colDefault, 3}
|
|
||||||
ColCurrent = ColorPair{colDefault, colDefault, 4}
|
|
||||||
ColCurrentMatch = ColorPair{colDefault, colDefault, 5}
|
|
||||||
ColSpinner = ColorPair{colDefault, colDefault, 6}
|
|
||||||
ColInfo = ColorPair{colDefault, colDefault, 7}
|
|
||||||
ColCursor = ColorPair{colDefault, colDefault, 8}
|
|
||||||
ColSelected = ColorPair{colDefault, colDefault, 9}
|
|
||||||
ColHeader = ColorPair{colDefault, colDefault, 10}
|
|
||||||
ColBorder = ColorPair{colDefault, colDefault, 11}
|
|
||||||
}
|
}
|
||||||
ColUser = ColorPair{colDefault, colDefault, 12}
|
return ColorPair{fg.Color, bg.Color, fg.Attr}
|
||||||
}
|
}
|
||||||
|
blank := theme.Fg
|
||||||
|
blank.Attr = AttrRegular
|
||||||
|
|
||||||
func attrFor(color ColorPair, attr Attr) Attr {
|
ColPrompt = pair(theme.Prompt, theme.Bg)
|
||||||
switch color {
|
ColNormal = pair(theme.Fg, theme.Bg)
|
||||||
case ColCurrent:
|
ColInput = pair(theme.Input, theme.Bg)
|
||||||
return attr | Reverse
|
ColDisabled = pair(theme.Disabled, theme.Bg)
|
||||||
case ColMatch:
|
ColMatch = pair(theme.Match, theme.Bg)
|
||||||
return attr | Underline
|
ColCursor = pair(theme.Cursor, theme.Gutter)
|
||||||
case ColCurrentMatch:
|
ColCursorEmpty = pair(blank, theme.Gutter)
|
||||||
return attr | Underline | Reverse
|
ColSelected = pair(theme.Selected, theme.Gutter)
|
||||||
}
|
ColCurrent = pair(theme.Current, theme.DarkBg)
|
||||||
return attr
|
ColCurrentMatch = pair(theme.CurrentMatch, theme.DarkBg)
|
||||||
|
ColCurrentCursor = pair(theme.Cursor, theme.DarkBg)
|
||||||
|
ColCurrentCursorEmpty = pair(blank, theme.DarkBg)
|
||||||
|
ColCurrentSelected = pair(theme.Selected, theme.DarkBg)
|
||||||
|
ColCurrentSelectedEmpty = pair(blank, theme.DarkBg)
|
||||||
|
ColSpinner = pair(theme.Spinner, theme.Bg)
|
||||||
|
ColInfo = pair(theme.Info, theme.Bg)
|
||||||
|
ColHeader = pair(theme.Header, theme.Bg)
|
||||||
|
ColSeparator = pair(theme.Separator, theme.Bg)
|
||||||
|
ColScrollbar = pair(theme.Scrollbar, theme.Bg)
|
||||||
|
ColBorder = pair(theme.Border, theme.Bg)
|
||||||
|
ColBorderLabel = pair(theme.BorderLabel, theme.Bg)
|
||||||
|
ColPreviewLabel = pair(theme.PreviewLabel, theme.PreviewBg)
|
||||||
|
ColPreview = pair(theme.PreviewFg, theme.PreviewBg)
|
||||||
|
ColPreviewBorder = pair(theme.PreviewBorder, theme.PreviewBg)
|
||||||
|
ColPreviewScrollbar = pair(theme.PreviewScrollbar, theme.PreviewBg)
|
||||||
|
ColPreviewSpinner = pair(theme.Spinner, theme.PreviewBg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
#!/usr/bin/env ruby
|
|
||||||
|
|
||||||
# http://www.rubydoc.info/github/rest-client/rest-client/RestClient
|
|
||||||
require 'rest_client'
|
|
||||||
require 'json'
|
|
||||||
|
|
||||||
if ARGV.length < 3
|
|
||||||
puts "usage: #$0 <token> <version> <files...>"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
token, version, *files = ARGV
|
|
||||||
base = "https://api.github.com/repos/junegunn/fzf-bin/releases"
|
|
||||||
|
|
||||||
# List releases
|
|
||||||
rels = JSON.parse(RestClient.get(base, :authorization => "token #{token}"))
|
|
||||||
rel = rels.find { |r| r['tag_name'] == version }
|
|
||||||
unless rel
|
|
||||||
puts "#{version} not found"
|
|
||||||
exit 1
|
|
||||||
end
|
|
||||||
|
|
||||||
# List assets
|
|
||||||
assets = Hash[rel['assets'].map { |a| a.values_at *%w[name id] }]
|
|
||||||
|
|
||||||
files.select { |f| File.exists? f }.map do |file|
|
|
||||||
Thread.new do
|
|
||||||
name = File.basename file
|
|
||||||
|
|
||||||
if asset_id = assets[name]
|
|
||||||
puts "#{name} found. Deleting asset id #{asset_id}."
|
|
||||||
RestClient.delete "#{base}/assets/#{asset_id}",
|
|
||||||
:authorization => "token #{token}"
|
|
||||||
else
|
|
||||||
puts "#{name} not found"
|
|
||||||
end
|
|
||||||
|
|
||||||
puts "Uploading #{name}"
|
|
||||||
RestClient.post(
|
|
||||||
"#{base.sub 'api', 'uploads'}/#{rel['id']}/assets?name=#{name}",
|
|
||||||
File.read(file),
|
|
||||||
:authorization => "token #{token}",
|
|
||||||
:content_type => "application/octet-stream")
|
|
||||||
end
|
|
||||||
end.each(&:join)
|
|
||||||
@@ -1,32 +1,34 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func convertBoolToInt32(b bool) int32 {
|
||||||
|
if b {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
// AtomicBool is a boxed-class that provides synchronized access to the
|
// AtomicBool is a boxed-class that provides synchronized access to the
|
||||||
// underlying boolean value
|
// underlying boolean value
|
||||||
type AtomicBool struct {
|
type AtomicBool struct {
|
||||||
mutex sync.Mutex
|
state int32 // "1" is true, "0" is false
|
||||||
state bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAtomicBool returns a new AtomicBool
|
// NewAtomicBool returns a new AtomicBool
|
||||||
func NewAtomicBool(initialState bool) *AtomicBool {
|
func NewAtomicBool(initialState bool) *AtomicBool {
|
||||||
return &AtomicBool{
|
return &AtomicBool{state: convertBoolToInt32(initialState)}
|
||||||
mutex: sync.Mutex{},
|
|
||||||
state: initialState}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the current boolean value synchronously
|
// Get returns the current boolean value synchronously
|
||||||
func (a *AtomicBool) Get() bool {
|
func (a *AtomicBool) Get() bool {
|
||||||
a.mutex.Lock()
|
return atomic.LoadInt32(&a.state) == 1
|
||||||
defer a.mutex.Unlock()
|
|
||||||
return a.state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set updates the boolean value synchronously
|
// Set updates the boolean value synchronously
|
||||||
func (a *AtomicBool) Set(newState bool) bool {
|
func (a *AtomicBool) Set(newState bool) bool {
|
||||||
a.mutex.Lock()
|
atomic.StoreInt32(&a.state, convertBoolToInt32(newState))
|
||||||
defer a.mutex.Unlock()
|
return newState
|
||||||
a.state = newState
|
|
||||||
return a.state
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,111 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"unicode"
|
"unicode"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
overflow64 uint64 = 0x8080808080808080
|
||||||
|
overflow32 uint32 = 0x80808080
|
||||||
)
|
)
|
||||||
|
|
||||||
type Chars struct {
|
type Chars struct {
|
||||||
runes []rune
|
slice []byte // or []rune
|
||||||
bytes []byte
|
inBytes bool
|
||||||
|
trimLengthKnown bool
|
||||||
|
trimLength uint16
|
||||||
|
|
||||||
|
// XXX Piggybacking item index here is a horrible idea. But I'm trying to
|
||||||
|
// minimize the memory footprint by not wasting padded spaces.
|
||||||
|
Index int32
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkAscii(bytes []byte) (bool, int) {
|
||||||
|
i := 0
|
||||||
|
for ; i <= len(bytes)-8; i += 8 {
|
||||||
|
if (overflow64 & *(*uint64)(unsafe.Pointer(&bytes[i]))) > 0 {
|
||||||
|
return false, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; i <= len(bytes)-4; i += 4 {
|
||||||
|
if (overflow32 & *(*uint32)(unsafe.Pointer(&bytes[i]))) > 0 {
|
||||||
|
return false, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; i < len(bytes); i++ {
|
||||||
|
if bytes[i] >= utf8.RuneSelf {
|
||||||
|
return false, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToChars converts byte array into rune array
|
// ToChars converts byte array into rune array
|
||||||
func ToChars(bytea []byte) Chars {
|
func ToChars(bytes []byte) Chars {
|
||||||
var runes []rune
|
inBytes, bytesUntil := checkAscii(bytes)
|
||||||
ascii := true
|
if inBytes {
|
||||||
numBytes := len(bytea)
|
return Chars{slice: bytes, inBytes: inBytes}
|
||||||
for i := 0; i < numBytes; {
|
|
||||||
if bytea[i] < utf8.RuneSelf {
|
|
||||||
if !ascii {
|
|
||||||
runes = append(runes, rune(bytea[i]))
|
|
||||||
}
|
}
|
||||||
i++
|
|
||||||
} else {
|
runes := make([]rune, bytesUntil, len(bytes))
|
||||||
if ascii {
|
for i := 0; i < bytesUntil; i++ {
|
||||||
ascii = false
|
runes[i] = rune(bytes[i])
|
||||||
runes = make([]rune, i, numBytes)
|
|
||||||
for j := 0; j < i; j++ {
|
|
||||||
runes[j] = rune(bytea[j])
|
|
||||||
}
|
}
|
||||||
}
|
for i := bytesUntil; i < len(bytes); {
|
||||||
r, sz := utf8.DecodeRune(bytea[i:])
|
r, sz := utf8.DecodeRune(bytes[i:])
|
||||||
i += sz
|
i += sz
|
||||||
runes = append(runes, r)
|
runes = append(runes, r)
|
||||||
}
|
}
|
||||||
}
|
return RunesToChars(runes)
|
||||||
if ascii {
|
|
||||||
return Chars{bytes: bytea}
|
|
||||||
}
|
|
||||||
return Chars{runes: runes}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunesToChars(runes []rune) Chars {
|
func RunesToChars(runes []rune) Chars {
|
||||||
return Chars{runes: runes}
|
return Chars{slice: *(*[]byte)(unsafe.Pointer(&runes)), inBytes: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) IsBytes() bool {
|
||||||
|
return chars.inBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) Bytes() []byte {
|
||||||
|
return chars.slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) optionalRunes() []rune {
|
||||||
|
if chars.inBytes {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return *(*[]rune)(unsafe.Pointer(&chars.slice))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) Get(i int) rune {
|
func (chars *Chars) Get(i int) rune {
|
||||||
if chars.runes != nil {
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
return chars.runes[i]
|
return runes[i]
|
||||||
}
|
}
|
||||||
return rune(chars.bytes[i])
|
return rune(chars.slice[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) Length() int {
|
func (chars *Chars) Length() int {
|
||||||
if chars.runes != nil {
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
return len(chars.runes)
|
return len(runes)
|
||||||
}
|
}
|
||||||
return len(chars.bytes)
|
return len(chars.slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string representation of a Chars object.
|
||||||
|
func (chars *Chars) String() string {
|
||||||
|
return fmt.Sprintf("Chars{slice: []byte(%q), inBytes: %v, trimLengthKnown: %v, trimLength: %d, Index: %d}", chars.slice, chars.inBytes, chars.trimLengthKnown, chars.trimLength, chars.Index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrimLength returns the length after trimming leading and trailing whitespaces
|
// TrimLength returns the length after trimming leading and trailing whitespaces
|
||||||
func (chars *Chars) TrimLength() int {
|
func (chars *Chars) TrimLength() uint16 {
|
||||||
|
if chars.trimLengthKnown {
|
||||||
|
return chars.trimLength
|
||||||
|
}
|
||||||
|
chars.trimLengthKnown = true
|
||||||
var i int
|
var i int
|
||||||
len := chars.Length()
|
len := chars.Length()
|
||||||
for i = len - 1; i >= 0; i-- {
|
for i = len - 1; i >= 0; i-- {
|
||||||
@@ -80,7 +126,20 @@ func (chars *Chars) TrimLength() int {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return i - j + 1
|
chars.trimLength = AsUint16(i - j + 1)
|
||||||
|
return chars.trimLength
|
||||||
|
}
|
||||||
|
|
||||||
|
func (chars *Chars) LeadingWhitespaces() int {
|
||||||
|
whitespaces := 0
|
||||||
|
for i := 0; i < chars.Length(); i++ {
|
||||||
|
char := chars.Get(i)
|
||||||
|
if !unicode.IsSpace(char) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
whitespaces++
|
||||||
|
}
|
||||||
|
return whitespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) TrailingWhitespaces() int {
|
func (chars *Chars) TrailingWhitespaces() int {
|
||||||
@@ -95,63 +154,45 @@ func (chars *Chars) TrailingWhitespaces() int {
|
|||||||
return whitespaces
|
return whitespaces
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) ToString() string {
|
func (chars *Chars) TrimTrailingWhitespaces() {
|
||||||
if chars.runes != nil {
|
whitespaces := chars.TrailingWhitespaces()
|
||||||
return string(chars.runes)
|
chars.slice = chars.slice[0 : len(chars.slice)-whitespaces]
|
||||||
}
|
}
|
||||||
return string(chars.bytes)
|
|
||||||
|
func (chars *Chars) ToString() string {
|
||||||
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
|
return string(runes)
|
||||||
|
}
|
||||||
|
return string(chars.slice)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) ToRunes() []rune {
|
func (chars *Chars) ToRunes() []rune {
|
||||||
if chars.runes != nil {
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
return chars.runes
|
return runes
|
||||||
}
|
}
|
||||||
runes := make([]rune, len(chars.bytes))
|
bytes := chars.slice
|
||||||
for idx, b := range chars.bytes {
|
runes := make([]rune, len(bytes))
|
||||||
|
for idx, b := range bytes {
|
||||||
runes[idx] = rune(b)
|
runes[idx] = rune(b)
|
||||||
}
|
}
|
||||||
return runes
|
return runes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) Slice(b int, e int) Chars {
|
func (chars *Chars) CopyRunes(dest []rune) {
|
||||||
if chars.runes != nil {
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
return Chars{runes: chars.runes[b:e]}
|
copy(dest, runes)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for idx, b := range chars.slice[:len(dest)] {
|
||||||
|
dest[idx] = rune(b)
|
||||||
}
|
}
|
||||||
return Chars{bytes: chars.bytes[b:e]}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (chars *Chars) Split(delimiter string) []Chars {
|
func (chars *Chars) Prepend(prefix string) {
|
||||||
delim := []rune(delimiter)
|
if runes := chars.optionalRunes(); runes != nil {
|
||||||
numChars := chars.Length()
|
runes = append([]rune(prefix), runes...)
|
||||||
numDelim := len(delim)
|
chars.slice = *(*[]byte)(unsafe.Pointer(&runes))
|
||||||
begin := 0
|
|
||||||
ret := make([]Chars, 0, 1)
|
|
||||||
|
|
||||||
for index := 0; index < numChars; {
|
|
||||||
if index+numDelim <= numChars {
|
|
||||||
match := true
|
|
||||||
for off, d := range delim {
|
|
||||||
if chars.Get(index+off) != d {
|
|
||||||
match = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Found the delimiter
|
|
||||||
if match {
|
|
||||||
incr := Max(numDelim, 1)
|
|
||||||
ret = append(ret, chars.Slice(begin, index+incr))
|
|
||||||
index += incr
|
|
||||||
begin = index
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Impossible to find the delimiter in the remaining substring
|
chars.slice = append([]byte(prefix), chars.slice...)
|
||||||
break
|
|
||||||
}
|
}
|
||||||
index++
|
|
||||||
}
|
|
||||||
if begin < numChars || len(ret) == 0 {
|
|
||||||
ret = append(ret, chars.Slice(begin, numChars))
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,27 +2,16 @@ package util
|
|||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestToCharsNil(t *testing.T) {
|
|
||||||
bs := Chars{bytes: []byte{}}
|
|
||||||
if bs.bytes == nil || bs.runes != nil {
|
|
||||||
t.Error()
|
|
||||||
}
|
|
||||||
rs := RunesToChars([]rune{})
|
|
||||||
if rs.bytes != nil || rs.runes == nil {
|
|
||||||
t.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestToCharsAscii(t *testing.T) {
|
func TestToCharsAscii(t *testing.T) {
|
||||||
chars := ToChars([]byte("foobar"))
|
chars := ToChars([]byte("foobar"))
|
||||||
if chars.ToString() != "foobar" || chars.runes != nil {
|
if !chars.inBytes || chars.ToString() != "foobar" || !chars.inBytes {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCharsLength(t *testing.T) {
|
func TestCharsLength(t *testing.T) {
|
||||||
chars := ToChars([]byte("\tabc한글 "))
|
chars := ToChars([]byte("\tabc한글 "))
|
||||||
if chars.Length() != 8 || chars.TrimLength() != 5 {
|
if chars.inBytes || chars.Length() != 8 || chars.TrimLength() != 5 {
|
||||||
t.Error()
|
t.Error()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,7 +25,7 @@ func TestCharsToString(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTrimLength(t *testing.T) {
|
func TestTrimLength(t *testing.T) {
|
||||||
check := func(str string, exp int) {
|
check := func(str string, exp uint16) {
|
||||||
chars := ToChars([]byte(str))
|
chars := ToChars([]byte(str))
|
||||||
trimmed := chars.TrimLength()
|
trimmed := chars.TrimLength()
|
||||||
if trimmed != exp {
|
if trimmed != exp {
|
||||||
@@ -55,28 +44,3 @@ func TestTrimLength(t *testing.T) {
|
|||||||
check(" h o ", 5)
|
check(" h o ", 5)
|
||||||
check(" ", 0)
|
check(" ", 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSplit(t *testing.T) {
|
|
||||||
check := func(str string, delim string, tokens ...string) {
|
|
||||||
input := ToChars([]byte(str))
|
|
||||||
result := input.Split(delim)
|
|
||||||
if len(result) != len(tokens) {
|
|
||||||
t.Errorf("Invalid Split result for '%s': %d tokens found (expected %d): %s",
|
|
||||||
str, len(result), len(tokens), result)
|
|
||||||
}
|
|
||||||
for idx, token := range tokens {
|
|
||||||
if result[idx].ToString() != token {
|
|
||||||
t.Errorf("Invalid Split result for '%s': %s (expected %s)",
|
|
||||||
str, result[idx].ToString(), token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
check("abc:def::", ":", "abc:", "def:", ":")
|
|
||||||
check("abc:def::", "-", "abc:def::")
|
|
||||||
check("abc", "", "a", "b", "c")
|
|
||||||
check("abc", "a", "a", "bc")
|
|
||||||
check("abc", "ab", "ab", "c")
|
|
||||||
check("abc", "abc", "abc")
|
|
||||||
check("abc", "abcd", "abc")
|
|
||||||
check("", "abcd", "")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,23 +26,23 @@ func NewEventBox() *EventBox {
|
|||||||
// Wait blocks the goroutine until signaled
|
// Wait blocks the goroutine until signaled
|
||||||
func (b *EventBox) Wait(callback func(*Events)) {
|
func (b *EventBox) Wait(callback func(*Events)) {
|
||||||
b.cond.L.Lock()
|
b.cond.L.Lock()
|
||||||
defer b.cond.L.Unlock()
|
|
||||||
|
|
||||||
if len(b.events) == 0 {
|
if len(b.events) == 0 {
|
||||||
b.cond.Wait()
|
b.cond.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(&b.events)
|
callback(&b.events)
|
||||||
|
b.cond.L.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set turns on the event type on the box
|
// Set turns on the event type on the box
|
||||||
func (b *EventBox) Set(event EventType, value interface{}) {
|
func (b *EventBox) Set(event EventType, value interface{}) {
|
||||||
b.cond.L.Lock()
|
b.cond.L.Lock()
|
||||||
defer b.cond.L.Unlock()
|
|
||||||
b.events[event] = value
|
b.events[event] = value
|
||||||
if _, found := b.ignore[event]; !found {
|
if _, found := b.ignore[event]; !found {
|
||||||
b.cond.Broadcast()
|
b.cond.Broadcast()
|
||||||
}
|
}
|
||||||
|
b.cond.L.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear clears the events
|
// Clear clears the events
|
||||||
@@ -56,27 +56,27 @@ func (events *Events) Clear() {
|
|||||||
// Peek peeks at the event box if the given event is set
|
// Peek peeks at the event box if the given event is set
|
||||||
func (b *EventBox) Peek(event EventType) bool {
|
func (b *EventBox) Peek(event EventType) bool {
|
||||||
b.cond.L.Lock()
|
b.cond.L.Lock()
|
||||||
defer b.cond.L.Unlock()
|
|
||||||
_, ok := b.events[event]
|
_, ok := b.events[event]
|
||||||
|
b.cond.L.Unlock()
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch deletes the events from the ignore list
|
// Watch deletes the events from the ignore list
|
||||||
func (b *EventBox) Watch(events ...EventType) {
|
func (b *EventBox) Watch(events ...EventType) {
|
||||||
b.cond.L.Lock()
|
b.cond.L.Lock()
|
||||||
defer b.cond.L.Unlock()
|
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
delete(b.ignore, event)
|
delete(b.ignore, event)
|
||||||
}
|
}
|
||||||
|
b.cond.L.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwatch adds the events to the ignore list
|
// Unwatch adds the events to the ignore list
|
||||||
func (b *EventBox) Unwatch(events ...EventType) {
|
func (b *EventBox) Unwatch(events ...EventType) {
|
||||||
b.cond.L.Lock()
|
b.cond.L.Lock()
|
||||||
defer b.cond.L.Unlock()
|
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
b.ignore[event] = true
|
b.ignore[event] = true
|
||||||
}
|
}
|
||||||
|
b.cond.L.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// WaitFor blocks the execution until the event is received
|
// WaitFor blocks the execution until the event is received
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ const (
|
|||||||
EvtSearchNew
|
EvtSearchNew
|
||||||
EvtSearchProgress
|
EvtSearchProgress
|
||||||
EvtSearchFin
|
EvtSearchFin
|
||||||
EvtClose
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEventBox(t *testing.T) {
|
func TestEventBox(t *testing.T) {
|
||||||
|
|||||||
@@ -3,25 +3,56 @@ package util
|
|||||||
import (
|
import (
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mattn/go-isatty"
|
"github.com/mattn/go-isatty"
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
|
"github.com/rivo/uniseg"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _runeWidths = make(map[rune]int)
|
// StringWidth returns string width where each CR/LF character takes 1 column
|
||||||
|
func StringWidth(s string) int {
|
||||||
// RuneWidth returns rune width
|
return runewidth.StringWidth(s) + strings.Count(s, "\n") + strings.Count(s, "\r")
|
||||||
func RuneWidth(r rune, prefixWidth int, tabstop int) int {
|
|
||||||
if r == '\t' {
|
|
||||||
return tabstop - prefixWidth%tabstop
|
|
||||||
} else if w, found := _runeWidths[r]; found {
|
|
||||||
return w
|
|
||||||
} else {
|
|
||||||
w := Max(runewidth.RuneWidth(r), 1)
|
|
||||||
_runeWidths[r] = w
|
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RunesWidth returns runes width
|
||||||
|
func RunesWidth(runes []rune, prefixWidth int, tabstop int, limit int) (int, int) {
|
||||||
|
width := 0
|
||||||
|
gr := uniseg.NewGraphemes(string(runes))
|
||||||
|
idx := 0
|
||||||
|
for gr.Next() {
|
||||||
|
rs := gr.Runes()
|
||||||
|
var w int
|
||||||
|
if len(rs) == 1 && rs[0] == '\t' {
|
||||||
|
w = tabstop - (prefixWidth+width)%tabstop
|
||||||
|
} else {
|
||||||
|
w = StringWidth(string(rs))
|
||||||
|
}
|
||||||
|
width += w
|
||||||
|
if width > limit {
|
||||||
|
return width, idx
|
||||||
|
}
|
||||||
|
idx += len(rs)
|
||||||
|
}
|
||||||
|
return width, -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate returns the truncated runes and its width
|
||||||
|
func Truncate(input string, limit int) ([]rune, int) {
|
||||||
|
runes := []rune{}
|
||||||
|
width := 0
|
||||||
|
gr := uniseg.NewGraphemes(input)
|
||||||
|
for gr.Next() {
|
||||||
|
rs := gr.Runes()
|
||||||
|
w := StringWidth(string(rs))
|
||||||
|
if width+w > limit {
|
||||||
|
return runes, width
|
||||||
|
}
|
||||||
|
width += w
|
||||||
|
runes = append(runes, rs...)
|
||||||
|
}
|
||||||
|
return runes, width
|
||||||
}
|
}
|
||||||
|
|
||||||
// Max returns the largest integer
|
// Max returns the largest integer
|
||||||
@@ -107,7 +138,42 @@ func DurWithin(
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsTty returns true is stdin is a terminal
|
// IsTty returns true if stdin is a terminal
|
||||||
func IsTty() bool {
|
func IsTty() bool {
|
||||||
return isatty.IsTerminal(os.Stdin.Fd())
|
return isatty.IsTerminal(os.Stdin.Fd())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToTty returns true if stdout is a terminal
|
||||||
|
func ToTty() bool {
|
||||||
|
return isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once returns a function that returns the specified boolean value only once
|
||||||
|
func Once(nextResponse bool) func() bool {
|
||||||
|
state := nextResponse
|
||||||
|
return func() bool {
|
||||||
|
prevState := state
|
||||||
|
state = false
|
||||||
|
return prevState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RepeatToFill repeats the given string to fill the given width
|
||||||
|
func RepeatToFill(str string, length int, limit int) string {
|
||||||
|
times := limit / length
|
||||||
|
rest := limit % length
|
||||||
|
output := strings.Repeat(str, times)
|
||||||
|
if rest > 0 {
|
||||||
|
for _, r := range str {
|
||||||
|
rest -= runewidth.RuneWidth(r)
|
||||||
|
if rest < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
output += string(r)
|
||||||
|
if rest == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,76 @@
|
|||||||
package util
|
package util
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"math"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestMax(t *testing.T) {
|
func TestMax(t *testing.T) {
|
||||||
|
if Max(10, 1) != 10 {
|
||||||
|
t.Error("Expected", 10)
|
||||||
|
}
|
||||||
if Max(-2, 5) != 5 {
|
if Max(-2, 5) != 5 {
|
||||||
t.Error("Invalid result")
|
t.Error("Expected", 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContrain(t *testing.T) {
|
func TestMax16(t *testing.T) {
|
||||||
|
if Max16(10, 1) != 10 {
|
||||||
|
t.Error("Expected", 10)
|
||||||
|
}
|
||||||
|
if Max16(-2, 5) != 5 {
|
||||||
|
t.Error("Expected", 5)
|
||||||
|
}
|
||||||
|
if Max16(math.MaxInt16, 0) != math.MaxInt16 {
|
||||||
|
t.Error("Expected", math.MaxInt16)
|
||||||
|
}
|
||||||
|
if Max16(0, math.MinInt16) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMax32(t *testing.T) {
|
||||||
|
if Max32(10, 1) != 10 {
|
||||||
|
t.Error("Expected", 10)
|
||||||
|
}
|
||||||
|
if Max32(-2, 5) != 5 {
|
||||||
|
t.Error("Expected", 5)
|
||||||
|
}
|
||||||
|
if Max32(math.MaxInt32, 0) != math.MaxInt32 {
|
||||||
|
t.Error("Expected", math.MaxInt32)
|
||||||
|
}
|
||||||
|
if Max32(0, math.MinInt32) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMin(t *testing.T) {
|
||||||
|
if Min(10, 1) != 1 {
|
||||||
|
t.Error("Expected", 1)
|
||||||
|
}
|
||||||
|
if Min(-2, 5) != -2 {
|
||||||
|
t.Error("Expected", -2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMin32(t *testing.T) {
|
||||||
|
if Min32(10, 1) != 1 {
|
||||||
|
t.Error("Expected", 1)
|
||||||
|
}
|
||||||
|
if Min32(-2, 5) != -2 {
|
||||||
|
t.Error("Expected", -2)
|
||||||
|
}
|
||||||
|
if Min32(math.MaxInt32, 0) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
if Min32(0, math.MinInt32) != math.MinInt32 {
|
||||||
|
t.Error("Expected", math.MinInt32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstrain(t *testing.T) {
|
||||||
if Constrain(-3, -1, 3) != -1 {
|
if Constrain(-3, -1, 3) != -1 {
|
||||||
t.Error("Expected", -1)
|
t.Error("Expected", -1)
|
||||||
}
|
}
|
||||||
@@ -20,3 +82,112 @@ func TestContrain(t *testing.T) {
|
|||||||
t.Error("Expected", 3)
|
t.Error("Expected", 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConstrain32(t *testing.T) {
|
||||||
|
if Constrain32(-3, -1, 3) != -1 {
|
||||||
|
t.Error("Expected", -1)
|
||||||
|
}
|
||||||
|
if Constrain32(2, -1, 3) != 2 {
|
||||||
|
t.Error("Expected", 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
if Constrain32(5, -1, 3) != 3 {
|
||||||
|
t.Error("Expected", 3)
|
||||||
|
}
|
||||||
|
if Constrain32(0, math.MinInt32, math.MaxInt32) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAsUint16(t *testing.T) {
|
||||||
|
if AsUint16(5) != 5 {
|
||||||
|
t.Error("Expected", 5)
|
||||||
|
}
|
||||||
|
if AsUint16(-10) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
if AsUint16(math.MaxUint16) != math.MaxUint16 {
|
||||||
|
t.Error("Expected", math.MaxUint16)
|
||||||
|
}
|
||||||
|
if AsUint16(math.MinInt32) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
if AsUint16(math.MinInt16) != 0 {
|
||||||
|
t.Error("Expected", 0)
|
||||||
|
}
|
||||||
|
if AsUint16(math.MaxUint16+1) != math.MaxUint16 {
|
||||||
|
t.Error("Expected", math.MaxUint16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDurWithIn(t *testing.T) {
|
||||||
|
if DurWithin(time.Duration(5), time.Duration(1), time.Duration(8)) != time.Duration(5) {
|
||||||
|
t.Error("Expected", time.Duration(0))
|
||||||
|
}
|
||||||
|
if DurWithin(time.Duration(0)*time.Second, time.Second, time.Duration(3)*time.Second) != time.Second {
|
||||||
|
t.Error("Expected", time.Second)
|
||||||
|
}
|
||||||
|
if DurWithin(time.Duration(10)*time.Second, time.Duration(0), time.Second) != time.Second {
|
||||||
|
t.Error("Expected", time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOnce(t *testing.T) {
|
||||||
|
o := Once(false)
|
||||||
|
if o() {
|
||||||
|
t.Error("Expected: false")
|
||||||
|
}
|
||||||
|
if o() {
|
||||||
|
t.Error("Expected: false")
|
||||||
|
}
|
||||||
|
|
||||||
|
o = Once(true)
|
||||||
|
if !o() {
|
||||||
|
t.Error("Expected: true")
|
||||||
|
}
|
||||||
|
if o() {
|
||||||
|
t.Error("Expected: false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunesWidth(t *testing.T) {
|
||||||
|
for _, args := range [][]int{
|
||||||
|
{100, 5, -1},
|
||||||
|
{3, 4, 3},
|
||||||
|
{0, 1, 0},
|
||||||
|
} {
|
||||||
|
width, overflowIdx := RunesWidth([]rune("hello"), 0, 0, args[0])
|
||||||
|
if width != args[1] {
|
||||||
|
t.Errorf("Expected width: %d, actual: %d", args[1], width)
|
||||||
|
}
|
||||||
|
if overflowIdx != args[2] {
|
||||||
|
t.Errorf("Expected overflow index: %d, actual: %d", args[2], overflowIdx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTruncate(t *testing.T) {
|
||||||
|
truncated, width := Truncate("가나다라마", 7)
|
||||||
|
if string(truncated) != "가나다" {
|
||||||
|
t.Errorf("Expected: 가나다, actual: %s", string(truncated))
|
||||||
|
}
|
||||||
|
if width != 6 {
|
||||||
|
t.Errorf("Expected: 6, actual: %d", width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepeatToFill(t *testing.T) {
|
||||||
|
if RepeatToFill("abcde", 10, 50) != strings.Repeat("abcde", 5) {
|
||||||
|
t.Error("Expected:", strings.Repeat("abcde", 5))
|
||||||
|
}
|
||||||
|
if RepeatToFill("abcde", 10, 42) != strings.Repeat("abcde", 4)+"abcde"[:2] {
|
||||||
|
t.Error("Expected:", strings.Repeat("abcde", 4)+"abcde"[:2])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringWidth(t *testing.T) {
|
||||||
|
w := StringWidth("─")
|
||||||
|
if w != 1 {
|
||||||
|
t.Errorf("Expected: %d, Actual: %d", 1, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// +build !windows
|
//go:build !windows
|
||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
@@ -6,15 +6,31 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecCommand executes the given command with $SHELL
|
// ExecCommand executes the given command with $SHELL
|
||||||
func ExecCommand(command string) *exec.Cmd {
|
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||||
shell := os.Getenv("SHELL")
|
shell := os.Getenv("SHELL")
|
||||||
if len(shell) == 0 {
|
if len(shell) == 0 {
|
||||||
shell = "sh"
|
shell = "sh"
|
||||||
}
|
}
|
||||||
return exec.Command(shell, "-c", command)
|
return ExecCommandWith(shell, command, setpgid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCommandWith executes the given command with the specified shell
|
||||||
|
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
||||||
|
cmd := exec.Command(shell, "-c", command)
|
||||||
|
if setpgid {
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// KillCommand kills the process for the given command
|
||||||
|
func KillCommand(cmd *exec.Cmd) error {
|
||||||
|
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWindows returns true on Windows
|
// IsWindows returns true on Windows
|
||||||
@@ -22,7 +38,7 @@ func IsWindows() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNonBlock executes syscall.SetNonblock on file descriptor
|
// SetNonblock executes syscall.SetNonblock on file descriptor
|
||||||
func SetNonblock(file *os.File, nonblock bool) {
|
func SetNonblock(file *os.File, nonblock bool) {
|
||||||
syscall.SetNonblock(int(file.Fd()), nonblock)
|
syscall.SetNonblock(int(file.Fd()), nonblock)
|
||||||
}
|
}
|
||||||
@@ -31,3 +47,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
|||||||
func Read(fd int, b []byte) (int, error) {
|
func Read(fd int, b []byte) (int, error) {
|
||||||
return syscall.Read(int(fd), b)
|
return syscall.Read(int(fd), b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetStdin(file *os.File) {
|
||||||
|
unix.Dup2(int(file.Fd()), 0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +1,70 @@
|
|||||||
// +build windows
|
//go:build windows
|
||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/mattn/go-shellwords"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var shellPath atomic.Value
|
||||||
|
|
||||||
// ExecCommand executes the given command with $SHELL
|
// ExecCommand executes the given command with $SHELL
|
||||||
func ExecCommand(command string) *exec.Cmd {
|
func ExecCommand(command string, setpgid bool) *exec.Cmd {
|
||||||
args, _ := shellwords.Parse(command)
|
var shell string
|
||||||
allArgs := make([]string, len(args)+1)
|
if cached := shellPath.Load(); cached != nil {
|
||||||
allArgs[0] = "/c"
|
shell = cached.(string)
|
||||||
copy(allArgs[1:], args)
|
} else {
|
||||||
return exec.Command("cmd", allArgs...)
|
shell = os.Getenv("SHELL")
|
||||||
|
if len(shell) == 0 {
|
||||||
|
shell = "cmd"
|
||||||
|
} else if strings.Contains(shell, "/") {
|
||||||
|
out, err := exec.Command("cygpath", "-w", shell).Output()
|
||||||
|
if err == nil {
|
||||||
|
shell = strings.Trim(string(out), "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shellPath.Store(shell)
|
||||||
|
}
|
||||||
|
return ExecCommandWith(shell, command, setpgid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCommandWith executes the given command with the specified shell
|
||||||
|
// FIXME: setpgid is unused. We set it in the Unix implementation so that we
|
||||||
|
// can kill preview process with its child processes at once.
|
||||||
|
// NOTE: For "powershell", we should ideally set output encoding to UTF8,
|
||||||
|
// but it is left as is now because no adverse effect has been observed.
|
||||||
|
func ExecCommandWith(shell string, command string, setpgid bool) *exec.Cmd {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
if strings.Contains(shell, "cmd") {
|
||||||
|
cmd = exec.Command(shell)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: false,
|
||||||
|
CmdLine: fmt.Sprintf(` /v:on/s/c "%s"`, command),
|
||||||
|
CreationFlags: 0,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(shell, "pwsh") || strings.Contains(shell, "powershell") {
|
||||||
|
cmd = exec.Command(shell, "-NoProfile", "-Command", command)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command(shell, "-c", command)
|
||||||
|
}
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||||
|
HideWindow: false,
|
||||||
|
CreationFlags: 0,
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// KillCommand kills the process for the given command
|
||||||
|
func KillCommand(cmd *exec.Cmd) error {
|
||||||
|
return cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsWindows returns true on Windows
|
// IsWindows returns true on Windows
|
||||||
@@ -24,7 +72,7 @@ func IsWindows() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetNonBlock executes syscall.SetNonblock on file descriptor
|
// SetNonblock executes syscall.SetNonblock on file descriptor
|
||||||
func SetNonblock(file *os.File, nonblock bool) {
|
func SetNonblock(file *os.File, nonblock bool) {
|
||||||
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
|
syscall.SetNonblock(syscall.Handle(file.Fd()), nonblock)
|
||||||
}
|
}
|
||||||
@@ -33,3 +81,7 @@ func SetNonblock(file *os.File, nonblock bool) {
|
|||||||
func Read(fd int, b []byte) (int, error) {
|
func Read(fd int, b []byte) (int, error) {
|
||||||
return syscall.Read(syscall.Handle(fd), b)
|
return syscall.Read(syscall.Handle(fd), b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetStdin(file *os.File) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ Execute (Setup):
|
|||||||
|
|
||||||
Execute (fzf#run with dir option):
|
Execute (fzf#run with dir option):
|
||||||
let cwd = getcwd()
|
let cwd = getcwd()
|
||||||
let result = fzf#run({ 'options': '--filter=vdr', 'dir': g:dir })
|
let result = fzf#run({ 'source': 'git ls-files', 'options': '--filter=vdr', 'dir': g:dir })
|
||||||
AssertEqual ['fzf.vader'], result
|
AssertEqual ['fzf.vader'], result
|
||||||
|
AssertEqual 0, haslocaldir()
|
||||||
AssertEqual getcwd(), cwd
|
AssertEqual getcwd(), cwd
|
||||||
|
|
||||||
let result = sort(fzf#run({ 'options': '--filter e', 'dir': g:dir }))
|
execute 'lcd' fnameescape(cwd)
|
||||||
|
let result = sort(fzf#run({ 'source': 'git ls-files', 'options': '--filter e', 'dir': g:dir }))
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||||
|
AssertEqual 1, haslocaldir()
|
||||||
AssertEqual getcwd(), cwd
|
AssertEqual getcwd(), cwd
|
||||||
|
|
||||||
Execute (fzf#run with Funcref command):
|
Execute (fzf#run with Funcref command):
|
||||||
@@ -19,7 +22,7 @@ Execute (fzf#run with Funcref command):
|
|||||||
function! g:FzfTest(e)
|
function! g:FzfTest(e)
|
||||||
call add(g:ret, a:e)
|
call add(g:ret, a:e)
|
||||||
endfunction
|
endfunction
|
||||||
let result = sort(fzf#run({ 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
let result = sort(fzf#run({ 'source': 'git ls-files', 'sink': function('g:FzfTest'), 'options': '--filter e', 'dir': g:dir }))
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
AssertEqual ['fzf.vader', 'test_go.rb'], result
|
||||||
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
AssertEqual ['fzf.vader', 'test_go.rb'], sort(g:ret)
|
||||||
|
|
||||||
@@ -56,11 +59,11 @@ Execute (Incomplete fzf#run with dir option and autochdir):
|
|||||||
" No change in working directory even if &acd is set
|
" No change in working directory even if &acd is set
|
||||||
AssertEqual cwd, getcwd()
|
AssertEqual cwd, getcwd()
|
||||||
|
|
||||||
Execute (fzf#run with dir option and autochdir):
|
Execute (FIXME: fzf#run with dir option and autochdir):
|
||||||
set acd
|
set acd
|
||||||
let cwd = getcwd()
|
|
||||||
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
|
call fzf#run({'source': ['/foobar'], 'sink': 'e', 'dir': '/tmp', 'options': '-1'})
|
||||||
" Working directory changed due to &acd
|
" Working directory changed due to &acd
|
||||||
|
AssertEqual '/foobar', expand('%')
|
||||||
AssertEqual '/', getcwd()
|
AssertEqual '/', getcwd()
|
||||||
|
|
||||||
Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
|
Execute (fzf#run with dir option and autochdir when final cwd is same as dir):
|
||||||
@@ -140,7 +143,7 @@ Execute (fzf#wrap):
|
|||||||
let g:fzf_history_dir = '/tmp'
|
let g:fzf_history_dir = '/tmp'
|
||||||
let opts = fzf#wrap('foobar', {'options': '--color light'})
|
let opts = fzf#wrap('foobar', {'options': '--color light'})
|
||||||
Log opts
|
Log opts
|
||||||
Assert opts.options =~ '--history /tmp/foobar'
|
Assert opts.options =~ "--history '/tmp/foobar'"
|
||||||
Assert opts.options =~ '--color light'
|
Assert opts.options =~ '--color light'
|
||||||
|
|
||||||
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
let g:fzf_colors = { 'fg': ['fg', 'Error'] }
|
||||||
@@ -149,21 +152,23 @@ Execute (fzf#wrap):
|
|||||||
|
|
||||||
Execute (fzf#shellescape with sh):
|
Execute (fzf#shellescape with sh):
|
||||||
AssertEqual '''''', fzf#shellescape('', 'sh')
|
AssertEqual '''''', fzf#shellescape('', 'sh')
|
||||||
|
AssertEqual '''\''', fzf#shellescape('\', 'sh')
|
||||||
AssertEqual '''""''', fzf#shellescape('""', 'sh')
|
AssertEqual '''""''', fzf#shellescape('""', 'sh')
|
||||||
AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh')
|
AssertEqual '''foobar>''', fzf#shellescape('foobar>', 'sh')
|
||||||
AssertEqual '''\"''', fzf#shellescape('\"', 'sh')
|
AssertEqual '''\\\"\\\''', fzf#shellescape('\\\"\\\', 'sh')
|
||||||
AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh')
|
AssertEqual '''echo ''\''''a''\'''' && echo ''\''''b''\''''''', fzf#shellescape('echo ''a'' && echo ''b''', 'sh')
|
||||||
|
|
||||||
Execute (fzf#shellescape with cmd.exe):
|
Execute (fzf#shellescape with cmd.exe):
|
||||||
AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe')
|
AssertEqual '^"^"', fzf#shellescape('', 'cmd.exe')
|
||||||
|
AssertEqual '^"\\^"', fzf#shellescape('\', 'cmd.exe')
|
||||||
AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe')
|
AssertEqual '^"\^"\^"^"', fzf#shellescape('""', 'cmd.exe')
|
||||||
AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe')
|
AssertEqual '^"foobar^>^"', fzf#shellescape('foobar>', 'cmd.exe')
|
||||||
AssertEqual '^"\\\^"\\^"', fzf#shellescape('\\\\\\\\"\', 'cmd.exe')
|
AssertEqual '^"\\\\\\\^"\\\\\\^"', fzf#shellescape('\\\"\\\', 'cmd.exe')
|
||||||
AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe')
|
AssertEqual '^"echo ''a'' ^&^& echo ''b''^"', fzf#shellescape('echo ''a'' && echo ''b''', 'cmd.exe')
|
||||||
|
|
||||||
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
|
AssertEqual '^"C:\Program Files ^(x86^)\\^"', fzf#shellescape('C:\Program Files (x86)\', 'cmd.exe')
|
||||||
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
|
AssertEqual '^"C:/Program Files ^(x86^)/^"', fzf#shellescape('C:/Program Files (x86)/', 'cmd.exe')
|
||||||
" AssertEqual '^"%%USERPROFILE%%^", fzf#shellescape('%USERPROFILE%', 'cmd.exe')
|
AssertEqual '^"%%USERPROFILE%%^"', fzf#shellescape('%USERPROFILE%', 'cmd.exe')
|
||||||
|
|
||||||
Execute (Cleanup):
|
Execute (Cleanup):
|
||||||
unlet g:dir
|
unlet g:dir
|
||||||
|
|||||||
3171
test/test_go.rb
Normal file → Executable file
3171
test/test_go.rb
Normal file → Executable file
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