mirror of
https://github.com/lotabout/skim.git
synced 2026-09-10 07:16:23 -04:00
Compare commits
837 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3b09a687 | ||
|
|
672639fd80 | ||
|
|
a177153700 | ||
|
|
801d1ab036 | ||
|
|
17c517fd02 | ||
|
|
0392eeb70d | ||
|
|
085f86b127 | ||
|
|
16c6f0b386 | ||
|
|
d7b799b9e6 | ||
|
|
a9b0fd4393 | ||
|
|
a53ada910a | ||
|
|
4b962af9ed | ||
|
|
e8f5b3ae65 | ||
|
|
d699733053 | ||
|
|
756b084841 | ||
|
|
52275d3a3e | ||
|
|
2ec03e630c | ||
|
|
c0786aa9d3 | ||
|
|
bb80ac138e | ||
|
|
9de4825e51 | ||
|
|
1b80cff3f7 | ||
|
|
94022921a3 | ||
|
|
59aecd4f2b | ||
|
|
fe45ed36b7 | ||
|
|
01be03f0d5 | ||
|
|
733aaeb148 | ||
|
|
f6a302390c | ||
|
|
5ff1bda145 | ||
|
|
070d0849b2 | ||
|
|
5fc3b0c1a0 | ||
|
|
a5bd0fefa6 | ||
|
|
f3c03000b3 | ||
|
|
1dead2b3c6 | ||
|
|
b17e93890a | ||
|
|
d99d6cecc7 | ||
|
|
7429ca3ec2 | ||
|
|
bc379b5f47 | ||
|
|
d2ae57625f | ||
|
|
c398374a85 | ||
|
|
2303a15b0c | ||
|
|
794ddddb71 | ||
|
|
4745613342 | ||
|
|
29370cc004 | ||
|
|
cbfb7fac0f | ||
|
|
e929c512e3 | ||
|
|
88ce5b97ac | ||
|
|
9016a712a2 | ||
|
|
0415ea7bcd | ||
|
|
3644242897 | ||
|
|
b2a732efa0 | ||
|
|
4f4cd15a42 | ||
|
|
df683e34a7 | ||
|
|
d4c878893b | ||
|
|
776d708ede | ||
|
|
76a27ed80c | ||
|
|
d7e294eb7d | ||
|
|
5c1c3379fb | ||
|
|
24a24619ea | ||
|
|
6dbe37a7fa | ||
|
|
6f94d80973 | ||
|
|
4d093a42e1 | ||
|
|
3c3f26ebc9 | ||
|
|
ad97bfa7de | ||
|
|
ff98133bbd | ||
|
|
dce26d622a | ||
|
|
55aa7dacd5 | ||
|
|
57a7487323 | ||
|
|
bb6c03f377 | ||
|
|
21f7ef8795 | ||
|
|
f87ce2075d | ||
|
|
29ca6f22bd | ||
|
|
b41a724a2a | ||
|
|
04c0a5b6c8 | ||
|
|
9be7392911 | ||
|
|
b1aa46917b | ||
|
|
4047a5e3fd | ||
|
|
95c360996a | ||
|
|
09479547eb | ||
|
|
44b9cf2011 | ||
|
|
8ea0afb63e | ||
|
|
974bd267cc | ||
|
|
815a9b4f94 | ||
|
|
c4bc5e1be4 | ||
|
|
ef59339727 | ||
|
|
1effd9a69d | ||
|
|
0a4d4b2ea0 | ||
|
|
026a622a9d | ||
|
|
8a1f2783a8 | ||
|
|
5a2dde8018 | ||
|
|
6c08be00c9 | ||
|
|
f2ca01e187 | ||
|
|
aeba919fab | ||
|
|
bfec6e198d | ||
|
|
5f4798325a | ||
|
|
d14407196b | ||
|
|
7d4502201f | ||
|
|
7e2cdf3c8e | ||
|
|
8187a12196 | ||
|
|
b56eed4125 | ||
|
|
e85cf6b708 | ||
|
|
7994e44412 | ||
|
|
fbd426f569 | ||
|
|
00165fbc98 | ||
|
|
0de1a2acbb | ||
|
|
42fa8e143b | ||
|
|
a5ca0c8885 | ||
|
|
1a223219fb | ||
|
|
b5b11a472f | ||
|
|
6ddf0314e9 | ||
|
|
3f9e5e5687 | ||
|
|
91e9cbe512 | ||
|
|
9ec676ae88 | ||
|
|
fef3cfd909 | ||
|
|
7e6115070b | ||
|
|
5ac979d42d | ||
|
|
c7860a1a16 | ||
|
|
a885c0b4fe | ||
|
|
cb8becdc77 | ||
|
|
0ce5764b11 | ||
|
|
d3592461fd | ||
|
|
fd7e8ac101 | ||
|
|
32c1d105c9 | ||
|
|
168ac72776 | ||
|
|
e361a8dec0 | ||
|
|
ec77fa7245 | ||
|
|
c3bac9f697 | ||
|
|
19d2640fe1 | ||
|
|
8d92d05b8e | ||
|
|
0457fd2e62 | ||
|
|
6fb2cc5feb | ||
|
|
87a2536193 | ||
|
|
44e8aef062 | ||
|
|
b9b64e6591 | ||
|
|
cf49f60d96 | ||
|
|
bf63404ad5 | ||
|
|
ca986f444c | ||
|
|
98f9612e97 | ||
|
|
d6f2800b64 | ||
|
|
de60d945b0 | ||
|
|
475dd515e5 | ||
|
|
aa5cf82ef6 | ||
|
|
2cf64ebd46 | ||
|
|
7375d30bf1 | ||
|
|
4997e2d253 | ||
|
|
e531182a2b | ||
|
|
c28d162890 | ||
|
|
ada7cc6264 | ||
|
|
80c8e1a62a | ||
|
|
4ee04e61dd | ||
|
|
bf8642a18e | ||
|
|
a95ad14142 | ||
|
|
797ca70da5 | ||
|
|
1eb51eecca | ||
|
|
931cd9afc9 | ||
|
|
06bfbc3e13 | ||
|
|
036fcc9455 | ||
|
|
e227cafda2 | ||
|
|
40f0e29448 | ||
|
|
6b355e144a | ||
|
|
a18e88aafd | ||
|
|
91e090e813 | ||
|
|
fb17c90704 | ||
|
|
39b120fbd8 | ||
|
|
230c2c7bd7 | ||
|
|
e6a997e3af | ||
|
|
71f7ead51c | ||
|
|
040fe9ed2d | ||
|
|
7ee36fdac3 | ||
|
|
1a10e405f1 | ||
|
|
94c132a94f | ||
|
|
987d2a5ca7 | ||
|
|
7e5d441e4c | ||
|
|
9d12e9d420 | ||
|
|
65c2dcde11 | ||
|
|
6332195605 | ||
|
|
eb5d5df44b | ||
|
|
78231e5244 | ||
|
|
90e23eb86e | ||
|
|
3757038aa4 | ||
|
|
cb037fbe27 | ||
|
|
2a77f6b4c1 | ||
|
|
7654782d64 | ||
|
|
a8c56626bc | ||
|
|
b9c3de302b | ||
|
|
c0a9be6b5f | ||
|
|
d40af611b6 | ||
|
|
1fd6292bbe | ||
|
|
b74ed55946 | ||
|
|
257f0185b0 | ||
|
|
9626606745 | ||
|
|
96f3320b15 | ||
|
|
7ae0802979 | ||
|
|
5028ef4d0b | ||
|
|
926c76ee16 | ||
|
|
a1f420a682 | ||
|
|
85c6964da5 | ||
|
|
4269838f32 | ||
|
|
95f09a455a | ||
|
|
5b0323a9fb | ||
|
|
86d824e6cf | ||
|
|
ab514a54c9 | ||
|
|
0ac1ac878d | ||
|
|
7d2efaf87f | ||
|
|
9c9e15c97b | ||
|
|
396f2953a9 | ||
|
|
349533fa1b | ||
|
|
e891a49ebe | ||
|
|
0bf6e8fb32 | ||
|
|
17adb040ac | ||
|
|
3a4b958f51 | ||
|
|
ffddee2d54 | ||
|
|
07b40a3281 | ||
|
|
9ef09b7da6 | ||
|
|
c5a3310ac4 | ||
|
|
b52dc0cfac | ||
|
|
600d78815e | ||
|
|
fa003bf7c5 | ||
|
|
c65274441a | ||
|
|
b47bda8902 | ||
|
|
205fdf610a | ||
|
|
5a56391190 | ||
|
|
d87a6a34bf | ||
|
|
7e462aa33e | ||
|
|
e313acbffc | ||
|
|
5e8ff26024 | ||
|
|
422b9dd0d7 | ||
|
|
5d9b4df1e5 | ||
|
|
684bbd2159 | ||
|
|
5daf7823eb | ||
|
|
f2388c0985 | ||
|
|
2161010be3 | ||
|
|
21e09295d6 | ||
|
|
f9e7760fdc | ||
|
|
c7da16f167 | ||
|
|
1a90f4f52f | ||
|
|
003243a93a | ||
|
|
d7117fbbc2 | ||
|
|
5c11314bb6 | ||
|
|
943e073cee | ||
|
|
1b2da17e88 | ||
|
|
091c7e8284 | ||
|
|
5f1d49d997 | ||
|
|
6b128b183b | ||
|
|
40c2ee6c41 | ||
|
|
2a7bd1db11 | ||
|
|
e7183acc70 | ||
|
|
0d399ea4e7 | ||
|
|
c8de29e239 | ||
|
|
480e147377 | ||
|
|
65ba8cffaa | ||
|
|
4147eccb94 | ||
|
|
ad91558749 | ||
|
|
19bfd34aaf | ||
|
|
5d9743f198 | ||
|
|
65b7c86d7e | ||
|
|
1e118caec6 | ||
|
|
a766762b5a | ||
|
|
98a9184055 | ||
|
|
de7eab7619 | ||
|
|
b02ef7b878 | ||
|
|
17c801bb07 | ||
|
|
4749154e5b | ||
|
|
4c12048762 | ||
|
|
267fb0c72e | ||
|
|
e9fb1877c0 | ||
|
|
6bb1008101 | ||
|
|
d2f0a7f952 | ||
|
|
149b47f37d | ||
|
|
d11e627ba6 | ||
|
|
495b9ae611 | ||
|
|
9d0fb5eec9 | ||
|
|
a4f32c5533 | ||
|
|
1b23121956 | ||
|
|
87b14ef577 | ||
|
|
d48f3f5e32 | ||
|
|
db507d6f59 | ||
|
|
812525d582 | ||
|
|
ef041436f7 | ||
|
|
c051be779e | ||
|
|
e23a259451 | ||
|
|
fdd93644b4 | ||
|
|
00d32b6f9d | ||
|
|
b3ebf9db92 | ||
|
|
a08f6ac9b3 | ||
|
|
719de73182 | ||
|
|
8a8b2c6149 | ||
|
|
b1b35d387f | ||
|
|
d245fdd002 | ||
|
|
e93c152d8d | ||
|
|
827688b63c | ||
|
|
85b811ab3b | ||
|
|
1c1279514b | ||
|
|
ee0d6dcd19 | ||
|
|
2ac22b6344 | ||
|
|
015fd28db6 | ||
|
|
b96c65507e | ||
|
|
5446f20e8c | ||
|
|
d45f4c41fe | ||
|
|
9f9771f79a | ||
|
|
77144056ca | ||
|
|
f31a844873 | ||
|
|
099ab75f8a | ||
|
|
923b018aa3 | ||
|
|
c0a368b901 | ||
|
|
edaea9f34a | ||
|
|
9654ad321a | ||
|
|
55b50cb3bf | ||
|
|
c7c50a4896 | ||
|
|
2b71c2bad9 | ||
|
|
d29c30c756 | ||
|
|
a99cae3ca2 | ||
|
|
13c1b70d61 | ||
|
|
2710e648c9 | ||
|
|
fbccdae64e | ||
|
|
30ef368c3e | ||
|
|
e5a0375e4a | ||
|
|
1d70a823e1 | ||
|
|
040fc6850e | ||
|
|
070b8347e4 | ||
|
|
5eb9703547 | ||
|
|
5d938b05fe | ||
|
|
0d67e1a126 | ||
|
|
960a6ca064 | ||
|
|
788eb81334 | ||
|
|
6a6fea3e3a | ||
|
|
8cf06ab256 | ||
|
|
94c4bd51fe | ||
|
|
9ab5c7356a | ||
|
|
d1494779ec | ||
|
|
c0f8cd4318 | ||
|
|
7ff7b01031 | ||
|
|
ec0658a9d4 | ||
|
|
a6613e4559 | ||
|
|
bc9d159e30 | ||
|
|
c5a95a3845 | ||
|
|
4e98ce34b3 | ||
|
|
6f548ae252 | ||
|
|
505c5deae7 | ||
|
|
f4264c7cbe | ||
|
|
c2ab623851 | ||
|
|
856c65bfc8 | ||
|
|
d55c9b0487 | ||
|
|
1536eb9657 | ||
|
|
47e547a9ad | ||
|
|
eef3e8dd28 | ||
|
|
653a3eb6b7 | ||
|
|
76812361b2 | ||
|
|
870267bda6 | ||
|
|
96a4c69646 | ||
|
|
2cfd33ccc9 | ||
|
|
16ef0f4a27 | ||
|
|
1c538e36b5 | ||
|
|
554896db64 | ||
|
|
ebab1d70c4 | ||
|
|
432a692e68 | ||
|
|
1b02d824c1 | ||
|
|
efb2ea8be5 | ||
|
|
c8acb4b976 | ||
|
|
3c5b2d4209 | ||
|
|
ba50acd7f6 | ||
|
|
662e92f835 | ||
|
|
79672e348f | ||
|
|
773293974e | ||
|
|
c5f2a3bdc9 | ||
|
|
1ee19d3caa | ||
|
|
1af0f643cb | ||
|
|
d78ab85e41 | ||
|
|
0792967ea6 | ||
|
|
3b145cce7f | ||
|
|
d7b61e34cb | ||
|
|
6d0c310bea | ||
|
|
2b5a6ac66f | ||
|
|
6848e5afb6 | ||
|
|
6fb14848fe | ||
|
|
66b12e7d43 | ||
|
|
9e80134abf | ||
|
|
d8eda628fe | ||
|
|
35a61fd147 | ||
|
|
540c4d7245 | ||
|
|
4d4a33542d | ||
|
|
c5eb948500 | ||
|
|
3b0e31387b | ||
|
|
b3a221b740 | ||
|
|
52bf6a247b | ||
|
|
3edc307768 | ||
|
|
5fa0ffa884 | ||
|
|
102f7c79f2 | ||
|
|
9d486b78fa | ||
|
|
c2c8bb6fe1 | ||
|
|
da74c01404 | ||
|
|
223414091a | ||
|
|
47c5cf829c | ||
|
|
f03894c25b | ||
|
|
33833ff5a6 | ||
|
|
c12b32cdc3 | ||
|
|
3e0cbd1e06 | ||
|
|
484eb6a75e | ||
|
|
8ae57f6d01 | ||
|
|
93ffbcea88 | ||
|
|
931ae8bc91 | ||
|
|
d17f5bd95d | ||
|
|
6f2412dde3 | ||
|
|
be121a7be4 | ||
|
|
7b6e0fd300 | ||
|
|
0c01f31898 | ||
|
|
128fc245f1 | ||
|
|
acde4f82ca | ||
|
|
8e25217a01 | ||
|
|
b144416d80 | ||
|
|
f608ef9d24 | ||
|
|
7c9b7194e0 | ||
|
|
e25e166003 | ||
|
|
8b4a048af9 | ||
|
|
577025224e | ||
|
|
3782ae85e1 | ||
|
|
1bf7b5266a | ||
|
|
0de00e972e | ||
|
|
1fef63e720 | ||
|
|
d3efe18b0d | ||
|
|
e4dc4d02f0 | ||
|
|
8e3d143819 | ||
|
|
0a8087d8bb | ||
|
|
15b6a14e0f | ||
|
|
60f4c1d17b | ||
|
|
48e60a932c | ||
|
|
21586f911c | ||
|
|
2dddd43ca0 | ||
|
|
c7ae5bf1a2 | ||
|
|
56f3ff76c4 | ||
|
|
56536b8eec | ||
|
|
fe3f470b8a | ||
|
|
3dc64cfb90 | ||
|
|
7e962392cd | ||
|
|
7a31fca789 | ||
|
|
26d84fae9b | ||
|
|
c1a7ae610a | ||
|
|
94c4d76ca2 | ||
|
|
bd7af947e3 | ||
|
|
19d19eb6c6 | ||
|
|
904ec2fa3e | ||
|
|
c364c5f128 | ||
|
|
b8dc423f9b | ||
|
|
00b93d4586 | ||
|
|
59f4f48395 | ||
|
|
96b25952a4 | ||
|
|
06c3a13ba7 | ||
|
|
cbd1765881 | ||
|
|
706620c5b7 | ||
|
|
aa4a774d68 | ||
|
|
48404fe4dd | ||
|
|
9c430c1381 | ||
|
|
8dbfd3ffbe | ||
|
|
7b0f5683df | ||
|
|
87ac95488e | ||
|
|
6907d0648e | ||
|
|
58ec498450 | ||
|
|
0f8268d6c2 | ||
|
|
d052b725b1 | ||
|
|
0a5e6bf7ee | ||
|
|
1c2606fad8 | ||
|
|
992814ce50 | ||
|
|
f29f6194b8 | ||
|
|
913deaf678 | ||
|
|
1c1dffb734 | ||
|
|
727be81929 | ||
|
|
3f3f2da83c | ||
|
|
0f4b13e8c4 | ||
|
|
e73e7f841b | ||
|
|
745f26b931 | ||
|
|
71b82d0f58 | ||
|
|
e66813518d | ||
|
|
ca229dba01 | ||
|
|
4f63ec5543 | ||
|
|
cd929542a2 | ||
|
|
cdc3286b85 | ||
|
|
f68bcb36f1 | ||
|
|
6b555b0d27 | ||
|
|
880b77fd40 | ||
|
|
90d9355dca | ||
|
|
2931b295b8 | ||
|
|
7a1744217b | ||
|
|
9cfbc0a470 | ||
|
|
6ffe7c55a4 | ||
|
|
d36b67fb43 | ||
|
|
f5ba2f48cc | ||
|
|
8573f36254 | ||
|
|
97f09721c2 | ||
|
|
8df581b122 | ||
|
|
b10acf48eb | ||
|
|
cc03874b8f | ||
|
|
991fbee680 | ||
|
|
a3e88a9f65 | ||
|
|
e0d01690d3 | ||
|
|
ed2b3fa811 | ||
|
|
4be4e5c266 | ||
|
|
df3480bcc8 | ||
|
|
5bba84f2cc | ||
|
|
689157af20 | ||
|
|
957c435709 | ||
|
|
c70ed2042f | ||
|
|
0a60090a6b | ||
|
|
d767bbe70c | ||
|
|
2bab7d7e31 | ||
|
|
7980972d1d | ||
|
|
b83e0311ec | ||
|
|
e2eacdf12b | ||
|
|
103e90fdfe | ||
|
|
708f9db876 | ||
|
|
39410102a7 | ||
|
|
8e686f9db4 | ||
|
|
074f4c1de7 | ||
|
|
df455aa614 | ||
|
|
8a29f270a2 | ||
|
|
0a822204ca | ||
|
|
ef4bae6b29 | ||
|
|
14241b3bb8 | ||
|
|
82f7807eaa | ||
|
|
f5f87e41b1 | ||
|
|
a8dcc7ec4c | ||
|
|
4ed7f0cbed | ||
|
|
8df7893f76 | ||
|
|
0df30b83fe | ||
|
|
9c96d48e55 | ||
|
|
a7e5803c80 | ||
|
|
a4b2ef550c | ||
|
|
8c3dd32834 | ||
|
|
08f3d12ab5 | ||
|
|
9bc221b14c | ||
|
|
adc4298f94 | ||
|
|
60b8ad1491 | ||
|
|
6a42c359e8 | ||
|
|
ac0ff31ba8 | ||
|
|
1b008f5b7f | ||
|
|
111039eefb | ||
|
|
76040003ca | ||
|
|
661ba9f28a | ||
|
|
7d9d682270 | ||
|
|
c5395bfc88 | ||
|
|
19ea887465 | ||
|
|
d15b328146 | ||
|
|
87d2ce4ba8 | ||
|
|
99134b9af9 | ||
|
|
4ba771cf99 | ||
|
|
3bd7f3c774 | ||
|
|
aad912519e | ||
|
|
bda413b83e | ||
|
|
177fed3622 | ||
|
|
f87ff6740b | ||
|
|
1952435b9b | ||
|
|
0a206adf23 | ||
|
|
73fd406827 | ||
|
|
e93e65c352 | ||
|
|
ba908a8243 | ||
|
|
a51e21c34c | ||
|
|
9d55c89d10 | ||
|
|
ed1c48d6d0 | ||
|
|
a9ba87b892 | ||
|
|
9aa27a8f25 | ||
|
|
e99de62297 | ||
|
|
4f9de7a115 | ||
|
|
dc45aef3e2 | ||
|
|
1e9e019885 | ||
|
|
c7035857b9 | ||
|
|
faeb2b4508 | ||
|
|
a3df0be787 | ||
|
|
d134e8d31a | ||
|
|
284725d651 | ||
|
|
fb896690b0 | ||
|
|
9a4b794122 | ||
|
|
8e2cf254dc | ||
|
|
fac63d5cb1 | ||
|
|
cdde8a9032 | ||
|
|
81799d1f65 | ||
|
|
47c5e1e54e | ||
|
|
301a0dabeb | ||
|
|
afc2abdbda | ||
|
|
6457a2700c | ||
|
|
a88d9ae10c | ||
|
|
9f02f95fbe | ||
|
|
b696fbbc1b | ||
|
|
d7f07acafe | ||
|
|
08e025bd00 | ||
|
|
c3e8d8cc9e | ||
|
|
25cd4cdd03 | ||
|
|
5698c6d8fd | ||
|
|
4102e5aaa1 | ||
|
|
b4138a5029 | ||
|
|
1dcdf06b5c | ||
|
|
226d9951fa | ||
|
|
5152be1945 | ||
|
|
03da73b0f4 | ||
|
|
68a9dda12e | ||
|
|
9f928a0dc3 | ||
|
|
950e7cd86f | ||
|
|
efe7789eff | ||
|
|
44342c69b8 | ||
|
|
55e7b51af9 | ||
|
|
69d98ef269 | ||
|
|
da499af34f | ||
|
|
c2f98e0c6e | ||
|
|
bf49f68c92 | ||
|
|
17d77f40e2 | ||
|
|
6d1aa4236c | ||
|
|
44bc7965a4 | ||
|
|
4fa222a9d9 | ||
|
|
0fabdf9704 | ||
|
|
8a21a9672f | ||
|
|
28e3034399 | ||
|
|
a4f06514f4 | ||
|
|
fc141fcb9a | ||
|
|
a55e9c97ac | ||
|
|
c9282818c6 | ||
|
|
74b2ea971d | ||
|
|
b265179f20 | ||
|
|
422bd35a57 | ||
|
|
7914c6f291 | ||
|
|
fdbf4ff380 | ||
|
|
290a9231c9 | ||
|
|
920e7743e9 | ||
|
|
8c35953121 | ||
|
|
37e96629c3 | ||
|
|
e9cd731969 | ||
|
|
3eb55911be | ||
|
|
1121509428 | ||
|
|
9db1478ab1 | ||
|
|
a3869e55c1 | ||
|
|
0992947bd5 | ||
|
|
3cca5c1823 | ||
|
|
c171f70389 | ||
|
|
04d79e664e | ||
|
|
1abf545ed9 | ||
|
|
bdaac9c8ff | ||
|
|
a6e233d3ca | ||
|
|
3d99879d4e | ||
|
|
17c261af1c | ||
|
|
7a7a1b4e3c | ||
|
|
e3637e068e | ||
|
|
3ff3832881 | ||
|
|
3f6a6bed85 | ||
|
|
621fa1e4c5 | ||
|
|
04541d515b | ||
|
|
db30b25241 | ||
|
|
b1dfcc7e30 | ||
|
|
dd41167d42 | ||
|
|
0963b974ff | ||
|
|
f24b0a9bbd | ||
|
|
246c8d86f3 | ||
|
|
993ba4a3ae | ||
|
|
a534f3593b | ||
|
|
26dc2f6c88 | ||
|
|
b624463543 | ||
|
|
358135342e | ||
|
|
c1679cce68 | ||
|
|
4eda4c40fd | ||
|
|
d6edf78474 | ||
|
|
5c4d21d55d | ||
|
|
a4bad87721 | ||
|
|
a4ffca712b | ||
|
|
c22515ef39 | ||
|
|
2d36bbac61 | ||
|
|
cc78aec4aa | ||
|
|
e995715dcd | ||
|
|
f7f8a5c259 | ||
|
|
36c47578f2 | ||
|
|
3277354116 | ||
|
|
05e7842268 | ||
|
|
45f0c36175 | ||
|
|
682ea271c7 | ||
|
|
e81e632f2b | ||
|
|
7b70685049 | ||
|
|
f5728dcb6e | ||
|
|
0bc1136316 | ||
|
|
fabf9c4180 | ||
|
|
a6c5df549b | ||
|
|
57bd3c7c76 | ||
|
|
00b04cd6a7 | ||
|
|
03c3529fbf | ||
|
|
c3d4e368cd | ||
|
|
262bce09d7 | ||
|
|
39e7614ba2 | ||
|
|
60b9b19d74 | ||
|
|
099e47641a | ||
|
|
a5b81818d6 | ||
|
|
b496259003 | ||
|
|
8b8920c3f9 | ||
|
|
f60d64b0c0 | ||
|
|
f89a366a5e | ||
|
|
99d2863bac | ||
|
|
5146457f76 | ||
|
|
921667ac73 | ||
|
|
8c10712c16 | ||
|
|
8933d31033 | ||
|
|
1a73d6cda0 | ||
|
|
51893fd4c9 | ||
|
|
4fd81a5a45 | ||
|
|
5ba98dfe75 | ||
|
|
f4a515d2f7 | ||
|
|
0090a564f1 | ||
|
|
77ecb0a298 | ||
|
|
1b8d7c5a7c | ||
|
|
427377a900 | ||
|
|
7212bf24c6 | ||
|
|
c9f9025da9 | ||
|
|
fefff76481 | ||
|
|
ec393a1b7f | ||
|
|
eae1424372 | ||
|
|
cd0c23eb63 | ||
|
|
74ac67058d | ||
|
|
7f0dec4b48 | ||
|
|
d9419c640b | ||
|
|
d7177531f1 | ||
|
|
c28f81cc00 | ||
|
|
68f1c408ce | ||
|
|
f6eb98e514 | ||
|
|
a2d8c3f602 | ||
|
|
be53f1f865 | ||
|
|
bcee1f4c02 | ||
|
|
4144879f00 | ||
|
|
4b4795e9bc | ||
|
|
1bc4653b68 | ||
|
|
7f54f5d5d6 | ||
|
|
f93389fa98 | ||
|
|
e649d084c3 | ||
|
|
24ccef193b | ||
|
|
c1314ea4cc | ||
|
|
a23c3ff90e | ||
|
|
0befe8d206 | ||
|
|
ad909c379f | ||
|
|
33ca4023c1 | ||
|
|
53612a720a | ||
|
|
dc5eb1ee19 | ||
|
|
4b47244922 | ||
|
|
4f1b2e1637 | ||
|
|
bd73f62112 | ||
|
|
63f4e336c7 | ||
|
|
08a8a74716 | ||
|
|
a4f1d90e7a | ||
|
|
32d9e70af4 | ||
|
|
56d2e794e1 | ||
|
|
014903c8d9 | ||
|
|
ab708046e4 | ||
|
|
8a8f273557 | ||
|
|
100c58b888 | ||
|
|
ec57496441 | ||
|
|
7ed9cd5471 | ||
|
|
e9b2ff5126 | ||
|
|
f4708ffa6b | ||
|
|
b99b68ec64 | ||
|
|
d6c6e25a2f | ||
|
|
d90e846053 | ||
|
|
dc457f67bb | ||
|
|
aa03781ef7 | ||
|
|
7df8b77739 | ||
|
|
a61f67ec63 | ||
|
|
064b7b78e2 | ||
|
|
c932a1fe1c | ||
|
|
5ef1b593b0 | ||
|
|
a8869d7d91 | ||
|
|
838ba211c6 | ||
|
|
8222a3f7d2 | ||
|
|
17c38c54e2 | ||
|
|
d95f6bc0cc | ||
|
|
da19402871 | ||
|
|
d6de28274b | ||
|
|
d937372313 | ||
|
|
966d8f50dd | ||
|
|
e0cd6483cf | ||
|
|
054d76048f | ||
|
|
703b224e6e | ||
|
|
81f391db97 | ||
|
|
17f37b8674 | ||
|
|
6b63ea452d | ||
|
|
d68823339f | ||
|
|
70f9e4c25d | ||
|
|
0b699430bd | ||
|
|
2c733f6199 | ||
|
|
bd400934ee | ||
|
|
8a579837da | ||
|
|
a00e78c69d | ||
|
|
291fc34c58 | ||
|
|
46a05168c9 | ||
|
|
cd1f85710c | ||
|
|
3ebe82e6bf | ||
|
|
db8e0be287 | ||
|
|
f9438c390d | ||
|
|
f857e277ac | ||
|
|
75a0b81fcb | ||
|
|
6b735d1273 | ||
|
|
db9f194c12 | ||
|
|
bdd5aeb396 | ||
|
|
08c61964c6 | ||
|
|
589c1bd2a3 | ||
|
|
29c993cd30 | ||
|
|
2c2c776da3 | ||
|
|
67e2f09d46 | ||
|
|
b4e7a9ae2a | ||
|
|
fbb270b772 | ||
|
|
41f139142f | ||
|
|
7b58296dcf | ||
|
|
8b145ad23c | ||
|
|
206843df90 | ||
|
|
9def5d2fce | ||
|
|
a18291495a | ||
|
|
afe16670dc | ||
|
|
d0690b62bd | ||
|
|
a2edae8fc4 | ||
|
|
af9dd2a868 | ||
|
|
4647c25af7 | ||
|
|
d1135f42b5 | ||
|
|
446fb190fa | ||
|
|
04d4544c21 | ||
|
|
35d6296560 | ||
|
|
66a2be6140 | ||
|
|
df54d77226 | ||
|
|
c4ab5e7de1 | ||
|
|
f8ed82f602 | ||
|
|
d634d5f60d | ||
|
|
512ddd949d | ||
|
|
fc18e191ba | ||
|
|
3042d4748e | ||
|
|
2e4fa2f3ca | ||
|
|
c31250622f | ||
|
|
7d922a02a0 | ||
|
|
68cba72cf9 | ||
|
|
a46d41748a | ||
|
|
3dc0c4ee19 | ||
|
|
9e6b50590a | ||
|
|
363efdd725 | ||
|
|
175a8daf4a | ||
|
|
7ca825105e | ||
|
|
f4407dad1e | ||
|
|
f1d17b787e | ||
|
|
7f5381af58 | ||
|
|
be3f129dc6 | ||
|
|
6df0159879 | ||
|
|
4036ac78b8 | ||
|
|
c49fb847e2 |
5
.codegraph/.gitignore
vendored
Normal file
5
.codegraph/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# CodeGraph data files — local to each machine, not for committing.
|
||||
# Ignore everything in .codegraph/ except this file itself, so transient
|
||||
# files (the database, daemon.pid, sockets, logs) never show up in git.
|
||||
*
|
||||
!.gitignore
|
||||
5
.config/insta.yaml
Normal file
5
.config/insta.yaml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
behavior:
|
||||
output: minimal
|
||||
|
||||
test:
|
||||
runner: nextest
|
||||
79
.config/nextest.toml
Normal file
79
.config/nextest.toml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
experimental = ["wrapper-scripts"]
|
||||
|
||||
# The end-to-end tests drive `sk` inside a Zellij session that each test creates
|
||||
# and tears down itself (see tests/common/zellij.rs), so — unlike the previous
|
||||
# tmux-based harness — no shared multiplexer session needs to be set up here.
|
||||
|
||||
# Valgrind wrapper for memory leak detection
|
||||
[scripts.wrapper.valgrind]
|
||||
command = [
|
||||
"valgrind",
|
||||
"--leak-check=full",
|
||||
"--show-leak-kinds=all",
|
||||
"--track-origins=yes",
|
||||
"--error-exitcode=1",
|
||||
"--suppressions=.config/valgrind.supp"
|
||||
]
|
||||
|
||||
# The end-to-end tests each spin up a full Zellij session (server + client +
|
||||
# PTY). Running many concurrently overwhelms the machine and makes the
|
||||
# timing-sensitive tests flaky, so serialize everything in this group while the
|
||||
# rest of the suite keeps running in parallel. (Under raw `cargo test`, pass
|
||||
# `--test-threads=1` for the e2e test binaries instead.)
|
||||
[test-groups]
|
||||
e2e = { max-threads = 1 }
|
||||
|
||||
[profile.default]
|
||||
fail-fast = false
|
||||
retries = 2
|
||||
[[profile.default.overrides]]
|
||||
filter = 'binary(interactive) | binary(listen) | binary(popup) | binary(execute)'
|
||||
test-group = 'e2e'
|
||||
[profile.default.junit]
|
||||
path = "junit.xml"
|
||||
|
||||
[profile.ci]
|
||||
retries = 9
|
||||
[[profile.ci.overrides]]
|
||||
filter = 'binary(interactive) | binary(listen) | binary(popup) | binary(execute)'
|
||||
test-group = 'e2e'
|
||||
|
||||
# Valgrind profile for memory leak detection
|
||||
# Usage: cargo nextest run --profile valgrind
|
||||
#
|
||||
# Note: Valgrind can detect memory leaks but does NOT detect dangling threads.
|
||||
# For thread leak detection, use ThreadSanitizer instead (see below).
|
||||
[profile.valgrind]
|
||||
fail-fast = false
|
||||
retries = 2
|
||||
test-threads = 1 # Run tests serially to avoid interleaved valgrind output
|
||||
[[profile.valgrind.scripts]]
|
||||
platform = "cfg(unix)"
|
||||
run-wrapper = "valgrind"
|
||||
|
||||
# ThreadSanitizer profile for detecting data races and thread issues
|
||||
# Usage:
|
||||
# 1. First build with sanitizer (rebuilds stdlib and all deps):
|
||||
# RUSTFLAGS="-Zsanitizer=thread" cargo +nightly build --tests -Zbuild-std --target x86_64-unknown-linux-gnu
|
||||
# 2. Then run tests:
|
||||
# TSAN_OPTIONS="detect_deadlocks=1" cargo +nightly nextest run --profile tsan --target x86_64-unknown-linux-gnu
|
||||
#
|
||||
# Note: ThreadSanitizer can detect:
|
||||
# - Data races (concurrent unsynchronized access to memory)
|
||||
# - Deadlocks (with TSAN_OPTIONS=detect_deadlocks=1)
|
||||
# - Thread leaks (threads not joined before program exit)
|
||||
#
|
||||
# Requirements:
|
||||
# - Rust nightly (for -Zsanitizer and -Zbuild-std flags)
|
||||
# - The -Zbuild-std flag rebuilds the standard library with ThreadSanitizer
|
||||
# instrumentation to avoid ABI mismatch errors
|
||||
#
|
||||
# Important: This takes a long time on first build as it recompiles everything
|
||||
# including the standard library with ThreadSanitizer instrumentation.
|
||||
#
|
||||
# Environment variables:
|
||||
# TSAN_OPTIONS="detect_deadlocks=1 second_deadlock_stack=1"
|
||||
[profile.tsan]
|
||||
fail-fast = false
|
||||
retries = 3
|
||||
test-threads = 1 # TSan requires running tests serially
|
||||
53
.config/valgrind.supp
Normal file
53
.config/valgrind.supp
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Valgrind suppressions for skim tests
|
||||
# This file suppresses known false positives from Rust stdlib and system libraries
|
||||
|
||||
# Rust std allocations that are intentionally not freed at program exit
|
||||
{
|
||||
rust_std_exit_cleanup
|
||||
Memcheck:Leak
|
||||
...
|
||||
fun:*std*
|
||||
}
|
||||
|
||||
# Thread-local storage cleanup
|
||||
{
|
||||
thread_local_cleanup
|
||||
Memcheck:Leak
|
||||
...
|
||||
fun:pthread_create*
|
||||
}
|
||||
|
||||
# Tokio runtime allocations
|
||||
{
|
||||
tokio_runtime
|
||||
Memcheck:Leak
|
||||
...
|
||||
fun:*tokio*runtime*
|
||||
}
|
||||
|
||||
# Crossterm/terminal allocations
|
||||
{
|
||||
crossterm_terminal
|
||||
Memcheck:Leak
|
||||
...
|
||||
fun:*crossterm*
|
||||
}
|
||||
|
||||
# Libc thread initialization
|
||||
{
|
||||
libc_thread_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:calloc
|
||||
fun:allocate_dtv
|
||||
}
|
||||
|
||||
# DL allocations
|
||||
{
|
||||
dl_init
|
||||
Memcheck:Leak
|
||||
match-leak-kinds: possible
|
||||
...
|
||||
fun:*dl_*
|
||||
}
|
||||
8
.dockerignore
Normal file
8
.dockerignore
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
target/
|
||||
.github/
|
||||
bin/
|
||||
plugin/
|
||||
man/
|
||||
shell/
|
||||
*.md
|
||||
install
|
||||
4
.envrc
Normal file
4
.envrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export _skim_flake_profile="default"
|
||||
# .envrc.local lets you override the _skim_flake_profile if needed: `export _skim_flake_profile="full"`
|
||||
source_env_if_exists .envrc.local
|
||||
use flake .#"$_skim_flake_profile"
|
||||
9
.githooks/pre-commit
Executable file
9
.githooks/pre-commit
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
set -xeuo pipefail
|
||||
|
||||
cargo +nightly fmt --check --all
|
||||
cargo clippy --all-targets -- -Dwarnings
|
||||
cargo clippy --no-default-features -- -Dwarnings
|
||||
cargo clippy --no-default-features --features cli -- -Dwarnings
|
||||
cargo clippy --no-default-features --features image -- -Dwarnings
|
||||
cargo clippy --no-default-features --features listen -- -Dwarnings
|
||||
cargo clippy --no-default-features --features frizbee -- -Dwarnings
|
||||
75
.github/CONTRIBUTING.md
vendored
Normal file
75
.github/CONTRIBUTING.md
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
# Contributor Guide
|
||||
|
||||
## Development environment
|
||||
|
||||
A [Nix flake](../flake.nix) is provided with opt-in package groups. The default shell contains only the base build tools (`rustup`, `just`); richer environments are available as named shells:
|
||||
|
||||
| Shell | Extra packages |
|
||||
|---|---|
|
||||
| `nix develop` | `rustup`, `just` |
|
||||
| `nix develop .#tests` | + nextest, cargo-insta, cargo-llvm-cov, tmux |
|
||||
| `nix develop .#utils` | + hyperfine, cargo-edit, cargo-public-api, git-cliff |
|
||||
| `nix develop .#gungraun` | + valgrind, libclang, binutils |
|
||||
| `nix develop .#bench` | + uv, matplotlib, requests (for `bench.py`) |
|
||||
| `nix develop .#vagrant` | + vagrant, rsync (for Windows testing) |
|
||||
| `nix develop .#full` | everything above |
|
||||
|
||||
## Running tests
|
||||
|
||||
All tests can be run by using [cargo-nextest](https://nexte.st/), which can be installed using `cargo install cargo-nextest` of following the instructions on the website.
|
||||
You will need `tmux` to run some integration tests.
|
||||
|
||||
You can then run `cargo nextest run --release`, which should automatically build a release binary, run the unit tests and the integration tests.
|
||||
|
||||
|
||||
Most integration tests use [cargo insta](https://insta.rs). If you need to add some tests or re-review them, you will need to install it, and run tests with `cargo insta test --tests --review`, which will let you review snapshots.
|
||||
|
||||
Note: you can run the tests without `--release`, but expect more flaky tests since the timings will be looser. I would advise testing manually any debug test failure if you have doubts.
|
||||
|
||||
Note2: A dockerfile is available if you want to run the tests inside docker. There is little to no cache, so the test will need to rebuild most of the application after each change.
|
||||
To use it, build the image with `docker build -f test.dockerfile . -t skim-test` then run it using `docker run --rm -it skim-test`.
|
||||
|
||||
## Windows testing
|
||||
|
||||
A [Vagrantfile](../Vagrantfile) is provided to spin up a headless Windows Server 2022 Core VM via KVM/libvirt, letting you test Windows compatibility without a GUI.
|
||||
|
||||
**Host prerequisites (NixOS):**
|
||||
|
||||
```nix
|
||||
virtualisation.libvirtd.enable = true;
|
||||
users.users.<you>.extraGroups = [ "libvirtd" ]; # log out/in after applying
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```sh
|
||||
nix develop .#vagrant
|
||||
vagrant up # first boot: ~15-20 min, downloads box + provisions
|
||||
vagrant ssh # connect to the VM
|
||||
vagrant halt # stop the VM
|
||||
vagrant destroy # delete the VM
|
||||
```
|
||||
|
||||
Inside the VM the project root is synced to `C:\vagrant`. Re-sync after local changes with `vagrant rsync`. To build:
|
||||
|
||||
```powershell
|
||||
cd C:\vagrant
|
||||
cargo build
|
||||
cargo test
|
||||
```
|
||||
|
||||
## Submitting code
|
||||
|
||||
To avoid using up CI minutes uselessly, make sure that :
|
||||
- You run `cargo clippy` and `cargo fmt` before pushing any code to an open PR.
|
||||
- Your PR's title respects [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/).
|
||||
|
||||
Not respecting these guidelines could end up consuming all our minutes and preventing us from testing and releasing any new code until the end of the month.
|
||||
|
||||
Note: a git pre-commit hook is available in .githooks/pre-commit which will make the clippy & fmt checks. To use it, run `git config core.hooksPath ".githooks"`.
|
||||
|
||||
## Vibe Coding guidelines
|
||||
|
||||
Any code generated partially or completely using LLMs will be treated the same way as if you wrote it yourself.
|
||||
|
||||
This means that you are expected to understand if fully and are responsible for it.
|
||||
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
33
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Report a bug encountered using `skim`
|
||||
title: "[BUG] xxx"
|
||||
labels: bug
|
||||
assignees: LoricAndre
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS (`uname -a`):
|
||||
- `skim` version (`sk --version`):
|
||||
- Shell and version:
|
||||
- Variables (`env | grep '^SKIM'`):
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
27
.github/dependabot.yml
vendored
Normal file
27
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: weekly
|
||||
commit-message:
|
||||
prefix: chore(deps)
|
||||
prefix-development: chore(dev-deps)
|
||||
groups:
|
||||
cargo-prod:
|
||||
dependency-type: production
|
||||
cargo-dev:
|
||||
dependency-type: development
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: monthly
|
||||
commit-message:
|
||||
prefix: chore(deps)
|
||||
prefix-development: chore(dev-deps)
|
||||
exclude-paths:
|
||||
- .github/workflows/release.yml
|
||||
groups:
|
||||
gha-prod:
|
||||
patterns:
|
||||
- "*"
|
||||
35
.github/pr-title-checker-config.json
vendored
Normal file
35
.github/pr-title-checker-config.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"LABEL": {
|
||||
"name": "invalid-title",
|
||||
"color": "B60205"
|
||||
},
|
||||
"CHECKS": {
|
||||
"prefixes": [
|
||||
"feat: ",
|
||||
"feature: ",
|
||||
"fix: ",
|
||||
"bugfix: ",
|
||||
"perf: ",
|
||||
"refactor: ",
|
||||
"test: ",
|
||||
"tests: ",
|
||||
"build: ",
|
||||
"ci: ",
|
||||
"doc: ",
|
||||
"docs: ",
|
||||
"style: ",
|
||||
"chore: ",
|
||||
"other: "
|
||||
],
|
||||
"regexp": "^\\w+(\\([a-z_-]+\\))?: ",
|
||||
"regexpFlags": "",
|
||||
"ignoreLabels": [
|
||||
"skip-title-check"
|
||||
]
|
||||
},
|
||||
"MESSAGES": {
|
||||
"success": "PR title is valid",
|
||||
"failure": "PR title is invalid",
|
||||
"notice": ""
|
||||
}
|
||||
}
|
||||
11
.github/pull_request_template.md
vendored
Normal file
11
.github/pull_request_template.md
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
## Checklist
|
||||
|
||||
- [ ] The title of my PR follows [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
|
||||
- [ ] I have updated the documentation (`README.md`, comments, `src/manpage.rs` and/or `src/options.rs` if applicable)
|
||||
- [ ] I have added unit tests
|
||||
- [ ] I have added [integration tests](https://github.com/skim-rs/skim/tree/master/tests)
|
||||
- [ ] I have linked all related issues or PRs
|
||||
|
||||
## Description of the changes
|
||||
|
||||
|
||||
75
.github/workflows/apt.yml
vendored
Normal file
75
.github/workflows/apt.yml
vendored
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
name: Deploy APT repo
|
||||
|
||||
# Publishes the release's .deb packages as a (flat) APT repository under
|
||||
# /apt on the gh-pages branch, alongside the coverage report at /coverage.
|
||||
# keep_files preserves the coverage content (and vice versa).
|
||||
#
|
||||
# Requires APT_GPG_PRIVATE_KEY to be set :
|
||||
#
|
||||
# ```sh
|
||||
# export GNUPGHOME=$(mktemp -d)
|
||||
# cat > "$GNUPGHOME/skim-key" <<'EOF'
|
||||
# %no-protection
|
||||
# Key-Type: eddsa
|
||||
# Key-Curve: ed25519
|
||||
# Subkey-Type: ecdh
|
||||
# Subkey-Curve: cv25519
|
||||
# Name-Real: skim apt repo
|
||||
# Name-Email: apt@skim-rs.github.io
|
||||
# Expire-Date: 0
|
||||
# %commit
|
||||
# EOF
|
||||
# gpg --batch --gen-key "$GNUPGHOME/skim-key" 2>&1 | tail -3
|
||||
# KEYID=$(gpg --list-secret-keys --with-colons apt@skim-rs.github.io | awk -F: '/^sec:/{print $5; exit}')
|
||||
# # Copy the output of the line below
|
||||
# gpg --export-secret-keys --armor "$KEYID"
|
||||
# ```
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
apt:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download release .deb packages
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TAG: ${{ fromJson(inputs.plan).announcement_tag }}
|
||||
run: |
|
||||
mkdir -p apt
|
||||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern '*.deb' --dir apt
|
||||
ls -l apt
|
||||
- name: Build APT index
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dpkg-dev apt-utils
|
||||
cd apt
|
||||
dpkg-scanpackages --multiversion . > Packages
|
||||
gzip -k -f Packages
|
||||
apt-ftparchive release . > Release
|
||||
- name: Sign the repo
|
||||
# Skipped when the secret is absent; the repo then stays unsigned
|
||||
# ([trusted=yes]). Add APT_GPG_PRIVATE_KEY (an ASCII-armored private
|
||||
# key) to enable signed-by verification.
|
||||
env:
|
||||
GPG_KEY: ${{ secrets.APT_GPG_PRIVATE_KEY }}
|
||||
run: |
|
||||
echo "$GPG_KEY" | gpg --batch --import
|
||||
KEYID=$(gpg --list-secret-keys --with-colons | awk -F: '/^sec:/{print $5; exit}')
|
||||
cd apt
|
||||
gpg --batch --yes --default-key "$KEYID" --clearsign -o InRelease Release
|
||||
gpg --batch --yes --default-key "$KEYID" -abs -o Release.gpg Release
|
||||
gpg --export --armor "$KEYID" > skim-archive-keyring.asc
|
||||
- name: Deploy to gh-pages under /apt
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: apt
|
||||
destination_dir: apt
|
||||
keep_files: true
|
||||
81
.github/workflows/ci.yml
vendored
81
.github/workflows/ci.yml
vendored
|
|
@ -1,81 +0,0 @@
|
|||
name: Build & Test
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
build: [linux, macos]
|
||||
include:
|
||||
- build: linux
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
- build: macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: x86_64-apple-darwin
|
||||
steps:
|
||||
- name: Install dependencies (for Linux)
|
||||
run: |
|
||||
sudo apt-get install zsh
|
||||
python3 -V
|
||||
tmux -V
|
||||
locale
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
- name: Install dependencies (for MacOS)
|
||||
run: |
|
||||
brew install tmux
|
||||
brew install zsh
|
||||
python3 -V
|
||||
tmux -V
|
||||
locale
|
||||
if: runner.os == 'macOS'
|
||||
env:
|
||||
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install correct toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
override: true
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
- name: Run Tests
|
||||
run: tmux new-session -d && python3 test/test_skim.py --verbose
|
||||
env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
TERM: xterm-256color
|
||||
- name: Cache
|
||||
uses: Swatinem/rust-cache@v1
|
||||
|
||||
rustfmt:
|
||||
name: rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
- name: Check formatting
|
||||
run: |
|
||||
cargo fmt --all -- --check
|
||||
29
.github/workflows/crates.yml
vendored
29
.github/workflows/crates.yml
vendored
|
|
@ -1,29 +0,0 @@
|
|||
name: Release to crates.io
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install correct toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Run cargo check
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
- name: Login crates.io
|
||||
run: cargo login ${CRATES_IO_TOKEN}
|
||||
env:
|
||||
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
||||
- run: cargo publish
|
||||
80
.github/workflows/package.yml
vendored
Normal file
80
.github/workflows/package.yml
vendored
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
name: Build Linux packages
|
||||
|
||||
# Custom dist job (wired in through `global-artifacts-jobs` in dist-workspace.toml).
|
||||
# Builds a `.deb` and a `.rpm` for amd64 and arm64, each containing the `sk`
|
||||
# executable, the man page and the shell completions, then uploads them under an
|
||||
# `artifacts-*` name so that dist's `host` job attaches them to the GitHub
|
||||
# Release. Both architectures build natively (no cross-compilation) on their
|
||||
# respective GitHub-hosted runners.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
linux-packages:
|
||||
name: Build .deb and .rpm (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-22.04
|
||||
- arch: arm64
|
||||
runner: ubuntu-22.04-arm
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install rust toolchain
|
||||
run: rustup toolchain install
|
||||
- name: Check the crate version matches the release plan
|
||||
# These packages are built from the checked-out source (the same tree
|
||||
# dist releases from), so the crate version must match the version dist
|
||||
# planned for this release. Consume the plan to guard against drift.
|
||||
env:
|
||||
ANNOUNCEMENT_TAG: ${{ fromJson(inputs.plan).announcement_tag }}
|
||||
run: |
|
||||
plan_version="${ANNOUNCEMENT_TAG#v}"
|
||||
crate_version="$(cargo metadata --no-deps --format-version 1 | jq -r '.packages[0].version')"
|
||||
echo "release plan: '$plan_version' | crate: '$crate_version'"
|
||||
if [ -n "$plan_version" ] && [ "$plan_version" != "$crate_version" ]; then
|
||||
echo "::error::Cargo.toml version ($crate_version) does not match the release plan ($plan_version)"
|
||||
exit 1
|
||||
fi
|
||||
- name: Install cargo-generate-rpm
|
||||
uses: taiki-e/install-action@v2.87.1
|
||||
with:
|
||||
tool: cargo-generate-rpm@0.21
|
||||
- name: Setup cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
# cargo-deb's prebuilt binary links GLIBC_2.39, which ubuntu-22.04
|
||||
# (glibc 2.35) lacks. Build it from source so it links the runner's
|
||||
# glibc. We stay on 22.04 on purpose: the sk binary it packages must
|
||||
# keep a low glibc floor for the .deb to run on older distros.
|
||||
- name: Install cargo-deb
|
||||
run: cargo install 'cargo-deb@^3' --locked
|
||||
- name: Build release binary
|
||||
run: cargo build --release --bin sk
|
||||
- name: Build .deb package
|
||||
run: cargo deb --no-build
|
||||
- name: Build .rpm package
|
||||
run: cargo generate-rpm
|
||||
- name: Collect packages
|
||||
run: |
|
||||
mkdir -p target/linux-packages
|
||||
cp target/debian/*.deb target/linux-packages/
|
||||
cp target/generate-rpm/*.rpm target/linux-packages/
|
||||
ls -l target/linux-packages/
|
||||
- name: Upload packages
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-linux-packages-${{ matrix.arch }}
|
||||
path: target/linux-packages/*
|
||||
if-no-files-found: error
|
||||
13
.github/workflows/pr-auto-update.yml
vendored
Normal file
13
.github/workflows/pr-auto-update.yml
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
name: Pull request update
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
autoupdate:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: allonsy-studio/actions-pr-auto-update@5fbf9661cbf5c1f092ec33e5a1cfbe70dc5bd81c
|
||||
104
.github/workflows/pr.yml
vendored
Normal file
104
.github/workflows/pr.yml
vendored
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- edited
|
||||
|
||||
jobs:
|
||||
check-title:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create git-cliff mock context
|
||||
env:
|
||||
CONTEXT: >
|
||||
[
|
||||
{
|
||||
"commits": [{
|
||||
"id": "foo",
|
||||
"message": $msg,
|
||||
"links": [],
|
||||
"author": {
|
||||
"name": "",
|
||||
"timestamp": 1
|
||||
},
|
||||
"committer": {
|
||||
"name": "",
|
||||
"timestamp": 1
|
||||
},
|
||||
"merge_commit": false,
|
||||
"github": {
|
||||
"pr_labels": [],
|
||||
"is_first_time": false
|
||||
},
|
||||
"gitlab": {
|
||||
"pr_labels": [],
|
||||
"is_first_time": false
|
||||
},
|
||||
"gitea": {
|
||||
"pr_labels": [],
|
||||
"is_first_time": false
|
||||
},
|
||||
"bitbucket": {
|
||||
"pr_labels": [],
|
||||
"is_first_time": false
|
||||
},
|
||||
"azure_devops": {
|
||||
"pr_labels": [],
|
||||
"is_first_time": false
|
||||
},
|
||||
}],
|
||||
"github": { "contributors": [] },
|
||||
"gitlab": { "contributors": [] },
|
||||
"gitea": { "contributors": [] },
|
||||
"bitbucket": { "contributors": [] },
|
||||
"azure_devops": { "contributors": [] },
|
||||
"submodule_commits": {}
|
||||
}
|
||||
]
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
run: |
|
||||
jq -nc --arg msg "$PR_TITLE" "$CONTEXT" | tee git-cliff-context.json
|
||||
- name: Generate mock changelog entry
|
||||
id: git-cliff
|
||||
uses: orhun/git-cliff-action@v4
|
||||
with:
|
||||
args: >
|
||||
--from-context git-cliff-context.json -s all
|
||||
- name: Check generated entry
|
||||
run: |
|
||||
echo "Generated:"
|
||||
cat "${{ steps.git-cliff.outputs.changelog }}"
|
||||
echo "Checking..."
|
||||
cat "${{ steps.git-cliff.outputs.changelog }}" | grep -Ev '^(## \[unreleased\]|)$' | grep -q '^.\+$'
|
||||
check-generated-files:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
- name: Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: ${{ runner.os }}
|
||||
add-job-id-key: "false"
|
||||
add-rust-environment-hash-key: "false"
|
||||
env-vars: "____"
|
||||
cache-on-failure: "true"
|
||||
cache-all-crates: "true"
|
||||
- name: Generate files
|
||||
run: |
|
||||
cargo run --locked -- --man > ./man/man1/sk.1
|
||||
cargo run --locked -- --shell bash > ./shell/completion.bash
|
||||
cargo run --locked -- --shell zsh > ./shell/completion.zsh
|
||||
cargo run --locked -- --shell fish > ./shell/completion.fish
|
||||
cargo run --locked -- --shell nushell > ./shell/completion.nu
|
||||
- name: Check diff
|
||||
run: |
|
||||
if git diff --exit-code; then
|
||||
echo "No changes to generated files found, PR is safe to merge"
|
||||
exit 0
|
||||
else
|
||||
echo "Found changes to generated files, regenerate them locally and push them using 'just generate-files'"
|
||||
exit 1
|
||||
fi
|
||||
140
.github/workflows/publish-github.yml
vendored
140
.github/workflows/publish-github.yml
vendored
|
|
@ -1,140 +0,0 @@
|
|||
name: Publish to Github
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v[0-9]+.[0-9]+.[0-9]+'
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
name: Create Github Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create artifacts directory
|
||||
run: mkdir artifacts
|
||||
- name: Get the release version from the tag
|
||||
run: |
|
||||
# Apparently, this is the right way to get a tag name. Really?
|
||||
#
|
||||
# See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
|
||||
echo "SK_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||
- name: Create Release
|
||||
id: release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ env.SK_VERSION }}
|
||||
body: ${{ env.SK_VERSION }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
- name: Save release upload URL to artifact
|
||||
run: echo "${{ steps.release.outputs.upload_url }}" > artifacts/release-upload-url
|
||||
- name: Save version number to artifact
|
||||
run: echo "${{ env.SK_VERSION }}" > artifacts/release-version
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: artifacts
|
||||
path: artifacts
|
||||
|
||||
publish-to-github:
|
||||
name: Publish to Github
|
||||
needs: ['create-release']
|
||||
runs-on: ${{matrix.os}}
|
||||
strategy:
|
||||
matrix:
|
||||
build: [linux, arm, arm-v7, macos]
|
||||
include:
|
||||
- build: linux
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: x86_64-unknown-linux-musl
|
||||
cross: false
|
||||
- build: arm
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: arm-unknown-linux-gnueabihf
|
||||
cross: true
|
||||
- build: arm-v7
|
||||
os: ubuntu-latest
|
||||
rust: stable
|
||||
target: armv7-unknown-linux-gnueabihf
|
||||
cross: true
|
||||
- build: macos
|
||||
os: macos-latest
|
||||
rust: stable
|
||||
target: x86_64-apple-darwin
|
||||
cross: false
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Install correct toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
target: ${{ matrix.target }}
|
||||
use-cross: ${{ matrix.cross }}
|
||||
override: true
|
||||
|
||||
- name: Get release download URL
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: artifacts
|
||||
path: artifacts
|
||||
- name: Set release upload URL and release version
|
||||
shell: bash
|
||||
run: |
|
||||
release_upload_url="$(cat artifacts/release-upload-url)"
|
||||
echo "RELEASE_UPLOAD_URL=$release_upload_url" >> $GITHUB_ENV
|
||||
echo "release upload url: $RELEASE_UPLOAD_URL"
|
||||
release_version="$(cat artifacts/release-version)"
|
||||
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
|
||||
echo "release version: $RELEASE_VERSION"
|
||||
|
||||
- name: build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
use-cross: ${{ matrix.cross }}
|
||||
command: build
|
||||
args: --release --target ${{ matrix.target }}
|
||||
- name: Package Artifacts
|
||||
run: |
|
||||
src=$(pwd)
|
||||
stage=
|
||||
|
||||
case $RUNNER_OS in
|
||||
Linux)
|
||||
stage=$(mktemp -d)
|
||||
;;
|
||||
macOS)
|
||||
stage=$(mktemp -d -t tmp)
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "src is: $src"
|
||||
echo "stage is: $stage"
|
||||
|
||||
cp target/${{ matrix.target }}/release/sk $stage/
|
||||
cd $stage
|
||||
|
||||
ASSET_NAME="skim-${{ env.RELEASE_VERSION }}-${{ matrix.target }}.tar.gz"
|
||||
ASSET_PATH="$src/$ASSET_NAME"
|
||||
echo "ASSET_NAME=$ASSET_NAME" >> $GITHUB_ENV
|
||||
echo "ASSET_PATH=$ASSET_PATH" >> $GITHUB_ENV
|
||||
|
||||
tar czf $ASSET_PATH *
|
||||
|
||||
cd $src
|
||||
- name: Upload release archive
|
||||
uses: actions/upload-release-asset@v1.0.1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ env.RELEASE_UPLOAD_URL }}
|
||||
asset_path: ${{ env.ASSET_PATH }}
|
||||
asset_name: ${{ env.ASSET_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
24
.github/workflows/publish.yml
vendored
Normal file
24
.github/workflows/publish.yml
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
name: Publish cargo crate
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
- run: rustup toolchain install
|
||||
- name: Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Login
|
||||
run: cargo login ${CRATES_IO_TOKEN}
|
||||
env:
|
||||
CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
|
||||
- name: Publish
|
||||
run: cargo publish
|
||||
175
.github/workflows/release-pr.yml
vendored
Normal file
175
.github/workflows/release-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
name: Release PR
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
types: [closed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-pr-${{ github.event_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
if: "${{ github.event_name == 'push' && !startsWith(github.event.head_commit.message, 'release: v') }}"
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
changelog: ${{ steps.changelog.outputs.content }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: taiki-e/install-action@742a3317eac7bd62f91cd888b4eead5e784ba833 # v2
|
||||
with:
|
||||
tool: git-cliff
|
||||
- id: version
|
||||
name: Compute next version
|
||||
shell: bash
|
||||
run: |
|
||||
version="$(git cliff --bumped-version)"
|
||||
version="${version#v}"
|
||||
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]
|
||||
if git rev-parse --verify --quiet "refs/tags/v$version"; then
|
||||
echo "No releasable changes since v$version"
|
||||
exit 0
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
- name: Prepare release files
|
||||
if: steps.version.outputs.version != ''
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
sed -i 's/^version = ".*"/version = "'"$VERSION"'"/' Cargo.toml
|
||||
SKIM_DEFAULT_OPTIONS='' cargo run -- --man > man/man1/sk.1
|
||||
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell bash > shell/completion.bash
|
||||
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell zsh > shell/completion.zsh
|
||||
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell fish > shell/completion.fish
|
||||
SKIM_DEFAULT_OPTIONS='' cargo run -- --shell nushell > shell/completion.nu
|
||||
git cliff -p CHANGELOG.md -t "v$VERSION" -u
|
||||
cargo generate-lockfile
|
||||
echo "$VERSION" > shell/version.txt
|
||||
git diff --binary -- CHANGELOG.md Cargo.lock Cargo.toml man shell > "$RUNNER_TEMP/release.patch"
|
||||
test -s "$RUNNER_TEMP/release.patch"
|
||||
- id: changelog
|
||||
name: Render release changelog
|
||||
if: steps.version.outputs.version != ''
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
delimiter="changelog-$(cat /proc/sys/kernel/random/uuid)"
|
||||
{
|
||||
echo "content<<$delimiter"
|
||||
git cliff -u -t "v$VERSION" --strip all
|
||||
echo "$delimiter"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
if: steps.version.outputs.version != ''
|
||||
with:
|
||||
name: release-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/release.patch
|
||||
if-no-files-found: error
|
||||
|
||||
update-pr:
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: release-${{ github.sha }}
|
||||
path: ${{ runner.temp }}/release
|
||||
- name: Create the release commit
|
||||
env:
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
run: |
|
||||
git fetch origin master
|
||||
test "$(git rev-parse origin/master)" = "$GITHUB_SHA"
|
||||
git apply "$RUNNER_TEMP/release/release.patch"
|
||||
unexpected="$(git diff --name-only | grep -Ev '^(CHANGELOG.md|Cargo.lock|Cargo.toml|man/|shell/)' || true)"
|
||||
test -z "$unexpected"
|
||||
git switch -c release-pr
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git add CHANGELOG.md Cargo.lock Cargo.toml man shell
|
||||
git commit -m "release: v$VERSION"
|
||||
# Mint the write token only on this fresh runner, after repository code has finished executing.
|
||||
- id: app-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
with:
|
||||
client-id: ${{ vars.SKIM_RS_BOT_CLIENT_ID }}
|
||||
private-key: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
- name: Push branch and create or update PR
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
VERSION: ${{ needs.prepare.outputs.version }}
|
||||
CHANGELOG: ${{ needs.prepare.outputs.changelog }}
|
||||
shell: bash
|
||||
run: |
|
||||
auth="$(printf 'x-access-token:%s' "$GH_TOKEN" | base64 -w0)"
|
||||
git -c http.extraheader="AUTHORIZATION: basic $auth" push --force origin HEAD:release-pr
|
||||
|
||||
cat > "$RUNNER_TEMP/pr-body.md" <<EOF
|
||||
Automated release preparation for v$VERSION.
|
||||
|
||||
This branch is rebuilt from master as one commit on every push. Merge it with the repository's required squash merge; the resulting commit will be tagged v$VERSION and picked up by the release workflow.
|
||||
|
||||
EOF
|
||||
printf '%s\n' "$CHANGELOG" >> "$RUNNER_TEMP/pr-body.md"
|
||||
|
||||
pr="$(gh pr list --base master --head release-pr --state open --json number --jq '.[0].number')"
|
||||
if [[ -n "$pr" ]]; then
|
||||
gh pr edit "$pr" --title "release: v$VERSION" --body-file "$RUNNER_TEMP/pr-body.md"
|
||||
else
|
||||
gh pr create --base master --head release-pr --title "release: v$VERSION" --body-file "$RUNNER_TEMP/pr-body.md"
|
||||
fi
|
||||
|
||||
tag:
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.merged == true &&
|
||||
github.event.pull_request.head.ref == 'release-pr' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
persist-credentials: false
|
||||
sparse-checkout: Cargo.toml
|
||||
- id: release
|
||||
name: Validate release merge
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
shell: bash
|
||||
run: |
|
||||
version="$(awk -F '"' '/^version = "/ { print $2; exit }' Cargo.toml)"
|
||||
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-+][0-9A-Za-z.-]+)?$ ]]
|
||||
test "$PR_TITLE" = "release: v$version"
|
||||
echo "tag=v$version" >> "$GITHUB_OUTPUT"
|
||||
# No checked-out code runs after this repository-scoped, contents-only token is minted.
|
||||
- id: app-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
|
||||
with:
|
||||
client-id: ${{ vars.SKIM_RS_BOT_CLIENT_ID }}
|
||||
private-key: ${{ secrets.SKIM_RS_BOT_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
- name: Tag the squash commit
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||
SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||
TAG: ${{ steps.release.outputs.tag }}
|
||||
run: gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" -f ref="refs/tags/$TAG" -f sha="$SHA"
|
||||
359
.github/workflows/release.yml
vendored
Normal file
359
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
|
||||
#
|
||||
# Copyright 2022-2024, axodotdev
|
||||
# SPDX-License-Identifier: MIT or Apache-2.0
|
||||
#
|
||||
# CI that:
|
||||
#
|
||||
# * checks for a Git Tag that looks like a release
|
||||
# * builds artifacts with dist (archives, installers, hashes)
|
||||
# * uploads those artifacts to temporary workflow zip
|
||||
# * on success, uploads the artifacts to a GitHub Release
|
||||
#
|
||||
# Note that the GitHub Release will be created with a generated
|
||||
# title/body based on your changelogs.
|
||||
|
||||
name: Release
|
||||
permissions:
|
||||
"contents": "write"
|
||||
|
||||
# This task will run whenever you push a git tag that looks like a version
|
||||
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
|
||||
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
|
||||
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
|
||||
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
|
||||
#
|
||||
# If PACKAGE_NAME is specified, then the announcement will be for that
|
||||
# package (erroring out if it doesn't have the given version or isn't dist-able).
|
||||
#
|
||||
# If PACKAGE_NAME isn't specified, then the announcement will be for all
|
||||
# (dist-able) packages in the workspace with that version (this mode is
|
||||
# intended for workspaces with only one dist-able package, or with all dist-able
|
||||
# packages versioned/released in lockstep).
|
||||
#
|
||||
# If you push multiple tags at once, separate instances of this workflow will
|
||||
# spin up, creating an independent announcement for each one. However, GitHub
|
||||
# will hard limit this to 3 tags per commit, as it will assume more tags is a
|
||||
# mistake.
|
||||
#
|
||||
# If there's a prerelease-style suffix to the version, then the release(s)
|
||||
# will be marked as a prerelease.
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '**[0-9]+.[0-9]+.[0-9]+*'
|
||||
|
||||
jobs:
|
||||
# Run 'dist plan' (or host) to determine what tasks we need to do
|
||||
plan:
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.plan.outputs.manifest }}
|
||||
tag: ${{ !github.event.pull_request && github.ref_name || '' }}
|
||||
tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
|
||||
publishing: ${{ !github.event.pull_request }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install dist
|
||||
# we specify bash to get pipefail; it guards against the `curl` command
|
||||
# failing. otherwise `sh` won't catch that `curl` returned non-0
|
||||
shell: bash
|
||||
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh"
|
||||
- name: Cache dist
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/dist
|
||||
# sure would be cool if github gave us proper conditionals...
|
||||
# so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
|
||||
# functionality based on whether this is a pull_request, and whether it's from a fork.
|
||||
# (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
|
||||
# but also really annoying to build CI around when it needs secrets to work right.)
|
||||
- id: plan
|
||||
run: |
|
||||
dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
cat plan-dist-manifest.json
|
||||
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-plan-dist-manifest
|
||||
path: plan-dist-manifest.json
|
||||
|
||||
custom-test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
secrets: inherit
|
||||
permissions:
|
||||
"contents": "write"
|
||||
"id-token": "write"
|
||||
|
||||
# Build and packages all the platform-specific things
|
||||
build-local-artifacts:
|
||||
name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
|
||||
# Let the initial task tell us to not run (currently very blunt)
|
||||
needs:
|
||||
- plan
|
||||
- custom-test
|
||||
if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Target platforms/runners are computed by dist in create-release.
|
||||
# Each member of the matrix has the following arguments:
|
||||
#
|
||||
# - runner: the github runner
|
||||
# - dist-args: cli flags to pass to dist
|
||||
# - install-dist: expression to run to install dist on the runner
|
||||
#
|
||||
# Typically there will be:
|
||||
# - 1 "global" task that builds universal installers
|
||||
# - N "local" tasks that build each platform's binaries and platform-specific installers
|
||||
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
container: ${{ matrix.container && matrix.container.image || null }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
|
||||
steps:
|
||||
- name: enable windows longpaths
|
||||
run: |
|
||||
git config --global core.longpaths true
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install Rust non-interactively if not already installed
|
||||
if: ${{ matrix.container }}
|
||||
run: |
|
||||
if ! command -v cargo > /dev/null 2>&1; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
fi
|
||||
- name: Install dist
|
||||
run: ${{ matrix.install_dist.run }}
|
||||
# Get the dist-manifest
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
${{ matrix.packages_install }}
|
||||
- name: Build artifacts
|
||||
run: |
|
||||
# Actually do builds and make zips and whatnot
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
- id: cargo-dist
|
||||
name: Post-build
|
||||
# We force bash here just because github makes it really hard to get values up
|
||||
# to "real" actions without writing to env-vars, and writing to env-vars has
|
||||
# inconsistent syntax between shell and powershell.
|
||||
shell: bash
|
||||
run: |
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
# Build and package all the platform-agnostic(ish) things
|
||||
build-global-artifacts:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
|
||||
- name: Fetch local artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: cargo-dist
|
||||
shell: bash
|
||||
run: |
|
||||
dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
|
||||
echo "dist ran successfully"
|
||||
|
||||
# Parse out what we just built and upload it to scratch storage
|
||||
echo "paths<<EOF" >> "$GITHUB_OUTPUT"
|
||||
jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
|
||||
echo "EOF" >> "$GITHUB_OUTPUT"
|
||||
|
||||
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
|
||||
- name: "Upload artifacts"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: artifacts-build-global
|
||||
path: |
|
||||
${{ steps.cargo-dist.outputs.paths }}
|
||||
${{ env.BUILD_MANIFEST_NAME }}
|
||||
|
||||
custom-package:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
uses: ./.github/workflows/package.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# Determines if we should publish/announce
|
||||
host:
|
||||
needs:
|
||||
- plan
|
||||
- build-local-artifacts
|
||||
- build-global-artifacts
|
||||
- custom-package
|
||||
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
|
||||
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.custom-package.result == 'skipped' || needs.custom-package.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
outputs:
|
||||
val: ${{ steps.host.outputs.manifest }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
- name: Install cached dist
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cargo-dist-cache
|
||||
path: ~/.cargo/bin/
|
||||
- run: chmod +x ~/.cargo/bin/dist
|
||||
# Fetch artifacts from scratch-storage
|
||||
- name: Fetch artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: target/distrib/
|
||||
merge-multiple: true
|
||||
- id: host
|
||||
shell: bash
|
||||
run: |
|
||||
dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
|
||||
echo "artifacts uploaded and released successfully"
|
||||
cat dist-manifest.json
|
||||
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
|
||||
- name: "Upload dist-manifest.json"
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# Overwrite the previous copy
|
||||
name: artifacts-dist-manifest
|
||||
path: dist-manifest.json
|
||||
# Create a GitHub Release while uploading all files to it
|
||||
- name: "Download GitHub Artifacts"
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: artifacts-*
|
||||
path: artifacts
|
||||
merge-multiple: true
|
||||
- name: Cleanup
|
||||
run: |
|
||||
# Remove the granular manifests
|
||||
rm -f artifacts/*-dist-manifest.json
|
||||
- name: Create GitHub Release
|
||||
env:
|
||||
PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
|
||||
ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
|
||||
ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
|
||||
RELEASE_COMMIT: "${{ github.sha }}"
|
||||
run: |
|
||||
# Write and read notes from a file to avoid quoting breaking things
|
||||
echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
|
||||
|
||||
gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
|
||||
|
||||
custom-publish:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
uses: ./.github/workflows/publish.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# publish jobs get escalated permissions
|
||||
permissions:
|
||||
"id-token": "write"
|
||||
"packages": "write"
|
||||
|
||||
custom-winget:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
uses: ./.github/workflows/winget.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# publish jobs get escalated permissions
|
||||
permissions:
|
||||
"id-token": "write"
|
||||
"packages": "write"
|
||||
|
||||
custom-apt:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
|
||||
uses: ./.github/workflows/apt.yml
|
||||
with:
|
||||
plan: ${{ needs.plan.outputs.val }}
|
||||
secrets: inherit
|
||||
# publish jobs get escalated permissions
|
||||
permissions:
|
||||
"contents": "write"
|
||||
"id-token": "write"
|
||||
|
||||
announce:
|
||||
needs:
|
||||
- plan
|
||||
- host
|
||||
- custom-publish
|
||||
- custom-winget
|
||||
- custom-apt
|
||||
# use "always() && ..." to allow us to wait for all publish jobs while
|
||||
# still allowing individual publish jobs to skip themselves (for prereleases).
|
||||
# "host" however must run to completion, no skipping allowed!
|
||||
if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish.result == 'skipped' || needs.custom-publish.result == 'success') && (needs.custom-winget.result == 'skipped' || needs.custom-winget.result == 'success') && (needs.custom-apt.result == 'skipped' || needs.custom-apt.result == 'success') }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
submodules: recursive
|
||||
304
.github/workflows/test.yml
vendored
Normal file
304
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
name: Build & Test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: false
|
||||
type: string
|
||||
# pull_request: # No need to trigger on PR, cargo-dist already does
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
nextest:
|
||||
runs-on: ${{matrix.runner}}
|
||||
# Cap the wall-clock so a harness regression that makes the Zellij pane never
|
||||
# render (each `wait_ready` then burning its full budget) fails the leg in
|
||||
# minutes instead of letting a serialized e2e run drag on for hours.
|
||||
timeout-minutes: 45
|
||||
strategy:
|
||||
# Report every OS independently: a failure on one (e.g. Windows) must not
|
||||
# cancel the others, so we still get a clear signal from Linux and macOS.
|
||||
fail-fast: false
|
||||
matrix: &matrix
|
||||
build: [linux, macos, windows]
|
||||
include:
|
||||
- build: linux
|
||||
runner: ubuntu-latest
|
||||
- build: macos
|
||||
runner: macos-latest
|
||||
- build: windows
|
||||
runner: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- &zellij-install
|
||||
# The e2e tests drive `sk` inside a Zellij session (Zellij 0.44+ runs on
|
||||
# Linux, macOS and Windows). taiki-e/install-action fetches a prebuilt
|
||||
# binary on Linux and macOS; it has no Windows binary and `cargo install
|
||||
# zellij` fails building openssl from source on the runner, so Windows
|
||||
# installs via winget below.
|
||||
name: Install zellij (Linux/macOS)
|
||||
if: runner.os != 'Windows'
|
||||
uses: taiki-e/install-action@v2.87.1
|
||||
with:
|
||||
tool: zellij@0.44.3
|
||||
- name: Install zellij (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = 'Stop'
|
||||
winget install --exact --id Zellij.Zellij --version 0.44.3 --source winget --accept-source-agreements --accept-package-agreements --disable-interactivity
|
||||
# The winget package is an MSI that installs zellij and adds its dir to
|
||||
# the machine PATH in the registry. Neither the current process nor
|
||||
# GITHUB_PATH sees that until reloaded, so refresh PATH from the
|
||||
# registry (with a Program Files fallback), then expose zellij's dir to
|
||||
# later steps.
|
||||
$env:PATH = [Environment]::GetEnvironmentVariable('Path','Machine') + ';' + [Environment]::GetEnvironmentVariable('Path','User') + ';' + $env:PATH
|
||||
$exe = (Get-Command zellij.exe -ErrorAction SilentlyContinue).Source
|
||||
if (-not $exe) { $exe = (Get-ChildItem 'C:\Program Files' -Recurse -Filter zellij.exe -ErrorAction SilentlyContinue | Select-Object -First 1).FullName }
|
||||
if (-not $exe) { throw "zellij.exe not found after winget install" }
|
||||
$dir = Split-Path -Parent $exe
|
||||
Write-Host "zellij installed at: $dir"
|
||||
Add-Content -Path $env:GITHUB_PATH -Value $dir
|
||||
& $exe --version
|
||||
- name: Show locale
|
||||
run: locale
|
||||
if: runner.os != 'Windows'
|
||||
|
||||
- &checkout
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- &toolchain
|
||||
name: Install rust toolchain
|
||||
run: rustup toolchain install
|
||||
- &nextest-install
|
||||
name: Install nextest
|
||||
uses: taiki-e/install-action@v2.87.1
|
||||
with:
|
||||
tool: nextest@0.9
|
||||
- &cache
|
||||
name: Setup cargo cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: Run doctests
|
||||
run: cargo test --doc
|
||||
- name: "Run tests"
|
||||
# Do not use `--all-targets` to avoid running benches
|
||||
run: cargo nextest run --release --locked --profile ci --bins --lib --examples --tests
|
||||
env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
TERM: xterm-256color
|
||||
- name: Show snapshot diffs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
find tests/snapshots/ -name "*.snap.new" | while read -r new_snap; do
|
||||
base="${new_snap%.new}"
|
||||
echo "=== Snapshot diff: $base ==="
|
||||
echo "new: $new_snap"
|
||||
cat "$new_snap"
|
||||
if [ -f "$base" ]; then
|
||||
echo "old: $base"
|
||||
cat "$base"
|
||||
diff "$base" "$new_snap"
|
||||
fi
|
||||
done
|
||||
shell: bash
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- *zellij-install
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- *nextest-install
|
||||
- uses: taiki-e/install-action@v2.87.1
|
||||
with:
|
||||
tool: cargo-llvm-cov@0.8
|
||||
- *cache
|
||||
- name: "Run tests with coverage"
|
||||
# Do not use `--all-targets` to avoid running benches
|
||||
run: |
|
||||
cargo +nightly llvm-cov nextest --locked --release --profile ci --branch --bins --lib --examples --tests --no-report
|
||||
cargo +nightly llvm-cov report --release --cobertura --output-path coverage.xml
|
||||
cargo +nightly llvm-cov report --release --html
|
||||
echo "COVERAGE_PERCENT=$(cargo +nightly llvm-cov report --release | tail -n1 | awk '{ print $13 }')" | tee --append $GITHUB_ENV
|
||||
env:
|
||||
LC_ALL: en_US.UTF-8
|
||||
TERM: xterm-256color
|
||||
- name: "Generate coverage badge"
|
||||
if: &if-master github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
||||
uses: emibcn/badge-action@v2.0.4
|
||||
with:
|
||||
label: 'Coverage'
|
||||
status: ${{ env.COVERAGE_PERCENT }}
|
||||
color: 'blue'
|
||||
path: 'target/llvm-cov/html/coverage.svg'
|
||||
- name: "Deploy coverage to gh-pages under /coverage"
|
||||
if: *if-master
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: target/llvm-cov/html
|
||||
destination_dir: coverage
|
||||
keep_files: true
|
||||
|
||||
clippy:
|
||||
runs-on: ${{matrix.runner}}
|
||||
strategy:
|
||||
matrix: *matrix
|
||||
steps:
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- *cache
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
rustfmt:
|
||||
runs-on: ${{matrix.runner}}
|
||||
strategy:
|
||||
matrix: *matrix
|
||||
steps:
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- name: Check formatting
|
||||
run: |
|
||||
cargo fmt --all -- --check
|
||||
|
||||
clippy-no-default-features:
|
||||
runs-on: ${{matrix.runner}}
|
||||
strategy:
|
||||
matrix: *matrix
|
||||
steps:
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- *cache
|
||||
- name: Run clippy with specific features
|
||||
run: |
|
||||
cargo clippy --locked --no-default-features -- -Dwarnings
|
||||
cargo clippy --locked --no-default-features --features cli -- -Dwarnings
|
||||
cargo clippy --locked --no-default-features --features image -- -Dwarnings
|
||||
cargo clippy --locked --no-default-features --features listen -- -Dwarnings
|
||||
cargo clippy --locked --no-default-features --features frizbee -- -Dwarnings
|
||||
|
||||
|
||||
msrv:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- name: MSRV Verify
|
||||
run: cargo +1.91.0 build --release --locked --all-targets
|
||||
|
||||
fuzz:
|
||||
permissions:
|
||||
contents: read
|
||||
runs-on: ${{matrix.runner}}
|
||||
strategy:
|
||||
matrix: *matrix
|
||||
steps:
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- *cache
|
||||
- name: Set up MSVC dev environment
|
||||
# Without a sanitizer, libFuzzer's coverage instrumentation fails to
|
||||
# link on Windows: neither MSVC's link.exe nor LLD's COFF driver
|
||||
# synthesize the __start/__stop section-boundary symbols it needs
|
||||
# (an ELF/Mach-O-only linker feature). MSVC's AddressSanitizer
|
||||
# runtime provides an equivalent shim for those symbols, so it's
|
||||
# required for the link to succeed at all, not just extra bug
|
||||
# detection. It needs its DLL directory on PATH at run time, which
|
||||
# this action sets up (see
|
||||
# https://rust-fuzz.github.io/book/cargo-fuzz/windows/setup.html).
|
||||
if: runner.os == 'Windows'
|
||||
uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: taiki-e/install-action@v2.87.1
|
||||
with:
|
||||
tool: cargo-fuzz@0.13
|
||||
- name: Run fuzz targets
|
||||
shell: bash
|
||||
# 5 targets * 60s = 5 minutes of fuzzing total per run.
|
||||
# Force the actual host target: the sanitizer build can't link
|
||||
# against a statically-linked libc (e.g. if CARGO_BUILD_TARGET
|
||||
# defaults to a musl target elsewhere in the matrix).
|
||||
run: |
|
||||
host_target="$(rustc +nightly -vV | sed -n 's/^host: //p')"
|
||||
if [ "$RUNNER_OS" = "Windows" ]; then
|
||||
# Git Bash's own coreutils `link` (for hardlinks) sits ahead of
|
||||
# the MSVC one on PATH within this shell, so cargo/rustc would
|
||||
# otherwise invoke the wrong `link.exe`. Point at the real MSVC
|
||||
# linker explicitly, using the dev env ilammy/msvc-dev-cmd set up.
|
||||
export CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="${VCToolsInstallDir}bin\\HostX64\\x64\\link.exe"
|
||||
fi
|
||||
for target in ansi_strip field_extract fuzzy_match query_match keymap_parse; do
|
||||
cargo +nightly fuzz run "$target" --target "$host_target" -- -max_total_time=60
|
||||
done
|
||||
- name: Upload crash artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fuzz-crashes-${{ runner.os }}
|
||||
path: fuzz/artifacts/
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Single aggregate status check for branch protection / repository rulesets,
|
||||
# so they don't need to enumerate every matrix leg by name individually.
|
||||
|
||||
public-api:
|
||||
name: Check for public API breaking changes
|
||||
permissions:
|
||||
contents: read
|
||||
continue-on-error: true
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- *checkout
|
||||
- *toolchain
|
||||
- *cache
|
||||
- uses: taiki-e/install-action@v2.87.1
|
||||
with:
|
||||
tool: cargo-public-api@0.52
|
||||
- run: rustup toolchain install nightly-x86_64-unknown-linux-gnu
|
||||
name: Install nightly toolchain
|
||||
# Compare two repository revisions so cargo-public-api uses their committed
|
||||
# lockfiles. A registry comparison resolves the released crate again and can
|
||||
# fail when a new, broken transitive dependency is published.
|
||||
- run: git fetch --force --tags
|
||||
name: Fetch release tags
|
||||
- run: |
|
||||
latest_tag="$(git tag --list 'v*' --sort=-version:refname | head -n1)"
|
||||
cargo public-api diff "${latest_tag}..HEAD" --deny removed --deny changed
|
||||
name: Run cargo-public-api
|
||||
|
||||
|
||||
ci-success:
|
||||
name: CI Success
|
||||
if: always()
|
||||
needs:
|
||||
- nextest
|
||||
- coverage
|
||||
- clippy
|
||||
- rustfmt
|
||||
- clippy-no-default-features
|
||||
- msrv
|
||||
- fuzz
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check all required jobs succeeded
|
||||
run: |
|
||||
echo "${{ toJson(needs) }}"
|
||||
if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" || "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
|
||||
echo "One or more required jobs failed or were cancelled."
|
||||
exit 1
|
||||
fi
|
||||
echo "All required jobs passed."
|
||||
38
.github/workflows/winget.yml
vendored
Normal file
38
.github/workflows/winget.yml
vendored
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: Publish to winget
|
||||
|
||||
# Custom dist publish job (wired in through `publish-jobs` in dist-workspace.toml).
|
||||
# After dist creates the GitHub Release, this submits the new version to the
|
||||
# Windows Package Manager Community Repository (microsoft/winget-pkgs) using the
|
||||
# Windows .zip artifact that dist already builds and attaches to the release.
|
||||
#
|
||||
# Requires a `WINGET_TOKEN` repository secret: a *classic* GitHub PAT with the
|
||||
# `public_repo` scope that owns a fork of microsoft/winget-pkgs under the
|
||||
# `fork-user` account (see below). Fine-grained tokens are not supported -- they
|
||||
# can commit but cannot open the cross-fork PR to winget-pkgs. Without it the job
|
||||
# fails at submission time.
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
plan:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
WINGET_TOKEN:
|
||||
required: true
|
||||
description: "GitHub PAT with public_repo scope for winget-pkgs PR"
|
||||
|
||||
jobs:
|
||||
winget:
|
||||
name: Publish winget package
|
||||
# winget-pkgs only accepts stable versions; never submit prereleases.
|
||||
if: ${{ !fromJson(inputs.plan).announcement_is_prerelease }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Submit skim to the Windows Package Manager Community Repository
|
||||
uses: vedantmgoyal9/winget-releaser@v2
|
||||
with:
|
||||
identifier: skim-rs.skim
|
||||
installers-regex: '-pc-windows-msvc\.zip$'
|
||||
release-tag: ${{ fromJson(inputs.plan).announcement_tag }}
|
||||
fork-user: skim-rs
|
||||
token: ${{ secrets.WINGET_TOKEN }}
|
||||
27
.gitignore
vendored
27
.gitignore
vendored
|
|
@ -10,6 +10,31 @@
|
|||
# Generated by Cargo
|
||||
/target/
|
||||
|
||||
/bin/*
|
||||
/bin/sk
|
||||
.idea/
|
||||
.ropeproject/
|
||||
.direnv
|
||||
|
||||
# Coverage
|
||||
lcov.info
|
||||
codecov.json
|
||||
*.profraw
|
||||
|
||||
# Profiling
|
||||
profile.json.gz
|
||||
*.data
|
||||
*.data.old
|
||||
flamegraph.svg
|
||||
cachegrind.out.*
|
||||
|
||||
/scripts/data/
|
||||
__pycache__/
|
||||
|
||||
# Vagrant
|
||||
.vagrant/
|
||||
|
||||
.envrc.local
|
||||
/public
|
||||
|
||||
.codex
|
||||
/coverage.xml
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
error_on_line_overflow = false
|
||||
max_width = 120
|
||||
imports_granularity = "Module"
|
||||
|
|
|
|||
148
AGENTS.md
Normal file
148
AGENTS.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
# Skim Agent Guidelines
|
||||
|
||||
## Build/Test/Lint Commands
|
||||
- Build: `cargo build [--release]`
|
||||
- Run: `cargo run [--release]`
|
||||
- Test (all): `cargo nextest run`
|
||||
- Test (single): `cargo nextest test_name`
|
||||
- Integration/E2E tests: `cargo nextest run --tests` (drives `sk` through Zellij under the hood; needs `zellij` >= 0.44 and `bash` on `$PATH`)
|
||||
- Memory leak detection: `cargo nextest run --profile valgrind`
|
||||
- Thread leak/race detection:
|
||||
1. Build: `RUSTFLAGS="-Zsanitizer=thread" cargo +nightly build --tests -Zbuild-std --target x86_64-unknown-linux-gnu`
|
||||
2. Run: `TSAN_OPTIONS="detect_deadlocks=1" cargo +nightly nextest run --profile tsan --target x86_64-unknown-linux-gnu`
|
||||
- Lint: `cargo clippy`
|
||||
- Format: `cargo +nightly fmt` (check only: `cargo +nightly fmt --check`)
|
||||
- Fuzz (requires nightly + `cargo install cargo-fuzz`): `cargo +nightly fuzz run <target>` — see `fuzz/README.md` for target list
|
||||
|
||||
## Code Style
|
||||
- Format with 120 char line width (defined in .rustfmt.toml)
|
||||
- Use standard Rust naming conventions (snake_case for functions/variables, CamelCase for types)
|
||||
- Organize imports by standard library, external crates, then internal modules
|
||||
- Prefer Option/Result types for error handling over panicking
|
||||
- Use proper error propagation with `?` operator
|
||||
- Document public API with rustdoc comments
|
||||
- Use meaningful type annotations, especially for public functions
|
||||
- Follow the existing structure for new modules (see src/engine/ or src/model/)
|
||||
- Implement relevant traits (SkimItem, etc.) for new types when needed
|
||||
|
||||
## Architecture Documentation
|
||||
- `ARCHITECTURE.md` documents the full architecture: data flow, operating modes, subsystems, threading model, and public API.
|
||||
- **Update `ARCHITECTURE.md` whenever you make structural changes**, including:
|
||||
- Adding, removing, or renaming modules, structs, or traits
|
||||
- Changing the data flow between subsystems (reader → pool → matcher → TUI)
|
||||
- Adding new operating modes or modifying existing ones
|
||||
- Changing the threading model or synchronization primitives
|
||||
- Adding or removing public API surface (`SkimItem`, `SkimOptions`, `SkimOutput`, etc.)
|
||||
- Changing the event/action system or key binding infrastructure
|
||||
|
||||
|
||||
## Testing
|
||||
|
||||
The end-to-end tests drive a real `sk` process through a terminal, using the
|
||||
Zellij-backed harness in `tests/common/zellij.rs` (`ZellijController` + the
|
||||
`sk_test!` DSL). It requires `zellij` (>= 0.44) and `bash` on `$PATH`. The
|
||||
harness is cross-platform (Linux, macOS and Windows). The pure-harness tests in
|
||||
`interactive.rs` run on all three platforms; `execute.rs`, `popup.rs` and
|
||||
`listen.rs` stay `#![cfg(unix)]` for reasons unrelated to the multiplexer (they
|
||||
install POSIX mock binaries / bind a unix socket), so they run on Linux and
|
||||
macOS. A few harness details make the non-Linux runners work: the pane's shell
|
||||
is resolved to an absolute `bash` path (the Zellij server's environment may lack
|
||||
`bash` on `PATH`); `ZELLIJ_SOCKET_DIR` is forced to a short path so the session's
|
||||
unix socket path stays under the OS cap (macOS's default `$TMPDIR` is too long);
|
||||
the drain thread answers the client's cursor-position report (`ESC[6n`), which
|
||||
the Windows ConPTY client blocks on to learn the terminal size; and `wait_ready`
|
||||
nudges the client's terminal size until the server gives the pane a non-zero
|
||||
geometry to render into. The harness drives Zellij with:
|
||||
- `zellij attach --create <session>` (spawned on an in-process PTY via
|
||||
`portable-pty`) to start a detached session; `SKIM_DEFAULT_OPTIONS` and friends
|
||||
are cleared on the spawned process.
|
||||
- `zellij --session <session> action write <bytes...>` to inject keystrokes.
|
||||
- `zellij --session <session> action dump-screen [--ansi]` to capture the pane.
|
||||
|
||||
When exploring manually you can reproduce the same flow with those commands; the
|
||||
config the harness writes disables startup tips, pane frames and the kitty
|
||||
keyboard protocol (so injected legacy escape sequences reach `sk`).
|
||||
|
||||
## Insta Snapshot Tests
|
||||
|
||||
Most TUI behaviour is covered by insta snapshot tests in `tests/`. The
|
||||
infrastructure lives in `tests/common/insta.rs` and is exposed through two
|
||||
macros: `snap!` and `insta_test!`.
|
||||
|
||||
### `insta_test!` — writing tests
|
||||
|
||||
**Simple variant** (single snapshot, no interaction):
|
||||
```rust
|
||||
insta_test!(my_test, ["item1", "item2"], &["--opt1", "opt2"]);
|
||||
insta_test!(my_test, @cmd "printf 'a\nb'", &["--ansi"]);
|
||||
insta_test!(my_test, @interactive, &["-i", "--cmd", "echo {q}"]);
|
||||
```
|
||||
|
||||
**DSL variant** (multiple snapshots with interaction between them):
|
||||
```rust
|
||||
insta_test!(my_test, ["a", "b", "c"], &["--multi"], {
|
||||
@snap; // take a snapshot (cell text only)
|
||||
@snap_color; // snapshot cell styling (fg/bg/modifier) instead
|
||||
@key Up; // send a named key (Enter, Down, Tab, …)
|
||||
@char 'f'; // send a single character
|
||||
@type "foo"; // type a string
|
||||
@ctrl 'w'; // Ctrl+key
|
||||
@alt 'b'; // Alt+key
|
||||
@shift Tab; // Shift+key
|
||||
@action Last; // send an Action variant (no args)
|
||||
@action Down(1); // send an Action variant (with args)
|
||||
@snap; // take another snapshot
|
||||
@assert(|h| condition); // boolean assertion (does not snapshot)
|
||||
@exited 0; // assert the app exited with this code
|
||||
});
|
||||
```
|
||||
|
||||
### Snapshot file naming
|
||||
|
||||
| Macro form | File pattern | Example |
|
||||
|---|---|---|
|
||||
| Simple variant | `{file}__{test}.snap` | `options__opt_wrap.snap` |
|
||||
| DSL variant — Nth `@snap` | `{file}__{test}@{NNN}.snap` | `options__opt_cycle@002.snap` |
|
||||
| DSL variant — Nth `@snap_color` | `{file}__{test}@color{NNN}.snap` | `ansi__ansi_flag_enabled@color002.snap` |
|
||||
|
||||
DSL snapshots use a zero-padded three-digit suffix (`@001`, `@002`, …) so that
|
||||
`cargo insta review` presents them in the order they were taken.
|
||||
|
||||
### Snapshot front-matter
|
||||
|
||||
Every snapshot includes a `description` field in its YAML front-matter. For a
|
||||
DSL test the description shows the input, options, and the DSL commands that
|
||||
ran **since the previous `@snap`**, making it easy to understand what state
|
||||
each screenshot captures:
|
||||
|
||||
```
|
||||
description: "input: items [\"a\", \"b\", \"c\"]\noptions: --multi\nafter:\n @key Up\n @shift Tab"
|
||||
```
|
||||
|
||||
The `expression` field is intentionally omitted (`omit_expression = true`) to
|
||||
keep the files free of internal implementation details.
|
||||
|
||||
### Snapshot workflow
|
||||
|
||||
Generate / update snapshots:
|
||||
```sh
|
||||
# Generate all missing snapshots and accept them immediately:
|
||||
INSTA_UPDATE=always cargo nextest run
|
||||
|
||||
# Generate missing snapshots as .snap.new files for manual review:
|
||||
INSTA_UPDATE=new cargo nextest run
|
||||
cargo insta review # accept / reject interactively
|
||||
```
|
||||
|
||||
When **adding new tests** that produce snapshots:
|
||||
1. Write the test with `@snap` markers.
|
||||
2. Run `INSTA_UPDATE=always cargo nextest run --test <file> <test_name>` to
|
||||
generate the initial snapshot files.
|
||||
3. Inspect the generated `.snap` files to verify the rendered output is correct.
|
||||
4. Commit both the test and its snapshot files.
|
||||
|
||||
When **changing rendering logic** that affects many existing snapshots:
|
||||
1. Delete the affected `.snap` files:
|
||||
`find tests/snapshots -name "prefix__*.snap" -delete`
|
||||
2. Regenerate: `INSTA_UPDATE=always cargo nextest run`
|
||||
3. Review the diff with `git diff tests/snapshots/` before committing.
|
||||
1401
ARCHITECTURE.md
Normal file
1401
ARCHITECTURE.md
Normal file
File diff suppressed because it is too large
Load diff
2386
CHANGELOG.md
2386
CHANGELOG.md
File diff suppressed because it is too large
Load diff
3708
Cargo.lock
generated
3708
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
189
Cargo.toml
189
Cargo.toml
|
|
@ -1,15 +1,15 @@
|
|||
[package]
|
||||
name = "skim"
|
||||
version = "0.9.4"
|
||||
authors = ["Zhang Jinzhou <lotabout@gmail.com>"]
|
||||
version = "5.7.0"
|
||||
authors = ["Loric ANDRE", "Zhang Jinzhou <lotabout@gmail.com>"]
|
||||
description = "Fuzzy Finder in rust!"
|
||||
documentation = "https://docs.rs/skim"
|
||||
homepage = "https://github.com/lotabout/skim"
|
||||
repository = "https://github.com/lotabout/skim"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/skim-rs/skim"
|
||||
keywords = ["fuzzy", "menu", "util"]
|
||||
license = "MIT"
|
||||
edition = "2018"
|
||||
edition = "2024"
|
||||
rust-version = "1.91"
|
||||
default-run = "sk"
|
||||
|
||||
[lib]
|
||||
name = "skim"
|
||||
|
|
@ -18,33 +18,162 @@ path = "src/lib.rs"
|
|||
[[bin]]
|
||||
name = "sk"
|
||||
path = "src/bin/main.rs"
|
||||
|
||||
[dependencies]
|
||||
nix = "0.19.1"
|
||||
atty = "0.2.14"
|
||||
regex = "1.1.5"
|
||||
lazy_static = "1.3.0"
|
||||
shlex = "0.1.1"
|
||||
unicode-width = "0.1.4"
|
||||
log = "0.4.6"
|
||||
env_logger = "0.8.2"
|
||||
time = "0.2.23"
|
||||
clap = "2.26.2"
|
||||
tuikit = "0.4.5"
|
||||
vte = "0.9.0"
|
||||
fuzzy-matcher = "0.3.7"
|
||||
rayon = "1.0.3"
|
||||
derive_builder = "0.9"
|
||||
bitflags = "1.0.4"
|
||||
timer = "0.2.0"
|
||||
chrono = "0.4"
|
||||
crossbeam = "0.8.0"
|
||||
beef = "0.5.0" # compact cow
|
||||
defer-drop = "1.0.1"
|
||||
required-features = ["cli"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
# Default is destined to the CLI, not to library usage.
|
||||
default = ["cli", "frizbee", "image", "listen"]
|
||||
# Everything needed to use skim as a cli (argument parsing, shell integrations...). This should not be needed for most libraries.
|
||||
cli = ["dep:clap", "dep:clap_complete", "dep:clap_complete_nushell", "dep:shlex", "dep:env_logger", "dep:clap_mangen"]
|
||||
# Include frizbee as a matching algorithm
|
||||
frizbee = ["dep:frizbee"]
|
||||
# Enable image previews (renders the preview argument as an image)
|
||||
image = ["dep:image", "dep:ratatui-image"]
|
||||
# Enable the IPC socket (--listen / --remote), driving skim from other processes
|
||||
listen = ["dep:interprocess", "dep:ron", "dep:serde"]
|
||||
# Enable gungraun (Valgrind-based) benchmarks
|
||||
gungraun = ["dep:gungraun"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
opt-level = 2
|
||||
strip = true
|
||||
|
||||
# Kept for compatibility
|
||||
[profile.dist]
|
||||
inherits = "release"
|
||||
|
||||
[profile.release-debug]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
strip = false
|
||||
|
||||
[profile.dev]
|
||||
debug = "line-tables-only"
|
||||
split-debuginfo = "unpacked"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
debug = false
|
||||
|
||||
[lints.rust]
|
||||
missing_docs = "warn"
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage, coverage_nightly)'] }
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
incompatible_msrv = "warn"
|
||||
collapsible_match = "allow"
|
||||
default_trait_access = "allow"
|
||||
|
||||
[dependencies]
|
||||
ansi-to-tui = "8.0.1"
|
||||
assert_enum_variants = "0.1.2"
|
||||
clap = { version = "4.6.1" , optional = true, features = ["cargo", "derive", "unstable-markdown"] }
|
||||
clap_complete = { version = "4.6.5", optional = true }
|
||||
clap_complete_nushell = { version = "4.6.0", optional = true }
|
||||
clap_mangen = { version = "0.3.0", optional = true }
|
||||
eyre = "0.6.12"
|
||||
# Crossterm's version is selected by ratatui
|
||||
crossterm = { version = ">=0.0.0", features = ["event-stream", "use-dev-tty", "libc"] }
|
||||
derive_builder = "0.20.2"
|
||||
derive_more = { version = "2.1.1", features = ["debug", "eq"] }
|
||||
env_logger = { version = "0.11.10", optional = true, features = ["humantime"] }
|
||||
frizbee = { version = "=0.13.0", optional = true }
|
||||
futures = "0.3.32"
|
||||
gungraun = { version = "0.19.1", optional = true }
|
||||
image = { version = "0.25.10", optional = true, default-features = false, features = ["png", "jpeg", "gif", "webp"] }
|
||||
indexmap = "2.13.1"
|
||||
interprocess = { version = "2.4.2", features = ["tokio"], optional = true }
|
||||
kanal = "0.1.1"
|
||||
log = "0.4.31"
|
||||
memchr = "2.8.1"
|
||||
mimalloc = { version = "0.1.48", features = ["v3"] }
|
||||
nix = { version = "0.31.3", features = ["fs", "poll", "signal"] }
|
||||
portable-pty = "0.9.0"
|
||||
ratatui = "0.30.0"
|
||||
ratatui-image = { version = "11.0.4", features = ["crossterm"], default-features = false, optional = true }
|
||||
regex = "1.12.3"
|
||||
roff = "1.1.1"
|
||||
ron = { version = "0.12.1", optional = true }
|
||||
serde = { version = "1.0.228", features = ["derive"], optional = true }
|
||||
shell-quote = "0.8.0"
|
||||
shlex = { version = "2.0.1", optional = true }
|
||||
tempfile = "3.27.0"
|
||||
thiserror = "2.0.18"
|
||||
thread_local = "1.1.9"
|
||||
tokio = { version = "1.52.3", features = ["macros", "net", "rt-multi-thread", "sync", "time", "tokio-macros"] }
|
||||
tokio-util = "0.7.18"
|
||||
tui-term = "0.3.4"
|
||||
unicode-display-width = "0.3.0"
|
||||
unicode-normalization = "0.1.25"
|
||||
which = "8.0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.8.2", features = ["async_tokio"] }
|
||||
gnuplot = "0.0.46"
|
||||
insta = "1.47"
|
||||
rand = "0.10.0"
|
||||
serde_json = { version = "=1.0.151" }
|
||||
serial_test = "=3.5.0"
|
||||
|
||||
[[bench]]
|
||||
name = "read_and_match"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "filter"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "partial"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "matcher_micro"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "gungraun"
|
||||
harness = false
|
||||
required-features = ["gungraun"]
|
||||
|
||||
[[bench]]
|
||||
name = "cli"
|
||||
harness = false
|
||||
bench = false
|
||||
|
||||
[package.metadata.wix]
|
||||
upgrade-guid = "6DDAED06-EBE2-41C4-94F5-CB03F2A4B92E"
|
||||
path-guid = "05D1A327-19E0-4C82-B5EC-6B28E40C691B"
|
||||
license = false
|
||||
eula = false
|
||||
|
||||
# Debian package (.deb) built via `cargo deb`.
|
||||
# Ships the `sk` executable, the man pages and the shell completions.
|
||||
[package.metadata.deb]
|
||||
maintainer = "Loric ANDRE, Zhang Jinzhou <lotabout@gmail.com>"
|
||||
copyright = "The skim developers"
|
||||
section = "utils"
|
||||
priority = "optional"
|
||||
extended-description = "skim is a general purpose fuzzy finder written in Rust, usable as a command line tool and as a library."
|
||||
assets = [
|
||||
["target/release/sk", "usr/bin/", "755"],
|
||||
["man/man1/sk.1", "usr/share/man/man1/", "644"],
|
||||
["shell/completion.bash", "usr/share/bash-completion/completions/sk", "644"],
|
||||
["shell/completion.zsh", "usr/share/zsh/vendor-completions/_sk", "644"],
|
||||
["shell/completion.fish", "usr/share/fish/vendor_completions.d/sk.fish", "644"],
|
||||
["README.md", "usr/share/doc/skim/README.md", "644"],
|
||||
]
|
||||
|
||||
# RPM package (.rpm) built via `cargo generate-rpm`.
|
||||
# Ships the `sk` executable, the man pages and the shell completions.
|
||||
[package.metadata.generate-rpm]
|
||||
assets = [
|
||||
{ source = "target/release/sk", dest = "/usr/bin/sk", mode = "755" },
|
||||
{ source = "man/man1/sk.1", dest = "/usr/share/man/man1/sk.1", mode = "644", doc = true },
|
||||
{ source = "shell/completion.bash", dest = "/usr/share/bash-completion/completions/sk", mode = "644" },
|
||||
{ source = "shell/completion.zsh", dest = "/usr/share/zsh/site-functions/_sk", mode = "644" },
|
||||
{ source = "shell/completion.fish", dest = "/usr/share/fish/vendor_completions.d/sk.fish", mode = "644" },
|
||||
{ source = "README.md", dest = "/usr/share/doc/skim/README.md", mode = "644", doc = true },
|
||||
{ source = "LICENSE", dest = "/usr/share/doc/skim/LICENSE", mode = "644", doc = true },
|
||||
]
|
||||
|
|
|
|||
581
README.md
581
README.md
|
|
@ -1,106 +1,206 @@
|
|||
[](https://crates.io/crates/skim)
|
||||
[](https://github.com/lotabout/skim/actions?query=workflow%3A%22Build+%26+Test%22)
|
||||
<p align="center">
|
||||
<a href="https://crates.io/crates/skim">
|
||||
<img src="https://img.shields.io/crates/v/skim.svg" alt="Crates.io" />
|
||||
</a>
|
||||
<a href="https://github.com/skim-rs/skim/actions?query=workflow%3A%22Build+%26+Test%22+event%3Apush">
|
||||
<img src="https://github.com/skim-rs/skim/actions/workflows/test.yml/badge.svg?event=push" alt="Build & Test" />
|
||||
</a>
|
||||
<a href="https://skim-rs.github.io/skim/coverage" >
|
||||
<img src="https://skim-rs.github.io/skim/coverage/coverage.svg" alt="coverage badge" />
|
||||
</a>
|
||||
<a href="https://repology.org/project/skim-fuzzy-finder/versions">
|
||||
<img src="https://repology.org/badge/tiny-repos/skim-fuzzy-finder.svg" alt="Packaging status" />
|
||||
</a>
|
||||
<a href="https://discord.gg/23PuxttufP">
|
||||
<img alt="Skim Discord" src="https://img.shields.io/discord/1031830957432504361?label=&color=7389d8&labelColor=6a7ec2&logoColor=ffffff&logo=discord" />
|
||||
</a>
|
||||
<a href="https://matrix.to/#/#skim:matrix.org">
|
||||
<img alt="Skim Matrix room" src="https://img.shields.io/badge/matrix-%23000000?style=flat&logo=matrix&logoColor=white" />
|
||||
</a>
|
||||
<a href="https://ratatui.rs">
|
||||
<img alt="Built with Ratatui" src="https://ratatui.rs/built-with-ratatui/badge.svg" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
> Life is short, skim!
|
||||
|
||||
Half of our life is spent on navigation: files, lines, commands… You need skim!
|
||||
It is a general fuzzy finder that saves you time.
|
||||
We spend so much of our time navigating through files, lines, and commands. That's where Skim comes in!
|
||||
It's a powerful fuzzy finder designed to make your workflow faster and more efficient.
|
||||
|
||||
[](https://asciinema.org/a/pIfwazaM0mTHA8F7qRbjrqOnm)
|
||||
|
||||
skim provides a single executable: `sk`. Basically anywhere you would want to use
|
||||
`grep`, try `sk` instead.
|
||||
Skim provides a single executable called `sk`. Think of it as a smarter alternative to tools like
|
||||
`grep` - once you try it, you'll wonder how you ever lived without it!
|
||||
|
||||
# Table of contents
|
||||
|
||||
- [Installation](#installation)
|
||||
* [Package Managers](#package-managers)
|
||||
* [Manually](#manually)
|
||||
- [Usage](#usage)
|
||||
- [As Filter](#as-filter)
|
||||
- [As Interactive Interface](#as-interactive-interface)
|
||||
- [Key Bindings](#key-bindings)
|
||||
- [Search Syntax](#search-syntax)
|
||||
- [Exit code](#exit-code)
|
||||
* [As Vim plugin](#as-vim-plugin)
|
||||
* [As filter](#as-filter)
|
||||
* [As Interactive Interface](#as-interactive-interface)
|
||||
* [Shell Bindings](#shell-bindings)
|
||||
* [Key Bindings](#key-bindings)
|
||||
* [Search Syntax](#search-syntax)
|
||||
* [exit code](#exit-code)
|
||||
- [Tools compatible with `skim`](#tools-compatible-with-skim)
|
||||
* [fzf-lua neovim plugin](#fzf-lua-neovim-plugin)
|
||||
* [nu_plugin_skim](#nu_plugin_skim)
|
||||
- [Customization](#customization)
|
||||
- [Keymap to redefine](#keymap)
|
||||
- [Sort Criteria](#sort-criteria)
|
||||
- [Color Scheme](#color-scheme)
|
||||
- [Misc](#misc)
|
||||
- [Advance Topics](#advance-topics)
|
||||
- [Interactive Mode](#interactive-mode)
|
||||
- [Executing external programs](#executing-external-programs)
|
||||
- [Preview Window](#preview-window)
|
||||
- [Fields Support](#fields-support)
|
||||
- [Use as a Library](#use-as-a-library)
|
||||
* [Keymap](#keymap)
|
||||
* [Sort Criteria](#sort-criteria)
|
||||
* [Color Scheme](#color-scheme)
|
||||
* [Misc](#misc)
|
||||
- [Advanced Topics](#advanced-topics)
|
||||
* [Interactive mode](#interactive-mode)
|
||||
+ [How does it work?](#how-does-it-work)
|
||||
* [Executing external programs](#executing-external-programs)
|
||||
* [Algorithms](#algorithms)
|
||||
* [Preview Window](#preview-window)
|
||||
+ [How does it work?](#how-does-it-work-1)
|
||||
* [Fields support](#fields-support)
|
||||
* [Use as a library](#use-as-a-library)
|
||||
* [Benchmarks](#benchmarks)
|
||||
- [FAQ](#faq)
|
||||
- [How to ignore files?](#how-to-ignore-files)
|
||||
- [Some files are not shown in vim plugin](#some-files-are-not-shown-in-vim-plugin)
|
||||
- [Differences to fzf](#differences-to-fzf)
|
||||
* [How to ignore files?](#how-to-ignore-files)
|
||||
* [Some files are not shown in Vim plugin](#some-files-are-not-shown-in-vim-plugin)
|
||||
- [Differences from fzf](#differences-from-fzf)
|
||||
- [How to contribute](#how-to-contribute)
|
||||
* [Windows compatibility testing](#windows-compatibility-testing)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
* [No line feed issues with nix, FreeBSD, termux](#no-line-feed-issues-with-nix-freebsd-termux)
|
||||
|
||||
# Installation
|
||||
|
||||
The skim project contains several components:
|
||||
|
||||
1. `sk` executable -- the core.
|
||||
2. `sk-tmux` -- script for launching `sk` in a tmux pane.
|
||||
3. Vim/Nvim plugin -- to call `sk` inside Vim/Nvim. check [skim.vim](https://github.com/lotabout/skim.vim) for more Vim support.
|
||||
1. `sk` executable - the core program
|
||||
2. Vim/Nvim plugin - to call `sk` inside Vim/Nvim. Check [skim.vim](https://github.com/skim-rs/skim/blob/master/plugin/skim.vim) for Vim support.
|
||||
|
||||
## Package Managers
|
||||
|
||||
| Distribution | Package Manager | Command |
|
||||
| -------------- | ----------------- | ------------------------- |
|
||||
| macOS | Homebrew | `brew install sk` |
|
||||
| macOS | MacPorts | `sudo port install skim` |
|
||||
| Fedora | dnf | `dnf install skim` |
|
||||
| Alpine | apk | `apk add skim` |
|
||||
| Arch | pacman | `pacman -S skim` |
|
||||
| OS | Package Manager | Command |
|
||||
| -------------- | --------------- | ---------------------------- |
|
||||
| macOS | Homebrew | `brew install sk` |
|
||||
| macOS | MacPorts | `sudo port install skim` |
|
||||
| Alpine | apk | `apk add skim` |
|
||||
| Arch | pacman | `pacman -S skim` |
|
||||
| Fedora | COPR | see below |
|
||||
| Gentoo | Portage | `emerge --ask app-misc/skim` |
|
||||
| Guix | guix | `guix install skim` |
|
||||
| Void | XBPS | `xbps-install -S skim` |
|
||||
| Windows | winget | `winget install skim` |
|
||||
| Windows | Scoop | `scoop install skim` |
|
||||
| Debian/Ubuntu | apt | see below |
|
||||
| Fedora/RHEL | dnf | see below |
|
||||
|
||||
## Install as Vim plugin
|
||||
<a href="https://repology.org/project/skim-fuzzy-finder/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/skim-fuzzy-finder.svg?columns=4" alt="Packaging status">
|
||||
</a>
|
||||
|
||||
Via vim-plug (recommended):
|
||||
### Debian/Ubuntu
|
||||
|
||||
```vim
|
||||
Plug 'lotabout/skim', { 'dir': '~/.skim', 'do': './install' }
|
||||
A custom APT repository is available and updated automatically during each release:
|
||||
|
||||
1. Import the signing key
|
||||
|
||||
With wget:
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
sudo wget -O /etc/apt/keyrings/skim.asc https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc
|
||||
```
|
||||
|
||||
## Hard Core
|
||||
Or with cURL:
|
||||
|
||||
```sh
|
||||
sudo mkdir -p /etc/apt/keyrings
|
||||
curl -fsSL https://skim-rs.github.io/skim/apt/skim-archive-keyring.asc | sudo tee /etc/apt/keyrings/skim.asc > /dev/null
|
||||
```
|
||||
|
||||
2. Add the repository
|
||||
|
||||
```sh
|
||||
echo 'deb [signed-by=/etc/apt/keyrings/skim.asc] https://skim-rs.github.io/skim/apt ./' | sudo tee /etc/apt/sources.list.d/skim.list
|
||||
sudo apt-get update
|
||||
```
|
||||
|
||||
3. Install
|
||||
|
||||
```sh
|
||||
sudo apt-get install skim
|
||||
```
|
||||
|
||||
Alternatively, `.deb` packages are attached directly to every [release](https://github.com/skim-rs/skim/releases/latest).
|
||||
Download the one matching your architecture and run `sudo dpkg -i skim_*_amd64.deb`
|
||||
|
||||
|
||||
### Fedora/RHEL
|
||||
|
||||
Up-to-date Fedora/RHEL packages are provided via an unofficial community-maintained COPR repository.
|
||||
```bash
|
||||
sudo dnf copr enable sisyphus1813/skim
|
||||
sudo dnf install skim
|
||||
```
|
||||
|
||||
Alternatively, `.rpm` packages are attached directly to every [release](https://github.com/skim-rs/skim/releases/latest).
|
||||
Download it and run `sudo rpm -i skim-*.x86_64.rpm`
|
||||
|
||||
## Manually
|
||||
|
||||
Any of the following applies:
|
||||
|
||||
- Using Git
|
||||
- Using the install script:
|
||||
```sh
|
||||
$ git clone --depth 1 git@github.com:lotabout/skim.git ~/.skim
|
||||
$ ~/.skim/install
|
||||
# Always check the content of the script before running it !
|
||||
$ curl --proto '=https' --tlsv1.2 -LsSf https://github.com/skim-rs/skim/releases/latest/download/skim-installer.sh | sh
|
||||
```
|
||||
- Using Binary: directly [download the sk executable](https://github.com/lotabout/skim/releases).
|
||||
- Using Binary: Simply [download the sk executable](https://github.com/skim-rs/skim/releases) directly.
|
||||
- Install from [crates.io](https://crates.io/): `cargo install skim`
|
||||
- Build Manually
|
||||
- Build Manually:
|
||||
```sh
|
||||
$ git clone --depth 1 git@github.com:lotabout/skim.git ~/.skim
|
||||
$ git clone --depth 1 git@github.com:skim-rs/skim.git ~/.skim
|
||||
$ cd ~/.skim
|
||||
$ cargo install
|
||||
$ cargo build --release
|
||||
$ # put the resulting `target/release/sk` executable on your PATH.
|
||||
$ # Add the resulting `target/release/sk` executable to your PATH
|
||||
```
|
||||
|
||||
You will then have access to:
|
||||
|
||||
- The man page, which you can either write to the correct path or run `man --local-file <(sk --man)`
|
||||
- The shell completions (and optional keybinds), using `source <(sk --shell \<shell> \[--shell-bindings])`, see below for details
|
||||
|
||||
# Usage
|
||||
|
||||
skim can be used as a general filter (like `grep`) or as an interactive
|
||||
interface for invoking commands.
|
||||
Skim can be used either as a general filter (similar to `grep`) or as an interactive
|
||||
interface for running commands.
|
||||
|
||||
## As Vim plugin (on neovim, checkout [fzf-lua](https://github.com/ibhagwan/fzf-lua) with the skim profile)
|
||||
|
||||
Via vim-plug (recommended):
|
||||
|
||||
Install skim, then :
|
||||
|
||||
```vim
|
||||
Plug 'skim-rs/skim'
|
||||
```
|
||||
|
||||
|
||||
## As filter
|
||||
|
||||
Try the following
|
||||
Here are some examples to get you started:
|
||||
|
||||
```bash
|
||||
# directly invoke skim
|
||||
sk
|
||||
|
||||
# or pipe some input to it: (press TAB key select multiple items with -m enabled)
|
||||
# Or pipe some input to it (press TAB key to select multiple items when -m is enabled)
|
||||
vim $(find . -name "*.rs" | sk -m)
|
||||
```
|
||||
The above command will allow you to select files with ".rs" extension and open
|
||||
the ones you selected in Vim.
|
||||
This last command lets you select files with the ".rs" extension and opens
|
||||
your selections in Vim - a great time-saver for developers!
|
||||
|
||||
## As Interactive Interface
|
||||
|
||||
|
|
@ -113,17 +213,63 @@ project directory:
|
|||
|
||||
```sh
|
||||
# works with grep
|
||||
sk --ansi -i -c 'grep -rI --color=always --line-number "{}" .'
|
||||
sk --ansi -i -c 'grep -rI --color=always --line-number {q} .'
|
||||
# works with ack
|
||||
sk --ansi -i -c 'ack --color "{}"'
|
||||
sk --ansi -i -c 'ack --color {q}'
|
||||
# works with ag
|
||||
sk --ansi -i -c 'ag --color "{}"'
|
||||
sk --ansi -i -c 'ag --color {q}'
|
||||
# works with rg
|
||||
sk --ansi -i -c 'rg --color=always --line-number "{}"'
|
||||
sk --ansi -i -c 'rg --color=always --line-number {q}'
|
||||
```
|
||||
|
||||
> **Note**: In these examples, `{q}` will be literally expanded to the current input query (wrapped in single quotes).
|
||||
> This means these examples will search for the exact query string, not fuzzily.
|
||||
> For fuzzy searching, pipe the command output into `sk` without using interactive mode.
|
||||
|
||||

|
||||
|
||||
## Shell Bindings
|
||||
|
||||
Bindings for Fish, Bash and Zsh are available in the `shell` directory:
|
||||
- `completion.{shell}` contains the completion scripts for `sk` cli usage
|
||||
- `key-bindings.{shell}` contains key-binds and shell integrations:
|
||||
- `ctrl-t` to select a file through `sk`
|
||||
- `ctrl-r` to select an history entry through `sk`
|
||||
- `alt-c` to `cd` into a directory selected through `sk`
|
||||
- (not available in `fish`) `**` to complete file paths, for example `ls **<tab>` will show a `sk` widget to select a folder
|
||||
|
||||
To enable these features, source the `key-bindings.{shell}` file and set up completions according to your shell's documentation or see below.
|
||||
|
||||
### Shell Completions
|
||||
|
||||
You can generate shell completions for your preferred shell using the `--shell` flag with one of the supported shells: `bash`, `zsh`, `fish`, `powershell`, or `elvish`:
|
||||
|
||||
#### Option 1: Source directly in your current shell session
|
||||
|
||||
```sh
|
||||
# For bash
|
||||
source <(sk --shell bash)
|
||||
|
||||
# For zsh
|
||||
source <(sk --shell zsh)
|
||||
|
||||
# For fish
|
||||
sk --shell fish | source
|
||||
```
|
||||
|
||||
#### Option 2: Save to a file to be loaded automatically on shell startup
|
||||
|
||||
```sh
|
||||
# For bash, add to ~/.bashrc
|
||||
echo 'source <(sk --shell bash)' >> ~/.bashrc # Or save to ~/.bash_completion
|
||||
|
||||
# For zsh, add to ~/.zshrc
|
||||
sk --shell zsh > ~/.zfunc/_sk # Create ~/.zfunc directory and add to fpath in ~/.zshrc
|
||||
|
||||
# For fish, add to ~/.config/fish/completions/
|
||||
sk --shell fish > ~/.config/fish/completions/sk.fish
|
||||
```
|
||||
|
||||
## Key Bindings
|
||||
|
||||
Some commonly used key bindings:
|
||||
|
|
@ -137,12 +283,12 @@ Some commonly used key bindings:
|
|||
| TAB | Toggle selection and move down (with `-m`) |
|
||||
| Shift-TAB | Toggle selection and move up (with `-m`) |
|
||||
|
||||
For full list of key bindings, check out the [man
|
||||
page](https://github.com/lotabout/skim/blob/master/man/man1/sk.1) (`man sk`).
|
||||
For a complete list of key bindings, refer to the [man
|
||||
page](https://github.com/skim-rs/skim/blob/master/man/man1/sk.1) (`man sk`).
|
||||
|
||||
## Search Syntax
|
||||
|
||||
`skim` borrowed `fzf`'s syntax for matching items:
|
||||
`skim` borrows `fzf`'s syntax for matching items:
|
||||
|
||||
| Token | Match type | Description |
|
||||
|----------|----------------------------|-----------------------------------|
|
||||
|
|
@ -160,10 +306,14 @@ page](https://github.com/lotabout/skim/blob/master/man/man1/sk.1) (`man sk`).
|
|||
- ` | ` means `OR` (note the spaces around `|`). With the term `.md$ |
|
||||
.markdown$`, `skim` will search for items ends with either `.md` or
|
||||
`.markdown`.
|
||||
- `OR` has higher precedence. So `readme .md$ | .markdown$` is grouped into
|
||||
- `OR` has higher precedence. For example, `readme .md$ | .markdown$` is interpreted as
|
||||
`readme AND (.md$ OR .markdown$)`.
|
||||
|
||||
In case that you want to use regular expressions, `skim` provides `regex` mode:
|
||||
- When using the `--split-match` option, each part around spaces or `|` will be matched in a split way:
|
||||
- If the option's value (defaulting to `:`) is absent from the query, do a normal match
|
||||
- If it is present, match everything before to everything before it in the items, and everything after it (including potential other occurrences of the delimiter) to the part after it in the items. This is particularly useful when piping in input from `rg` to match on both file name and content.
|
||||
|
||||
If you prefer using regular expressions, `skim` offers a `regex` mode:
|
||||
|
||||
```sh
|
||||
sk --regex
|
||||
|
|
@ -173,11 +323,44 @@ You can switch to `regex` mode dynamically by pressing `Ctrl-R` (Rotate Mode).
|
|||
|
||||
## exit code
|
||||
|
||||
| Exit Code | Meaning |
|
||||
|-----------|-----------------------------------|
|
||||
| 0 | Exit normally |
|
||||
| 1 | No Match found |
|
||||
| 130 | Abort by Ctrl-C/Ctrl-G/ESC/etc... |
|
||||
| Exit Code | Meaning |
|
||||
|-----------|-------------------------------------|
|
||||
| 0 | Exited normally |
|
||||
| 1 | No Match found |
|
||||
| 130 | Aborted by Ctrl-C/Ctrl-G/ESC/etc... |
|
||||
|
||||
# Tools compatible with `skim`
|
||||
|
||||
These tools are or aim to be compatible with `skim`:
|
||||
|
||||
## [fzf-lua neovim plugin](https://github.com/ibhagwan/fzf-lua)
|
||||
|
||||
A [neovim](https://neovim.io) plugin allowing fzf and skim to be used in a to navigate your code.
|
||||
|
||||
Install it with your package manager, following the README. For instance, with `lazy.nvim`:
|
||||
|
||||
```lua
|
||||
{
|
||||
"ibhagwan/fzf-lua",
|
||||
-- enable `sk` support instead of the default `fzf`
|
||||
opts = {'skim'}
|
||||
}
|
||||
```
|
||||
|
||||
## [nu_plugin_skim](https://github.com/idanarye/nu_plugin_skim)
|
||||
|
||||
A [nushell](https://www.nushell.sh/) plugin to allow for better interaction between skim and nushell.
|
||||
|
||||
Following the instruction in the plugin's README, you can install it with cargo:
|
||||
```nu
|
||||
cargo install nu_plugin_skim
|
||||
plugin add ~/.cargo/bin/nu_plugin_skim
|
||||
```
|
||||
|
||||
## [sqlite extension](https://github.com/tzachar/sqlite_skim)
|
||||
|
||||
An `sqlite` loadable module which enables a `skim_score` function in SQL
|
||||
queries.
|
||||
|
||||
# Customization
|
||||
|
||||
|
|
@ -186,7 +369,7 @@ list of options.
|
|||
|
||||
## Keymap
|
||||
|
||||
Specify the bindings with comma separated pairs (no space allowed), example:
|
||||
Specify the bindings with comma separated pairs (no space allowed). For example:
|
||||
|
||||
```sh
|
||||
sk --bind 'alt-a:select-all,alt-d:deselect-all'
|
||||
|
|
@ -198,29 +381,84 @@ See the _KEY BINDINGS_ section of the man page for details.
|
|||
|
||||
## Sort Criteria
|
||||
|
||||
There are five sort keys for results: `score, index, begin, end, length`, you can
|
||||
There are five sort keys for results: `score, index, begin, end, length`. You can
|
||||
specify how the records are sorted by `sk --tiebreak score,index,-begin` or any
|
||||
other order you want.
|
||||
|
||||
## Color Scheme
|
||||
|
||||
It is a high chance that you are a better artist than me. Luckily you won't
|
||||
be stuck with the default colors, `skim` supports customization of the color scheme.
|
||||
You probably have your own aesthetic preferences! Fortunately, you aren't
|
||||
limited to the default appearance - Skim supports comprehensive customization of its color scheme.
|
||||
|
||||
```sh
|
||||
--color=[BASE_SCHEME][,COLOR:ANSI]
|
||||
```
|
||||
|
||||
The configuration of colors starts with the name of the base color scheme,
|
||||
followed by custom color mappings. For example:
|
||||
Skim also respects the `NO_COLOR` environment variable. Set it to anything and `sk` (and many other terminal apps) will disable all colored output. See [no-color.org](https://no-color.org/) for more details.
|
||||
|
||||
### Available Base Color Schemes
|
||||
|
||||
Skim comes with several built-in color schemes that you can use as a starting point:
|
||||
|
||||
```sh
|
||||
sk --color=dark # Default dark theme (256 colors)
|
||||
sk --color=light # Light theme (256 colors)
|
||||
sk --color=16 # Simple 16-color theme
|
||||
sk --color=bw # Minimal black & white theme (no colors, just styles)
|
||||
sk --color=none # Minimal black & white theme (no colors, no styles)
|
||||
sk --color=molokai # Molokai-inspired theme (256 colors)
|
||||
```
|
||||
|
||||
### Customizing Colors
|
||||
|
||||
You can customize individual UI elements by specifying color values after the base scheme:
|
||||
|
||||
```sh
|
||||
sk --color=current_bg:24
|
||||
sk --color=light,fg:232,bg:255,current_bg:116,info:27
|
||||
```
|
||||
|
||||
See `--color` option in the man page for details.
|
||||
Colors can be specified in several ways:
|
||||
|
||||
- ANSI colors (0-255): `sk --color=fg:232,bg:255`
|
||||
- RGB hex values: `sk --color=fg:#FF0000` (red text)
|
||||
|
||||
### Available Color Customization Options
|
||||
|
||||
The following UI elements can be customized:
|
||||
|
||||
| Element | Description | Example |
|
||||
|--------------------|---------------------------------------------|--------------------------------|
|
||||
| `fg` | Normal text foreground color | `--color=fg:232` |
|
||||
| `bg` | Normal text background color | `--color=bg:255` |
|
||||
| `matched` | Matched text in search results | `--color=matched:108` |
|
||||
| `matched_bg` | Background of matched text | `--color=matched_bg:0` |
|
||||
| `current` | Current line foreground color | `--color=current:254` |
|
||||
| `current_bg` | Current line background color | `--color=current_bg:236` |
|
||||
| `current_match` | Matched text in current line | `--color=current_match:151` |
|
||||
| `current_match_bg` | Background of matched text in current line | `--color=current_match_bg:236` |
|
||||
| `spinner` | Progress indicator color | `--color=spinner:148` |
|
||||
| `info` | Information line color | `--color=info:144` |
|
||||
| `prompt` | Prompt color | `--color=prompt:110` |
|
||||
| `cursor` | Cursor color | `--color=cursor:161` |
|
||||
| `selected` | Selected item marker color | `--color=selected:168` |
|
||||
| `header` | Header text color | `--color=header:109` |
|
||||
| `border` | Border color for preview/layout | `--color=border:59` |
|
||||
| `scrollbar` | Item list scrollbar thumb color | `--color=scrollbar:59` |
|
||||
|
||||
### Examples
|
||||
|
||||
```sh
|
||||
# Use light theme but change the current line background
|
||||
sk --color=light,current_bg:24
|
||||
|
||||
# Custom theme with multiple colors
|
||||
sk --color=dark,matched:#00FF00,current:#FFFFFF,current_bg:#000080
|
||||
|
||||
# High contrast theme
|
||||
sk --color=fg:232,bg:255,matched:160,current:255,current_bg:20
|
||||
```
|
||||
|
||||
For more details, check the man page (`man sk`).
|
||||
|
||||
## Misc
|
||||
|
||||
|
|
@ -231,28 +469,28 @@ See `--color` option in the man page for details.
|
|||
|
||||
## Interactive mode
|
||||
|
||||
With "interactive mode", you could invoke command dynamically. Try out:
|
||||
In **interactive mode**, you can invoke a command dynamically. Try it out:
|
||||
|
||||
```sh
|
||||
sk --ansi -i -c 'rg --color=always --line-number "{}"'
|
||||
sk --ansi -i -c 'rg --color=always --line-number {q}'
|
||||
```
|
||||
|
||||
How it works?
|
||||
### How does it work?
|
||||
|
||||

|
||||

|
||||
|
||||
- Skim could accept two kinds of source: command output or piped input
|
||||
- Skim accepts two kinds of sources: Command output or piped input
|
||||
- Skim has two kinds of prompts: A query prompt to specify the query pattern and a
|
||||
command prompt to specify the "arguments" of the command
|
||||
- `-c` is used to specify the command to execute while defaults to `SKIM_DEFAULT_COMMAND`
|
||||
- `-i` is to tell skim open command prompt on startup, which will show `c>` by default.
|
||||
- `-c` is used to specify the command to execute and defaults to `SKIM_DEFAULT_COMMAND`
|
||||
- `-i` tells skim to open command prompt on startup, which will show `c>` by default.
|
||||
|
||||
If you want to further narrow down the results returned by the command, press
|
||||
To further narrow down the results returned by the command, press
|
||||
`Ctrl-Q` to toggle interactive mode.
|
||||
|
||||
## Executing external programs
|
||||
|
||||
You can set up key bindings for starting external processes without leaving skim (`execute`, `execute-silent`).
|
||||
You can configure key bindings to start external processes without leaving Skim (`execute`, `execute-silent`).
|
||||
|
||||
```sh
|
||||
# Press F1 to open the file with less without leaving skim
|
||||
|
|
@ -260,20 +498,29 @@ You can set up key bindings for starting external processes without leaving skim
|
|||
sk --bind 'f1:execute(less -f {}),ctrl-y:execute-silent(echo {} | pbcopy)+abort'
|
||||
```
|
||||
|
||||
## Algorithms
|
||||
|
||||
Skim offers multiple algorithms, check the help or manpage for an exhaustive list. Among them are:
|
||||
- `skim_v2`, the default algorithm, loosely based on `fzf`'s algorithm
|
||||
- `frizbee`, uses [frizbee](https://crates.io/frizbee), the typo-resistant algorithm from the [blink.cmp](https://github.com/saghen/blink.cmp) neovim plugin
|
||||
- `fzy`, based on [fzy](https://github.com/jhawthorn/fzy/)'s algorithm expanded for basic typo-resistance
|
||||
- `arinae`, skim's newest algorithm, designed in-house with typo-resistance in mind, expanding on all the above to make typo-resistant matching feel more natural while keeping the per-item performance up to the best standards
|
||||
|
||||
## Preview Window
|
||||
|
||||
This is a great feature of fzf that skim borrows. For example, we use 'ag' to
|
||||
find the matched lines, once we narrow down to the target lines, we want to
|
||||
find the matched lines, and once we narrow down to the target lines, we want to
|
||||
finally decide which lines to pick by checking the context around the line.
|
||||
`grep` and `ag` has an option `--context`, skim can do better with preview
|
||||
window. For example:
|
||||
`grep` and `ag` have the option `--context`, and skim can make use of `--context` for
|
||||
a better preview window. For example:
|
||||
|
||||
```sh
|
||||
sk --ansi -i -c 'ag --color "{}"' --preview "preview.sh {}"
|
||||
sk --ansi -i -c 'ag --color {q}' --preview "preview.sh {}"
|
||||
```
|
||||
|
||||
(Note the [preview.sh](https://github.com/junegunn/fzf.vim/blob/master/bin/preview.sh) is a script to print the context given filename:lines:columns)
|
||||
You got things like this:
|
||||
(Note that [preview.sh](https://github.com/junegunn/fzf.vim/blob/master/bin/preview.sh) is a script to print the context given filename:lines:columns)
|
||||
|
||||
You get things like this:
|
||||
|
||||

|
||||
|
||||
|
|
@ -285,13 +532,12 @@ command to get the output, and print the output on the preview window.
|
|||
|
||||
Sometimes you don't need the whole line for invoking the command. In this case
|
||||
you can use `{}`, `{1..}`, `{..3}` or `{1..5}` to select the fields. The
|
||||
syntax is explained in the section "Fields Support".
|
||||
syntax is explained in the section [Fields Support](#filds-support).
|
||||
|
||||
Last, you might want to configure the position of preview windows, use
|
||||
`--preview-window`.
|
||||
Lastly, you might want to configure the position of preview window with `--preview-window`:
|
||||
- `--preview-window up:30%` to put the window in the up position with height
|
||||
30% of the total height of skim.
|
||||
- `--preview-window left:10:wrap`, to specify the `wrap` allows the preview
|
||||
- `--preview-window left:10:wrap` to specify the `wrap` allows the preview
|
||||
window to wrap the output of the preview command.
|
||||
- `--preview-window wrap:hidden` to hide the preview window at startup, later
|
||||
it can be shown by the action `toggle-preview`.
|
||||
|
|
@ -312,12 +558,12 @@ but not matching line number or column number.
|
|||
|
||||
You can use `sk --delimiter ':' --nth 1` to achieve this.
|
||||
|
||||
Also you can use `--with-nth` to re-arrange the order of fields.
|
||||
You can also use `--with-nth` to re-arrange the order of fields.
|
||||
|
||||
**Range Syntax**
|
||||
|
||||
- `<num>` -- to specify the `num`-th fields, starting with 1.
|
||||
- `start..` -- starting from the `start`-th fields, and the rest.
|
||||
- `start..` -- starting from the `start`-th fields and the rest.
|
||||
- `..end` -- starting from the `0`-th field, all the way to `end`-th field,
|
||||
including `end`.
|
||||
- `start..end` -- starting from `start`-th field, all the way to `end`-th
|
||||
|
|
@ -331,9 +577,14 @@ First, add skim into your `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
skim = "*"
|
||||
skim = { version = "<version>", default-features = false, features = [..] }
|
||||
```
|
||||
|
||||
_Note on features_:
|
||||
- the `cli` feature is required to use skim as a cli, it *should* not be needed when using it as a library.
|
||||
|
||||
### Basic usage
|
||||
|
||||
Then try to run this simple example:
|
||||
|
||||
```rust
|
||||
|
|
@ -343,7 +594,7 @@ use std::io::Cursor;
|
|||
|
||||
pub fn main() {
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.height(Some("50%"))
|
||||
.height("50%")
|
||||
.multi(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
|
@ -361,11 +612,18 @@ pub fn main() {
|
|||
.unwrap_or_else(|| Vec::new());
|
||||
|
||||
for item in selected_items.iter() {
|
||||
print!("{}{}", item.output(), "\n");
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Fine-grained usage
|
||||
|
||||
You can also gain fine-grained usage of skim as a library using `tokio` and async code, allowing you to dynamically interact with
|
||||
|
||||
|
||||
### Internal workings
|
||||
|
||||
Given an `Option<SkimItemReceiver>`, skim will read items accordingly, do its
|
||||
job and bring us back the user selection including the selected items, the
|
||||
query, etc. Note that:
|
||||
|
|
@ -378,17 +636,25 @@ Trait `SkimItem` is provided to customize how a line could be displayed,
|
|||
compared and previewed. It is implemented by default for `AsRef<str>`
|
||||
|
||||
Plus, `SkimItemReader` is a helper to convert a `BufRead` into
|
||||
`SkimItemReceiver` (we can easily turn a `File` for `String` into `BufRead`).
|
||||
So that you could deal with strings or files easily.
|
||||
`SkimItemReceiver` (we can easily turn a `File` or `String` into `BufRead`),
|
||||
so that you could deal with strings or files easily.
|
||||
|
||||
Check more examples under [examples/](https://github.com/lotabout/skim/tree/master/examples) directory.
|
||||
Check out more examples under the [examples/](https://github.com/skim-rs/skim/tree/master/skim/examples) directory.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
This benchmarks runs the interactive interface in a tmux session, and waits for the UI to stabilize.
|
||||
|
||||

|
||||
|
||||
You can generate the graphs by using `just bench-plot` or running the recipe manually in GNU bash.
|
||||
|
||||
# FAQ
|
||||
|
||||
## How to ignore files?
|
||||
|
||||
Skim invokes `find .` to fetch a list of files for filtering. You can override
|
||||
that by setting the environment variable `SKIM_DEFAULT_COMMAND`. For example:
|
||||
this by setting the environment variable `SKIM_DEFAULT_COMMAND`. For example:
|
||||
|
||||
```sh
|
||||
$ SKIM_DEFAULT_COMMAND="fd --type f || git ls-tree -r --name-only HEAD || rg --files || find ."
|
||||
|
|
@ -399,38 +665,119 @@ You could put it in your `.bashrc` or `.zshrc` if you like it to be default.
|
|||
|
||||
## Some files are not shown in Vim plugin
|
||||
|
||||
If you use the Vim plugin and execute the `:SK` command, you might find some
|
||||
If you use the Vim plugin and execute the `:SK` command, you may find some
|
||||
of your files not shown.
|
||||
|
||||
As described in [#3](https://github.com/lotabout/skim/issues/3), in the Vim
|
||||
As described in [#3](https://github.com/skim-rs/skim/issues/3), in the Vim
|
||||
plugin, `SKIM_DEFAULT_COMMAND` is set to the command by default:
|
||||
|
||||
```vim
|
||||
let $SKIM_DEFAULT_COMMAND = "git ls-tree -r --name-only HEAD || rg --files || ag -l -g \"\" || find ."
|
||||
```
|
||||
|
||||
That means, the files not recognized by git will not shown. Either override the
|
||||
default with `let $SKIM_DEFAULT_COMMAND = ''` or find the missing file by
|
||||
This means files not recognized by git won't be shown. You can either override the
|
||||
default with `let $SKIM_DEFAULT_COMMAND = ''` or locate the missing files by
|
||||
yourself.
|
||||
|
||||
# Differences to fzf
|
||||
# Differences from fzf
|
||||
|
||||
[fzf](https://github.com/junegunn/fzf) is a command-line fuzzy finder written
|
||||
in Go and [skim](https://github.com/lotabout/skim) tries to implement a new one
|
||||
in Go and [skim](https://github.com/skim-rs/skim) tries to implement a new one
|
||||
in Rust!
|
||||
|
||||
This project is written from scratch. Some decisions of implementation are
|
||||
different from fzf. For example:
|
||||
|
||||
1. `skim` is a binary as well as a library while fzf is only a binary.
|
||||
2. `skim` has an interactive mode.
|
||||
3. `skim` supports pre-selection
|
||||
4. The fuzzy search algorithm is different.
|
||||
5. ~~UI of showing matched items. `fzf` will show only the range matched while
|
||||
`skim` will show each character matched.~~ (fzf has this now)
|
||||
6. ~~`skim`'s range syntax is Git style~~: now it is the same with fzf.
|
||||
1. `skim` has an interactive mode.
|
||||
2. `skim` supports pre-selection.
|
||||
3. The fuzzy search algorithm is different.
|
||||
|
||||
More generally, `skim`'s maintainers allow themselves some freedom of implementation.
|
||||
The goal is to keep `skim` as feature-full as `fzf` is, but the command flags might differ.
|
||||
|
||||
# How to contribute
|
||||
|
||||
[Create new issues](https://github.com/lotabout/skim/issues/new) if you meet any bugs
|
||||
[Create new issues](https://github.com/skim-rs/skim/issues/new) if you encounter any bugs
|
||||
or have any ideas. Pull requests are warmly welcomed.
|
||||
|
||||
## Windows compatibility testing
|
||||
|
||||
A `Vagrantfile` is included to spin up a headless Windows Server 2022 Core VM for testing
|
||||
Windows compatibility without needing a GUI. It requires [VirtualBox](https://www.virtualbox.org/)
|
||||
and [Vagrant](https://www.vagrantup.com/) on your host (`vagrant` is included in the Nix dev
|
||||
shell via `flake.nix`).
|
||||
|
||||
```sh
|
||||
vagrant up # First boot: downloads the box and provisions (~15–20 min)
|
||||
ssh -p 2222 vagrant@localhost # Password: vagrant
|
||||
```
|
||||
|
||||
Inside the VM, the project root is mounted at `C:\vagrant`:
|
||||
|
||||
```powershell
|
||||
cd C:\vagrant
|
||||
cargo build
|
||||
cargo test
|
||||
```
|
||||
|
||||
Subsequent boots are fast — provisioning only runs once:
|
||||
|
||||
```sh
|
||||
vagrant halt # Stop the VM
|
||||
vagrant up # Resume
|
||||
vagrant destroy # Delete the VM entirely
|
||||
```
|
||||
|
||||
# Troubleshooting
|
||||
|
||||
To troubleshoot what's happening, you can set the environment variable `SKIM_LOG` or the flag `--log-level` to either `debug` or even `trace`, and set the environment variable `SKIM_LOG_FILE` or the flag `--log-file` to a path. You can then read those logs during or after the execution to better understand what's happening. Don't hesitate to add those logs to an issue if you need help.
|
||||
|
||||
## No line feed issues with nix, FreeBSD, termux
|
||||
|
||||
If you encounter display issues like:
|
||||
|
||||
```bash
|
||||
$ for n in {1..10}; do echo "$n"; done | sk
|
||||
0/10 0/0.> 10/10 10 9 8 7 6 5 4 3 2> 1
|
||||
```
|
||||
|
||||
For example
|
||||
|
||||
- https://github.com/skim-rs/skim/issues/412
|
||||
- https://github.com/skim-rs/skim/issues/455
|
||||
|
||||
You need to set TERMINFO or TERMINFO_DIRS to the path of a correct terminfo database path
|
||||
|
||||
For example, with termux, you can add this in your bashrc:
|
||||
|
||||
```
|
||||
export TERMINFO=/data/data/com.termux/files/usr/share/terminfo
|
||||
```
|
||||
|
||||
# Benchmarks
|
||||
|
||||
## Interactive benchmark (`cli`)
|
||||
|
||||
The `cli` bench benchmarks skim (or any compatible binary) against other versions or fzf by running the interactive interface inside a tmux session and polling the status line until the matched count stabilises. This is by no means a precise or foolproof measurement, but it has the added benefit of benchmarking against `fzf` and of providing resource metrics (peak RSS and CPU).
|
||||
|
||||
```sh
|
||||
cargo bench --bench cli -- run # defaults: sk, 1 M items, query "test"
|
||||
cargo bench --bench cli -- run sk -n 500000 -q foo # bare name resolved via $PATH
|
||||
cargo bench --bench cli -- run ./old/sk ./new/sk -r 5 # compare two binaries, 5 runs each
|
||||
cargo bench --bench cli -- run sk -f input.txt -q search # use an existing file
|
||||
cargo bench --bench cli -- generate -f testdata.txt -n 2000000 # generate input file and exit
|
||||
cargo bench --bench cli -- run sk --perf # record perf data (auto-named file)
|
||||
cargo bench --bench cli -- run sk --strace # record strace data (auto-named file)
|
||||
cargo bench --bench cli -- run sk -p perf.data # record perf data to perf.data
|
||||
cargo bench --bench cli -- run sk -j # JSON output
|
||||
cargo bench --bench cli -- run sk -r 3 -- --tiebreak=index # pass extra flags to sk
|
||||
```
|
||||
|
||||
Binary names are resolved to absolute paths via `which` before use, so bare names like `sk` or `fzf` work as long as they are on `$PATH`.
|
||||
|
||||
### Criterion benchmarks
|
||||
|
||||
Criterion benchmarks are available to measure skim's performance more precisely.
|
||||
To run them, you need to generate input data using `cargo bench --bench cli -- -g benches/fixtures/10M.txt -n 10000000 && cargo bench --bench cli -- -g benches/fixtures/1M.txt -n 1000000`, then run `cargo bench -j 1`.
|
||||
|
||||
These will run for several minutes.
|
||||
|
|
|
|||
121
Vagrantfile
vendored
Normal file
121
Vagrantfile
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
#
|
||||
# Windows development VM for testing skim on Windows without a GUI.
|
||||
#
|
||||
# Prerequisites (host, NixOS):
|
||||
# virtualisation.libvirtd.enable = true;
|
||||
# users.users.<you>.extraGroups = [ "libvirtd" ]; # then log out/in
|
||||
#
|
||||
# Usage:
|
||||
# vagrant up # First boot: downloads box, provisions (~15-20 min)
|
||||
# vagrant up --provision # Re-run provisioning on existing VM
|
||||
# vagrant ssh # SSH in via vagrant
|
||||
# vagrant ssh-config # Show IP/key if you prefer a raw ssh command
|
||||
# vagrant halt # Stop the VM
|
||||
# vagrant destroy # Delete the VM
|
||||
#
|
||||
# Inside the VM:
|
||||
# cd C:\vagrant # Project root (synced from host, see note below)
|
||||
# cargo build # Build skim
|
||||
# cargo test # Run tests
|
||||
#
|
||||
# Note: The first `vagrant up` requires internet access on the VM to install
|
||||
# packages via Chocolatey.
|
||||
|
||||
Vagrant.configure("2") do |config|
|
||||
# Windows Server 2022 Core — minimal footprint, no desktop GUI.
|
||||
# Box source: https://app.vagrantup.com/gusztavvargadr/boxes/windows-server-2022-standard-core
|
||||
config.vm.box = "gusztavvargadr/windows-server-2022-standard-core"
|
||||
|
||||
# Vagrant manages the VM via WinRM (the Windows default).
|
||||
config.vm.communicator = "winrm"
|
||||
config.winrm.username = "vagrant"
|
||||
config.winrm.password = "vagrant"
|
||||
config.winrm.timeout = 600 # provisioning can take a while on first boot
|
||||
|
||||
config.vm.provider "libvirt" do |lv|
|
||||
lv.driver = "kvm"
|
||||
lv.memory = 2048
|
||||
lv.cpus = 2
|
||||
end
|
||||
|
||||
# rsync is used for the synced folder because libvirt has no native
|
||||
# shared-folder support for Windows guests. rsync must be present on the
|
||||
# guest, so the folder is disabled on boot and synced via a post-provision
|
||||
# trigger (after Chocolatey installs rsync below).
|
||||
# Re-sync manually at any time with: vagrant rsync
|
||||
# rsync is used because libvirt has no native shared-folder support for
|
||||
# Windows guests. On a brand-new VM the very first `vagrant up` will fail
|
||||
# the rsync step (rsync not yet installed on the guest); run
|
||||
# `vagrant provision && vagrant rsync` to recover, or just
|
||||
# `vagrant destroy && vagrant up` after the box is cached locally.
|
||||
# cwRsync (the Windows rsync from Chocolatey) uses Cygwin paths, so the
|
||||
# guest path must use /cygdrive/c/... rather than a bare /vagrant.
|
||||
config.vm.synced_folder ".", "/cygdrive/c/vagrant", type: "rsync",
|
||||
rsync__exclude: [".git/", "target/", ".jj/"],
|
||||
rsync__args: ["--verbose", "--archive", "--delete", "--copy-links", "--no-owner", "--no-group"]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provisioning: configure OpenSSH + install Rust toolchain via Chocolatey.
|
||||
# The box ships with Win32-OpenSSH already present, so we only configure it.
|
||||
# Runs once on `vagrant up`; re-run with `vagrant provision`.
|
||||
# ---------------------------------------------------------------------------
|
||||
config.vm.provision "shell", privileged: true, inline: <<-'POWERSHELL'
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# --- Chocolatey -------------------------------------------------------------
|
||||
Write-Host "==> Installing Chocolatey..."
|
||||
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
[System.Net.ServicePointManager]::SecurityProtocol =
|
||||
[System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||
Invoke-Expression (
|
||||
(New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')
|
||||
)
|
||||
}
|
||||
|
||||
# --- OpenSSH Server ---------------------------------------------------------
|
||||
# The box ships with Win32-OpenSSH binaries at C:\Program Files\OpenSSH-Win64.
|
||||
# Re-run install-sshd.ps1 to register the service (idempotent; safe to
|
||||
# re-run if the service is already present).
|
||||
Write-Host "==> Registering and starting sshd..."
|
||||
& "C:\Program Files\OpenSSH-Win64\install-sshd.ps1"
|
||||
Set-Service -Name sshd -StartupType Automatic
|
||||
Start-Service -Name sshd
|
||||
|
||||
# Use PowerShell as the default shell for SSH sessions.
|
||||
$regPath = "HKLM:\SOFTWARE\OpenSSH"
|
||||
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
|
||||
Set-ItemProperty -Path $regPath -Name DefaultShell `
|
||||
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
|
||||
|
||||
# Allow inbound SSH through the Windows firewall.
|
||||
$rule = Get-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -ErrorAction SilentlyContinue
|
||||
if (-not $rule) {
|
||||
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP" `
|
||||
-DisplayName "OpenSSH Server (sshd)" `
|
||||
-Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22
|
||||
}
|
||||
|
||||
# --- Rust, Git, rsync -------------------------------------------------------
|
||||
Write-Host "==> Installing Rust, Git, rsync, and MinGW..."
|
||||
choco install -y rust git rsync mingw
|
||||
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") +
|
||||
";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
|
||||
|
||||
# Add MinGW bin to the persistent system PATH so dlltool.exe is found in
|
||||
# SSH sessions (which don't run the Chocolatey shim refresh).
|
||||
$mingwBin = "C:\ProgramData\mingw64\mingw64\bin"
|
||||
$machinePath = [System.Environment]::GetEnvironmentVariable("Path", "Machine")
|
||||
if ($machinePath -notlike "*$mingwBin*") {
|
||||
[System.Environment]::SetEnvironmentVariable("Path", "$machinePath;$mingwBin", "Machine")
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "==> Provisioning complete."
|
||||
Write-Host " SSH into the VM: vagrant ssh-config (then ssh to the reported IP)"
|
||||
Write-Host " Build skim: cd C:\vagrant && cargo build"
|
||||
POWERSHELL
|
||||
end
|
||||
1855
benches/cli.rs
Normal file
1855
benches/cli.rs
Normal file
File diff suppressed because it is too large
Load diff
285
benches/filter.rs
Normal file
285
benches/filter.rs
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
#![allow(missing_docs, clippy::pedantic)]
|
||||
|
||||
use std::fs;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use skim::Typos;
|
||||
use skim::helper::item::DefaultSkimItem;
|
||||
use skim::prelude::*;
|
||||
|
||||
const CHUNK_SIZE: usize = 1024;
|
||||
fn load_lines(file: &str) -> Vec<String> {
|
||||
let data = fs::read_to_string(format!("benches/fixtures/{file}")).expect("{file} missing");
|
||||
data.lines().map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
fn prepare(file: &str, opt_builder: &mut SkimOptionsBuilder) -> (SkimOptions, SkimItemReceiver) {
|
||||
let lines = load_lines(file);
|
||||
let opts = opt_builder.build().unwrap();
|
||||
let (tx, rx) = unbounded();
|
||||
let mut chunk_size = 0;
|
||||
let mut chunk = Vec::new();
|
||||
for line in lines {
|
||||
if chunk_size >= CHUNK_SIZE {
|
||||
tx.send(chunk).unwrap();
|
||||
chunk_size = 0;
|
||||
chunk = Vec::new();
|
||||
}
|
||||
chunk.push(Arc::new(DefaultSkimItem::from(line)) as Arc<dyn SkimItem>);
|
||||
}
|
||||
tx.send(chunk).unwrap();
|
||||
(opts, rx)
|
||||
}
|
||||
|
||||
fn criterion_benchmark_10m(c: &mut Criterion) {
|
||||
c.bench_function("filter_10M_regex", |b| {
|
||||
b.iter_batched(
|
||||
|| prepare("10M.txt", SkimOptionsBuilder::default().filter("test").regex(true)),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("filter_10M_frizbee", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("filter_10M_frizbee_typos", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_clangd", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Clangd),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_fzy", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_fzy_typos", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_arinae", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_10M_arinae_typos", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"10M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
fn criterion_benchmark_1m(c: &mut Criterion) {
|
||||
c.bench_function("filter_1M_regex", |b| {
|
||||
b.iter_batched(
|
||||
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("test").regex(true)),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("filter_1M_frizbee", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("filter_1M_frizbee_typos", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_clangd", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Clangd),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_fzy", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_fzy_typos", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Fzy)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_arinae", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Disabled),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
c.bench_function("filter_1M_arinae_typos", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
prepare(
|
||||
"1M.txt",
|
||||
SkimOptionsBuilder::default()
|
||||
.filter("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.typos(Typos::Smart),
|
||||
)
|
||||
},
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("filter_1M_andor", |b| {
|
||||
b.iter_batched(
|
||||
|| prepare("1M.txt", SkimOptionsBuilder::default().filter("boot foo | mnt foo")),
|
||||
|(opts, rx)| Skim::run_with(opts, Some(rx)),
|
||||
criterion::BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
name = benches_10m;
|
||||
config = Criterion::default().sample_size(10);
|
||||
targets = criterion_benchmark_10m
|
||||
);
|
||||
criterion_group!(
|
||||
name = benches_1m;
|
||||
config = Criterion::default().sample_size(100);
|
||||
targets = criterion_benchmark_1m
|
||||
);
|
||||
criterion_main!(benches_1m, benches_10m);
|
||||
2
benches/fixtures/.gitignore
vendored
Normal file
2
benches/fixtures/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
!.gitignore
|
||||
*
|
||||
67
benches/gungraun.rs
Normal file
67
benches/gungraun.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
#![allow(missing_docs, clippy::pedantic)]
|
||||
|
||||
use gungraun::{library_benchmark, library_benchmark_group, main};
|
||||
use std::fs;
|
||||
use std::hint::black_box;
|
||||
|
||||
use skim::CaseMatching;
|
||||
use skim::fuzzy_matcher::FuzzyMatcher;
|
||||
use skim::fuzzy_matcher::arinae::ArinaeMatcher;
|
||||
use skim::fuzzy_matcher::frizbee::FrizbeeMatcher;
|
||||
use skim::prelude::SkimMatcherV2;
|
||||
|
||||
fn load_lines() -> Vec<String> {
|
||||
let data = fs::read_to_string("benches/fixtures/1M.txt").expect("1M.txt missing");
|
||||
data.lines().map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn bench_matcher(m: impl FuzzyMatcher, lines: Vec<String>) -> u64 {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_indices(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
#[library_benchmark]
|
||||
fn skim_v2() -> u64 {
|
||||
bench_matcher(SkimMatcherV2::default().smart_case(), black_box(load_lines()))
|
||||
}
|
||||
#[library_benchmark]
|
||||
fn frizbee() -> u64 {
|
||||
bench_matcher(
|
||||
FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(0)),
|
||||
black_box(load_lines()),
|
||||
)
|
||||
}
|
||||
#[library_benchmark]
|
||||
fn frizbee_typos() -> u64 {
|
||||
bench_matcher(
|
||||
FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(1)),
|
||||
black_box(load_lines()),
|
||||
)
|
||||
}
|
||||
#[library_benchmark]
|
||||
fn arinae() -> u64 {
|
||||
bench_matcher(
|
||||
ArinaeMatcher::new(CaseMatching::Smart, false, false),
|
||||
black_box(load_lines()),
|
||||
)
|
||||
}
|
||||
#[library_benchmark]
|
||||
fn arinae_typos() -> u64 {
|
||||
bench_matcher(
|
||||
ArinaeMatcher::new(CaseMatching::Smart, true, false),
|
||||
black_box(load_lines()),
|
||||
)
|
||||
}
|
||||
|
||||
library_benchmark_group!(
|
||||
name = benches,
|
||||
benchmarks = [skim_v2, frizbee, frizbee_typos, arinae, arinae_typos]
|
||||
);
|
||||
|
||||
main!(library_benchmark_groups = benches);
|
||||
151
benches/matcher_micro.rs
Normal file
151
benches/matcher_micro.rs
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
#![allow(missing_docs, clippy::pedantic)]
|
||||
|
||||
//! Microbenchmark that isolates the fuzzy matcher DP from all other overhead
|
||||
//! (I/O, threading, sorting).
|
||||
|
||||
use std::fs;
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
|
||||
use skim::CaseMatching;
|
||||
use skim::fuzzy_matcher::FuzzyMatcher;
|
||||
use skim::fuzzy_matcher::arinae::ArinaeMatcher;
|
||||
#[cfg(feature = "frizbee")]
|
||||
use skim::fuzzy_matcher::frizbee::FrizbeeMatcher;
|
||||
use skim::prelude::SkimMatcherV2;
|
||||
|
||||
fn load_lines() -> Vec<String> {
|
||||
let data = fs::read_to_string("benches/fixtures/100K.txt").expect("100K.txt missing");
|
||||
data.lines().map(|l| l.to_string()).collect()
|
||||
}
|
||||
|
||||
fn bench_matcher(c: &mut Criterion) {
|
||||
let lines = load_lines();
|
||||
|
||||
c.bench_function("micro_skim_v2", |b| {
|
||||
let m = SkimMatcherV2::default().smart_case();
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_indices(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("micro_frizbee_score", |b| {
|
||||
let m = FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(0));
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_match(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("micro_frizbee", |b| {
|
||||
let m = FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(0));
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_indices(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("micro_typos_frizbee", |b| {
|
||||
let m = FrizbeeMatcher::default().case(CaseMatching::Smart).max_typos(Some(1));
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_indices(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
c.bench_function("micro_arinae", |b| {
|
||||
let m = ArinaeMatcher::new(CaseMatching::Smart, false, false);
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_indices(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
c.bench_function("micro_arinae_range", |b| {
|
||||
let m = ArinaeMatcher::new(CaseMatching::Smart, false, false);
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_match_range(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
c.bench_function("micro_arinae_score", |b| {
|
||||
let m = ArinaeMatcher::new(CaseMatching::Smart, false, false);
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_match(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
c.bench_function("micro_typos_arinae", |b| {
|
||||
let m = ArinaeMatcher::new(CaseMatching::Smart, true, false);
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_indices(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
c.bench_function("micro_typos_arinae_range", |b| {
|
||||
let m = ArinaeMatcher::new(CaseMatching::Smart, true, false);
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_match_range(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
c.bench_function("micro_typos_arinae_score", |b| {
|
||||
let m = ArinaeMatcher::new(CaseMatching::Smart, true, false);
|
||||
b.iter(|| {
|
||||
let mut count = 0u64;
|
||||
for line in &lines {
|
||||
if m.fuzzy_match(line, "test").is_some() {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
count
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_matcher);
|
||||
criterion_main!(benches);
|
||||
291
benches/partial.rs
Normal file
291
benches/partial.rs
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
#![allow(missing_docs, clippy::pedantic)]
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Cursor, Stderr};
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::Parser as _;
|
||||
use criterion::{BatchSize, Criterion, Throughput, criterion_group, criterion_main};
|
||||
|
||||
use ratatui::backend::TestBackend;
|
||||
use ratatui::prelude::CrosstermBackend;
|
||||
use skim::prelude::*;
|
||||
|
||||
/// Small inline fixture — fast to load, good for latency benchmarks.
|
||||
const SMALL_ITEMS: &[&str] = &[
|
||||
"src/main.rs",
|
||||
"src/lib.rs",
|
||||
"src/options.rs",
|
||||
"src/skim.rs",
|
||||
"benches/partial.rs",
|
||||
"tests/common/insta.rs",
|
||||
"Cargo.toml",
|
||||
"README.md",
|
||||
];
|
||||
|
||||
/// Path to the medium fixture shipped with the repo (≈664 lines).
|
||||
const FIXTURE_DEFAULT: &str = "benches/fixtures/default.txt";
|
||||
|
||||
/// Path to the large fixture (100 000 lines). Skip in low-time CI runs if absent.
|
||||
const FIXTURE_100K: &str = "benches/fixtures/100K.txt";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Read a fixture file into memory and return each non-empty line as a `String`.
|
||||
fn load_fixture(path: &str) -> Vec<String> {
|
||||
let f = File::open(path).unwrap_or_else(|e| panic!("cannot open fixture {path}: {e}"));
|
||||
BufReader::new(f)
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.filter(|l| !l.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Build a newline-separated `Vec<u8>` from a slice of items.
|
||||
fn items_to_bytes(items: &[impl AsRef<str>]) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
for item in items {
|
||||
buf.extend_from_slice(item.as_ref().as_bytes());
|
||||
buf.push(b'\n');
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
/// Create a `SkimItemReceiver` that will produce `items` via the full
|
||||
/// `SkimItemReader` pipeline (the same path used when skim reads stdin).
|
||||
fn make_receiver(items: &[impl AsRef<str>], options: &SkimOptions) -> SkimItemReceiver {
|
||||
let reader_opts = SkimItemReaderOption::from_options(options);
|
||||
let item_reader = SkimItemReader::new(reader_opts);
|
||||
item_reader.of_bufread(Cursor::new(items_to_bytes(items)))
|
||||
}
|
||||
|
||||
/// Spin-wait until the reader is done and the matcher has stopped (mirrors the
|
||||
/// logic in `tests/common/insta.rs`). Panics after `timeout`.
|
||||
fn wait_until_done(skim: &mut Skim<TestBackend>, timeout: Duration) {
|
||||
let start = std::time::Instant::now();
|
||||
while !skim.reader_done() {
|
||||
assert!(start.elapsed() < timeout, "timeout waiting for reader");
|
||||
skim.check_reader();
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
skim.check_reader();
|
||||
while !skim.app().matcher_control.stopped() {
|
||||
assert!(start.elapsed() < timeout, "timeout waiting for matcher");
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Benchmark group
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase 0 — Options
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
c.bench_function("parse_options", |b| {
|
||||
b.iter(|| SkimOptions::parse_from(Vec::<&str>::new()));
|
||||
});
|
||||
|
||||
// `from_env` merges SKIM_DEFAULT_OPTIONS, SKIM_OPTIONS_FILE, and argv.
|
||||
// In a clean test environment it should behave identically to parse_from,
|
||||
// but the code path is different and may regress independently.
|
||||
c.bench_function("options_from_env", |b| {
|
||||
b.iter(SkimOptions::from_env);
|
||||
});
|
||||
|
||||
// `build()` post-processes raw options: expands keymaps, normalises
|
||||
// tiebreak / layout, reads history files, etc.
|
||||
c.bench_function("options_build", |b| {
|
||||
b.iter_batched(
|
||||
|| SkimOptions::parse_from(Vec::<&str>::new()),
|
||||
|opts| opts.build(),
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase 1 — SkimItemReader pipeline construction
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
// Building the reader option struct from a SkimOptions (called in sk_main
|
||||
// before Skim::init).
|
||||
c.bench_function("item_reader_option_from_options", |b| {
|
||||
let opts = SkimOptions::default().build();
|
||||
b.iter(|| SkimItemReaderOption::from_options(&opts));
|
||||
});
|
||||
|
||||
// Constructing a new SkimItemReader (spawns a thread-pool).
|
||||
c.bench_function("item_reader_new", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let opts = SkimOptions::default().build();
|
||||
SkimItemReaderOption::from_options(&opts)
|
||||
},
|
||||
SkimItemReader::new,
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// `of_bufread` starts the I/O dispatcher and pool threads but does NOT
|
||||
// wait for all items to be processed; it just returns the receiver.
|
||||
// Benchmark with the default fixture to get a realistic buffer.
|
||||
c.bench_function("of_bufread_setup", |b| {
|
||||
let opts = SkimOptions::default().build();
|
||||
let data = items_to_bytes(&load_fixture(FIXTURE_DEFAULT));
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let reader_opts = SkimItemReaderOption::from_options(&opts);
|
||||
(SkimItemReader::new(reader_opts), data.clone())
|
||||
},
|
||||
|(reader, bytes)| {
|
||||
// Calling of_bufread starts the background threads; we
|
||||
// deliberately drop the receiver immediately so they clean up.
|
||||
let _rx = reader.of_bufread(Cursor::new(bytes));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase 2 — Skim::init (unchanged from before, kept for comparison)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
c.bench_function("init", |b| {
|
||||
b.iter_batched(
|
||||
|| SkimOptions::default().build(),
|
||||
|options: SkimOptions| Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(options, None),
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
c.bench_function("init_with_source", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let (_tx, rx) = bounded(8);
|
||||
(SkimOptions::default().build(), rx)
|
||||
},
|
||||
|input: (SkimOptions, SkimItemReceiver)| {
|
||||
Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(input.0, Some(input.1))
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase 2+3 — Skim::init + start
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
c.bench_function("start", |b| {
|
||||
b.iter_batched(
|
||||
|| Skim::<CrosstermBackend<BufWriter<Stderr>>>::init(SkimOptions::default().build(), None).unwrap(),
|
||||
|mut skim: Skim| skim.start(),
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase 3+4 — ingest + match with realistic data
|
||||
// -----------------------------------------------------------------------
|
||||
// These benchmarks measure end-to-end reader+matcher throughput: time from
|
||||
// start() until all items are ingested and the matcher has finished its
|
||||
// first pass. They are the closest in-process analog to the CLI startup
|
||||
// time measured by `cargo bench --bench cli`.
|
||||
|
||||
// Small inline fixture (8 items) — baseline latency floor.
|
||||
c.bench_function("ingest_and_match_small", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let opts = SkimOptions::default().build();
|
||||
let rx = make_receiver(SMALL_ITEMS, &opts);
|
||||
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
|
||||
},
|
||||
|mut skim: Skim<TestBackend>| {
|
||||
skim.start();
|
||||
wait_until_done(&mut skim, Duration::from_secs(5));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
|
||||
// Medium fixture (≈664 lines from FIXTURE_DEFAULT).
|
||||
{
|
||||
let items = load_fixture(FIXTURE_DEFAULT);
|
||||
let n = items.len() as u64;
|
||||
let mut group = c.benchmark_group("ingest_and_match_default_fixture");
|
||||
group.throughput(Throughput::Elements(n));
|
||||
group.bench_function("ingest_and_match", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let opts = SkimOptions::default().build();
|
||||
let rx = make_receiver(&items, &opts);
|
||||
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
|
||||
},
|
||||
|mut skim: Skim<TestBackend>| {
|
||||
skim.start();
|
||||
wait_until_done(&mut skim, Duration::from_secs(10));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// Large fixture (100 000 lines). Only run when the file exists so the
|
||||
// bench suite does not fail in environments without the fixture.
|
||||
if std::path::Path::new(FIXTURE_100K).exists() {
|
||||
let items = load_fixture(FIXTURE_100K);
|
||||
let n = items.len() as u64;
|
||||
let mut group = c.benchmark_group("ingest_and_match_100k_fixture");
|
||||
group.throughput(Throughput::Elements(n));
|
||||
// Fewer samples: each iteration loads 100 k items.
|
||||
group.sample_size(10);
|
||||
group.bench_function("ingest_and_match", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let opts = SkimOptions::default().build();
|
||||
let rx = make_receiver(&items, &opts);
|
||||
Skim::<TestBackend>::init(opts, Some(rx)).unwrap()
|
||||
},
|
||||
|mut skim: Skim<TestBackend>| {
|
||||
skim.start();
|
||||
wait_until_done(&mut skim, Duration::from_secs(30));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
);
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Phase 2–5 — full_setup (unchanged from before, kept for comparison)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
c.bench_function("full_setup", |b| {
|
||||
b.iter(|| {
|
||||
let mut options = SkimOptions::default().build();
|
||||
if let Some(ref filter_query) = options.filter
|
||||
&& options.query.is_none()
|
||||
{
|
||||
options.query = Some(filter_query.clone());
|
||||
}
|
||||
let mut skim = Skim::init(options, None).unwrap();
|
||||
|
||||
skim.start();
|
||||
|
||||
if skim.should_enter() {
|
||||
skim.init_tui().unwrap();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
name = benches;
|
||||
config = Criterion::default().sample_size(100);
|
||||
targets = criterion_benchmark
|
||||
);
|
||||
criterion_main!(benches);
|
||||
131
benches/read_and_match.rs
Normal file
131
benches/read_and_match.rs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
#![allow(missing_docs, clippy::pedantic)]
|
||||
|
||||
use criterion::{Criterion, criterion_group, criterion_main};
|
||||
use eyre::{Ok, Result};
|
||||
|
||||
use skim::prelude::*;
|
||||
|
||||
async fn wait_until_done(mut opts: SkimOptions) -> Result<SkimOutput> {
|
||||
opts.cmd = Some(String::from("cat benches/fixtures/10M.txt"));
|
||||
let mut skim = Skim::init(opts, None)?;
|
||||
skim.start();
|
||||
skim.init_tui()?;
|
||||
skim.enter().await?;
|
||||
while !skim.tick().await? {
|
||||
if skim.reader_done() && skim.matcher_stopped() {
|
||||
skim.event_sender().send(Event::Action(Action::Accept(None))).await?;
|
||||
}
|
||||
}
|
||||
Ok(skim.output())
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("default", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt)
|
||||
.iter(async || wait_until_done(SkimOptions::default()).await);
|
||||
});
|
||||
c.bench_function("query", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt)
|
||||
.iter(async || wait_until_done(SkimOptionsBuilder::default().query("test").build().unwrap()).await);
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("query_frizbee", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt).iter(async || {
|
||||
wait_until_done(
|
||||
SkimOptionsBuilder::default()
|
||||
.query("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.no_typos(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
});
|
||||
c.bench_function("query_ari", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt).iter(async || {
|
||||
wait_until_done(
|
||||
SkimOptionsBuilder::default()
|
||||
.query("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.no_typos(true)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
});
|
||||
#[cfg(feature = "frizbee")]
|
||||
c.bench_function("query_frizbee_typos", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt).iter(async || {
|
||||
wait_until_done(
|
||||
SkimOptionsBuilder::default()
|
||||
.query("test")
|
||||
.algorithm(FuzzyAlgorithm::Frizbee)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
});
|
||||
c.bench_function("query_ari_typos", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt).iter(async || {
|
||||
wait_until_done(
|
||||
SkimOptionsBuilder::default()
|
||||
.query("test")
|
||||
.algorithm(FuzzyAlgorithm::Arinae)
|
||||
.build()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
});
|
||||
});
|
||||
c.bench_function("typing", |b| {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
b.to_async(rt).iter(async || {
|
||||
let mut skim = Skim::init(SkimOptionsBuilder::default().cmd("cat bench_data.txt").build()?, None)?;
|
||||
skim.start();
|
||||
skim.init_tui()?;
|
||||
skim.enter().await?;
|
||||
let s = skim.event_sender();
|
||||
let mut sent = false;
|
||||
let mut done_since = 0;
|
||||
let mut done = false;
|
||||
while !skim.tick().await? {
|
||||
if skim.reader_done() && skim.matcher_stopped() {
|
||||
if done {
|
||||
done_since += 1;
|
||||
} else {
|
||||
done_since = 1;
|
||||
}
|
||||
if sent && done_since > 50 {
|
||||
s.send(Event::Action(Action::Accept(None))).await?;
|
||||
} else if !sent {
|
||||
s.send(Event::Action(Action::AddChar('t'))).await?;
|
||||
s.send(Event::Action(Action::AddChar('e'))).await?;
|
||||
s.send(Event::Action(Action::AddChar('s'))).await?;
|
||||
s.send(Event::Action(Action::AddChar('t'))).await?;
|
||||
sent = true;
|
||||
}
|
||||
done = true;
|
||||
} else {
|
||||
done = false;
|
||||
}
|
||||
}
|
||||
Ok(skim.output())
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
name = benches;
|
||||
config = Criterion::default().sample_size(10);
|
||||
targets = criterion_benchmark
|
||||
);
|
||||
criterion_main!(benches);
|
||||
12
bin/sk-tmux
12
bin/sk-tmux
|
|
@ -28,6 +28,8 @@
|
|||
# sk-tmux: starts sk in a tmux pane
|
||||
# usage: sk-tmux [LAYOUT OPTIONS] [--] [SK OPTIONS]
|
||||
|
||||
echo "[WRN] This script is deprecated in favor of \`sk --tmux\` and will be removed in a later release" >&2
|
||||
|
||||
fail() {
|
||||
>&2 echo "$1"
|
||||
exit 2
|
||||
|
|
@ -84,7 +86,7 @@ while [[ $# -gt 0 ]]; do
|
|||
;;
|
||||
-p*|-w*|-h*|-x*|-y*|-d*|-u*|-r*|-l*)
|
||||
if [[ "$arg" =~ ^-[pwhxy] ]]; then
|
||||
[[ "$opt" =~ "-K -E" ]] || opt="-K -E"
|
||||
[[ "$opt" =~ "-E" ]] || opt="-E"
|
||||
elif [[ "$arg" =~ ^.[lr] ]]; then
|
||||
opt="-h"
|
||||
if [[ "$arg" =~ ^.l ]]; then
|
||||
|
|
@ -165,7 +167,7 @@ fi
|
|||
args=("${args[@]}" "--no-height")
|
||||
|
||||
# Handle zoomed tmux pane without popup options by moving it to a temp window
|
||||
if [[ ! "$opt" =~ "-K -E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
if [[ ! "$opt" =~ "-E" ]] && tmux list-panes -F '#F' | grep -q Z; then
|
||||
zoomed_without_popup=1
|
||||
original_window=$(tmux display-message -p "#{window_id}")
|
||||
tmp_window=$(tmux new-window -d -P -F "#{window_id}" "bash -c 'while :; do for c in \\| / - '\\;' do sleep 0.2; printf \"\\r\$c sk-tmux is running\\r\"; done; done'")
|
||||
|
|
@ -207,7 +209,7 @@ trap 'cleanup 1' SIGUSR1
|
|||
trap 'cleanup' EXIT
|
||||
|
||||
envs="export TERM=$TERM "
|
||||
[[ "$opt" =~ "-K -E" ]] && SKIM_DEFAULT_OPTIONS="--margin 0,1 $SKIM_DEFAULT_OPTIONS"
|
||||
[[ "$opt" =~ "-E" ]] && SKIM_DEFAULT_OPTIONS="--margin 0,1 $SKIM_DEFAULT_OPTIONS"
|
||||
[[ -n "$SKIM_DEFAULT_OPTIONS" ]] && envs="$envs SKIM_DEFAULT_OPTIONS=$(printf %q "$SKIM_DEFAULT_OPTIONS")"
|
||||
[[ -n "$SKIM_DEFAULT_COMMAND" ]] && envs="$envs SKIM_DEFAULT_COMMAND=$(printf %q "$SKIM_DEFAULT_COMMAND")"
|
||||
echo "$envs;" > "$argsf"
|
||||
|
|
@ -221,7 +223,7 @@ close="; trap - EXIT SIGINT SIGTERM $close"
|
|||
|
||||
export TMUX=$(cut -d , -f 1,2 <<< "$TMUX")
|
||||
mkfifo -m o+w $fifo2
|
||||
if [[ "$opt" =~ "-K -E" ]]; then
|
||||
if [[ "$opt" =~ "-E" ]]; then
|
||||
cat $fifo2 &
|
||||
if [[ -n "$term" ]] || [[ -t 0 ]]; then
|
||||
cat <<< "\"$sk\" $opts > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
|
|
@ -230,7 +232,7 @@ if [[ "$opt" =~ "-K -E" ]]; then
|
|||
cat <<< "\"$sk\" $opts < $fifo1 > $fifo2; out=\$? $close; exit \$out" >> $argsf
|
||||
cat <&0 > $fifo1 &
|
||||
fi
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt -R "bash $argsf" > /dev/null 2>&1
|
||||
tmux popup -d "$PWD" "${tmux_args[@]}" $opt "bash $argsf" > /dev/null 2>&1
|
||||
exit $?
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
# This script takes care of building your crate and packaging it for release
|
||||
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
local src=$(pwd) \
|
||||
stage=
|
||||
|
||||
case $TRAVIS_OS_NAME in
|
||||
linux)
|
||||
stage=$(mktemp -d)
|
||||
;;
|
||||
osx)
|
||||
stage=$(mktemp -d -t tmp)
|
||||
;;
|
||||
esac
|
||||
|
||||
test -f Cargo.lock || cargo generate-lockfile
|
||||
|
||||
# TODO Update this to build the artifacts that matter to you
|
||||
cross build --release --target $TARGET
|
||||
|
||||
# TODO Update this to package the right artifacts
|
||||
cp target/$TARGET/release/sk $stage/
|
||||
|
||||
cd $stage
|
||||
tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz *
|
||||
cd $src
|
||||
|
||||
rm -rf $stage
|
||||
}
|
||||
|
||||
main
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
set -ex
|
||||
|
||||
main() {
|
||||
local target=
|
||||
if [ $TRAVIS_OS_NAME = linux ]; then
|
||||
target=x86_64-unknown-linux-musl
|
||||
sort=sort
|
||||
else
|
||||
target=x86_64-apple-darwin
|
||||
sort=gsort # for `sort --sort-version`, from brew's coreutils.
|
||||
fi
|
||||
|
||||
# Builds for iOS are done on OSX, but require the specific target to be
|
||||
# installed.
|
||||
case $TARGET in
|
||||
aarch64-apple-ios)
|
||||
rustup target install aarch64-apple-ios
|
||||
;;
|
||||
armv7-apple-ios)
|
||||
rustup target install armv7-apple-ios
|
||||
;;
|
||||
armv7s-apple-ios)
|
||||
rustup target install armv7s-apple-ios
|
||||
;;
|
||||
i386-apple-ios)
|
||||
rustup target install i386-apple-ios
|
||||
;;
|
||||
x86_64-apple-ios)
|
||||
rustup target install x86_64-apple-ios
|
||||
;;
|
||||
esac
|
||||
|
||||
# This fetches latest stable release
|
||||
local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \
|
||||
| cut -d/ -f3 \
|
||||
| grep -E '^v[0.1.0-9.]+$' \
|
||||
| $sort --version-sort \
|
||||
| tail -n1)
|
||||
curl -LSfs https://japaric.github.io/trust/install.sh | \
|
||||
sh -s -- \
|
||||
--force \
|
||||
--git japaric/cross \
|
||||
--tag $tag \
|
||||
--target $target
|
||||
|
||||
# For test
|
||||
case $TARGET in
|
||||
x86_64-unknown-linux-gnu|i686-unknown-linux-gnu)
|
||||
python3 -V
|
||||
tmux -V
|
||||
sudo apt-get install -y zsh
|
||||
stty cols 80
|
||||
;;
|
||||
x86_64-apple-darwin|i686-apple-darwin)
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install tmux
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 brew install zsh
|
||||
python -V
|
||||
python3 -V
|
||||
tmux -V
|
||||
stty cols 80
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main
|
||||
|
||||
30
ci/script.sh
30
ci/script.sh
|
|
@ -1,30 +0,0 @@
|
|||
# This script takes care of testing
|
||||
set -ex
|
||||
|
||||
main() {
|
||||
if [ ! -z $DISABLE_TESTS ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cross test --release --target $TARGET
|
||||
cross build --release --target $TARGET
|
||||
mkdir -p target/release
|
||||
cp target/$TARGET/release/sk target/release
|
||||
|
||||
case $TARGET in
|
||||
x86_64-unknown-linux-gnu|i686-unknown-linux-gnu|x86_64-unknown-linux-musl)
|
||||
# run the integration test
|
||||
tmux new "python3 test/test_skim.py &> out && touch ok" && cat out && [ -e ok ]
|
||||
;;
|
||||
x86_64-apple-darwin|i686-apple-darwin)
|
||||
# run the integration test
|
||||
tmux new "python3 test/test_skim.py &> out && touch ok" && cat out && [ -e ok ]
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -z $TRAVIS_TAG ]; then
|
||||
main
|
||||
fi
|
||||
121
cliff.toml
Normal file
121
cliff.toml
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# git-cliff ~ configuration file
|
||||
# https://git-cliff.org/docs/configuration
|
||||
|
||||
|
||||
[changelog]
|
||||
# A Tera template to be rendered for each release in the changelog.
|
||||
# See https://keats.github.io/tera/docs/#introduction
|
||||
body = """
|
||||
{%- macro remote_url() -%}
|
||||
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{% if version %}\
|
||||
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## [unreleased]
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | striptags | trim | upper_first }}
|
||||
{% for commit in commits %}
|
||||
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
||||
{% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{{ commit.message | upper_first }}\
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||
### New Contributors
|
||||
{%- endif -%}
|
||||
|
||||
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
||||
* @{{ contributor.username }} made their first contribution
|
||||
{%- if contributor.pr_number %} in \
|
||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||
{%- endif %}
|
||||
{%- endfor %}\n
|
||||
|
||||
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}{% raw %}\n{% endraw -%}{% endif %}
|
||||
|
||||
"""
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
"""
|
||||
# Remove leading and trailing whitespaces from the changelog's body.
|
||||
footer = "<!-- generated by git-cliff -->"
|
||||
trim = true
|
||||
# Render body even when there are no releases to process.
|
||||
render_always = true
|
||||
# An array of regex based postprocessors to modify the changelog.
|
||||
postprocessors = [
|
||||
# Replace the placeholder <REPO> with a URL.
|
||||
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
|
||||
]
|
||||
# render body even when there are no releases to process
|
||||
# render_always = true
|
||||
# output file path
|
||||
# output = "test.md"
|
||||
|
||||
[git]
|
||||
# Parse commits according to the conventional commits specification.
|
||||
# See https://www.conventionalcommits.org
|
||||
conventional_commits = true
|
||||
# Exclude commits that do not match the conventional commits specification.
|
||||
filter_unconventional = true
|
||||
# Require all commits to be conventional.
|
||||
# Takes precedence over filter_unconventional.
|
||||
require_conventional = false
|
||||
# Split commits on newlines, treating each line as an individual commit.
|
||||
split_commits = false
|
||||
# An array of regex based parsers to modify commit messages prior to further processing.
|
||||
commit_preprocessors = [
|
||||
# Allow commits named `type!(scope): desc` and treat them like `type(scope)!: desc`
|
||||
{ pattern = '(.*)!\((.*)\):(.*)', replace = "$1($2)!:$3" },
|
||||
]
|
||||
# Prevent commits that are breaking from being excluded by commit parsers.
|
||||
protect_breaking_commits = false
|
||||
# An array of regex based parsers for extracting data from the commit message.
|
||||
# Assigns commits to groups.
|
||||
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
|
||||
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
|
||||
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
|
||||
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
|
||||
{ message = "^refactor", group = "<!-- 2 -->🚜 Refactor" },
|
||||
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
|
||||
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
|
||||
{ message = "^ci", group = "<!-- 7 -->🤖 CI" },
|
||||
{ message = "^chore\\(release\\): prepare for", skip = true },
|
||||
{ message = "^chore\\(deps.*\\)", skip = true },
|
||||
{ message = "^chore\\(pr\\)", skip = true },
|
||||
{ message = "^chore\\(pull\\)", skip = true },
|
||||
{ message = "^chore", group = "<!-- 8 -->⚙️ Miscellaneous Tasks" },
|
||||
{ body = ".*security", group = "<!-- 9 -->🛡️ Security" },
|
||||
{ message = "^revert", group = "<!-- 10 -->◀️ Revert" },
|
||||
{ message = "^release", skip = true },
|
||||
{ message = ".*", group = "<!-- 11 -->💼 Other" },
|
||||
]
|
||||
# Exclude commits that are not matched by any commit parser.
|
||||
filter_commits = false
|
||||
# Fail on a commit that is not matched by any commit parser.
|
||||
fail_on_unmatched_commit = false
|
||||
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
|
||||
link_parsers = []
|
||||
# Include only the tags that belong to the current branch.
|
||||
use_branch_tags = false
|
||||
# Order releases topologically instead of chronologically.
|
||||
topo_order = false
|
||||
# Order commits topologically instead of chronologically.
|
||||
topo_order_commits = true
|
||||
# Order of commits in each group/release within the changelog.
|
||||
# Allowed values: newest, oldest
|
||||
sort_commits = "oldest"
|
||||
# Process submodules commits
|
||||
recurse_submodules = false
|
||||
35
dist-workspace.toml
Normal file
35
dist-workspace.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[workspace]
|
||||
members = ["cargo:."]
|
||||
|
||||
# Config for 'dist'
|
||||
[dist]
|
||||
# The preferred dist version to use in CI (Cargo.toml SemVer syntax)
|
||||
cargo-dist-version = "0.32.0"
|
||||
# CI backends to support
|
||||
ci = "github"
|
||||
# The installers to generate for each app
|
||||
installers = ["shell", "powershell", "msi"]
|
||||
# Target platforms to build apps for (Rust target-triple syntax)
|
||||
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
|
||||
# Path that installers should place binaries in
|
||||
install-path = "CARGO_HOME"
|
||||
# Whether to install an updater program
|
||||
install-updater = false
|
||||
# Extra static files to include in each App (path relative to this Cargo.toml's dir)
|
||||
include = ["./man/", "./shell/"]
|
||||
# Plan jobs to run in CI
|
||||
plan-jobs = ["./test"]
|
||||
# Global artifacts jobs to run in CI
|
||||
global-artifacts-jobs = ["./package"]
|
||||
# Publish jobs to run in CI
|
||||
publish-jobs = ["./publish", "./winget", "./apt"]
|
||||
# Whether to publish prereleases to package managers
|
||||
publish-prereleases = true
|
||||
|
||||
[dist.github-custom-job-permissions.test]
|
||||
contents = "write"
|
||||
id-token = "write"
|
||||
|
||||
[dist.github-custom-job-permissions.apt]
|
||||
contents = "write"
|
||||
id-token = "write"
|
||||
BIN
examples/Lenna.png
Normal file
BIN
examples/Lenna.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 463 KiB |
33
examples/ansi.rs
Normal file
33
examples/ansi.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
//! Demonstrates selecting ANSI-colored command output.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
use skim::reader::CommandCollector;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let glogm = "git log --oneline --color=always | head -n10";
|
||||
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.height("50%")
|
||||
.cmd(glogm)
|
||||
.preview("echo {}")
|
||||
.multi(true)
|
||||
.reverse(true)
|
||||
.cmd_collector(Rc::new(RefCell::new(SkimItemReader::new(
|
||||
SkimItemReaderOption::default().ansi(true),
|
||||
))) as Rc<RefCell<dyn CommandCollector>>)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
log::debug!("Options: ansi {}", options.ansi);
|
||||
|
||||
let selected_items = Skim::run_with(options, None)
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in &selected_items {
|
||||
println!("selected: {}", item.output());
|
||||
}
|
||||
}
|
||||
10
examples/async.rs
Normal file
10
examples/async.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
//! Runs skim from an async Tokio entry point.
|
||||
|
||||
use skim::Skim;
|
||||
use skim::prelude::SkimOptionsBuilder;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let options = SkimOptionsBuilder::default().build().unwrap();
|
||||
Skim::run_with(options, None).unwrap();
|
||||
}
|
||||
19
examples/base.rs
Normal file
19
examples/base.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
//! Demonstrates basic item selection with inline status information.
|
||||
|
||||
use skim::prelude::*;
|
||||
use skim::tui::statusline::InfoDisplay;
|
||||
|
||||
fn main() -> eyre::Result<()> {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.multi(true)
|
||||
.reverse(true)
|
||||
.info(InfoDisplay::Inline)
|
||||
.build()?;
|
||||
let res = Skim::run_items(opts, ["hello", "world"])?;
|
||||
|
||||
for item in res.selected_items {
|
||||
println!("Selected {} (id {})", item.output(), item.rank.index);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
54
examples/cmd_collector.rs
Normal file
54
examples/cmd_collector.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//! Demonstrates collecting items from a command.
|
||||
|
||||
extern crate skim;
|
||||
use reader::CommandCollector;
|
||||
use skim::prelude::*;
|
||||
|
||||
struct BasicSkimItem {
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl SkimItem for BasicSkimItem {
|
||||
fn text(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(&self.value)
|
||||
}
|
||||
}
|
||||
|
||||
struct BasicCmdCollector {
|
||||
pub items: Vec<String>,
|
||||
}
|
||||
|
||||
impl CommandCollector for BasicCmdCollector {
|
||||
fn invoke(&mut self, _cmd: &str, _components_to_stop: Arc<AtomicUsize>) -> (SkimItemReceiver, Sender<i32>) {
|
||||
let (tx, rx) = unbounded();
|
||||
let (tx_interrupt, _rx_interrupt) = unbounded();
|
||||
let mut batch = Vec::new();
|
||||
while let Some(value) = self.items.pop() {
|
||||
let item = BasicSkimItem { value };
|
||||
batch.push(Arc::from(item) as Arc<dyn SkimItem>);
|
||||
}
|
||||
if !batch.is_empty() {
|
||||
tx.send(batch).unwrap();
|
||||
}
|
||||
|
||||
(rx, tx_interrupt)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let cmd_collector = BasicCmdCollector {
|
||||
items: vec![String::from("foo"), String::from("bar"), String::from("baz")],
|
||||
};
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.cmd_collector(Rc::from(RefCell::from(cmd_collector)))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let selected_items = Skim::run_with(options, None)
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
96
examples/custom_action_keybinding.rs
Normal file
96
examples/custom_action_keybinding.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
//! Demonstrates binding custom action callbacks to keyboard shortcuts.
|
||||
|
||||
extern crate skim;
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use skim::prelude::*;
|
||||
use skim::tui::event::{Action, ActionCallback, Event};
|
||||
use std::io::Cursor;
|
||||
|
||||
/// Runs the custom action keybinding example.
|
||||
///
|
||||
/// It shows how to:
|
||||
/// 1. Create custom action callbacks (both sync and async)
|
||||
/// 2. Bind them to specific key combinations
|
||||
/// 3. Use them interactively in skim
|
||||
fn main() {
|
||||
// Create a synchronous callback that adds a prefix to the query.
|
||||
// Use `new_sync` for plain closures that do not need to await anything.
|
||||
let add_prefix_callback = ActionCallback::new_sync(|app: &mut skim::tui::App| {
|
||||
// Get current query and add prefix
|
||||
let current_query = app.input.value.clone();
|
||||
|
||||
// Clear the line first, then add new content
|
||||
let mut events = vec![Event::Action(Action::UnixLineDiscard)];
|
||||
|
||||
let prefix = "TODO: ";
|
||||
for ch in prefix.chars() {
|
||||
events.push(Event::Action(Action::AddChar(ch)));
|
||||
}
|
||||
|
||||
// Add back the original query
|
||||
for ch in current_query.chars() {
|
||||
events.push(Event::Action(Action::AddChar(ch)));
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
});
|
||||
|
||||
// Create an async callback that selects all and exits.
|
||||
// Use `new` for async closures or blocks that may await futures.
|
||||
let select_all_callback = ActionCallback::new(|app: &mut skim::tui::App| {
|
||||
let count = app.item_pool.len();
|
||||
async move {
|
||||
// Async work could go here (e.g. HTTP requests, file I/O, …).
|
||||
Ok(vec![
|
||||
Event::Action(Action::SelectAll),
|
||||
Event::Action(Action::Accept(Some(format!("Selected {count} items")))),
|
||||
])
|
||||
}
|
||||
});
|
||||
|
||||
// Build basic options
|
||||
let mut options = SkimOptionsBuilder::default()
|
||||
.multi(true)
|
||||
.prompt("Select> ")
|
||||
.header("<C-p>: add prefix to prompt\t<C-a>: select all and exit with count")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Now manually add custom keybindings to the keymap
|
||||
// We can access the keymap directly since it's public
|
||||
|
||||
// Bind Ctrl-P to add prefix
|
||||
options.keymap.insert(
|
||||
KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL),
|
||||
vec![Action::Custom(add_prefix_callback)],
|
||||
);
|
||||
|
||||
// Bind Ctrl-A to select all with message
|
||||
options.keymap.insert(
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||
vec![Action::Custom(select_all_callback)],
|
||||
);
|
||||
|
||||
// Create sample items
|
||||
let items = [
|
||||
"Write documentation",
|
||||
"Fix bug #123",
|
||||
"Implement feature X",
|
||||
"Review pull request",
|
||||
"Update dependencies",
|
||||
"Refactor module Y",
|
||||
"Add unit tests",
|
||||
"Optimize performance",
|
||||
];
|
||||
|
||||
let item_reader = SkimItemReader::default();
|
||||
let input = items.join("\n");
|
||||
let item_source = item_reader.of_bufread(Cursor::new(input));
|
||||
|
||||
// Run skim with our custom keybindings
|
||||
if let Ok(output) = Skim::run_with(options, Some(item_source)) {
|
||||
println!("output: {output:?}");
|
||||
} else {
|
||||
println!("\nAborted!");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
//! Demonstrates using a custom item type with skim.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
|
||||
|
|
@ -6,7 +8,7 @@ struct MyItem {
|
|||
}
|
||||
|
||||
impl SkimItem for MyItem {
|
||||
fn text(&self) -> Cow<str> {
|
||||
fn text(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(&self.inner)
|
||||
}
|
||||
|
||||
|
|
@ -19,31 +21,35 @@ impl SkimItem for MyItem {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
fn main() {
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.height(Some("50%"))
|
||||
.height("50%")
|
||||
.multi(true)
|
||||
.preview(Some("")) // preview should be specified to enable preview window
|
||||
.preview("") // preview should be specified to enable preview window
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
env_logger::init();
|
||||
|
||||
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
|
||||
let _ = tx_item.send(Arc::new(MyItem {
|
||||
inner: "color aaaa".to_string(),
|
||||
}));
|
||||
let _ = tx_item.send(Arc::new(MyItem {
|
||||
inner: "bbbb".to_string(),
|
||||
}));
|
||||
let _ = tx_item.send(Arc::new(MyItem {
|
||||
inner: "ccc".to_string(),
|
||||
}));
|
||||
let _ = tx_item.send(vec![
|
||||
Arc::new(MyItem {
|
||||
inner: "color aaaa".to_string(),
|
||||
}) as Arc<dyn SkimItem>,
|
||||
Arc::new(MyItem {
|
||||
inner: "bbbb".to_string(),
|
||||
}) as Arc<dyn SkimItem>,
|
||||
Arc::new(MyItem {
|
||||
inner: "ccc".to_string(),
|
||||
}) as Arc<dyn SkimItem>,
|
||||
]);
|
||||
drop(tx_item); // so that skim could know when to stop waiting for more items.
|
||||
|
||||
let selected_items = Skim::run_with(&options, Some(rx_item))
|
||||
let selected_items = Skim::run_with(options, Some(rx_item))
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_else(|| Vec::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in selected_items.iter() {
|
||||
print!("{}{}", item.output(), "\n");
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,32 +1,39 @@
|
|||
//! Demonstrates binding custom key combinations to custom actions.
|
||||
|
||||
extern crate skim;
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use skim::prelude::*;
|
||||
|
||||
// No action is actually performed on your filesystem!
|
||||
// This example only produce friendly print statements!
|
||||
|
||||
fn fake_delete_item(item: &str) {
|
||||
println!("Deleting item `{}`...", item);
|
||||
println!("Deleting item `{item}`...");
|
||||
}
|
||||
|
||||
fn fake_create_item(item: &str) {
|
||||
println!("Creating a new item `{}`...", item);
|
||||
println!("Creating a new item `{item}`...");
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
fn main() {
|
||||
// Note: `accept` is a keyword used define custom actions.
|
||||
// For full list of accepted keywords see `parse_event` in `src/event.rs`.
|
||||
// `delete` and `create` are arbitrary keywords used for this example.
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.multi(true)
|
||||
.bind(vec!["bs:abort", "Enter:accept"])
|
||||
.bind(vec!["bs:abort".into(), "enter:accept".into()])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
Skim::run_with(&options, None).map(|out| match out.final_key {
|
||||
// Delete each selected item
|
||||
Key::Backspace => out.selected_items.iter().for_each(|i| fake_delete_item(&i.text())),
|
||||
// Create a new item based on the query
|
||||
Key::Enter => fake_create_item(out.query.as_ref()),
|
||||
_ => (),
|
||||
});
|
||||
if let Ok(out) = Skim::run_with(options, None) {
|
||||
match (out.final_key.code, out.final_key.modifiers) {
|
||||
// Delete each selected item
|
||||
(KeyCode::Backspace, KeyModifiers::NONE) => {
|
||||
out.selected_items.iter().for_each(|i| fake_delete_item(&i.text()));
|
||||
}
|
||||
// Create a new item based on the query
|
||||
(KeyCode::Enter, KeyModifiers::NONE) => fake_create_item(out.query.as_ref()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
50
examples/downcast.rs
Normal file
50
examples/downcast.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
//! Illustrates downcasting custom structs that implement `SkimItem`.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Item {
|
||||
text: String,
|
||||
}
|
||||
|
||||
impl SkimItem for Item {
|
||||
fn text(&self) -> Cow<'_, str> {
|
||||
Cow::Borrowed(&self.text)
|
||||
}
|
||||
|
||||
fn preview(&self, _context: PreviewContext) -> ItemPreview {
|
||||
ItemPreview::Text(self.text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.height("50%")
|
||||
.multi(true)
|
||||
.preview("")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let (tx, rx): (SkimItemSender, SkimItemReceiver) = unbounded();
|
||||
|
||||
tx.send(vec![
|
||||
Arc::new(Item { text: "a".into() }) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item { text: "b".into() }) as Arc<dyn SkimItem>,
|
||||
Arc::new(Item { text: "c".into() }) as Arc<dyn SkimItem>,
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
drop(tx);
|
||||
|
||||
let selected_items = Skim::run_with(options, Some(rx))
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|selected_item| selected_item.downcast_item::<Item>().unwrap().to_owned())
|
||||
.collect::<Vec<Item>>();
|
||||
|
||||
for item in selected_items {
|
||||
println!("{item:?}");
|
||||
}
|
||||
}
|
||||
36
examples/fine-grain.rs
Normal file
36
examples/fine-grain.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
//! Demonstrates fine-grained control over skim lifecycle events.
|
||||
|
||||
extern crate skim;
|
||||
use eyre::Result;
|
||||
use skim::prelude::*;
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
let options = SkimOptionsBuilder::default().height("50%").multi(true).build()?;
|
||||
|
||||
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
|
||||
let mut skim = Skim::init(options, Some(rx_item))?;
|
||||
|
||||
skim.start();
|
||||
skim.init_tui()?;
|
||||
|
||||
let event_tx = skim.event_sender();
|
||||
|
||||
skim.enter().await?;
|
||||
|
||||
let output = skim
|
||||
.run_until(async move {
|
||||
for i in 1..=10 {
|
||||
let _ = event_tx.try_send(Event::ClearItems);
|
||||
let _ = tx_item.send(vec![Arc::new(format!("item {i}")) as Arc<dyn SkimItem>]);
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
for item in &output.selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
65
examples/fuzzy_matcher_fz.rs
Normal file
65
examples/fuzzy_matcher_fz.rs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
//! Demonstrates fuzzy matching lines with selectable matching algorithms.
|
||||
|
||||
use skim::fuzzy_matcher::FuzzyMatcher;
|
||||
use skim::fuzzy_matcher::clangd::ClangdMatcher;
|
||||
use skim::fuzzy_matcher::skim::SkimMatcherV2;
|
||||
use std::env;
|
||||
use std::io::{self, BufRead};
|
||||
use std::process::exit;
|
||||
|
||||
type IndexType = usize;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = env::args().collect();
|
||||
|
||||
// arg parsing (manually)
|
||||
let mut arg_iter = args.iter().skip(1);
|
||||
let mut pattern = String::new();
|
||||
let mut algorithm = Some("skim");
|
||||
|
||||
while let Some(arg) = arg_iter.next() {
|
||||
if arg == "--algo" {
|
||||
algorithm = arg_iter.next().map(String::as_ref);
|
||||
} else {
|
||||
pattern.clone_from(arg);
|
||||
}
|
||||
}
|
||||
|
||||
if pattern.is_empty() {
|
||||
eprintln!("Usage: echo <piped_input> | fz --algo [skim|clangd] <pattern>");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let matcher: Box<dyn FuzzyMatcher> = match algorithm {
|
||||
Some("skim" | "skim_v2") => Box::new(SkimMatcherV2::default()),
|
||||
Some("clangd") => Box::new(ClangdMatcher::default()),
|
||||
_ => panic!("Algorithm not supported: {algorithm:?}"),
|
||||
};
|
||||
|
||||
let stdin = io::stdin();
|
||||
for line in stdin.lock().lines() {
|
||||
if let Ok(line) = line
|
||||
&& let Some((score, indices)) = matcher.fuzzy_indices(&line, &pattern)
|
||||
{
|
||||
println!("{:8}: {}", score, wrap_matches(&line, &indices));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_matches(line: &str, indices: &[IndexType]) -> String {
|
||||
let mut ret = String::new();
|
||||
let mut peekable = indices.iter().peekable();
|
||||
let ansi_invert: &str = str::from_utf8(&[27, b'[', b'7', b'm']).unwrap();
|
||||
let ansi_reset: &str = str::from_utf8(&[27, b'[', b'0', b'm']).unwrap();
|
||||
for (idx, ch) in line.chars().enumerate() {
|
||||
let next_id = **peekable.peek().unwrap_or(&&(line.len() as IndexType));
|
||||
if next_id == (idx as IndexType) {
|
||||
ret.push_str(format!("{ansi_invert}{ch}{ansi_reset}").as_str());
|
||||
peekable.next();
|
||||
} else {
|
||||
ret.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
25
examples/image.rs
Normal file
25
examples/image.rs
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//! Demonstrates image previews using a literal image path.
|
||||
//!
|
||||
//! Run with:
|
||||
//! `cargo run --example image`
|
||||
|
||||
use skim::options::ImageProtocol;
|
||||
use skim::prelude::*;
|
||||
|
||||
fn main() -> eyre::Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.preview("{}")
|
||||
.preview_window("right:60%")
|
||||
.image(ImageProtocol::Halfblocks)
|
||||
.build()?;
|
||||
|
||||
let output = Skim::run_items(options, ["examples/Lenna.png"])?;
|
||||
|
||||
for item in &output.selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
26
examples/multiple_runs.rs
Normal file
26
examples/multiple_runs.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
//! Runs skim repeatedly to check that runs clean up their worker threads.
|
||||
|
||||
use skim::Skim;
|
||||
use skim::prelude::SkimOptionsBuilder;
|
||||
|
||||
// Hint: use `ps -T -p $(pgrep -f target/debug/examples/multiple_runs)` to watch threads while the
|
||||
// different invocations run, and make sure none is leaking through
|
||||
fn main() {
|
||||
for i in 0..3 {
|
||||
let opts = SkimOptionsBuilder::default()
|
||||
.header(format!("run {i}"))
|
||||
.cmd("cat benches/fixtures/10M.txt")
|
||||
.build()
|
||||
.unwrap();
|
||||
let res = Skim::run_with(opts, None).unwrap();
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||
unsafe {
|
||||
nix::libc::malloc_trim(0);
|
||||
}
|
||||
println!(
|
||||
"run {i}: {:?}, sleeping for 5 secs",
|
||||
res.selected_items.first().map(|x| x.output())
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +1,25 @@
|
|||
//! Demonstrates matching against selected fields with the `nth` option.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
/// `nth` option is supported by SkimItemReader.
|
||||
/// Runs the `nth` example.
|
||||
///
|
||||
/// `nth` option is supported by `SkimItemReader`.
|
||||
/// In the example below, with `nth=2` set, only `123` could be matched.
|
||||
|
||||
pub fn main() {
|
||||
fn main() {
|
||||
let input = "foo 123";
|
||||
|
||||
let options = SkimOptionsBuilder::default().query(Some("f")).build().unwrap();
|
||||
let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth("2").build());
|
||||
let options = SkimOptionsBuilder::default().query("f").build().unwrap();
|
||||
let item_reader = SkimItemReader::new(SkimItemReaderOption::default().nth(vec!["2"].into_iter()).build());
|
||||
|
||||
let items = item_reader.of_bufread(Cursor::new(input));
|
||||
let selected_items = Skim::run_with(&options, Some(items))
|
||||
let selected_items = Skim::run_with(options, Some(items))
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_else(|| Vec::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in selected_items.iter() {
|
||||
print!("{}{}", item.output(), "\n");
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,35 @@
|
|||
//! Demonstrates configuring skim with `SkimOptionsBuilder`.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
pub fn main() {
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.height(Some("50%"))
|
||||
.multi(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
fn main() {
|
||||
let item_reader = SkimItemReader::default();
|
||||
|
||||
//==================================================
|
||||
// first run
|
||||
let options = SkimOptionsBuilder::default().height("50%").multi(true).build().unwrap();
|
||||
let input = "aaaaa\nbbbb\nccc";
|
||||
let items = item_reader.of_bufread(Cursor::new(input));
|
||||
let selected_items = Skim::run_with(&options, Some(items))
|
||||
let selected_items = Skim::run_with(options, Some(items))
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_else(|| Vec::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in selected_items.iter() {
|
||||
print!("{}{}", item.output(), "\n");
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
|
||||
//==================================================
|
||||
// second run
|
||||
let options = SkimOptionsBuilder::default().height("50%").multi(true).build().unwrap();
|
||||
let input = "11111\n22222\n333333333";
|
||||
let items = item_reader.of_bufread(Cursor::new(input));
|
||||
let selected_items = Skim::run_with(&options, Some(items))
|
||||
let selected_items = Skim::run_with(options, Some(items))
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_else(|| Vec::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in selected_items.iter() {
|
||||
print!("{}{}", item.output(), "\n");
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
examples/preview_callback.rs
Normal file
27
examples/preview_callback.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//! Demonstrates generating preview content with a callback.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use skim::prelude::*;
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.multi(true)
|
||||
.preview_fn(PreviewCallback::from(|items: Vec<Arc<dyn SkimItem>>| {
|
||||
items.iter().map(|s| s.text().to_ascii_uppercase()).collect::<Vec<_>>()
|
||||
}))
|
||||
.build()
|
||||
.unwrap();
|
||||
let item_reader = SkimItemReader::default();
|
||||
|
||||
let input = "aaaaa\nbbbb\nccc";
|
||||
let items = item_reader.of_bufread(Cursor::new(input));
|
||||
let selected_items = Skim::run_with(options, Some(items))
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
20
examples/receiver_multi.rs
Normal file
20
examples/receiver_multi.rs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
//! Sends multiple batches of items through a skim item receiver.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use skim::prelude::*;
|
||||
|
||||
fn main() {
|
||||
let (sender, receiver): (SkimItemSender, SkimItemReceiver) = unbounded();
|
||||
let mut batch = Vec::new();
|
||||
for num in 1..=8 {
|
||||
batch.push(Arc::new(format!("Option {num}")) as Arc<dyn SkimItem>);
|
||||
}
|
||||
sender.send(batch).unwrap();
|
||||
drop(sender); // bug replicates even without this
|
||||
|
||||
let _ = Skim::run_with(
|
||||
SkimOptionsBuilder::default().multi(true).build().unwrap(),
|
||||
Some(receiver),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
//! Minimal example that runs skim and prints selected items.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
|
||||
pub fn main() {
|
||||
fn main() {
|
||||
let options = SkimOptions::default();
|
||||
|
||||
let selected_items = Skim::run_with(&options, None)
|
||||
let selected_items = Skim::run_with(options, None)
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_else(|| Vec::new());
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in selected_items.iter() {
|
||||
print!("{}{}", item.output(), "\n");
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
examples/selector.rs
Normal file
34
examples/selector.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
//! Demonstrates a custom selector implementation.
|
||||
|
||||
extern crate skim;
|
||||
use skim::prelude::*;
|
||||
|
||||
struct BasicSelector {
|
||||
pub pat: String,
|
||||
}
|
||||
|
||||
impl Selector for BasicSelector {
|
||||
fn should_select(&self, _index: usize, item: &dyn SkimItem) -> bool {
|
||||
item.text().contains(&self.pat)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let selector = BasicSelector {
|
||||
pat: String::from("examples"),
|
||||
};
|
||||
let options = SkimOptionsBuilder::default()
|
||||
.multi(true)
|
||||
.selector(Rc::from(selector))
|
||||
.query("skim/")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let selected_items = Skim::run_with(options, None)
|
||||
.map(|out| out.selected_items)
|
||||
.unwrap_or_default();
|
||||
|
||||
for item in &selected_items {
|
||||
println!("{}", item.output());
|
||||
}
|
||||
}
|
||||
23
examples/tick.rs
Normal file
23
examples/tick.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//! Demonstrates manually driving skim ticks.
|
||||
|
||||
use skim::prelude::*;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let opts = SkimOptionsBuilder::default().cmd("cat bench_data.txt").build()?;
|
||||
|
||||
println!("START");
|
||||
let mut skim = Skim::init(opts, None)?;
|
||||
skim.start();
|
||||
skim.init_tui()?;
|
||||
skim.enter().await?;
|
||||
while !skim.tick().await? {
|
||||
if skim.reader_done() && skim.matcher_stopped() {
|
||||
skim.event_sender()
|
||||
.send(Event::Action(Action::Accept(Some(String::from("Done")))))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
println!("DONE: {:?}", skim.output());
|
||||
eyre::Ok(())
|
||||
}
|
||||
24
flake.lock
Normal file
24
flake.lock
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1782545840,
|
||||
"narHash": "sha256-CAi8oAZaE6pTkcYQBnnOlvmfMgG/p1AO0FohKKN3J7I=",
|
||||
"rev": "3d46470bb3030020f7e1361f33514854f5bfa86d",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.11pre1023445.3d46470bb303/nixexprs.tar.xz?lastModified=1782545840&rev=3d46470bb3030020f7e1361f33514854f5bfa86d"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
85
flake.nix
Normal file
85
flake.nix
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
{
|
||||
description = "Nix flake for skim development";
|
||||
|
||||
inputs.nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
|
||||
|
||||
outputs =
|
||||
inputs:
|
||||
let
|
||||
inherit (inputs.nixpkgs) lib;
|
||||
systems = lib.systems.flakeExposed;
|
||||
eachSystem = lib.genAttrs systems;
|
||||
pkgsFor =
|
||||
system:
|
||||
import inputs.nixpkgs {
|
||||
inherit system;
|
||||
config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "vagrant" ];
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells = eachSystem (
|
||||
system:
|
||||
let
|
||||
pkgs = pkgsFor system;
|
||||
|
||||
# --- package groups -------------------------------------------------------
|
||||
base = with pkgs; [
|
||||
rustup
|
||||
just
|
||||
];
|
||||
tests = with pkgs; [
|
||||
cargo-nextest
|
||||
cargo-insta
|
||||
cargo-llvm-cov
|
||||
cargo-fuzz
|
||||
tmux
|
||||
];
|
||||
utils = with pkgs; [
|
||||
hyperfine
|
||||
cargo-edit
|
||||
cargo-public-api
|
||||
cargo-msrv
|
||||
git-cliff
|
||||
cargo-dist
|
||||
cargo-cross
|
||||
cargo-xwin
|
||||
gnuplot
|
||||
llvm
|
||||
cargo-bloat
|
||||
cargo-public-api
|
||||
];
|
||||
gungraun = with pkgs; [
|
||||
valgrind
|
||||
libclang
|
||||
binutils
|
||||
];
|
||||
vagrantDeps = with pkgs; [
|
||||
vagrant
|
||||
rsync
|
||||
];
|
||||
|
||||
# --- shell hooks (only groups that need env vars) -------------------------
|
||||
gungraunHook = ''
|
||||
export LIBCLANG_PATH="${pkgs.libclang.lib}/lib"
|
||||
export LD_LIBRARY_PATH="${pkgs.valgrind.out}/lib:$LD_LIBRARY_PATH"
|
||||
'';
|
||||
vagrantHook = ''
|
||||
export VAGRANT_LIBVIRT_OVMF_CODE="${pkgs.OVMF.fd}/FV/OVMF_CODE.fd"
|
||||
'';
|
||||
|
||||
mkShell = packages: shellHook: pkgs.mkShellNoCC { inherit packages shellHook; };
|
||||
in
|
||||
{
|
||||
default = mkShell base "";
|
||||
tests = mkShell (base ++ tests) "";
|
||||
utils = mkShell (base ++ utils) "";
|
||||
dev = mkShell (base ++ tests ++ utils) "";
|
||||
gungraun = mkShell (base ++ gungraun) gungraunHook;
|
||||
vagrant = mkShell (base ++ vagrantDeps) vagrantHook;
|
||||
full = mkShell (base ++ tests ++ utils ++ gungraun ++ vagrantDeps) (gungraunHook + vagrantHook);
|
||||
}
|
||||
);
|
||||
|
||||
formatter = eachSystem (system: (pkgsFor system).nixfmt);
|
||||
};
|
||||
}
|
||||
4
fuzz/.gitignore
vendored
Normal file
4
fuzz/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
target
|
||||
corpus
|
||||
artifacts
|
||||
coverage
|
||||
2267
fuzz/Cargo.lock
generated
Normal file
2267
fuzz/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
52
fuzz/Cargo.toml
Normal file
52
fuzz/Cargo.toml
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
[package]
|
||||
name = "skim-fuzz"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = "2024"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
libfuzzer-sys = "0.4"
|
||||
regex = "1"
|
||||
|
||||
[dependencies.skim]
|
||||
path = ".."
|
||||
default-features = false
|
||||
|
||||
[[bin]]
|
||||
name = "ansi_strip"
|
||||
path = "fuzz_targets/ansi_strip.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "field_extract"
|
||||
path = "fuzz_targets/field_extract.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzzy_match"
|
||||
path = "fuzz_targets/fuzzy_match.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "query_match"
|
||||
path = "fuzz_targets/query_match.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
|
||||
[[bin]]
|
||||
name = "keymap_parse"
|
||||
path = "fuzz_targets/keymap_parse.rs"
|
||||
test = false
|
||||
doc = false
|
||||
bench = false
|
||||
57
fuzz/README.md
Normal file
57
fuzz/README.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# Fuzzing
|
||||
|
||||
This directory contains [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)
|
||||
(libFuzzer) targets for skim's hand-written, untrusted-input-facing parsers:
|
||||
text that flows in from stdin, `--ansi` sequences, `--nth`/`--with-nth` field
|
||||
specs, the search query syntax, and `--bind` key maps. These are exactly the
|
||||
places where skim does manual byte/char-index bookkeeping on attacker- or
|
||||
data-controlled strings, which is the most panic-prone code in the project.
|
||||
|
||||
## Targets
|
||||
|
||||
| Target | Exercises |
|
||||
|-------------------|----------------------------------------------------------------------------|
|
||||
| `ansi_strip` | `helper::item::strip_ansi` — ANSI escape stripping & byte/char index map |
|
||||
| `field_extract` | `field::{FieldRange, get_string_by_field, parse_matching_fields, parse_transform_fields}` — `--nth`/`--with-nth` |
|
||||
| `fuzzy_match` | `fuzzy_matcher::{skim, fzy, clangd}` — the fuzzy matching algorithms |
|
||||
| `query_match` | `Matcher::create_engine_factory` + `DefaultSkimItem` — the full query → engine → match pipeline (exact/regex/AND-OR/fuzzy, with ANSI) |
|
||||
| `keymap_parse` | `binds::KeyMap` — the `--bind` key-map parser |
|
||||
|
||||
Each target asserts more than "doesn't panic" where a cheap invariant is
|
||||
available (e.g. reported match indices must be valid char indices into the
|
||||
matched text, index mappings must stay monotonic and land on char
|
||||
boundaries).
|
||||
|
||||
## Running
|
||||
|
||||
Install `cargo-fuzz` (requires a nightly toolchain):
|
||||
|
||||
```sh
|
||||
cargo install cargo-fuzz
|
||||
```
|
||||
|
||||
Run a target:
|
||||
|
||||
```sh
|
||||
cargo +nightly fuzz run ansi_strip
|
||||
```
|
||||
|
||||
Run for a bounded time (useful in CI or for a quick check):
|
||||
|
||||
```sh
|
||||
cargo +nightly fuzz run query_match -- -max_total_time=60
|
||||
```
|
||||
|
||||
## Reproducing a crash
|
||||
|
||||
`cargo fuzz run` writes failing inputs to `fuzz/artifacts/<target>/`. Replay one with:
|
||||
|
||||
```sh
|
||||
cargo +nightly fuzz run <target> fuzz/artifacts/<target>/crash-<hash>
|
||||
```
|
||||
|
||||
## Adding a target
|
||||
|
||||
Add a new `fuzz_targets/<name>.rs`, register it in `fuzz/Cargo.toml`'s
|
||||
`[[bin]]` list, and prefer asserting a real invariant of the function under
|
||||
test (bounds, monotonicity, round-tripping) rather than only catching panics.
|
||||
37
fuzz/fuzz_targets/ansi_strip.rs
Normal file
37
fuzz/fuzz_targets/ansi_strip.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use skim::helper::item::strip_ansi;
|
||||
|
||||
// `strip_ansi` hand-parses ESC sequences while tracking a byte/char index
|
||||
// mapping back to the original string. It is run on every line read from
|
||||
// stdin when `--ansi` is set, so it must never panic on adversarial input
|
||||
// and the mapping it returns must stay internally consistent.
|
||||
fuzz_target!(|input: &str| {
|
||||
let (stripped, mapping) = strip_ansi(input);
|
||||
|
||||
assert_eq!(
|
||||
mapping.len(),
|
||||
stripped.chars().count(),
|
||||
"mapping length must match the number of chars in the stripped string"
|
||||
);
|
||||
|
||||
let mut prev_byte_pos = None;
|
||||
for &(byte_pos, char_idx) in &mapping {
|
||||
assert!(
|
||||
input.is_char_boundary(byte_pos),
|
||||
"byte_pos {byte_pos} is not a char boundary in the original string"
|
||||
);
|
||||
// char_idx must be exactly the char position of byte_pos in the
|
||||
// original string; this is stronger than (and implies) monotonicity.
|
||||
let expected_char_idx = input[..byte_pos].chars().count();
|
||||
assert_eq!(
|
||||
char_idx, expected_char_idx,
|
||||
"char_idx must equal the char position of byte_pos in the original string"
|
||||
);
|
||||
if let Some(prev) = prev_byte_pos {
|
||||
assert!(prev < byte_pos, "byte positions in mapping must be strictly increasing");
|
||||
}
|
||||
prev_byte_pos = Some(byte_pos);
|
||||
}
|
||||
});
|
||||
47
fuzz/fuzz_targets/field_extract.rs
Normal file
47
fuzz/fuzz_targets/field_extract.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#![no_main]
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use regex::Regex;
|
||||
use skim::field::{FieldRange, get_string_by_field, parse_matching_fields, parse_transform_fields};
|
||||
|
||||
// Fuzzes the --nth/--with-nth field range parser and extractor, which slices
|
||||
// arbitrary user-supplied text on an arbitrary user-supplied delimiter regex.
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct FieldFuzzInput<'a> {
|
||||
delimiter_pattern: &'a str,
|
||||
text: &'a str,
|
||||
range_specs: Vec<&'a str>,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: FieldFuzzInput| {
|
||||
// Bound the delimiter pattern length so we spend fuzzing time on the
|
||||
// field logic rather than on the regex engine's own parser.
|
||||
if input.delimiter_pattern.len() > 32 {
|
||||
return;
|
||||
}
|
||||
let Ok(delimiter) = Regex::new(input.delimiter_pattern) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let fields: Vec<FieldRange> = input
|
||||
.range_specs
|
||||
.iter()
|
||||
.filter_map(|s| FieldRange::from_str(s))
|
||||
.collect();
|
||||
|
||||
// `fields` can repeat/overlap ranges, so the transformed text is not
|
||||
// bounded by the input length; just check it doesn't panic.
|
||||
let _ = parse_transform_fields(&delimiter, input.text, &fields);
|
||||
|
||||
for (begin, end) in parse_matching_fields(&delimiter, input.text, &fields) {
|
||||
assert!(begin <= end, "field range must not be inverted");
|
||||
assert!(end <= input.text.len(), "field range must stay within the text");
|
||||
// Slicing must not panic: begin/end must land on char boundaries.
|
||||
let _ = &input.text[begin..end];
|
||||
}
|
||||
|
||||
for field in &fields {
|
||||
let _ = get_string_by_field(&delimiter, input.text, field);
|
||||
}
|
||||
});
|
||||
39
fuzz/fuzz_targets/fuzzy_match.rs
Normal file
39
fuzz/fuzz_targets/fuzzy_match.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#![no_main]
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use skim::fuzzy_matcher::FuzzyMatcher;
|
||||
use skim::fuzzy_matcher::clangd::ClangdMatcher;
|
||||
use skim::fuzzy_matcher::fzy::FzyMatcher;
|
||||
use skim::fuzzy_matcher::skim::SkimMatcherV2;
|
||||
|
||||
// Fuzzes the fuzzy matching algorithms directly on arbitrary unicode
|
||||
// (choice, pattern) pairs. These run a lot of hand-written index/DP-matrix
|
||||
// arithmetic over `char` boundaries, so they're prone to panics (overflow,
|
||||
// out-of-bounds) on adversarial unicode input, and the returned match
|
||||
// indices must always be valid character indices into `choice`.
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct MatchInput<'a> {
|
||||
choice: &'a str,
|
||||
pattern: &'a str,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: MatchInput| {
|
||||
let skim_matcher = SkimMatcherV2::default();
|
||||
let fzy_matcher = FzyMatcher::default();
|
||||
let clangd_matcher = ClangdMatcher::default();
|
||||
|
||||
let matchers: [&dyn FuzzyMatcher; 3] = [&skim_matcher, &fzy_matcher, &clangd_matcher];
|
||||
|
||||
let num_chars = input.choice.chars().count();
|
||||
for matcher in matchers {
|
||||
if let Some((_score, indices)) = matcher.fuzzy_indices(input.choice, input.pattern) {
|
||||
for &idx in &indices {
|
||||
assert!(
|
||||
idx < num_chars,
|
||||
"match index {idx} out of bounds for choice with {num_chars} chars"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
10
fuzz/fuzz_targets/keymap_parse.rs
Normal file
10
fuzz/fuzz_targets/keymap_parse.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
#![no_main]
|
||||
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use skim::binds::KeyMap;
|
||||
|
||||
// Fuzzes the `--bind` key-map parser, which splits an arbitrary user-supplied
|
||||
// string on commas and colons to build key/action bindings.
|
||||
fuzz_target!(|input: &str| {
|
||||
let _ = KeyMap::from(input);
|
||||
});
|
||||
58
fuzz/fuzz_targets/query_match.rs
Normal file
58
fuzz/fuzz_targets/query_match.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#![no_main]
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
use regex::Regex;
|
||||
use skim::helper::item::DefaultSkimItem;
|
||||
use skim::matcher::Matcher;
|
||||
use skim::{SkimItem, SkimOptions};
|
||||
|
||||
// End-to-end fuzz of the query -> engine -> match pipeline: builds a real
|
||||
// `DefaultSkimItem` (exercising ANSI stripping / field transforms) and
|
||||
// matches it with an engine built the same way skim builds it from CLI
|
||||
// options (exact/regex/andor/fuzzy-algorithm wrapping), using an arbitrary
|
||||
// query string. Checks that matching never panics and that any reported
|
||||
// match range stays within the bounds of the text that was actually matched.
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct QueryInput<'a> {
|
||||
query: &'a str,
|
||||
text: &'a str,
|
||||
exact: bool,
|
||||
regex: bool,
|
||||
ansi: bool,
|
||||
case: u8,
|
||||
}
|
||||
|
||||
fuzz_target!(|input: QueryInput| {
|
||||
// The regex engine path takes the query as a user-supplied pattern; keep
|
||||
// it short so fuzzing time goes into skim's logic, not regex parsing.
|
||||
if input.regex && input.query.len() > 32 {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut options = SkimOptions::default();
|
||||
options.exact = input.exact;
|
||||
options.regex = input.regex;
|
||||
options.case = match input.case % 3 {
|
||||
0 => skim::CaseMatching::Respect,
|
||||
1 => skim::CaseMatching::Ignore,
|
||||
_ => skim::CaseMatching::Smart,
|
||||
};
|
||||
|
||||
let factory = Matcher::create_engine_factory(&options);
|
||||
let engine = factory.create_engine_with_case(input.query, options.case);
|
||||
|
||||
let delimiter = Regex::new(" ").unwrap();
|
||||
let item = DefaultSkimItem::new(input.text, input.ansi, &[], &[], &delimiter);
|
||||
|
||||
if let Some(result) = engine.match_item(&item) {
|
||||
let matched_text = item.text();
|
||||
let num_chars = matched_text.chars().count();
|
||||
for idx in result.range_char_indices(&matched_text) {
|
||||
assert!(
|
||||
idx <= num_chars,
|
||||
"matched char index {idx} out of bounds ({num_chars} chars)"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
69
install
69
install
|
|
@ -1,69 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# This script trys to download the correct version of binary from github.
|
||||
# You can download it manually and put it(i.e. `sk`) under `bin/`.
|
||||
#
|
||||
# If you know rust or have rust installed, you can build it with
|
||||
# `cargo build --release`
|
||||
# Note that nightly rust is required.
|
||||
|
||||
set -u
|
||||
|
||||
version="0.9.4"
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
skim_base="$(pwd)"
|
||||
|
||||
check_binary() {
|
||||
echo -n " - Checking skim executable ... "
|
||||
local output
|
||||
output=$("$skim_base"/bin/sk --version 2>&1)
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: $output"
|
||||
elif [ "$version" != "$output" ]; then
|
||||
echo "$output != $version"
|
||||
else
|
||||
echo "$output"
|
||||
return 0
|
||||
fi
|
||||
rm -f "$skim_base"/bin/sk
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
# download version
|
||||
download() {
|
||||
echo "Downloading bin/sk ..."
|
||||
mkdir -p "$skim_base"/bin && cd "$skim_base"/bin
|
||||
if [ $? -ne 0 ]; then
|
||||
binary_error="Failed to create bin directory"
|
||||
return
|
||||
fi
|
||||
|
||||
check_binary
|
||||
|
||||
local url=https://github.com/lotabout/skim/releases/download/v$version/${1}.tar.gz
|
||||
echo "Downloading: $url"
|
||||
if command -v curl > /dev/null; then
|
||||
curl -fL $url | tar xz
|
||||
elif command -v wget > /dev/null; then
|
||||
wget -O - $url | tar xz
|
||||
else
|
||||
binary_error="curl or wget not found"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -f $1 ]; then
|
||||
binary_error="Failed to download ${1}"
|
||||
return
|
||||
fi
|
||||
}
|
||||
|
||||
archi=$(uname -sm)
|
||||
case "$archi" in
|
||||
Darwin\ x86_64) download skim-v$version-${binary_arch:-x86_64}-apple-darwin;;
|
||||
Linux\ x86_64) download skim-v$version-${binary_arch:-x86_64}-unknown-linux-musl;;
|
||||
*) ;;
|
||||
esac
|
||||
|
||||
echo "Done :)"
|
||||
99
justfile
Normal file
99
justfile
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
alias pr := pr-review
|
||||
|
||||
bump-version version:
|
||||
sed -i 's/^version = ".*"/version = "{{ version }}"/' ./Cargo.toml
|
||||
|
||||
generate-files:
|
||||
SKIM_DEFAULT_OPTIONS= cargo run -- --man > ./man/man1/sk.1
|
||||
SKIM_DEFAULT_OPTIONS= cargo run -- --shell bash > ./shell/completion.bash
|
||||
SKIM_DEFAULT_OPTIONS= cargo run -- --shell zsh > ./shell/completion.zsh
|
||||
SKIM_DEFAULT_OPTIONS= cargo run -- --shell fish > ./shell/completion.fish
|
||||
SKIM_DEFAULT_OPTIONS= cargo run -- --shell nushell > ./shell/completion.nu
|
||||
|
||||
changelog version:
|
||||
git cliff -p CHANGELOG.md -t 'v{{ version }}' -u
|
||||
|
||||
release version: (bump-version version) generate-files (changelog version) test
|
||||
cargo generate-lockfile
|
||||
echo '{{ version }}' > shell/version.txt
|
||||
git add CHANGELOG.md Cargo.lock Cargo.toml man/ shell/
|
||||
git commit -m 'release: v{{ version }}'
|
||||
git tag 'v{{ version }}'
|
||||
read -p "Press any key to confirm pushing tag v{{ version }}"
|
||||
git push
|
||||
git push --tags
|
||||
|
||||
auto-release:
|
||||
just release $(git cliff --bumped-version | sed 's/v\(.*\)/\1/')
|
||||
|
||||
test target="":
|
||||
cargo test --doc
|
||||
cargo nextest run {{ target }}
|
||||
# Each e2e test creates and tears down its own Zellij session, so there is
|
||||
# no shared multiplexer session to clean up here.
|
||||
|
||||
bench-plot bins="./target/release/sk sk fzf":
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
declare -A inputs=(
|
||||
["1"]=1
|
||||
["10"]=10
|
||||
["100"]=100
|
||||
["1K"]=1000
|
||||
["10K"]=10000
|
||||
["100K"]=100000
|
||||
["1M"]=1000000
|
||||
["10M"]=10000000
|
||||
["100M"]=100000000
|
||||
)
|
||||
|
||||
echo "" > /tmp/bench.json
|
||||
for f in "${!inputs[@]}"; do
|
||||
p="benches/fixtures/$f.txt"
|
||||
n="${inputs[$f]}"
|
||||
if [ ! -f "$p" ]; then
|
||||
cargo bench --bench cli -- generate -n "$n" -f "$p"
|
||||
fi
|
||||
# The formula for `--stable-secs` might need adjusting if the number of results varies between runs with the same input
|
||||
cargo bench --bench cli -- run {{ bins }} -f "$p" --runs 10 --stable-secs "$(( 3 * $n / 10000000 )).5" --json >> /tmp/bench.json
|
||||
done
|
||||
|
||||
cargo bench --bench cli -- plot -i /tmp/bench.json
|
||||
|
||||
pr-review id="":
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PR_ID="{{ id }}"
|
||||
if [[ -z "$PR_ID" ]]; then
|
||||
PR_ID="$(gh pr list | sk | cut -d' ' -f1)"
|
||||
fi
|
||||
echo "Checking out PR $PR_ID"
|
||||
|
||||
gh pr checkout "$PR_ID"
|
||||
|
||||
# Check the PR title
|
||||
PR_TITLE="$(gh pr view | head -n1 | sed 's@title:\s\+@@;s@skim-rs/skim#.*@@')"
|
||||
echo "Checking $PR_TITLE by simulating a git cliff generation"
|
||||
|
||||
git cliff --from-context <(echo '[{"commits":[{"id": "foo", "message": "'$PR_TITLE'", "links": [], "author": {"name": "","timestamp":1}, "committer": {"name": "", "timestamp":0}, "merge_commit": false,"github":{"pr_labels":[], "is_first_time": false},"gitlab":{"pr_labels":[],"is_first_time":false},"gitea":{"pr_labels":[],"is_first_time":false},"bitbucket":{"pr_labels":[],"is_first_time":false},"azure_devops":{"pr_labels":[],"is_first_time":false}}],"submodule_commits":{},"github":{"contributors":[]},"gitlab":{"contributors":[]},"gitea":{"contributors":[]},"bitbucket":{"contributors":[]},"azure_devops":{"contributors":[]}}]') -s all | grep -Ev '^(## \[unreleased\]|)$' | grep -q '^.\+$'
|
||||
|
||||
echo "Check done, PR title is valid"
|
||||
|
||||
git diff master
|
||||
tty -s && read -p "Confirm code review ?"
|
||||
|
||||
just generate-files
|
||||
(git add man/ shell/ && git commit -m 'chore: generate-files' && git push) || echo "Nothing to do"
|
||||
|
||||
coverage *args="":
|
||||
cargo +nightly llvm-cov nextest --ignore-run-fail --branch --lib --bins --examples --tests {{ args }}
|
||||
cargo +nightly llvm-cov report --html
|
||||
|
||||
clippy *args="":
|
||||
cargo clippy --all-targets {{ args }}
|
||||
|
||||
fmt *args="":
|
||||
cargo +nightly fmt {{ args }}
|
||||
|
|
@ -22,7 +22,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
..
|
||||
.TH sk-tmux 1 "Oct 2018" "sk 0.17.5" "sk-tmux - open sk in tmux split pane"
|
||||
.TH sk-tmux 1 "Oct 2018" "sk 0.10.4" "sk-tmux - open sk in tmux split pane"
|
||||
|
||||
.SH NAME
|
||||
sk-tmux - open sk in tmux split pane
|
||||
|
|
|
|||
1864
man/man1/sk.1
1864
man/man1/sk.1
File diff suppressed because it is too large
Load diff
|
|
@ -136,38 +136,12 @@ function! s:default_layout()
|
|||
\ : { 'down': '~40%' }
|
||||
endfunction
|
||||
|
||||
function! skim#install()
|
||||
if s:is_win && !has('win32unix')
|
||||
let script = s:base_dir.'/install.ps1'
|
||||
if !filereadable(script)
|
||||
throw script.' not found'
|
||||
endif
|
||||
let script = 'powershell -ExecutionPolicy Bypass -file ' . script
|
||||
else
|
||||
let script = s:base_dir.'/install'
|
||||
if !executable(script)
|
||||
throw script.' not found'
|
||||
endif
|
||||
let script .= ' --bin'
|
||||
endif
|
||||
|
||||
call s:warn('Running skim installer ...')
|
||||
call system(script)
|
||||
if v:shell_error
|
||||
throw 'Failed to download skim: '.script
|
||||
endif
|
||||
endfunction
|
||||
|
||||
function! skim#exec()
|
||||
if !exists('s:exec')
|
||||
if executable(s:skim_rs)
|
||||
let s:exec = s:skim_rs
|
||||
elseif executable('sk')
|
||||
let s:exec = 'sk'
|
||||
elseif input('skim executable not found. Download binary? (y/n) ') =~? '^y'
|
||||
redraw
|
||||
call skim#install()
|
||||
return skim#exec()
|
||||
else
|
||||
redraw
|
||||
throw 'skim executable not found'
|
||||
|
|
@ -342,6 +316,12 @@ function! skim#wrap(...)
|
|||
endif
|
||||
endif
|
||||
|
||||
" Interactive commands should use --cmd-history for query history
|
||||
let history_option = '--history'
|
||||
if index(opts['options'], '-i') != -1
|
||||
let history_option = '--cmd-history'
|
||||
endif
|
||||
|
||||
" Colors: g:skim_colors
|
||||
let opts.options = s:defaults() .' '. s:evaluate_opts(get(opts, 'options', ''))
|
||||
|
||||
|
|
@ -352,13 +332,13 @@ function! skim#wrap(...)
|
|||
call mkdir(dir, 'p')
|
||||
endif
|
||||
let history = skim#shellescape(dir.'/'.name)
|
||||
let opts.options = join(['--history', history, opts.options])
|
||||
let opts.options = join([history_option, history, opts.options])
|
||||
endif
|
||||
|
||||
" Action: g:skim_action
|
||||
if !s:has_any(opts, ['sink', 'sink*'])
|
||||
let opts._action = get(g:, 'skim_action', s:default_action)
|
||||
let opts.options .= ' --expect='.join(keys(opts._action), ',')
|
||||
let opts.options .= ' '.skim#shellescape('--bind='.join(map(keys(opts._action), 'v:val.":accept(".v:val.")"'), ','))
|
||||
function! opts.sink(lines) abort
|
||||
return s:common_sink(self._action, a:lines)
|
||||
endfunction
|
||||
|
|
@ -512,21 +492,17 @@ function! s:dopopd()
|
|||
return
|
||||
endif
|
||||
|
||||
" FIXME: We temporarily change the working directory to 'dir' entry
|
||||
" Note: We temporarily change the working directory to 'dir' entry
|
||||
" of options dictionary (set to the current working directory if not given)
|
||||
" before running skim.
|
||||
"
|
||||
" e.g. call skim#run({'dir': '/tmp', 'source': 'ls', 'sink': 'e'})
|
||||
"
|
||||
" After processing the sink function, we have to restore the current working
|
||||
" directory. But doing so may not be desirable if the function changed the
|
||||
" working directory on purpose.
|
||||
"
|
||||
" So how can we tell if we should do it or not? A simple heuristic we use
|
||||
" here is that we change directory only if the current working directory
|
||||
" matches 'dir' entry. However, it is possible that the sink function did
|
||||
" change the directory to 'dir'. In that case, the user will have an
|
||||
" unexpected result.
|
||||
" After processing the sink function, we restore the current working
|
||||
" directory using a heuristic: we only change directory if the current
|
||||
" working directory matches 'dir' entry. This handles most cases correctly,
|
||||
" though it may not restore the directory if the sink function explicitly
|
||||
" changed to the 'dir' path.
|
||||
if s:skim_getcwd() ==# w:skim_pushd.dir && (!&autochdir || w:skim_pushd.bufname ==# bufname(''))
|
||||
execute w:skim_pushd.command s:escape(w:skim_pushd.origin)
|
||||
endif
|
||||
|
|
@ -931,6 +907,7 @@ function! s:popup(opts) abort
|
|||
endfunction
|
||||
|
||||
let s:default_action = {
|
||||
\ 'enter': 'edit',
|
||||
\ 'ctrl-t': 'tab split',
|
||||
\ 'ctrl-x': 'split',
|
||||
\ 'ctrl-v': 'vsplit' }
|
||||
|
|
|
|||
4
rust-toolchain.toml
Normal file
4
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
[toolchain]
|
||||
channel = "stable"
|
||||
profile = "default"
|
||||
components = ["rust-analyzer"]
|
||||
|
|
@ -1,373 +1,438 @@
|
|||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.bash
|
||||
#
|
||||
# - $SKIM_TMUX (default: 0)
|
||||
# - $SKIM_TMUX_OPTS (default: empty)
|
||||
# - $SKIM_COMPLETION_TRIGGER (default: '**')
|
||||
# - $SKIM_COMPLETION_OPTS (default: empty)
|
||||
# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/completion.bash
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
# To use custom commands instead of find, override _skim_compgen_{path,dir}
|
||||
if ! declare -f _skim_compgen_path > /dev/null; then
|
||||
_skim_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _skim_compgen_dir > /dev/null; then
|
||||
_skim_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
###########################################################
|
||||
|
||||
# To redraw line after skim closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line'
|
||||
|
||||
__skim_comprun() {
|
||||
if [ "$(type -t _skim_comprun 2>&1)" = function ]; then
|
||||
_skim_comprun "$@"
|
||||
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
|
||||
shift
|
||||
sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- "$@"
|
||||
else
|
||||
shift
|
||||
sk "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
__skim_orig_completion_filter() {
|
||||
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _skim_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__skim_nospace_commands" = *" \3 "* ]] \&\& __skim_nospace_commands="$__skim_nospace_commands \3 ";/' |
|
||||
awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1'
|
||||
}
|
||||
|
||||
_skim_opts_completion() {
|
||||
local cur prev opts
|
||||
COMPREPLY=()
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="${COMP_WORDS[COMP_CWORD-1]}"
|
||||
opts="
|
||||
-x --extended
|
||||
-e --exact
|
||||
--algo
|
||||
-i +i
|
||||
-n --nth
|
||||
--with-nth
|
||||
-d --delimiter
|
||||
+s --no-sort
|
||||
--tac
|
||||
--tiebreak
|
||||
-m --multi
|
||||
--no-mouse
|
||||
--bind
|
||||
--cycle
|
||||
--no-hscroll
|
||||
--jump-labels
|
||||
--height
|
||||
--literal
|
||||
--reverse
|
||||
--margin
|
||||
--inline-info
|
||||
--prompt
|
||||
--pointer
|
||||
--marker
|
||||
--header
|
||||
--header-lines
|
||||
--ansi
|
||||
--tabstop
|
||||
--color
|
||||
--no-bold
|
||||
--history
|
||||
--history-size
|
||||
--preview
|
||||
--preview-window
|
||||
-q --query
|
||||
-1 --select-1
|
||||
-0 --exit-0
|
||||
-f --filter
|
||||
--print-query
|
||||
--expect
|
||||
--sync"
|
||||
|
||||
case "${prev}" in
|
||||
--tiebreak)
|
||||
COMPREPLY=( $(compgen -W "length begin end index" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=( $(compgen -W "dark light 16 bw" -- "$cur") )
|
||||
return 0
|
||||
;;
|
||||
--history)
|
||||
_sk() {
|
||||
local i cur prev opts cmd
|
||||
COMPREPLY=()
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$cur" =~ ^-|\+ ]]; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "$cur") )
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
_skim_handle_dynamic_completion() {
|
||||
local cmd orig_var orig ret orig_cmd orig_complete
|
||||
cmd="$1"
|
||||
shift
|
||||
orig_cmd="$1"
|
||||
orig_var="_skim_orig_completion_$cmd"
|
||||
orig="${!orig_var##*#}"
|
||||
if [ -n "$orig" ] && type "$orig" > /dev/null 2>&1; then
|
||||
$orig "$@"
|
||||
elif [ -n "$_skim_completion_loader" ]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2> /dev/null)
|
||||
_completion_loader "$@"
|
||||
ret=$?
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [ "$(complete -p "$orig_cmd" 2> /dev/null)" != "$orig_complete" ]; then
|
||||
eval "$(complete | command grep " -F.* $orig_cmd$" | __skim_orig_completion_filter)"
|
||||
if [[ "$__skim_nospace_commands" = *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
fi
|
||||
fi
|
||||
return $ret
|
||||
fi
|
||||
}
|
||||
|
||||
__skim_generic_path_completion() {
|
||||
local cur base dir leftover matches trigger cmd
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
COMPREPLY=()
|
||||
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base"
|
||||
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [ -z "$dir" ] || [ -d "$dir" ]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $2" __skim_comprun "$4" -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "$__skim_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [ -n "$matches" ]; then
|
||||
COMPREPLY=( "$matches" )
|
||||
else
|
||||
COMPREPLY=( "$cur" )
|
||||
fi
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
shift
|
||||
shift
|
||||
shift
|
||||
_skim_handle_dynamic_completion "$cmd" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
_skim_complete() {
|
||||
# Split arguments around --
|
||||
local args rest str_arg i sep
|
||||
args=("$@")
|
||||
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 | awk '{print $2}')_post"
|
||||
type -t "$post" > /dev/null 2>&1 || post=cat
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [ -n "$selected" ]; then
|
||||
COMPREPLY=("$selected")
|
||||
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
|
||||
cur="$2"
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
fi
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
else
|
||||
_skim_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||
fi
|
||||
}
|
||||
prev="$3"
|
||||
cmd=""
|
||||
opts=""
|
||||
|
||||
_skim_path_completion() {
|
||||
__skim_generic_path_completion _skim_compgen_path "-m" "" "$@"
|
||||
}
|
||||
for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
|
||||
do
|
||||
case "${cmd},${i}" in
|
||||
",$1")
|
||||
cmd="sk"
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Deprecated. No file only completion.
|
||||
_skim_file_completion() {
|
||||
_skim_path_completion "$@"
|
||||
}
|
||||
|
||||
_skim_dir_completion() {
|
||||
__skim_generic_path_completion _skim_compgen_dir "" "/" "$@"
|
||||
}
|
||||
|
||||
_skim_complete_kill() {
|
||||
local trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ -z "$cur" ]]; then
|
||||
COMP_WORDS[$COMP_CWORD]=$trigger
|
||||
elif [[ "$cur" != *"$trigger" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_skim_proc_completion "$@"
|
||||
}
|
||||
|
||||
_skim_proc_completion() {
|
||||
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
)
|
||||
}
|
||||
|
||||
_skim_proc_completion_post() {
|
||||
awk '{print $2}'
|
||||
}
|
||||
|
||||
_skim_host_completion() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
|
||||
_skim_var_completion() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_alias_completion() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
alias | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
# skim options
|
||||
complete -o default -F _skim_opts_completion sk
|
||||
|
||||
d_cmds="${SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||
find git grep gunzip gzip hg jar
|
||||
ln ls mv open rm rsync scp
|
||||
svn tar unzip zip"
|
||||
|
||||
# Preserve existing completion
|
||||
eval "$(complete |
|
||||
sed -E '/-F/!d; / _skim/d; '"/ ($(echo $d_cmds $a_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
|
||||
__skim_orig_completion_filter)"
|
||||
|
||||
if type _completion_loader > /dev/null 2>&1; then
|
||||
_skim_completion_loader=1
|
||||
fi
|
||||
|
||||
__skim_defc() {
|
||||
local cmd func opts orig_var orig def
|
||||
cmd="$1"
|
||||
func="$2"
|
||||
opts="$3"
|
||||
orig_var="_skim_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var}"
|
||||
if [ -n "$orig" ]; then
|
||||
printf -v def "$orig" "$func"
|
||||
eval "$def"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
# Anything
|
||||
for cmd in $a_cmds; do
|
||||
__skim_defc "$cmd" _skim_path_completion "-o default -o bashdefault"
|
||||
done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
__skim_defc "$cmd" _skim_dir_completion "-o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# Kill completion (supports empty completion trigger)
|
||||
complete -F _skim_complete_kill -o default -o bashdefault kill
|
||||
|
||||
unset cmd d_cmds a_cmds
|
||||
|
||||
_skim_setup_completion() {
|
||||
local kind fn cmd
|
||||
kind=$1
|
||||
fn=_skim_${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
|
||||
eval "$(complete -p "$@" 2> /dev/null | grep -v "$fn" | __skim_orig_completion_filter)"
|
||||
for cmd in "$@"; do
|
||||
case "$kind" in
|
||||
dir) __skim_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||
var) __skim_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||
alias) __skim_defc "$cmd" "$fn" "-a" ;;
|
||||
*) __skim_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||
case "${cmd}" in
|
||||
sk)
|
||||
opts="-t -n -d -e -b -m -c -i -I -p -q -1 -0 -f -x -h -V --tac --min-query-length --no-sort --tiebreak --nth --with-nth --hide-nth --delimiter --exact --regex --algo --case --typos --no-typos --normalize --split-match --last-match --scheme --bind --multi --no-multi --no-mouse --cmd --interactive --color --highlight-line --no-hscroll --keep-right --skip-to-pattern --no-clear-if-empty --no-clear-start --no-clear --show-cmd-error --cycle --disabled --disable-pattern --layout --reverse --height --no-height --min-height --margin --prompt --cmd-prompt --selector --multi-selector --ansi --tabstop --ellipsis --info --no-info --inline-info --header --header-lines --border --border-no-collapse --no-border --wrap --multiline --scrollbar --no-scrollbar --history --history-size --cmd-history --cmd-history-size --preview --preview-window --image --query --cmd-query --read0 --print0 --print-query --print-cmd --print-score --print-header --print-current --output-format --no-strip-ansi --select-1 --exit-0 --sync --pre-select-n --pre-select-pat --pre-select-items --pre-select-file --filter --shell --shell-bindings --man --listen --remote --popup --log-level --log-file --flags --extended --literal --hscroll-off --filepath-word --jump-labels --no-bold --phony --tail --style --no-color --padding --border-label --border-label-pos --wrap-sign --no-multi-line --raw --track --gap --gap-line --freeze-left --freeze-right --scroll-off --gutter --gutter-raw --marker-multi-line --list-border --list-label --list-label-pos --no-input --info-command --separator --no-separator --ghost --input-border --input-label --input-label-pos --preview-label --preview-label-pos --header-first --header-border --header-lines-border --footer --footer-border --footer-label --footer-label-pos --with-shell --expect --help --version"
|
||||
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
fi
|
||||
case "${prev}" in
|
||||
--min-query-length)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--tiebreak)
|
||||
COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index pathname -pathname" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-t)
|
||||
COMPREPLY=($(compgen -W "score -score begin -begin end -end length -length index -index pathname -pathname" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--nth)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-n)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--with-nth)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--hide-nth)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--delimiter)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-d)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--algo)
|
||||
COMPREPLY=($(compgen -W "arinae clangd fzy frizbee skim_v2" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--case)
|
||||
COMPREPLY=($(compgen -W "respect ignore smart" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--typos)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--split-match)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--scheme)
|
||||
COMPREPLY=($(compgen -W "default path history" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--bind)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-b)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--cmd)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-c)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-I)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--color)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--skip-to-pattern)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--disable-pattern)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--layout)
|
||||
COMPREPLY=($(compgen -W "default reverse reverse-list" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--height)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--min-height)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--margin)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--prompt)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-p)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--cmd-prompt)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--selector)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--multi-selector)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--tabstop)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--ellipsis)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--info)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--header)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--header-lines)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--border)
|
||||
COMPREPLY=($(compgen -W "force-off none plain rounded double thick light-double-dashed heavy-double-dashed light-triple-dashed heavy-triple-dashed light-quadruple-dashed heavy-quadruple-dashed quadrant-inside quadrant-outside" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--multiline)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--scrollbar)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--history)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--history-size)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--cmd-history)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--cmd-history-size)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--preview)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--preview-window)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--image)
|
||||
COMPREPLY=($(compgen -W "detect halfblocks" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--query)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-q)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--cmd-query)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--output-format)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--pre-select-n)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--pre-select-pat)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--pre-select-items)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--pre-select-file)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--filter)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
-f)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--shell)
|
||||
COMPREPLY=($(compgen -W "bash elvish fish nushell power-shell zsh" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--listen)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--remote)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--popup)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--log-level)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--log-file)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--flags)
|
||||
COMPREPLY=($(compgen -W "no-preview-pty show-score show-index single-reader single-matcher" -- "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--hscroll-off)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--jump-labels)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--tail)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--style)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--padding)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--border-label)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--border-label-pos)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--wrap-sign)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--gap)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--gap-line)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--freeze-left)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--freeze-right)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--scroll-off)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--gutter)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--gutter-raw)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--marker-multi-line)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--list-border)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--list-label)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--list-label-pos)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--info-command)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--separator)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--ghost)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--input-border)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--input-label)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--input-label-pos)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--preview-label)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--preview-label-pos)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--header-border)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--header-lines-border)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--footer)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--footer-border)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--footer-label)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--footer-label-pos)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--with-shell)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
--expect)
|
||||
COMPREPLY=($(compgen -f "${cur}"))
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=()
|
||||
;;
|
||||
esac
|
||||
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts
|
||||
_skim_setup_completion 'var' export unset
|
||||
_skim_setup_completion 'alias' unalias
|
||||
_skim_setup_completion 'host' ssh telnet
|
||||
|
||||
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
|
||||
complete -F _sk -o nosort -o bashdefault -o default sk
|
||||
else
|
||||
complete -F _sk -o bashdefault -o default sk
|
||||
fi
|
||||
|
|
|
|||
190
shell/completion.fish
Normal file
190
shell/completion.fish
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
complete -c sk -l min-query-length -d 'Minimum query length to start showing results' -r
|
||||
complete -c sk -s t -l tiebreak -d 'Comma-separated list of sort criteria to apply when the scores are tied.' -r -f -a "score\t''
|
||||
-score\t''
|
||||
begin\t''
|
||||
-begin\t''
|
||||
end\t''
|
||||
-end\t''
|
||||
length\t''
|
||||
-length\t''
|
||||
index\t''
|
||||
-index\t''
|
||||
pathname\t''
|
||||
-pathname\t''"
|
||||
complete -c sk -s n -l nth -d 'Fields to be matched' -r
|
||||
complete -c sk -l with-nth -d 'Fields to be transformed' -r
|
||||
complete -c sk -l hide-nth -d 'Fields to hide from display while keeping them searchable' -r
|
||||
complete -c sk -s d -l delimiter -d 'Delimiter between fields' -r
|
||||
complete -c sk -l algo -d 'Fuzzy matching algorithm' -r -f -a "arinae\t'Arinae: typo-resistant & natural algorithm, default'
|
||||
clangd\t'Clangd fuzzy matching algorithm'
|
||||
fzy\t'Fzy matching algorithm (https://github.com/jhawthorn/fzy)'
|
||||
frizbee\t'Frizbee matching algorithm, typo resistant'
|
||||
skim_v2\t'Previous skim fuzzy matching algorithm (v2)'"
|
||||
complete -c sk -l case -d 'Case sensitivity' -r -f -a "respect\t'Case-sensitive matching'
|
||||
ignore\t'Case-insensitive matching'
|
||||
smart\t'Smart case: case-insensitive unless query contains uppercase'"
|
||||
complete -c sk -l typos -d 'Enable typo-tolerant matching' -r
|
||||
complete -c sk -l split-match -d 'Enable split matching and set delimiter' -r
|
||||
complete -c sk -l scheme -r -f -a "default\t'Default scheme, no modifications to the options'
|
||||
path\t'Path scheme: will find the furthest match in the item and set pathname as the main tiebreak'
|
||||
history\t'History scheme: will force index as the first tiebreak'"
|
||||
complete -c sk -s b -l bind -d 'Comma-separated key, event, and action bindings' -r
|
||||
complete -c sk -s c -l cmd -d 'Command to invoke dynamically in interactive mode' -r
|
||||
complete -c sk -s I -d 'Replace replstr with the selected item in commands' -r
|
||||
complete -c sk -l color -d 'Set color theme' -r
|
||||
complete -c sk -l skip-to-pattern -d 'Show the matched pattern at the line start' -r
|
||||
complete -c sk -l disable-pattern -d 'Disable items based on this regex pattern' -r
|
||||
complete -c sk -l layout -d 'Set layout' -r -f -a "default\t'Display from the bottom of the screen'
|
||||
reverse\t'Display from the top of the screen'
|
||||
reverse-list\t'Display from the top of the screen, prompt at the bottom'"
|
||||
complete -c sk -l height -d 'Height of skim\'s window' -r
|
||||
complete -c sk -l min-height -d 'Minimum height of skim\'s window as a non-negative row count' -r
|
||||
complete -c sk -l margin -d 'Screen margin' -r
|
||||
complete -c sk -s p -l prompt -d 'Set prompt' -r
|
||||
complete -c sk -l cmd-prompt -d 'Set prompt in command mode' -r
|
||||
complete -c sk -l selector -d 'Set selected item icon' -r
|
||||
complete -c sk -l multi-selector -d 'Set multi-selected item icon' -r
|
||||
complete -c sk -l tabstop -d 'Number of spaces that make up a tab' -r
|
||||
complete -c sk -l ellipsis -d 'The characters used to display truncated lines' -r
|
||||
complete -c sk -l info -d 'Set matching result count display position' -r
|
||||
complete -c sk -l header -d 'Set header, displayed next to the info' -r
|
||||
complete -c sk -l header-lines -d 'Number of lines of the input treated as header' -r
|
||||
complete -c sk -l border -d 'Draw borders around the UI components' -r -f -a "force-off\t'ForceOff disables borders around popups too set with no_border'
|
||||
none\t''
|
||||
plain\t''
|
||||
rounded\t''
|
||||
double\t''
|
||||
thick\t''
|
||||
light-double-dashed\t''
|
||||
heavy-double-dashed\t''
|
||||
light-triple-dashed\t''
|
||||
heavy-triple-dashed\t''
|
||||
light-quadruple-dashed\t''
|
||||
heavy-quadruple-dashed\t''
|
||||
quadrant-inside\t''
|
||||
quadrant-outside\t''"
|
||||
complete -c sk -l multiline -d 'Split item text into multiple display lines at the given separator character defaults to \\n if read0 is set, and \\\\n if not (matching literal \\n in text)' -r
|
||||
complete -c sk -l scrollbar -d 'Set scrollbar style for the item list' -r
|
||||
complete -c sk -l history -d 'History file' -r
|
||||
complete -c sk -l history-size -d 'Maximum number of query history entries to keep' -r
|
||||
complete -c sk -l cmd-history -d 'Command history file' -r
|
||||
complete -c sk -l cmd-history-size -d 'Maximum number of query history entries to keep' -r
|
||||
complete -c sk -l preview -d 'Preview command' -r
|
||||
complete -c sk -l preview-window -d 'Preview window layout' -r
|
||||
complete -c sk -l image -d 'Enable image preview' -r -f -a "detect\t'Default: automatically detect the available backend at startup'
|
||||
halfblocks\t'Force halfblocks if you want blurry previews but a faster startup or if the detection fails'"
|
||||
complete -c sk -s q -l query -d 'Initial query' -r
|
||||
complete -c sk -l cmd-query -d 'Initial query in interactive mode' -r
|
||||
complete -c sk -l output-format -d 'Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands' -r
|
||||
complete -c sk -l pre-select-n -d 'Pre-select the first n items in multi-selection mode' -r
|
||||
complete -c sk -l pre-select-pat -d 'Pre-select the matched items in multi-selection mode' -r
|
||||
complete -c sk -l pre-select-items -d 'Pre-select the items separated by newline character' -r
|
||||
complete -c sk -l pre-select-file -d 'Pre-select the items read from this file' -r
|
||||
complete -c sk -s f -l filter -d 'Query for filter mode' -r
|
||||
complete -c sk -l shell -d 'Generate shell completion script' -r -f -a "bash\t'Bourne Again SHell'
|
||||
elvish\t'Elvish shell'
|
||||
fish\t'Friendly Interactive SHell'
|
||||
nushell\t'Nushell (nu)'
|
||||
power-shell\t'PowerShell'
|
||||
zsh\t'Zsh'"
|
||||
complete -c sk -l listen -d 'Run an IPC socket with optional name (defaults to sk)' -r
|
||||
complete -c sk -l remote -d 'Send commands to an IPC socket with optional name (defaults to sk)' -r
|
||||
complete -c sk -l popup -d 'Run in a tmux or zellij popup' -r
|
||||
complete -c sk -l log-level -d 'Set the log level' -r
|
||||
complete -c sk -l log-file -d 'Pipe log output to a file' -r
|
||||
complete -c sk -l flags -d 'Feature flags' -r -f -a "no-preview-pty\t'Disable preview PTY on Linux'
|
||||
show-score\t'Display the item\'s match score before its value in the item list (for matcher debugging)'
|
||||
show-index\t'Display the item\'s index before its value in the item list'
|
||||
single-reader\t'Limit the reader thread pool to a single thread'
|
||||
single-matcher\t'Limit the matcher thread pool to a single thread'"
|
||||
complete -c sk -l hscroll-off -r
|
||||
complete -c sk -l jump-labels -r
|
||||
complete -c sk -l tail -r
|
||||
complete -c sk -l style -r
|
||||
complete -c sk -l padding -r
|
||||
complete -c sk -l border-label -r
|
||||
complete -c sk -l border-label-pos -r
|
||||
complete -c sk -l wrap-sign -r
|
||||
complete -c sk -l gap -r
|
||||
complete -c sk -l gap-line -r
|
||||
complete -c sk -l freeze-left -r
|
||||
complete -c sk -l freeze-right -r
|
||||
complete -c sk -l scroll-off -r
|
||||
complete -c sk -l gutter -r
|
||||
complete -c sk -l gutter-raw -r
|
||||
complete -c sk -l marker-multi-line -r
|
||||
complete -c sk -l list-border -r
|
||||
complete -c sk -l list-label -r
|
||||
complete -c sk -l list-label-pos -r
|
||||
complete -c sk -l info-command -r
|
||||
complete -c sk -l separator -r
|
||||
complete -c sk -l ghost -r
|
||||
complete -c sk -l input-border -r
|
||||
complete -c sk -l input-label -r
|
||||
complete -c sk -l input-label-pos -r
|
||||
complete -c sk -l preview-label -r
|
||||
complete -c sk -l preview-label-pos -r
|
||||
complete -c sk -l header-border -r
|
||||
complete -c sk -l header-lines-border -r
|
||||
complete -c sk -l footer -r
|
||||
complete -c sk -l footer-border -r
|
||||
complete -c sk -l footer-label -r
|
||||
complete -c sk -l footer-label-pos -r
|
||||
complete -c sk -l with-shell -r
|
||||
complete -c sk -l expect -d 'Deprecated, kept for compatibility purposes. See accept() bind instead' -r
|
||||
complete -c sk -l tac -d 'Show results in reverse order'
|
||||
complete -c sk -l no-sort -d 'Do not sort the results'
|
||||
complete -c sk -s e -l exact -d 'Run in exact mode'
|
||||
complete -c sk -l regex -d 'Start in regex mode instead of fuzzy-match'
|
||||
complete -c sk -l no-typos -d 'Disable typo-tolerant matching'
|
||||
complete -c sk -l normalize -d 'Normalize unicode characters'
|
||||
complete -c sk -l last-match -d 'Highlight the last match found, not the first one This makes tiebreak more pertinent on path items where we want to prioritize a match on the last parts'
|
||||
complete -c sk -s m -l multi -d 'Enable multiple selection'
|
||||
complete -c sk -l no-multi -d 'Disable multiple selection'
|
||||
complete -c sk -l no-mouse -d 'Disable mouse'
|
||||
complete -c sk -s i -l interactive -d 'Start skim in interactive mode'
|
||||
complete -c sk -l highlight-line -d 'Highlight the entire current line, not just the text'
|
||||
complete -c sk -l no-hscroll -d 'Disable horizontal scroll'
|
||||
complete -c sk -l keep-right -d 'Keep the right end of the line visible on overflow'
|
||||
complete -c sk -l no-clear-if-empty -d 'Do not clear previous line if the command returns an empty result'
|
||||
complete -c sk -l no-clear-start -d 'Do not clear items on start'
|
||||
complete -c sk -l no-clear -d 'Do not clear screen on exit'
|
||||
complete -c sk -l show-cmd-error -d 'Show error message if command fails'
|
||||
complete -c sk -l cycle -d 'Cycle the results by wrapping around when scrolling'
|
||||
complete -c sk -l disabled -d 'Disable matching entirely'
|
||||
complete -c sk -l reverse -d 'Shorthand for reverse layout'
|
||||
complete -c sk -l no-height -d 'Disable height (force full screen)'
|
||||
complete -c sk -l ansi -d 'Parse ANSI color codes in input strings'
|
||||
complete -c sk -l no-info -d 'Alias for --info=hidden'
|
||||
complete -c sk -l inline-info -d 'Alias for --info=inline'
|
||||
complete -c sk -l border-no-collapse -d 'Do not collapse adjacent borders into a shared row or column'
|
||||
complete -c sk -l no-border -d 'Disables all borders, including in tmux/zellij popups'
|
||||
complete -c sk -l wrap -d 'Wrap items in the item list'
|
||||
complete -c sk -l no-scrollbar -d 'Disable the scrollbar in the item list'
|
||||
complete -c sk -l read0 -d 'Read input delimited by ASCII NUL(\\0) characters'
|
||||
complete -c sk -l print0 -d 'Print output delimited by ASCII NUL(\\0) characters'
|
||||
complete -c sk -l print-query -d 'Print the query as the first line'
|
||||
complete -c sk -l print-cmd -d 'Print the command as the first line (after print-query)'
|
||||
complete -c sk -l print-score -d 'Print the score after each item'
|
||||
complete -c sk -l print-header -d 'Print the header as the first line (after print-score)'
|
||||
complete -c sk -l print-current -d 'Print the current (highlighted) item as the first line (after print-header)'
|
||||
complete -c sk -l no-strip-ansi -d 'Print the ANSI codes, making the output exactly match the input even when --ansi is on'
|
||||
complete -c sk -s 1 -l select-1 -d 'Do not enter the TUI if the query passed in -q matches only one item and return it'
|
||||
complete -c sk -s 0 -l exit-0 -d 'Do not enter the TUI if the query passed in -q does not match any item'
|
||||
complete -c sk -l sync -d 'Synchronous search for multi-staged filtering'
|
||||
complete -c sk -l shell-bindings -d 'Generate shell key bindings - only for bash, zsh and fish'
|
||||
complete -c sk -l man -d 'Generate man page and output it to stdout'
|
||||
complete -c sk -s x -l extended
|
||||
complete -c sk -l literal
|
||||
complete -c sk -l filepath-word
|
||||
complete -c sk -l no-bold
|
||||
complete -c sk -l phony
|
||||
complete -c sk -l no-color
|
||||
complete -c sk -l no-multi-line
|
||||
complete -c sk -l raw
|
||||
complete -c sk -l track
|
||||
complete -c sk -l no-input
|
||||
complete -c sk -l no-separator
|
||||
complete -c sk -l header-first
|
||||
complete -c sk -s h -l help -d 'Print help (see more with \'--help\')'
|
||||
complete -c sk -s V -l version -d 'Print version'
|
||||
191
shell/completion.nu
Normal file
191
shell/completion.nu
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
module completions {
|
||||
|
||||
def "nu-complete sk tiebreak" [] {
|
||||
[ "score" "-score" "begin" "-begin" "end" "-end" "length" "-length" "index" "-index" "pathname" "-pathname" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk algorithm" [] {
|
||||
[ "arinae" "clangd" "fzy" "frizbee" "skim_v2" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk case" [] {
|
||||
[ "respect" "ignore" "smart" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk scheme" [] {
|
||||
[ "default" "path" "history" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk layout" [] {
|
||||
[ "default" "reverse" "reverse-list" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk border" [] {
|
||||
[ "force-off" "none" "plain" "rounded" "double" "thick" "light-double-dashed" "heavy-double-dashed" "light-triple-dashed" "heavy-triple-dashed" "light-quadruple-dashed" "heavy-quadruple-dashed" "quadrant-inside" "quadrant-outside" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk image" [] {
|
||||
[ "detect" "halfblocks" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk shell" [] {
|
||||
[ "bash" "elvish" "fish" "nushell" "power-shell" "zsh" ]
|
||||
}
|
||||
|
||||
def "nu-complete sk flags" [] {
|
||||
[ "no-preview-pty" "show-score" "show-index" "single-reader" "single-matcher" ]
|
||||
}
|
||||
|
||||
# Fuzzy Finder in rust!
|
||||
export extern sk [
|
||||
--tac # Show results in reverse order
|
||||
--min-query-length: string # Minimum query length to start showing results
|
||||
--no-sort # Do not sort the results
|
||||
--tiebreak(-t): string@"nu-complete sk tiebreak" # Comma-separated list of sort criteria to apply when the scores are tied.
|
||||
--nth(-n): string # Fields to be matched
|
||||
--with-nth: string # Fields to be transformed
|
||||
--hide-nth: string # Fields to hide from display while keeping them searchable
|
||||
--delimiter(-d): string # Delimiter between fields
|
||||
--exact(-e) # Run in exact mode
|
||||
--regex # Start in regex mode instead of fuzzy-match
|
||||
--algo: string@"nu-complete sk algorithm" # Fuzzy matching algorithm
|
||||
--case: string@"nu-complete sk case" # Case sensitivity
|
||||
--typos: string # Enable typo-tolerant matching
|
||||
--no-typos # Disable typo-tolerant matching
|
||||
--normalize # Normalize unicode characters
|
||||
--split-match: string # Enable split matching and set delimiter
|
||||
--last-match # Highlight the last match found, not the first one This makes tiebreak more pertinent on path items where we want to prioritize a match on the last parts
|
||||
--scheme: string@"nu-complete sk scheme"
|
||||
--bind(-b): string # Comma-separated key, event, and action bindings
|
||||
--multi(-m) # Enable multiple selection
|
||||
--no-multi # Disable multiple selection
|
||||
--no-mouse # Disable mouse
|
||||
--cmd(-c): string # Command to invoke dynamically in interactive mode
|
||||
--interactive(-i) # Start skim in interactive mode
|
||||
-I: string # Replace replstr with the selected item in commands
|
||||
--color: string # Set color theme
|
||||
--highlight-line # Highlight the entire current line, not just the text
|
||||
--no-hscroll # Disable horizontal scroll
|
||||
--keep-right # Keep the right end of the line visible on overflow
|
||||
--skip-to-pattern: string # Show the matched pattern at the line start
|
||||
--no-clear-if-empty # Do not clear previous line if the command returns an empty result
|
||||
--no-clear-start # Do not clear items on start
|
||||
--no-clear # Do not clear screen on exit
|
||||
--show-cmd-error # Show error message if command fails
|
||||
--cycle # Cycle the results by wrapping around when scrolling
|
||||
--disabled # Disable matching entirely
|
||||
--disable-pattern: string # Disable items based on this regex pattern
|
||||
--layout: string@"nu-complete sk layout" # Set layout
|
||||
--reverse # Shorthand for reverse layout
|
||||
--height: string # Height of skim's window
|
||||
--no-height # Disable height (force full screen)
|
||||
--min-height: string # Minimum height of skim's window as a non-negative row count
|
||||
--margin: string # Screen margin
|
||||
--prompt(-p): string # Set prompt
|
||||
--cmd-prompt: string # Set prompt in command mode
|
||||
--selector: string # Set selected item icon
|
||||
--multi-selector: string # Set multi-selected item icon
|
||||
--ansi # Parse ANSI color codes in input strings
|
||||
--tabstop: string # Number of spaces that make up a tab
|
||||
--ellipsis: string # The characters used to display truncated lines
|
||||
--info: string # Set matching result count display position
|
||||
--no-info # Alias for --info=hidden
|
||||
--inline-info # Alias for --info=inline
|
||||
--header: string # Set header, displayed next to the info
|
||||
--header-lines: string # Number of lines of the input treated as header
|
||||
--border: string@"nu-complete sk border" # Draw borders around the UI components
|
||||
--border-no-collapse # Do not collapse adjacent borders into a shared row or column
|
||||
--no-border # Disables all borders, including in tmux/zellij popups
|
||||
--wrap # Wrap items in the item list
|
||||
--multiline: string # Split item text into multiple display lines at the given separator character defaults to \n if read0 is set, and \\n if not (matching literal \n in text)
|
||||
--scrollbar: string # Set scrollbar style for the item list
|
||||
--no-scrollbar # Disable the scrollbar in the item list
|
||||
--history: string # History file
|
||||
--history-size: string # Maximum number of query history entries to keep
|
||||
--cmd-history: string # Command history file
|
||||
--cmd-history-size: string # Maximum number of query history entries to keep
|
||||
--preview: string # Preview command
|
||||
--preview-window: string # Preview window layout
|
||||
--image: string@"nu-complete sk image" # Enable image preview
|
||||
--query(-q): string # Initial query
|
||||
--cmd-query: string # Initial query in interactive mode
|
||||
--read0 # Read input delimited by ASCII NUL(\0) characters
|
||||
--print0 # Print output delimited by ASCII NUL(\0) characters
|
||||
--print-query # Print the query as the first line
|
||||
--print-cmd # Print the command as the first line (after print-query)
|
||||
--print-score # Print the score after each item
|
||||
--print-header # Print the header as the first line (after print-score)
|
||||
--print-current # Print the current (highlighted) item as the first line (after print-header)
|
||||
--output-format: string # Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands
|
||||
--no-strip-ansi # Print the ANSI codes, making the output exactly match the input even when --ansi is on
|
||||
--select-1(-1) # Do not enter the TUI if the query passed in -q matches only one item and return it
|
||||
--exit-0(-0) # Do not enter the TUI if the query passed in -q does not match any item
|
||||
--sync # Synchronous search for multi-staged filtering
|
||||
--pre-select-n: string # Pre-select the first n items in multi-selection mode
|
||||
--pre-select-pat: string # Pre-select the matched items in multi-selection mode
|
||||
--pre-select-items: string # Pre-select the items separated by newline character
|
||||
--pre-select-file: string # Pre-select the items read from this file
|
||||
--filter(-f): string # Query for filter mode
|
||||
--shell: string@"nu-complete sk shell" # Generate shell completion script
|
||||
--shell-bindings # Generate shell key bindings - only for bash, zsh and fish
|
||||
--man # Generate man page and output it to stdout
|
||||
--listen: string # Run an IPC socket with optional name (defaults to sk)
|
||||
--remote: string # Send commands to an IPC socket with optional name (defaults to sk)
|
||||
--popup: string # Run in a tmux or zellij popup
|
||||
--log-level: string # Set the log level
|
||||
--log-file: string # Pipe log output to a file
|
||||
--flags: string@"nu-complete sk flags" # Feature flags
|
||||
--extended(-x)
|
||||
--literal
|
||||
--hscroll-off: string
|
||||
--filepath-word
|
||||
--jump-labels: string
|
||||
--no-bold
|
||||
--phony
|
||||
--tail: string
|
||||
--style: string
|
||||
--no-color
|
||||
--padding: string
|
||||
--border-label: string
|
||||
--border-label-pos: string
|
||||
--wrap-sign: string
|
||||
--no-multi-line
|
||||
--raw
|
||||
--track
|
||||
--gap: string
|
||||
--gap-line: string
|
||||
--freeze-left: string
|
||||
--freeze-right: string
|
||||
--scroll-off: string
|
||||
--gutter: string
|
||||
--gutter-raw: string
|
||||
--marker-multi-line: string
|
||||
--list-border: string
|
||||
--list-label: string
|
||||
--list-label-pos: string
|
||||
--no-input
|
||||
--info-command: string
|
||||
--separator: string
|
||||
--no-separator
|
||||
--ghost: string
|
||||
--input-border: string
|
||||
--input-label: string
|
||||
--input-label-pos: string
|
||||
--preview-label: string
|
||||
--preview-label-pos: string
|
||||
--header-first
|
||||
--header-border: string
|
||||
--header-lines-border: string
|
||||
--footer: string
|
||||
--footer-border: string
|
||||
--footer-label: string
|
||||
--footer-label-pos: string
|
||||
--with-shell: string
|
||||
--expect: string # Deprecated, kept for compatibility purposes. See accept() bind instead
|
||||
--help(-h) # Print help (see more with '--help')
|
||||
--version(-V) # Print version
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
export use completions *
|
||||
|
|
@ -1,329 +1,226 @@
|
|||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ completion.zsh
|
||||
#
|
||||
# - $SKIM_TMUX (default: 0)
|
||||
# - $SKIM_TMUX_OPTS (default: '-d 40%')
|
||||
# - $SKIM_COMPLETION_TRIGGER (default: '**')
|
||||
# - $SKIM_COMPLETION_OPTS (default: empty)
|
||||
#compdef sk
|
||||
|
||||
# Both branches of the following `if` do the same thing -- define
|
||||
# __skim_completion_options such that `eval $__skim_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. __skim_completion_options ends up with a value
|
||||
# like this: "options=(shwordsplit off aliases on ...)".
|
||||
__skim_completion_options="options=(${(j: :)${(kv)options[@]}})"
|
||||
autoload -U is-at-least
|
||||
|
||||
_sk() {
|
||||
typeset -A opt_args
|
||||
typeset -a _arguments_options
|
||||
local ret=1
|
||||
|
||||
if is-at-least 5.2; then
|
||||
_arguments_options=(-s -S -C)
|
||||
else
|
||||
_arguments_options=(-s -C)
|
||||
fi
|
||||
|
||||
local context curcontext="$curcontext" state line
|
||||
_arguments "${_arguments_options[@]}" : \
|
||||
'--min-query-length=[Minimum query length to start showing results]:MIN_QUERY_LENGTH:_default' \
|
||||
'*-t+[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length index -index pathname -pathname)' \
|
||||
'*--tiebreak=[Comma-separated list of sort criteria to apply when the scores are tied.]:TIEBREAK:(score -score begin -begin end -end length -length index -index pathname -pathname)' \
|
||||
'*-n+[Fields to be matched]:NTH:_default' \
|
||||
'*--nth=[Fields to be matched]:NTH:_default' \
|
||||
'*--with-nth=[Fields to be transformed]:WITH_NTH:_default' \
|
||||
'*--hide-nth=[Fields to hide from display while keeping them searchable]:HIDE_NTH:_default' \
|
||||
'-d+[Delimiter between fields]:DELIMITER:_default' \
|
||||
'--delimiter=[Delimiter between fields]:DELIMITER:_default' \
|
||||
'--algo=[Fuzzy matching algorithm]:ALGORITHM:((arinae\:"Arinae\: typo-resistant & natural algorithm, default"
|
||||
clangd\:"Clangd fuzzy matching algorithm"
|
||||
fzy\:"Fzy matching algorithm (https\://github.com/jhawthorn/fzy)"
|
||||
frizbee\:"Frizbee matching algorithm, typo resistant"
|
||||
skim_v2\:"Previous skim fuzzy matching algorithm (v2)"))' \
|
||||
'--case=[Case sensitivity]:CASE:((respect\:"Case-sensitive matching"
|
||||
ignore\:"Case-insensitive matching"
|
||||
smart\:"Smart case\: case-insensitive unless query contains uppercase"))' \
|
||||
'--typos=[Enable typo-tolerant matching]::TYPOS:_default' \
|
||||
'--split-match=[Enable split matching and set delimiter]::SPLIT_MATCH:_default' \
|
||||
'--scheme=[]:SCHEME:((default\:"Default scheme, no modifications to the options"
|
||||
path\:"Path scheme\: will find the furthest match in the item and set pathname as the main tiebreak"
|
||||
history\:"History scheme\: will force index as the first tiebreak"))' \
|
||||
'*-b+[Comma-separated key, event, and action bindings]::BIND:_default' \
|
||||
'*--bind=[Comma-separated key, event, and action bindings]::BIND:_default' \
|
||||
'-c+[Command to invoke dynamically in interactive mode]:CMD:_default' \
|
||||
'--cmd=[Command to invoke dynamically in interactive mode]:CMD:_default' \
|
||||
'-I+[Replace replstr with the selected item in commands]:REPLSTR:_default' \
|
||||
'--color=[Set color theme]:COLOR:_default' \
|
||||
'--skip-to-pattern=[Show the matched pattern at the line start]:SKIP_TO_PATTERN:_default' \
|
||||
'--disable-pattern=[Disable items based on this regex pattern]:DISABLE_PATTERN:_default' \
|
||||
'--layout=[Set layout]:LAYOUT:((default\:"Display from the bottom of the screen"
|
||||
reverse\:"Display from the top of the screen"
|
||||
reverse-list\:"Display from the top of the screen, prompt at the bottom"))' \
|
||||
'--height=[Height of skim'\''s window]:HEIGHT:_default' \
|
||||
'--min-height=[Minimum height of skim'\''s window as a non-negative row count]:MIN_HEIGHT:_default' \
|
||||
'--margin=[Screen margin]:MARGIN:_default' \
|
||||
'-p+[Set prompt]:PROMPT:_default' \
|
||||
'--prompt=[Set prompt]:PROMPT:_default' \
|
||||
'--cmd-prompt=[Set prompt in command mode]:CMD_PROMPT:_default' \
|
||||
'--selector=[Set selected item icon]:SELECTOR_ICON:_default' \
|
||||
'--multi-selector=[Set multi-selected item icon]:MULTI_SELECT_ICON:_default' \
|
||||
'--tabstop=[Number of spaces that make up a tab]:TABSTOP:_default' \
|
||||
'--ellipsis=[The characters used to display truncated lines]:ELLIPSIS:_default' \
|
||||
'--info=[Set matching result count display position]:INFO:_default' \
|
||||
'--header=[Set header, displayed next to the info]:HEADER:_default' \
|
||||
'--header-lines=[Number of lines of the input treated as header]:HEADER_LINES:_default' \
|
||||
'--border=[Draw borders around the UI components]::BORDER:((force-off\:"ForceOff disables borders around popups too set with no_border"
|
||||
none\:""
|
||||
plain\:""
|
||||
rounded\:""
|
||||
double\:""
|
||||
thick\:""
|
||||
light-double-dashed\:""
|
||||
heavy-double-dashed\:""
|
||||
light-triple-dashed\:""
|
||||
heavy-triple-dashed\:""
|
||||
light-quadruple-dashed\:""
|
||||
heavy-quadruple-dashed\:""
|
||||
quadrant-inside\:""
|
||||
quadrant-outside\:""))' \
|
||||
'--multiline=[Split item text into multiple display lines at the given separator character defaults to \\n if read0 is set, and \\\\n if not (matching literal \\n in text)]::MULTILINE:_default' \
|
||||
'--scrollbar=[Set scrollbar style for the item list]:THUMB:_default' \
|
||||
'--history=[History file]:HISTORY_FILE:_default' \
|
||||
'--history-size=[Maximum number of query history entries to keep]:HISTORY_SIZE:_default' \
|
||||
'--cmd-history=[Command history file]:CMD_HISTORY_FILE:_default' \
|
||||
'--cmd-history-size=[Maximum number of query history entries to keep]:CMD_HISTORY_SIZE:_default' \
|
||||
'--preview=[Preview command]:PREVIEW:_default' \
|
||||
'--preview-window=[Preview window layout]:PREVIEW_WINDOW:_default' \
|
||||
'--image=[Enable image preview]::IMAGE:((detect\:"Default\: automatically detect the available backend at startup"
|
||||
halfblocks\:"Force halfblocks if you want blurry previews but a faster startup or if the detection fails"))' \
|
||||
'-q+[Initial query]:QUERY:_default' \
|
||||
'--query=[Initial query]:QUERY:_default' \
|
||||
'--cmd-query=[Initial query in interactive mode]:CMD_QUERY:_default' \
|
||||
'--output-format=[Set the output format If set, overrides all print_ options Will be expanded the same way as preview or commands]:OUTPUT_FORMAT:_default' \
|
||||
'--pre-select-n=[Pre-select the first n items in multi-selection mode]:PRE_SELECT_N:_default' \
|
||||
'--pre-select-pat=[Pre-select the matched items in multi-selection mode]:PRE_SELECT_PAT:_default' \
|
||||
'--pre-select-items=[Pre-select the items separated by newline character]:PRE_SELECT_ITEMS:_default' \
|
||||
'--pre-select-file=[Pre-select the items read from this file]:PRE_SELECT_FILE:_default' \
|
||||
'-f+[Query for filter mode]:FILTER:_default' \
|
||||
'--filter=[Query for filter mode]:FILTER:_default' \
|
||||
'--shell=[Generate shell completion script]:SHELL:((bash\:"Bourne Again SHell"
|
||||
elvish\:"Elvish shell"
|
||||
fish\:"Friendly Interactive SHell"
|
||||
nushell\:"Nushell (nu)"
|
||||
power-shell\:"PowerShell"
|
||||
zsh\:"Zsh"))' \
|
||||
'--listen=[Run an IPC socket with optional name (defaults to sk)]::LISTEN:_default' \
|
||||
'--remote=[Send commands to an IPC socket with optional name (defaults to sk)]::REMOTE:_default' \
|
||||
'--popup=[Run in a tmux or zellij popup]::POPUP:_default' \
|
||||
'--log-level=[Set the log level]:LOG_LEVEL:_default' \
|
||||
'--log-file=[Pipe log output to a file]:LOG_FILE:_default' \
|
||||
'*--flags=[Feature flags]:FLAGS:((no-preview-pty\:"Disable preview PTY on Linux"
|
||||
show-score\:"Display the item'\''s match score before its value in the item list (for matcher debugging)"
|
||||
show-index\:"Display the item'\''s index before its value in the item list"
|
||||
single-reader\:"Limit the reader thread pool to a single thread"
|
||||
single-matcher\:"Limit the matcher thread pool to a single thread"))' \
|
||||
'--hscroll-off=[]:HSCROLL_OFF:_default' \
|
||||
'--jump-labels=[]:JUMP_LABELS:_default' \
|
||||
'--tail=[]:TAIL:_default' \
|
||||
'--style=[]:STYLE:_default' \
|
||||
'--padding=[]:PADDING:_default' \
|
||||
'--border-label=[]:BORDER_LABEL:_default' \
|
||||
'--border-label-pos=[]:BORDER_LABEL_POS:_default' \
|
||||
'--wrap-sign=[]:WRAP_SIGN:_default' \
|
||||
'--gap=[]:GAP:_default' \
|
||||
'--gap-line=[]:GAP_LINE:_default' \
|
||||
'--freeze-left=[]:FREEZE_LEFT:_default' \
|
||||
'--freeze-right=[]:FREEZE_RIGHT:_default' \
|
||||
'--scroll-off=[]:SCROLL_OFF:_default' \
|
||||
'--gutter=[]:GUTTER:_default' \
|
||||
'--gutter-raw=[]:GUTTER_RAW:_default' \
|
||||
'--marker-multi-line=[]:MARKER_MULTI_LINE:_default' \
|
||||
'--list-border=[]:LIST_BORDER:_default' \
|
||||
'--list-label=[]:LIST_LABEL:_default' \
|
||||
'--list-label-pos=[]:LIST_LABEL_POS:_default' \
|
||||
'--info-command=[]:INFO_COMMAND:_default' \
|
||||
'--separator=[]:SEPARATOR:_default' \
|
||||
'--ghost=[]:GHOST:_default' \
|
||||
'--input-border=[]:INPUT_BORDER:_default' \
|
||||
'--input-label=[]:INPUT_LABEL:_default' \
|
||||
'--input-label-pos=[]:INPUT_LABEL_POS:_default' \
|
||||
'--preview-label=[]:PREVIEW_LABEL:_default' \
|
||||
'--preview-label-pos=[]:PREVIEW_LABEL_POS:_default' \
|
||||
'--header-border=[]:HEADER_BORDER:_default' \
|
||||
'--header-lines-border=[]:HEADER_LINES_BORDER:_default' \
|
||||
'--footer=[]:FOOTER:_default' \
|
||||
'--footer-border=[]:FOOTER_BORDER:_default' \
|
||||
'--footer-label=[]:FOOTER_LABEL:_default' \
|
||||
'--footer-label-pos=[]:FOOTER_LABEL_POS:_default' \
|
||||
'--with-shell=[]:WITH_SHELL:_default' \
|
||||
'--expect=[Deprecated, kept for compatibility purposes. See accept() bind instead]:EXPECT:_default' \
|
||||
'--tac[Show results in reverse order]' \
|
||||
'--no-sort[Do not sort the results]' \
|
||||
'-e[Run in exact mode]' \
|
||||
'--exact[Run in exact mode]' \
|
||||
'--regex[Start in regex mode instead of fuzzy-match]' \
|
||||
'--no-typos[Disable typo-tolerant matching]' \
|
||||
'--normalize[Normalize unicode characters]' \
|
||||
'--last-match[Highlight the last match found, not the first one This makes tiebreak more pertinent on path items where we want to prioritize a match on the last parts]' \
|
||||
'-m[Enable multiple selection]' \
|
||||
'--multi[Enable multiple selection]' \
|
||||
'--no-multi[Disable multiple selection]' \
|
||||
'--no-mouse[Disable mouse]' \
|
||||
'-i[Start skim in interactive mode]' \
|
||||
'--interactive[Start skim in interactive mode]' \
|
||||
'--highlight-line[Highlight the entire current line, not just the text]' \
|
||||
'--no-hscroll[Disable horizontal scroll]' \
|
||||
'--keep-right[Keep the right end of the line visible on overflow]' \
|
||||
'--no-clear-if-empty[Do not clear previous line if the command returns an empty result]' \
|
||||
'--no-clear-start[Do not clear items on start]' \
|
||||
'--no-clear[Do not clear screen on exit]' \
|
||||
'--show-cmd-error[Show error message if command fails]' \
|
||||
'--cycle[Cycle the results by wrapping around when scrolling]' \
|
||||
'--disabled[Disable matching entirely]' \
|
||||
'--reverse[Shorthand for reverse layout]' \
|
||||
'--no-height[Disable height (force full screen)]' \
|
||||
'--ansi[Parse ANSI color codes in input strings]' \
|
||||
'--no-info[Alias for --info=hidden]' \
|
||||
'--inline-info[Alias for --info=inline]' \
|
||||
'--border-no-collapse[Do not collapse adjacent borders into a shared row or column]' \
|
||||
'--no-border[Disables all borders, including in tmux/zellij popups]' \
|
||||
'--wrap[Wrap items in the item list]' \
|
||||
'--no-scrollbar[Disable the scrollbar in the item list]' \
|
||||
'--read0[Read input delimited by ASCII NUL(\\0) characters]' \
|
||||
'--print0[Print output delimited by ASCII NUL(\\0) characters]' \
|
||||
'--print-query[Print the query as the first line]' \
|
||||
'--print-cmd[Print the command as the first line (after print-query)]' \
|
||||
'--print-score[Print the score after each item]' \
|
||||
'--print-header[Print the header as the first line (after print-score)]' \
|
||||
'--print-current[Print the current (highlighted) item as the first line (after print-header)]' \
|
||||
'--no-strip-ansi[Print the ANSI codes, making the output exactly match the input even when --ansi is on]' \
|
||||
'-1[Do not enter the TUI if the query passed in -q matches only one item and return it]' \
|
||||
'--select-1[Do not enter the TUI if the query passed in -q matches only one item and return it]' \
|
||||
'-0[Do not enter the TUI if the query passed in -q does not match any item]' \
|
||||
'--exit-0[Do not enter the TUI if the query passed in -q does not match any item]' \
|
||||
'--sync[Synchronous search for multi-staged filtering]' \
|
||||
'--shell-bindings[Generate shell key bindings - only for bash, zsh and fish]' \
|
||||
'--man[Generate man page and output it to stdout]' \
|
||||
'-x[]' \
|
||||
'--extended[]' \
|
||||
'--literal[]' \
|
||||
'--filepath-word[]' \
|
||||
'--no-bold[]' \
|
||||
'--phony[]' \
|
||||
'--no-color[]' \
|
||||
'--no-multi-line[]' \
|
||||
'--raw[]' \
|
||||
'--track[]' \
|
||||
'--no-input[]' \
|
||||
'--no-separator[]' \
|
||||
'--header-first[]' \
|
||||
'-h[Print help (see more with '\''--help'\'')]' \
|
||||
'--help[Print help (see more with '\''--help'\'')]' \
|
||||
'-V[Print version]' \
|
||||
'--version[Print version]' \
|
||||
&& ret=0
|
||||
}
|
||||
|
||||
(( $+functions[_sk_commands] )) ||
|
||||
_sk_commands() {
|
||||
local commands; commands=()
|
||||
_describe -t commands 'sk commands' commands "$@"
|
||||
}
|
||||
|
||||
if [ "$funcstack[1]" = "_sk" ]; then
|
||||
_sk "$@"
|
||||
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' '__skim_opt'
|
||||
__skim_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. __skim_opt
|
||||
# ends up iterating over option names: shwordsplit, aliases, etc.
|
||||
for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
||||
if [[ -o "$__skim_opt" ]]; then
|
||||
# Option $__skim_opt is currently on, so remember to set it back on.
|
||||
__skim_completion_options+=" -o $__skim_opt"
|
||||
else
|
||||
# Option $__skim_opt is currently off, so remember to set it back off.
|
||||
__skim_completion_options+=" +o $__skim_opt"
|
||||
fi
|
||||
done
|
||||
# The value of __skim_completion_options here looks like this:
|
||||
# "setopt +o shwordsplit -o aliases ..."
|
||||
}
|
||||
compdef _sk sk
|
||||
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.
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
|
||||
# This brace is the start of try-always block. The `always` part is like
|
||||
# `finally` in lesser languages. We use it to *always* restore user options.
|
||||
{
|
||||
|
||||
# Bail out if not interactive shell.
|
||||
[[ -o interactive ]] || return 0
|
||||
|
||||
# To use custom commands instead of find, override _skim_compgen_{path,dir}
|
||||
if ! declare -f _skim_compgen_path > /dev/null; then
|
||||
_skim_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _skim_compgen_dir > /dev/null; then
|
||||
_skim_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
###########################################################
|
||||
|
||||
__skim_comprun() {
|
||||
if [[ "$(type _skim_comprun 2>&1)" =~ function ]]; then
|
||||
_skim_comprun "$@"
|
||||
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
|
||||
shift
|
||||
if [ -n "$SKIM_TMUX_OPTS" ]; then
|
||||
sk-tmux ${(Q)${(Z+n+)SKIM_TMUX_OPTS}} -- "$@"
|
||||
else
|
||||
sk-tmux -d ${SKIM_TMUX_HEIGHT:-40%} -- "$@"
|
||||
fi
|
||||
else
|
||||
shift
|
||||
sk "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
||||
__skim_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]}"
|
||||
}
|
||||
|
||||
__skim_generic_path_completion() {
|
||||
local base lbuf cmd compgen skim_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
cmd=$(__skim_extract_command "$lbuf")
|
||||
compgen=$3
|
||||
skim_opts=$4
|
||||
suffix=$5
|
||||
tail=$6
|
||||
|
||||
setopt localoptions nonomatch
|
||||
eval "base=$base"
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while [ 1 ]; do
|
||||
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS" __skim_comprun "$cmd" ${(Q)${(Z+n+)skim_opts}} -q "$leftover" | while read item; do
|
||||
echo -n "${(q)item}$suffix "
|
||||
done)
|
||||
matches=${matches% }
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
fi
|
||||
zle reset-prompt
|
||||
break
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
dir=${dir%/}/
|
||||
done
|
||||
}
|
||||
|
||||
_skim_path_completion() {
|
||||
__skim_generic_path_completion "$1" "$2" _skim_compgen_path \
|
||||
"-m" "" " "
|
||||
}
|
||||
|
||||
_skim_dir_completion() {
|
||||
__skim_generic_path_completion "$1" "$2" _skim_compgen_dir \
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_skim_feed_fifo() (
|
||||
command rm -f "$1"
|
||||
mkfifo "$1"
|
||||
cat <&0 > "$1" &
|
||||
)
|
||||
|
||||
_skim_complete() {
|
||||
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}/skim-complete-fifo-$$"
|
||||
lbuf=${rest[0]}
|
||||
cmd=$(__skim_extract_command "$lbuf")
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
_skim_feed_fifo "$fifo"
|
||||
matches=$(SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" < "$fifo" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
zle reset-prompt
|
||||
command rm -f "$fifo"
|
||||
}
|
||||
|
||||
_skim_complete_telnet() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_ssh() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
setopt localoptions nonomatch
|
||||
command cat <(command tail -n +1 ~/.ssh/config ~/.ssh/config.d/* /etc/ssh/ssh_config 2> /dev/null | command grep -i '^\s*host\(name\)\? ' | awk '{for (i = 2; i <= NF; i++) print $1 " " $i}' | command grep -v '[*?]') \
|
||||
<(command grep -oE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -v '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_export() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_unset() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_unalias() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
alias | sed 's/=.*//'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_kill() {
|
||||
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_kill_post() {
|
||||
awk '{print $2}'
|
||||
}
|
||||
|
||||
skim-completion() {
|
||||
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
tokens=(${(z)LBUFFER})
|
||||
if [ ${#tokens} -lt 1 ]; then
|
||||
zle ${skim_default_completion:-expand-or-complete}
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=$(__skim_extract_command "$LBUFFER")
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||
|
||||
# When the trigger starts with ';', it becomes a separate token
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
|
||||
tokens[-2]="${tokens[-2]}${tokens[-1]}"
|
||||
tokens=(${tokens[0,-2]})
|
||||
fi
|
||||
|
||||
lbuf=$LBUFFER
|
||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||
# Kill completion (do not require trigger sequence)
|
||||
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
||||
tail=$trigger
|
||||
tokens+=$trigger
|
||||
lbuf="$lbuf$trigger"
|
||||
fi
|
||||
|
||||
# Trigger sequence given
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _skim_complete_${cmd} > /dev/null"; then
|
||||
prefix="$prefix" eval _skim_complete_${cmd} ${(q)lbuf}
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
_skim_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_skim_path_completion "$prefix" "$lbuf"
|
||||
fi
|
||||
# Fall back to default completion
|
||||
else
|
||||
zle ${skim_default_completion:-expand-or-complete}
|
||||
fi
|
||||
}
|
||||
|
||||
[ -z "$skim_default_completion" ] && {
|
||||
binding=$(bindkey '^I')
|
||||
[[ $binding =~ 'undefined-key' ]] || skim_default_completion=$binding[(s: :w)2]
|
||||
unset binding
|
||||
}
|
||||
|
||||
zle -N skim-completion
|
||||
bindkey '^I' skim-completion
|
||||
|
||||
} always {
|
||||
# Restore the original options.
|
||||
eval $__skim_completion_options
|
||||
'unset' '__skim_completion_options'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +1,400 @@
|
|||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.bash
|
||||
# skim key bindings for bash
|
||||
#
|
||||
# - $SKIM_TMUX_OPTS
|
||||
# - $SKIM_CTRL_T_COMMAND
|
||||
# - $SKIM_CTRL_T_OPTS
|
||||
# - $SKIM_CTRL_R_OPTS
|
||||
# - $SKIM_CTRL_R_IDX_COLOR (default: \033[2m, set NO_COLOR to disable)
|
||||
# - $SKIM_ALT_C_COMMAND
|
||||
# - $SKIM_ALT_C_OPTS
|
||||
# - $SKIM_COMPLETION_TRIGGER (default: '**')
|
||||
# - $SKIM_COMPLETION_OPTS (default: empty)
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/key-bindings.bash
|
||||
__skim_select__() {
|
||||
local cmd="${SKIM_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
local cmd="${SKIM_CTRL_T_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type f -print \
|
||||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | cut -b3-"}"
|
||||
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
done
|
||||
echo
|
||||
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read -r item; do
|
||||
printf '%q ' "$item"
|
||||
done
|
||||
echo
|
||||
}
|
||||
|
||||
if [[ $- =~ i ]]; then
|
||||
|
||||
__skimcmd() {
|
||||
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
|
||||
echo "sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- " || echo "sk"
|
||||
}
|
||||
__skimcmd() {
|
||||
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
|
||||
echo "sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}}" || echo "sk"
|
||||
}
|
||||
|
||||
skim-file-widget() {
|
||||
local selected="$(__skim_select__)"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$(( READLINE_POINT + ${#selected} ))
|
||||
}
|
||||
skim-file-widget() {
|
||||
local selected="$(__skim_select__)"
|
||||
READLINE_LINE="${READLINE_LINE:0:$READLINE_POINT}$selected${READLINE_LINE:$READLINE_POINT}"
|
||||
READLINE_POINT=$((READLINE_POINT + ${#selected}))
|
||||
}
|
||||
|
||||
__skim_cd__() {
|
||||
local cmd dir
|
||||
cmd="${SKIM_ALT_C_COMMAND:-"command find -L . -mindepth 1 \\( -path '*/\\.*' -o -fstype 'sysfs' -o -fstype 'devfs' -o -fstype 'devtmpfs' -o -fstype 'proc' \\) -prune \
|
||||
-o -type d -print 2> /dev/null | cut -b3-"}"
|
||||
dir=$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi) && printf 'cd %q' "$dir"
|
||||
}
|
||||
__skim_cd__() {
|
||||
local cmd dir
|
||||
cmd="${SKIM_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 -printf '%P\\n' 2>/dev/null"}"
|
||||
dir=$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)
|
||||
if [ -n "$dir" ]; then
|
||||
printf 'cd %q' "$dir"
|
||||
fi
|
||||
}
|
||||
|
||||
__skim_history__() {
|
||||
local output
|
||||
output=$(
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) perl -n -l0 -e 'BEGIN { getc; $/ = "\n\t"; $HISTCMD = $ENV{last_hist} + 1 } s/^[ *]//; print $HISTCMD - $. . "\t$_" if !$seen{$_}++' |
|
||||
SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} $SKIM_DEFAULT_OPTIONS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi --read0" $(__skimcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [ -z "$READLINE_POINT" ]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
__skim_history__() {
|
||||
local output
|
||||
local c_idx='' c_reset='' ansi_opt=''
|
||||
if [[ ! -v NO_COLOR ]]; then
|
||||
c_idx="${SKIM_CTRL_R_IDX_COLOR:-\033[2m}"
|
||||
c_reset='\033[0m'
|
||||
ansi_opt='--ansi'
|
||||
fi
|
||||
output=$(
|
||||
builtin fc -lnr -2147483648 |
|
||||
last_hist=$(HISTTIMEFORMAT='' builtin history 1) awk -v last_hist="$last_hist" -v c_idx="$c_idx" -v c_reset="$c_reset" '
|
||||
BEGIN { HISTCMD = last_hist + 1; cmd = ""; idx = 0 }
|
||||
/^\t/ {
|
||||
if (cmd != "" && !seen[cmd]++) printf "%s%d%s\t%s%c", c_idx, HISTCMD - idx, c_reset, cmd, 0
|
||||
idx++; cmd = substr($0, 2); sub(/^[ *]/, "", cmd); next
|
||||
}
|
||||
{ cmd = cmd "\n" $0 }
|
||||
END { if (cmd != "" && !seen[cmd]++) printf "%s%d%s\t%s%c", c_idx, HISTCMD - idx, c_reset, cmd, 0 }
|
||||
' |
|
||||
SKIM_DEFAULT_OPTIONS="$SKIM_DEFAULT_OPTIONS -n2..,.. --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi --read0 --multiline $ansi_opt" $(__skimcmd) --query "$READLINE_LINE"
|
||||
) || return
|
||||
echo -e "\033[0m"
|
||||
READLINE_LINE=${output#*$'\t'}
|
||||
if [ -z "$READLINE_POINT" ]; then
|
||||
echo "$READLINE_LINE"
|
||||
else
|
||||
READLINE_POINT=0x7fffffff
|
||||
fi
|
||||
}
|
||||
|
||||
# Required to refresh the prompt after skim
|
||||
bind -m emacs-standard '"\er": redraw-current-line'
|
||||
# Required to refresh the prompt after skim
|
||||
bind -m emacs-standard '"\er": redraw-current-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'
|
||||
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]}" -lt 4 ]; then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__skim_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard '"\C-t": " \C-b\C-k \C-u`__skim_select__`\e\C-e\er\C-a\C-y\C-h\C-e\e \C-y\ey\C-x\C-x\C-f"'
|
||||
bind -m vi-command '"\C-t": "\C-z\C-t\C-z"'
|
||||
bind -m vi-insert '"\C-t": "\C-z\C-t\C-z"'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__skim_history__)"\e\C-e\er"'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard -x '"\C-t": skim-file-widget'
|
||||
bind -m vi-command -x '"\C-t": skim-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": skim-file-widget'
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard '"\C-r": "\C-e \C-u\C-y\ey\C-u"$(__skim_history__)"\e\C-e\er"'
|
||||
bind -m vi-command '"\C-r": "\C-z\C-r\C-z"'
|
||||
bind -m vi-insert '"\C-r": "\C-z\C-r\C-z"'
|
||||
else
|
||||
# CTRL-T - Paste the selected file path into the command line
|
||||
bind -m emacs-standard -x '"\C-t": skim-file-widget'
|
||||
bind -m vi-command -x '"\C-t": skim-file-widget'
|
||||
bind -m vi-insert -x '"\C-t": skim-file-widget'
|
||||
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard -x '"\C-r": __skim_history__'
|
||||
bind -m vi-command -x '"\C-r": __skim_history__'
|
||||
bind -m vi-insert -x '"\C-r": __skim_history__'
|
||||
fi
|
||||
# CTRL-R - Paste the selected command from history into the command line
|
||||
bind -m emacs-standard -x '"\C-r": __skim_history__'
|
||||
bind -m vi-command -x '"\C-r": __skim_history__'
|
||||
bind -m vi-insert -x '"\C-r": __skim_history__'
|
||||
fi
|
||||
|
||||
# ALT-C - cd into the selected directory
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__skim_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
# ALT-C - cd into the selected directory
|
||||
bind -m emacs-standard '"\ec": " \C-b\C-k \C-u`__skim_cd__`\e\C-e\er\C-m\C-y\C-h\e \C-y\ey\C-x\C-x\C-d"'
|
||||
bind -m vi-command '"\ec": "\C-z\ec\C-z"'
|
||||
bind -m vi-insert '"\ec": "\C-z\ec\C-z"'
|
||||
|
||||
# Completion
|
||||
|
||||
if ! declare -f _skim_compgen_path >/dev/null; then
|
||||
_skim_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2>/dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _skim_compgen_dir >/dev/null; then
|
||||
_skim_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2>/dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
###########################################################
|
||||
|
||||
__skim_comprun() {
|
||||
if [ "$(type -t _skim_comprun 2>&1)" = function ]; then
|
||||
_skim_comprun "$@"
|
||||
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
|
||||
shift
|
||||
sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}} "$@"
|
||||
else
|
||||
shift
|
||||
sk "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
__skim_orig_completion_filter() {
|
||||
sed 's/^\(.*-F\) *\([^ ]*\).* \([^ ]*\)$/export _skim_orig_completion_\3="\1 %s \3 #\2"; [[ "\1" = *" -o nospace "* ]] \&\& [[ ! "$__skim_nospace_commands" = *" \3 "* ]] \&\& __skim_nospace_commands="$__skim_nospace_commands \3 ";/' |
|
||||
awk -F= '{OFS = FS} {gsub(/[^A-Za-z0-9_= ;]/, "_", $1);}1'
|
||||
}
|
||||
|
||||
_skim_handle_dynamic_completion() {
|
||||
local cmd orig_var orig ret orig_cmd orig_complete
|
||||
cmd="$1"
|
||||
shift
|
||||
orig_cmd="$1"
|
||||
orig_var="_skim_orig_completion_$cmd"
|
||||
orig="${!orig_var##*#}"
|
||||
if [ -n "$orig" ] && type "$orig" >/dev/null 2>&1; then
|
||||
$orig "$@"
|
||||
elif [ -n "$_skim_completion_loader" ]; then
|
||||
orig_complete=$(complete -p "$orig_cmd" 2>/dev/null)
|
||||
_completion_loader "$@"
|
||||
ret=$?
|
||||
# _completion_loader may not have updated completion for the command
|
||||
if [ "$(complete -p "$orig_cmd" 2>/dev/null)" != "$orig_complete" ]; then
|
||||
eval "$(complete | command grep " -F.* $orig_cmd$" | __skim_orig_completion_filter)"
|
||||
if [[ "$__skim_nospace_commands" = *" $orig_cmd "* ]]; then
|
||||
eval "${orig_complete/ -F / -o nospace -F }"
|
||||
else
|
||||
eval "$orig_complete"
|
||||
fi
|
||||
fi
|
||||
return $ret
|
||||
fi
|
||||
}
|
||||
|
||||
__skim_generic_path_completion() {
|
||||
local cur base dir leftover matches trigger cmd
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
COMPREPLY=()
|
||||
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
base=${cur:0:${#cur}-${#trigger}}
|
||||
eval "base=$base"
|
||||
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while true; do
|
||||
if [ -z "$dir" ] || [ -d "$dir" ]; then
|
||||
leftover=${base/#"$dir"/}
|
||||
leftover=${leftover/#\//}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$1 $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $2" __skim_comprun "$4" -q "$leftover" | while read -r item; do
|
||||
printf "%q$3 " "$item"
|
||||
done)
|
||||
matches=${matches% }
|
||||
[[ -z "$3" ]] && [[ "$__skim_nospace_commands" = *" ${COMP_WORDS[0]} "* ]] && matches="$matches "
|
||||
if [ -n "$matches" ]; then
|
||||
COMPREPLY=("$matches")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
# To redraw line after skim closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line'
|
||||
printf '\e[5n'
|
||||
return 0
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
[[ "$dir" =~ /$ ]] || dir="$dir"/
|
||||
done
|
||||
else
|
||||
shift
|
||||
shift
|
||||
shift
|
||||
_skim_handle_dynamic_completion "$cmd" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
_skim_complete() {
|
||||
# Split arguments around --
|
||||
local args rest str_arg i sep
|
||||
args=("$@")
|
||||
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 | awk '{print $2}')_post"
|
||||
type -t "$post" >/dev/null 2>&1 || post=cat
|
||||
|
||||
cmd="${COMP_WORDS[0]//[^A-Za-z0-9_=]/_}"
|
||||
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ "$cur" == *"$trigger" ]]; then
|
||||
cur=${cur:0:${#cur}-${#trigger}}
|
||||
|
||||
selected=$(SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "${rest[0]}" "${args[@]}" -q "$cur" | $post | tr '\n' ' ')
|
||||
selected=${selected% } # Strip trailing space not to repeat "-o nospace"
|
||||
if [ -n "$selected" ]; then
|
||||
COMPREPLY=("$selected")
|
||||
else
|
||||
COMPREPLY=("$cur")
|
||||
fi
|
||||
# To redraw line after skim closes (printf '\e[5n')
|
||||
bind '"\e[0n": redraw-current-line'
|
||||
printf '\e[5n'
|
||||
echo -e "\033[0m"
|
||||
return 0
|
||||
else
|
||||
_skim_handle_dynamic_completion "$cmd" "${rest[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
_skim_path_completion() {
|
||||
__skim_generic_path_completion _skim_compgen_path "-m" "" "$@"
|
||||
}
|
||||
|
||||
# Deprecated. No file only completion.
|
||||
_skim_file_completion() {
|
||||
_skim_path_completion "$@"
|
||||
}
|
||||
|
||||
_skim_dir_completion() {
|
||||
__skim_generic_path_completion _skim_compgen_dir "" "/" "$@"
|
||||
}
|
||||
|
||||
_skim_complete_kill() {
|
||||
local trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
local cur="${COMP_WORDS[COMP_CWORD]}"
|
||||
if [[ -z "$cur" ]]; then
|
||||
COMP_WORDS[$COMP_CWORD]=$trigger
|
||||
elif [[ "$cur" != *"$trigger" ]]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
_skim_proc_completion "$@"
|
||||
}
|
||||
|
||||
_skim_proc_completion() {
|
||||
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
)
|
||||
}
|
||||
|
||||
_skim_proc_completion_post() {
|
||||
awk '{print $2}'
|
||||
}
|
||||
|
||||
_skim_host_completion() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
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 -soE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
|
||||
_skim_var_completion() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_alias_completion() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
alias | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
d_cmds="${SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir}"
|
||||
a_cmds="
|
||||
awk cat diff diff3
|
||||
emacs emacsclient ex file ftp g++ gcc gvim head hg java
|
||||
javac ld less more mvim nvim patch perl python ruby
|
||||
sed sftp sort source tail tee uniq vi view vim wc xdg-open
|
||||
basename bunzip2 bzip2 chmod chown curl cp dirname du
|
||||
find git grep gunzip gzip hg jar
|
||||
ln ls mv open rm rsync scp
|
||||
svn tar unzip zip"
|
||||
|
||||
# Preserve existing completion
|
||||
eval "$(complete |
|
||||
sed -E '/-F/!d; / _skim/d; '"/ ($(echo $d_cmds $a_cmds | sed 's/ /|/g; s/+/\\+/g'))$/"'!d' |
|
||||
__skim_orig_completion_filter)"
|
||||
|
||||
if type _completion_loader >/dev/null 2>&1; then
|
||||
_skim_completion_loader=1
|
||||
fi
|
||||
|
||||
__skim_defc() {
|
||||
local cmd func opts orig_var orig def
|
||||
cmd="$1"
|
||||
func="$2"
|
||||
opts="$3"
|
||||
orig_var="_skim_orig_completion_${cmd//[^A-Za-z0-9_]/_}"
|
||||
orig="${!orig_var}"
|
||||
if [ -n "$orig" ]; then
|
||||
printf -v def "$orig" "$func"
|
||||
eval "$def"
|
||||
else
|
||||
complete -F "$func" $opts "$cmd"
|
||||
fi
|
||||
}
|
||||
|
||||
# Anything
|
||||
for cmd in $a_cmds; do
|
||||
__skim_defc "$cmd" _skim_path_completion "-o default -o bashdefault"
|
||||
done
|
||||
|
||||
# Directory
|
||||
for cmd in $d_cmds; do
|
||||
__skim_defc "$cmd" _skim_dir_completion "-o nospace -o dirnames"
|
||||
done
|
||||
|
||||
# Kill completion (supports empty completion trigger)
|
||||
complete -F _skim_complete_kill -o default -o bashdefault kill
|
||||
|
||||
unset cmd d_cmds a_cmds
|
||||
|
||||
_skim_setup_completion() {
|
||||
local kind fn cmd
|
||||
kind=$1
|
||||
fn=_skim_${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
|
||||
eval "$(complete -p "$@" 2>/dev/null | grep -v "$fn" | __skim_orig_completion_filter)"
|
||||
for cmd in "$@"; do
|
||||
case "$kind" in
|
||||
dir) __skim_defc "$cmd" "$fn" "-o nospace -o dirnames" ;;
|
||||
var) __skim_defc "$cmd" "$fn" "-o default -o nospace -v" ;;
|
||||
alias) __skim_defc "$cmd" "$fn" "-a" ;;
|
||||
*) __skim_defc "$cmd" "$fn" "-o default -o bashdefault" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# Environment variables / Aliases / Hosts
|
||||
_skim_setup_completion 'var' export unset
|
||||
_skim_setup_completion 'alias' unalias
|
||||
_skim_setup_completion 'host' ssh telnet
|
||||
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
#!/bin/fish
|
||||
# completion.fish
|
||||
# copied and modified from https://github.com/junegunn/fzf/blob/master/shell/key-bindings.fish
|
||||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.fish
|
||||
# skim key bindings for fish
|
||||
#
|
||||
# - $SKIM_TMUX_OPTS
|
||||
# - $SKIM_CTRL_T_COMMAND
|
||||
|
|
@ -13,6 +7,8 @@
|
|||
# - $SKIM_CTRL_R_OPTS
|
||||
# - $SKIM_ALT_C_COMMAND
|
||||
# - $SKIM_ALT_C_OPTS
|
||||
# - $SKIM_COMPLETION_TRIGGER (default: '**')
|
||||
# - $SKIM_COMPLETION_OPTS (default: empty)
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
|
@ -32,9 +28,8 @@ function skim_key_bindings
|
|||
-o -type d -print \
|
||||
-o -type l -print 2> /dev/null | sed 's@^\./@@'"
|
||||
|
||||
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx SKIM_DEFAULT_OPTIONS "--height $SKIM_TMUX_HEIGHT --reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS"
|
||||
set -lx SKIM_DEFAULT_OPTIONS "--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS"
|
||||
eval "$SKIM_CTRL_T_COMMAND | "(__skimcmd)' -m --query "'$skim_query'"' | while read -l r; set result $result $r; end
|
||||
end
|
||||
if [ -z "$result" ]
|
||||
|
|
@ -52,9 +47,8 @@ function skim_key_bindings
|
|||
end
|
||||
|
||||
function skim-history-widget -d "Show command history"
|
||||
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx SKIM_DEFAULT_OPTIONS "--height $SKIM_TMUX_HEIGHT $SKIM_DEFAULT_OPTIONS --tiebreak=index --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi"
|
||||
set -lx SKIM_DEFAULT_OPTIONS "$SKIM_DEFAULT_OPTIONS --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --no-multi"
|
||||
|
||||
set -l FISH_MAJOR (echo $version | cut -f1 -d.)
|
||||
set -l FISH_MINOR (echo $version | cut -f2 -d.)
|
||||
|
|
@ -63,7 +57,7 @@ function skim_key_bindings
|
|||
# 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 (__skimcmd) --read0 --print0 -q '(commandline)' | read -lz result
|
||||
history -z | eval (__skimcmd) --read0 --multiline --print0 -q '(commandline)' | read -lz result
|
||||
and commandline -- $result
|
||||
else
|
||||
history | eval (__skimcmd) -q '(commandline)' | read -l result
|
||||
|
|
@ -81,9 +75,8 @@ function skim_key_bindings
|
|||
test -n "$SKIM_ALT_C_COMMAND"; or set -l SKIM_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 "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
|
||||
begin
|
||||
set -lx SKIM_DEFAULT_OPTIONS "--height $SKIM_TMUX_HEIGHT --reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS"
|
||||
set -lx SKIM_DEFAULT_OPTIONS "--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS"
|
||||
eval "$SKIM_ALT_C_COMMAND | "(__skimcmd)' --no-multi --query "'$skim_query'"' | read -l result
|
||||
|
||||
if [ -n "$result" ]
|
||||
|
|
@ -101,9 +94,9 @@ function skim_key_bindings
|
|||
test -n "$SKIM_TMUX"; or set SKIM_TMUX 0
|
||||
test -n "$SKIM_TMUX_HEIGHT"; or set SKIM_TMUX_HEIGHT 40%
|
||||
if [ -n "$SKIM_TMUX_OPTS" ]
|
||||
echo "sk-tmux $SKIM_TMUX_OPTS -- "
|
||||
echo "sk --tmux=$SKIM_TMUX_OPTS "
|
||||
else if [ $SKIM_TMUX -eq 1 ]
|
||||
echo "sk-tmux -d$SKIM_TMUX_HEIGHT -- "
|
||||
echo "sk --tmux=center,$SKIM_TMUX_HEIGHT"
|
||||
else
|
||||
echo "sk"
|
||||
end
|
||||
|
|
@ -160,5 +153,4 @@ function skim_key_bindings
|
|||
|
||||
echo $dir
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
# ____ ____
|
||||
# / __/___ / __/
|
||||
# / /_/_ / / /_
|
||||
# / __/ / /_/ __/
|
||||
# /_/ /___/_/ key-bindings.zsh
|
||||
# skim key bindings for zsh
|
||||
#
|
||||
# - $SKIM_TMUX_OPTS
|
||||
# - $SKIM_CTRL_T_COMMAND
|
||||
# - $SKIM_CTRL_T_OPTS
|
||||
# - $SKIM_CTRL_R_OPTS
|
||||
# - $SKIM_CTRL_R_IDX_COLOR (default: \033[2m, set NO_COLOR to disable)
|
||||
# - $SKIM_CTRL_R_DATE_COLOR (default: \033[32m, set NO_COLOR to disable)
|
||||
# - $SKIM_ALT_C_COMMAND
|
||||
# - $SKIM_ALT_C_OPTS
|
||||
# - $SKIM_COMPLETION_TRIGGER (default: '**')
|
||||
# - $SKIM_COMPLETION_OPTS (default: empty)
|
||||
|
||||
# Key bindings
|
||||
# ------------
|
||||
|
||||
# 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
|
||||
__skim_key_bindings_options="options=(${(j: :)${(kv)options[@]}})"
|
||||
__skim_completion_options="options=(${(j: :)${(kv)options[@]}})"
|
||||
else
|
||||
() {
|
||||
__skim_key_bindings_options="setopt"
|
||||
|
|
@ -30,6 +29,17 @@ else
|
|||
fi
|
||||
done
|
||||
}
|
||||
() {
|
||||
'local' '__skim_opt'
|
||||
__skim_completion_options="setopt"
|
||||
for __skim_opt in "${(@)${(@f)$(set -o)}%% *}"; do
|
||||
if [[ -o "$__skim_opt" ]]; then
|
||||
__skim_completion_options+=" -o $__skim_opt"
|
||||
else
|
||||
__skim_completion_options+=" +o $__skim_opt"
|
||||
fi
|
||||
done
|
||||
}
|
||||
fi
|
||||
|
||||
'emulate' 'zsh' '-o' 'no_aliases'
|
||||
|
|
@ -47,7 +57,7 @@ __fsel() {
|
|||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
REPORTTIME_=$REPORTTIME
|
||||
unset REPORTTIME
|
||||
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read item; do
|
||||
eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_CTRL_T_OPTS" $(__skimcmd) -m "$@" | while read item; do
|
||||
echo -n "${(q)item} "
|
||||
done
|
||||
local ret=$?
|
||||
|
|
@ -61,7 +71,7 @@ __fsel() {
|
|||
|
||||
__skimcmd() {
|
||||
[ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; } &&
|
||||
echo "sk-tmux ${SKIM_TMUX_OPTS:--d${SKIM_TMUX_HEIGHT:-40%}} -- " || echo "sk"
|
||||
echo "sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}}" || echo "sk"
|
||||
}
|
||||
|
||||
skim-file-widget() {
|
||||
|
|
@ -90,7 +100,7 @@ skim-cd-widget() {
|
|||
setopt localoptions pipefail no_aliases 2> /dev/null
|
||||
REPORTTIME_=$REPORTTIME
|
||||
unset REPORTTIME
|
||||
local dir="$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} --reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)"
|
||||
local dir="$(eval "$cmd" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_ALT_C_OPTS" $(__skimcmd) --no-multi)"
|
||||
if ! [ -z $REPORTTIME_ ]; then
|
||||
REPORTTIME=$REPORTTIME_
|
||||
fi
|
||||
|
|
@ -109,6 +119,7 @@ skim-cd-widget() {
|
|||
local ret=$?
|
||||
unset dir # ensure this doesn't end up appearing in prompt expansion
|
||||
zle skim-redraw-prompt
|
||||
tput cnorm
|
||||
return $ret
|
||||
}
|
||||
zle -N skim-cd-widget
|
||||
|
|
@ -118,8 +129,35 @@ bindkey '\ec' skim-cd-widget
|
|||
skim-history-widget() {
|
||||
local selected num
|
||||
setopt localoptions noglobsubst noposixbuiltins pipefail no_aliases 2> /dev/null
|
||||
selected=( $(fc -rl 1 | perl -ne 'print if !$seen{(/^\s*[0-9]+\**\s+(.*)/, $1)}++' |
|
||||
SKIM_DEFAULT_OPTIONS="--height ${SKIM_TMUX_HEIGHT:-40%} $SKIM_DEFAULT_OPTIONS -n2..,.. --tiebreak=index --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --query=${(qqq)LBUFFER} --no-multi" $(__skimcmd)) )
|
||||
local c_idx='' c_date='' c_reset='' ansi_opt=''
|
||||
if [[ ! -v NO_COLOR ]]; then
|
||||
c_idx="${SKIM_CTRL_R_IDX_COLOR:-\033[2m}"
|
||||
c_date="${SKIM_CTRL_R_DATE_COLOR:-\033[32m}"
|
||||
c_reset='\033[0m'
|
||||
ansi_opt='--ansi'
|
||||
fi
|
||||
local awk_filter='{ $1=$1; cmd=$0; sub(/^[ \t]*[0-9]+\**[ \t]+/, "", cmd); if (!seen[cmd]++) { idx=$1; sub(idx, c_idx idx c_reset); print } }'
|
||||
local n=2 fc_opts=''
|
||||
if [[ -o extended_history ]]; then
|
||||
local today=$(date +%Y-%m-%d)
|
||||
# For today's commands, replace date ($2) with "today", otherwise remove time ($3).
|
||||
# And filter out duplicates.
|
||||
awk_filter='{
|
||||
$1=$1;
|
||||
cmd = $0; sub(/^[ \t]*[0-9]+\**[ \t]+[^ \t]+[ \t]+[^ \t]+[ \t]+/, "", cmd)
|
||||
if (!seen[cmd]++) {
|
||||
time = $3; date = $2; idx = $1
|
||||
if (date == today) sub(date " " time " ", c_date "today@" time c_reset "\t")
|
||||
else sub(date " " time " ", c_date date c_reset "\t")
|
||||
sub(idx, c_idx idx c_reset)
|
||||
print
|
||||
}
|
||||
}'
|
||||
fc_opts='-i'
|
||||
n=3
|
||||
fi
|
||||
selected=( $(fc -rl $fc_opts 1 | awk -v c_idx="$c_idx" -v c_date="$c_date" -v c_reset="$c_reset" -v today="$today" "$awk_filter" | sed 's/\\n/\\n\t/g' |
|
||||
SKIM_DEFAULT_OPTIONS="$SKIM_DEFAULT_OPTIONS -n$n..,.. --bind=ctrl-r:toggle-sort $SKIM_CTRL_R_OPTS --query=${(qqq)LBUFFER} --no-multi $ansi_opt --tabstop=20 --multiline" $(__skimcmd)) )
|
||||
local ret=$?
|
||||
if [ -n "$selected" ]; then
|
||||
num=$selected[1]
|
||||
|
|
@ -128,12 +166,254 @@ skim-history-widget() {
|
|||
fi
|
||||
fi
|
||||
zle reset-prompt
|
||||
tput cnorm
|
||||
return $ret
|
||||
}
|
||||
zle -N skim-history-widget
|
||||
bindkey '^R' skim-history-widget
|
||||
|
||||
# Completion
|
||||
# To use custom commands instead of find, override _skim_compgen_{path,dir}
|
||||
if ! declare -f _skim_compgen_path > /dev/null; then
|
||||
_skim_compgen_path() {
|
||||
echo "$1"
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o \( -type d -o -type f -o -type l \) \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
if ! declare -f _skim_compgen_dir > /dev/null; then
|
||||
_skim_compgen_dir() {
|
||||
command find -L "$1" \
|
||||
-name .git -prune -o -name .hg -prune -o -name .svn -prune -o -type d \
|
||||
-a -not -path "$1" -print 2> /dev/null | sed 's@^\./@@'
|
||||
}
|
||||
fi
|
||||
|
||||
###########################################################
|
||||
|
||||
__skim_comprun() {
|
||||
if [[ "$(type _skim_comprun 2>&1)" =~ function ]]; then
|
||||
_skim_comprun "$@"
|
||||
elif [ -n "$TMUX_PANE" ] && { [ "${SKIM_TMUX:-0}" != 0 ] || [ -n "$SKIM_TMUX_OPTS" ]; }; then
|
||||
shift
|
||||
if [ -n "$SKIM_TMUX_OPTS" ]; then
|
||||
sk --tmux=${(Q)${(Z+n+)SKIM_TMUX_OPTS}} "$@"
|
||||
else
|
||||
sk --tmux=${SKIM_TMUX_OPTS:-center,${SKIM_TMUX_HEIGHT:-40%}} "$@"
|
||||
fi
|
||||
else
|
||||
shift
|
||||
sk "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Extract the name of the command. e.g. foo=1 bar baz**<tab>
|
||||
__skim_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]}"
|
||||
}
|
||||
|
||||
__skim_generic_path_completion() {
|
||||
local base lbuf cmd compgen skim_opts suffix tail dir leftover matches
|
||||
base=$1
|
||||
lbuf=$2
|
||||
cmd=$(__skim_extract_command "$lbuf")
|
||||
compgen=$3
|
||||
skim_opts=$4
|
||||
suffix=$5
|
||||
tail=$6
|
||||
|
||||
setopt localoptions nonomatch
|
||||
eval "base=$base"
|
||||
[[ $base = *"/"* ]] && dir="$base"
|
||||
while [ 1 ]; do
|
||||
if [[ -z "$dir" || -d ${dir} ]]; then
|
||||
leftover=${base/#"$dir"}
|
||||
leftover=${leftover/#\/}
|
||||
[ -z "$dir" ] && dir='.'
|
||||
[ "$dir" != "/" ] && dir="${dir/%\//}"
|
||||
matches=$(eval "$compgen $(printf %q "$dir")" | SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS" __skim_comprun "$cmd" ${(Q)${(Z+n+)skim_opts}} -q "$leftover" | while read item; do
|
||||
echo -n "${(q)item}$suffix "
|
||||
done)
|
||||
matches=${matches% }
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches$tail"
|
||||
fi
|
||||
zle reset-prompt
|
||||
break
|
||||
fi
|
||||
dir=$(dirname "$dir")
|
||||
dir=${dir%/}/
|
||||
done
|
||||
}
|
||||
|
||||
_skim_path_completion() {
|
||||
__skim_generic_path_completion "$1" "$2" _skim_compgen_path \
|
||||
"-m" "" " "
|
||||
}
|
||||
|
||||
_skim_dir_completion() {
|
||||
__skim_generic_path_completion "$1" "$2" _skim_compgen_dir \
|
||||
"" "/" ""
|
||||
}
|
||||
|
||||
_skim_complete() {
|
||||
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 lbuf cmd matches post
|
||||
lbuf=${rest[0]}
|
||||
cmd=$(__skim_extract_command "$lbuf")
|
||||
post="${funcstack[1]}_post"
|
||||
type $post > /dev/null 2>&1 || post=cat
|
||||
|
||||
matches=$(SKIM_DEFAULT_OPTIONS="--reverse $SKIM_DEFAULT_OPTIONS $SKIM_COMPLETION_OPTS $str_arg" __skim_comprun "$cmd" "${args[@]}" -q "${(Q)prefix}" | $post | tr '\n' ' ')
|
||||
if [ -n "$matches" ]; then
|
||||
LBUFFER="$lbuf$matches"
|
||||
fi
|
||||
zle reset-prompt
|
||||
}
|
||||
|
||||
_skim_complete_telnet() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0' |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_ssh() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
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 -osE '^[[a-z0-9.,:-]+' ~/.ssh/known_hosts | tr ',' '\n' | tr -d '[' | awk '{ print $1 " " $1 }') \
|
||||
<(command grep -sv '^\s*\(#\|$\)' /etc/hosts | command grep -Fv '0.0.0.0') |
|
||||
awk '{if (length($2) > 0) {print $2}}' | sort -u
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_export() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_unset() {
|
||||
_skim_complete -m -- "$@" < <(
|
||||
declare -xp | sed 's/=.*//' | sed 's/.* //'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_unalias() {
|
||||
_skim_complete --no-multi -- "$@" < <(
|
||||
alias | sed 's/=.*//'
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_kill() {
|
||||
_skim_complete -m --preview 'echo {}' --preview-window down:3:wrap --min-height 15 -- "$@" < <(
|
||||
command ps -ef | sed 1d
|
||||
)
|
||||
}
|
||||
|
||||
_skim_complete_kill_post() {
|
||||
awk '{print $2}'
|
||||
}
|
||||
|
||||
skim-completion() {
|
||||
local tokens cmd prefix trigger tail matches lbuf d_cmds
|
||||
setopt localoptions noshwordsplit noksh_arrays noposixbuiltins
|
||||
|
||||
# http://zsh.sourceforge.net/FAQ/zshfaq03.html
|
||||
# http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion-Flags
|
||||
tokens=(${(z)LBUFFER})
|
||||
if [ ${#tokens} -lt 1 ]; then
|
||||
zle ${skim_default_completion:-expand-or-complete}
|
||||
return
|
||||
fi
|
||||
|
||||
cmd=$(__skim_extract_command "$LBUFFER")
|
||||
|
||||
# Explicitly allow for empty trigger.
|
||||
trigger=${SKIM_COMPLETION_TRIGGER-'**'}
|
||||
[ -z "$trigger" -a ${LBUFFER[-1]} = ' ' ] && tokens+=("")
|
||||
|
||||
# When the trigger starts with ';', it becomes a separate token
|
||||
if [[ ${LBUFFER} = *"${tokens[-2]}${tokens[-1]}" ]]; then
|
||||
tokens[-2]="${tokens[-2]}${tokens[-1]}"
|
||||
tokens=(${tokens[0,-2]})
|
||||
fi
|
||||
|
||||
lbuf=$LBUFFER
|
||||
tail=${LBUFFER:$(( ${#LBUFFER} - ${#trigger} ))}
|
||||
# Kill completion (do not require trigger sequence)
|
||||
if [ "$cmd" = kill -a ${LBUFFER[-1]} = ' ' ]; then
|
||||
tail=$trigger
|
||||
tokens+=$trigger
|
||||
lbuf="$lbuf$trigger"
|
||||
fi
|
||||
|
||||
# Trigger sequence given
|
||||
if [ ${#tokens} -gt 1 -a "$tail" = "$trigger" ]; then
|
||||
d_cmds=(${=SKIM_COMPLETION_DIR_COMMANDS:-cd pushd rmdir})
|
||||
|
||||
[ -z "$trigger" ] && prefix=${tokens[-1]} || prefix=${tokens[-1]:0:-${#trigger}}
|
||||
[ -n "${tokens[-1]}" ] && lbuf=${lbuf:0:-${#tokens[-1]}}
|
||||
|
||||
if eval "type _skim_complete_${cmd} > /dev/null"; then
|
||||
prefix="$prefix" eval _skim_complete_${cmd} ${(q)lbuf}
|
||||
elif [ ${d_cmds[(i)$cmd]} -le ${#d_cmds} ]; then
|
||||
_skim_dir_completion "$prefix" "$lbuf"
|
||||
else
|
||||
_skim_path_completion "$prefix" "$lbuf"
|
||||
fi
|
||||
tput cnorm
|
||||
# Fall back to default completion
|
||||
else
|
||||
zle ${skim_default_completion:-expand-or-complete}
|
||||
fi
|
||||
}
|
||||
|
||||
[ -z "$skim_default_completion" ] && {
|
||||
binding=$(bindkey '^I')
|
||||
[[ $binding =~ 'undefined-key' ]] || skim_default_completion=$binding[(s: :w)2]
|
||||
unset binding
|
||||
}
|
||||
|
||||
zle -N skim-completion
|
||||
bindkey '^I' skim-completion
|
||||
|
||||
} always {
|
||||
eval $__skim_key_bindings_options
|
||||
'unset' '__skim_key_bindings_options'
|
||||
eval $__skim_completion_options
|
||||
'unset' '__skim_completion_options'
|
||||
}
|
||||
|
|
|
|||
1
shell/version.txt
Normal file
1
shell/version.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
5.7.0
|
||||
624
src/ansi.rs
624
src/ansi.rs
|
|
@ -1,624 +0,0 @@
|
|||
// Parse ANSI attr code
|
||||
use std::default::Default;
|
||||
use std::mem;
|
||||
|
||||
use beef::lean::Cow;
|
||||
use std::cmp::max;
|
||||
use tuikit::prelude::*;
|
||||
use vte::{Params, Perform};
|
||||
|
||||
/// An ANSI Parser, will parse one line at a time.
|
||||
///
|
||||
/// It will cache the latest attribute used, that means if an attribute affect multiple
|
||||
/// lines, the parser will recognize it.
|
||||
pub struct ANSIParser {
|
||||
partial_str: String,
|
||||
last_attr: Attr,
|
||||
|
||||
stripped: String,
|
||||
stripped_char_count: usize,
|
||||
fragments: Vec<(Attr, (u32, u32))>, // [char_index_start, char_index_end)
|
||||
}
|
||||
|
||||
impl Default for ANSIParser {
|
||||
fn default() -> Self {
|
||||
ANSIParser {
|
||||
partial_str: String::new(),
|
||||
last_attr: Attr::default(),
|
||||
|
||||
stripped: String::new(),
|
||||
stripped_char_count: 0,
|
||||
fragments: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Perform for ANSIParser {
|
||||
fn print(&mut self, ch: char) {
|
||||
self.partial_str.push(ch);
|
||||
}
|
||||
|
||||
fn execute(&mut self, byte: u8) {
|
||||
match byte {
|
||||
// \b to delete character back
|
||||
0x08 => {
|
||||
self.partial_str.pop();
|
||||
}
|
||||
// put back \0 \r \n \t
|
||||
0x00 | 0x0d | 0x0A | 0x09 => self.partial_str.push(byte as char),
|
||||
// ignore all others
|
||||
_ => trace!("AnsiParser:execute ignored {:?}", byte),
|
||||
}
|
||||
}
|
||||
|
||||
fn hook(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, _action: char) {
|
||||
trace!("AnsiParser:hook ignored {:?}", params);
|
||||
}
|
||||
|
||||
fn put(&mut self, byte: u8) {
|
||||
trace!("AnsiParser:put ignored {:?}", byte);
|
||||
}
|
||||
|
||||
fn unhook(&mut self) {
|
||||
trace!("AnsiParser:unhook ignored");
|
||||
}
|
||||
|
||||
fn osc_dispatch(&mut self, params: &[&[u8]], _bell_terminated: bool) {
|
||||
trace!("AnsiParser:osc ignored {:?}", params);
|
||||
}
|
||||
|
||||
fn csi_dispatch(&mut self, params: &Params, _intermediates: &[u8], _ignore: bool, action: char) {
|
||||
// https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters
|
||||
// Only care about graphic modes, ignore all others
|
||||
|
||||
if action != 'm' {
|
||||
trace!("ignore: params: {:?}, action : {:?}", params, action);
|
||||
return;
|
||||
}
|
||||
|
||||
// \[[m => means reset
|
||||
let mut attr = if params.is_empty() {
|
||||
Attr::default()
|
||||
} else {
|
||||
self.last_attr
|
||||
};
|
||||
|
||||
let mut iter = params.iter();
|
||||
while let Some(code) = iter.next() {
|
||||
match code[0] {
|
||||
0 => attr = Attr::default(),
|
||||
1 => attr.effect |= Effect::BOLD,
|
||||
2 => attr.effect |= !Effect::BOLD,
|
||||
4 => attr.effect |= Effect::UNDERLINE,
|
||||
5 => attr.effect |= Effect::BLINK,
|
||||
7 => attr.effect |= Effect::REVERSE,
|
||||
num if num >= 30 && num <= 37 => {
|
||||
attr.fg = Color::AnsiValue((num - 30) as u8);
|
||||
}
|
||||
38 => match iter.next() {
|
||||
Some(&[2]) => {
|
||||
// ESC[ 38;2;<r>;<g>;<b> m Select RGB foreground color
|
||||
let (r, g, b) = match (iter.next(), iter.next(), iter.next()) {
|
||||
(Some(r), Some(g), Some(b)) => (r[0] as u8, g[0] as u8, b[0] as u8),
|
||||
_ => {
|
||||
trace!("ignore CSI {:?} m", params);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
attr.fg = Color::Rgb(r, g, b);
|
||||
}
|
||||
Some(&[5]) => {
|
||||
// ESC[ 38;5;<n> m Select foreground color
|
||||
let color = match iter.next() {
|
||||
Some(color) => color[0] as u8,
|
||||
None => {
|
||||
trace!("ignore CSI {:?} m", params);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
attr.fg = Color::AnsiValue(color);
|
||||
}
|
||||
_ => {
|
||||
trace!("error on parsing CSI {:?} m", params);
|
||||
}
|
||||
},
|
||||
39 => attr.fg = Color::Default,
|
||||
num if num >= 40 && num <= 47 => {
|
||||
attr.bg = Color::AnsiValue((num - 40) as u8);
|
||||
}
|
||||
48 => match iter.next() {
|
||||
Some(&[2]) => {
|
||||
// ESC[ 48;2;<r>;<g>;<b> m Select RGB background color
|
||||
let (r, g, b) = match (iter.next(), iter.next(), iter.next()) {
|
||||
(Some(r), Some(g), Some(b)) => (r[0] as u8, g[0] as u8, b[0] as u8),
|
||||
_ => {
|
||||
trace!("ignore CSI {:?} m", params);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
attr.bg = Color::Rgb(r, g, b);
|
||||
}
|
||||
Some(&[5]) => {
|
||||
// ESC[ 48;5;<n> m Select background color
|
||||
let color = match iter.next() {
|
||||
Some(color) => color[0] as u8,
|
||||
None => {
|
||||
trace!("ignore CSI {:?} m", params);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
attr.bg = Color::AnsiValue(color);
|
||||
}
|
||||
_ => {
|
||||
trace!("ignore CSI {:?} m", params);
|
||||
}
|
||||
},
|
||||
49 => attr.bg = Color::Default,
|
||||
_ => {
|
||||
trace!("ignore CSI {:?} m", params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.attr_change(attr);
|
||||
}
|
||||
|
||||
fn esc_dispatch(&mut self, _intermediates: &[u8], _ignore: bool, _byte: u8) {
|
||||
// ESC characters are replaced with \[
|
||||
self.partial_str.push('"');
|
||||
self.partial_str.push('[');
|
||||
}
|
||||
}
|
||||
|
||||
impl ANSIParser {
|
||||
/// save the partial_str into fragments with current attr
|
||||
fn save_str(&mut self) {
|
||||
if self.partial_str.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let string = mem::replace(&mut self.partial_str, String::new());
|
||||
let string_char_count = string.chars().count();
|
||||
self.fragments.push((
|
||||
self.last_attr,
|
||||
(
|
||||
self.stripped_char_count as u32,
|
||||
(self.stripped_char_count + string_char_count) as u32,
|
||||
),
|
||||
));
|
||||
self.stripped_char_count += string_char_count;
|
||||
self.stripped.push_str(&string);
|
||||
}
|
||||
|
||||
// accept a new attr
|
||||
fn attr_change(&mut self, new_attr: Attr) {
|
||||
if new_attr == self.last_attr {
|
||||
return;
|
||||
}
|
||||
|
||||
self.save_str();
|
||||
self.last_attr = new_attr;
|
||||
}
|
||||
|
||||
pub fn parse_ansi(&mut self, text: &str) -> AnsiString<'static> {
|
||||
let mut statemachine = vte::Parser::new();
|
||||
|
||||
for byte in text.as_bytes() {
|
||||
statemachine.advance(self, *byte);
|
||||
}
|
||||
self.save_str();
|
||||
|
||||
let stripped = mem::replace(&mut self.stripped, String::new());
|
||||
self.stripped_char_count = 0;
|
||||
let fragments = mem::replace(&mut self.fragments, Vec::new());
|
||||
AnsiString::new_string(stripped, fragments)
|
||||
}
|
||||
}
|
||||
|
||||
/// A String that contains ANSI state (e.g. colors)
|
||||
///
|
||||
/// It is internally represented as Vec<(attr, string)>
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AnsiString<'a> {
|
||||
stripped: Cow<'a, str>,
|
||||
// attr: start, end
|
||||
fragments: Option<Vec<(Attr, (u32, u32))>>,
|
||||
}
|
||||
|
||||
impl<'a> AnsiString<'a> {
|
||||
pub fn new_empty() -> Self {
|
||||
Self {
|
||||
stripped: Cow::borrowed(""),
|
||||
fragments: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_raw_string(string: String) -> Self {
|
||||
Self {
|
||||
stripped: Cow::owned(string),
|
||||
fragments: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_raw_str(str_ref: &'a str) -> Self {
|
||||
Self {
|
||||
stripped: Cow::borrowed(str_ref),
|
||||
fragments: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// assume the fragments are ordered by (start, end) while end is exclusive
|
||||
pub fn new_str(stripped: &'a str, fragments: Vec<(Attr, (u32, u32))>) -> Self {
|
||||
let fragments_empty = fragments.is_empty() || (fragments.len() == 1 && fragments[0].0 == Attr::default());
|
||||
Self {
|
||||
stripped: Cow::borrowed(stripped),
|
||||
fragments: if fragments_empty { None } else { Some(fragments) },
|
||||
}
|
||||
}
|
||||
|
||||
/// assume the fragments are ordered by (start, end) while end is exclusive
|
||||
pub fn new_string(stripped: String, fragments: Vec<(Attr, (u32, u32))>) -> Self {
|
||||
let fragments_empty = fragments.is_empty() || (fragments.len() == 1 && fragments[0].0 == Attr::default());
|
||||
Self {
|
||||
stripped: Cow::owned(stripped),
|
||||
fragments: if fragments_empty { None } else { Some(fragments) },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(raw: &'a str) -> AnsiString<'static> {
|
||||
ANSIParser::default().parse_ansi(raw)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.stripped.is_empty()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> std::borrow::Cow<'a, str> {
|
||||
std::borrow::Cow::Owned(self.stripped.into_owned())
|
||||
}
|
||||
|
||||
pub fn iter(&'a self) -> Box<dyn Iterator<Item = (char, Attr)> + 'a> {
|
||||
if self.fragments.is_none() {
|
||||
return Box::new(self.stripped.chars().map(|c| (c, Attr::default())));
|
||||
}
|
||||
|
||||
Box::new(AnsiStringIterator::new(
|
||||
&self.stripped,
|
||||
self.fragments.as_ref().unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn has_attrs(&self) -> bool {
|
||||
self.fragments.is_some()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn stripped(&self) -> &str {
|
||||
&self.stripped
|
||||
}
|
||||
|
||||
pub fn override_attrs(&mut self, attrs: Vec<(Attr, (u32, u32))>) {
|
||||
if attrs.is_empty() {
|
||||
// pass
|
||||
} else if self.fragments.is_none() {
|
||||
self.fragments = Some(attrs);
|
||||
} else {
|
||||
let current_fragments = self.fragments.take().expect("unreachable");
|
||||
let new_fragments = merge_fragments(¤t_fragments, &attrs);
|
||||
self.fragments.replace(new_fragments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for AnsiString<'a> {
|
||||
fn from(s: &'a str) -> AnsiString<'a> {
|
||||
AnsiString::new_raw_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AnsiString<'static> {
|
||||
fn from(s: String) -> Self {
|
||||
AnsiString::new_raw_string(s)
|
||||
}
|
||||
}
|
||||
|
||||
// (text, indices, highlight attribute) -> AnsiString
|
||||
impl<'a> From<(&'a str, &'a [usize], Attr)> for AnsiString<'a> {
|
||||
fn from((text, indices, attr): (&'a str, &'a [usize], Attr)) -> Self {
|
||||
let fragments = indices
|
||||
.iter()
|
||||
.map(|&idx| (attr, (idx as u32, 1 + idx as u32)))
|
||||
.collect();
|
||||
AnsiString::new_str(text, fragments)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over all the (char, attr) characters.
|
||||
pub struct AnsiStringIterator<'a> {
|
||||
fragments: &'a [(Attr, (u32, u32))],
|
||||
fragment_idx: usize,
|
||||
chars_iter: std::iter::Enumerate<std::str::Chars<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> AnsiStringIterator<'a> {
|
||||
pub fn new(stripped: &'a str, fragments: &'a [(Attr, (u32, u32))]) -> Self {
|
||||
Self {
|
||||
fragments,
|
||||
fragment_idx: 0,
|
||||
chars_iter: stripped.chars().enumerate(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for AnsiStringIterator<'a> {
|
||||
type Item = (char, Attr);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.chars_iter.next() {
|
||||
Some((char_idx, char)) => {
|
||||
// update fragment_idx
|
||||
loop {
|
||||
if self.fragment_idx >= self.fragments.len() {
|
||||
break;
|
||||
}
|
||||
|
||||
let (_attr, (_start, end)) = self.fragments[self.fragment_idx];
|
||||
if char_idx < (end as usize) {
|
||||
break;
|
||||
} else {
|
||||
self.fragment_idx += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let (attr, (start, end)) = if self.fragment_idx >= self.fragments.len() {
|
||||
(Attr::default(), (char_idx as u32, 1 + char_idx as u32))
|
||||
} else {
|
||||
self.fragments[self.fragment_idx]
|
||||
};
|
||||
|
||||
if (start as usize) <= char_idx && char_idx < (end as usize) {
|
||||
Some((char, attr))
|
||||
} else {
|
||||
Some((char, Attr::default()))
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_fragments(old: &[(Attr, (u32, u32))], new: &[(Attr, (u32, u32))]) -> Vec<(Attr, (u32, u32))> {
|
||||
let mut ret = vec![];
|
||||
let mut i = 0;
|
||||
let mut j = 0;
|
||||
let mut os = 0;
|
||||
|
||||
while i < old.len() && j < new.len() {
|
||||
let (oa, (o_start, oe)) = old[i];
|
||||
let (na, (ns, ne)) = new[j];
|
||||
os = max(os, o_start);
|
||||
|
||||
if ns <= os && ne >= oe {
|
||||
// [--old--] | [--old--] | [--old--] | [--old--]
|
||||
// [----new----] | [---new---] | [---new---] | [--new--]
|
||||
i += 1; // skip old
|
||||
} else if ns <= os {
|
||||
// [--old--] | [--old--] | [--old--] | [---old---]
|
||||
// [--new--] | [--new--] | [--new--] | [--new--]
|
||||
ret.push((na, (ns, ne)));
|
||||
os = ne;
|
||||
j += 1;
|
||||
} else if ns >= oe {
|
||||
// [--old--] | [--old--]
|
||||
// [--new--] | [--new--]
|
||||
ret.push((oa, (os, oe)));
|
||||
i += 1;
|
||||
} else {
|
||||
// [---old---] | [---old---] | [--old--]
|
||||
// [--new--] | [--new--] | [--new--]
|
||||
ret.push((oa, (os, ns)));
|
||||
os = ns;
|
||||
}
|
||||
}
|
||||
|
||||
if i < old.len() {
|
||||
for &(oa, (s, e)) in old[i..].iter() {
|
||||
ret.push((oa, (max(os, s), e)))
|
||||
}
|
||||
}
|
||||
if j < new.len() {
|
||||
ret.extend_from_slice(&new[j..]);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_ansi_iterator() {
|
||||
let input = "\x1B[48;2;5;10;15m\x1B[38;2;70;130;180mhi\x1B[0m";
|
||||
let ansistring = ANSIParser::default().parse_ansi(input);
|
||||
let mut it = ansistring.iter();
|
||||
let attr = Attr {
|
||||
fg: Color::Rgb(70, 130, 180),
|
||||
bg: Color::Rgb(5, 10, 15),
|
||||
..Attr::default()
|
||||
};
|
||||
|
||||
assert_eq!(Some(('h', attr)), it.next());
|
||||
assert_eq!(Some(('i', attr)), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
assert_eq!(ansistring.stripped(), "hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_highlight_indices() {
|
||||
let text = "abc";
|
||||
let indices: Vec<usize> = vec![1];
|
||||
let attr = Attr {
|
||||
fg: Color::Rgb(70, 130, 180),
|
||||
bg: Color::Rgb(5, 10, 15),
|
||||
..Attr::default()
|
||||
};
|
||||
|
||||
let ansistring = AnsiString::from((text, &indices as &[usize], attr));
|
||||
let mut it = ansistring.iter();
|
||||
|
||||
assert_eq!(Some(('a', Attr::default())), it.next());
|
||||
assert_eq!(Some(('b', attr)), it.next());
|
||||
assert_eq!(Some(('c', Attr::default())), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normal_string() {
|
||||
let input = "ab";
|
||||
let ansistring = ANSIParser::default().parse_ansi(input);
|
||||
|
||||
assert_eq!(false, ansistring.has_attrs());
|
||||
|
||||
let mut it = ansistring.iter();
|
||||
assert_eq!(Some(('a', Attr::default())), it.next());
|
||||
assert_eq!(Some(('b', Attr::default())), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
|
||||
assert_eq!(ansistring.stripped(), "ab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_attributes() {
|
||||
let input = "\x1B[1;31mhi";
|
||||
let ansistring = ANSIParser::default().parse_ansi(input);
|
||||
let mut it = ansistring.iter();
|
||||
let attr = Attr {
|
||||
fg: Color::AnsiValue(1),
|
||||
effect: Effect::BOLD,
|
||||
..Attr::default()
|
||||
};
|
||||
|
||||
assert_eq!(Some(('h', attr)), it.next());
|
||||
assert_eq!(Some(('i', attr)), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
assert_eq!(ansistring.stripped(), "hi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reset() {
|
||||
let input = "\x1B[35mA\x1B[mB";
|
||||
let ansistring = ANSIParser::default().parse_ansi(input);
|
||||
assert_eq!(ansistring.fragments.as_ref().map(|x| x.len()).unwrap(), 2);
|
||||
assert_eq!(ansistring.stripped(), "AB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_bytes() {
|
||||
let input = "中`\x1B[0m\x1B[1m\x1B[31mXYZ\x1B[0ms`";
|
||||
let ansistring = ANSIParser::default().parse_ansi(input);
|
||||
let mut it = ansistring.iter();
|
||||
let default_attr = Attr::default();
|
||||
let annotated = Attr {
|
||||
fg: Color::AnsiValue(1),
|
||||
effect: Effect::BOLD,
|
||||
..default_attr
|
||||
};
|
||||
|
||||
assert_eq!(Some(('中', default_attr)), it.next());
|
||||
assert_eq!(Some(('`', default_attr)), it.next());
|
||||
assert_eq!(Some(('X', annotated)), it.next());
|
||||
assert_eq!(Some(('Y', annotated)), it.next());
|
||||
assert_eq!(Some(('Z', annotated)), it.next());
|
||||
assert_eq!(Some(('s', default_attr)), it.next());
|
||||
assert_eq!(Some(('`', default_attr)), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_fragments() {
|
||||
let ao = Attr::default();
|
||||
let an = Attr::default().bg(Color::BLUE);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (0, 1)), (ao, (1, 2))], &[]),
|
||||
vec![(ao, (0, 1)), (ao, (1, 2))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[], &[(an, (0, 1)), (an, (1, 2))]),
|
||||
vec![(an, (0, 1)), (an, (1, 2))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 6)), (ao, (9, 10))], &[(an, (0, 1))]),
|
||||
vec![(an, (0, 1)), (ao, (1, 3)), (ao, (5, 6)), (ao, (9, 10))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (0, 2))]),
|
||||
vec![(an, (0, 2)), (ao, (2, 3)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (0, 3))]),
|
||||
vec![(an, (0, 3)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(
|
||||
&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))],
|
||||
&[(an, (0, 6)), (an, (6, 7))]
|
||||
),
|
||||
vec![(an, (0, 6)), (an, (6, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (1, 2))]),
|
||||
vec![(an, (1, 2)), (ao, (2, 3)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (1, 3))]),
|
||||
vec![(an, (1, 3)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (1, 4))]),
|
||||
vec![(an, (1, 4)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (2, 3))]),
|
||||
vec![(ao, (1, 2)), (an, (2, 3)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (2, 4))]),
|
||||
vec![(ao, (1, 2)), (an, (2, 4)), (ao, (5, 7)), (ao, (9, 11))]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
merge_fragments(&[(ao, (1, 3)), (ao, (5, 7)), (ao, (9, 11))], &[(an, (2, 6))]),
|
||||
vec![(ao, (1, 2)), (an, (2, 6)), (ao, (6, 7)), (ao, (9, 11))]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_byte_359() {
|
||||
// https://github.com/lotabout/skim/issues/359
|
||||
let highlight = Attr::default().effect(Effect::BOLD);
|
||||
let ansistring = AnsiString::new_str("ああa", vec![(highlight, (2, 3))]);
|
||||
let mut it = ansistring.iter();
|
||||
assert_eq!(Some(('あ', Attr::default())), it.next());
|
||||
assert_eq!(Some(('あ', Attr::default())), it.next());
|
||||
assert_eq!(Some(('a', highlight)), it.next());
|
||||
assert_eq!(None, it.next());
|
||||
}
|
||||
}
|
||||
689
src/bin/main.rs
689
src/bin/main.rs
|
|
@ -1,470 +1,205 @@
|
|||
//! Command-line interface for skim fuzzy finder.
|
||||
//!
|
||||
//! This binary provides the `sk` command-line tool for fuzzy finding and filtering.
|
||||
#![cfg_attr(coverage, allow(unused_features), feature(coverage_attribute))]
|
||||
|
||||
extern crate clap;
|
||||
extern crate env_logger;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate atty;
|
||||
extern crate shlex;
|
||||
extern crate skim;
|
||||
extern crate time;
|
||||
|
||||
use derive_builder::Builder;
|
||||
use std::env;
|
||||
use eyre::{Result, eyre};
|
||||
#[cfg(feature = "listen")]
|
||||
use interprocess::bound_util::RefWrite;
|
||||
#[cfg(feature = "listen")]
|
||||
use interprocess::local_socket::ToNsName as _;
|
||||
#[cfg(feature = "listen")]
|
||||
use interprocess::local_socket::traits::Stream as _;
|
||||
use log::trace;
|
||||
#[cfg(feature = "listen")]
|
||||
use skim::binds::parse_action_chain;
|
||||
use skim::reader::CommandCollector;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader, BufWriter, Write};
|
||||
use std::io;
|
||||
use std::io::{BufReader, BufWriter, IsTerminal, Write};
|
||||
|
||||
use clap::{App, Arg, ArgMatches};
|
||||
use skim::prelude::*;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
fn init_logger(opts: &SkimOptions) {
|
||||
let target = if let Some(ref log_file) = opts.log_file.as_ref().or(std::env::var("SKIM_LOG_FILE").ok().as_ref()) {
|
||||
env_logger::Target::Pipe(Box::new(File::create(log_file).expect("Failed to create log file")))
|
||||
} else {
|
||||
env_logger::Target::Stdout
|
||||
};
|
||||
|
||||
const USAGE: &str = "
|
||||
Usage: sk [options]
|
||||
let env_var = "SKIM_LOG";
|
||||
|
||||
Options
|
||||
-h, --help print this help menu
|
||||
--version print out the current version of skim
|
||||
let format = |buf: &mut env_logger::fmt::Formatter, record: &log::Record<'_>| {
|
||||
writeln!(
|
||||
buf,
|
||||
"[{} {} {} ({}:{})] [{}/{:?}] {}",
|
||||
buf.timestamp_nanos(),
|
||||
record.level().as_str(),
|
||||
record.module_path().unwrap_or("sk"),
|
||||
record.file().unwrap_or_default(),
|
||||
record.line().unwrap_or_default(),
|
||||
std::thread::current().name().unwrap_or("?"),
|
||||
std::thread::current().id(),
|
||||
record.args()
|
||||
)
|
||||
};
|
||||
|
||||
Search
|
||||
--tac reverse the order of search result
|
||||
--no-sort Do not sort the result
|
||||
-t, --tiebreak [score,begin,end,-score,length...]
|
||||
|
||||
comma seperated criteria
|
||||
-n, --nth 1,2..5 specify the fields to be matched
|
||||
--with-nth 1,2..5 specify the fields to be transformed
|
||||
-d, --delimiter \\t specify the delimiter(in REGEX) for fields
|
||||
-e, --exact start skim in exact mode
|
||||
--regex use regex instead of fuzzy match
|
||||
--algo=TYPE Fuzzy matching algorithm:
|
||||
[skim_v1|skim_v2|clangd] (default: skim_v2)
|
||||
--case [respect,ignore,smart] (default: smart)
|
||||
case sensitive or not
|
||||
|
||||
Interface
|
||||
-b, --bind KEYBINDS comma seperated keybindings, in KEY:ACTION
|
||||
such as 'ctrl-j:accept,ctrl-k:kill-line'
|
||||
-m, --multi Enable Multiple Selection
|
||||
--no-multi Disable Multiple Selection
|
||||
--no-mouse Disable mouse events
|
||||
-c, --cmd ag command to invoke dynamically
|
||||
-i, --interactive Start skim in interactive(command) mode
|
||||
--color [BASE][,COLOR:ANSI]
|
||||
change color theme
|
||||
--no-hscroll Disable horizontal scroll
|
||||
--keep-right Keep the right end of the line visible on overflow
|
||||
--skip-to-pattern Line starts with the start of matched pattern
|
||||
--no-clear-if-empty Do not clear previous items if command returns empty result
|
||||
--show-cmd-error Send command error message if command fails
|
||||
|
||||
Layout
|
||||
--layout=LAYOUT Choose layout: [default|reverse|reverse-list]
|
||||
--height=HEIGHT Height of skim's window (--height 40%)
|
||||
--no-height Disable height feature
|
||||
--min-height=HEIGHT Minimum height when --height is given by percent
|
||||
(default: 10)
|
||||
--margin=MARGIN Screen Margin (TRBL / TB,RL / T,RL,B / T,R,B,L)
|
||||
e.g. (sk --margin 1,10%)
|
||||
-p, --prompt '> ' prompt string for query mode
|
||||
--cmd-prompt '> ' prompt string for command mode
|
||||
|
||||
Display
|
||||
--ansi parse ANSI color codes for input strings
|
||||
--tabstop=SPACES Number of spaces for a tab character (default: 8)
|
||||
--inline-info Display info next to query
|
||||
--header=STR Display STR next to info
|
||||
--header-lines=N The first N lines of the input are treated as header
|
||||
|
||||
History
|
||||
--history=FILE History file
|
||||
--history-size=N Maximum number of query history entries (default: 1000)
|
||||
--cmd-history=FILE command History file
|
||||
--cmd-history-size=N Maximum number of command history entries (default: 1000)
|
||||
|
||||
Preview
|
||||
--preview=COMMAND command to preview current highlighted line ({})
|
||||
We can specify the fields. e.g. ({1}, {..3}, {0..})
|
||||
--preview-window=OPT Preview window layout (default: right:50%)
|
||||
[up|down|left|right][:SIZE[%]][:hidden][:+SCROLL[-OFFSET]]
|
||||
|
||||
Scripting
|
||||
-q, --query \"\" specify the initial query
|
||||
--cmd-query \"\" specify the initial query for interactive mode
|
||||
--expect KEYS comma seperated keys that can be used to complete skim
|
||||
--read0 Read input delimited by ASCII NUL(\\0) characters
|
||||
--print0 Print output delimited by ASCII NUL(\\0) characters
|
||||
--no-clear Do not clear screen on exit
|
||||
--print-query Print query as the first line
|
||||
--print-cmd Print command query as the first line (after --print-query)
|
||||
--print-score Print matching score in filter output (with --filter)
|
||||
-1, --select-1 Automatically select the only match
|
||||
-0, --exit-0 Exit immediately when there's no match
|
||||
--sync Synchronous search for multi-staged filtering
|
||||
--pre-select-n=NUM Pre-select the first n items in multi-selection mode
|
||||
--pre-select-pat=REGEX
|
||||
Pre-select the matched items in multi-selection mode
|
||||
--pre-select-items=$'item1\\nitem2'
|
||||
Pre-select the items separated by newline character
|
||||
--pre-select-file=FILENAME
|
||||
Pre-select the items read from file
|
||||
|
||||
Environment variables
|
||||
SKIM_DEFAULT_COMMAND Default command to use when input is tty
|
||||
SKIM_DEFAULT_OPTIONS Default options (e.g. '--ansi --regex')
|
||||
You should not include other environment variables
|
||||
(e.g. '-c \"$HOME/bin/ag\"')
|
||||
|
||||
Removed
|
||||
-I replstr replace `replstr` with the selected item
|
||||
|
||||
Reserved (not used for now)
|
||||
--extended
|
||||
--literal
|
||||
--cycle
|
||||
--hscroll-off=COL
|
||||
--filepath-word
|
||||
--jump-labels=CHARS
|
||||
--border
|
||||
--no-bold
|
||||
--info
|
||||
--pointer
|
||||
--marker
|
||||
--phony
|
||||
";
|
||||
|
||||
const DEFAULT_HISTORY_SIZE: usize = 1000;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
fn main() {
|
||||
env_logger::builder().format_timestamp_nanos().init();
|
||||
|
||||
match real_main() {
|
||||
Ok(exit_code) => std::process::exit(exit_code),
|
||||
Err(err) => {
|
||||
// if downstream pipe is closed, exit silently, see PR#279
|
||||
if err.kind() == std::io::ErrorKind::BrokenPipe {
|
||||
std::process::exit(0)
|
||||
}
|
||||
std::process::exit(2)
|
||||
}
|
||||
if let Some(level) = opts.log_level {
|
||||
env_logger::builder()
|
||||
.filter_level(level)
|
||||
.parse_env(env_var)
|
||||
.target(target)
|
||||
.format(format)
|
||||
.init();
|
||||
} else {
|
||||
env_logger::builder()
|
||||
.parse_env(env_var)
|
||||
.target(target)
|
||||
.format(format)
|
||||
.init();
|
||||
}
|
||||
}
|
||||
|
||||
#[rustfmt::skip]
|
||||
fn real_main() -> Result<i32, std::io::Error> {
|
||||
let mut stdout = std::io::stdout();
|
||||
//------------------------------------------------------------------------------
|
||||
fn main() -> Result<()> {
|
||||
let mut opts = SkimOptions::from_env().unwrap_or_else(|e| {
|
||||
e.exit();
|
||||
});
|
||||
init_logger(&opts);
|
||||
|
||||
let mut args = Vec::new();
|
||||
// Build the options after setting the log target
|
||||
opts = opts.build();
|
||||
trace!("Command line: {:?}", std::env::args());
|
||||
|
||||
args.push(env::args().next().expect("there should be at least one arg: the application name"));
|
||||
args.extend(env::var("SKIM_DEFAULT_OPTIONS")
|
||||
.ok()
|
||||
.and_then(|val| shlex::split(&val))
|
||||
.unwrap_or_default());
|
||||
for arg in env::args().skip(1) {
|
||||
args.push(arg);
|
||||
// Shell completion scripts
|
||||
if let Some(shell) = opts.shell {
|
||||
// Generate completion script directly to stdout
|
||||
skim::shell::generate_completions(&shell, &mut std::io::stdout());
|
||||
if opts.shell_bindings {
|
||||
skim::shell::generate_key_bindings(&shell, &mut std::io::stdout())?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// Man page
|
||||
if opts.man {
|
||||
crate::manpage::generate(&mut std::io::stdout())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// parse options
|
||||
let opts = App::new("sk")
|
||||
.author("Jinzhou Zhang<lotabout@gmail.com>")
|
||||
.arg(Arg::with_name("help").long("help").short("h"))
|
||||
.arg(Arg::with_name("version").long("version").short("v"))
|
||||
.arg(Arg::with_name("bind").long("bind").short("b").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("multi").long("multi").short("m").multiple(true))
|
||||
.arg(Arg::with_name("no-multi").long("no-multi").multiple(true))
|
||||
.arg(Arg::with_name("prompt").long("prompt").short("p").multiple(true).takes_value(true).default_value("> "))
|
||||
.arg(Arg::with_name("cmd-prompt").long("cmd-prompt").multiple(true).takes_value(true).default_value("c> "))
|
||||
.arg(Arg::with_name("expect").long("expect").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("tac").long("tac").multiple(true))
|
||||
.arg(Arg::with_name("tiebreak").long("tiebreak").short("t").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("ansi").long("ansi").multiple(true))
|
||||
.arg(Arg::with_name("exact").long("exact").short("e").multiple(true))
|
||||
.arg(Arg::with_name("cmd").long("cmd").short("c").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("interactive").long("interactive").short("i").multiple(true))
|
||||
.arg(Arg::with_name("query").long("query").short("q").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("cmd-query").long("cmd-query").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("regex").long("regex").multiple(true))
|
||||
.arg(Arg::with_name("delimiter").long("delimiter").short("d").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("nth").long("nth").short("n").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("with-nth").long("with-nth").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("replstr").short("I").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("color").long("color").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("margin").long("margin").multiple(true).takes_value(true).default_value("0,0,0,0"))
|
||||
.arg(Arg::with_name("min-height").long("min-height").multiple(true).takes_value(true).default_value("10"))
|
||||
.arg(Arg::with_name("height").long("height").multiple(true).takes_value(true).default_value("100%"))
|
||||
.arg(Arg::with_name("no-height").long("no-height").multiple(true))
|
||||
.arg(Arg::with_name("no-clear").long("no-clear").multiple(true))
|
||||
.arg(Arg::with_name("no-mouse").long("no-mouse").multiple(true))
|
||||
.arg(Arg::with_name("preview").long("preview").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("preview-window").long("preview-window").multiple(true).takes_value(true).default_value("right:50%"))
|
||||
.arg(Arg::with_name("reverse").long("reverse").multiple(true))
|
||||
|
||||
.arg(Arg::with_name("algorithm").long("algo").multiple(true).takes_value(true).default_value("skim_v2"))
|
||||
.arg(Arg::with_name("case").long("case").multiple(true).takes_value(true).default_value("smart"))
|
||||
.arg(Arg::with_name("literal").long("literal").multiple(true))
|
||||
.arg(Arg::with_name("cycle").long("cycle").multiple(true))
|
||||
.arg(Arg::with_name("no-hscroll").long("no-hscroll").multiple(true))
|
||||
.arg(Arg::with_name("hscroll-off").long("hscroll-off").multiple(true).takes_value(true).default_value("10"))
|
||||
.arg(Arg::with_name("filepath-word").long("filepath-word").multiple(true))
|
||||
.arg(Arg::with_name("jump-labels").long("jump-labels").multiple(true).takes_value(true).default_value("abcdefghijklmnopqrstuvwxyz"))
|
||||
.arg(Arg::with_name("border").long("border").multiple(true))
|
||||
.arg(Arg::with_name("inline-info").long("inline-info").multiple(true))
|
||||
.arg(Arg::with_name("header").long("header").multiple(true).takes_value(true).default_value(""))
|
||||
.arg(Arg::with_name("header-lines").long("header-lines").multiple(true).takes_value(true).default_value("0"))
|
||||
.arg(Arg::with_name("tabstop").long("tabstop").multiple(true).takes_value(true).default_value("8"))
|
||||
.arg(Arg::with_name("no-bold").long("no-bold").multiple(true))
|
||||
.arg(Arg::with_name("history").long("history").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("cmd-history").long("cmd-history").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("history-size").long("history-size").multiple(true).takes_value(true).default_value("1000"))
|
||||
.arg(Arg::with_name("cmd-history-size").long("cmd-history-size").multiple(true).takes_value(true).default_value("1000"))
|
||||
.arg(Arg::with_name("print-query").long("print-query").multiple(true))
|
||||
.arg(Arg::with_name("print-cmd").long("print-cmd").multiple(true))
|
||||
.arg(Arg::with_name("print-score").long("print-score").multiple(true))
|
||||
.arg(Arg::with_name("read0").long("read0").multiple(true))
|
||||
.arg(Arg::with_name("print0").long("print0").multiple(true))
|
||||
.arg(Arg::with_name("sync").long("sync").multiple(true))
|
||||
.arg(Arg::with_name("extended").long("extended").short("x").multiple(true))
|
||||
.arg(Arg::with_name("no-sort").long("no-sort").multiple(true))
|
||||
.arg(Arg::with_name("select-1").long("select-1").short("1").multiple(true))
|
||||
.arg(Arg::with_name("exit-0").long("exit-0").short("0").multiple(true))
|
||||
.arg(Arg::with_name("filter").long("filter").short("f").takes_value(true).multiple(true))
|
||||
.arg(Arg::with_name("layout").long("layout").multiple(true).takes_value(true).default_value("default"))
|
||||
.arg(Arg::with_name("keep-right").long("keep-right").multiple(true))
|
||||
.arg(Arg::with_name("skip-to-pattern").long("skip-to-pattern").multiple(true).takes_value(true).default_value(""))
|
||||
.arg(Arg::with_name("pre-select-n").long("pre-select-n").multiple(true).takes_value(true).default_value("0"))
|
||||
.arg(Arg::with_name("pre-select-pat").long("pre-select-pat").multiple(true).takes_value(true).default_value(""))
|
||||
.arg(Arg::with_name("pre-select-items").long("pre-select-items").multiple(true).takes_value(true))
|
||||
.arg(Arg::with_name("pre-select-file").long("pre-select-file").multiple(true).takes_value(true).default_value(""))
|
||||
.arg(Arg::with_name("no-clear-if-empty").long("no-clear-if-empty").multiple(true))
|
||||
.arg(Arg::with_name("show-cmd-error").long("show-cmd-error").multiple(true))
|
||||
.get_matches_from(args);
|
||||
|
||||
if opts.is_present("help") {
|
||||
write!(stdout, "{}", USAGE)?;
|
||||
return Ok(0);
|
||||
#[cfg(feature = "listen")]
|
||||
if let Some(remote) = opts.remote {
|
||||
let ns_name = remote
|
||||
.to_ns_name::<interprocess::local_socket::GenericNamespaced>()
|
||||
.unwrap();
|
||||
let stream = interprocess::local_socket::Stream::connect(ns_name)?;
|
||||
let mut action_chain = String::new();
|
||||
loop {
|
||||
action_chain.clear();
|
||||
let len = std::io::stdin().read_line(&mut action_chain)?;
|
||||
log::debug!("Got line {} from stdin", action_chain.trim());
|
||||
if len == 0 {
|
||||
break;
|
||||
}
|
||||
let actions = parse_action_chain(action_chain.trim())?;
|
||||
for act in actions {
|
||||
stream
|
||||
.as_write()
|
||||
.write_all(format!("{}\n", ron::ser::to_string(&act)?).as_bytes())?;
|
||||
log::debug!("Sent action {act:?} to listener");
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if opts.is_present("version") {
|
||||
writeln!(stdout, "{}", VERSION)?;
|
||||
return Ok(0);
|
||||
match sk_main(opts) {
|
||||
Ok(exit_code) => std::process::exit(exit_code),
|
||||
Err(err) => match err.downcast_ref::<clap::error::Error>() {
|
||||
Some(e) => e.exit(),
|
||||
None => Err(eyre!(err)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
let mut options = parse_options(&opts);
|
||||
|
||||
let preview_window_joined = opts.values_of("preview-window").map(|x| x.collect::<Vec<_>>().join(":"));
|
||||
options.preview_window = preview_window_joined.as_ref().map(|x| x.as_str());
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// initialize collector
|
||||
let item_reader_option = SkimItemReaderOption::default()
|
||||
.ansi(opts.is_present("ansi"))
|
||||
.delimiter(opts.values_of("delimiter").and_then(|vals| vals.last()).unwrap_or(""))
|
||||
.with_nth(opts.values_of("with-nth").and_then(|vals| vals.last()).unwrap_or(""))
|
||||
.nth(opts.values_of("nth").and_then(|vals| vals.last()).unwrap_or(""))
|
||||
.read0(opts.is_present("read0"))
|
||||
.show_error(opts.is_present("show-cmd-error"))
|
||||
.build();
|
||||
|
||||
let cmd_collector = Rc::new(RefCell::new(SkimItemReader::new(item_reader_option)));
|
||||
options.cmd_collector = cmd_collector.clone();
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// read in the history file
|
||||
let fz_query_histories = opts.values_of("history").and_then(|vals| vals.last());
|
||||
let cmd_query_histories = opts.values_of("cmd-history").and_then(|vals| vals.last());
|
||||
let query_history = fz_query_histories.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_else(|| vec![]);
|
||||
let cmd_history = cmd_query_histories.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_else(|| vec![]);
|
||||
|
||||
if fz_query_histories.is_some() || cmd_query_histories.is_some() {
|
||||
options.query_history = &query_history;
|
||||
options.cmd_history = &cmd_history;
|
||||
// bind ctrl-n and ctrl-p to handle history
|
||||
options.bind.insert(0, "ctrl-p:previous-history,ctrl-n:next-history");
|
||||
/// Returns `None` if the popup should not open, otherwise run the popup and return the result
|
||||
#[cfg(unix)]
|
||||
#[allow(clippy::option_option)]
|
||||
fn check_and_run_popup(opts: &SkimOptions) -> Option<Option<SkimOutput>> {
|
||||
if opts.popup.is_some() && popup::check_env() {
|
||||
Some(crate::popup::run_with(opts))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
#[allow(clippy::option_option)]
|
||||
fn check_and_run_popup(_opts: &SkimOptions) -> Option<Option<SkimOutput>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn sk_main(mut opts: SkimOptions) -> Result<i32> {
|
||||
let reader_opts = SkimItemReaderOption::from_options(&opts);
|
||||
let cmd_collector = Rc::new(RefCell::new(SkimItemReader::new(reader_opts)));
|
||||
opts.cmd_collector = cmd_collector.clone() as Rc<RefCell<dyn CommandCollector>>;
|
||||
|
||||
let cmd_history = opts.cmd_history.clone();
|
||||
let cmd_history_size = opts.cmd_history_size;
|
||||
let cmd_history_file = opts.cmd_history_file.clone();
|
||||
|
||||
let query_history = opts.query_history.clone();
|
||||
let history_size = opts.history_size;
|
||||
let history_file = opts.history_file.clone();
|
||||
//------------------------------------------------------------------------------
|
||||
// handle pre-selection options
|
||||
let pre_select_n: Option<usize> = opts.values_of("pre-select-n").and_then(|vals| vals.last()).and_then(|s| s.parse().ok());
|
||||
let pre_select_pat = opts.values_of("pre-select-pat").and_then(|vals| vals.last());
|
||||
let pre_select_items: Option<Vec<String>> = opts.values_of("pre-select-items").map(|vals| vals.flat_map(|m|m.split('\n')).map(|s|s.to_string()).collect());
|
||||
let pre_select_file = opts.values_of("pre-select-file").and_then(|vals| vals.last());
|
||||
|
||||
if pre_select_n.is_some() || pre_select_pat.is_some() || pre_select_items.is_some() || pre_select_file.is_some() {
|
||||
let first_n = pre_select_n.unwrap_or(0);
|
||||
let pattern = pre_select_pat.unwrap_or("");
|
||||
let preset_items = pre_select_items.unwrap_or(vec![]);
|
||||
let preset_file = pre_select_file.and_then(|filename| read_file_lines(filename).ok()).unwrap_or_else(|| vec![]);
|
||||
|
||||
let selector = DefaultSkimSelector::default()
|
||||
.first_n(first_n)
|
||||
.regex(pattern)
|
||||
.preset(preset_items)
|
||||
.preset(preset_file);
|
||||
options.selector = Some(Rc::new(selector));
|
||||
}
|
||||
|
||||
let options = options;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
let bin_options = BinOptionsBuilder::default()
|
||||
.filter(opts.values_of("filter").and_then(|vals| vals.last()))
|
||||
.print_query(opts.is_present("print-query"))
|
||||
.print_cmd(opts.is_present("print-cmd"))
|
||||
.output_ending(if opts.is_present("print0") { "\0" } else { "\n" })
|
||||
.build()
|
||||
.expect("");
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// read from pipe or command
|
||||
let rx_item = if atty::isnt(atty::Stream::Stdin) {
|
||||
let rx_item = cmd_collector.borrow().of_bufread(BufReader::new(std::io::stdin()));
|
||||
Some(rx_item)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// filter mode
|
||||
if opts.is_present("filter") {
|
||||
return filter(&bin_options, &options, rx_item);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
let output = Skim::run_with(&options, rx_item);
|
||||
if output.is_none() { // error
|
||||
return Ok(135);
|
||||
}
|
||||
let bin_options = BinOptions::from_opts(&opts);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// output
|
||||
let output = output.unwrap();
|
||||
if output.is_abort {
|
||||
|
||||
let Some(result) = check_and_run_popup(&opts).unwrap_or_else(|| {
|
||||
// read from pipe or command
|
||||
let rx_item = if io::stdin().is_terminal() || (opts.interactive && opts.cmd.is_some()) {
|
||||
None
|
||||
} else {
|
||||
let rx_item = cmd_collector.borrow().of_bufread(BufReader::new(std::io::stdin()));
|
||||
Some(rx_item)
|
||||
};
|
||||
Skim::run_with(opts, rx_item).ok()
|
||||
}) else {
|
||||
return Ok(135);
|
||||
};
|
||||
log::debug!("result: {result:?}");
|
||||
|
||||
if result.is_abort {
|
||||
return Ok(130);
|
||||
}
|
||||
|
||||
// output query
|
||||
if bin_options.print_query {
|
||||
write!(stdout, "{}{}", output.query, bin_options.output_ending)?;
|
||||
}
|
||||
|
||||
if bin_options.print_cmd {
|
||||
write!(stdout, "{}{}", output.cmd, bin_options.output_ending)?;
|
||||
}
|
||||
|
||||
if opts.is_present("expect") {
|
||||
match output.final_event {
|
||||
Event::EvActAccept(Some(accept_key)) => {
|
||||
write!(stdout, "{}{}", accept_key, bin_options.output_ending)?;
|
||||
}
|
||||
Event::EvActAccept(None) => {
|
||||
write!(stdout, "{}", bin_options.output_ending)?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for item in output.selected_items.iter() {
|
||||
write!(stdout, "{}{}", item.output(), bin_options.output_ending)?;
|
||||
// Output — use a large BufWriter to batch all writes into a few syscalls
|
||||
// instead of one syscall per item (Rust's default LineWriter flushes on \n).
|
||||
{
|
||||
let stdout = io::stdout();
|
||||
let mut out = BufWriter::with_capacity(1 << 20, stdout.lock());
|
||||
result.write_output(&mut out, &bin_options)?;
|
||||
out.flush()?;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// write the history with latest item
|
||||
if let Some(file) = fz_query_histories {
|
||||
let limit = opts.values_of("history-size").and_then(|vals| vals.last())
|
||||
.and_then(|size| size.parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_HISTORY_SIZE);
|
||||
write_history_to_file(&query_history, &output.query, limit, file)?;
|
||||
if let Some(file) = history_file {
|
||||
let limit = history_size;
|
||||
write_history_to_file(&query_history, &result.query, limit, &file)?;
|
||||
}
|
||||
|
||||
if let Some(file) = cmd_query_histories {
|
||||
let limit = opts.values_of("cmd-history-size").and_then(|vals| vals.last())
|
||||
.and_then(|size| size.parse::<usize>().ok())
|
||||
.unwrap_or(DEFAULT_HISTORY_SIZE);
|
||||
write_history_to_file(&cmd_history, &output.cmd, limit, file)?;
|
||||
if let Some(file) = cmd_history_file {
|
||||
let limit = cmd_history_size;
|
||||
write_history_to_file(&cmd_history, &result.cmd, limit, &file)?;
|
||||
}
|
||||
|
||||
Ok(if output.selected_items.is_empty() { 1 } else { 0 })
|
||||
}
|
||||
|
||||
fn parse_options<'a>(options: &'a ArgMatches) -> SkimOptions<'a> {
|
||||
SkimOptionsBuilder::default()
|
||||
.color(options.values_of("color").and_then(|vals| vals.last()))
|
||||
.min_height(options.values_of("min-height").and_then(|vals| vals.last()))
|
||||
.no_height(options.is_present("no-height"))
|
||||
.height(options.values_of("height").and_then(|vals| vals.last()))
|
||||
.margin(options.values_of("margin").and_then(|vals| vals.last()))
|
||||
.preview(options.values_of("preview").and_then(|vals| vals.last()))
|
||||
.cmd(options.values_of("cmd").and_then(|vals| vals.last()))
|
||||
.query(options.values_of("query").and_then(|vals| vals.last()))
|
||||
.cmd_query(options.values_of("cmd-query").and_then(|vals| vals.last()))
|
||||
.interactive(options.is_present("interactive"))
|
||||
.prompt(options.values_of("prompt").and_then(|vals| vals.last()))
|
||||
.cmd_prompt(options.values_of("cmd-prompt").and_then(|vals| vals.last()))
|
||||
.bind(
|
||||
options
|
||||
.values_of("bind")
|
||||
.map(|x| x.collect::<Vec<_>>())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.expect(options.values_of("expect").map(|x| x.collect::<Vec<_>>().join(",")))
|
||||
.multi(if options.is_present("no-multi") {
|
||||
false
|
||||
} else {
|
||||
options.is_present("multi")
|
||||
})
|
||||
.layout(options.values_of("layout").and_then(|vals| vals.last()).unwrap_or(""))
|
||||
.reverse(options.is_present("reverse"))
|
||||
.no_hscroll(options.is_present("no-hscroll"))
|
||||
.no_mouse(options.is_present("no-mouse"))
|
||||
.no_clear(options.is_present("no-clear"))
|
||||
.tabstop(options.values_of("tabstop").and_then(|vals| vals.last()))
|
||||
.tiebreak(options.values_of("tiebreak").map(|x| x.collect::<Vec<_>>().join(",")))
|
||||
.tac(options.is_present("tac"))
|
||||
.nosort(options.is_present("no-sort"))
|
||||
.exact(options.is_present("exact"))
|
||||
.regex(options.is_present("regex"))
|
||||
.delimiter(options.values_of("delimiter").and_then(|vals| vals.last()))
|
||||
.inline_info(options.is_present("inline-info"))
|
||||
.header(options.values_of("header").and_then(|vals| vals.last()))
|
||||
.header_lines(
|
||||
options
|
||||
.values_of("header-lines")
|
||||
.and_then(|vals| vals.last())
|
||||
.map(|s| s.parse::<usize>().unwrap_or(0))
|
||||
.unwrap_or(0),
|
||||
)
|
||||
.layout(options.values_of("layout").and_then(|vals| vals.last()).unwrap_or(""))
|
||||
.algorithm(FuzzyAlgorithm::of(
|
||||
options.values_of("algorithm").and_then(|vals| vals.last()).unwrap(),
|
||||
))
|
||||
.case(match options.value_of("case") {
|
||||
Some("smart") => CaseMatching::Smart,
|
||||
Some("ignore") => CaseMatching::Ignore,
|
||||
_ => CaseMatching::Respect,
|
||||
})
|
||||
.keep_right(options.is_present("keep-right"))
|
||||
.skip_to_pattern(
|
||||
options
|
||||
.values_of("skip-to-pattern")
|
||||
.and_then(|vals| vals.last())
|
||||
.unwrap_or(""),
|
||||
)
|
||||
.select1(options.is_present("select-1"))
|
||||
.exit0(options.is_present("exit-0"))
|
||||
.sync(options.is_present("sync"))
|
||||
.no_clear_if_empty(options.is_present("no-clear-if-empty"))
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn read_file_lines(filename: &str) -> Result<Vec<String>, std::io::Error> {
|
||||
let file = File::open(filename)?;
|
||||
let ret = BufReader::new(file).lines().collect();
|
||||
debug!("file content: {:?}", ret);
|
||||
ret
|
||||
Ok(i32::from(result.selected_items.is_empty()))
|
||||
}
|
||||
|
||||
fn write_history_to_file(
|
||||
|
|
@ -473,11 +208,11 @@ fn write_history_to_file(
|
|||
limit: usize,
|
||||
filename: &str,
|
||||
) -> Result<(), std::io::Error> {
|
||||
if orig_history.last().map(|l| l.as_str()) == Some(latest) {
|
||||
if orig_history.last().map(String::as_str) == Some(latest) {
|
||||
// no point of having at the end of the history 5x the same command...
|
||||
return Ok(());
|
||||
}
|
||||
let additional_lines = if latest.trim().is_empty() { 0 } else { 1 };
|
||||
let additional_lines = usize::from(!latest.trim().is_empty());
|
||||
let start_index = if orig_history.len() + additional_lines > limit {
|
||||
orig_history.len() + additional_lines - limit
|
||||
} else {
|
||||
|
|
@ -493,69 +228,51 @@ fn write_history_to_file(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Builder)]
|
||||
pub struct BinOptions<'a> {
|
||||
filter: Option<&'a str>,
|
||||
output_ending: &'a str,
|
||||
print_query: bool,
|
||||
print_cmd: bool,
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[cfg_attr(coverage, coverage(off))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
pub fn filter(
|
||||
bin_option: &BinOptions,
|
||||
options: &SkimOptions,
|
||||
source: Option<SkimItemReceiver>,
|
||||
) -> Result<i32, std::io::Error> {
|
||||
let mut stdout = std::io::stdout();
|
||||
|
||||
let default_command = match env::var("SKIM_DEFAULT_COMMAND").as_ref().map(String::as_ref) {
|
||||
Ok("") | Err(_) => "find .".to_owned(),
|
||||
Ok(val) => val.to_owned(),
|
||||
};
|
||||
let query = bin_option.filter.unwrap_or(&"");
|
||||
let cmd = options.cmd.unwrap_or(&default_command);
|
||||
|
||||
// output query
|
||||
if bin_option.print_query {
|
||||
write!(stdout, "{}{}", query, bin_option.output_ending)?;
|
||||
fn read(path: &std::path::Path) -> String {
|
||||
std::fs::read_to_string(path).unwrap_or_default()
|
||||
}
|
||||
|
||||
if bin_option.print_cmd {
|
||||
write!(stdout, "{}{}", cmd, bin_option.output_ending)?;
|
||||
#[test]
|
||||
fn write_history_appends_latest_entry() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("hist");
|
||||
let file_str = file.to_str().unwrap();
|
||||
write_history_to_file(&["a".to_string(), "b".to_string()], "c", 10, file_str).unwrap();
|
||||
assert_eq!(read(&file), "a\nb\nc");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// matcher
|
||||
let engine_factory: Box<dyn MatchEngineFactory> = if options.regex {
|
||||
Box::new(RegexEngineFactory::builder())
|
||||
} else {
|
||||
let fuzzy_engine_factory = ExactOrFuzzyEngineFactory::builder()
|
||||
.fuzzy_algorithm(options.algorithm)
|
||||
.exact_mode(options.exact)
|
||||
.build();
|
||||
Box::new(AndOrEngineFactory::new(fuzzy_engine_factory))
|
||||
};
|
||||
#[test]
|
||||
fn write_history_skips_duplicate_of_last() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("hist");
|
||||
let file_str = file.to_str().unwrap();
|
||||
// The latest equals the last entry → nothing is written, no file created.
|
||||
write_history_to_file(&["a".to_string(), "b".to_string()], "b", 10, file_str).unwrap();
|
||||
assert!(!file.exists());
|
||||
}
|
||||
|
||||
let engine = engine_factory.create_engine_with_case(query, options.case);
|
||||
#[test]
|
||||
fn write_history_truncates_to_limit() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("hist");
|
||||
let file_str = file.to_str().unwrap();
|
||||
// limit 2 with 3 existing + 1 new keeps only the newest entries.
|
||||
write_history_to_file(&["a".to_string(), "b".to_string(), "c".to_string()], "d", 2, file_str).unwrap();
|
||||
assert_eq!(read(&file), "c\nd");
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// start
|
||||
let components_to_stop = Arc::new(AtomicUsize::new(0));
|
||||
|
||||
let stream_of_item = source.unwrap_or_else(|| {
|
||||
let cmd_collector = options.cmd_collector.clone();
|
||||
let (ret, _control) = cmd_collector.borrow_mut().invoke(cmd, components_to_stop);
|
||||
ret
|
||||
});
|
||||
|
||||
let mut num_matched = 0;
|
||||
stream_of_item
|
||||
.into_iter()
|
||||
.filter_map(|item| engine.match_item(item.clone()).map(|result| (item, result)))
|
||||
.try_for_each(|(item, _match_result)| {
|
||||
num_matched += 1;
|
||||
write!(stdout, "{}{}", item.output(), bin_option.output_ending)
|
||||
})?;
|
||||
|
||||
Ok(if num_matched == 0 { 1 } else { 0 })
|
||||
#[test]
|
||||
fn write_history_empty_latest_does_not_count_towards_limit() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("hist");
|
||||
let file_str = file.to_str().unwrap();
|
||||
// An empty latest adds 0 to the length, so no truncation occurs at limit 3.
|
||||
write_history_to_file(&["a".to_string(), "b".to_string(), "c".to_string()], "", 3, file_str).unwrap();
|
||||
assert_eq!(read(&file), "a\nb\nc\n");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
423
src/binds.rs
Normal file
423
src/binds.rs
Normal file
|
|
@ -0,0 +1,423 @@
|
|||
//! Key binding configuration and parsing.
|
||||
//!
|
||||
//! This module provides utilities for parsing and managing keyboard shortcuts
|
||||
//! and their associated actions in skim.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use eyre::{Result, eyre};
|
||||
|
||||
use crate::tui::actions::{self, Action};
|
||||
|
||||
/// Synthetic events that skim fires internally and that can be bound to actions
|
||||
/// via the keymap, exactly like a real key press.
|
||||
///
|
||||
/// The keymap is keyed by crossterm's [`KeyEvent`], which cannot express
|
||||
/// "the query changed" or "reading finished" directly. Each variant is
|
||||
/// therefore represented *transparently* as a reserved function-key code in the
|
||||
/// high-`F` range (`F(248)`–`F(255)`) that no real terminal ever emits.
|
||||
/// Giving these reserved codes named variants keeps them in one place instead
|
||||
/// of scattering magic function-key literals across the codebase, and lets
|
||||
/// [`parse_key`] accept every friendly event name.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub enum SkimEvent {
|
||||
/// Fired once, when skim has started up and entered its event loop.
|
||||
Start,
|
||||
/// Fired when the reader finishes producing items (once per read; a
|
||||
/// `reload` starts a new read and fires it again).
|
||||
Load,
|
||||
/// Fired whenever the query changes.
|
||||
Change,
|
||||
/// Fired when filtering for the current query completes and the result
|
||||
/// list is ready.
|
||||
Result,
|
||||
/// Fired when the focused item changes (cursor movement or a result update).
|
||||
Focus,
|
||||
/// Fired when a completed search yields no matches.
|
||||
Zero,
|
||||
/// Fired when a completed search yields exactly one match.
|
||||
One,
|
||||
/// Fired after two left mouse-button presses no more than 500 ms apart.
|
||||
DoubleClick,
|
||||
}
|
||||
|
||||
impl SkimEvent {
|
||||
/// The reserved [`KeyCode`] used to route this event through the keymap.
|
||||
#[must_use]
|
||||
pub const fn key_code(self) -> KeyCode {
|
||||
match self {
|
||||
SkimEvent::Change => KeyCode::F(255),
|
||||
SkimEvent::Start => KeyCode::F(254),
|
||||
SkimEvent::Load => KeyCode::F(253),
|
||||
SkimEvent::Result => KeyCode::F(252),
|
||||
SkimEvent::Focus => KeyCode::F(251),
|
||||
SkimEvent::Zero => KeyCode::F(250),
|
||||
SkimEvent::One => KeyCode::F(249),
|
||||
SkimEvent::DoubleClick => KeyCode::F(248),
|
||||
}
|
||||
}
|
||||
|
||||
/// The reserved [`KeyEvent`] used to route this event through the keymap.
|
||||
#[must_use]
|
||||
pub const fn key_event(self) -> KeyEvent {
|
||||
KeyEvent::new(self.key_code(), KeyModifiers::NONE)
|
||||
}
|
||||
|
||||
/// Parses an event name (`start`, `load`, `change`) into a [`SkimEvent`].
|
||||
///
|
||||
/// Returns `None` if the name is not a recognised event.
|
||||
#[must_use]
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"start" => Some(SkimEvent::Start),
|
||||
"load" => Some(SkimEvent::Load),
|
||||
"change" => Some(SkimEvent::Change),
|
||||
"result" => Some(SkimEvent::Result),
|
||||
"focus" => Some(SkimEvent::Focus),
|
||||
"zero" => Some(SkimEvent::Zero),
|
||||
"one" => Some(SkimEvent::One),
|
||||
"double-click" => Some(SkimEvent::DoubleClick),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SkimEvent> for KeyEvent {
|
||||
fn from(event: SkimEvent) -> Self {
|
||||
event.key_event()
|
||||
}
|
||||
}
|
||||
|
||||
/// A map of key events to their associated actions
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyMap(pub HashMap<KeyEvent, Vec<Action>>);
|
||||
|
||||
impl Deref for KeyMap {
|
||||
type Target = HashMap<KeyEvent, Vec<Action>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
impl DerefMut for KeyMap {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for KeyMap {
|
||||
fn from(value: &str) -> Self {
|
||||
parse_keymaps(split_top_level(value, ',').into_iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KeyMap {
|
||||
fn default() -> Self {
|
||||
get_default_key_map()
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyMap {
|
||||
/// Adds keymaps from a comma-separated string.
|
||||
pub(crate) fn add_keymaps_str(&mut self, source: &str) {
|
||||
self.add_keymaps(split_top_level(source, ',').into_iter());
|
||||
}
|
||||
|
||||
/// Adds keymaps from the source, parsing them using `parse_keymap`
|
||||
pub fn add_keymaps<'a, T>(&mut self, source: T)
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
for map in source {
|
||||
if let Ok((key, action_chain)) = parse_keymap(map) {
|
||||
self.bind(key, action_chain)
|
||||
.unwrap_or_else(|err| debug!("Failed to bind key {map}: {err}"));
|
||||
} else {
|
||||
debug!("Failed to parse key: {map}");
|
||||
}
|
||||
}
|
||||
}
|
||||
fn bind(&mut self, key: &str, action_chain: Vec<Action>) -> Result<()> {
|
||||
let key = parse_key(key)?;
|
||||
|
||||
// remove the key for existing keymap;
|
||||
let _ = self.remove(&key);
|
||||
self.entry(key).or_insert(action_chain);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the default key bindings for skim
|
||||
#[rustfmt::skip]
|
||||
#[must_use]
|
||||
pub fn get_default_key_map() -> KeyMap {
|
||||
let mut ret = HashMap::new();
|
||||
|
||||
ret.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::NONE), vec![Action::Down(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE), vec![Action::Up(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE), vec![Action::PageUp(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE), vec![Action::PageDown(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::End, KeyModifiers::NONE), vec![Action::EndOfLine]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE), vec![Action::BeginningOfLine]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Delete, KeyModifiers::NONE), vec![Action::DeleteChar]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE), vec![Action::Toggle, Action::Down(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::BackTab, KeyModifiers::all()), vec![Action::Toggle, Action::Up(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE), vec![Action::Abort]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), vec![Action::Accept(None)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE), vec![Action::BackwardChar]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::NONE), vec![Action::ForwardChar]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE), vec![Action::BackwardDeleteChar]);
|
||||
ret.insert(SkimEvent::DoubleClick.key_event(), vec![Action::Accept(None)]);
|
||||
|
||||
|
||||
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT), vec![Action::BackwardWord]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT), vec![Action::ForwardWord]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT), vec![Action::PreviewUp(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT), vec![Action::PreviewDown(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT), vec![Action::Toggle, Action::Up(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::BackTab, KeyModifiers::SHIFT), vec![Action::Toggle, Action::Up(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Home, KeyModifiers::SHIFT), vec![Action::BeginningOfLine]);
|
||||
|
||||
|
||||
ret.insert(KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL), vec![Action::BackwardWord]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL), vec![Action::ForwardWord]);
|
||||
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL), vec![Action::BeginningOfLine]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::CONTROL), vec![Action::BackwardChar]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL), vec![Action::Abort]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::CONTROL), vec![Action::Abort]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL), vec![Action::EndOfLine]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::CONTROL), vec![Action::ForwardChar]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('g'), KeyModifiers::CONTROL), vec![Action::Abort]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL), vec![Action::BackwardDeleteChar]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL), vec![Action::Down(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('k'), KeyModifiers::CONTROL), vec![Action::Up(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL), vec![Action::ClearScreen]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL), vec![Action::Down(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL), vec![Action::Up(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL), vec![Action::ToggleInteractive]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL), vec![Action::RotateMode]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('u'), KeyModifiers::CONTROL), vec![Action::UnixLineDiscard]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL), vec![Action::UnixWordRubout]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('y'), KeyModifiers::CONTROL), vec![Action::Yank]);
|
||||
|
||||
|
||||
ret.insert(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT), vec![Action::BackwardKillWord]);
|
||||
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::ALT), vec![Action::BackwardWord]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::ALT), vec![Action::KillWord]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('f'), KeyModifiers::ALT), vec![Action::ForwardWord]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT), vec![Action::ScrollLeft(1)]);
|
||||
ret.insert(KeyEvent::new(KeyCode::Char('l'), KeyModifiers::ALT), vec![Action::ScrollRight(1)]);
|
||||
|
||||
KeyMap(ret)
|
||||
}
|
||||
|
||||
/// Parses a key str into a crossterm `KeyEvent`.
|
||||
///
|
||||
/// In addition to keyboard names, accepts all names recognized by
|
||||
/// [`SkimEvent::from_name`], including `change`, `start`, and `double-click`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the key string is empty, contains an unknown modifier,
|
||||
/// or does not correspond to a recognised key name.
|
||||
pub fn parse_key(key: &str) -> Result<KeyEvent> {
|
||||
if key.is_empty() {
|
||||
return Err(eyre!("Cannot parse empty key"));
|
||||
}
|
||||
if let Some(event) = SkimEvent::from_name(key) {
|
||||
return Ok(event.key_event());
|
||||
}
|
||||
let parts = key.split('-').collect::<Vec<&str>>();
|
||||
let mut mods = KeyModifiers::NONE;
|
||||
|
||||
if parts.len() > 1 {
|
||||
let mod_strs = &parts[..parts.len() - 1];
|
||||
for mod_str in mod_strs {
|
||||
mods |= match *mod_str {
|
||||
"ctrl" => KeyModifiers::CONTROL,
|
||||
"alt" => KeyModifiers::ALT,
|
||||
"shift" => KeyModifiers::SHIFT,
|
||||
s => return Err(eyre!("Failed to parse {} as key modifier", s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
let key = parts.last().unwrap_or(&"").to_string();
|
||||
|
||||
let keycode: KeyCode;
|
||||
if key.len() == 1 {
|
||||
let char = key.chars().next().unwrap_or_default();
|
||||
if char.is_uppercase() {
|
||||
mods |= KeyModifiers::SHIFT;
|
||||
keycode = KeyCode::Char(char.to_ascii_lowercase());
|
||||
} else {
|
||||
keycode = KeyCode::Char(char);
|
||||
}
|
||||
} else if let Some(f) = key.strip_prefix('f')
|
||||
&& let Ok(f_index) = f.parse::<u8>()
|
||||
{
|
||||
// A function key like `f10`. If the suffix isn't numeric (e.g. `focus`,
|
||||
// `first`), fall through to the named-key / event matching below.
|
||||
keycode = KeyCode::F(f_index);
|
||||
} else {
|
||||
keycode = match key.as_str() {
|
||||
"space" => KeyCode::Char(' '),
|
||||
"enter" => KeyCode::Enter,
|
||||
"bspace" | "bs" => KeyCode::Backspace,
|
||||
"up" => KeyCode::Up,
|
||||
"down" => KeyCode::Down,
|
||||
"left" => KeyCode::Left,
|
||||
"right" => KeyCode::Right,
|
||||
"tab" => KeyCode::Tab,
|
||||
"btab" => KeyCode::BackTab,
|
||||
"esc" => KeyCode::Esc,
|
||||
"home" => KeyCode::Home,
|
||||
"end" => KeyCode::End,
|
||||
"pgup" => KeyCode::PageUp,
|
||||
"pgdown" => KeyCode::PageDown,
|
||||
s => match SkimEvent::from_name(s) {
|
||||
Some(event) => event.key_code(),
|
||||
None => return Err(eyre!("Unknown key {}", s)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
debug!("parsed key {keycode:?} and mods {mods:?}");
|
||||
|
||||
Ok(KeyEvent::new(keycode, mods))
|
||||
}
|
||||
|
||||
/// Parse an iterator of keymaps into a `KeyMap`
|
||||
pub fn parse_keymaps<'a, T>(maps: T) -> KeyMap
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
let mut res = KeyMap::default();
|
||||
res.add_keymaps(maps);
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn split_top_level(value: &str, separator: char) -> Vec<&str> {
|
||||
let mut depth = 0_u32;
|
||||
let mut start = 0;
|
||||
let mut parts = Vec::new();
|
||||
|
||||
for (index, ch) in value.char_indices() {
|
||||
match ch {
|
||||
'(' => depth += 1,
|
||||
')' => depth = depth.saturating_sub(1),
|
||||
_ if ch == separator && depth == 0 => {
|
||||
parts.push(&value[start..index]);
|
||||
start = index + ch.len_utf8();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
parts.push(&value[start..]);
|
||||
parts
|
||||
}
|
||||
|
||||
/// Parses follow-up action bindings from raw `--bind` specs.
|
||||
///
|
||||
/// Any action can be bound as if it were an event: when the "key" of a bind is
|
||||
/// not a real key but is a known action name, the bound chain becomes a
|
||||
/// *follow-up* that runs right after that action. For example `reload:first`
|
||||
/// queues `first` immediately after a `reload`. The returned map is keyed by the
|
||||
/// action's canonical name (see [`Action::name`](crate::tui::actions::Action::name)),
|
||||
/// so it can be looked up directly from the action that just ran.
|
||||
///
|
||||
/// Keys take precedence: if the "key" resolves to a real key it is left to the
|
||||
/// key map, so a name shared by a key and an action (e.g. `up`) always binds the
|
||||
/// key. To target the action in that case, prefix it with `act-` (`act-up`).
|
||||
#[must_use]
|
||||
pub fn parse_action_binds<'a, T>(maps: T) -> HashMap<String, Vec<Action>>
|
||||
where
|
||||
T: Iterator<Item = &'a str>,
|
||||
{
|
||||
let mut res = HashMap::new();
|
||||
for map in maps {
|
||||
let Some((key, chain)) = map.split_once(':') else {
|
||||
continue;
|
||||
};
|
||||
// Keys win: anything that parses as a real key is not an action trigger.
|
||||
if parse_key(key).is_ok() {
|
||||
continue;
|
||||
}
|
||||
let Some(name) = action_trigger_name(key) else {
|
||||
debug!("Ignoring bind `{map}`: `{key}` is neither a key nor an action");
|
||||
continue;
|
||||
};
|
||||
match parse_action_chain(chain) {
|
||||
Ok(actions) => {
|
||||
res.insert(name.to_string(), actions);
|
||||
}
|
||||
Err(err) => debug!("Ignoring bind `{map}`: invalid action chain `{chain}`: {err}"),
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
/// Resolves a bind trigger to the canonical name of the action it targets
|
||||
/// (see [`Action::name`](crate::tui::actions::Action::name)). `act-<name>`
|
||||
/// explicitly targets the action `<name>`, even when `<name>` is also a key;
|
||||
/// without the prefix, a bare action name works too. Returns `None` if the
|
||||
/// name is not a known action.
|
||||
///
|
||||
/// This performs pure name resolution: callers that want "keys win" semantics
|
||||
/// (e.g. [`parse_action_binds`]) must check [`parse_key`] first.
|
||||
pub(crate) fn action_trigger_name(trigger: &str) -> Option<&'static str> {
|
||||
let action_name = trigger.strip_prefix("act-").unwrap_or(trigger);
|
||||
// Some actions require an argument when executed, but their canonical
|
||||
// name is still valid as a trigger. `()` supplies the parser's empty
|
||||
// placeholder solely for name validation.
|
||||
let action = actions::parse_action(action_name).or_else(|| actions::parse_action(&format!("{action_name}()")))?;
|
||||
Some(action.name())
|
||||
}
|
||||
|
||||
/// Parses an action chain, separated by '+'s into the corresponding actions
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the action chain is empty or contains only unknown actions.
|
||||
pub fn parse_action_chain(action_chain: &str) -> Result<Vec<Action>> {
|
||||
let mut actions: Vec<Action> = vec![];
|
||||
let mut split = split_top_level(action_chain, '+').into_iter();
|
||||
|
||||
while let Some(mut s) = split.next().map(String::from) {
|
||||
if (s.starts_with("if-") || s.ends_with('{'))
|
||||
&& let Some(otherwise) = split.next()
|
||||
{
|
||||
s += &(String::from("+") + otherwise);
|
||||
}
|
||||
if let Some(act) = actions::parse_action(&s) {
|
||||
actions.push(act);
|
||||
}
|
||||
}
|
||||
if actions.is_empty() {
|
||||
Err(eyre!("Empty action chain or unknown action `{}`", action_chain))
|
||||
} else {
|
||||
Ok(actions)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a single keymap and return the key and action(s)
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error if the string is empty, missing a colon separator, or the
|
||||
/// action chain cannot be parsed.
|
||||
pub fn parse_keymap(key_action: &str) -> Result<(&str, Vec<Action>)> {
|
||||
if key_action.is_empty() {
|
||||
return Err(eyre!("Got an empty keybind, skipping"));
|
||||
}
|
||||
debug!("got key_action: {key_action:?}");
|
||||
let (key, action_chain) = key_action
|
||||
.split_once(':')
|
||||
.ok_or(eyre!("Failed to parse {} as key and action", key_action))?;
|
||||
debug!("parsed key_action: {key:?}: {action_chain:?}");
|
||||
Ok((key, parse_action_chain(action_chain)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "binds_tests.rs"]
|
||||
mod tests;
|
||||
309
src/binds_tests.rs
Normal file
309
src/binds_tests.rs
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
use super::*;
|
||||
use actions::Action::*;
|
||||
#[test]
|
||||
fn test_parse_action_chain() {
|
||||
let parsed = parse_action_chain(
|
||||
"execute-silent:1 {}+execute-silent:2 {+}+execute-silent:3 {+n}+reload+if-query-empty:reload+up",
|
||||
);
|
||||
assert!(parsed.is_ok());
|
||||
let res = parsed.unwrap();
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
ExecuteSilent("1 {}".into()),
|
||||
ExecuteSilent("2 {+}".into()),
|
||||
ExecuteSilent("3 {+n}".into()),
|
||||
Reload(None),
|
||||
IfQueryEmpty("reload".into(), Some("up".into())),
|
||||
]
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_parse_key() {
|
||||
assert_eq!(
|
||||
parse_key("a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("A").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SHIFT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("alt-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("alt-A").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT | KeyModifiers::SHIFT)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("alt-shift-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::ALT | KeyModifiers::SHIFT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("ctrl-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("ctrl-A").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("ctrl-shift-a").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::SHIFT)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("f10").unwrap(),
|
||||
KeyEvent::new(KeyCode::F(10), KeyModifiers::empty())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse_key("space").unwrap(),
|
||||
KeyEvent::new(KeyCode::Char(' '), KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("enter").unwrap(),
|
||||
KeyEvent::new(KeyCode::Enter, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("bspace").unwrap(),
|
||||
KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("bs").unwrap(),
|
||||
KeyEvent::new(KeyCode::Backspace, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("up").unwrap(),
|
||||
KeyEvent::new(KeyCode::Up, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("down").unwrap(),
|
||||
KeyEvent::new(KeyCode::Down, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("left").unwrap(),
|
||||
KeyEvent::new(KeyCode::Left, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("right").unwrap(),
|
||||
KeyEvent::new(KeyCode::Right, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("tab").unwrap(),
|
||||
KeyEvent::new(KeyCode::Tab, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("btab").unwrap(),
|
||||
KeyEvent::new(KeyCode::BackTab, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("esc").unwrap(),
|
||||
KeyEvent::new(KeyCode::Esc, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("home").unwrap(),
|
||||
KeyEvent::new(KeyCode::Home, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("end").unwrap(),
|
||||
KeyEvent::new(KeyCode::End, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("pgup").unwrap(),
|
||||
KeyEvent::new(KeyCode::PageUp, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("pgdown").unwrap(),
|
||||
KeyEvent::new(KeyCode::PageDown, KeyModifiers::empty())
|
||||
);
|
||||
assert_eq!(
|
||||
parse_key("change").unwrap(),
|
||||
KeyEvent::new(KeyCode::F(255), KeyModifiers::empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skim_event_name_roundtrip() {
|
||||
// Named events resolve to distinct reserved key events and back.
|
||||
for (name, event) in [
|
||||
("start", SkimEvent::Start),
|
||||
("load", SkimEvent::Load),
|
||||
("change", SkimEvent::Change),
|
||||
("result", SkimEvent::Result),
|
||||
("focus", SkimEvent::Focus),
|
||||
("zero", SkimEvent::Zero),
|
||||
("one", SkimEvent::One),
|
||||
("double-click", SkimEvent::DoubleClick),
|
||||
] {
|
||||
assert_eq!(SkimEvent::from_name(name), Some(event));
|
||||
assert_eq!(parse_key(name).unwrap(), KeyEvent::from(event));
|
||||
}
|
||||
// Unknown names are not events.
|
||||
assert_eq!(SkimEvent::from_name("nope"), None);
|
||||
// A binding referencing an event name resolves to an action chain.
|
||||
let keymap = KeyMap::from("start:first,load:last,change:first,double-click:accept");
|
||||
for event in [
|
||||
SkimEvent::Start,
|
||||
SkimEvent::Load,
|
||||
SkimEvent::Change,
|
||||
SkimEvent::DoubleClick,
|
||||
] {
|
||||
assert!(keymap.get(&event.key_event()).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_click_accepts_by_default() {
|
||||
assert_eq!(
|
||||
get_default_key_map().get(&SkimEvent::DoubleClick.key_event()),
|
||||
Some(&vec![Accept(None)])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_key_error_cases() {
|
||||
// Empty input.
|
||||
assert!(parse_key("").is_err());
|
||||
// Unknown modifier.
|
||||
assert!(parse_key("hyper-a").is_err());
|
||||
// Unknown key name.
|
||||
assert!(parse_key("notakey").is_err());
|
||||
// Invalid function-key index.
|
||||
assert!(parse_key("fXY").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_from_str_parses_bindings() {
|
||||
let keymap = KeyMap::from("ctrl-a:abort,enter:accept");
|
||||
// Both keys resolve to action chains.
|
||||
assert!(keymap.get(&parse_key("ctrl-a").unwrap()).is_some());
|
||||
assert!(keymap.get(&parse_key("enter").unwrap()).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_from_str_preserves_nested_bind_separators() {
|
||||
let keymap = KeyMap::from("ctrl-z:bind(ctrl-x:abort+up),ctrl-w:unbind(ctrl-x,ctrl-y)");
|
||||
|
||||
assert_eq!(
|
||||
keymap.get(&parse_key("ctrl-z").unwrap()),
|
||||
Some(&vec![Bind("ctrl-x:abort+up".into())])
|
||||
);
|
||||
assert_eq!(
|
||||
keymap.get(&parse_key("ctrl-w").unwrap()),
|
||||
Some(&vec![Unbind("ctrl-x,ctrl-y".into())])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_action_chain_preserves_nested_bind_chain() {
|
||||
assert_eq!(
|
||||
parse_action_chain("bind(ctrl-x:abort+up)").unwrap(),
|
||||
vec![Bind("ctrl-x:abort+up".into())]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_keymaps_collects_iterator() {
|
||||
let keymap = parse_keymaps(["ctrl-x:abort", "up:up"].into_iter());
|
||||
assert!(keymap.get(&parse_key("ctrl-x").unwrap()).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_binds_key_wins_over_action() {
|
||||
// A bare action name that is not a key binds the action as a follow-up.
|
||||
let binds = parse_action_binds(["first:last"].into_iter());
|
||||
assert_eq!(binds.get("first"), Some(&vec![Last]));
|
||||
|
||||
// A name that is also a real key (`up`) is left to the key map, so it is
|
||||
// NOT registered as an action trigger.
|
||||
let binds = parse_action_binds(["up:down"].into_iter());
|
||||
assert!(!binds.contains_key("up"));
|
||||
|
||||
// `act-` forces the action interpretation even for a key-shaped name.
|
||||
let binds = parse_action_binds(["act-up:down"].into_iter());
|
||||
assert_eq!(binds.get("up"), Some(&vec![Down(1)]));
|
||||
|
||||
// Actions that require arguments when executed are still valid triggers.
|
||||
let binds = parse_action_binds(
|
||||
[
|
||||
"act-add-char:last",
|
||||
"act-execute:last",
|
||||
"act-execute-silent:last",
|
||||
"act-set-preview-cmd:last",
|
||||
"act-set-query:last",
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
for name in ["add-char", "execute", "execute-silent", "set-preview-cmd", "set-query"] {
|
||||
assert_eq!(binds.get(name), Some(&vec![Last]), "missing trigger `{name}`");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_binds_parse_suppress_chain() {
|
||||
// `suppress` is parsed like any other action and kept in the chain.
|
||||
let binds = parse_action_binds(["act-up:suppress+down"].into_iter());
|
||||
assert_eq!(binds.get("up"), Some(&vec![Suppress, Down(1)]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_trigger_name_resolves_actions() {
|
||||
// `act-` targets the action explicitly; a bare action name works too.
|
||||
assert_eq!(action_trigger_name("act-up"), Some("up"));
|
||||
assert_eq!(action_trigger_name("first"), Some("first"));
|
||||
// Argument-taking actions resolve by name alone.
|
||||
assert_eq!(action_trigger_name("act-execute"), Some("execute"));
|
||||
// Unknown names are not triggers.
|
||||
assert_eq!(action_trigger_name("nope"), None);
|
||||
assert_eq!(action_trigger_name("act-nope"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_binds_invalid_specs_are_skipped() {
|
||||
// An unknown trigger and an invalid chain are both dropped (with a debug
|
||||
// log) without affecting valid binds in the same list.
|
||||
let binds = parse_action_binds(["nokey:last", "act-up:not-an-action", "first:last"].into_iter());
|
||||
assert_eq!(binds.len(), 1);
|
||||
assert_eq!(binds.get("first"), Some(&vec![Last]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_binds_split_top_level_preserves_commas_in_args() {
|
||||
// A single `--bind` spec containing a comma inside `(...)` must not be split
|
||||
// there: `options.rs` uses `split_top_level(part, ',')` so the comma stays
|
||||
// part of the action argument instead of garbling the follow-up binding.
|
||||
let spec = "act-up:execute(echo a,b),first:last";
|
||||
let binds = parse_action_binds(split_top_level(spec, ',').into_iter());
|
||||
assert_eq!(binds.get("up"), Some(&vec![Execute(String::from("echo a,b"))]));
|
||||
assert_eq!(binds.get("first"), Some(&vec![Last]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_action_chain_unknown_is_error() {
|
||||
assert!(parse_action_chain("not-a-real-action").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_action_chain_accept_execute_reload_with_args() {
|
||||
// `accept:hello`, `execute(...)` and `reload(...)` parse to the expected actions.
|
||||
assert_eq!(
|
||||
parse_action_chain("accept:hello").unwrap(),
|
||||
vec![Accept(Some("hello".into()))]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_action_chain("execute(echo foo)").unwrap(),
|
||||
vec![Execute("echo foo".into())]
|
||||
);
|
||||
assert_eq!(
|
||||
parse_action_chain("reload(echo hello)").unwrap(),
|
||||
vec![Reload(Some("echo hello".into()))]
|
||||
);
|
||||
assert_eq!(parse_action_chain("reload").unwrap(), vec![Reload(None)]);
|
||||
}
|
||||
|
|
@ -28,10 +28,10 @@ impl MatchAllEngine {
|
|||
}
|
||||
|
||||
impl MatchEngine for MatchAllEngine {
|
||||
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
|
||||
let item_len = item.text().len();
|
||||
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
|
||||
let item_text = item.text();
|
||||
Some(MatchResult {
|
||||
rank: self.rank_builder.build_rank(0, 0, 0, item_len),
|
||||
rank: self.rank_builder.build_rank(0, 0, 0, &item_text),
|
||||
matched_range: MatchRange::ByteRange(0, 0),
|
||||
})
|
||||
}
|
||||
|
|
@ -42,3 +42,30 @@ impl Display for MatchAllEngine {
|
|||
write!(f, "Noop")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg_attr(coverage, coverage(off))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn matches_every_item_with_empty_range() {
|
||||
let engine = MatchAllEngine::builder().build();
|
||||
let result = engine.match_item(&"anything".to_string()).unwrap();
|
||||
assert_eq!(result.matched_range, MatchRange::ByteRange(0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rank_builder_override_is_used() {
|
||||
let engine = MatchAllEngine::builder()
|
||||
.rank_builder(Arc::new(RankBuilder::default()))
|
||||
.build();
|
||||
assert!(engine.match_item(&"x".to_string()).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_is_noop() {
|
||||
let engine = MatchAllEngine::builder().build();
|
||||
assert_eq!(format!("{engine}"), "Noop");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt::{Display, Error, Formatter};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::fuzzy_matcher::MatchIndices;
|
||||
use crate::{MatchEngine, MatchRange, MatchResult, SkimItem};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
|
@ -25,15 +25,14 @@ impl OrEngine {
|
|||
}
|
||||
|
||||
impl MatchEngine for OrEngine {
|
||||
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
|
||||
for engine in &self.engines {
|
||||
let result = engine.match_item(Arc::clone(&item));
|
||||
if result.is_some() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
|
||||
let result = self
|
||||
.engines
|
||||
.iter()
|
||||
.map(|e| e.match_item(item))
|
||||
.max_by_key(|res| res.as_ref().map(|matched| matched.rank.score));
|
||||
|
||||
None
|
||||
result?
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -44,7 +43,7 @@ impl Display for OrEngine {
|
|||
"(Or: {})",
|
||||
self.engines
|
||||
.iter()
|
||||
.map(|e| format!("{}", e))
|
||||
.map(|e| format!("{e}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
|
|
@ -71,21 +70,32 @@ impl AndEngine {
|
|||
self
|
||||
}
|
||||
|
||||
fn merge_matched_items(&self, items: Vec<MatchResult>, text: &str) -> MatchResult {
|
||||
let rank = items[0].rank;
|
||||
let mut ranges = vec![];
|
||||
fn merge_matched_items(items: Vec<MatchResult>, text: &str) -> MatchResult {
|
||||
let mut ranges = MatchIndices::new();
|
||||
let mut rank = crate::Rank {
|
||||
score: 0,
|
||||
begin: i32::MAX,
|
||||
end: i32::MIN,
|
||||
..items[0].rank
|
||||
};
|
||||
for item in items {
|
||||
match item.matched_range {
|
||||
MatchRange::ByteRange(..) => {
|
||||
ranges.extend(item.range_char_indices(text));
|
||||
}
|
||||
MatchRange::CharRange(start, end) => {
|
||||
ranges.extend(start..end);
|
||||
}
|
||||
MatchRange::Chars(vec) => {
|
||||
ranges.extend(vec.iter());
|
||||
ranges.extend(vec.iter().copied());
|
||||
}
|
||||
}
|
||||
rank.score = rank.score.saturating_add(item.rank.score);
|
||||
rank.begin = rank.begin.min(item.rank.begin);
|
||||
rank.end = rank.end.max(item.rank.end);
|
||||
}
|
||||
|
||||
ranges.sort();
|
||||
ranges.sort_unstable();
|
||||
ranges.dedup();
|
||||
MatchResult {
|
||||
rank,
|
||||
|
|
@ -95,18 +105,22 @@ impl AndEngine {
|
|||
}
|
||||
|
||||
impl MatchEngine for AndEngine {
|
||||
fn match_item(&self, item: Arc<dyn SkimItem>) -> Option<MatchResult> {
|
||||
// mock
|
||||
fn match_item(&self, item: &dyn SkimItem) -> Option<MatchResult> {
|
||||
// Fast path: single sub-engine — skip merge entirely.
|
||||
if self.engines.len() == 1 {
|
||||
return self.engines[0].match_item(item);
|
||||
}
|
||||
|
||||
let mut results = vec![];
|
||||
for engine in &self.engines {
|
||||
let result = engine.match_item(Arc::clone(&item))?;
|
||||
let result = engine.match_item(item)?;
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
if results.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.merge_matched_items(results, &item.text()))
|
||||
Some(Self::merge_matched_items(results, &item.text()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,9 +132,13 @@ impl Display for AndEngine {
|
|||
"(And: {})",
|
||||
self.engines
|
||||
.iter()
|
||||
.map(|e| format!("{}", e))
|
||||
.map(|e| format!("{e}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "andor_tests.rs"]
|
||||
mod tests;
|
||||
|
|
|
|||
101
src/engine/andor_tests.rs
Normal file
101
src/engine/andor_tests.rs
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
use super::*;
|
||||
use crate::engine::exact::{ExactEngine, ExactMatchingParam};
|
||||
|
||||
fn exact(query: &str) -> Box<dyn MatchEngine> {
|
||||
Box::new(ExactEngine::builder(query, ExactMatchingParam::default()).build())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_engine_matches_if_any_subengine_matches() {
|
||||
let engine = OrEngine::builder().engines(vec![exact("foo"), exact("zzz")]).build();
|
||||
assert!(engine.match_item(&"a foo bar".to_string()).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_engine_returns_none_when_no_subengine_matches() {
|
||||
let engine = OrEngine::builder().engines(vec![exact("xxx"), exact("zzz")]).build();
|
||||
assert!(engine.match_item(&"a foo bar".to_string()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn or_engine_empty_returns_none() {
|
||||
let engine = OrEngine::builder().build();
|
||||
assert!(engine.match_item(&"anything".to_string()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_engine_single_engine_fast_path() {
|
||||
let engine = AndEngine::builder().engines(vec![exact("foo")]).build();
|
||||
assert!(engine.match_item(&"foobar".to_string()).is_some());
|
||||
assert!(engine.match_item(&"nope".to_string()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_engine_requires_all_subengines_to_match() {
|
||||
let engine = AndEngine::builder().engines(vec![exact("foo"), exact("bar")]).build();
|
||||
// Both substrings present -> matched, ranges merged.
|
||||
let result = engine.match_item(&"foo and bar".to_string());
|
||||
assert!(result.is_some());
|
||||
let result = result.unwrap();
|
||||
assert!(matches!(result.matched_range, MatchRange::Chars(_)));
|
||||
|
||||
// Missing one substring -> no match.
|
||||
assert!(engine.match_item(&"foo only".to_string()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn and_engine_empty_returns_none() {
|
||||
// With no sub-engines the multi-engine path collects nothing and bails.
|
||||
let engine = AndEngine::builder().build();
|
||||
assert!(engine.match_item(&"anything".to_string()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_formats_combinators() {
|
||||
let or = OrEngine::builder().engines(vec![exact("a")]).build();
|
||||
assert!(format!("{or}").starts_with("(Or:"));
|
||||
let and = AndEngine::builder().engines(vec![exact("a")]).build();
|
||||
assert!(format!("{and}").starts_with("(And:"));
|
||||
}
|
||||
|
||||
fn result(range: MatchRange, score: i32, begin: i32, end: i32) -> MatchResult {
|
||||
MatchResult {
|
||||
rank: crate::Rank {
|
||||
score,
|
||||
begin,
|
||||
end,
|
||||
..Default::default()
|
||||
},
|
||||
matched_range: range,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_handles_char_range_and_chars_variants() {
|
||||
// CharRange expands to its index span; Chars copies indices verbatim.
|
||||
// Scores are summed and begin/end take the widest span.
|
||||
let merged = AndEngine::merge_matched_items(
|
||||
vec![
|
||||
result(MatchRange::CharRange(0, 2), 5, 0, 2),
|
||||
result(MatchRange::Chars(vec![4, 5]), 3, 4, 5),
|
||||
],
|
||||
"abcdef",
|
||||
);
|
||||
assert_eq!(merged.rank.score, 8);
|
||||
assert_eq!(merged.rank.begin, 0);
|
||||
assert_eq!(merged.rank.end, 5);
|
||||
assert_eq!(merged.matched_range, MatchRange::Chars(vec![0, 1, 4, 5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_dedups_and_sorts_overlapping_ranges() {
|
||||
let merged = AndEngine::merge_matched_items(
|
||||
vec![
|
||||
result(MatchRange::Chars(vec![3, 1]), 1, 1, 3),
|
||||
result(MatchRange::CharRange(1, 3), 1, 1, 3),
|
||||
],
|
||||
"abcdef",
|
||||
);
|
||||
// Sorted and de-duplicated union of {3,1} and {1,2}.
|
||||
assert_eq!(merged.matched_range, MatchRange::Chars(vec![1, 2, 3]));
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue