mirror of
https://github.com/tridactyl/tridactyl.git
synced 2026-09-10 15:26:18 -04:00
Compare commits
721 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa225538ed | ||
|
|
697b73dab9 | ||
|
|
066b046285 | ||
|
|
11693da62e | ||
|
|
c2e6452a46 | ||
|
|
c330f7eebd | ||
|
|
ef99ffb858 | ||
|
|
e4793a3284 | ||
|
|
f6173ecb3e | ||
|
|
f1ceadc2d0 | ||
|
|
1a186c97d2 | ||
|
|
ae8eb44146 | ||
|
|
d806a7b83e | ||
|
|
7d0a3ca1d9 | ||
|
|
d30541a2ff | ||
|
|
5020c4c46f | ||
|
|
e7caf837ae | ||
|
|
8a6bae175e | ||
|
|
a0073eaf01 | ||
|
|
5e1929db02 | ||
|
|
341aa6621d | ||
|
|
39e23d58cf | ||
|
|
e04d35fe87 | ||
|
|
0b6492bd85 | ||
|
|
fea4865033 | ||
|
|
14ade554b5 | ||
|
|
e744f5651d | ||
|
|
a7d6115688 | ||
|
|
62d4cb89e3 | ||
|
|
b9e85e6dec | ||
|
|
23fcba50d3 | ||
|
|
51b880173d | ||
|
|
e771471e8e | ||
|
|
87bb006f14 | ||
|
|
50ff8dc6c4 | ||
|
|
63bb26908f | ||
|
|
1f4c5a2c33 | ||
|
|
20f60a2119 | ||
|
|
6f8cee4e40 | ||
|
|
9c8c15ff77 | ||
|
|
a9f2a523c0 | ||
|
|
d9e727780b | ||
|
|
dfe15d12a7 | ||
|
|
20372d6a7d | ||
|
|
912b718755 | ||
|
|
4e55951e55 | ||
|
|
9422bd8e5b | ||
|
|
af727c7b8d | ||
|
|
903c148011 | ||
|
|
cdb99fce76 | ||
|
|
46568e8af0 | ||
|
|
cf78876e33 | ||
|
|
c8103dc0f6 | ||
|
|
d50ddc38f5 | ||
|
|
cb065cf04f | ||
|
|
4af189ccd3 | ||
|
|
b5ba02e695 | ||
|
|
e8d85058f7 | ||
|
|
5d0c10ea37 | ||
|
|
475ae04e79 | ||
|
|
0ab3f5bf62 | ||
|
|
edf442fe67 | ||
|
|
82985ae14c | ||
|
|
1ad5c34ac6 | ||
|
|
224a8930e1 | ||
|
|
169613e242 | ||
|
|
b8c41e0c24 | ||
|
|
dddf6d9fc6 | ||
|
|
6b52c0fa36 | ||
|
|
03320b51ae | ||
|
|
7e14dd2e0d | ||
|
|
2c97a45f28 | ||
|
|
2aec1280e7 | ||
|
|
38183e472a | ||
|
|
5af3d20005 | ||
|
|
09131a269f | ||
|
|
f094c3f4dc | ||
|
|
c55948419f | ||
|
|
ebd4f13bf3 | ||
|
|
e4e1357cb4 | ||
|
|
143d8c70df | ||
|
|
d9851b2f72 | ||
|
|
0b4f3ac665 | ||
|
|
77056b37c4 | ||
|
|
7af3a596eb | ||
|
|
f16e1176a4 | ||
|
|
b0ba29586d | ||
|
|
9f93d3f7d1 | ||
|
|
3322fe2271 | ||
|
|
ff049787b8 | ||
|
|
e66125794d | ||
|
|
3a51611161 | ||
|
|
0a9e073aaf | ||
|
|
e7993f216c | ||
|
|
8dd6e14550 | ||
|
|
b75db50a5f | ||
|
|
a24e83be86 | ||
|
|
a544a0dba1 | ||
|
|
1b16054cd1 | ||
|
|
87c15254d0 | ||
|
|
35d6eb571c | ||
|
|
3387a4f864 | ||
|
|
d0739c51d9 | ||
|
|
d27a2acaf9 | ||
|
|
d38ea7fba5 | ||
|
|
51775ac49f | ||
|
|
8ed0f11d59 | ||
|
|
91f1ff22b1 | ||
|
|
a51d72a2a6 | ||
|
|
77d3635c9b | ||
|
|
6d858e3389 | ||
|
|
a1e247528b | ||
|
|
19b0ef104b | ||
|
|
9a2277f1fe | ||
|
|
ebaa6038dc | ||
|
|
a03f58442e | ||
|
|
f08e640423 | ||
|
|
b85b2279c1 | ||
|
|
3453ba767c | ||
|
|
e8a263aa08 | ||
|
|
4996ffab3e | ||
|
|
43183deb2a | ||
|
|
89bdcc923e | ||
|
|
1ed0fdea4f | ||
|
|
4193c11862 | ||
|
|
e7bcc3cae1 | ||
|
|
6a207ab39f | ||
|
|
48d4f33d2a | ||
|
|
de37f815e9 | ||
|
|
6eb3073144 | ||
|
|
9dbd179929 | ||
|
|
469fe06b69 | ||
|
|
aabb26c937 | ||
|
|
8bb2b2110b | ||
|
|
ece3c4fd75 | ||
|
|
c5f49d0d5d | ||
|
|
a98d724d25 | ||
|
|
9890b0a1bd | ||
|
|
8bd4144ff7 | ||
|
|
db892be9bc | ||
|
|
1ac287cfc8 | ||
|
|
a329e17533 | ||
|
|
c889f9a78a | ||
|
|
8628390720 | ||
|
|
0184b24c91 | ||
|
|
4c67dc3d54 | ||
|
|
ec51dee8ae | ||
|
|
2d63db45fc | ||
|
|
fa820b1b18 | ||
|
|
548c41c9b4 | ||
|
|
7d1ed019e5 | ||
|
|
e0dfff51ca | ||
|
|
f5ce92069f | ||
|
|
902b3b6e58 | ||
|
|
385b818c44 | ||
|
|
89dfecec09 | ||
|
|
2bf0e5bd54 | ||
|
|
f5f9f2708a | ||
|
|
1f4ada905f | ||
|
|
87eb964638 | ||
|
|
f6cea640c1 | ||
|
|
da03de2109 | ||
|
|
07fea47e43 | ||
|
|
97145f3b68 | ||
|
|
ff5511691a | ||
|
|
25b18c6e8d | ||
|
|
2e70faefd7 | ||
|
|
caa555e540 | ||
|
|
432d38a12d | ||
|
|
f0037b1551 | ||
|
|
ceccbce949 | ||
|
|
837dbf5f2e | ||
|
|
18f9a2c9b8 | ||
|
|
91feb5eb7a | ||
|
|
28c91c6a53 | ||
|
|
8241b63aab | ||
|
|
03528e983c | ||
|
|
f40a9a98c0 | ||
|
|
d396ec1ba0 | ||
|
|
b67d84cd0e | ||
|
|
2c0d8322d3 | ||
|
|
7af0c160c6 | ||
|
|
c7281edab6 | ||
|
|
ae2abcd412 | ||
|
|
7942896402 | ||
|
|
40be93ad12 | ||
|
|
9216afb75d | ||
|
|
ae7ba91d1a | ||
|
|
2d750e300f | ||
|
|
e032d16da6 | ||
|
|
5887493a8d | ||
|
|
b40b78c8f6 | ||
|
|
c3c5ec5b62 | ||
|
|
4f3889d47d | ||
|
|
edb5a60799 | ||
|
|
926f906ae4 | ||
|
|
d471bd568d | ||
|
|
dd1820b471 | ||
|
|
86d205f84f | ||
|
|
7d5c9cf083 | ||
|
|
7811e5f531 | ||
|
|
b22f3a708e | ||
|
|
7888e42385 | ||
|
|
40dfc79dd2 | ||
|
|
63deefc23c | ||
|
|
008c02b1e1 | ||
|
|
7b054aebb0 | ||
|
|
0162446b9f | ||
|
|
3a4fa48a46 | ||
|
|
84ac7c87ec | ||
|
|
e098d4e281 | ||
|
|
7ac426ed95 | ||
|
|
5b78e75a0c | ||
|
|
c219e92bf9 | ||
|
|
6186d32e38 | ||
|
|
af5efbcb3f | ||
|
|
ee6692b607 | ||
|
|
958b369703 | ||
|
|
7e93e91940 | ||
|
|
ff28b24da4 | ||
|
|
a766f77cc4 | ||
|
|
d1d113586c | ||
|
|
7ea0dbc3ac | ||
|
|
d55f2665ff | ||
|
|
fbcd7b6b2b | ||
|
|
8fc07143b2 | ||
|
|
60f4016cc9 | ||
|
|
79d8bc79e2 | ||
|
|
9b773435b3 | ||
|
|
f694159642 | ||
|
|
4df421beb4 | ||
|
|
6b1376df37 | ||
|
|
34d41ec76c | ||
|
|
fd0351eba4 | ||
|
|
2d28b57d0d | ||
|
|
9ee9fcd931 | ||
|
|
26d8ad194e | ||
|
|
958139740d | ||
|
|
ced0cce829 | ||
|
|
e60b8dc836 | ||
|
|
b26ba76d50 | ||
|
|
f912b3513b | ||
|
|
42c4ed27a3 | ||
|
|
e83e71b401 | ||
|
|
0e640b05aa | ||
|
|
0f6afd6cb4 | ||
|
|
076e4940fc | ||
|
|
8591d4c94a | ||
|
|
f79b4db0d8 | ||
|
|
058fb34c7e | ||
|
|
47dd838e11 | ||
|
|
68765720d0 | ||
|
|
2349904b1d | ||
|
|
1668b18632 | ||
|
|
8a07ca307e | ||
|
|
b05a11d872 | ||
|
|
14c7d77cd2 | ||
|
|
b487a712bd | ||
|
|
5b31cc42eb | ||
|
|
9ffb8f55ee | ||
|
|
4c2be15b65 | ||
|
|
8eeee54e76 | ||
|
|
c4054c2050 | ||
|
|
7d2c6322b3 | ||
|
|
a578e0641f | ||
|
|
913ebe09c2 | ||
|
|
bba7479c9c | ||
|
|
4e3787f064 | ||
|
|
40b7db5060 | ||
|
|
f398b1e875 | ||
|
|
cba51dfbd5 | ||
|
|
9ac2f3f973 | ||
|
|
1853b6ebaf | ||
|
|
77ebad6c2c | ||
|
|
8eb19e69e6 | ||
|
|
eabaecc548 | ||
|
|
2200e7ba3f | ||
|
|
53a137ad78 | ||
|
|
206ada93fa | ||
|
|
51b13deda6 | ||
|
|
877bd0d5d5 | ||
|
|
ad315d7a8b | ||
|
|
13bddb2330 | ||
|
|
b5f2ab64c5 | ||
|
|
8dd9beddbd | ||
|
|
1d33a3278f | ||
|
|
5e97a9c9b1 | ||
|
|
51b9a6f8e2 | ||
|
|
454d9f0ff8 | ||
|
|
b2179417af | ||
|
|
0e7b6e9437 | ||
|
|
ed8b0e9f05 | ||
|
|
e909093dd6 | ||
|
|
3d1a1daf91 | ||
|
|
49d403b699 | ||
|
|
8374ae666d | ||
|
|
6f8ccff86f | ||
|
|
3d9d536f2a | ||
|
|
6dade7d1fb | ||
|
|
649a77962a | ||
|
|
346c13570e | ||
|
|
457154fae1 | ||
|
|
8887423545 | ||
|
|
838e1519c9 | ||
|
|
bc712e9327 | ||
|
|
021cf44d00 | ||
|
|
244b29c769 | ||
|
|
cc893cc264 | ||
|
|
8d3ac6a503 | ||
|
|
d64c29689c | ||
|
|
5857258a2b | ||
|
|
d2bbb80892 | ||
|
|
3cd207c93c | ||
|
|
b8e26f8d31 | ||
|
|
34fc0020a8 | ||
|
|
5d63fe4edd | ||
|
|
0059a45670 | ||
|
|
84edddf07a | ||
|
|
a4834ce2d5 | ||
|
|
fcb55685d2 | ||
|
|
15aee6cf3f | ||
|
|
944de159bd | ||
|
|
9edd57214c | ||
|
|
894f4d78a6 | ||
|
|
367db1302b | ||
|
|
442d11336f | ||
|
|
8a4171e9b5 | ||
|
|
187dcf69e3 | ||
|
|
41ef1726f0 | ||
|
|
0067efeccc | ||
|
|
cd6ec5f37f | ||
|
|
2bd1f5502b | ||
|
|
6cd9a1843f | ||
|
|
0b55eaa1ec | ||
|
|
cb76c5b070 | ||
|
|
dc63a6cb18 | ||
|
|
51c79132a9 | ||
|
|
77fc67ca0c | ||
|
|
a5b8ab7e89 | ||
|
|
a4c318b416 | ||
|
|
82c2ca4859 | ||
|
|
c27332c3d5 | ||
|
|
d53d87d7fe | ||
|
|
47e2436ccf | ||
|
|
e7ad13dbca | ||
|
|
cac2c078e8 | ||
|
|
9ec6688b31 | ||
|
|
1ed1804d9b | ||
|
|
59010c256f | ||
|
|
0ef70c2a26 | ||
|
|
6d50ecbda7 | ||
|
|
ad61b8c6a5 | ||
|
|
85d7e8b81a | ||
|
|
fdb53c012e | ||
|
|
253bfc52bb | ||
|
|
f7c870b045 | ||
|
|
cfbfbd511b | ||
|
|
5382257329 | ||
|
|
1d158cc124 | ||
|
|
f04f5cfa8a | ||
|
|
8b4dfc2c28 | ||
|
|
64d6e1949a | ||
|
|
b742bbeab5 | ||
|
|
e921cb901e | ||
|
|
f771106b65 | ||
|
|
45f2e0e157 | ||
|
|
76fd349a76 | ||
|
|
2766a2c6f2 | ||
|
|
d959d654ab | ||
|
|
cb4725e596 | ||
|
|
397cfca593 | ||
|
|
048f6d09e4 | ||
|
|
ac017eb7f6 | ||
|
|
c127fb6bea | ||
|
|
8d18f541bc | ||
|
|
410eb34528 | ||
|
|
a696038929 | ||
|
|
c37db0e6d6 | ||
|
|
afbf5d214f | ||
|
|
000ccf551d | ||
|
|
d2883e599b | ||
|
|
20023db656 | ||
|
|
9d767bb018 | ||
|
|
d23d3ae461 | ||
|
|
abea927768 | ||
|
|
debea430a0 | ||
|
|
f3e6e961bc | ||
|
|
052f3c3f33 | ||
|
|
a6a010b286 | ||
|
|
c27c2d48c8 | ||
|
|
fac4885cc7 | ||
|
|
468223cb39 | ||
|
|
93123ee5ed | ||
|
|
6f848ea768 | ||
|
|
8b2f71df6b | ||
|
|
91c8097800 | ||
|
|
063a1e3874 | ||
|
|
386efca4ef | ||
|
|
93020300a6 | ||
|
|
363f4fb3b7 | ||
|
|
a3bea8b0bc | ||
|
|
75d231b7c1 | ||
|
|
36b0efcb7a | ||
|
|
006540ba49 | ||
|
|
7c84a9a520 | ||
|
|
cb2fbc3fd2 | ||
|
|
d7bd210e07 | ||
|
|
1c394454b5 | ||
|
|
185fe1f2de | ||
|
|
7d228c72a3 | ||
|
|
f2c5b1ebde | ||
|
|
e603a3eb24 | ||
|
|
4dfacf9bf4 | ||
|
|
09811efbe3 | ||
|
|
8897ded643 | ||
|
|
f34b7d08cd | ||
|
|
29fa4b8ba0 | ||
|
|
6f8202d5ee | ||
|
|
718b8a142d | ||
|
|
d21ad1f029 | ||
|
|
811022bf7b | ||
|
|
b8c8c4aebe | ||
|
|
2922ded7bc | ||
|
|
40aba31190 | ||
|
|
1fa04aaa2f | ||
|
|
a9eb82e49f | ||
|
|
cc2694131e | ||
|
|
fcf30bae40 | ||
|
|
254097cd3f | ||
|
|
81a0df7f54 | ||
|
|
0a91af6a7f | ||
|
|
03e230cc5a | ||
|
|
9a31a79ae8 | ||
|
|
808317f3d2 | ||
|
|
eddfe2b41a | ||
|
|
1febc15c16 | ||
|
|
4bf6032a08 | ||
|
|
91356c9ce4 | ||
|
|
f6a527fe84 | ||
|
|
256a8fa72a | ||
|
|
dbed660872 | ||
|
|
ecdb3338fe | ||
|
|
24b012e666 | ||
|
|
5cddc889fa | ||
|
|
06457ee187 | ||
|
|
50aae4a898 | ||
|
|
b006c219d9 | ||
|
|
6b7c346169 | ||
|
|
37bf4cb3e5 | ||
|
|
4d1e672bf1 | ||
|
|
e5619e8ef1 | ||
|
|
d1f51136fd | ||
|
|
9739c7df86 | ||
|
|
98b92d562a | ||
|
|
f3a43e14c1 | ||
|
|
c02669a8f5 | ||
|
|
0c69ad2f22 | ||
|
|
858f2ee15d | ||
|
|
ff9daffd83 | ||
|
|
345f1dbef5 | ||
|
|
fb00b01981 | ||
|
|
007d93a181 | ||
|
|
fdff6d999f | ||
|
|
3d558b5f38 | ||
|
|
ee8a2f6aeb | ||
|
|
3a67cc805f | ||
|
|
48baeb91f6 | ||
|
|
028f72ae9e | ||
|
|
2b6cab5e13 | ||
|
|
c0263c2976 | ||
|
|
0f0aaef006 | ||
|
|
b8d343ede8 | ||
|
|
159d81fdca | ||
|
|
9cac02f2f9 | ||
|
|
9074d0355a | ||
|
|
22d3cf73d9 | ||
|
|
ae67820ddf | ||
|
|
af488f66d0 | ||
|
|
11bec3f1dd | ||
|
|
cbcc92694c | ||
|
|
5cc7312882 | ||
|
|
38163d0e1a | ||
|
|
e4e5c56802 | ||
|
|
1eeab1f980 | ||
|
|
9cd67eee4c | ||
|
|
a328821a12 | ||
|
|
4fb30adfdb | ||
|
|
12cb96e14e | ||
|
|
67b893ca6c | ||
|
|
8596302451 | ||
|
|
ffa4902da9 | ||
|
|
f75c90ee23 | ||
|
|
0831811699 | ||
|
|
38631348db | ||
|
|
e61f883661 | ||
|
|
1b74a2641c | ||
|
|
ade0e50e68 | ||
|
|
80c38680ff | ||
|
|
6162b13809 | ||
|
|
dc18d58edc | ||
|
|
a05eda5a8b | ||
|
|
d2aa586833 | ||
|
|
bddedf3b0c | ||
|
|
b186f5f4a6 | ||
|
|
96e03c14cb | ||
|
|
405ee11d2f | ||
|
|
4e26f5fbe7 | ||
|
|
02dfd6b78d | ||
|
|
e08e1a272f | ||
|
|
1280ad682b | ||
|
|
c6563992a7 | ||
|
|
1b0d6d3297 | ||
|
|
4b6e75b4f1 | ||
|
|
566c4870b9 | ||
|
|
9c8e3fe0eb | ||
|
|
69c3d825a4 | ||
|
|
5c644595ba | ||
|
|
e78b927df6 | ||
|
|
6b36607d5e | ||
|
|
2ea75f90f5 | ||
|
|
687789c623 | ||
|
|
4b27bd52af | ||
|
|
e868f1b10f | ||
|
|
86de01e1a7 | ||
|
|
70494c9596 | ||
|
|
0486ed7b70 | ||
|
|
09bb55924b | ||
|
|
bcdfa98298 | ||
|
|
7563fd9e28 | ||
|
|
44f23f14e7 | ||
|
|
749dd59903 | ||
|
|
22ab6b4f65 | ||
|
|
4d196fd88f | ||
|
|
1606193bd6 | ||
|
|
25bb2764cb | ||
|
|
0cd0e505fe | ||
|
|
b2e6bc62a6 | ||
|
|
30ae6d7a79 | ||
|
|
8f87fbef3d | ||
|
|
faeec5384d | ||
|
|
2c700182f8 | ||
|
|
6f85b38a69 | ||
|
|
521d43f1fa | ||
|
|
d88fa4e975 | ||
|
|
ab5e5c4649 | ||
|
|
c15f0c9003 | ||
|
|
3f9a19243f | ||
|
|
a4ab5c5932 | ||
|
|
c47960ca6e | ||
|
|
6124ab93f4 | ||
|
|
42a55663b8 | ||
|
|
030ef4d2ab | ||
|
|
90e437cc35 | ||
|
|
ba93521570 | ||
|
|
b85f619290 | ||
|
|
1e88b17533 | ||
|
|
8e01db94ab | ||
|
|
652eb493f3 | ||
|
|
239f4248cd | ||
|
|
33cc24ea0f | ||
|
|
e0eac6ec7f | ||
|
|
2820c94a50 | ||
|
|
83d11d4846 | ||
|
|
24fcb22cc2 | ||
|
|
8909457a16 | ||
|
|
2398626335 | ||
|
|
ce50461ec6 | ||
|
|
bc954dc33c | ||
|
|
46a7d8edf7 | ||
|
|
c59770c981 | ||
|
|
c97d3f35e8 | ||
|
|
636df7ae3e | ||
|
|
0bc38cb546 | ||
|
|
6fb98288a4 | ||
|
|
6ed23de314 | ||
|
|
89225aeebe | ||
|
|
4e01b741b5 | ||
|
|
834c7755d1 | ||
|
|
bc6d051591 | ||
|
|
199db429bc | ||
|
|
1dafb76912 | ||
|
|
1b860da584 | ||
|
|
a9de0897b9 | ||
|
|
5039e1f687 | ||
|
|
dc675ceff2 | ||
|
|
8a18322c5f | ||
|
|
8f60f430c0 | ||
|
|
0972873803 | ||
|
|
5b278be67e | ||
|
|
c0b902fb8d | ||
|
|
e150da80ba | ||
|
|
934aa8c4cd | ||
|
|
5aa667c4cf | ||
|
|
ceac48fc13 | ||
|
|
f57849c2db | ||
|
|
c9969cdd11 | ||
|
|
ccc77b583c | ||
|
|
2a407f87f9 | ||
|
|
e1cce5f2b9 | ||
|
|
555a0d6179 | ||
|
|
b6d6ed88fe | ||
|
|
0885811abe | ||
|
|
6b8cf13056 | ||
|
|
46d1eba5e0 | ||
|
|
7d54aad581 | ||
|
|
6e3d17906e | ||
|
|
8e7557d648 | ||
|
|
344fc9fad7 | ||
|
|
63b27d8c7b | ||
|
|
681ccc9023 | ||
|
|
c5d4329274 | ||
|
|
f49ca25703 | ||
|
|
11d6f1f509 | ||
|
|
fe57d2c263 | ||
|
|
f1d827a784 | ||
|
|
40a157f8cc | ||
|
|
d5e7f7b570 | ||
|
|
50f19a34fc | ||
|
|
a6caa97352 | ||
|
|
db60e42943 | ||
|
|
e35f3716d3 | ||
|
|
ea3666365d | ||
|
|
d3bdc9a898 | ||
|
|
f25b9658ef | ||
|
|
f9df0688db | ||
|
|
fbda0a2970 | ||
|
|
2fe6825b37 | ||
|
|
3fce68da15 | ||
|
|
ebe5117e69 | ||
|
|
3af370cf1a | ||
|
|
94eb591033 | ||
|
|
4b4a9f41ce | ||
|
|
5270e1ef53 | ||
|
|
7310194afd | ||
|
|
5c2ba72206 | ||
|
|
0c66ad0365 | ||
|
|
13258b5483 | ||
|
|
8c18d372b2 | ||
|
|
19190d3118 | ||
|
|
3c41724497 | ||
|
|
f9e3e5585a | ||
|
|
964b83c328 | ||
|
|
bf9f5fd193 | ||
|
|
1b674d637d | ||
|
|
be35470616 | ||
|
|
b4be583cfc | ||
|
|
cf912fbad7 | ||
|
|
02a5731d88 | ||
|
|
01f18bf213 | ||
|
|
60118c49ec | ||
|
|
dd6a0a8893 | ||
|
|
cc0dccda32 | ||
|
|
e14eae6898 | ||
|
|
616efd681e | ||
|
|
2e8f21380c | ||
|
|
09c9d9a3fd | ||
|
|
961726bfbd | ||
|
|
7d4765a7ad | ||
|
|
83e6211687 | ||
|
|
937ab820c9 | ||
|
|
9efffe78a5 | ||
|
|
d1a6551f36 | ||
|
|
d270d5e3dd | ||
|
|
8df96a5995 | ||
|
|
0f58004816 | ||
|
|
9e6b18cf2f | ||
|
|
254c7edcf7 | ||
|
|
bb7150d998 | ||
|
|
f1156d5c72 | ||
|
|
4157002ff3 | ||
|
|
729f064855 | ||
|
|
0fda7cd274 | ||
|
|
ca2165d57c | ||
|
|
49928b37e4 | ||
|
|
659e6e84bb | ||
|
|
9a9bd5e5d3 | ||
|
|
2d97f9ffad | ||
|
|
95efb18f15 | ||
|
|
6cad579065 | ||
|
|
72be7ed9aa | ||
|
|
c7e59eba10 | ||
|
|
9910b7288c | ||
|
|
cf4baae9cf | ||
|
|
b7272fb378 | ||
|
|
4ec9254111 | ||
|
|
dad52e9b12 | ||
|
|
5ba1f94608 | ||
|
|
f68a157b9d | ||
|
|
94272d2c4f | ||
|
|
276fffe0ef | ||
|
|
4fcc6d3072 | ||
|
|
7a68826987 | ||
|
|
e8eaca07d6 | ||
|
|
c7b633dba9 | ||
|
|
c240917cff | ||
|
|
92ce6e2f81 | ||
|
|
193e2eb090 | ||
|
|
457eaf2443 | ||
|
|
15685b7ddc | ||
|
|
a4dc22b919 | ||
|
|
57abfc5123 | ||
|
|
f50181ba04 | ||
|
|
a3be85fc72 | ||
|
|
71c1f5f92b | ||
|
|
e232bd3de9 | ||
|
|
7ff1b79bb2 | ||
|
|
a0c38cc1d9 | ||
|
|
cfdeaed925 | ||
|
|
e00679966f | ||
|
|
af90d4cec2 | ||
|
|
10c3a1a727 | ||
|
|
38c5eeb16e | ||
|
|
3af7b1f05d | ||
|
|
3046814c8d | ||
|
|
bd4107a6ea | ||
|
|
6b5865aafd | ||
|
|
34db87492b | ||
|
|
d0c935ac45 | ||
|
|
5fbb1b0cf1 | ||
|
|
046c014893 | ||
|
|
5bb88b1bb3 |
102
.eslintrc.js
102
.eslintrc.js
|
|
@ -1,3 +1,5 @@
|
|||
const browserTargets = require("./browser-targets.json")
|
||||
|
||||
/*
|
||||
👋 Hi! This file was autogenerated by tslint-to-eslint-config.
|
||||
https://github.com/typescript-eslint/tslint-to-eslint-config
|
||||
|
|
@ -30,11 +32,17 @@ module.exports = {
|
|||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"@typescript-eslint/tslint",
|
||||
"sonarjs"
|
||||
],
|
||||
"rules": {
|
||||
"unsupported-apis": "warn",
|
||||
"unsupported-apis-chrome": [
|
||||
"warn",
|
||||
{ "minimumVersion": browserTargets.chrome.minimumVersion }
|
||||
],
|
||||
"unsupported-apis-firefox": [
|
||||
"warn",
|
||||
{ "minimumVersion": browserTargets.firefox.minimumVersion }
|
||||
],
|
||||
"sonarjs/cognitive-complexity": "off", //"error",
|
||||
"sonarjs/no-duplicate-string": "off",
|
||||
"sonarjs/no-unused-collection": "off", //"error", // There seems to be a bug with this rule - exported collections are assumed unused
|
||||
|
|
@ -42,32 +50,16 @@ module.exports = {
|
|||
"@typescript-eslint/array-type": "off",
|
||||
"@typescript-eslint/await-thenable": "error",
|
||||
"@typescript-eslint/ban-ts-comment": "error",
|
||||
"@typescript-eslint/ban-types": [
|
||||
"@typescript-eslint/no-restricted-types": [
|
||||
"error",
|
||||
{
|
||||
"types": {
|
||||
"Object": {
|
||||
"message": "Avoid using the `Object` type. Did you mean `object`?"
|
||||
},
|
||||
"Function": {
|
||||
"message": "Avoid using the `Function` type. Prefer a specific function type, like `() => void`."
|
||||
},
|
||||
"Boolean": {
|
||||
"message": "Avoid using the `Boolean` type. Did you mean `boolean`?"
|
||||
},
|
||||
"Number": {
|
||||
"message": "Avoid using the `Number` type. Did you mean `number`?"
|
||||
},
|
||||
"String": {
|
||||
"message": "Avoid using the `String` type. Did you mean `string`?"
|
||||
},
|
||||
"Symbol": {
|
||||
"message": "Avoid using the `Symbol` type. Did you mean `symbol`?"
|
||||
"KeyboardEvent": {
|
||||
"message": "Use `TrustedKeyboardEvent` to prevent remote code injection from hostile pages."
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"@typescript-eslint/consistent-type-assertions": "error",
|
||||
"@typescript-eslint/consistent-type-definitions": "error",
|
||||
"@typescript-eslint/dot-notation": "off", // this should be "error" but the fix silently breaks code almost 100% of the time. not worth the headaches
|
||||
|
|
@ -78,25 +70,14 @@ module.exports = {
|
|||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off", //"warn", // This is another hard one to enable
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/interface-name-prefix": "off",
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
"@typescript-eslint/member-ordering": [
|
||||
"error",
|
||||
{ "default": ["field", "constructor", "method"] },
|
||||
],
|
||||
"@typescript-eslint/member-ordering": "error",
|
||||
"@typescript-eslint/no-array-delete": "off",
|
||||
"@typescript-eslint/no-array-constructor": "error",
|
||||
"@typescript-eslint/no-base-to-string": "off",
|
||||
"@typescript-eslint/no-empty-function": "error",
|
||||
"@typescript-eslint/no-empty-interface": "error",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-extra-non-null-assertion": "error",
|
||||
"@typescript-eslint/no-floating-promises": "off", //"error", // We should turn this on eventually but it will take a while to fix
|
||||
|
|
@ -112,10 +93,11 @@ module.exports = {
|
|||
"@typescript-eslint/no-namespace": "error",
|
||||
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
|
||||
"@typescript-eslint/no-non-null-assertion": "warn",
|
||||
"@typescript-eslint/no-parameter-properties": "off",
|
||||
"@typescript-eslint/no-redundant-type-constituents": "off",
|
||||
"@typescript-eslint/no-this-alias": "error",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "error",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off", //"error",
|
||||
"@typescript-eslint/no-unsafe-argument": "off", //"error",
|
||||
"@typescript-eslint/no-unsafe-call": "off", //"error",
|
||||
"@typescript-eslint/no-unsafe-member-access": "off", //"error", // We've done this a lot, but it would be a good idea to fix it
|
||||
"@typescript-eslint/no-unsafe-return": "off", //"error", // We've done this a lot, but it would be a good idea to fix it
|
||||
|
|
@ -126,21 +108,24 @@ module.exports = {
|
|||
"allowTernary": true,
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars-experimental": [
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"ignoreArgsIfArgsAfterAreUsed": true,
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_",
|
||||
"caughtErrors": "none",
|
||||
"varsIgnorePattern": "^_",
|
||||
},
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/no-var-requires": "error",
|
||||
"@typescript-eslint/only-throw-error": "off",
|
||||
"@typescript-eslint/prefer-as-const": "error",
|
||||
"@typescript-eslint/prefer-for-of": "error",
|
||||
"@typescript-eslint/prefer-function-type": "error",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "error",
|
||||
"@typescript-eslint/prefer-promise-reject-errors": "off",
|
||||
"@typescript-eslint/prefer-regexp-exec": "error",
|
||||
"@typescript-eslint/quotes": [
|
||||
"quotes": [
|
||||
"error",
|
||||
"double",
|
||||
{
|
||||
|
|
@ -151,10 +136,6 @@ module.exports = {
|
|||
"@typescript-eslint/require-await": "error",
|
||||
"@typescript-eslint/restrict-plus-operands": "off", //"error", // We use this a lot - fixing it is a problem for a rainy day
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/triple-slash-reference": [
|
||||
"error",
|
||||
{
|
||||
|
|
@ -163,7 +144,6 @@ module.exports = {
|
|||
"lib": "always"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/unbound-method": "error",
|
||||
"@typescript-eslint/unified-signatures": "error",
|
||||
"arrow-body-style": "error",
|
||||
|
|
@ -191,7 +171,6 @@ module.exports = {
|
|||
"import/order": "off",
|
||||
"jsdoc/check-alignment": "off",
|
||||
"jsdoc/check-indentation": "off",
|
||||
"jsdoc/newline-after-description": "off",
|
||||
"max-classes-per-file": "off",
|
||||
"max-len": "off",
|
||||
"new-parens": "error",
|
||||
|
|
@ -255,6 +234,12 @@ module.exports = {
|
|||
"valid-typeof": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.d.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": ["src/completions/*.ts", "src/excmds.ts"],
|
||||
"rules": {
|
||||
|
|
@ -270,5 +255,24 @@ module.exports = {
|
|||
"@typescript-eslint/prefer-regexp-exec": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": ["src/content.ts", "src/commandline_frame.ts"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "error",
|
||||
"@typescript-eslint/no-restricted-types": [
|
||||
"error",
|
||||
{
|
||||
"types": {
|
||||
"TrustedKeyboardEvent": {
|
||||
"message": "Events must be validated with `isTrustedKeyboardEvent` at runtime"
|
||||
},
|
||||
"KeyboardEvent": {
|
||||
"message": "Use `Event` instead"
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
],
|
||||
};
|
||||
|
|
|
|||
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
|
@ -2,5 +2,6 @@
|
|||
|
||||
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: bovine3dom
|
||||
liberapay: bovine3dom
|
||||
patreon: tridactyl
|
||||
custom: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY
|
||||
|
|
|
|||
27
.github/workflows/e2e.yml
vendored
27
.github/workflows/e2e.yml
vendored
|
|
@ -26,10 +26,12 @@ jobs:
|
|||
matrix:
|
||||
os: [ubuntu, windows]
|
||||
# os: [ubuntu, macos, windows]
|
||||
browser: [firefox, firefoxesr]
|
||||
browser: ${{ fromJSON(github.event_name == 'schedule' && '["latest", "latest-esr", "latest-beta"]' || '["latest", "latest-esr"]') }}
|
||||
exclude:
|
||||
- os: windows
|
||||
browser: firefoxesr
|
||||
browser: latest-esr
|
||||
- os: windows
|
||||
browser: latest-beta
|
||||
|
||||
runs-on: ${{ matrix.os }}-latest
|
||||
|
||||
|
|
@ -42,12 +44,23 @@ jobs:
|
|||
cache: 'yarn'
|
||||
|
||||
- name: Install deps
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Setup Firefox
|
||||
uses: browser-actions/setup-firefox@v1
|
||||
with:
|
||||
firefox-version: ${{ matrix.browser == 'firefox' && 'latest' || 'latest-esr' }}
|
||||
firefox-version: ${{ matrix.browser }}
|
||||
|
||||
- name: Cache build outputs
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
build
|
||||
dist
|
||||
out
|
||||
key: build-${{ matrix.os }}-${{ matrix.browser }}-${{ hashFiles('yarn.lock', 'package.json', 'src/**/*') }}
|
||||
restore-keys: |
|
||||
build-${{ matrix.os }}-${{ matrix.browser }}-
|
||||
|
||||
- name: Print Firefox version (Unix-like)
|
||||
if: matrix.os == 'ubuntu' || matrix.os == 'macos'
|
||||
|
|
@ -55,12 +68,10 @@ jobs:
|
|||
|
||||
- name: Build and test (Firefox)
|
||||
uses: nick-fields/retry@v3
|
||||
env:
|
||||
HEADLESS: 1
|
||||
with:
|
||||
max_attempts: ${{ github.event_name == 'pull_request' && 5 || 10 }}
|
||||
max_attempts: ${{ github.event_name == 'pull_request' && ( github.event.pull_request.draft && 1 || 5 ) || 10 }}
|
||||
timeout_minutes: 10
|
||||
retry_wait_seconds: 10
|
||||
shell: bash
|
||||
command: |
|
||||
yarn run clean && yarn run build --old-native && yarn make-zip && yarn jest
|
||||
yarn run build --old-native && yarn make-zip && yarn jest
|
||||
|
|
|
|||
19
.github/workflows/lint.yml
vendored
19
.github/workflows/lint.yml
vendored
|
|
@ -12,6 +12,19 @@ on:
|
|||
- '/readme.md'
|
||||
|
||||
jobs:
|
||||
commit-emails:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Check commits for noreply addresses
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: sh ./ci/check-commit-emails.sh
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
|
|
@ -20,8 +33,12 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'yarn'
|
||||
- name: Setup
|
||||
run: yarn install
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: ${{ matrix.step }}
|
||||
env:
|
||||
STEP: ${{ matrix.step }}
|
||||
|
|
|
|||
28
.github/workflows/website.yml
vendored
28
.github/workflows/website.yml
vendored
|
|
@ -28,17 +28,35 @@ jobs:
|
|||
repository: 'tridactyl/site'
|
||||
submodules: 'recursive' # we shouldn't need this, public should be generated each time
|
||||
path: 'site'
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '24'
|
||||
|
||||
- name: Setup Hugo
|
||||
uses: peaceiris/actions-hugo@v3
|
||||
with:
|
||||
hugo-version: 'latest'
|
||||
extended: true
|
||||
|
||||
- name: Setup PostCSS
|
||||
run: |
|
||||
yarn global add postcss postcss-cli autoprefixer
|
||||
echo "$(yarn global bin)" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
cd tridactyl
|
||||
yarn install
|
||||
yarn install --frozen-lockfile
|
||||
yarn run build
|
||||
find . -iname "*.html" -exec sed 's@href="/static@href="/build/static@' -i '{}' ';' # ideally this url would be less gnarly
|
||||
# The hosted copy cannot run WebExtension scripts and lives under /build.
|
||||
find build -iname "*.html" -exec sed -E -i \
|
||||
-e 's@="/static@="/build/static@g' \
|
||||
-e 's@<script src="/(content|help)\.js"></script>@@g' '{}' +
|
||||
cd ../site
|
||||
sudo apt-get install -y hugo
|
||||
hugo
|
||||
cp -r ../tridactyl/build/ public
|
||||
- uses: actions/upload-pages-artifact@v1
|
||||
- uses: actions/upload-pages-artifact@v4
|
||||
with:
|
||||
path: 'site/public'
|
||||
retention-days: 2
|
||||
|
|
@ -55,4 +73,4 @@ jobs:
|
|||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v2
|
||||
uses: actions/deploy-pages@v4
|
||||
|
|
|
|||
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -10,10 +10,10 @@ native/native_main
|
|||
native_main.spec
|
||||
.wine-pyinstaller
|
||||
tags
|
||||
compiler/*.js
|
||||
compiler/**/*.js
|
||||
.*.generated.ts
|
||||
.*.generated.json
|
||||
.tmp/
|
||||
.DS_Store
|
||||
.build_cache/
|
||||
yarn-error.log
|
||||
*.orig
|
||||
|
|
|
|||
44
.tridactylrc
44
.tridactylrc
|
|
@ -34,6 +34,19 @@
|
|||
" " get it in the browser, or just restart.
|
||||
"
|
||||
" " If you're bovine3dom run sed 's|^" ||' .tridactylrc > ~/.config/tridactyl/tridactylrc
|
||||
"
|
||||
" "
|
||||
" " Cleanup (requires Tridactyl 1.25.0+)
|
||||
" " Persisted settings may briefly apply at startup before this file is sourced.
|
||||
" "
|
||||
" sanitise tridactylconfig
|
||||
" set configversion 2.0
|
||||
"
|
||||
" " horizontal scrolling layout for reader
|
||||
" colours --module=reader --url=https://raw.githubusercontent.com/tridactyl/tridactyl/refs/heads/master/contrib/themes/reader/newspaper.css newspaper
|
||||
"
|
||||
" colours quakelight
|
||||
" set tabsort mru
|
||||
"
|
||||
"
|
||||
" "
|
||||
|
|
@ -76,7 +89,7 @@
|
|||
" " bindurl ^https://duckduckgo.com F hint -Jbc [data-testid="result-title-a"]
|
||||
|
||||
" " Make `gi` on GitHub take you to the search box
|
||||
" bindurl ^https://github.com gi hint -Vc .AppHeader-searchButton
|
||||
" bindurl ^https://github.com gi hint -!Jc [aria-label*="Search"]
|
||||
"
|
||||
" " Allow Ctrl-a to select all in the commandline
|
||||
" unbind --mode=ex <C-a>
|
||||
|
|
@ -98,11 +111,29 @@
|
|||
" bind gr reader
|
||||
" bind gR reader --tab
|
||||
"
|
||||
" " Suspend / "discard" all tabs - handy for stretching out battery life
|
||||
" command discardall jsb browser.tabs.query({}).then(ts => browser.tabs.discard(ts.map(t=>t.id)))
|
||||
" " Use experimental :find mode
|
||||
" bind / fillcmdline find -r
|
||||
" bind ? fillcmdline find -r --reverse
|
||||
" bind n findnext --search-from-view
|
||||
" bind <C-g> findnext --search-from-view
|
||||
" bind N findnext --search-from-view --reverse
|
||||
" bind <C-G> findnext --search-from-view --reverse
|
||||
" bind gN findselect
|
||||
" " conflicts with quickmarks
|
||||
" " bind gN composite findnext --search-from-view --reverse; findselect
|
||||
" bind ,<Space> nohlsearch
|
||||
"
|
||||
" " Suspend / "discard" all tabs - handy for stretching out battery life. Now a default command, aliased here for my muscle memory
|
||||
" command discardall tabdiscard --all
|
||||
"
|
||||
" " Julia docs' built in search is bad
|
||||
" set searchurls.julia https://www.google.com/search?q=site:http://docs.julialang.org/en/v1%20
|
||||
" " Search the current website with `open site [query]`
|
||||
" set jsurls.site async q => `https://www.google.com/search?num=100&q=${encodeURIComponent("site:" + new URL((await tri.webext.activeTab()).url).hostname + " " + q)}`
|
||||
"
|
||||
" " Free up ctrl+[ for back in case H etc. has been stolen
|
||||
" unbind --mode=normal <C-[>
|
||||
"
|
||||
" " get rid of annoying google 'ai overviews'
|
||||
" set searchengine google
|
||||
"
|
||||
" "
|
||||
" " Misc settings
|
||||
|
|
@ -154,6 +185,9 @@
|
|||
|
||||
" " Quickmark for PRs by humans
|
||||
" quickmark p https://github.com/tridactyl/tridactyl/pulls?q=is%3Apr+is%3Aopen+-label%3Adependencies+-author%3Abovine3dom+sort%3Aupdated-desc
|
||||
|
||||
" " Quick issue navigation
|
||||
" bind --mode=normal ,i js tri.excmds.gobble("<CR>", 'js -d£ window.location.href = "https://github.com/tridactyl/tridactyl/issues/" + JS_ARGS[1]£')
|
||||
"
|
||||
" " Inject Google Translate
|
||||
" " This (clearly) is remotely hosted code. Google will be sent the whole
|
||||
|
|
|
|||
29
AGENTS.md
Normal file
29
AGENTS.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Agent guidelines from contributing to Tridactyl
|
||||
|
||||
This document defines how AI agents such as GitHub Copilot, Claude, OpenClaw and OpenCode must behave when interacting with this repository and contributing code. AI agents are not specifically encouraged to contribute to Tridactyl, but, equally, small and concise pull requests that cleanly fix real issues are welcome.
|
||||
|
||||
Your code will be reviewed by a time-pressed human. Pull requests that do not follow these guidelines will be rejected without further review. At the time of writing, there were approximately 80 open pull requests and the mean time from opening a pull request to getting it merged was about 90 days.
|
||||
|
||||
## Before you start coding
|
||||
|
||||
- Make sure you define success or failure for your patch and that it corresponds clearly to an issue.
|
||||
|
||||
- Make sure you can reproduce the issue locally.
|
||||
|
||||
- CRITICAL: do NOT work on a 'help wanted' issue for your first patch. These issues are the hardest and most complex and are difficult to reproduce.
|
||||
|
||||
## Patch contents
|
||||
|
||||
- All listed commit authors and co-authors must be human and their git email addresses must be monitored. Do not use `noreply` email addresses.
|
||||
|
||||
- CRITICAL: your patch must _ONLY_ make changes to relevant files and lines. Pull requests that change formatting on unrelated lines or add cruft to the repository will be rejected without review.
|
||||
|
||||
- CRITICAL: your first pull request _MUST_ be FEWER than 50 lines of code. Once a pull request has been merged, _SUBSEQUENT_ pull requests may be up to 100 lines of code. If it is not possible to fix the issue within these limits, choose a different issue.
|
||||
|
||||
- Tridactyl is a complex piece of software which interacts with Firefox, one of the most complex pieces of software on the planet. You should create failing end to end tests before writing any code and only submit a patch once these tests are pass. You should not edit the failing tests once you have started writing any code. _DO NOT_ submit new end-to-end tests as part of your patch: these are for your own use only.
|
||||
|
||||
## Making the pull request
|
||||
|
||||
- CRITICAL: your pull requests and commits _MUST_ be prefixed with the 🤖 emoji. Pull requests that seem to be created by agents but do not have this prefix will be REJECTED WITHOUT REVIEW.
|
||||
|
||||
- CRITICAL: you must make a HUMAN test that your patch fixes the original issue and you must document the steps they took to do so. If you cannot find a human to test your patch, you MUST document this in your pull request and explain what steps you took to ensure that the patch truly fixes the issue it claims to in an end-to-end fashion.
|
||||
2566
CHANGELOG.md
2566
CHANGELOG.md
File diff suppressed because it is too large
Load diff
13
browser-targets.json
Normal file
13
browser-targets.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"firefox": {
|
||||
"minimumVersion": "94.0",
|
||||
"manifestVersionPath": [
|
||||
"applications",
|
||||
"gecko",
|
||||
"strict_min_version"
|
||||
]
|
||||
},
|
||||
"chrome": {
|
||||
"minimumVersion": "114"
|
||||
}
|
||||
}
|
||||
12
ci/check-commit-emails.sh
Executable file
12
ci/check-commit-emails.sh
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
authors=$(git log --format='%H%x09%an%x09%ae' "$BASE_SHA..$HEAD_SHA")
|
||||
coauthors=$(git log --format='%H%x09%(trailers:key=Co-authored-by)' "$BASE_SHA..$HEAD_SHA")
|
||||
bad_authors=$(printf '%s\n' "$authors" | grep -iE 'noreply' | grep -ivE 'dependabot(\[bot\])?' || true)
|
||||
bad_coauthors=$(printf '%s\n' "$coauthors" | grep -iE 'noreply' || true)
|
||||
if [ -n "$bad_authors$bad_coauthors" ]; then
|
||||
printf '%s\n%s\n' "$bad_authors" "$bad_coauthors"
|
||||
echo "'noreply' address found in patch. Please provide real contact details or ask a maintainer to adopt your commits."
|
||||
exit 1
|
||||
fi
|
||||
15
ci/lint.sh
15
ci/lint.sh
|
|
@ -12,3 +12,18 @@ if [ "$incompatible_sed" ]; then
|
|||
fi
|
||||
yarn run lint
|
||||
"$(yarn bin)/eslint" --rulesdir custom-eslint-rules --ext .ts .
|
||||
|
||||
if [ "$(git rev-parse --abbrev-ref HEAD)" = "master" ]; then
|
||||
if ! yarn prettier --check '**/*.ts' ; then
|
||||
echo "Warning: the files above have prettier formatting issues. Run 'yarn run prettier -w [files]' to fix them."
|
||||
fi
|
||||
else
|
||||
git fetch origin master 2>/dev/null || true
|
||||
changed=$(git diff --name-only origin/master...HEAD -- '*.ts' 2>/dev/null || true)
|
||||
if [ -n "$changed" ]; then
|
||||
if ! yarn prettier --check $changed; then
|
||||
echo "The files above have prettier formatting issues. Run 'yarn run prettier -w [files]' to fix them."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,274 +0,0 @@
|
|||
import * as ts from "typescript"
|
||||
import * as fs from "fs"
|
||||
import * as commandLineArgs from "command-line-args"
|
||||
import * as AllTypes from "./types/AllTypes"
|
||||
import * as AllMetadata from "./metadata/AllMetadata"
|
||||
|
||||
export function toSimpleType(typeNode) {
|
||||
switch (typeNode.kind) {
|
||||
case ts.SyntaxKind.VoidKeyword:
|
||||
return new AllTypes.VoidType()
|
||||
// IndexedAccessTypes are things like `fn<T keyof Class>(x: T, y: Class[T])`
|
||||
// This doesn't seem to be easy to deal with so let's kludge it for now
|
||||
case ts.SyntaxKind.IndexedAccessType:
|
||||
// Unknown is just like any, but slightly stricter
|
||||
case ts.SyntaxKind.UnknownKeyword:
|
||||
case ts.SyntaxKind.AnyKeyword:
|
||||
return new AllTypes.AnyType()
|
||||
case ts.SyntaxKind.BooleanKeyword:
|
||||
return new AllTypes.BooleanType()
|
||||
case ts.SyntaxKind.NumberKeyword:
|
||||
return new AllTypes.NumberType()
|
||||
case ts.SyntaxKind.ObjectKeyword:
|
||||
return new AllTypes.ObjectType()
|
||||
case ts.SyntaxKind.StringKeyword:
|
||||
return new AllTypes.StringType()
|
||||
case ts.SyntaxKind.Parameter:
|
||||
let n = toSimpleType(typeNode.type)
|
||||
n.name = typeNode.name.original.escapedText
|
||||
n.isDotDotDot = !!typeNode.dotDotDotToken
|
||||
n.isQuestion = !!typeNode.questionToken
|
||||
return n
|
||||
case ts.SyntaxKind.TypeReference:
|
||||
if (!typeNode.typeArguments) {
|
||||
// If there are no typeArguments, this is not a parametric type and we can return the type directly
|
||||
try {
|
||||
return toSimpleType(
|
||||
typeNode.typeName.symbol.declarations[0].type,
|
||||
)
|
||||
} catch (e) {
|
||||
// Fall back to what you'd do with typeArguments
|
||||
}
|
||||
}
|
||||
let args = typeNode.typeArguments
|
||||
? typeNode.typeArguments.map(t =>
|
||||
toSimpleType(typeNode.typeArguments[0]),
|
||||
)
|
||||
: []
|
||||
return new AllTypes.TypeReferenceType(
|
||||
typeNode.typeName.escapedText,
|
||||
args,
|
||||
)
|
||||
case ts.SyntaxKind.FunctionType:
|
||||
// generics = (typeNode.typeParameters || []).map(p => new AllTypes.SimpleType(p))
|
||||
return new AllTypes.FunctionType(
|
||||
typeNode.parameters.map(p => toSimpleType(p)),
|
||||
toSimpleType(typeNode.type),
|
||||
)
|
||||
case ts.SyntaxKind.TypeLiteral:
|
||||
let members = typeNode.members
|
||||
.map(member => {
|
||||
if (member.kind == ts.SyntaxKind.IndexSignature) {
|
||||
// Something like this: { [str: string]: string[] }
|
||||
return ["", toSimpleType(member.type)]
|
||||
}
|
||||
// Very fun feature: when you have an object literal with >20 members, typescript will decide to replace some of them with a "... X more ..." node that obviously doesn't have a corresponding symbol, hence this check and the filter after the map
|
||||
if (member.name.symbol)
|
||||
return [
|
||||
member.name.symbol.escapedName,
|
||||
toSimpleType(member.type),
|
||||
]
|
||||
})
|
||||
.filter(m => m)
|
||||
return new AllTypes.ObjectType(new Map(members))
|
||||
case ts.SyntaxKind.ArrayType:
|
||||
return new AllTypes.ArrayType(toSimpleType(typeNode.elementType))
|
||||
case ts.SyntaxKind.TupleType:
|
||||
return new AllTypes.TupleType(
|
||||
typeNode.elementTypes.map(t => toSimpleType(t)),
|
||||
)
|
||||
case ts.SyntaxKind.UnionType:
|
||||
return new AllTypes.UnionType(
|
||||
typeNode.types.map(t => toSimpleType(t)),
|
||||
)
|
||||
break
|
||||
case ts.SyntaxKind.LiteralType:
|
||||
return new AllTypes.LiteralTypeType(typeNode.literal.text)
|
||||
break
|
||||
default:
|
||||
console.log(typeNode)
|
||||
throw new Error(`Unhandled kind (${typeNode.kind}) for ${typeNode}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** True if node is visible outside its file, false otherwise */
|
||||
function isNodeExported(node: ts.Node): boolean {
|
||||
return (
|
||||
(ts.getCombinedModifierFlags(<ts.Declaration>node) &
|
||||
ts.ModifierFlags.Export) !==
|
||||
0 ||
|
||||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile)
|
||||
)
|
||||
}
|
||||
|
||||
/** True if node is marked as @hidden in its documentation */
|
||||
function isNodeHidden(sourceFile, node): boolean {
|
||||
return (
|
||||
sourceFile &&
|
||||
node.jsDoc &&
|
||||
!!node.jsDoc.find(
|
||||
doc =>
|
||||
sourceFile.text.slice(doc.pos, doc.end).search("@hidden") != -1,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function visit(
|
||||
checker: any,
|
||||
sourceFile: any,
|
||||
file: AllMetadata.FileMetadata,
|
||||
node: any,
|
||||
) {
|
||||
let symbol = checker.getSymbolAtLocation(node.name)
|
||||
if (symbol && isNodeExported(node)) {
|
||||
let nodeName = symbol.escapedName
|
||||
|
||||
switch (node.kind) {
|
||||
case ts.SyntaxKind.FunctionDeclaration:
|
||||
// Grab the doc, default to empty string
|
||||
let doc =
|
||||
ts.displayPartsToString(symbol.getDocumentationComment()) ||
|
||||
""
|
||||
// Grab the type
|
||||
let ttype = checker.getTypeOfSymbolAtLocation(
|
||||
symbol,
|
||||
symbol.valueDeclaration!,
|
||||
)
|
||||
// If the function has a type, try to convert it, if it doesn't, default to any
|
||||
let t = ttype
|
||||
? toSimpleType(checker.typeToTypeNode(ttype))
|
||||
: new AllTypes.AnyType()
|
||||
file.setFunction(
|
||||
nodeName,
|
||||
new AllMetadata.SymbolMetadata(
|
||||
doc,
|
||||
t,
|
||||
isNodeHidden(sourceFile, node),
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
case ts.SyntaxKind.ClassDeclaration:
|
||||
let clazz = file.getClass(nodeName)
|
||||
if (!clazz) {
|
||||
clazz = new AllMetadata.ClassMetadata()
|
||||
file.setClass(nodeName, clazz)
|
||||
}
|
||||
symbol.members.forEach((sym, name, map) => {
|
||||
// Can't get doc/type from these special functions
|
||||
// Or at least, it requires work that might not be needed for now
|
||||
if (["__constructor", "get", "set"].includes(name)) return
|
||||
|
||||
// Grab the doc, default to empty string
|
||||
let doc =
|
||||
ts.displayPartsToString(
|
||||
sym.getDocumentationComment(),
|
||||
) || ""
|
||||
// Grab the type
|
||||
let ttype = checker.getTypeOfSymbolAtLocation(
|
||||
sym,
|
||||
sym.valueDeclaration!,
|
||||
)
|
||||
// If the function has a type, try to convert it, if it doesn't, default to any
|
||||
let t = ttype
|
||||
? toSimpleType(checker.typeToTypeNode(ttype))
|
||||
: new AllTypes.AnyType()
|
||||
clazz.setMember(
|
||||
name,
|
||||
new AllMetadata.SymbolMetadata(
|
||||
doc,
|
||||
t,
|
||||
isNodeHidden(sourceFile, node),
|
||||
),
|
||||
)
|
||||
})
|
||||
return
|
||||
|
||||
// Other declaration syntaxkinds:
|
||||
// case ts.SyntaxKind.VariableDeclaration:
|
||||
// case ts.SyntaxKind.VariableDeclarationList:
|
||||
// case ts.SyntaxKind.PropertyDeclaration:
|
||||
// case ts.SyntaxKind.MethodDeclaration:
|
||||
// case ts.SyntaxKind.EndOfDeclarationMarker:
|
||||
// case ts.SyntaxKind.MergeDeclarationMarker:
|
||||
// case ts.SyntaxKind.MissingDeclaration:
|
||||
// case ts.SyntaxKind.ClassExpression:
|
||||
// case ts.SyntaxKind.InterfaceDeclaration:
|
||||
// case ts.SyntaxKind.TypeAliasDeclaration:
|
||||
// case ts.SyntaxKind.EnumDeclaration:
|
||||
// case ts.SyntaxKind.ModuleDeclaration:
|
||||
// case ts.SyntaxKind.ImportEqualsDeclaration:
|
||||
// case ts.SyntaxKind.ImportDeclaration:
|
||||
// case ts.SyntaxKind.NamespaceExportDeclaration:
|
||||
// case ts.SyntaxKind.ExportDeclaration:
|
||||
// case ts.SyntaxKind.Constructor:
|
||||
}
|
||||
}
|
||||
|
||||
ts.forEachChild(node, node => visit(checker, sourceFile, file, node))
|
||||
}
|
||||
|
||||
function generateMetadata(
|
||||
out: string,
|
||||
themedir: string,
|
||||
fileNames: string[],
|
||||
): void {
|
||||
/* Parse Tridactyl */
|
||||
let program = ts.createProgram(fileNames, {
|
||||
target: ts.ScriptTarget.ES5,
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
})
|
||||
|
||||
let metadata = new AllMetadata.ProgramMetadata()
|
||||
|
||||
for (const sourceFile of program.getSourceFiles()) {
|
||||
let name = (fileNames as any).find(name =>
|
||||
sourceFile.fileName.match(name),
|
||||
)
|
||||
if (name) {
|
||||
let file = metadata.getFile(name)
|
||||
if (!file) {
|
||||
file = new AllMetadata.FileMetadata()
|
||||
metadata.setFile(name, file)
|
||||
}
|
||||
visit(program.getTypeChecker(), sourceFile, file, sourceFile)
|
||||
}
|
||||
}
|
||||
|
||||
// We need to specify Type itself because it won't exist in AllTypes.js since it's an interface
|
||||
let imports =
|
||||
`import { Type } from "../compiler/types/AllTypes"\n` +
|
||||
`import {${Object.keys(AllTypes).join(
|
||||
", ",
|
||||
)}} from "../compiler/types/AllTypes"\n` +
|
||||
`import {${Object.keys(AllMetadata).join(
|
||||
", ",
|
||||
)}} from "../compiler/metadata/AllMetadata"\n`
|
||||
|
||||
let metadataString =
|
||||
imports + `\nexport let everything = ${metadata.toConstructor()}\n`
|
||||
|
||||
if (themedir) {
|
||||
metadataString += `\nexport let staticThemes = ${JSON.stringify(
|
||||
fs.readdirSync(themedir),
|
||||
)}\n`
|
||||
}
|
||||
|
||||
// print out the doc
|
||||
fs.writeFileSync(out, metadataString)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let opts = commandLineArgs([
|
||||
{ name: "out", type: String },
|
||||
{ name: "themeDir", type: String },
|
||||
{ name: "src", type: String, multiple: true, defaultOption: true },
|
||||
])
|
||||
|
||||
if (!opts.out || opts.src.length < 1)
|
||||
throw new Error(
|
||||
"Argument syntax: --out outfile [--src] file1.ts [file2.ts ...]",
|
||||
)
|
||||
|
||||
generateMetadata(opts.out, opts.themeDir, opts.src)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
export { SymbolMetadata } from "./SymbolMetadata"
|
||||
export { ClassMetadata } from "./ClassMetadata"
|
||||
export { FileMetadata } from "./FileMetadata"
|
||||
export { ProgramMetadata } from "./ProgramMetadata"
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
|
||||
import { SymbolMetadata } from "./SymbolMetadata"
|
||||
|
||||
export class ClassMetadata {
|
||||
constructor(
|
||||
public members: Map<string, SymbolMetadata> = new Map<
|
||||
string,
|
||||
SymbolMetadata
|
||||
>(),
|
||||
) {}
|
||||
|
||||
public setMember(name: string, s: SymbolMetadata) {
|
||||
this.members.set(name, s)
|
||||
}
|
||||
|
||||
public getMember(name: string) {
|
||||
return this.members.get(name)
|
||||
}
|
||||
|
||||
public getMembers() {
|
||||
return this.members.keys()
|
||||
}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new ClassMetadata(new Map<string, SymbolMetadata>([` +
|
||||
Array.from(this.members.entries())
|
||||
.map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { ClassMetadata } from "./ClassMetadata"
|
||||
import { SymbolMetadata } from "./SymbolMetadata"
|
||||
|
||||
export class FileMetadata {
|
||||
constructor(
|
||||
public classes: Map<string, ClassMetadata> = new Map<
|
||||
string,
|
||||
ClassMetadata
|
||||
>(),
|
||||
public functions: Map<string, SymbolMetadata> = new Map<
|
||||
string,
|
||||
SymbolMetadata
|
||||
>(),
|
||||
) {}
|
||||
|
||||
public setClass(name: string, c: ClassMetadata) {
|
||||
this.classes.set(name, c)
|
||||
}
|
||||
|
||||
public getClass(name: string) {
|
||||
return this.classes.get(name)
|
||||
}
|
||||
|
||||
public getClasses() {
|
||||
return Array.from(this.classes.keys())
|
||||
}
|
||||
|
||||
public setFunction(name: string, f: SymbolMetadata) {
|
||||
this.functions.set(name, f)
|
||||
}
|
||||
|
||||
public getFunction(name: string) {
|
||||
return this.functions.get(name)
|
||||
}
|
||||
|
||||
public getFunctions() {
|
||||
return Array.from(this.functions.entries())
|
||||
}
|
||||
|
||||
public getFunctionNames() {
|
||||
return Array.from(this.functions.keys())
|
||||
}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new FileMetadata(new Map<string, ClassMetadata>([` +
|
||||
Array.from(this.classes.entries())
|
||||
.map(([n, c]) => `[${JSON.stringify(n)}, ${c.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]), new Map<string, SymbolMetadata>([` +
|
||||
Array.from(this.functions.entries())
|
||||
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { FileMetadata } from "./FileMetadata"
|
||||
|
||||
export class ProgramMetadata {
|
||||
constructor(
|
||||
public files: Map<string, FileMetadata> = new Map<
|
||||
string,
|
||||
FileMetadata
|
||||
>(),
|
||||
) {}
|
||||
|
||||
public setFile(name: string, file: FileMetadata) {
|
||||
this.files.set(name, file)
|
||||
}
|
||||
|
||||
public getFile(name: string) {
|
||||
return this.files.get(name)
|
||||
}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new ProgramMetadata(new Map<string, FileMetadata>([` +
|
||||
Array.from(this.files.entries())
|
||||
.map(([n, f]) => `[${JSON.stringify(n)}, ${f.toConstructor()}]`)
|
||||
.join(",\n") +
|
||||
`]))`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { Type } from "../types/AllTypes"
|
||||
|
||||
export class SymbolMetadata {
|
||||
constructor(public doc: string, public type: Type, public hidden = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new SymbolMetadata(${JSON.stringify(
|
||||
this.doc,
|
||||
)}, ${this.type.toConstructor()}, ${this.hidden})`
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
export { Type } from "./Type"
|
||||
export { AnyType } from "./AnyType"
|
||||
export { BooleanType } from "./BooleanType"
|
||||
export { FunctionType } from "./FunctionType"
|
||||
export { NumberType } from "./NumberType"
|
||||
export { ObjectType } from "./ObjectType"
|
||||
export { StringType } from "./StringType"
|
||||
export { TypeReferenceType } from "./TypeReferenceType"
|
||||
export { VoidType } from "./VoidType"
|
||||
export { ArrayType } from "./ArrayType"
|
||||
export { LiteralTypeType } from "./LiteralTypeType"
|
||||
export { TupleType } from "./TupleType"
|
||||
export { UnionType } from "./UnionType"
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class AnyType implements Type {
|
||||
public kind = "any"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new AnyType(${!this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
return argument
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class ArrayType implements Type {
|
||||
public kind = "array"
|
||||
|
||||
constructor(public elemType: Type, public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new ArrayType(${this.elemType.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `${this.elemType.toString()}[]`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (!Array.isArray(argument)) {
|
||||
try {
|
||||
argument = JSON.parse(argument)
|
||||
} catch (e) {
|
||||
throw new Error(`Can't convert ${argument} to array:`)
|
||||
}
|
||||
if (!Array.isArray(argument)) {
|
||||
throw new Error(`Can't convert ${argument} to array:`)
|
||||
}
|
||||
}
|
||||
return argument.map(v => this.elemType.convert(v))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class BooleanType implements Type {
|
||||
public kind = "boolean"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new BooleanType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (argument === "true") {
|
||||
return true
|
||||
} else if (argument === "false") {
|
||||
return false
|
||||
}
|
||||
throw new Error("Can't convert ${argument} to boolean")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class FunctionType implements Type {
|
||||
public kind = "function"
|
||||
|
||||
constructor(public args: Type[], public ret: Type, public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new FunctionType([` +
|
||||
// Convert every argument type to its string constructor representation
|
||||
this.args.map(cur => cur.toConstructor()) +
|
||||
`], ${this.ret.toConstructor()}, ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `(${this.args.map(a => a.toString()).join(", ")}) => ${this.ret.toString()}`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
// Possible strategies:
|
||||
// - eval()
|
||||
// - window[argument]
|
||||
// - tri.excmds[argument]
|
||||
throw new Error(`Conversion to function not implemented: ${argument}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class LiteralTypeType implements Type {
|
||||
public kind = "LiteralType"
|
||||
|
||||
constructor(public value: string, public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new LiteralTypeType(${JSON.stringify(this.value)}, ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return JSON.stringify(this.value)
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (argument === this.value) {
|
||||
return argument
|
||||
}
|
||||
throw new Error(
|
||||
`Argument does not match expected value (${
|
||||
this.value
|
||||
}): ${argument}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class NumberType implements Type {
|
||||
public kind = "number"
|
||||
|
||||
public constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new NumberType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
const n = parseFloat(argument)
|
||||
if (!Number.isNaN(n)) {
|
||||
return n
|
||||
}
|
||||
throw new Error(`Can't convert to number: ${argument}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class ObjectType implements Type {
|
||||
public kind = "object"
|
||||
|
||||
// Note: a map that has an empty key ("") uses the corresponding type as default type
|
||||
constructor(public members: Map<string, Type> = new Map<string, Type>(), public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new ObjectType(new Map<string, Type>([` +
|
||||
Array.from(this.members.entries()).map(([n, m]) => `[${JSON.stringify(n)}, ${m.toConstructor()}]`)
|
||||
.join(", ") +
|
||||
`]), ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convertMember(memberName: string[], memberValue: string) {
|
||||
let type = this.members.get(memberName[0])
|
||||
if (!type) {
|
||||
// No type, try to get the default type
|
||||
type = this.members.get("")
|
||||
if (!type) {
|
||||
// No info for this member and no default type, anything goes
|
||||
return memberValue
|
||||
}
|
||||
}
|
||||
if (type.kind === "object") {
|
||||
return (type as ObjectType).convertMember(memberName.slice(1), memberValue)
|
||||
}
|
||||
return type.convert(memberValue)
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
try {
|
||||
return JSON.parse(argument)
|
||||
} catch (e) {
|
||||
throw new Error(`Can't convert to object: ${argument}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class StringType implements Type {
|
||||
public kind = "string"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new StringType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (typeof argument === "string") {
|
||||
return argument
|
||||
}
|
||||
throw new Error(`Can't convert to string: ${argument}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class TupleType implements Type {
|
||||
public kind = "tuple"
|
||||
|
||||
constructor(public elemTypes: Type[], public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new TupleType([` +
|
||||
// Convert every element type to its constructor representation
|
||||
this.elemTypes.map(cur => cur.toConstructor()).join(",\n") +
|
||||
`], ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `[${this.elemTypes.map(e => e.toString()).join(", ")}]`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
if (!Array.isArray(argument)) {
|
||||
try {
|
||||
argument = JSON.parse(argument)
|
||||
} catch (e) {
|
||||
throw new Error(`Can't convert to tuple: ${argument}`)
|
||||
}
|
||||
if (!Array.isArray(argument)) {
|
||||
throw new Error(`Can't convert to tuple: ${argument}`)
|
||||
}
|
||||
}
|
||||
if (argument.length !== this.elemTypes.length) {
|
||||
throw new Error(
|
||||
`Error converting tuple: number of elements and type mismatch ${argument}`,
|
||||
)
|
||||
}
|
||||
return argument.map((v, i) => this.elemTypes[i].convert(v))
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
|
||||
export interface Type {
|
||||
// Only available on argument types
|
||||
name?: string
|
||||
isDotDotDot?: boolean
|
||||
isQuestion?: boolean
|
||||
// available everywhere
|
||||
kind: string
|
||||
toConstructor(): string
|
||||
toString(): string
|
||||
convert(argument: string): any
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class TypeReferenceType implements Type {
|
||||
public constructor(public kind: string, public args: Type[], public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new TypeReferenceType(${JSON.stringify(this.kind)}, [` +
|
||||
// Turn every type argument into its constructor representation
|
||||
this.args.map(cur => cur.toConstructor()).join(",\n") +
|
||||
`], ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return `${this.kind}<${this.args.map(a => a.toString()).join(", ")}>`
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
throw new Error("Conversion of simple type references not implemented.")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class UnionType implements Type {
|
||||
public kind = "union"
|
||||
|
||||
constructor(public types: Type[], public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return (
|
||||
`new UnionType([` +
|
||||
// Convert every type to its string constructor representation
|
||||
this.types.map(cur => cur.toConstructor()).join(",\n") +
|
||||
`], ${this.isDotDotDot}, ${this.isQuestion})`
|
||||
)
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.types.map(t => t.toString()).join(" | ")
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
for (const t of this.types) {
|
||||
try {
|
||||
return t.convert(argument)
|
||||
} catch (e) {}
|
||||
}
|
||||
throw new Error(`Can't convert "${argument}" to any of: ${this.types}`)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
import { Type } from "./Type"
|
||||
|
||||
export class VoidType implements Type {
|
||||
public kind = "void"
|
||||
|
||||
constructor(public isDotDotDot = false, public isQuestion = false) {}
|
||||
|
||||
public toConstructor() {
|
||||
return `new VoidType(${this.isDotDotDot}, ${this.isQuestion})`
|
||||
}
|
||||
|
||||
public toString() {
|
||||
return this.kind
|
||||
}
|
||||
|
||||
public convert(argument) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
3
contrib/readme.md
Normal file
3
contrib/readme.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Tridactyl `contrib`
|
||||
|
||||
Not sure what we'll put in this directory, but it won't be as polished as the rest of Tridactyl.
|
||||
112
contrib/themes/reader/newspaper.css
Normal file
112
contrib/themes/reader/newspaper.css
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Sidescrolling newspaper-style layout for :reader
|
||||
*
|
||||
* Author: bovine3dom
|
||||
*
|
||||
* Usage: `:colourscheme --module=reader --url=[path to raw version of this file] newspaper`
|
||||
*
|
||||
*/
|
||||
|
||||
:root {
|
||||
--serif-font: EconomistSerif, Plantin, Garamond, serif;
|
||||
}
|
||||
|
||||
html:not(.TridactylCommandline) {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
font-family: var(--serif-font);
|
||||
}
|
||||
|
||||
:not(.TridactylCommandline) {
|
||||
|
||||
body {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 2rem;
|
||||
column-width: 24rem;
|
||||
column-gap: 3rem;
|
||||
column-fill: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
|
||||
body > main {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
body > header,
|
||||
body > footer,
|
||||
figure, section, article, .notice,
|
||||
h1, h2, h3, h4, h5, h6,
|
||||
img, video, table {
|
||||
break-inside: avoid;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 { break-after: avoid-column; }
|
||||
h1 + *, h2 + *, h3 + *, h4 + *, h5 + *, h6 + * { break-before: avoid-column; }
|
||||
|
||||
img, video, table, pre, figure {
|
||||
display: block;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
img, video, table {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table, pre, figure, img {
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 6rem);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
body > header {
|
||||
display: block;
|
||||
width: 100%;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
body > footer {
|
||||
display: block;
|
||||
margin-top: 4rem;
|
||||
border-top: 2px solid var(--accent);
|
||||
background-color: var(--accent-bg);
|
||||
}
|
||||
|
||||
body > header h1,
|
||||
body > header p {
|
||||
margin: 0.5rem auto;
|
||||
}
|
||||
|
||||
body > header h1 { max-width: 100%; }
|
||||
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.7rem; }
|
||||
|
||||
h2, h3 { margin-top: 0.5rem; }
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
html, body {
|
||||
height: auto;
|
||||
display: block;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
columns: 1 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body > main {
|
||||
display: block;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
body > header {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@ Tridactyl is very lucky to have a wide base of contributors, 30 at the time of w
|
|||
- You could work on some feature that you really want to see in Tridactyl that we haven't even thought of yet.
|
||||
- Our build process is a bit convoluted, but [excmds.ts][excmds] is probably where you want to start. Most of the business happens there.
|
||||
- We use TypeDoc to produce the `:help` page. Look at the other functions in [excmds.ts][excmds] to get an idea of how to use it; if your function is not supposed to called from the command line, then please add `/** @hidden */` above it to prevent it being shown on the help page.
|
||||
- Our pre-commit hook runs prettier to format your code. Please don't circumvent it.
|
||||
- Email addresses mentioned in commits must be monitored. Do not use `noreply` addresses. If you have a good reason why you cannot provide a monitored email address, you can ask a maintainer to adopt your commits under their email address.
|
||||
|
||||
If you are making a substantial or potentially controversial change, your first port of call should be to stop by and chat to us on [Matrix][matrix] or file an issue to discuss what you would like to change. We really don't want you to waste time on a pull request (GitHub jargon for a contribution) that has no chance of being merged; that said, we are probably happy to gate even the most controversial changes behind an option.
|
||||
|
||||
|
|
|
|||
90
custom-eslint-rules/lib/unsupported-apis.js
Normal file
90
custom-eslint-rules/lib/unsupported-apis.js
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
const bcd = require("@mdn/browser-compat-data")
|
||||
const api = bcd.webextensions.api
|
||||
|
||||
function propertyNameOrValue(n) {
|
||||
return n.property.type == "Literal" ? n.property.value : n.property.name
|
||||
}
|
||||
|
||||
function isVersionNewer(version, minimumVersion) {
|
||||
const versionParts = String(version).split(".").map(Number)
|
||||
const minimumParts = String(minimumVersion).split(".").map(Number)
|
||||
if (versionParts.some(Number.isNaN) || minimumParts.some(Number.isNaN))
|
||||
return false
|
||||
const length = Math.max(versionParts.length, minimumParts.length)
|
||||
for (let i = 0; i < length; i++) {
|
||||
const difference = (versionParts[i] || 0) - (minimumParts[i] || 0)
|
||||
if (difference !== 0) return difference > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function detectBrowserUsage(context, node, browser, minimumVersion) {
|
||||
let localApi = api
|
||||
const fullName = []
|
||||
while (
|
||||
node.type == "MemberExpression" &&
|
||||
propertyNameOrValue(node) in localApi
|
||||
) {
|
||||
const n = node
|
||||
node = node.parent
|
||||
const name = propertyNameOrValue(n)
|
||||
fullName.push(name)
|
||||
localApi = localApi[name]
|
||||
if (!localApi.__compat) {
|
||||
continue
|
||||
}
|
||||
const support = localApi.__compat.support
|
||||
if (support[browser].version_added === false) {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "unsupportedApis",
|
||||
data: {
|
||||
name: browser,
|
||||
api: fullName.join("."),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
const version = support[browser].version_added
|
||||
if (
|
||||
minimumVersion !== undefined &&
|
||||
isVersionNewer(version, minimumVersion)
|
||||
) {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "apiTooRecent",
|
||||
data: {
|
||||
api: fullName.join("."),
|
||||
name: browser,
|
||||
version: minimumVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = browser => ({
|
||||
meta: {
|
||||
schema: [
|
||||
{
|
||||
type: "object",
|
||||
properties: { minimumVersion: { type: "string" } },
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
messages: {
|
||||
unsupportedApis: "{{ api }} unsupported on '{{ name }}'",
|
||||
apiTooRecent:
|
||||
"{{ api }} is not supported on {{ name }} {{ version }}",
|
||||
},
|
||||
},
|
||||
create(context) {
|
||||
const { minimumVersion } = context.options[0] || {}
|
||||
const detect = node =>
|
||||
detectBrowserUsage(context, node, browser, minimumVersion)
|
||||
return {
|
||||
'MemberExpression[object.name="browser"]': detect,
|
||||
'MemberExpression[object.name="browserBg"]': detect,
|
||||
}
|
||||
},
|
||||
})
|
||||
24
custom-eslint-rules/readme.md
Normal file
24
custom-eslint-rules/readme.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# adding a new target
|
||||
|
||||
make a file here like
|
||||
|
||||
```js
|
||||
// unsupported-apis-mosaic.js
|
||||
module.exports = require("./lib/unsupported-apis")("mosaic")
|
||||
```
|
||||
|
||||
|
||||
add a line to `../browser-targets.json`
|
||||
|
||||
```json
|
||||
"mosaic": { "minimumVersion": "1.0.0" }
|
||||
```
|
||||
|
||||
add a few lines to `../.eslintrc.js`
|
||||
|
||||
```js
|
||||
"unsupported-apis-mosaic": [
|
||||
"warn",
|
||||
{ "minimumVersion": browserTargets.mosaic.minimumVersion }
|
||||
],
|
||||
```
|
||||
1
custom-eslint-rules/unsupported-apis-chrome.js
Normal file
1
custom-eslint-rules/unsupported-apis-chrome.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./lib/unsupported-apis")("chrome")
|
||||
1
custom-eslint-rules/unsupported-apis-firefox.js
Normal file
1
custom-eslint-rules/unsupported-apis-firefox.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
module.exports = require("./lib/unsupported-apis")("firefox")
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
const bcd = require('@mdn/browser-compat-data');
|
||||
const api = bcd.webextensions.api;
|
||||
const supported_browsers = ["firefox", "chrome"];
|
||||
const minimalSupportedFirefoxVersion = 114;
|
||||
|
||||
function propertyNameOrValue(n) {
|
||||
return (n.property.type == "Literal" ? n.property.value : n.property.name)
|
||||
}
|
||||
|
||||
function detectBrowserUsage(context, node) {
|
||||
let localApi = api;
|
||||
let fullName = [];
|
||||
while (node.type == "MemberExpression" && propertyNameOrValue(node) in localApi) {
|
||||
const n = node;
|
||||
node = node.parent;
|
||||
let name = propertyNameOrValue(n);
|
||||
fullName.push(name);
|
||||
localApi = localApi[name];
|
||||
if (!localApi.__compat) {
|
||||
continue;
|
||||
}
|
||||
let support = localApi.__compat.support;
|
||||
for (let browser of supported_browsers) {
|
||||
if (support[browser].version_added === false) {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "unsupportedApis",
|
||||
data: {
|
||||
name: browser,
|
||||
api: fullName.join("."),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const version = Number(support[browser].version_added);
|
||||
if (!isNaN(version) && version > minimalSupportedFirefoxVersion) {
|
||||
context.report({
|
||||
node: n,
|
||||
messageId: "apiTooRecent",
|
||||
data: {
|
||||
api: fullName.join("."),
|
||||
version: minimalSupportedFirefoxVersion
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
messages: {
|
||||
unsupportedApis: "{{ api }} unsupported on '{{ name }}'",
|
||||
apiTooRecent: "{{ api }} is not supported on firefox {{ version }}",
|
||||
}
|
||||
},
|
||||
create(context) {
|
||||
return {
|
||||
'MemberExpression[object.name="browser"]': (n) => detectBrowserUsage(context, n),
|
||||
'MemberExpression[object.name="browserBg"]': (n) => detectBrowserUsage(context, n),
|
||||
};
|
||||
}
|
||||
};
|
||||
10
doc/amo.md
10
doc/amo.md
|
|
@ -1,6 +1,6 @@
|
|||
**Control your browser with your keyboard _only_.**
|
||||
|
||||
Replace Firefox's control mechanism with one modelled on VIM. This is a "Firefox Quantum" replacement for VimFX, Vimperator and Pentadactyl. Most common tasks you want your browser to perform are bound to a single key press:
|
||||
Replace Firefox's control mechanism with one modelled on VIM. We were heavily inspired by VimFX, Vimperator and Pentadactyl, but more modern equivalents are Vimium or qutebrowser. Most common tasks you want your browser to perform are bound to a single key press:
|
||||
|
||||
- You want to open a new tab? Hit `t`.
|
||||
- You want to follow that link? Hit `f` and type the displayed label. (Note: hint characters should be typed in lowercase.)
|
||||
|
|
@ -52,14 +52,16 @@ Since Tridactyl aims to provide all the features Vimperator and Pentadactyl had,
|
|||
- Access recently closed tabs:
|
||||
- If you've accidentally closed a tab or window, Tridactyl will let you open it again with the `:undo` command which is bound to `u` by default.
|
||||
- Access browser tabs:
|
||||
- Tridactyl provides a quick tab-switching menu/command with the `:buffer` command (bound to `b`). This permission is also required to close, move, and pin tabs, amongst other things.
|
||||
- Tridactyl provides a quick tab-switching menu/command with the `:tab` command (bound to `b`). This permission is also required to close, move, and pin tabs, amongst other things.
|
||||
- Access browser activity during navigation:
|
||||
- This is needed for Tridactyl to be able to go back to normal mode every time you open a new page. In the future we may use it for autocommands.
|
||||
- This is needed for Tridactyl to be able to go back to normal mode every time you open a new page. It is also used for autocommands.
|
||||
- Read the text of all open tabs:
|
||||
- This allows us to use Firefox's built-in find-in-page API, for, for example, allowing you to bind find-next and find-previous to `n` and `N`.
|
||||
- Monitor extension usage and manage themes:
|
||||
- Tridactyl needs this to integrate with and avoid conflicts with other extensions. For example, Tridactyl's contextual identity features use this to cooperate with the Multi-Account Containers extension.
|
||||
- Hide tabs:
|
||||
- Hide and show browser tabs:
|
||||
- Tridactyl needs this for tab group commands, which allow associating names with different groups of tabs and showing the tabs from only of those groups at a time.
|
||||
- Control browser proxy settings:
|
||||
- This allows you to set proxies for accessing different websites with :autocontain.
|
||||
|
||||
[betas]: https://tridactyl.cmcaine.co.uk/betas/?sort=time&order=desc
|
||||
|
|
|
|||
44
doc/glossary.jsonl
Normal file
44
doc/glossary.jsonl
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{"word": "mozilla", "definition": "The foundation that controls Firefox and Thunderbird."}
|
||||
{"word": "bovine3dom", "definition": "The esteemed maintainer of Tridactyl and author of this glossary."}
|
||||
{"word": "vimperator", "definition": "A now defunct Firefox extension that was the main inspiration for Tridactyl."}
|
||||
{"word": "commandline", "definition": "A text-based interface to Tridactyl for running commands interactively and selecting from completion options, also called ex-mode. By default, bound to `:`"}
|
||||
{"word": "completion", "definition": "An object that can be selected from the commandline to complete a partially typed command, such as a URL for `:open` or a tab for `:tab`"}
|
||||
{"word": "keybind", "definition": "A relationship between a key press or series of key presses and a command"}
|
||||
{"word": "keysequence", "definition": "A series of key presses or key combinations that are descibed using a special language where, e.g. `<C-j>` represents `ctrl + j`"}
|
||||
{"word": "keydown", "definition": "An event triggered when a key is first pressed, represented in keysequeences as `<D-[character]>`"}
|
||||
{"word": "keyup", "definition": "The event triggered when a key is released, represented in keysequences as `<U-[character]>`"}
|
||||
{"word": "keypress", "definition": "An event triggered when a key is pressed and periodically while it is held down, represented in keysequences as `[character]`"}
|
||||
{"word": "pentadactyl", "definition": "A now defunct fork of Vimperator and one of the key inspirations for Tridactyl."}
|
||||
{"word": "treestyletabs", "definition": "A popular Firefox extension that adds a vertical tree of tabs to the user interface"}
|
||||
{"word": "tridactyl", "definition": "The best Firefox extension on the planet and the one you are using right now. Accept no imitations!"}
|
||||
{"word": "userchrome", "definition": "A semi-secret file that Firefox reads, if the preferences are set correctly, and allows you to edit the GUI. Tridactyl has a helper function for this called `:guiset`"}
|
||||
{"word": "tridactylrc", "definition": "A 'run commands' file that simply runs a series of commands at startup. Many users use these files to 'store' their settings, but it is important to remember that it really is just a bunch of commands to run at startup. See `:source`, `:mkt` and `:native`"}
|
||||
{"word": "xclip", "definition": "A command line utility that allows you to manipulate the clipboard under Linux and X11. See `:help yankto` and `:help putfrom`"}
|
||||
{"word": "xsel", "definition": "A command line utility that allows you to manipulate the clipboard under Linux and X11. See `:help yankto` and `:help putfrom`"}
|
||||
{"word": "sandboxed", "definition": "A security feature that prevents processes such as Firefox from accessing the file system, which breaks most of `:native` without some extra fiddling and whitelisting. 'snap' and 'flatpak' are common forms of sandboxing."}
|
||||
{"word": "github", "definition": "A popular code hosting site (aka 'forge') that allows you to store, share and discuss code with other users. At the time of writing, it's where Tridactyl was mostly hosted, although it's important to remember that `git` is decentralised and so really Tridactyl is hosted by developer who has ever cloned it"}
|
||||
{"word": "mappings", "definition": "Another word for keybinds"}
|
||||
{"word": "rebind", "definition": "To change the keybind a command is associated with"}
|
||||
{"word": "gitter", "definition": "A now mostly dead chat service that has been replaced by Matrix, but still works"}
|
||||
{"word": "deprecated", "definition": "A command or setting that will eventually be removed from Tridactyl, but works for now"}
|
||||
{"word": "emacs", "definition": "An operating system that only lacks a decent text editor. Inexplicably popular despite the fact that `nano` and `notepad.exe` are superior and pre-installed on most machines"}
|
||||
{"word": "popup", "definition": "A small window that appears in the middle of the screen, usually without consent to display something that the website wishes to annoy you with. Thankfully, web browsers block them universally, and so now they appear within webpages instead in the form of newsletter signups and 'cookie consent' dialogs, which is vastly better than having easily detectable new windows created because ... there's got to be a reason, right? Anyway, don't use the internet without an adblocker like uBlock Origin installed, kids"}
|
||||
{"word": "javascript", "definition": "Brendan Eich's long-running joke played on humanity. Tragically, it is also the language in which we are forced to write Tridactyl."}
|
||||
{"word": "firefox", "definition": "The web browser you're almost certainly using right now"}
|
||||
{"word": "vim", "definition": "The best text editor in the universe, second only to 'ex'"}
|
||||
{"word": "libera", "definition": "An IRC network that became popular after Freenode imploded. Come talk with us on #tridactyl like it's 2002"}
|
||||
{"word": "wiki", "definition": "Tridactyl's largely unmoderated and well hidden wiki, stored on GitHub"}
|
||||
{"word": "layout", "definition": "Which button inserts which character on a keyboard. You can change this in software."}
|
||||
{"word": "rc", "definition": "Shorthand for 'tridactylrc'"}
|
||||
{"word": "sidebar", "definition": "Part of the Firefox UI that appears on the left and extensions can put things in. We mostly use it for `:escapehatch` to get focus back onto the page, but `:sidebaropen` exists and has some enthusiastic users"}
|
||||
{"word": "addon", "definition": "A Firefox extension such as Tridactyl"}
|
||||
{"word": "matrix", "definition": "A chat network"}
|
||||
{"word": "pipe", "definition": "A way of sending data between commands, usually represented by `|`"}
|
||||
{"word": "messenger", "definition": "A small program that runs in userland and makes `:native` work for various tasks such as opening URLs Mozilla thinks are dangerous, starting system utilities like `xclip` or restarting the browser"}
|
||||
{"word": "ex", "definition": "The original line-based text editor upon which Vim was initially based. The commandline is called 'ex mode' in honour of this, commands are called 'ex commands' and we sometimes call series of ex commands 'ex scripts'"}
|
||||
{"word": "marks", "definition": "A way of remembering a position or URL"}
|
||||
{"word": "issue", "definition": "A bug report, feature request or question on GitHub. We often talk about 'filing' issues to mean creating them"}
|
||||
{"word": "recently", "definition": "Anything within the last decade"}
|
||||
{"word": "control", "definition": "The key marked 'ctrl' on your keyboard. Unlike many other programs, we really do mean the key marked 'ctrl', and not the splat key on MacOS, which we call 'meta'"}
|
||||
{"word": "jumplist", "definition": "A kind of history of bits of pages you have been on, accessible with `<C-o>` and `<C-i>`, and `g;` for text fields you have edited"}
|
||||
{"word": "sponsor", "definition": "Someone who gives money to bovine3dom so he can keep working on Tridactyl and spend his time writing definitions for the glossary like he is doing right now"}
|
||||
55
doc/newsletters/10-winter-2024.md
Normal file
55
doc/newsletters/10-winter-2024.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Tridactyl news - Winter 2024
|
||||
|
||||
Hello,
|
||||
|
||||
Welcome to the tenth Tridactyl newsletter! It looks like I've fallen into a routine of doing them biennially; I'll try to make them at least once a year because we're adding stuff to Tridactyl at such a rate that it becomes a little difficult to summarise it all. On top of this, I wrote this newsletter in around December 2024 but somehow managed to forget to release it, although it was on a public git branch. But everything in it is still relevant, so I am publishing it now in December 2025. And I will aim to write an actually new newsletter in January/February. Thanks as ever for your patience :)
|
||||
|
||||
What follows is a brief exploration of the changes in Tridactyl since the last newsletter came out in February 2023.
|
||||
|
||||
## Highlighted new features
|
||||
|
||||
I'll go through a few of my favourite new features, ones that I myself am using daily. The first is that we added a new `:reader` mode, left unbound by default because while we take every precaution, running it on untrusted websites opens up a larger attack surface than using the built-in Firefox reader (which remains accessible on the `gr` bind and with the `:reader --old` command). You can of course bind to the new mode with `:bind gr reader`, which is what I personally do. This lets you use all of the Tridactyl commands and interface on a minimal "reader" style interface and seems in general to work on slightly more websites than the built-in mode. I use it on websites that go slightly overboard with the cookie warnings, modal newsletter pop-ups and banner adverts ... that is to say, most of them :)
|
||||
|
||||
A totally different but equally transformative feature we've added is the ability to sort tabs everywhere in Tridactyl in a "most recently used" fashion. By running `:set tabsort mru`, not only will `b`/`:tab` completions be sorted by recency, but within Tridactyl internally all the commands will use recency indices too, meaning that for example `[n]gt` will take you to the nth-most-recently-used tab rather than the tab with index n. I found it took about a week to get used to but now I can't imagine using Firefox and Tridactyl in any other way. It makes doing housekeeping on tabs easy too, because one can just press shift-tab to get to the end of the `:tab` list in recency to find the ones that haven't been used in a while, and then close them one by one with shift-delete.
|
||||
|
||||
For something a little more niche, Tridactyl's support for non-English keyboard layouts has got a little better with the addition of `:set keyboardlayoutforce true`. This forces Tridactyl to see your keyboard as if it was a different layout - by default, US, but configurable via `:set keyboardlayoutbase`. This means that for example, pressing the home row key for the right hand will always be interpreted as j, which is bound by default to scrolling down. If you're frequently switching between keyboard layouts so you can easily type in different languages (for example, colemak and bépo), your Tridactyl binds will stay in the same physical location.
|
||||
|
||||
A cool command that I've been using a lot personally and will be of interest to anyone who uses a mobile device is the new `yq` bind, which temporarily displays the current URL as a QR code. There's also a `q` bind in visual mode and a `:text2qr` command for general programmatic use.
|
||||
|
||||
A small quality of life improvement is that `:open` and related commands now have completions for searchurls and previous searches.
|
||||
|
||||
A few improvements have come to hints: there's now `:set hintstyles.{bg, fg, outline}` for changing the style of hints without changing your theme, `;c` and `;:` for context menu / mouse hover binds added for `:native` and `xdotool` users. There are also new flags: `-x` for excluding CSS selectors and `-C` for including extra CSS selectors alongside the defaults.
|
||||
|
||||
With the command line open, `<C-o>t` opens a new tab in the background for the selected completion which I find especially useful with `:back` completions. Don't forget that all binds for the command line can be viewed with `:viewconfig exmaps` and in `:bind --mode=ex ` completions - I think most people are unaware of them, which is a shame because `<S-Delete>` to close `:tab ` completions is one of my favourite Tridactyl features.
|
||||
|
||||
A pretty odd feature that I am not totally unconvinced of the utility of is that we now have experimental support for opening arbitrary pages in the sidebar. See `:help sidebartoggle` and `:help sidebaropen` for more details; it works somewhat well with `:hint -W sidebaropen` but again I'm still not sure what it's useful for. Feel free to tell us if you do find it useful.
|
||||
|
||||
An extremely niche feature is the new `:jsua` command which preserves "user action" intent when using browser binds, needed for triggering certain Web Extension APIs such as `browser.sidebarAction.open()`. I suspect there are about three people in the world who care about this :). We've also added `tri.bg` and `tri.tabs` for easy communication between tab and background contexts in `:jsb` and `:js`, see their help pages for more information.
|
||||
|
||||
Something that I suspect will interest people who don't read this newsletter more than people who do is that we've started the groundwork for multi-browser support in our linting scripts. We haven't considered how to go about Manifest V3 support at all, so support for Chromium would still be a long way off, even if we wanted to. But it's nice that the option to slowly port to Chromium-based browsers who maintain support for Manifest V2 is there.
|
||||
|
||||
For those of you who want to talk about Tridactyl to other people, you'll be happy to know that docs are now built and hosted [on our website](https://tridactyl.xyz/build/static/docs/modules/_src_excmds_.html). I find I use it a lot when helping people on Discord/IRC/Matrix, maybe you will too.
|
||||
|
||||
## Neat bug fixes
|
||||
|
||||
We've finally fixed a feature that had been broken in Tridactyl for years - the grey hints for elements that have Javascript event handlers associated with them now work again. On some sites, this means that there are lots of duplicated hints - just run `:hint -J` (with perhaps `:bindurl ... f hint -J`) to exclude them from the displayed hints.
|
||||
|
||||
`:guiset gui none` works again in Firefox 133 - you may find that you need to rerun it.
|
||||
|
||||
In an apparent attempt to make me hate React even more than I already did, server-side rendering on React and Next.js started vandalising Tridactyl. I had to write a workaround so that Tridactyl reinserts itself when it detects that server-side React is running. Our detection isn't foolproof so may need to use `:seturl ... commandlineterriblewebsitefix true` to force Tridactyl to reinsert itself proactively. Since a fair number of the readers of this newsletter are probably web-developers, this might be a good time to remind you all that [friends don't let friends use React](https://infrequently.org/2024/11/if-not-react-then-what/). There are plenty of modern approaches to web development that don't spend their time reimplementing basic browser features, like Svelte or Lit. I have to say that my opinions on this hardened slightly after I was forced to write some code to deal with React tearing up the whole web page because the user agent dared to make some modifications to it.
|
||||
|
||||
This one probably only affects a small number of you, but `:apropos` will now work even if you have created a custom setting, which is important, because it's a great command for exploring Tridactyl :)
|
||||
|
||||
A bug that had been annoying me for most of the life of Tridactyl is that keypresses typed too quickly after a command opened the commandline would be lost. We now buffers character keypresses which has improved this a lot (although we can still lose some keys before the buffer starts collecting them, depending on how quick your computer is).
|
||||
|
||||
Most `searchurls` have been removed because people (especially me) found them annoying, you can add them back with `:set searchurls.[name] [url]` if there are any that you find yourself missing.
|
||||
|
||||
Finally, and most importantly, I added my [mastodon account](https://masto.ai/@bovine3dom) to the new tab page so that people can listen to me complain about about trains.
|
||||
|
||||
## Plans for next few months (years?)
|
||||
|
||||
I don't have anything terribly exciting planned for Tridactyl at the moment - we've got 82 pull requests open at the time of writing which is about 70 more than I would like, so I think I'll start trying to go through them and see what can be merged or freshened up and what should be closed. It's been a long time since I picked issues out at random to fix them so I'll probably set aside a weekend or two for that soon. At the back of my mind, I always think about rewriting our completion/commandline to be more deterministic and more configurable but the trouble is that it kind of works OK so it doesn't seem worth the time required to get it going. I would also like to fix up the ["keyup" bind PR](https://github.com/tridactyl/tridactyl/pull/4727) since that would enable real smooth scrolling which seems to be one of the main barriers to people using Tridactyl, but keybinds are such a key part of Tridactyl that the idea of touching that code and breaking it slightly is honestly a little bit terrifying. And then finally I still have [godmode](https://github.com/tridactyl/godmode) on my wishlist, which should get Tridactyl running on PDF and other pages like Firefox's own extensions such as the screenshot tool, for people who are prepared to patch Firefox.
|
||||
|
||||
As always, thanks for your generous support,
|
||||
|
||||
bovine3dom and the rest of the Tridactyl developers
|
||||
|
|
@ -8,10 +8,22 @@ If changing one of these settings fixes your bug, please visit the corresponding
|
|||
|
||||
- `:seturl $URL_OF_THE_WEBSITE superignore true` and then reload the page. This totally disables Tridactyl from loading on the page. No specific issue, please make a new one: https://github.com/tridactyl/tridactyl/issues/821
|
||||
|
||||
- "i can't open the commandline :((" `:seturl [the website you're on] commmandlineterriblewebsitefix true`. No need to file an issue with us, but maybe consider telling the website owner that they should [make their website less bad](https://infrequently.org/2024/11/if-not-react-then-what/). Our relevant issue is [#5050](https://github.com/tridactyl/tridactyl/issues/5050)
|
||||
|
||||
# Firefox settings that can break Tridactyl
|
||||
|
||||
If you have `privacy.resistFingerprinting` set to `true` in `about:config`, Tridactyl will have a lot of trouble understanding your keypresses. See [#760](https://github.com/tridactyl/tridactyl/issues/760#issuecomment-433679201) and [#1699](https://github.com/tridactyl/tridactyl/issues/1699). We strongly recommend setting it to `false`, as it is by default.
|
||||
|
||||
Selecting **Never remember history** in the History section of Firefox's Privacy & Security settings enables permanent private browsing. Tridactyl then treats every window as private and cannot persist state such as command history or global marks. Choose another history setting unless this behaviour is intended.
|
||||
|
||||
# Keyboard layout issues
|
||||
|
||||
Tridactyl's completion-aware `<Space>` binding can interfere with dead-key composition in the command line. Run `:unbind --mode=ex <Space>` to let Firefox handle Space normally, then `:bind --mode=ex <S-Space> ex.insert_character_or_completion` to move completion to Shift-Space. [#5061](https://github.com/tridactyl/tridactyl/issues/5061)
|
||||
|
||||
# RC file issues
|
||||
|
||||
If `:source` loads unexpected settings or does not reflect your changes, run `:findrc` to display the local RC file selected when no path is supplied. Check that path for an old or duplicate RC file.
|
||||
|
||||
# Native Editor/Messenger issues
|
||||
|
||||
If you're having trouble running your editor on OSX, you might be having \$PATH issues: [#684](https://github.com/tridactyl/tridactyl/issues/684). The solution is to specify the absolute path to your editor, like this: `:set editorcmd /usr/local/bin/vimr`.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
getDriver,
|
||||
getDriverAndProfileDirs,
|
||||
iframeLoaded,
|
||||
quitDrivers,
|
||||
sendKeys,
|
||||
} from "./utils"
|
||||
|
||||
|
|
@ -22,9 +23,7 @@ describe("webdriver", () => {
|
|||
driver = await getDriver()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await driver.quit()
|
||||
})
|
||||
afterEach(quitDrivers)
|
||||
|
||||
interface Tab {
|
||||
active: boolean
|
||||
|
|
@ -124,11 +123,14 @@ describe("webdriver", () => {
|
|||
"elem.innerText=`%u`;" +
|
||||
"document.body.appendChild(elem)<CR>",
|
||||
)
|
||||
await driver.executeScript(`
|
||||
["news/rss.xml", "views/atom.xml", "pews/rss.xml", "tews/atom.xml"].forEach(href => {
|
||||
const link = document.createElement("a")
|
||||
link.href = href
|
||||
document.body.appendChild(link)
|
||||
})`)
|
||||
|
||||
// First, make sure completions are offered
|
||||
await driver.get(
|
||||
"file:///" + process.cwd() + "/e2e_tests/html/rss.html",
|
||||
)
|
||||
const iframe = await iframeLoaded(driver)
|
||||
await sendKeys(driver, ":rssexec ")
|
||||
await driver.switchTo().frame(iframe)
|
||||
|
|
@ -196,8 +198,8 @@ describe("webdriver", () => {
|
|||
const { driver, newProfiles } = await getDriverAndProfileDirs()
|
||||
try {
|
||||
// Then, make sure `:guiset` is offering completions
|
||||
const iframe = await iframeLoaded(driver)
|
||||
await sendKeys(driver, ":guiset ")
|
||||
const iframe = await iframeLoaded(driver)
|
||||
await driver.switchTo().frame(iframe)
|
||||
const elements = await driver.findElements(
|
||||
By.className("GuisetCompletionOption"),
|
||||
|
|
@ -212,7 +214,7 @@ describe("webdriver", () => {
|
|||
`return document.getElementById("tridactyl-input").value`,
|
||||
),
|
||||
).toEqual(
|
||||
"userChrome.css written. Please restart Firefox to see the changes.",
|
||||
`userChrome.css written to ${newProfiles[0]}/chrome/userChrome.css. Please restart Firefox to see the changes.`,
|
||||
)
|
||||
const profile = newProfiles.find(async p =>
|
||||
(await fs.readdir(path.join(p, "chrome"))).find(files =>
|
||||
|
|
@ -222,8 +224,6 @@ describe("webdriver", () => {
|
|||
expect(profile).toBeDefined()
|
||||
} catch (e) {
|
||||
fail(e)
|
||||
} finally {
|
||||
await driver.quit()
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -368,7 +368,7 @@ describe("webdriver", () => {
|
|||
await untilTabUrlMatches(
|
||||
driver,
|
||||
newTab.id,
|
||||
new RegExp("^https://www.google.com/search.*qwant"),
|
||||
new RegExp("^https?:\/\/.*qwant", "i"),
|
||||
)
|
||||
} catch (e) {
|
||||
fail(e)
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Tridactyl RSS test page</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="news/rss.xml">
|
||||
<a href="views/atom.xml">
|
||||
<a href="pews/rss.xml">
|
||||
<a href="tews/atom.xml">
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,9 +3,10 @@ import * as path from "path"
|
|||
import * as os from "os"
|
||||
import * as process from "process"
|
||||
import { Browser, Builder, By, Key, WebDriver } from "selenium-webdriver"
|
||||
import { Driver, Options } from "selenium-webdriver/firefox"
|
||||
import { Driver, Options, ServiceBuilder } from "selenium-webdriver/firefox"
|
||||
import * as Until from "selenium-webdriver/lib/until"
|
||||
const env = process.env
|
||||
const drivers = new Set<Driver>()
|
||||
|
||||
/** Returns the path of the newest file in directory */
|
||||
export async function getNewestFileIn(directory: string): Promise<string> {
|
||||
|
|
@ -41,26 +42,41 @@ export async function getDriver() {
|
|||
)
|
||||
|
||||
const options = new Options()
|
||||
if (env["HEADLESS"]) {
|
||||
if (!env["HEADED"]) {
|
||||
options.addArguments("--headless")
|
||||
}
|
||||
const driver = new Builder()
|
||||
.forBrowser(Browser.FIREFOX)
|
||||
.setFirefoxOptions(options)
|
||||
// Required to evaluate scripts in extension pages; only for test browsers.
|
||||
.setFirefoxService(new ServiceBuilder().addArguments("--allow-system-access"))
|
||||
.build() as unknown as Driver
|
||||
drivers.add(driver)
|
||||
|
||||
// This will be the default tab.
|
||||
await driver.installAddon(extensionPath, true)
|
||||
// Wait until addon is loaded and :tutor is displayed
|
||||
await iframeLoaded(driver)
|
||||
const handles = await driver.getAllWindowHandles()
|
||||
// And wait a bit more otherwise Tridactyl won't be happy
|
||||
// Wait for multiple window handles to be available (extension may open in new tab)
|
||||
await driver.wait(async () => {
|
||||
const handles = await driver.getAllWindowHandles()
|
||||
return handles.length >= 2
|
||||
}, 10000)
|
||||
// Give the extension a bit more time to initialize
|
||||
await driver.sleep(500)
|
||||
let handles = await driver.getAllWindowHandles()
|
||||
// Handle edge case where extension loads in same tab (some headless configurations)
|
||||
if (handles.length === 1) {
|
||||
await driver.wait(async () => {
|
||||
const newHandles = await driver.getAllWindowHandles()
|
||||
return newHandles.length >= 2
|
||||
}, 10000)
|
||||
handles = await driver.getAllWindowHandles()
|
||||
}
|
||||
// Kill the original tab.
|
||||
await driver.switchTo().window(handles[0])
|
||||
await driver.close()
|
||||
// Switch back to the good tab.
|
||||
// Switch to the new tab (extension opens in new tab)
|
||||
await driver.switchTo().window(handles[1])
|
||||
await driver.wait(() => driver.executeScript<boolean>("return Boolean(window.tri)"), 10000)
|
||||
// Now return the window that we want to use.
|
||||
return driver
|
||||
}
|
||||
|
|
@ -83,6 +99,18 @@ export async function getDriverAndProfileDirs() {
|
|||
return { driver, newProfiles }
|
||||
}
|
||||
|
||||
export async function quitDrivers() {
|
||||
const results = await Promise.allSettled(
|
||||
[...drivers].map(driver =>
|
||||
driver.quit().finally(() => drivers.delete(driver)),
|
||||
),
|
||||
)
|
||||
const failure = results.find(result => result.status === "rejected")
|
||||
if (failure?.status === "rejected") {
|
||||
throw failure.reason
|
||||
}
|
||||
}
|
||||
|
||||
const vimToSelenium = {
|
||||
Down: Key.ARROW_DOWN,
|
||||
Left: Key.ARROW_LEFT,
|
||||
|
|
|
|||
111
funding.json
Normal file
111
funding.json
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
{
|
||||
"$schema": "https://fundingjson.org/schema/v1.1.0.json",
|
||||
"version": "v1.1.0",
|
||||
|
||||
"entity": {
|
||||
"type": "individual",
|
||||
"role": "owner",
|
||||
"name": "Oliver Blanthorn",
|
||||
"email": "freedom4cows@gmail.com",
|
||||
"description": "Creator and maintainer of Tridactyl, a popular vim-like web extension for Firefox, and various open-data/geospatial projects.",
|
||||
"webpageUrl": {
|
||||
"url": "https://github.com/bovine3dom"
|
||||
}
|
||||
},
|
||||
|
||||
"projects": [
|
||||
{
|
||||
"guid": "tridactyl",
|
||||
"name": "Tridactyl",
|
||||
"description": "A popular vim-like web extension for Firefox, spiritual successor to Vimperator/Pentadactyl",
|
||||
"webpageUrl": {
|
||||
"url": "https://github.com/tridactyl/tridactyl"
|
||||
},
|
||||
"repositoryUrl": {
|
||||
"url": "https://github.com/tridactyl/tridactyl"
|
||||
},
|
||||
"licenses": ["spdx:Apache-2.0"],
|
||||
"tags": ["browser-extensions", "developer-tools"]
|
||||
}
|
||||
],
|
||||
|
||||
"funding": {
|
||||
"channels": [
|
||||
{
|
||||
"guid": "github",
|
||||
"type": "payment-provider",
|
||||
"address": "https://github.com/bovine3dom/sponsors",
|
||||
"description": "For most people, the most efficient way of funding me - Microsoft covers all card/exchange fees"
|
||||
},
|
||||
{
|
||||
"guid": "liberapay",
|
||||
"type": "payment-provider",
|
||||
"address": "https://liberapay.com/bovine3dom"
|
||||
},
|
||||
{
|
||||
"guid": "bank",
|
||||
"type": "cash",
|
||||
"address": "freedom4cows@gmail.com",
|
||||
"description": "Contact me directly for my IBAN/BIC"
|
||||
},
|
||||
{
|
||||
"guid": "revolut",
|
||||
"type": "payment-provider",
|
||||
"address": "https://revolut.me/oab"
|
||||
},
|
||||
{
|
||||
"guid": "crypto",
|
||||
"type": "other",
|
||||
"address": "freedom4cows@gmail.com",
|
||||
"description": "Contact me directly for crypto donations because there are too many different coins to list :)"
|
||||
},
|
||||
{
|
||||
"guid": "paypal",
|
||||
"type": "payment-provider",
|
||||
"address": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY",
|
||||
"description": "NB: I only receive ~80% of the income after fees"
|
||||
},
|
||||
{
|
||||
"guid": "patreon",
|
||||
"type": "payment-provider",
|
||||
"address": "https://patreon.com/tridactyl",
|
||||
"description": "NB: I only receive ~60% of the income after fees and forced VAT"
|
||||
}
|
||||
],
|
||||
|
||||
"plans": [{
|
||||
"guid": "feedme",
|
||||
"status": "active",
|
||||
"name": "Monthly donations",
|
||||
"description": "Most of my living costs are monthly and so monthly donations massively help me to plan",
|
||||
"amount": 0,
|
||||
"currency": "EUR",
|
||||
"frequency": "monthly",
|
||||
"channels": ["github", "liberapay", "bank", "crypto", "revolut", "paypal", "patreon"]
|
||||
}],
|
||||
|
||||
"history": [
|
||||
{
|
||||
"year": 2023,
|
||||
"income": 4200,
|
||||
"taxes": 1000,
|
||||
"currency": "EUR",
|
||||
"description": "Approximate"
|
||||
},
|
||||
{
|
||||
"year": 2024,
|
||||
"income": 6500,
|
||||
"taxes": 1500,
|
||||
"currency": "EUR",
|
||||
"description": "Approximate"
|
||||
},
|
||||
{
|
||||
"year": 2025,
|
||||
"income": 5800,
|
||||
"taxes": 1400,
|
||||
"currency": "EUR",
|
||||
"description": "Approximate"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -14,12 +14,15 @@ source ./scripts/common.sh
|
|||
jsfiles=$(cachedTSLintFiles)
|
||||
otherfiles=$(cachedPrettierFiles)
|
||||
|
||||
# Exit early if no relevant files are staged
|
||||
if [ -z "$jsfiles" ] && [ -z "$otherfiles" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Running pre-commit hook..."
|
||||
|
||||
# Check if any of the files are ugly or contain a console.log call
|
||||
consoleFiles=$(noisy $jsfiles)
|
||||
uglyFiles="$(eslintUgly $jsfiles)"
|
||||
if [ ! -n "$uglyFiles" ]; then
|
||||
uglyFiles="$(prettierUgly $otherfiles)"
|
||||
fi
|
||||
|
||||
if [ -n "$consoleFiles" ]; then
|
||||
echo "Warning: adding console.log calls in ${consoleFiles[@]}"
|
||||
|
|
@ -27,10 +30,26 @@ if [ -n "$consoleFiles" ]; then
|
|||
echo
|
||||
fi
|
||||
|
||||
if [ -n "$uglyFiles" ]; then
|
||||
echo "Prettify your files first:"
|
||||
echo 'yarn run pretty'
|
||||
echo ''
|
||||
echo 'If you see this message repeatedly, skip the check with git commit -n'
|
||||
exit 1
|
||||
# # disabled because it was too noisy
|
||||
#
|
||||
# if [ -n "$jsfiles" ]; then
|
||||
# yarn git-format-staged \
|
||||
# --formatter "yarn --silent prettier --stdin-filepath \"{}\"" \
|
||||
# "*.ts" "*.tsx"
|
||||
# fi
|
||||
|
||||
# if [ -n "$otherfiles" ]; then
|
||||
# yarn git-format-staged \
|
||||
# --formatter "yarn --silent prettier --stdin-filepath \"{}\"" \
|
||||
# "*.md" "*.css"
|
||||
# fi
|
||||
|
||||
if [ -n "$jsfiles" ]; then
|
||||
echo "Linting staged files..."
|
||||
if ! yarn eslint --rulesdir custom-eslint-rules --quiet $jsfiles; then
|
||||
echo ""
|
||||
echo -e "eslint failed. Please fix the errors above."
|
||||
echo 'If you see this message repeatedly, skip the check with git commit -n'
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
const tsConfig = require('./tsconfig');
|
||||
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "jsdom",
|
||||
testRunner: "jest-jasmine2",
|
||||
setupFiles: [
|
||||
"jest-webextension-mock"
|
||||
],
|
||||
|
|
@ -9,16 +10,19 @@ module.exports = {
|
|||
"./e2e_tests/failfast.js"
|
||||
],
|
||||
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: {
|
||||
...tsConfig.compilerOptions,
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsconfig: {
|
||||
...tsConfig.compilerOptions,
|
||||
types: ["jest", "node", "@types/firefox-webext-browser"]
|
||||
},
|
||||
diagnostics: {
|
||||
ignoreCodes: [151001]
|
||||
},
|
||||
}
|
||||
},
|
||||
diagnostics: {
|
||||
ignoreCodes: [151001]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"@src/(.*)": "<rootDir>/src/$1"
|
||||
|
|
|
|||
53
package.json
53
package.json
|
|
@ -13,52 +13,52 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@mozilla/readability": "^0.5.0",
|
||||
"cleanslate": "^0.10.1",
|
||||
"cleanslate": "git+https://github.com/tridactyl/cleanslate.git#master",
|
||||
"compute-scroll-into-view": "^3.0.0",
|
||||
"csp-serdes": "github:cmcaine/csp-serdes",
|
||||
"css": "^3.0.0",
|
||||
"editor-adapter": "^0.0.5",
|
||||
"editor-adapter": "git+https://github.com/tridactyl/editor-adapter.git#master",
|
||||
"esbuild": "^0.20.2",
|
||||
"fuse.js": "^7.0.0",
|
||||
"nearley": "^2.20.1",
|
||||
"ramda": "^0.30.1",
|
||||
"reading-time": "^1.5.0",
|
||||
"semver-compare": "^1.0.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"tridactyl-arg": "git+https://github.com/GHolk/arg.git#v5.1.0",
|
||||
"tsdef": "^0.0.14",
|
||||
"typedoc": "^0.19.2",
|
||||
"typedoc-default-themes": "^0.12.10",
|
||||
"typedoc": "0.28.20",
|
||||
"xss": "^1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/css": "0.0.37",
|
||||
"@types/css": "0.0.38",
|
||||
"@types/firefox-webext-browser": "^120.0.4",
|
||||
"@types/jest": "^27.5.0",
|
||||
"@types/jest": "29.5.12",
|
||||
"@types/nearley": "^2.11.5",
|
||||
"@types/selenium-webdriver": "^4.1.10",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/eslint-plugin-tslint": "^6.21.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
"command-line-args": "^5.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.64.0",
|
||||
"@typescript-eslint/parser": "8.64.0",
|
||||
"command-line-args": "^6.0.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-jsdoc": "^48.2.1",
|
||||
"eslint-plugin-jsdoc": "^50.6.1",
|
||||
"eslint-plugin-prefer-arrow": "^1.2.3",
|
||||
"eslint-plugin-sonarjs": "^0.25.1",
|
||||
"geckodriver": "^4.3.3",
|
||||
"geckodriver": "^5.0.0",
|
||||
"git-format-staged": "^4.0.1",
|
||||
"jasmine-fail-fast": "^2.0.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest-webextension-mock": "^3.9.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"jest-jasmine2": "29.7.0",
|
||||
"jest-webextension-mock": "^4.0.0",
|
||||
"marked": "^12.0.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier": "^3.4.2",
|
||||
"selenium-webdriver": "^4.7.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"tslint": "^5.20.1",
|
||||
"tslint-etc": "^1.13.10",
|
||||
"tslint-sonarts": "^1.9.0",
|
||||
"typescript": "^3.9.10",
|
||||
"web-ext": "^7.10.0"
|
||||
"ts-jest": "29.4.11",
|
||||
"typescript": "6.0.3",
|
||||
"web-ext": "^7.10.0",
|
||||
"yaml-lint": "^1.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "sh scripts/build.sh",
|
||||
|
|
@ -70,10 +70,14 @@
|
|||
"make-zip": "web-ext build --source-dir build --overwrite-dest",
|
||||
"pretty": "bash scripts/pretty.sh",
|
||||
"run": "web-ext run -s build/ -u 'paste.to'",
|
||||
"runwithprofile": "node scripts/runwithprofile.js",
|
||||
"test": "yarn run build && web-ext build --source-dir ./build --overwrite-dest && jest --silent",
|
||||
"update-buildsystem": "rm -rf src/node_modules; yarn run clean",
|
||||
"watch": "echo 'watch is broken, use build instead'; exit 0;",
|
||||
"install": "git config core.hookspath hooks/"
|
||||
"install": "git config core.hookspath hooks/",
|
||||
"_dev": "yarn run rebuild && yarn run run",
|
||||
"dev": "ls **/*.ts | entr -r yarn run _dev",
|
||||
"githublint": "yamllint .github/workflows/**.yml"
|
||||
},
|
||||
"author": "Colin Caine",
|
||||
"repository": {
|
||||
|
|
@ -90,5 +94,6 @@
|
|||
"bugs": {
|
||||
"url": "https://github.com/tridactyl/tridactyl/issues"
|
||||
},
|
||||
"homepage": "https://github.com/tridactyl/tridactyl#readme"
|
||||
"homepage": "https://github.com/tridactyl/tridactyl#readme",
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
|||
40
readme.md
40
readme.md
|
|
@ -9,7 +9,6 @@ Tridactyl
|
|||
<h4 align="center">Replace Firefox's default control mechanism with one modelled on the one true editor, Vim.</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://travis-ci.org/tridactyl/tridactyl"><img src="https://travis-ci.org/tridactyl/tridactyl.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://matrix.to/#/#tridactyl:matrix.org"><img src="https://img.shields.io/badge/matrix-join%20chat-green" alt="Matrix Chat"></a>
|
||||
<a href="https://gitter.im/tridactyl/Lobby"><img src="https://badges.gitter.im/Join%20Chat.svg" alt="Join Gitter Chat"></a>
|
||||
<a href="https://discord.gg/DWbNGTAvmh"><img src="https://img.shields.io/discord/854326924402622474?color=%235865F2&label=discord" alt="Join Discord Chat"></a>
|
||||
|
|
@ -53,7 +52,7 @@ Tridactyl stable can be installed from the [Mozilla add-ons website (the AMO)][a
|
|||
|
||||
### Extra features through [Native Messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging)
|
||||
|
||||
If you want to use advanced features such as edit-in-Vim, you'll also need to install the native messenger or executable, instructions for which can be found by typing `:installnative` and hitting enter once you are in Tridactyl. Arch users can install the [AUR package](https://aur.archlinux.org/packages/firefox-tridactyl-native/) `firefox-tridactyl-native` instead.
|
||||
If you want to use advanced features such as edit-in-Vim, you'll also need to install the native messenger or executable, instructions for which can be found by typing `:nativeinstall` and hitting enter once you are in Tridactyl. Arch users can install the [AUR package](https://aur.archlinux.org/packages/firefox-tridactyl-native/) `firefox-tridactyl-native` instead.
|
||||
|
||||
#### Containerized/sandboxed Firefox Installations
|
||||
|
||||
|
|
@ -81,7 +80,7 @@ The changelog for the stable versions can be found [here](https://github.com/tri
|
|||
|
||||
Type `:help` or press `<F1>` for online help once you're in, or `:tutor` for a friendly introduction. You might also find the [unofficial Tridactyl Memrise course](https://app.memrise.com/community/course/5995499/tridactyls-main-shortcuts/) (requires login) useful for memorising keybinds.
|
||||
|
||||
Remember that Tridactyl cannot run on any page on about:\*, data:\*, view-source:\* and file:\*. We're sorry about that and we're working with Firefox to improve this situation by removing restrictions on existing APIs and developing a new API.
|
||||
Remember that Tridactyl cannot run on any page on about:\*, data:\*, view-source:\* and file:\*. We're sorry about that :(
|
||||
|
||||
If you're enjoying Tridactyl, or not, please leave a review on the [AMO](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/reviews/).
|
||||
|
||||
|
|
@ -111,11 +110,11 @@ You can try `:help key` to know more about `key`. If it is an existing binding,
|
|||
- `gi` — scroll to and focus the last-used input on the page
|
||||
- `r`/`R` — reload page or hard reload page
|
||||
- `yy` — copy the current page URL to the clipboard
|
||||
- `[[`/`]]` — navigate forward/backward though paginated pages, for example comics, multi-part articles, search result pages, etc.
|
||||
- `[[`/`]]` — navigate backward/forward though paginated pages, for example comics, multi-part articles, search result pages, etc.
|
||||
- `]c`/`[c` — increment/decrement the current URL by 1
|
||||
- `gu` — go to the parent of the current URL
|
||||
- `gU` — go to the root domain of the current URL
|
||||
- `gr` — open Firefox reader mode (note: Tridactyl will not work in this mode)
|
||||
- `gr` — open Firefox reader mode (note: Tridactyl will only work on our own `:reader` command which has complicated security implications)
|
||||
- `zi`/`zo`/`zz` — zoom in/out/reset zoom
|
||||
- `<C-f>`/`<C-b>` — jump to the next/previous part of the page
|
||||
- `g?` — Apply Caesar cipher to page (run `g?` again to switch back)
|
||||
|
|
@ -162,7 +161,7 @@ If you want to use Firefox's default `<C-b>` binding to open the bookmarks sideb
|
|||
- `u` — undo the last tab/window closure
|
||||
- `gt`/`gT` — go to the next/previous tab
|
||||
- `g^ OR g0`/`g$` — go to the first/last tab
|
||||
- `ga` — go to the tab currently playing audio
|
||||
- `ga` — go to the tab currently playing audio, or the one that most recently stopped
|
||||
- `<C-^>` — go to the last active tab
|
||||
- `b` — bring up a list of open tabs in the current window; you can type the tab ID or part of the title or URL to choose a tab
|
||||
|
||||
|
|
@ -185,7 +184,7 @@ Additionally, you can hint elements matching a custom CSS selector with `:hint -
|
|||
|
||||
### Binding custom commands
|
||||
|
||||
You can bind your own shortcuts in normal mode with the `:bind` command. For example `:bind J tabprev` to bind `J` to switch to the previous tab. See `:help bind` for details about this command.
|
||||
You can bind your own shortcuts in normal mode with the `:bind` command. For example `:bind gD composite tabduplicate; tabdetach` to duplicate and detach the current tab. See `:help bind` for details about this command.
|
||||
|
||||
## WebExtension-related issues
|
||||
|
||||
|
|
@ -210,6 +209,10 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
`:set searchengine esa`
|
||||
|
||||
Note that this does not apply to `:open` when run without arguments, as it simply opens the newtab page which can be set like this:
|
||||
|
||||
`:set newtab [newtab url]`, e.g. `:set newtab https://www.google.com`
|
||||
|
||||
- How can I add a search engine?
|
||||
|
||||
`:set searchurls.esa http://www.esa.int/esasearch?q=`
|
||||
|
|
@ -222,7 +225,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
- Can I import/export settings, and does Tridactyl use an external configuration file just like Vimperator?
|
||||
|
||||
Yes. `:source --url [URL]` accepts a URL (which must contain only an RC file, e.g. `raw.githubusercontent.com/...`). If you have `native` working, `$XDG_CONFIG_HOME/tridactyl/tridactylrc` or `~/.tridactylrc` will be read at startup via an `autocmd` and `source`. There is an [example file available on our repository](https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc).
|
||||
Yes. `:source --url [URL]` accepts a URL (which must contain only an RC file, e.g. `raw.githubusercontent.com/...`). If you have `native` working, `$XDG_CONFIG_HOME/tridactyl/tridactylrc` or `~/.tridactylrc` will be read at startup via an `autocmd` and `source`. Run `:findrc` to see which local RC file this automatic search selected. There is an [example file available on our repository](https://github.com/tridactyl/tridactyl/blob/master/.tridactylrc), or you can [search GitHub for other RC files](https://github.com/search?q=path%3Atridactylrc&type=code) (requires login).
|
||||
|
||||
There's also `mkt` which exports your configuration to a file which may be read with `source`. (NB: this currently requires `native`).
|
||||
|
||||
|
|
@ -231,6 +234,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
To use one of the built in themes use: `:colors <color>`. The current options are:
|
||||
|
||||
- default
|
||||
- auto
|
||||
- dark (authored by @furgerf)
|
||||
- shydactyl (authored by @atrnh)
|
||||
- greenmat (authored by @caputchinefrobles)
|
||||
|
|
@ -238,12 +242,14 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
- quake
|
||||
- quakelight
|
||||
- midnight (authored by @karizma)
|
||||
- vimium
|
||||
- tokyonight
|
||||
|
||||
Tridactyl can also load themes from disk or URL. You could use this for example to load one of the themes originally authored by @bezmi ([tridactyl/base16-tridactyl](https://github.com/tridactyl/base16-tridactyl)). See `:help colors` for more information.
|
||||
|
||||
- How to remap keybindings? or How can I bind keys using the control/alt key modifiers (eg: `ctrl+^`)?
|
||||
|
||||
You can remap keys in normal, ignore, input and insert mode with `:bind --mode=$mode $key $excmd`. Hint mode and the command line are currently special and can't be rebound. See `:help bind` for more information.
|
||||
You can remap keys with `:bind --mode=$mode $key $excmd`. See `:help bind` for more information.
|
||||
|
||||
Modifiers can be bound like this: `:bind <C-f> scrollpage 1`. Special keys can be bound too: `:bind <F3> colors dark` and with modifiers: `:bind <S-F3> colors default` and with multiple modifiers: `:bind <SA-F3> composite set hintchars 1234567890 | set hintfiltermode vimperator-reflow`
|
||||
|
||||
|
|
@ -275,7 +281,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
- Can I change proxy via commands?
|
||||
|
||||
Not yet, but this feature will eventually be implemented.
|
||||
Yes, see `:help proxyadd`
|
||||
|
||||
- How do I disable Tridactyl on certain sites?
|
||||
|
||||
|
|
@ -285,7 +291,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
- How can I list the current bindings?
|
||||
|
||||
`viewconfig nmaps` works OK, but Tridactyl commands won't work on the shown page for "security reasons". We'll eventually provide a better way. See [#98](https://github.com/tridactyl/tridactyl/issues/98).
|
||||
`viewconfig nmaps` works OK, but Tridactyl commands won't work on the shown page for "security reasons". We'll eventually provide a better way. See [#98](https://github.com/tridactyl/tridactyl/issues/98). You can also look at `:bind ` completions and `:apropos `
|
||||
|
||||
- How can I know which mode I'm in/have a status line?
|
||||
|
||||
|
|
@ -293,7 +299,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
- Does anyone actually use Tridactyl?
|
||||
|
||||
In addition to the developers, some other people do. Mozilla keeps tabs on stable users [here](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/statistics/?last=30), but, as of a while ago, you can't see that link if you aren't listed as a Tridactyl developer on the AMO. The maintainers guess the number of unstable users from unique IPs downloading the betas each week when they feel like it. Last time they checked there were 4600 of them.
|
||||
In addition to the developers, some other people do. Mozilla keeps tabs on stable users [here](https://addons.mozilla.org/en-US/firefox/addon/tridactyl-vim/statistics/?last=30), but, as of a while ago, you can't see that link if you aren't listed as a Tridactyl developer on the AMO. The maintainers guess the number of unstable users from unique IPs downloading the betas each week when they feel like it. Last time they checked there were 4600 of them. Unscientifically extrapolating from Arch linux's pop contest for package installations gives us an estimate of around 50,000 users.
|
||||
|
||||
- How do I prevent websites from stealing focus?
|
||||
|
||||
|
|
@ -303,7 +309,7 @@ You can bind your own shortcuts in normal mode with the `:bind` command. For exa
|
|||
|
||||
### Donations
|
||||
|
||||
We gratefully accept donations via [GitHub Sponsors](https://github.com/users/bovine3dom/sponsorship) (we receive 100% of your donation), [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY) (about 70% of your donation makes it to our bank account after fees) and [Patreon](https://www.patreon.com/tridactyl) (about 70% of your donation makes it to our account). If you can, please make this a monthly donation as it makes it much easier to plan. People who donate more than 10USD a month via GitHub or Patreon get a special monthly "tips and tricks" newsletter - see an example [here](https://github.com/tridactyl/tridactyl/blob/master/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md). All GitHub and Patreon donors get a quarterly newsletter on Tridactyl development.
|
||||
We gratefully accept donations via [GitHub Sponsors](https://github.com/users/bovine3dom/sponsorship) (we receive 100% of your donation), [Liberapay](https://liberapay.com/bovine3dom/) (about 95% of your donation makes it to our account), [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY) (about 70% of your donation makes it to our bank account after fees) and [Patreon](https://www.patreon.com/tridactyl) (about 70% of your donation makes it to our account). If you can, please make this a monthly donation as it makes it much easier to plan. People who donate more than 10USD a month via GitHub or Patreon get a special monthly "tips and tricks" newsletter - see an example [here](https://github.com/tridactyl/tridactyl/blob/master/doc/newsletters/tips-and-tricks/1-hint-css-selectors.md). All GitHub and Patreon donors get a quarterly newsletter on Tridactyl development.
|
||||
|
||||
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=7JQHV4N2YZCTY"><img src="https://www.paypalobjects.com/en_US/GB/i/btn/btn_donateCC_LG.gif" alt="PayPal"></a>
|
||||
|
||||
|
|
@ -343,6 +349,8 @@ If you want to build a signed copy (e.g. for the non-developer release), you can
|
|||
|
||||
You can build unsigned copies with `scripts/sign nosign{stable,beta}`. NB: The `stable` versus `beta` part of the argument tells our build process which extension ID to use (and therefore which settings to use). If you want a stable build, make sure you are on the latest tag, i.e. `git checkout $(git tag | grep '^[0-9]\+\.[0-9]\+\.[0-9]\+$' | sort -t. -k 1,1n -k 2,2n -k 3,3n | tail -1)`.
|
||||
|
||||
Maintainers create the next release after fetching tags with `scripts/version.js next {0,1,2} [release name]` and committing the changed manifest. Beta builds use that version while retaining the release name in their displayed version. Once its changelog entry is ready, `scripts/version.js release` dates the entry, commits it, and tags the version already in `src/manifest.json`.
|
||||
|
||||
If you are on a distribution which builds Firefox with `--with-unsigned-addon-scopes=` set to `app` and/or `system` (which is most of them by users: Arch, Debian, Ubuntu), you can install your unsigned copy of Tridactyl with `scripts/install.sh [directory]`. If you're on Arch, the correct directory is probably selected by default; on other distributions you might have to go hunting, but it probably looks like `/usr/lib/firefox/browser/extensions`.
|
||||
|
||||
### Building on Windows
|
||||
|
|
@ -351,13 +359,11 @@ If you are on a distribution which builds Firefox with `--with-unsigned-addon-sc
|
|||
|
||||
- Install [NodeJS for Windows][win-nodejs]
|
||||
|
||||
- Current 8.11.1 LTS seems to work fine
|
||||
|
||||
- Launch the installation steps described above from MinTTY shell
|
||||
- Also known as "Git Bash"
|
||||
|
||||
[win-git]: https://git-scm.com/download/win
|
||||
[win-nodejs]: https://nodejs.org/dist/v8.11.1/node-v8.11.1-x64.msi
|
||||
[win-nodejs]: https://nodejs.org/dist/
|
||||
[pyinstaller]: https://www.pyinstaller.org
|
||||
[gpg4win]: https://www.gpg4win.org
|
||||
|
||||
|
|
@ -375,7 +381,7 @@ You can speed up the build process after your first build by using `yarn run reb
|
|||
|
||||
### Committing
|
||||
|
||||
A pre-commit hook is added by `yarn install` that simply runs `yarn test`. If you know that your commit doesn't break the tests you can commit with `git commit -n` to ignore the hooks. If you're making a PR, travis will check your build anyway.
|
||||
A pre-commit hook is added by `yarn install` that simply runs `yarn test`. If you know that your commit doesn't break the tests you can commit with `git commit -n` to ignore the hooks. If you're making a PR, GitHub will check your build anyway once a maintainer has approved it.
|
||||
|
||||
### Documentation
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ authors="../../build/static/authors.html"
|
|||
sed "/REPLACETHIS/,$ d" authors.html > "$authors"
|
||||
|
||||
# If we're in a git repo, refresh the cache
|
||||
if [ -d "../../.git/" ]; then
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git shortlog -sn HEAD | cut -c8- | awk '!seen[$0]++' | sed 's/^/<p>/' | sed 's/$/<\/p>/' > ../../.build_cache/authors
|
||||
fi
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ mkdir -p build
|
|||
mkdir -p build/static
|
||||
mkdir -p generated/static
|
||||
mkdir -p generated/static/clippy
|
||||
node scripts/make_glossary.js
|
||||
|
||||
if [ "$(isWindowsMinGW)" = "True" ]; then
|
||||
$WIN_PYTHON scripts/excmds_macros.py
|
||||
|
|
@ -57,13 +58,19 @@ if [ "$QUICK_BUILD" != "1" ]; then
|
|||
"$(yarn bin)/nearleyc" src/grammars/bracketexpr.ne > \
|
||||
src/grammars/.bracketexpr.generated.ts
|
||||
|
||||
# It's important to generate the metadata before the documentation because
|
||||
# missing imports might break documentation generation on clean builds
|
||||
"$(yarn bin)/tsc" compiler/gen_metadata.ts -m commonjs --target es2017 \
|
||||
&& node compiler/gen_metadata.js \
|
||||
--out src/.metadata.generated.ts \
|
||||
--themeDir src/static/themes \
|
||||
src/excmds.ts src/lib/config.ts
|
||||
printf '{"version":1,"commands":{},"settings":{}}' > src/.metadata.generated.json
|
||||
printf 'export * from "./lib/metadata"\n' > src/.metadata.generated.ts
|
||||
node -e "var fs=require('fs');fs.writeFileSync('src/.themes.generated.json',JSON.stringify(fs.readdirSync('src/static/themes').sort()))"
|
||||
|
||||
# Generate runtime metadata via typedoc. The generated TypeScript shim
|
||||
# routes the public @src/.metadata.generated import to the JSON loader.
|
||||
"$(yarn bin)/typedoc" --json src/.metadata.generated.json \
|
||||
--excludeExternals --disableSources --readme none \
|
||||
--excludePrivate false --excludePrivateClassFields false \
|
||||
--validation.notExported false \
|
||||
--exclude 'src/.excmds_*.generated.ts' \
|
||||
src/excmds.ts src/lib/config.ts src/content/state_content.ts
|
||||
node scripts/convert_typedoc_metadata.js src/.metadata.generated.json
|
||||
|
||||
scripts/newtab.md.sh
|
||||
scripts/make_tutorial.sh
|
||||
|
|
@ -85,7 +92,7 @@ rmdir buildtemp
|
|||
|
||||
# Copy extra static files across
|
||||
|
||||
cp src/manifest.json build/
|
||||
node scripts/generate_manifest.js firefox src/manifest.json build/manifest.json
|
||||
cp -r src/static build
|
||||
cp -r generated/static build
|
||||
cp issue_template.md build/
|
||||
|
|
|
|||
|
|
@ -12,51 +12,12 @@ cachedPrettierFiles() {
|
|||
git diff --cached --name-only --diff-filter=ACM "*.md" "*.css"
|
||||
}
|
||||
|
||||
# Accepts a single argument which is the name of a file tracked by git
|
||||
# Returns a string which is the content of the file as stored in the git index
|
||||
staged() {
|
||||
git show :"$1"
|
||||
}
|
||||
|
||||
# Accepts a single string argument made of multiple file names separated by a newline
|
||||
# Returns an array of files that prettier wants to lint
|
||||
prettierUgly() {
|
||||
local acc=""
|
||||
local IFS=$'\n'
|
||||
for jsfile in $1; do
|
||||
diff <(staged "$jsfile") <(staged "$jsfile" | "$(yarn bin)/prettier" --stdin-filepath "$jsfile") >/dev/null || acc="$jsfile"$'\n'"$acc"
|
||||
done
|
||||
echo "$acc"
|
||||
}
|
||||
|
||||
eslintUgly() {
|
||||
local acc=""
|
||||
local IFS=$'\n'
|
||||
local tmpdir
|
||||
|
||||
mkdir -p ".tmp"
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
tmpdir=$(gmktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
|
||||
else
|
||||
tmpdir=$(mktemp --tmpdir=".tmp/" -d "tslint.XXXXXXXXX")
|
||||
fi
|
||||
|
||||
for jsfile in "$@"; do
|
||||
tmpfile="$tmpdir/$jsfile"
|
||||
mkdir -p "$(dirname "$tmpfile")"
|
||||
staged "$jsfile" > "$tmpfile"
|
||||
"$(yarn bin)/eslint" --rulesdir custom-eslint-rules --no-ignore --quiet -o /dev/null "$tmpfile" || acc="$jsfile"$'\n'"$acc"
|
||||
done
|
||||
rm -rf "$tmpdir"
|
||||
echo "$acc"
|
||||
}
|
||||
|
||||
noisy() {
|
||||
local acc=()
|
||||
local acc=""
|
||||
for jsfile in "$@"; do
|
||||
if [ "$(git diff --cached "$jsfile" | grep '^+.*console.log' -c)" -gt '0' ] ; then
|
||||
acc+=("jsfile")
|
||||
acc+="$acc$jsfile"$'\n'
|
||||
fi
|
||||
done
|
||||
echo "${acc[@]}"
|
||||
echo "$acc"
|
||||
}
|
||||
|
|
|
|||
210
scripts/convert_typedoc_metadata.js
Normal file
210
scripts/convert_typedoc_metadata.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs")
|
||||
|
||||
const TYPEDOC_SCHEMA_VERSION = "2.0"
|
||||
const METADATA_VERSION = 1
|
||||
const KIND = { Module: 2, Function: 64, Class: 128 }
|
||||
|
||||
const compareNames = (a, b) => (a < b ? -1 : a > b ? 1 : 0)
|
||||
const sortedRecord = entries =>
|
||||
Object.fromEntries(entries.sort(([a], [b]) => compareNames(a, b)))
|
||||
|
||||
function convertMetadata(project) {
|
||||
if (project.schemaVersion !== TYPEDOC_SCHEMA_VERSION)
|
||||
throw new Error(
|
||||
`Unsupported TypeDoc schemaVersion ${JSON.stringify(project.schemaVersion)}; expected ${TYPEDOC_SCHEMA_VERSION}`,
|
||||
)
|
||||
|
||||
const reflections = {}
|
||||
const collect = value => {
|
||||
if (!value || typeof value !== "object") return
|
||||
if (Array.isArray(value)) return value.forEach(collect)
|
||||
if (value.id !== undefined) reflections[value.id] = value
|
||||
Object.values(value).forEach(collect)
|
||||
}
|
||||
collect(project)
|
||||
|
||||
const accessor = node => node?.getSignature || node?.setSignature
|
||||
const memberType = node => node?.type || accessor(node)?.type
|
||||
const commentText = comment =>
|
||||
(comment?.summary || [])
|
||||
.map(part => part.text || "")
|
||||
.join("")
|
||||
.replace(/\n+$/, "")
|
||||
const argFlags = comment => {
|
||||
const flags = {}
|
||||
for (const tag of comment?.blockTags || []) {
|
||||
if (tag.tag !== "@flag") continue
|
||||
const text = (tag.content || [])
|
||||
.map(part => part.text || "")
|
||||
.join("")
|
||||
const flagText = text.split(/\r?\n[ \t]*\r?\n/, 1)[0]
|
||||
const m = /^(-\S+)[ \t]+([^\n]+)\n*([\s\S]*)$/.exec(flagText.trim())
|
||||
if (!m) continue
|
||||
const [, flag, short, rest] = m
|
||||
const elaboration = rest.trim()
|
||||
flags[flag] = {
|
||||
short,
|
||||
description: elaboration ? `${short}\n\n${elaboration}` : short,
|
||||
}
|
||||
}
|
||||
return flags
|
||||
}
|
||||
|
||||
const normalizeParameter = (parameter, resolving) => ({
|
||||
name: parameter.name,
|
||||
type: normalizeType(parameter.type, resolving),
|
||||
...(parameter.flags?.isRest ? { flags: { isRest: true } } : {}),
|
||||
})
|
||||
|
||||
const normalizeType = (type, resolving = new Set()) => {
|
||||
if (!type) return { type: "intrinsic", name: "any" }
|
||||
if (
|
||||
type.type === "reference" &&
|
||||
typeof type.target === "number" &&
|
||||
!type.typeArguments?.length &&
|
||||
reflections[type.target]?.type &&
|
||||
!resolving.has(type.target)
|
||||
) {
|
||||
const next = new Set(resolving)
|
||||
next.add(type.target)
|
||||
return normalizeType(reflections[type.target].type, next)
|
||||
}
|
||||
|
||||
switch (type.type) {
|
||||
case "intrinsic":
|
||||
return { type: "intrinsic", name: type.name }
|
||||
case "array":
|
||||
return {
|
||||
type: "array",
|
||||
elementType: normalizeType(type.elementType, resolving),
|
||||
}
|
||||
case "tuple":
|
||||
return {
|
||||
type: "tuple",
|
||||
elements: (type.elements || []).map(element =>
|
||||
normalizeType(element, resolving),
|
||||
),
|
||||
}
|
||||
case "union":
|
||||
return {
|
||||
type: "union",
|
||||
types: (type.types || []).map(member =>
|
||||
normalizeType(member, resolving),
|
||||
),
|
||||
}
|
||||
case "literal":
|
||||
return { type: "literal", value: type.value }
|
||||
case "reference":
|
||||
return {
|
||||
type: "reference",
|
||||
name: type.name,
|
||||
...(type.typeArguments?.length
|
||||
? {
|
||||
typeArguments: type.typeArguments.map(argument =>
|
||||
normalizeType(argument, resolving),
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
case "reflection": {
|
||||
const source = type.declaration || {}
|
||||
const declaration = {}
|
||||
const signature = source.signatures?.[0]
|
||||
if (signature)
|
||||
declaration.signatures = [
|
||||
{
|
||||
parameters: (signature.parameters || []).map(
|
||||
parameter =>
|
||||
normalizeParameter(parameter, resolving),
|
||||
),
|
||||
type: normalizeType(signature.type, resolving),
|
||||
},
|
||||
]
|
||||
|
||||
const children = (source.children || [])
|
||||
.map(child => {
|
||||
const type = memberType(child)
|
||||
return type
|
||||
? {
|
||||
name: child.name,
|
||||
type: normalizeType(type, resolving),
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => compareNames(a.name, b.name))
|
||||
if (children.length) declaration.children = children
|
||||
|
||||
const indexSignatures = (source.indexSignatures || []).map(
|
||||
signature => ({
|
||||
type: normalizeType(signature.type, resolving),
|
||||
}),
|
||||
)
|
||||
if (indexSignatures.length)
|
||||
declaration.indexSignatures = indexSignatures
|
||||
return { type: "reflection", declaration }
|
||||
}
|
||||
default:
|
||||
return { type: "intrinsic", name: "any" }
|
||||
}
|
||||
}
|
||||
|
||||
const modules = Object.fromEntries(
|
||||
(project.children || [])
|
||||
.filter(node => node.kind === KIND.Module)
|
||||
.map(node => [
|
||||
(node.name || "").replace(/^"|"$/g, "").replace(/\\/g, "/"),
|
||||
node,
|
||||
]),
|
||||
)
|
||||
const excmds = modules.excmds
|
||||
const configClass = modules["lib/config"]?.children?.find(
|
||||
node => node.kind === KIND.Class && node.name === "default_config",
|
||||
)
|
||||
if (!excmds || !configClass)
|
||||
throw new Error("TypeDoc output is missing excmds or default_config")
|
||||
|
||||
const commands = sortedRecord(
|
||||
(excmds.children || [])
|
||||
.filter(node => node.kind === KIND.Function)
|
||||
.map(node => {
|
||||
const signature = node.signatures?.[0]
|
||||
const comment = signature?.comment || node.comment
|
||||
const flags = argFlags(comment)
|
||||
return [
|
||||
node.name,
|
||||
{
|
||||
doc: commentText(comment),
|
||||
params: (signature?.parameters || []).map(parameter =>
|
||||
normalizeParameter(parameter),
|
||||
),
|
||||
...(Object.keys(flags).length ? { flags } : {}),
|
||||
},
|
||||
]
|
||||
}),
|
||||
)
|
||||
const settings = sortedRecord(
|
||||
(configClass.children || []).map(node => [
|
||||
node.name,
|
||||
{
|
||||
doc:
|
||||
commentText(accessor(node)?.comment) ||
|
||||
commentText(node.comment),
|
||||
type: normalizeType(memberType(node)),
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
return { version: METADATA_VERSION, commands, settings }
|
||||
}
|
||||
|
||||
module.exports = { convertMetadata, METADATA_VERSION, TYPEDOC_SCHEMA_VERSION }
|
||||
|
||||
if (require.main === module) {
|
||||
const file = process.argv[2]
|
||||
if (!file) throw new Error("Usage: convert_typedoc_metadata.js FILE")
|
||||
const metadata = convertMetadata(JSON.parse(fs.readFileSync(file, "utf8")))
|
||||
fs.writeFileSync(file, JSON.stringify(metadata))
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
const esbuild = require('esbuild')
|
||||
|
||||
for (let f of ["content", "background", "help", "newtab", "reader", "commandline_frame", "qrCodeGenerator"]) {
|
||||
for (let f of ["content", "background", "help", "newtab", "reader", "commandline_frame", "qrCodeGenerator", "browser_action_popup"]) {
|
||||
esbuild.build({
|
||||
entryPoints: [`src/${f}.ts`],
|
||||
bundle: true,
|
||||
|
|
|
|||
|
|
@ -201,5 +201,5 @@ def main():
|
|||
print(output.rstrip(), file=sink)
|
||||
|
||||
|
||||
PRELUDE = "/** Generated from excmds.ts. Don't edit this file! */"
|
||||
PRELUDE = "/* Generated from excmds.ts. Don't edit this file! */"
|
||||
main()
|
||||
|
|
|
|||
30
scripts/generate_manifest.js
Normal file
30
scripts/generate_manifest.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs")
|
||||
const targets = require("../browser-targets.json")
|
||||
|
||||
function generateManifest(template, targetName) {
|
||||
const target = targets[targetName]
|
||||
if (!target || !target.manifestVersionPath) {
|
||||
throw new Error(`No manifest settings for ${targetName}`)
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(JSON.stringify(template))
|
||||
const path = target.manifestVersionPath.slice()
|
||||
const property = path.pop()
|
||||
const parent = path.reduce((value, name) => {
|
||||
if (!value[name]) value[name] = {}
|
||||
return value[name]
|
||||
}, manifest)
|
||||
parent[property] = target.minimumVersion
|
||||
return manifest
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const [targetName, source, destination] = process.argv.slice(2)
|
||||
const template = JSON.parse(fs.readFileSync(source, "utf8"))
|
||||
const manifest = generateManifest(template, targetName)
|
||||
fs.writeFileSync(destination, JSON.stringify(manifest, null, 4) + "\n")
|
||||
}
|
||||
|
||||
module.exports = generateManifest
|
||||
|
|
@ -1,4 +1,24 @@
|
|||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
dest=generated/static/docs
|
||||
"$(yarn bin)/typedoc" --theme src/static/typedoc/ --exclude "src/**/?(test_utils|*.test).ts" --out $dest src --ignoreCompilerErrors
|
||||
cp -r $dest build/static/
|
||||
if revision=$(git rev-parse HEAD 2>/dev/null); then
|
||||
set -- --basePath src --disableGit --gitRevision "$revision" \
|
||||
--sourceLinkTemplate 'https://github.com/tridactyl/tridactyl/blob/{gitRevision}/src/{path}#L{line}'
|
||||
else
|
||||
set -- --disableSources
|
||||
fi
|
||||
"$(yarn bin)/typedoc" --plugin ./scripts/typedoc-theme.mjs \
|
||||
--theme tridactyl --router tridactyl \
|
||||
"$@" \
|
||||
--entryPointStrategy expand --validation.notExported false \
|
||||
--excludeInternal \
|
||||
--excludePrivate false --excludePrivateClassFields false \
|
||||
--includeHierarchySummary false \
|
||||
--exclude "src/**/?(test_utils|*.test).ts" \
|
||||
--out "$dest" src
|
||||
for command in next_completion prev_history accept_line; do
|
||||
grep -q "id=\"$command\"" "$dest/modules/_src_commandline_frame_.html"
|
||||
done
|
||||
rm -rf build/static/docs
|
||||
cp -r "$dest" build/static/
|
||||
|
|
|
|||
76
scripts/make_glossary.js
Normal file
76
scripts/make_glossary.js
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs")
|
||||
const path = require("path")
|
||||
const root = path.resolve(__dirname, "..")
|
||||
const source = path.join(root, "doc/glossary.jsonl")
|
||||
const entries = []
|
||||
const anchors = new Set()
|
||||
|
||||
for (const [index, line] of fs
|
||||
.readFileSync(source, "utf8")
|
||||
.split("\n")
|
||||
.entries()) {
|
||||
if (!line.trim()) continue
|
||||
let entry
|
||||
try {
|
||||
entry = JSON.parse(line)
|
||||
} catch (error) {
|
||||
throw new Error(`${source}:${index + 1}: ${error.message}`)
|
||||
}
|
||||
if (
|
||||
!entry ||
|
||||
typeof entry.word !== "string" ||
|
||||
typeof entry.definition !== "string" ||
|
||||
!entry.word.trim() ||
|
||||
!entry.definition.trim()
|
||||
)
|
||||
throw new Error(
|
||||
`${source}:${index + 1}: word and definition must be non-empty strings`,
|
||||
)
|
||||
const word = entry.word.trim().normalize("NFC")
|
||||
const definition = entry.definition.trim()
|
||||
const anchor = word.toLowerCase()
|
||||
if (anchors.has(anchor))
|
||||
throw new Error(
|
||||
`${source}:${index + 1}: duplicate word ${JSON.stringify(word)}`,
|
||||
)
|
||||
anchors.add(anchor)
|
||||
entries.push({ word, definition, anchor })
|
||||
}
|
||||
|
||||
if (!entries.length) throw new Error(`${source}: glossary must not be empty`)
|
||||
entries.sort((a, b) => (a.word < b.word ? -1 : a.word > b.word ? 1 : 0))
|
||||
const escape = value =>
|
||||
value.replace(/[&<>"']/g, char => `&#${char.charCodeAt(0)};`)
|
||||
const body = [
|
||||
"<h1>Glossary</h1>",
|
||||
"<p>Terms used in Tridactyl's documentation.</p>",
|
||||
'<dl class="glossary-list">',
|
||||
...entries.map(
|
||||
entry =>
|
||||
`<div class="glossary-entry" id="${escape(entry.anchor)}"><dt><code>${escape(entry.word)}</code></dt><dd>${escape(entry.definition)}</dd></div>`,
|
||||
),
|
||||
"</dl>",
|
||||
].join("\n")
|
||||
let html = fs.readFileSync(
|
||||
path.join(root, "src/static/clippy/tutor.template.html"),
|
||||
"utf8",
|
||||
)
|
||||
for (const [marker, replacement] of [
|
||||
["<title>Tridactyl Tutorial</title>", "<title>Tridactyl Glossary</title>"],
|
||||
['href="./glossary.html"', 'aria-current="page" href="./glossary.html"'],
|
||||
["REPLACETHIS", body],
|
||||
]) {
|
||||
if (html.split(marker).length !== 2)
|
||||
throw new Error(
|
||||
`Expected one ${JSON.stringify(marker)} in tutor.template.html`,
|
||||
)
|
||||
html = html.replace(marker, replacement)
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(root, "src/.glossary.generated.json"),
|
||||
JSON.stringify(entries),
|
||||
)
|
||||
fs.writeFileSync(path.join(root, "generated/static/clippy/glossary.html"), html)
|
||||
|
|
@ -13,8 +13,9 @@ dest="../../../generated/static/clippy/"
|
|||
for page in $pages
|
||||
do
|
||||
fileroot=$(echo "$page" | cut -d'.' -f-2)
|
||||
sed "/REPLACETHIS/,$ d" tutor.template.html > "$dest$fileroot.html"
|
||||
sed -e "s|href=\"$fileroot.html\"|aria-current=\"page\" &|" \
|
||||
-e "/REPLACETHIS/,$ d" tutor.template.html > "$dest$fileroot.html"
|
||||
"$(yarn bin)/marked" "$page" >> "$dest$fileroot.html"
|
||||
sed "1,/REPLACETHIS/ d" tutor.template.html >> "$dest$fileroot.html"
|
||||
sed -i.bak "s|\.md|.html|g" "$dest$fileroot.html"
|
||||
sed -i.bak "s|\(href=['\"]\./[^'\"]*\)\.md\(['\"]\)|\1.html\2|g" "$dest$fileroot.html"
|
||||
done
|
||||
|
|
|
|||
7
scripts/minify_json.js
Executable file
7
scripts/minify_json.js
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
require("fs").writeFileSync(
|
||||
process.argv[2],
|
||||
JSON.stringify(
|
||||
JSON.parse(require("fs").readFileSync(process.argv[2], "utf8")),
|
||||
),
|
||||
)
|
||||
|
|
@ -20,13 +20,14 @@ sed "1,/REPLACETHIS/ d" newtab.template.html >> "$newtabtemp"
|
|||
sed "/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/,$ d" "$newtabtemp"
|
||||
# Note: If you're going to change this HTML, make sure you don't break the JS in src/newtab.ts
|
||||
cat <<EOF
|
||||
<input type="checkbox" id="spoilerbutton" />
|
||||
<label for="spoilerbutton" onclick=""><div id="nagbar-changelog">New features!</div>Changelog</label>
|
||||
<details id="changelog-details">
|
||||
<summary><span id="nagbar-changelog">New features!</span>Changelog</summary>
|
||||
<div id="changelog" class="spoiler">
|
||||
EOF
|
||||
"$(yarn bin)/marked" ../../CHANGELOG.md
|
||||
echo """
|
||||
</div>
|
||||
</details>
|
||||
"""
|
||||
sed "1,/REPLACE_ME_WITH_THE_CHANGE_LOG_USING_SED/ d" "$newtabtemp"
|
||||
) > "$newtab"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@ echoe() {
|
|||
echo "$@" >&2
|
||||
}
|
||||
|
||||
staged() {
|
||||
git show :"$1"
|
||||
}
|
||||
|
||||
lock() {
|
||||
local lockfile="$1"
|
||||
if [ -e "$lockfile" ]; then
|
||||
|
|
@ -27,13 +31,14 @@ unlock() {
|
|||
trap 'unlock $(git rev-parse --show-toplevel)/.git/index.lock || true' ERR
|
||||
|
||||
main() {
|
||||
local stagedFiles
|
||||
local stagedFiles originalIndex
|
||||
stagedFiles="$(cachedTSLintFiles)"$'\n'"$(cachedPrettierFiles)"
|
||||
|
||||
if [ -n "$stagedFiles" ]; then
|
||||
# Could use git-update-index --cacheinfo to add a file without creating directories and stuff.
|
||||
IFS=$'\n'
|
||||
for file in $stagedFiles; do
|
||||
originalIndex=$(git write-tree)
|
||||
if cmp -s <(staged "$file") "$file"; then
|
||||
echo "WARN: Staged copy of '$file' matches working copy. Modifying both"
|
||||
echo "WARN: Modifications may break builds: check that your code still builds"
|
||||
|
|
@ -70,6 +75,8 @@ main() {
|
|||
rm -rf "$tmpdir"
|
||||
)
|
||||
fi
|
||||
echo "Changes made by pretty to '$file':"
|
||||
git diff --cached "$originalIndex" -- "$file"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
|
|
|||
45
scripts/runwithprofile.js
Normal file
45
scripts/runwithprofile.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { spawnSync } = require("child_process")
|
||||
const fs = require("fs")
|
||||
const os = require("os")
|
||||
const path = require("path")
|
||||
|
||||
const firefoxDir =
|
||||
process.platform === "win32"
|
||||
? path.join(process.env.APPDATA || os.homedir(), "Mozilla", "Firefox")
|
||||
: process.platform === "darwin"
|
||||
? path.join(os.homedir(), "Library", "Application Support", "Firefox")
|
||||
: path.join(os.homedir(), ".mozilla", "firefox")
|
||||
|
||||
function findDefaultProfile() {
|
||||
const sections = []
|
||||
let section
|
||||
for (const line of fs.readFileSync(path.join(firefoxDir, "profiles.ini"), "utf8").split(/\r?\n/)) {
|
||||
const header = line.trim().match(/^\[([^\]]+)\]$/)
|
||||
if (header) {
|
||||
section = { name: header[1] }
|
||||
sections.push(section)
|
||||
} else if (section) {
|
||||
const separator = line.indexOf("=")
|
||||
if (separator > 0) section[line.slice(0, separator)] = line.slice(separator + 1)
|
||||
}
|
||||
}
|
||||
const profilePath =
|
||||
sections.find(section => section.name.startsWith("Profile") && section.Name === "default-release")?.Path ??
|
||||
sections.find(section => section.name.startsWith("Install") && section.Default)?.Default ??
|
||||
sections.find(section => section.name.startsWith("Profile") && section.Default === "1")?.Path
|
||||
if (!profilePath) throw new Error(`No default profile found in ${firefoxDir}`)
|
||||
return path.isAbsolute(profilePath) ? profilePath : path.resolve(firefoxDir, profilePath)
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = process.argv[2] || findDefaultProfile()
|
||||
const command = process.platform === "win32" ? "web-ext.cmd" : "web-ext"
|
||||
const result = spawnSync(command, ["run", "--source-dir", "build/", "--firefox", "deved", "--pre-install", "--firefox-profile", profile, "--no-reload", "--pref", "browser.startup.page=3", "--pref", "browser.sessionstore.resume_from_crash=true"], { stdio: "inherit" })
|
||||
if (result.error) throw result.error
|
||||
process.exitCode = result.status ?? 1
|
||||
} catch (error) {
|
||||
console.error(`runwithprofile: ${error.message}`)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ set -e
|
|||
|
||||
sign_and_submit() {
|
||||
# Don't trust the return value of web-ext sign.
|
||||
(source AMOKEYS && (yarn run web-ext sign -s build --api-key "$AMOKEY" --api-secret "$AMOSECRET" "$@" || true))
|
||||
(set +x; source AMOKEYS && (yarn run web-ext sign -s build --api-key "$AMOKEY" --api-secret "$AMOSECRET" "$@" || true))
|
||||
}
|
||||
|
||||
publish_beta_nonewtab() {
|
||||
|
|
@ -30,6 +30,7 @@ build_no_sign_beta(){
|
|||
yarn run clean
|
||||
yarn run build --no-native
|
||||
scripts/version.js beta
|
||||
node -e 'const fs = require("fs"), manifest = require("./build/manifest.json"); delete manifest.applications.gecko.update_url; fs.writeFileSync("./build/manifest.json", JSON.stringify(manifest, null, 4))'
|
||||
sed 's/"name": "Tridactyl"/"name": "Tridactyl: Beta"/' -i build/manifest.json
|
||||
mkdir -p web-ext-artifacts
|
||||
yarn run web-ext build --source-dir ./build --overwrite-dest
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ export LAST_VERSION="$1"
|
|||
|
||||
allcontributors="$(git shortlog -sn $LAST_VERSION..HEAD | cut -c8- | awk '!seen[$0]++' | paste -sd "," - | sed 's/,/, /g')"
|
||||
newcontributors="$(diff --changed-group-format='%<' --unchanged-group-format='' <(git shortlog -sn $LAST_VERSION..HEAD | cut -c8- | awk '!seen[$0]++'| sort) <(git shortlog -sn $LAST_VERSION | cut -c8- | awk '!seen[$0]++'| sort) | paste -sd "," - | sed 's/,/, /g')"
|
||||
issuereporters="$(gh issue list --state closed --search "closed:>$(git log -1 --format=%cI "$LAST_VERSION")" --limit 10000 --json author --jq '.[] | select(.author) | .author.login' | sort | uniq -c | sort -k1,1nr -k2,2 | awk '{print $2}' | paste -sd "," - | sed 's/,/, /g; s#app/#people who have since deleted their GitHub accounts#g')"
|
||||
|
||||
echo "Thanks to all of our contributors for this release: $allcontributors"
|
||||
echo
|
||||
echo "Extra special thanks go to $newcontributors"
|
||||
echo
|
||||
echo Last, but not least - thank you to everyone who reported issues.
|
||||
echo "Last, but not least - thank you to everyone who reported the issues we closed in this release: $issuereporters"
|
||||
|
|
|
|||
471
scripts/typedoc-theme.mjs
Normal file
471
scripts/typedoc-theme.mjs
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
import {
|
||||
Converter,
|
||||
DefaultTheme,
|
||||
IntrinsicType,
|
||||
JSX,
|
||||
KindRouter,
|
||||
PageKind,
|
||||
Reflection,
|
||||
ReflectionKind,
|
||||
ReflectionType,
|
||||
Slugger,
|
||||
TypeScript as ts,
|
||||
} from "typedoc"
|
||||
|
||||
const h = JSX.createElement
|
||||
const css = href => h("link", { rel: "stylesheet", href })
|
||||
const cleanName = name => name.replace(/[^a-z0-9]/gi, "_").toLowerCase()
|
||||
const isGeneratedSource = source =>
|
||||
/\/\.[^/]+\.generated\.ts$/.test(source.fullFileName)
|
||||
|
||||
function rewriteWikiLinks(parts, owner, reflections) {
|
||||
return parts.flatMap(part => {
|
||||
if (part.kind !== "text" || !part.text.includes("[[")) return part
|
||||
|
||||
const out = []
|
||||
let end = 0
|
||||
for (const match of part.text.matchAll(
|
||||
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
|
||||
)) {
|
||||
if (match.index > end)
|
||||
out.push({
|
||||
kind: "text",
|
||||
text: part.text.slice(end, match.index),
|
||||
})
|
||||
const candidates = reflections.get(match[1]) || []
|
||||
const ownerModule = getModule(owner)
|
||||
const target =
|
||||
(!owner.kindOf(
|
||||
ReflectionKind.Module | ReflectionKind.Namespace,
|
||||
) &&
|
||||
candidates.find(
|
||||
candidate => candidate.parent === owner.parent,
|
||||
)) ||
|
||||
candidates.find(
|
||||
candidate => getModule(candidate) === ownerModule,
|
||||
) ||
|
||||
candidates.find(
|
||||
candidate =>
|
||||
!candidate.kindOf(
|
||||
ReflectionKind.Module | ReflectionKind.Namespace,
|
||||
) && !candidate.sources?.some(isGeneratedSource),
|
||||
) ||
|
||||
candidates.find(
|
||||
candidate =>
|
||||
!candidate.kindOf(
|
||||
ReflectionKind.Module | ReflectionKind.Namespace,
|
||||
),
|
||||
) ||
|
||||
candidates[0]
|
||||
out.push(
|
||||
target
|
||||
? {
|
||||
kind: "inline-tag",
|
||||
tag: "@link",
|
||||
text: match[2] || match[1],
|
||||
target,
|
||||
}
|
||||
: { kind: "text", text: match[2] || match[1] },
|
||||
)
|
||||
end = match.index + match[0].length
|
||||
}
|
||||
if (end < part.text.length)
|
||||
out.push({ kind: "text", text: part.text.slice(end) })
|
||||
return out
|
||||
})
|
||||
}
|
||||
|
||||
function getModule(reflection) {
|
||||
while (reflection.parent && !reflection.parent.isProject())
|
||||
reflection = reflection.parent
|
||||
return reflection
|
||||
}
|
||||
|
||||
function escapeDependencyTags(parts) {
|
||||
return parts.map(part =>
|
||||
part.kind === "text"
|
||||
? {
|
||||
...part,
|
||||
text: part.text.replace(
|
||||
/<(\/?)(iframe|object|frame)>/gi,
|
||||
"<$1$2>",
|
||||
),
|
||||
}
|
||||
: part,
|
||||
)
|
||||
}
|
||||
|
||||
function restoreWikiLinks(context) {
|
||||
const reflections = new Map()
|
||||
for (const reflection of Object.values(context.project.reflections)) {
|
||||
if (!reflection.isDeclaration()) continue
|
||||
const matches = reflections.get(reflection.name) || []
|
||||
matches.push(reflection)
|
||||
reflections.set(reflection.name, matches)
|
||||
}
|
||||
for (const reflection of Object.values(context.project.reflections)) {
|
||||
const fromDependency = reflection.sources?.some(source =>
|
||||
source.fullFileName.includes("/node_modules/"),
|
||||
)
|
||||
for (const source of reflection.sources || [])
|
||||
if (
|
||||
source.fullFileName.includes("/node_modules/") ||
|
||||
isGeneratedSource(source)
|
||||
)
|
||||
source.url = undefined
|
||||
const comment = reflection.comment
|
||||
if (!comment) continue
|
||||
comment.summary = rewriteWikiLinks(
|
||||
comment.summary,
|
||||
reflection,
|
||||
reflections,
|
||||
)
|
||||
for (const tag of comment.blockTags)
|
||||
tag.content = rewriteWikiLinks(tag.content, reflection, reflections)
|
||||
if (fromDependency) {
|
||||
comment.summary = escapeDependencyTags(comment.summary)
|
||||
for (const tag of comment.blockTags)
|
||||
tag.content = escapeDependencyTags(tag.content)
|
||||
}
|
||||
}
|
||||
if (context.project.readme)
|
||||
context.project.readme = rewriteWikiLinks(
|
||||
context.project.readme,
|
||||
context.project,
|
||||
reflections,
|
||||
)
|
||||
}
|
||||
|
||||
function restoreObjectLiteral(context, reflection) {
|
||||
const node = context.getSymbolFromReflection(reflection)?.valueDeclaration
|
||||
const initializer = node && "initializer" in node && node.initializer
|
||||
if (!initializer || !ts.isObjectLiteralExpression(initializer)) return
|
||||
if (!initializer.properties.length) return
|
||||
|
||||
if (reflection.type instanceof ReflectionType)
|
||||
context.project.removeTypeReflections(reflection.type)
|
||||
const inferred = context.converter.convertType(
|
||||
context.withScope(reflection),
|
||||
context.checker.getTypeAtLocation(initializer),
|
||||
)
|
||||
if (!(inferred instanceof ReflectionType)) return
|
||||
|
||||
reflection.type = new IntrinsicType("object")
|
||||
context.project.mergeReflections(inferred.declaration, reflection)
|
||||
}
|
||||
|
||||
function restoreCommandlineFns(context) {
|
||||
const commandline = context.project.getChildByName("commandline_frame")
|
||||
const functions = commandline?.getChildByName("commandlineFns")
|
||||
if (functions) context.project.mergeReflections(functions, commandline)
|
||||
}
|
||||
|
||||
class LegacySlugger extends Slugger {
|
||||
serialize(value) {
|
||||
return value.startsWith("\0") ? value.slice(1) : super.serialize(value)
|
||||
}
|
||||
}
|
||||
|
||||
class TridactylRouter extends KindRouter {
|
||||
aliases = new Map()
|
||||
aliasCounts = new Map()
|
||||
|
||||
getSlugger(target) {
|
||||
while (!this.sluggers.has(target)) target = target.parent
|
||||
let slugger = this.sluggers.get(target)
|
||||
if (!(slugger instanceof LegacySlugger)) {
|
||||
slugger = new LegacySlugger(this.sluggerConfiguration)
|
||||
this.sluggers.set(target, slugger)
|
||||
}
|
||||
return slugger
|
||||
}
|
||||
|
||||
getPageKind(target) {
|
||||
if (!(target instanceof Reflection)) return
|
||||
if (
|
||||
target.kindOf(
|
||||
ReflectionKind.Class |
|
||||
ReflectionKind.Interface |
|
||||
ReflectionKind.Enum |
|
||||
ReflectionKind.Module |
|
||||
ReflectionKind.Namespace,
|
||||
)
|
||||
)
|
||||
return PageKind.Reflection
|
||||
if (target.kindOf(ReflectionKind.Document)) return PageKind.Document
|
||||
}
|
||||
|
||||
getIdealBaseName(reflection) {
|
||||
const directory = this.directories.get(reflection.kind)
|
||||
const parts = []
|
||||
do {
|
||||
let name = reflection.name
|
||||
if (
|
||||
reflection.parent?.isProject() &&
|
||||
reflection.kind === ReflectionKind.Module
|
||||
)
|
||||
name = `"src/${name}"`
|
||||
parts.unshift(cleanName(name))
|
||||
reflection = reflection.parent
|
||||
} while (reflection && !reflection.isProject())
|
||||
return `${directory}/${parts.join(".")}`
|
||||
}
|
||||
|
||||
getLegacyAlias(reflection, pageTarget) {
|
||||
if (this.aliases.has(reflection)) return this.aliases.get(reflection)
|
||||
|
||||
const base = cleanName(reflection.name) || `reflection-${reflection.id}`
|
||||
let counts = this.aliasCounts.get(pageTarget)
|
||||
if (!counts) this.aliasCounts.set(pageTarget, (counts = new Map()))
|
||||
const count = counts.get(base) || 0
|
||||
const alias = count ? `${base}-${count}` : base
|
||||
counts.set(base, count + 1)
|
||||
this.aliases.set(reflection, alias)
|
||||
return alias
|
||||
}
|
||||
|
||||
createAnchor(target, pageTarget) {
|
||||
if (!target.isDeclaration())
|
||||
return super.createAnchor(target, pageTarget)
|
||||
|
||||
const parts = []
|
||||
for (
|
||||
let current = target;
|
||||
current !== pageTarget;
|
||||
current = current.parent
|
||||
)
|
||||
parts.unshift(this.getLegacyAlias(current, pageTarget))
|
||||
return this.getSlugger(pageTarget).slug("\0" + parts.join("."))
|
||||
}
|
||||
|
||||
buildPages(project) {
|
||||
this.aliases.clear()
|
||||
this.aliasCounts.clear()
|
||||
const pages = super.buildPages(project)
|
||||
const globals = pages.find(page => page.url === "modules.html")
|
||||
if (globals) {
|
||||
globals.url = "globals.html"
|
||||
this.fullUrls.set(project, "globals.html")
|
||||
}
|
||||
return pages
|
||||
}
|
||||
}
|
||||
|
||||
function parseFlagTag(tag) {
|
||||
const parts = tag.content || []
|
||||
let text = ""
|
||||
let trailing = []
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
const match = part.kind === "text" && /\r?\n[ \t]*\r?\n/.exec(part.text)
|
||||
if (!match) {
|
||||
text += part.text || ""
|
||||
continue
|
||||
}
|
||||
text += part.text.slice(0, match.index)
|
||||
trailing = [
|
||||
{
|
||||
...part,
|
||||
text: part.text.slice(match.index + match[0].length),
|
||||
},
|
||||
...parts.slice(i + 1),
|
||||
]
|
||||
break
|
||||
}
|
||||
const m = /^(-\S+)[ \t]+([^\n]+)\n*([\s\S]*)$/.exec(text.trim())
|
||||
if (!m) return undefined
|
||||
const [, flag, short, rest] = m
|
||||
const elaboration = rest.trim()
|
||||
return [flag, short, elaboration, trailing]
|
||||
}
|
||||
|
||||
function renderFlagList(context, parsed) {
|
||||
if (parsed.length === 0) return null
|
||||
return h(
|
||||
"ul",
|
||||
{ class: "tsd-tag-flag tsd-parameter-list" },
|
||||
parsed.map(([flag, short, elaboration]) =>
|
||||
h(
|
||||
"li",
|
||||
null,
|
||||
h("code", null, flag),
|
||||
" ",
|
||||
short,
|
||||
elaboration &&
|
||||
context.displayParts([{ kind: "text", text: elaboration }]),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
class TridactylTheme extends DefaultTheme {
|
||||
getRenderContext(page) {
|
||||
const context = super.getRenderContext(page)
|
||||
const defaultCommentSummary = context.commentSummary
|
||||
context.commentSummary = props => {
|
||||
const blockTags = props.comment?.blockTags || []
|
||||
if (!blockTags.some(tag => tag.tag === "@flag"))
|
||||
return defaultCommentSummary(props)
|
||||
|
||||
const summaryHeadingCount = page.pageHeadings.length
|
||||
const nodes = [context.displayParts(props.comment?.summary || [])]
|
||||
page.pageHeadings.length = summaryHeadingCount
|
||||
let flagRun = []
|
||||
const flushFlags = () => {
|
||||
if (flagRun.length === 0) return
|
||||
nodes.push(renderFlagList(context, flagRun))
|
||||
flagRun = []
|
||||
}
|
||||
for (const tag of blockTags) {
|
||||
if (tag.tag === "@flag") {
|
||||
tag.skipRendering = true
|
||||
const parsed = parseFlagTag(tag)
|
||||
if (!parsed) continue
|
||||
const [flag, short, elaboration, trailing] = parsed
|
||||
flagRun.push([flag, short, elaboration])
|
||||
if (trailing.length === 0) continue
|
||||
flushFlags()
|
||||
const headingCount = page.pageHeadings.length
|
||||
nodes.push(context.displayParts(trailing))
|
||||
page.pageHeadings.length = headingCount
|
||||
}
|
||||
}
|
||||
flushFlags()
|
||||
return h(JSX.Fragment, null, ...nodes)
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
getReflectionClasses(reflection) {
|
||||
const kind = ReflectionKind.classString(reflection.kind)
|
||||
const parent =
|
||||
reflection.parent &&
|
||||
ReflectionKind.classString(reflection.parent.kind).replace(
|
||||
"tsd-kind-",
|
||||
"tsd-parent-kind-",
|
||||
)
|
||||
return [super.getReflectionClasses(reflection), kind, parent]
|
||||
.filter(Boolean)
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
constructor(renderer) {
|
||||
super(renderer)
|
||||
const reflectionTemplate = this.reflectionTemplate
|
||||
this.reflectionTemplate = page => {
|
||||
const content = reflectionTemplate(page)
|
||||
if (
|
||||
!page.model.kindOf(
|
||||
ReflectionKind.Module | ReflectionKind.Namespace,
|
||||
)
|
||||
)
|
||||
return content
|
||||
|
||||
const context = this.getRenderContext(page)
|
||||
page.pageSections.length = 0
|
||||
const renderMember = context.member
|
||||
context.member = reflection =>
|
||||
reflection.isReference() ? null : renderMember(reflection)
|
||||
return h(JSX.Fragment, null, content, context.members(page.model))
|
||||
}
|
||||
|
||||
this.defaultLayoutTemplate = (page, template) => {
|
||||
const context = this.getRenderContext(page)
|
||||
const content = template(page)
|
||||
const helpLink = (label, href) =>
|
||||
h("li", null, h("a", { href }, label))
|
||||
const docLink = (label, url) =>
|
||||
helpLink(label, context.relativeURL(url))
|
||||
return h(
|
||||
"html",
|
||||
{ class: "minimal no-js TridactylOwnNamespace" },
|
||||
h(
|
||||
"head",
|
||||
null,
|
||||
h("meta", { charset: "utf-8" }),
|
||||
h("meta", {
|
||||
"http-equiv": "X-UA-Compatible",
|
||||
content: "IE=edge",
|
||||
}),
|
||||
h(
|
||||
"title",
|
||||
null,
|
||||
`${page.model.name} | ${page.project.name}`,
|
||||
),
|
||||
h("meta", {
|
||||
name: "viewport",
|
||||
content: "width=device-width, initial-scale=1",
|
||||
}),
|
||||
css(context.relativeURL("assets/style.css")),
|
||||
css(context.relativeURL("assets/highlight.css")),
|
||||
h("script", { src: "/content.js" }),
|
||||
h("script", { src: "/help.js" }),
|
||||
css("/static/css/content.css"),
|
||||
css("/static/css/hint.css"),
|
||||
css("/static/css/viewsource.css"),
|
||||
css("/static/typedoc/assets/css/main.css"),
|
||||
),
|
||||
h(
|
||||
"body",
|
||||
null,
|
||||
h(
|
||||
"nav",
|
||||
{ class: "tsd-navigation secondary scroller" },
|
||||
h(
|
||||
"section",
|
||||
{ class: "TridactylHelpNavigation" },
|
||||
h("h3", null, "Help Pages"),
|
||||
h(
|
||||
"ul",
|
||||
null,
|
||||
docLink(
|
||||
"Commands",
|
||||
"modules/_src_excmds_.html",
|
||||
),
|
||||
docLink(
|
||||
"Settings",
|
||||
"classes/_src_lib_config_.default_config.html",
|
||||
),
|
||||
docLink("Tutorial", "../clippy/1-tutor.html"),
|
||||
docLink(
|
||||
"Editor Functions",
|
||||
"modules/_src_lib_editor_.html",
|
||||
),
|
||||
docLink(
|
||||
"Command-Line Functions",
|
||||
"modules/_src_commandline_frame_.html",
|
||||
),
|
||||
docLink(
|
||||
"Hint Mode Commands",
|
||||
"modules/_src_content_hinting_.html",
|
||||
),
|
||||
helpLink(
|
||||
"Wiki",
|
||||
"https://github.com/tridactyl/tridactyl/wiki",
|
||||
),
|
||||
),
|
||||
),
|
||||
context.pageNavigation(page),
|
||||
),
|
||||
h(
|
||||
"div",
|
||||
{ class: "container container-main scroller" },
|
||||
h("div", { class: "content-wrap" }, content),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function load(app) {
|
||||
app.converter.on(Converter.EVENT_RESOLVE_BEGIN, restoreCommandlineFns)
|
||||
app.converter.on(Converter.EVENT_RESOLVE_END, restoreWikiLinks, 100)
|
||||
app.converter.on(
|
||||
Converter.EVENT_CREATE_DECLARATION,
|
||||
restoreObjectLiteral,
|
||||
100,
|
||||
)
|
||||
app.renderer.defineRouter("tridactyl", TridactylRouter)
|
||||
app.renderer.defineTheme("tridactyl", TridactylTheme)
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
const { exec } = require("child_process")
|
||||
const { exec, execFileSync } = require("child_process")
|
||||
const fs = require("fs")
|
||||
|
||||
function bump_version(versionstr, component = 2) {
|
||||
|
|
@ -12,7 +12,33 @@ function bump_version(versionstr, component = 2) {
|
|||
return versionarr.join(".")
|
||||
}
|
||||
|
||||
async function add_beta(versionstr) {
|
||||
function release_name(manifest) {
|
||||
return (manifest.version_name || manifest.version)
|
||||
.slice(manifest.version.length)
|
||||
.trim()
|
||||
}
|
||||
|
||||
function validate_release_version(manifest) {
|
||||
if (!/^\d+\.\d+\.\d+$/.test(manifest.version) || manifest.version_name !== [manifest.version, release_name(manifest)].filter(Boolean).join(" ")) {
|
||||
throw new Error("Manifest version_name must be the version followed by an optional release name")
|
||||
}
|
||||
}
|
||||
|
||||
function tagged(version) {
|
||||
return fs.existsSync(".git") && execFileSync("git", ["tag", "--list", version]).toString().trim() !== ""
|
||||
}
|
||||
|
||||
function set_release_version(manifest, component, name = "") {
|
||||
if (![0, 1, 2].includes(component)) throw new Error("Version component must be 0, 1 or 2")
|
||||
if (component === 2 && name) throw new Error("Only major and minor releases can be named")
|
||||
|
||||
const nameForRelease =
|
||||
component === 2 ? release_name(manifest) : name.trim()
|
||||
manifest.version = bump_version(manifest.version, component)
|
||||
manifest.version_name = [manifest.version, nameForRelease].filter(Boolean).join(" ")
|
||||
}
|
||||
|
||||
async function beta_number() {
|
||||
await fs.promises.mkdir(".build_cache", {recursive: true})
|
||||
try {
|
||||
await fs.promises.access(".git")
|
||||
|
|
@ -26,7 +52,7 @@ async function add_beta(versionstr) {
|
|||
catch {
|
||||
; // Not in a git directory - don't do anything
|
||||
}
|
||||
return versionstr + "pre" + (await fs.promises.readFile(".build_cache/count", {encoding: "utf8"})).trim()
|
||||
return (await fs.promises.readFile(".build_cache/count", {encoding: "utf8"})).trim()
|
||||
}
|
||||
|
||||
async function get_hash() {
|
||||
|
|
@ -81,34 +107,76 @@ function save_manifest(filename, manifest) {
|
|||
fs.writeFileSync(filename, JSON.stringify(manifest, null, 4))
|
||||
}
|
||||
|
||||
function set_beta_version(manifest, number, hash) {
|
||||
const version = manifest.version
|
||||
const name = release_name(manifest)
|
||||
manifest.version = `${version}.${number}`
|
||||
manifest.version_name = [`${version}pre${number}-${hash}`, name].filter(Boolean).join(" ")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let filename, manifest
|
||||
switch (process.argv[2]) {
|
||||
case "bump":
|
||||
// Load src manifest and bump
|
||||
case "next": {
|
||||
filename = "./src/manifest.json"
|
||||
manifest = require("." + filename)
|
||||
manifest.version = bump_version(
|
||||
manifest.version,
|
||||
if (!tagged(manifest.version)) throw new Error(`Version ${manifest.version} is not tagged`)
|
||||
set_release_version(
|
||||
manifest,
|
||||
Number(process.argv[3]),
|
||||
process.argv.slice(4).join(" "),
|
||||
)
|
||||
manifest.version_name = manifest.version
|
||||
validate_release_version(manifest)
|
||||
if (tagged(manifest.version)) throw new Error(`Version ${manifest.version} is already tagged`)
|
||||
save_manifest(filename, manifest)
|
||||
exec(
|
||||
`git add ${filename} && git commit -m 'release ${
|
||||
manifest.version
|
||||
}' && git tag ${manifest.version}`,
|
||||
break
|
||||
}
|
||||
case "release": {
|
||||
filename = "./src/manifest.json"
|
||||
manifest = require("." + filename)
|
||||
validate_release_version(manifest)
|
||||
if (tagged(manifest.version)) throw new Error(`Version ${manifest.version} is already tagged`)
|
||||
if (execFileSync("git", ["status", "--porcelain"]).toString().trim()) throw new Error("Release requires a clean worktree")
|
||||
const changelog = fs.readFileSync("./CHANGELOG.md", "utf8")
|
||||
const releaseHeading = `Release ${manifest.version} / Unreleased`
|
||||
const releaseNotes = changelog
|
||||
.split(/(?=^#+ Release )/m)
|
||||
.find(notes =>
|
||||
notes.split("\n", 1)[0].trimEnd().endsWith(releaseHeading),
|
||||
)
|
||||
if (!releaseNotes) {
|
||||
throw new Error(`No ${releaseHeading} changelog entry`)
|
||||
}
|
||||
const datedReleaseNotes = releaseNotes.replace(
|
||||
"Unreleased",
|
||||
new Date().toISOString().slice(0, 10),
|
||||
)
|
||||
fs.writeFileSync(
|
||||
"./CHANGELOG.md",
|
||||
changelog.replace(releaseNotes, () => datedReleaseNotes),
|
||||
)
|
||||
execFileSync("git", ["add", "./CHANGELOG.md"])
|
||||
execFileSync("git", [
|
||||
"commit",
|
||||
"--cleanup=verbatim",
|
||||
"-m",
|
||||
`release ${manifest.version}`,
|
||||
"-m",
|
||||
datedReleaseNotes.trim(),
|
||||
])
|
||||
execFileSync("git", ["tag", manifest.version])
|
||||
console.log(
|
||||
`Make sure you use the release checklist before committing this.`,
|
||||
)
|
||||
console.log(`https://github.com/tridactyl/tridactyl/issues/714`)
|
||||
break
|
||||
}
|
||||
case "beta":
|
||||
filename = "./build/manifest.json"
|
||||
manifest = require("." + filename)
|
||||
manifest.version = await add_beta(manifest.version)
|
||||
manifest.version_name = manifest.version + "-" + (await get_hash())
|
||||
validate_release_version(manifest)
|
||||
if (tagged(manifest.version)) throw new Error(`Version ${manifest.version} is already tagged`)
|
||||
set_beta_version(manifest, await beta_number(), await get_hash())
|
||||
manifest.applications.gecko.update_url =
|
||||
"https://tridactyl.cmcaine.co.uk/betas/updates.json"
|
||||
|
||||
|
|
@ -130,4 +198,6 @@ async function main() {
|
|||
}
|
||||
}
|
||||
|
||||
main()
|
||||
if (require.main === module) main()
|
||||
|
||||
module.exports = { set_beta_version, set_release_version }
|
||||
|
|
|
|||
22
scripts/version.test.js
Normal file
22
scripts/version.test.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const { set_beta_version, set_release_version } = require("./version")
|
||||
|
||||
test("manages major and minor release names", () => {
|
||||
const manifest = { version: "1.24.6", version_name: "1.24.6" }
|
||||
|
||||
set_release_version(manifest, 1, "Carpenter")
|
||||
expect(manifest.version_name).toBe("1.25.0 Carpenter")
|
||||
|
||||
set_release_version(manifest, 2)
|
||||
expect(manifest.version_name).toBe("1.25.1 Carpenter")
|
||||
|
||||
set_release_version(manifest, 0, "Joiner")
|
||||
expect(manifest.version_name).toBe("2.0.0 Joiner")
|
||||
|
||||
expect(() =>
|
||||
set_release_version({ version: "1.25.0" }, 2, "Carpenter"),
|
||||
).toThrow()
|
||||
|
||||
set_beta_version(manifest, "7685", "1ac287cf")
|
||||
expect(manifest.version).toBe("2.0.0.7685")
|
||||
expect(manifest.version_name).toBe("2.0.0pre7685-1ac287cf Joiner")
|
||||
})
|
||||
|
|
@ -20,21 +20,25 @@ export WINEDEBUG="fixme-all"
|
|||
# stop wine whining
|
||||
export DISPLAY=
|
||||
|
||||
PREREQUISITES="tput printf 7z wine"
|
||||
PREREQUISITES="tput printf sort 7z wine"
|
||||
|
||||
MIN_WINE_VER="4"
|
||||
MIN_7ZIP_VER="16"
|
||||
|
||||
versionAtLeast() {
|
||||
printf '%s\n' "$1" "$2" | sort -VC
|
||||
}
|
||||
|
||||
checkRequiredVersions() {
|
||||
if ! 7z | awk '/Version/{print $3}' | grep -q "${MIN_7ZIP_VER}"; then
|
||||
if ! versionAtLeast "${MIN_7ZIP_VER}" "$(7z | awk '/Version/{print $3}')"; then
|
||||
colorEcho \
|
||||
'[-] p7zip minimum version '"${MIN_7ZIP_VER}"' required\n' \
|
||||
"alert"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! wine --version 2> /dev/null | grep -q "wine-${MIN_WINE_VER}"; then
|
||||
colorecho \
|
||||
if ! versionAtLeast "${MIN_WINE_VER}" "$(wine --version 2> /dev/null | cut -d- -f2-)"; then
|
||||
colorEcho \
|
||||
'[-] wine minimum version '"${MIN_WINE_VER}"' required\n' \
|
||||
"alert"
|
||||
exit 1
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { omniscient_controller } from "@src/lib/omniscient_controller"
|
|||
import * as perf from "@src/perf"
|
||||
import { listenForCounters } from "@src/perf"
|
||||
import * as messaging from "@src/lib/messaging"
|
||||
import { messageTabChanges } from "@src/background/tab_changes"
|
||||
import * as excmds_background from "@src/.excmds_background.generated"
|
||||
import { CmdlineCmds } from "@src/background/commandline_cmds"
|
||||
import { EditorCmds } from "@src/background/editor"
|
||||
|
|
@ -27,6 +28,7 @@ import * as omnibox from "@src/background/omnibox"
|
|||
import * as R from "ramda"
|
||||
import * as webrequests from "@src/background/webrequests"
|
||||
import * as commands from "@src/background/commands"
|
||||
import * as browser_action from "@src/background/browser_action"
|
||||
import * as meta from "@src/background/meta"
|
||||
import * as Logging from "@src/lib/logging"
|
||||
import * as Proxy from "@src/lib/proxy"
|
||||
|
|
@ -48,7 +50,10 @@ import { tabsProxy } from "@src/lib/tabs"
|
|||
state,
|
||||
webext,
|
||||
webrequests,
|
||||
l: (prom: Promise<any>) => prom.then(console.log).catch(console.error),
|
||||
l: (value: any) =>
|
||||
typeof value?.then === "function"
|
||||
? value.then(console.log).catch(console.error)
|
||||
: console.log(value),
|
||||
contentLocation: window.location,
|
||||
R,
|
||||
perf,
|
||||
|
|
@ -69,22 +74,24 @@ controller.setExCmds({
|
|||
})
|
||||
|
||||
// {{{ tri.contentLocation
|
||||
// When loading the background, use the active tab to know what the current content url is
|
||||
browser.tabs.query({ currentWindow: true, active: true }).then(t => {
|
||||
;(window as any).tri.contentLocation = new URL(t[0].url)
|
||||
})
|
||||
// After that, on every tab change, update the current url
|
||||
let contentLocationCount = 0
|
||||
browser.tabs.onActivated.addListener(ev => {
|
||||
function updateContentLocation(windowId = browser.windows.WINDOW_ID_CURRENT) {
|
||||
const myId = contentLocationCount + 1
|
||||
contentLocationCount = myId
|
||||
browser.tabs.get(ev.tabId).then(t => {
|
||||
// Note: we're using contentLocationCount and myId in order to make sure that only the last onActivated event is used in order to set contentLocation
|
||||
// This is needed because otherWise the following chain of execution might happen: onActivated1 => onActivated2 => tabs.get2 => tabs.get1
|
||||
if (contentLocationCount === myId) {
|
||||
;(window as any).tri.contentLocation = new URL(t.url)
|
||||
}
|
||||
})
|
||||
browser.tabs
|
||||
.query({ windowId, active: true })
|
||||
.then(t => {
|
||||
// Ignore stale queries when focus or active tabs change quickly.
|
||||
if (contentLocationCount === myId && t[0]?.url) {
|
||||
;(window as any).tri.contentLocation = new URL(t[0].url)
|
||||
}
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
browser.tabs.onActivated.addListener(() => updateContentLocation())
|
||||
browser.windows.onFocusChanged.addListener(windowId => {
|
||||
if (windowId === browser.windows.WINDOW_ID_NONE) return
|
||||
updateContentLocation(windowId)
|
||||
})
|
||||
|
||||
browser.proxy.onRequest.addListener(Proxy.onRequestListener, {
|
||||
|
|
@ -94,32 +101,44 @@ browser.proxy.onRequest.addListener(Proxy.onRequestListener, {
|
|||
/**
|
||||
* Declare Tab Event Listeners
|
||||
*/
|
||||
browser.tabs.onRemoved.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_close", [tabId])
|
||||
})
|
||||
const tabChangeListener = (command: string) => () => messageTabChanges(command)
|
||||
browser.tabs.onRemoved.addListener(tabChangeListener("tab_close"))
|
||||
// Fired when a tab is attached to a window, for example because it was moved between windows.
|
||||
browser.tabs.onAttached.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_attached", [tabId])
|
||||
})
|
||||
browser.tabs.onAttached.addListener(tabChangeListener("tab_attached"))
|
||||
// Fired when a tab is created. Note that the tab's URL may not be set at the time this event fired.
|
||||
browser.tabs.onCreated.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_created", [tabId])
|
||||
})
|
||||
browser.tabs.onCreated.addListener(tabChangeListener("tab_created"))
|
||||
// Fired when a tab is detached from a window, for example because it is being moved between windows.
|
||||
browser.tabs.onDetached.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_detached", [tabId])
|
||||
})
|
||||
browser.tabs.onDetached.addListener(tabChangeListener("tab_detached"))
|
||||
// Fired when a tab is moved within a window.
|
||||
browser.tabs.onMoved.addListener(tabId => {
|
||||
messaging.messageAllTabs("tab_changes", "tab_moved", [tabId])
|
||||
})
|
||||
browser.tabs.onMoved.addListener(tabChangeListener("tab_moved"))
|
||||
browser.tabs.onUpdated.addListener(
|
||||
tabChangeListener("tab_updated"),
|
||||
{
|
||||
properties: [
|
||||
"audible",
|
||||
"discarded",
|
||||
"favIconUrl",
|
||||
"hidden",
|
||||
"mutedInfo",
|
||||
"pinned",
|
||||
"title",
|
||||
"url",
|
||||
],
|
||||
},
|
||||
)
|
||||
browser.tabs.onActivated.addListener(tabChangeListener("tab_activated"))
|
||||
|
||||
// Update on navigation too (but remember that sometimes people open tabs in the background :) )
|
||||
browser.webNavigation.onDOMContentLoaded.addListener(() => {
|
||||
browser.tabs.query({ currentWindow: true, active: true }).then(t => {
|
||||
;(window as any).tri.contentLocation = new URL(t[0].url)
|
||||
})
|
||||
updateContentLocation()
|
||||
})
|
||||
const messageHistoryState = (details: { frameId: number; tabId: number }) => {
|
||||
if (details.frameId !== 0) return
|
||||
messaging.messageTab(details.tabId, "history_state").catch(() => undefined)
|
||||
}
|
||||
browser.webNavigation.onHistoryStateUpdated.addListener(messageHistoryState)
|
||||
browser.webNavigation.onReferenceFragmentUpdated.addListener(messageHistoryState)
|
||||
updateContentLocation()
|
||||
|
||||
// Prevent Tridactyl from being updated while it is running in the hope of fixing #290
|
||||
browser.runtime.onUpdateAvailable.addListener(_ => undefined)
|
||||
|
|
@ -195,27 +214,6 @@ for (const requestEvent of webrequests.requestEvents) {
|
|||
})
|
||||
}
|
||||
|
||||
config.addChangeListener("autocmds", (previous, current) =>
|
||||
webrequests.requestEvents.forEach(
|
||||
requestEvent =>
|
||||
// If there are autocmd(s) for this requestEvent
|
||||
current[requestEvent] !== undefined &&
|
||||
Object.entries(
|
||||
current[requestEvent] as Record<string, string>,
|
||||
).forEach(([pattern, func]) => {
|
||||
// R.path returns undefined if any part of the path is missing rather than saying "computer says no"
|
||||
const path = R.path([requestEvent, pattern])
|
||||
|
||||
// If this is a new autocmd, register it
|
||||
path(current) !== path(previous) &&
|
||||
webrequests.registerWebRequestAutocmd(
|
||||
requestEvent,
|
||||
pattern,
|
||||
func,
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
// }}}
|
||||
|
||||
|
|
@ -244,6 +242,7 @@ browser.webRequest.onBeforeRequest.addListener(
|
|||
)
|
||||
|
||||
browser.tabs.onCreated.addListener(aucon.tabCreatedListener)
|
||||
browser.tabs.onRemoved.addListener(aucon.tabRemovedListener)
|
||||
|
||||
// }}}
|
||||
|
||||
|
|
@ -252,6 +251,18 @@ browser.tabs.onCreated.addListener(aucon.tabCreatedListener)
|
|||
// An object to collect all of our statistics in one place.
|
||||
const statsLogger: perf.StatsLogger = new perf.StatsLogger()
|
||||
const messages = {
|
||||
browser_action_background: {
|
||||
getState: browser_action.getState,
|
||||
toggle: browser_action.toggle,
|
||||
},
|
||||
config_background: {
|
||||
clear: config.clear,
|
||||
pull: config.pull,
|
||||
push: config.push,
|
||||
ready: () => config.getAsync().then(() => undefined),
|
||||
set: config.set,
|
||||
unset: config.unset,
|
||||
},
|
||||
excmd_background: excmds_background,
|
||||
controller_background: controller,
|
||||
performance_background: statsLogger,
|
||||
|
|
@ -287,9 +298,8 @@ omnibox.init()
|
|||
|
||||
// }}}
|
||||
|
||||
setTimeout(config.update, 5000)
|
||||
|
||||
commands.updateListener()
|
||||
browser_action.init()
|
||||
|
||||
// {{{ Obey Mozilla's orders https://github.com/tridactyl/tridactyl/issues/1800
|
||||
|
||||
|
|
|
|||
43
src/background/browser_action.test.ts
Normal file
43
src/background/browser_action.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import * as config from "@src/lib/config"
|
||||
import { getState, init, toggle } from "@src/background/browser_action"
|
||||
|
||||
jest.mock("@src/lib/config", () => {
|
||||
const userconfig = { superignore: "false" }
|
||||
return {
|
||||
DEFAULTS: { superignore: "false" },
|
||||
USERCONFIG: userconfig,
|
||||
getAsync: jest.fn().mockResolvedValue(undefined),
|
||||
set: jest.fn((_key, value) => {
|
||||
userconfig.superignore = value
|
||||
return Promise.resolve()
|
||||
}),
|
||||
addChangeListener: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
test("browser action toggles superignore without reloading tabs", async () => {
|
||||
await config.set("superignore", "false")
|
||||
|
||||
init()
|
||||
await expect(getState()).resolves.toBe("false")
|
||||
await expect(Promise.all([toggle(), toggle()])).resolves.toEqual([
|
||||
"true",
|
||||
"false",
|
||||
])
|
||||
expect(config.USERCONFIG.superignore).toBe("false")
|
||||
expect(browser.browserAction.setBadgeText).toHaveBeenCalledWith({
|
||||
text: "OFF",
|
||||
})
|
||||
expect(browser.tabs.reload).not.toHaveBeenCalled()
|
||||
expect(browser.browserAction.onClicked.addListener).not.toHaveBeenCalled()
|
||||
expect(browser.browserAction.setTitle).toHaveBeenLastCalledWith({
|
||||
title: "Tridactyl enabled",
|
||||
})
|
||||
|
||||
jest.mocked(config.set).mockImplementationOnce(async (_key, value) => {
|
||||
config.USERCONFIG.superignore = value as "true" | "false"
|
||||
throw new Error("write failed")
|
||||
})
|
||||
await expect(toggle()).rejects.toThrow("write failed")
|
||||
expect(config.USERCONFIG.superignore).toBe("false")
|
||||
})
|
||||
48
src/background/browser_action.ts
Normal file
48
src/background/browser_action.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import * as config from "@src/lib/config"
|
||||
|
||||
const ready = config.getAsync()
|
||||
const superignore = () =>
|
||||
config.USERCONFIG.superignore ?? config.DEFAULTS.superignore
|
||||
|
||||
function updateButton(value) {
|
||||
const disabled = value === "true"
|
||||
return Promise.all([
|
||||
browser.browserAction.setBadgeText({ text: disabled ? "OFF" : "" }),
|
||||
browser.browserAction.setTitle({
|
||||
title: disabled ? "Tridactyl disabled" : "Tridactyl enabled",
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
export async function getState() {
|
||||
await ready
|
||||
return superignore()
|
||||
}
|
||||
|
||||
let toggleQueue: Promise<unknown> = Promise.resolve()
|
||||
export function toggle() {
|
||||
const pending = toggleQueue
|
||||
.catch(() => undefined)
|
||||
.then(async () => {
|
||||
const previous = config.USERCONFIG.superignore
|
||||
const value = (await getState()) === "true" ? "false" : "true"
|
||||
try {
|
||||
await config.set("superignore", value)
|
||||
} catch (error) {
|
||||
if (previous === undefined) delete config.USERCONFIG.superignore
|
||||
else config.USERCONFIG.superignore = previous
|
||||
throw error
|
||||
}
|
||||
updateButton(value).catch(console.error)
|
||||
return value
|
||||
})
|
||||
toggleQueue = pending
|
||||
return pending
|
||||
}
|
||||
|
||||
export function init() {
|
||||
config.addChangeListener("superignore", (_, value) =>
|
||||
updateButton(value).catch(console.error),
|
||||
)
|
||||
getState().then(updateButton).catch(console.error)
|
||||
}
|
||||
38
src/background/config_rc.test.ts
Normal file
38
src/background/config_rc.test.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import * as controller from "@src/lib/controller"
|
||||
import * as config from "@src/lib/config"
|
||||
import { rcFileToExCmds, runRc } from "@src/background/config_rc"
|
||||
|
||||
jest.mock("@src/lib/controller")
|
||||
global.structuredClone ??= value => JSON.parse(JSON.stringify(value))
|
||||
|
||||
const backslash = "\\"
|
||||
test.each([
|
||||
[`set foo one ${backslash}\ntwo`, ["set foo one two"]],
|
||||
[
|
||||
`keymap foo ${backslash}${backslash}\nset bar baz`,
|
||||
[`keymap foo ${backslash}`, "set bar baz"],
|
||||
],
|
||||
[`keymap foo ${backslash}${backslash}\n`, [`keymap foo ${backslash}`]],
|
||||
[`keymap foo ${backslash}`, [`keymap foo ${backslash}`]],
|
||||
])("parses RC line ending backslashes", (rc, expected) => {
|
||||
expect(rcFileToExCmds(rc)).toEqual(expected)
|
||||
})
|
||||
|
||||
test("runRc updates and saves versioned config", async () => {
|
||||
await config.clear()
|
||||
jest.mocked(controller.acceptExCmd).mockImplementation(async cmd => {
|
||||
const [, key, value] = cmd.split(" ")
|
||||
await config.set(key, value)
|
||||
})
|
||||
|
||||
await runRc("set configversion 1.0\nset vimium-gi false")
|
||||
|
||||
expect(browser.storage.local.set).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
userconfig: expect.objectContaining({
|
||||
configversion: "2.0",
|
||||
gimode: "firefox",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import * as controller from "@src/lib/controller"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as Native from "@src/lib/native"
|
||||
|
||||
export async function source(filename = "auto") {
|
||||
|
|
@ -48,14 +49,16 @@ export async function writeRc(conf: string, force = false, filename = "auto") {
|
|||
} else {
|
||||
path = filename
|
||||
}
|
||||
return await Native.writerc(path, force, conf)
|
||||
await Native.writerc(path, force, conf)
|
||||
return path
|
||||
}
|
||||
|
||||
export async function runRc(rc: string) {
|
||||
for (const cmd of rcFileToExCmds(rc)) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
await controller.acceptExCmd(cmd)
|
||||
}
|
||||
// Sourced commands have already been saved to the current local config.
|
||||
await config.update(true)
|
||||
}
|
||||
|
||||
export function rcFileToExCmds(rcText: string): string[] {
|
||||
|
|
@ -69,10 +72,13 @@ export function rcFileToExCmds(rcText: string): string[] {
|
|||
!x.trim().startsWith('"') &&
|
||||
!x.trim().startsWith("#"),
|
||||
)
|
||||
const res = ex.join("\n")
|
||||
const res = ex.join("\n") + (rcText.endsWith("\n") ? "\n" : "")
|
||||
|
||||
// string-join lines that end with /
|
||||
const joined = res.replace(/\\\n/g, "")
|
||||
// Join lines ending in an unescaped backslash and unescape trailing pairs.
|
||||
const joined = res.replace(/(\\+)\n/g, (_, backslashes: string) => {
|
||||
const escaped = "\\".repeat(Math.floor(backslashes.length / 2))
|
||||
return escaped + (backslashes.length % 2 === 0 ? "\n" : "")
|
||||
})
|
||||
|
||||
return joined.split("\n")
|
||||
return joined.replace(/\n$/, "").split("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,10 +79,10 @@ export async function downloadUrl(url: string, saveAs: boolean) {
|
|||
*
|
||||
* Note: this requires a native messenger >=0.1.9. Make sure to nativegate for this.
|
||||
*
|
||||
* @param URL the URL to download
|
||||
* @param url the URL to download
|
||||
* @param saveAs If beginning with a slash, this is the absolute path the document should be moved to. If the first character of the string is a tilda, it will be expanded to an absolute path to the user's home directory. If saveAs begins with any other character, it will be considered a path relative to where the native messenger binary is located (e.g. "$HOME/.local/share/tridactyl" on linux).
|
||||
* @param If true, overwrite the destination file, returns error code 1 otherwise if file exists
|
||||
* @param If true, cleans up temporary downloaded source file e.g. in $HOME/Downlods/downloaded.doc when the move operation fails e.g. due to target destination exists, OS error etc.
|
||||
* @param overwrite If true, overwrite the destination file, returns error code 1 otherwise if file exists
|
||||
* @param cleanup If true, cleans up temporary downloaded source file e.g. in $HOME/Downlods/downloaded.doc when the move operation fails e.g. due to target destination exists, OS error etc.
|
||||
*/
|
||||
export async function downloadUrlAs(
|
||||
url: string,
|
||||
|
|
@ -137,12 +137,18 @@ export async function downloadUrlAs(
|
|||
})
|
||||
)[0]
|
||||
if (downloadDelta.state.current === "complete") {
|
||||
const operation = await Native.move(
|
||||
const placeholder = config.get("downloadfilenamemarker")
|
||||
let finalSaveAs = saveAs
|
||||
if (placeholder.length > 0 && finalSaveAs.includes(placeholder)) {
|
||||
finalSaveAs = finalSaveAs.split(placeholder).join(fileName)
|
||||
}
|
||||
const operation = await Native.move(
|
||||
downloadItem.filename,
|
||||
saveAs,
|
||||
finalSaveAs,
|
||||
overwrite,
|
||||
cleanup,
|
||||
)
|
||||
)
|
||||
|
||||
const code2human = n =>
|
||||
R.defaultTo(
|
||||
"Unknown error",
|
||||
|
|
@ -161,7 +167,7 @@ export async function downloadUrlAs(
|
|||
),
|
||||
)
|
||||
} else {
|
||||
resolve(downloadItem.filename)
|
||||
resolve({filename: downloadItem.filename, finalSaveAs})
|
||||
}
|
||||
} else {
|
||||
reject(
|
||||
|
|
|
|||
29
src/background/tab_changes.test.ts
Normal file
29
src/background/tab_changes.test.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { messageTabChanges } from "@src/background/tab_changes"
|
||||
test("tab change bursts are delivered in order without overlap", async () => {
|
||||
jest.useFakeTimers()
|
||||
let finishSending
|
||||
const query = jest.mocked(browser.tabs.query)
|
||||
const send = jest.mocked(browser.tabs.sendMessage)
|
||||
query.mockResolvedValue([{ id: 1 }, { id: 2 }] as browser.tabs.Tab[])
|
||||
const firstSend = new Promise<void>(resolve => (finishSending = resolve))
|
||||
send.mockReturnValueOnce(firstSend).mockResolvedValue(undefined)
|
||||
const waitForSends = async (count: number) => {
|
||||
for (let i = 0; i < 10 && send.mock.calls.length < count; i++)
|
||||
await Promise.resolve()
|
||||
}
|
||||
for (let id = 0; id < 1500; id++) messageTabChanges("tab_created")
|
||||
jest.runOnlyPendingTimers()
|
||||
await waitForSends(2)
|
||||
expect(query).toHaveBeenCalledWith({ active: true })
|
||||
expect(send.mock.calls.slice(0, 2)).toEqual(
|
||||
[1, 2].map(id => [id, { type: "tab_changes", command: "priority" }]),
|
||||
)
|
||||
messageTabChanges("tab_updated")
|
||||
expect(send).toHaveBeenCalledTimes(2)
|
||||
finishSending()
|
||||
await waitForSends(4)
|
||||
expect(send.mock.calls.slice(2)).toEqual(
|
||||
[1, 2].map(id => [id, { type: "tab_changes", command: "updated" }]),
|
||||
)
|
||||
jest.useRealTimers()
|
||||
})
|
||||
35
src/background/tab_changes.ts
Normal file
35
src/background/tab_changes.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
const UPDATE_PENDING = 1
|
||||
const PRIORITY_PENDING = 2
|
||||
let tabChanges = 0
|
||||
let sendingTabChanges: Promise<void>
|
||||
export function messageTabChanges(command: string) {
|
||||
tabChanges |=
|
||||
/^tab_(?:close|created|moved|activated|attached|detached)$/u.test(
|
||||
command,
|
||||
)
|
||||
? PRIORITY_PENDING
|
||||
: UPDATE_PENDING
|
||||
sendingTabChanges ||= new Promise<void>(resolve =>
|
||||
setTimeout(resolve, 0),
|
||||
).then(async () => {
|
||||
while (tabChanges) {
|
||||
const changes = tabChanges
|
||||
tabChanges = 0
|
||||
const priority = changes & PRIORITY_PENDING
|
||||
const tabs = await browser.tabs
|
||||
.query({ active: true })
|
||||
.catch(() => [])
|
||||
await Promise.all(
|
||||
tabs.map(tab =>
|
||||
browser.tabs
|
||||
.sendMessage(tab.id, {
|
||||
type: "tab_changes",
|
||||
command: priority ? "priority" : "updated",
|
||||
})
|
||||
.catch(() => undefined),
|
||||
),
|
||||
)
|
||||
}
|
||||
sendingTabChanges = undefined
|
||||
})
|
||||
}
|
||||
|
|
@ -12,27 +12,41 @@ export const requestEventExpraInfoSpecMap = {
|
|||
export const requestEvents = Object.keys(requestEventExpraInfoSpecMap)
|
||||
|
||||
// I'm being lazy - strictly the functions map strings to void | blocking responses
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export const LISTENERS: Record<string, Record<string, Function>> = {}
|
||||
|
||||
export const registerWebRequestAutocmd = (
|
||||
export const registerWebRequestAutocmd = async (
|
||||
requestEvent: string,
|
||||
pattern: string,
|
||||
func: string,
|
||||
) => {
|
||||
// I'm being lazy - strictly the functions map strings to void | blocking responses
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
const listener = eval(func) as Function
|
||||
|
||||
if (!LISTENERS[requestEvent]) LISTENERS[requestEvent] = {}
|
||||
LISTENERS[requestEvent][pattern] = listener
|
||||
return browser.webRequest["on" + requestEvent].addListener(
|
||||
|
||||
await browser.webRequest["on" + requestEvent].addListener(
|
||||
listener,
|
||||
{ urls: [pattern] },
|
||||
requestEventExpraInfoSpecMap[requestEvent],
|
||||
)
|
||||
const oldListener = LISTENERS[requestEvent][pattern];
|
||||
// Add the new listener to our list if everything was successful
|
||||
LISTENERS[requestEvent][pattern] = listener
|
||||
|
||||
// Remove any previously registered autocmd for the same pattern
|
||||
if (oldListener) {
|
||||
await browser.webRequest["on" + requestEvent].removeListener(
|
||||
oldListener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const unregisterWebRequestAutocmd = (requestEvent, pattern) =>
|
||||
browser.webRequest["on" + requestEvent].removeListener(
|
||||
LISTENERS[requestEvent][pattern],
|
||||
)
|
||||
export const unregisterWebRequestAutocmd = async (requestEvent, pattern) => {
|
||||
if (LISTENERS[requestEvent] && LISTENERS[requestEvent][pattern]) {
|
||||
await browser.webRequest["on" + requestEvent].removeListener(
|
||||
LISTENERS[requestEvent][pattern],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
src/browser_action_popup.test.ts
Normal file
55
src/browser_action_popup.test.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
jest.mocked(browser.runtime.sendMessage)
|
||||
.mockResolvedValueOnce("false")
|
||||
.mockResolvedValueOnce("true")
|
||||
|
||||
const flush = () => new Promise(resolve => setTimeout(resolve))
|
||||
|
||||
test("popup separates toggling from reloading the active tab", async () => {
|
||||
document.body.innerHTML = `
|
||||
<p id="state"></p>
|
||||
<button id="toggle" disabled></button>
|
||||
<button id="reload" disabled></button>`
|
||||
jest.mocked(browser.tabs.query).mockResolvedValue([
|
||||
{ id: 7 } as browser.tabs.Tab,
|
||||
])
|
||||
jest.mocked(browser.tabs.reload).mockResolvedValue(undefined)
|
||||
const close = jest.spyOn(window, "close").mockImplementation()
|
||||
|
||||
await import("@src/browser_action_popup")
|
||||
await flush()
|
||||
expect(document.querySelector("#state").textContent).toBe(
|
||||
"Enabled globally",
|
||||
)
|
||||
expect(browser.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
type: "browser_action_background",
|
||||
command: "getState",
|
||||
args: [],
|
||||
})
|
||||
|
||||
document.querySelector<HTMLElement>("#toggle").click()
|
||||
expect(document.querySelector<HTMLButtonElement>("#reload").disabled).toBe(
|
||||
true,
|
||||
)
|
||||
await flush()
|
||||
expect(browser.runtime.sendMessage).toHaveBeenCalledWith({
|
||||
type: "browser_action_background",
|
||||
command: "toggle",
|
||||
args: [],
|
||||
})
|
||||
expect(browser.tabs.reload).not.toHaveBeenCalled()
|
||||
expect(document.querySelector("#state").textContent).toBe(
|
||||
"Disabled globally",
|
||||
)
|
||||
|
||||
document.querySelector<HTMLElement>("#reload").click()
|
||||
expect(document.querySelector<HTMLButtonElement>("#toggle").disabled).toBe(
|
||||
true,
|
||||
)
|
||||
await flush()
|
||||
expect(browser.tabs.query).toHaveBeenCalledWith({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
expect(browser.tabs.reload).toHaveBeenCalledWith(7)
|
||||
expect(close).toHaveBeenCalled()
|
||||
})
|
||||
63
src/browser_action_popup.ts
Normal file
63
src/browser_action_popup.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
export {}
|
||||
|
||||
function message(command: "getState" | "toggle") {
|
||||
return browser.runtime.sendMessage({
|
||||
type: "browser_action_background",
|
||||
command,
|
||||
args: [],
|
||||
})
|
||||
}
|
||||
|
||||
const state = document.querySelector<HTMLElement>("#state")
|
||||
const toggleButton = document.querySelector<HTMLButtonElement>("#toggle")
|
||||
const reloadButton = document.querySelector<HTMLButtonElement>("#reload")
|
||||
|
||||
function showState(value) {
|
||||
const disabled = value === "true"
|
||||
state.textContent = disabled ? "Disabled globally" : "Enabled globally"
|
||||
toggleButton.textContent = disabled
|
||||
? "Enable Tridactyl"
|
||||
: "Disable Tridactyl"
|
||||
}
|
||||
|
||||
function showError(error) {
|
||||
state.textContent = `Error: ${error instanceof Error ? error.message : error}`
|
||||
}
|
||||
|
||||
function setBusy(busy) {
|
||||
toggleButton.disabled = busy
|
||||
reloadButton.disabled = busy
|
||||
}
|
||||
|
||||
async function run(action: () => Promise<void>) {
|
||||
setBusy(true)
|
||||
try {
|
||||
await action()
|
||||
} catch (error) {
|
||||
showError(error)
|
||||
} finally {
|
||||
setBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
message("getState")
|
||||
.then(showState)
|
||||
.then(() => setBusy(false), showError)
|
||||
|
||||
toggleButton.addEventListener("click", () =>
|
||||
run(async () => {
|
||||
showState(await message("toggle"))
|
||||
}),
|
||||
)
|
||||
|
||||
reloadButton.addEventListener("click", () =>
|
||||
run(async () => {
|
||||
const [tab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true,
|
||||
})
|
||||
if (tab?.id === undefined) throw new Error("No active tab found")
|
||||
await browser.tabs.reload(tab.id)
|
||||
window.close()
|
||||
}),
|
||||
)
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
*
|
||||
* Contrary to the main tridactyl help page, this one doesn't tell you whether a specific function is bound to something. For now, you'll have to make do with `:bind` and `:viewconfig`.
|
||||
*
|
||||
* @packageDocumentation
|
||||
*/
|
||||
/** ignore this line */
|
||||
|
||||
|
|
@ -22,13 +23,20 @@ import { CompletionSourceFuse } from "@src/completions"
|
|||
import { AproposCompletionSource } from "@src/completions/Apropos"
|
||||
import { AutocmdCompletionSource } from "@src/completions/Autocmd"
|
||||
import { BindingsCompletionSource } from "@src/completions/Bindings"
|
||||
import { BmarkCompletionSource } from "@src/completions/Bmark"
|
||||
import {
|
||||
BmarkCompletionSource,
|
||||
BookmarkFolderCompletionSource,
|
||||
} from "@src/completions/Bmark"
|
||||
import { CompositeCompletionSource } from "@src/completions/Composite"
|
||||
import { ContainerCompletionSource } from "@src/completions/Container"
|
||||
import { DialogCompletionSource } from "@src/completions/Dialog"
|
||||
import { ExcmdCompletionSource } from "@src/completions/Excmd"
|
||||
import { ExtensionsCompletionSource } from "@src/completions/Extensions"
|
||||
import { FileSystemCompletionSource } from "@src/completions/FileSystem"
|
||||
import { FindCompletionSource } from "@src/completions/Find"
|
||||
import { GotoCompletionSource } from "@src/completions/Goto"
|
||||
import { GuisetCompletionSource } from "@src/completions/Guiset"
|
||||
import { GlossaryCompletionSource } from "@src/completions/Glossary"
|
||||
import { HelpCompletionSource } from "@src/completions/Help"
|
||||
import { HistoryCompletionSource } from "@src/completions/History"
|
||||
import { PreferenceCompletionSource } from "@src/completions/Preferences"
|
||||
|
|
@ -43,7 +51,7 @@ import { WindowCompletionSource } from "@src/completions/Window"
|
|||
import { ProxyCompletionSource } from "@src/completions/Proxy"
|
||||
import { contentState } from "@src/content/state_content"
|
||||
import { theme } from "@src/content/styling"
|
||||
import { getCommandlineFns } from "@src/lib/commandline_cmds"
|
||||
import { expandAbbreviation, getCommandlineFns } from "@src/lib/commandline_cmds"
|
||||
import * as tri_editor from "@src/lib/editor"
|
||||
import "@src/lib/DANGEROUS-html-tagged-template"
|
||||
import Logger from "@src/lib/logging"
|
||||
|
|
@ -53,8 +61,13 @@ import * as genericParser from "@src/parsers/genericmode"
|
|||
import * as perf from "@src/perf"
|
||||
import state, * as State from "@src/state"
|
||||
import * as R from "ramda"
|
||||
import { MinimalKey, minimalKeyFromKeyboardEvent } from "@src/lib/keyseq"
|
||||
import {
|
||||
MinimalKey,
|
||||
minimalKeyFromKeyboardEvent,
|
||||
isTrustedKeyboardEvent,
|
||||
} from "@src/lib/keyseq"
|
||||
import { TabGroupCompletionSource } from "@src/completions/TabGroup"
|
||||
import { ProfileCompletionSource } from "@src/completions/Profile"
|
||||
|
||||
/** @hidden **/
|
||||
const logger = new Logger("cmdline")
|
||||
|
|
@ -70,6 +83,11 @@ const commandline_state = {
|
|||
completionsDiv: window.document.getElementById("completions"),
|
||||
fns: undefined as ReturnType<typeof getCommandlineFns>,
|
||||
getCompletion,
|
||||
getCompletions: () =>
|
||||
(commandline_state.activeCompletions || []).flatMap(source =>
|
||||
source.visibleCompletions(),
|
||||
),
|
||||
getActiveCompletionSource,
|
||||
history,
|
||||
/** @hidden
|
||||
* This is to handle Escape key which, while the cmdline is focused,
|
||||
|
|
@ -82,6 +100,7 @@ const commandline_state = {
|
|||
keyEvents: new Array<MinimalKey>(),
|
||||
initialClInputValue: "",
|
||||
refresh_completions,
|
||||
resolveCloseWaiters,
|
||||
state,
|
||||
}
|
||||
|
||||
|
|
@ -95,39 +114,49 @@ function resizeArea() {
|
|||
focus()
|
||||
}
|
||||
}
|
||||
window.addEventListener("tridactyl-refresh-completions", resizeArea)
|
||||
|
||||
/** @hidden
|
||||
* This is a bit loosely defined at the moment.
|
||||
* Should work so long as there's only one completion source per prefix.
|
||||
*/
|
||||
function getCompletion(args_only = false) {
|
||||
function getActiveCompletionSource(): CompletionSourceFuse | undefined {
|
||||
if (!commandline_state.activeCompletions) return undefined
|
||||
|
||||
for (const comp of commandline_state.activeCompletions) {
|
||||
if (comp.state === "normal" && comp.completion !== undefined) {
|
||||
return args_only ? comp.args : comp.completion
|
||||
}
|
||||
}
|
||||
return commandline_state.activeCompletions.filter(
|
||||
({ state, completion }) =>
|
||||
state === "normal" && completion !== undefined,
|
||||
)[0]
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
function getCompletion(args_only = false): string | undefined {
|
||||
const activeSource = getActiveCompletionSource()
|
||||
if (!activeSource) return undefined
|
||||
return args_only ? activeSource.args : activeSource.completion
|
||||
}
|
||||
commandline_state.getCompletion = getCompletion
|
||||
|
||||
/** @hidden **/
|
||||
export function enableCompletions() {
|
||||
if (!commandline_state.activeCompletions) {
|
||||
commandline_state.activeCompletions = [
|
||||
AutocmdCompletionSource,
|
||||
// FindCompletionSource,
|
||||
FindCompletionSource,
|
||||
BindingsCompletionSource,
|
||||
BmarkCompletionSource,
|
||||
BookmarkFolderCompletionSource,
|
||||
TabAllCompletionSource,
|
||||
BufferCompletionSource,
|
||||
ExcmdCompletionSource,
|
||||
ThemeCompletionSource,
|
||||
TabHistoryCompletionSource,
|
||||
CompositeCompletionSource,
|
||||
ContainerCompletionSource,
|
||||
DialogCompletionSource,
|
||||
FileSystemCompletionSource,
|
||||
GotoCompletionSource,
|
||||
GuisetCompletionSource,
|
||||
GlossaryCompletionSource,
|
||||
HelpCompletionSource,
|
||||
AproposCompletionSource,
|
||||
HistoryCompletionSource,
|
||||
|
|
@ -136,6 +165,7 @@ export function enableCompletions() {
|
|||
SessionsCompletionSource,
|
||||
SettingsCompletionSource,
|
||||
TabGroupCompletionSource,
|
||||
ProfileCompletionSource,
|
||||
WindowCompletionSource,
|
||||
ExtensionsCompletionSource,
|
||||
ProxyCompletionSource,
|
||||
|
|
@ -160,24 +190,44 @@ export function enableCompletions() {
|
|||
/** @hidden **/
|
||||
const noblur = () => setTimeout(() => commandline_state.clInput.focus(), 0)
|
||||
|
||||
/** @hidden **/
|
||||
const closeWaiters: (() => void)[] = []
|
||||
|
||||
/** @hidden **/
|
||||
function waitForClose() {
|
||||
return new Promise<void>(resolve => closeWaiters.push(resolve))
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
function resolveCloseWaiters() {
|
||||
closeWaiters.splice(0).forEach(resolve => resolve())
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
export function focus() {
|
||||
function consumeBufferedPageKeys(bufferedPageKeys: string[]) {
|
||||
const clInputStillFocused = window.document.activeElement === commandline_state.clInput;
|
||||
logger.debug("stop_buffering_page_keys response received, bufferedPageKeys = ", bufferedPageKeys,
|
||||
"clInputStillFocused = " + clInputStillFocused)
|
||||
const clInputStillFocused =
|
||||
window.document.activeElement === commandline_state.clInput
|
||||
logger.debug(
|
||||
"stop_buffering_page_keys response received, bufferedPageKeys = ",
|
||||
bufferedPageKeys,
|
||||
"clInputStillFocused = " + clInputStillFocused,
|
||||
)
|
||||
if (bufferedPageKeys.length !== 0) {
|
||||
const currentClInputValue = commandline_state.clInput.value;
|
||||
const initialClInputValue = commandline_state.initialClInputValue;
|
||||
logger.debug("Consuming buffered page keys", bufferedPageKeys,
|
||||
const currentClInputValue = commandline_state.clInput.value
|
||||
const initialClInputValue = commandline_state.initialClInputValue
|
||||
logger.debug(
|
||||
"Consuming buffered page keys",
|
||||
bufferedPageKeys,
|
||||
"initialClInputValue = " + initialClInputValue,
|
||||
"currentClInputValue = " + currentClInputValue);
|
||||
"currentClInputValue = " + currentClInputValue,
|
||||
)
|
||||
// Native events are assumed to be character keydown events,
|
||||
// i.e. characters appended at the end of clInput.
|
||||
commandline_state.clInput.value =
|
||||
initialClInputValue
|
||||
+ bufferedPageKeys.join("")
|
||||
+ currentClInputValue.substring(initialClInputValue.length)
|
||||
initialClInputValue +
|
||||
bufferedPageKeys.join("") +
|
||||
currentClInputValue.substring(initialClInputValue.length)
|
||||
// Update completion.
|
||||
clInputValueChanged()
|
||||
}
|
||||
|
|
@ -185,8 +235,13 @@ export function focus() {
|
|||
commandline_state.clInput.focus()
|
||||
commandline_state.clInput.removeEventListener("blur", noblur)
|
||||
commandline_state.clInput.addEventListener("blur", noblur)
|
||||
logger.debug("commandline_frame clInput focus(), activeElement is clInput: " + (window.document.activeElement === commandline_state.clInput))
|
||||
Messaging.messageOwnTab("stop_buffering_page_keys").then(consumeBufferedPageKeys)
|
||||
logger.debug(
|
||||
"commandline_frame clInput focus(), activeElement is clInput: " +
|
||||
(window.document.activeElement === commandline_state.clInput),
|
||||
)
|
||||
Messaging.messageOwnTab("stop_buffering_page_keys").then(
|
||||
consumeBufferedPageKeys,
|
||||
)
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
|
|
@ -195,25 +250,45 @@ let HISTORY_SEARCH_STRING: string
|
|||
/** @hidden
|
||||
* Command line keybindings
|
||||
**/
|
||||
const keyParser = keys => genericParser.parser("exmaps", keys)
|
||||
const keyParser = keys => genericParser.parser("exmaps", keys, false)
|
||||
/** @hidden **/
|
||||
let history_called = false
|
||||
/** @hidden **/
|
||||
let prev_cmd_called_history = false
|
||||
|
||||
// Save programmer time by generating an immediately resolved promise
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const QUEUE: Promise<any>[] = [(async () => {})()]
|
||||
let commandSession = { pending: 0, queue: [Promise.resolve()] }
|
||||
const nativeInsertFallbacks = new Map<object, () => boolean>()
|
||||
|
||||
/** @hidden **/
|
||||
commandline_state.clInput.addEventListener(
|
||||
"keydown",
|
||||
function (keyevent: KeyboardEvent) {
|
||||
if (!keyevent.isTrusted) return
|
||||
logger.debug("commandline_frame clInput keydown event listener", keyevent)
|
||||
function (keyevent: Event) {
|
||||
if (!isTrustedKeyboardEvent(keyevent)) return
|
||||
logger.debug(
|
||||
"commandline_frame clInput keydown event listener",
|
||||
keyevent,
|
||||
)
|
||||
const session = commandSession
|
||||
commandline_state.keyEvents.push(minimalKeyFromKeyboardEvent(keyevent))
|
||||
const response = keyParser(commandline_state.keyEvents)
|
||||
if (response.isMatch) {
|
||||
const [funcname, ...args] = response.value?.startsWith("ex.")
|
||||
? response.value.slice(3).split(/\s+/)
|
||||
: []
|
||||
const command =
|
||||
commandline_state.fns[funcname as keyof typeof commandline_state.fns]
|
||||
const nativeInsertFallback = nativeInsertFallbacks.get(command)
|
||||
const commandArgument = args.length
|
||||
? args.join(" ")
|
||||
: nativeInsertFallback && keyevent.key.length === 1
|
||||
? keyevent.key
|
||||
: undefined
|
||||
const insertCharacterNatively =
|
||||
args.length === 0 &&
|
||||
keyevent.key.length === 1 &&
|
||||
!(keyevent.altKey || keyevent.ctrlKey || keyevent.metaKey) &&
|
||||
session.pending === 0 &&
|
||||
nativeInsertFallback?.()
|
||||
if (response.isMatch && !insertCharacterNatively) {
|
||||
keyevent.preventDefault()
|
||||
keyevent.stopImmediatePropagation()
|
||||
} else {
|
||||
|
|
@ -227,25 +302,24 @@ commandline_state.clInput.addEventListener(
|
|||
if (response.value) {
|
||||
commandline_state.keyEvents = []
|
||||
history_called = false
|
||||
if (insertCharacterNatively) return
|
||||
|
||||
// If excmds start with 'ex.' they're coming back to us anyway, so skip that.
|
||||
// This is definitely a hack. Should expand aliases with exmode, etc.
|
||||
// but this whole thing should be scrapped soon, so whatever.
|
||||
if (response.value.startsWith("ex.")) {
|
||||
const [funcname, ...args] = response.value.slice(3).split(/\s+/)
|
||||
if (funcname) {
|
||||
session.pending++
|
||||
|
||||
QUEUE[QUEUE.length - 1].then(() => {
|
||||
QUEUE.push(
|
||||
session.queue[session.queue.length - 1].then(() => {
|
||||
session.queue.push(
|
||||
// Abuse async to wrap non-promises in a promise
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
(async () =>
|
||||
commandline_state.fns[
|
||||
funcname as keyof typeof commandline_state.fns
|
||||
](
|
||||
args.length === 0 ? undefined : args.join(" "),
|
||||
))(),
|
||||
(async () => session === commandSession && command(commandArgument))()
|
||||
.catch(error => void logger.error(error))
|
||||
.finally(() => session.pending--),
|
||||
)
|
||||
prev_cmd_called_history = history_called
|
||||
if (session === commandSession)
|
||||
prev_cmd_called_history = history_called
|
||||
})
|
||||
} else {
|
||||
// Send excmds directly to our own tab, which fixes the
|
||||
|
|
@ -258,6 +332,7 @@ commandline_state.clInput.addEventListener(
|
|||
// to be a touch less latency-sensitive.
|
||||
Messaging.messageOwnTab("controller_content", "acceptExCmd", [
|
||||
response.value,
|
||||
"commandline",
|
||||
]).then(_ => (prev_cmd_called_history = history_called))
|
||||
}
|
||||
} else {
|
||||
|
|
@ -267,50 +342,80 @@ commandline_state.clInput.addEventListener(
|
|||
true,
|
||||
)
|
||||
|
||||
let refreshQueue: Promise<unknown> = Promise.resolve()
|
||||
export function refresh_completions(exstr) {
|
||||
const session = commandSession
|
||||
const result = refreshQueue.then(() =>
|
||||
session === commandSession ? refreshCompletions(exstr) : undefined,
|
||||
)
|
||||
refreshQueue = result.catch(() => undefined)
|
||||
return result
|
||||
}
|
||||
|
||||
function refreshCompletions(exstr) {
|
||||
if (!commandline_state.activeCompletions) enableCompletions()
|
||||
// We can't use the regular logging mechanism because the user is using the command line.
|
||||
return Promise.all(
|
||||
commandline_state.activeCompletions.map(comp =>
|
||||
comp.filter(exstr).then(() => {
|
||||
if (comp.shouldRefresh()) {
|
||||
return resizeArea()
|
||||
}
|
||||
}),
|
||||
comp
|
||||
.filter(exstr)
|
||||
.then(() => {
|
||||
if (comp.shouldRefresh()) {
|
||||
return resizeArea()
|
||||
}
|
||||
})
|
||||
.catch(err => console.error(err)),
|
||||
),
|
||||
).catch(err => {
|
||||
console.error(err)
|
||||
return []
|
||||
}) // We can't use the regular logging mechanism because the user is using the command line.
|
||||
)
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
let onInputPromise: Promise<any> = Promise.resolve()
|
||||
let onInputPromise: Promise<void | void[]> = Promise.resolve()
|
||||
const COMPLETION_THROTTLE_MS = 100
|
||||
let completionTimer
|
||||
let lastCompletionStarted = -Infinity
|
||||
/** @hidden **/
|
||||
commandline_state.clInput.addEventListener("input", () => {
|
||||
logger.debug("commandline_frame clInput input event listener")
|
||||
clInputValueChanged();
|
||||
clInputValueChanged()
|
||||
})
|
||||
|
||||
/** @hidden **/
|
||||
async function updateCompletions(exstr: string, session = commandSession) {
|
||||
lastCompletionStarted = performance.now()
|
||||
await onInputPromise
|
||||
if (session !== commandSession) return
|
||||
if (exstr !== commandline_state.clInput.value) {
|
||||
contentState.cmdline_filter = exstr
|
||||
return
|
||||
}
|
||||
|
||||
onInputPromise = refresh_completions(exstr)
|
||||
onInputPromise.then(() => {
|
||||
contentState.cmdline_filter = exstr
|
||||
})
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
function clInputValueChanged() {
|
||||
const exstr = commandline_state.clInput.value
|
||||
const session = commandSession
|
||||
contentState.current_cmdline = exstr
|
||||
contentState.cmdline_filter = ""
|
||||
// Schedule completion computation. We do not start computing immediately because this would incur a slow down on quickly repeated input events (e.g. maintaining <Backspace> pressed)
|
||||
setTimeout(async () => {
|
||||
// Make sure the previous computation has ended
|
||||
await onInputPromise
|
||||
// If we're not the current completion computation anymore, stop
|
||||
if (exstr !== commandline_state.clInput.value) {
|
||||
contentState.cmdline_filter = exstr
|
||||
return
|
||||
}
|
||||
|
||||
onInputPromise = refresh_completions(exstr)
|
||||
onInputPromise.then(() => {
|
||||
contentState.cmdline_filter = exstr
|
||||
})
|
||||
}, 100)
|
||||
// Run immediately when idle, otherwise retain one trailing refresh.
|
||||
clearTimeout(completionTimer)
|
||||
const delay =
|
||||
COMPLETION_THROTTLE_MS - (performance.now() - lastCompletionStarted)
|
||||
if (delay <= 0) void updateCompletions(exstr, session)
|
||||
else
|
||||
completionTimer = setTimeout(
|
||||
() =>
|
||||
void updateCompletions(
|
||||
commandline_state.clInput.value,
|
||||
session,
|
||||
),
|
||||
delay,
|
||||
)
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
|
|
@ -322,6 +427,12 @@ let cmdline_history_current = ""
|
|||
* Otherwise, no need to pass an argument.
|
||||
*/
|
||||
export function clear(evlistener = false) {
|
||||
if (evlistener) {
|
||||
commandSession = { pending: 0, queue: [Promise.resolve()] }
|
||||
clearTimeout(completionTimer)
|
||||
lastCompletionStarted = -Infinity
|
||||
}
|
||||
if (evlistener) prev_cmd_called_history = false
|
||||
if (evlistener)
|
||||
commandline_state.clInput.removeEventListener("blur", noblur)
|
||||
commandline_state.clInput.value = ""
|
||||
|
|
@ -352,6 +463,7 @@ async function history(n) {
|
|||
const pot_history = matches[clamped_ind]
|
||||
commandline_state.clInput.value =
|
||||
pot_history === undefined ? cmdline_history_current : pot_history
|
||||
clInputValueChanged()
|
||||
|
||||
// if there was no clampage, update history position
|
||||
// there's a more sensible way of doing this but that would require more programmer time
|
||||
|
|
@ -369,19 +481,29 @@ export function fillcmdline(
|
|||
newcommand?: string,
|
||||
trailspace = true,
|
||||
ffocus = true,
|
||||
wait = false,
|
||||
) {
|
||||
logger.debug("commandline_frame fillcmdline(newcommand = " + newcommand + " trailspace = " + trailspace + " ffocus = " + ffocus + ")")
|
||||
logger.debug(
|
||||
"commandline_frame fillcmdline(newcommand = " +
|
||||
newcommand +
|
||||
" trailspace = " +
|
||||
trailspace +
|
||||
" ffocus = " +
|
||||
ffocus +
|
||||
")",
|
||||
)
|
||||
if (trailspace) commandline_state.clInput.value = newcommand + " "
|
||||
else commandline_state.clInput.value = newcommand
|
||||
commandline_state.initialClInputValue = commandline_state.clInput.value
|
||||
commandline_state.isVisible = true
|
||||
const closed = wait ? waitForClose() : undefined
|
||||
let result = Promise.resolve([])
|
||||
// Focus is lost for some reason.
|
||||
if (ffocus) {
|
||||
focus()
|
||||
result = refresh_completions(commandline_state.clInput.value)
|
||||
}
|
||||
return result
|
||||
return wait ? result.then(() => closed) : result
|
||||
}
|
||||
|
||||
/** @hidden **/
|
||||
|
|
@ -406,7 +528,17 @@ export function editor_function(fn_name: keyof typeof tri_editor, ...args) {
|
|||
Messaging.addListener("commandline_frame", Messaging.attributeCaller(SELF))
|
||||
logger.debug("Added commandline_frame message listener")
|
||||
|
||||
commandline_state.fns = getCommandlineFns(commandline_state)
|
||||
/** @namespace */
|
||||
export const commandlineFns = getCommandlineFns(commandline_state)
|
||||
commandline_state.fns = commandlineFns
|
||||
nativeInsertFallbacks.set(
|
||||
commandline_state.fns.insert_character_or_completion,
|
||||
() => {
|
||||
if (getCompletion()) return false
|
||||
expandAbbreviation(commandline_state.clInput)
|
||||
return true
|
||||
},
|
||||
)
|
||||
Messaging.addListener(
|
||||
"commandline_cmd",
|
||||
Messaging.attributeCaller(commandline_state.fns),
|
||||
|
|
@ -417,8 +549,8 @@ Messaging.addListener(
|
|||
// object since there's apparently a bug that causes performance
|
||||
// observers to be GC'd even if they're still the target of a
|
||||
// callback.
|
||||
;(window as any).tri = Object.assign(window.tri || {}, {
|
||||
window["tri"] = Object.assign(window.tri || {}, {
|
||||
perfObserver: perf.listenForCounters(),
|
||||
})
|
||||
|
||||
Messaging.messageOwnTab("commandline_frame_ready_to_receive_messages")
|
||||
Messaging.messageOwnTab("commandline_frame_ready_to_receive_messages", window.name)
|
||||
|
|
|
|||
|
|
@ -11,11 +11,14 @@ Concrete completion classes have been moved to src/completions/.
|
|||
*/
|
||||
|
||||
import Fuse from "fuse.js"
|
||||
import { enumerate } from "@src/lib/itertools"
|
||||
import { toNumber } from "@src/lib/convert"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import { backoff } from "@src/lib/patience"
|
||||
import * as config from "@src/lib/config"
|
||||
export { decodeUrlForDisplay } from "@src/lib/url_util"
|
||||
|
||||
export function treePrefix(level: number) {
|
||||
return ` ${" ".repeat(Math.max(level - 1, 0))}${level ? "┌─" : ""}· `
|
||||
}
|
||||
|
||||
export const DEFAULT_FAVICON = browser.runtime.getURL(
|
||||
"static/defaultFavicon.svg",
|
||||
|
|
@ -33,16 +36,17 @@ export abstract class CompletionOption {
|
|||
}
|
||||
|
||||
export abstract class CompletionSource {
|
||||
readonly options: CompletionOption[]
|
||||
options: CompletionOption[]
|
||||
node: HTMLElement
|
||||
public completion: string
|
||||
public args: string
|
||||
public trailingSpace: boolean
|
||||
protected prefixes: string[] = []
|
||||
protected lastFocused: CompletionOption
|
||||
private _state: OptionState
|
||||
private _prevState: OptionState
|
||||
|
||||
constructor(prefixes) {
|
||||
constructor(prefixes, options = { trailingSpace: true }) {
|
||||
const commands = aliases.getCmdAliasMapping()
|
||||
|
||||
// Now, for each prefix given as argument, add it to the completionsource's prefix list and also add any alias it has
|
||||
|
|
@ -56,6 +60,11 @@ export abstract class CompletionSource {
|
|||
|
||||
// Not sure this is necessary but every completion source has it
|
||||
this.prefixes = this.prefixes.map(p => p + " ")
|
||||
this.trailingSpace = options.trailingSpace
|
||||
}
|
||||
|
||||
protected canonicalisePrefix(prefix: string) {
|
||||
return aliases.expandExstr(prefix).trim()
|
||||
}
|
||||
|
||||
/** Control presentation of Source */
|
||||
|
|
@ -145,14 +154,13 @@ export interface CompletionOptionFuse extends CompletionOptionHTML {
|
|||
}
|
||||
|
||||
export interface ScoredOption {
|
||||
index: number
|
||||
option: CompletionOptionFuse
|
||||
score: number
|
||||
}
|
||||
|
||||
export abstract class CompletionSourceFuse extends CompletionSource {
|
||||
options: CompletionOptionFuse[]
|
||||
public node
|
||||
public options: CompletionOptionFuse[]
|
||||
|
||||
fuseOptions = {
|
||||
keys: ["fuseKeys"],
|
||||
|
|
@ -174,11 +182,22 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
|
||||
protected optionContainer = html`<table class="optionContainer"></table>`
|
||||
|
||||
constructor(prefixes, className: string, title?: string) {
|
||||
super(prefixes)
|
||||
this.node = html`<div class="${className} hidden">
|
||||
<div class="sectionHeader">${title || className}</div>
|
||||
</div>`
|
||||
private fusedOptions: CompletionOptionFuse[]
|
||||
|
||||
constructor(
|
||||
prefixes,
|
||||
className: string,
|
||||
title?: string | HTMLElement,
|
||||
options = { trailingSpace: true },
|
||||
) {
|
||||
super(prefixes, options)
|
||||
this.node = html`<div class="${className} hidden"></div>`
|
||||
const header =
|
||||
typeof title === "string" || title === undefined
|
||||
? html`<div>${title || className}</div>`
|
||||
: title
|
||||
header.classList.add("sectionHeader")
|
||||
this.node.appendChild(header)
|
||||
this.node.appendChild(this.optionContainer)
|
||||
this.state = "hidden"
|
||||
}
|
||||
|
|
@ -224,6 +243,18 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
this.updateDisplay()
|
||||
}
|
||||
|
||||
completionForOption(option: CompletionOption) {
|
||||
const [prefix] = this.splitOnPrefix(this.lastExstr)
|
||||
return prefix ? [prefix, option.value].join(" ") : option.value
|
||||
}
|
||||
|
||||
visibleCompletions() {
|
||||
if (this.state === "hidden") return []
|
||||
return (this.options || [])
|
||||
.filter(option => option.state !== "hidden")
|
||||
.map(option => this.completionForOption(option))
|
||||
}
|
||||
|
||||
select(option: CompletionOption) {
|
||||
if (this.lastExstr !== undefined && option !== undefined) {
|
||||
const [prefix] = this.splitOnPrefix(this.lastExstr)
|
||||
|
|
@ -248,20 +279,15 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
|
||||
/** Rtn sorted array of {option, score} */
|
||||
scoredOptions(query: string): ScoredOption[] {
|
||||
const searchThis = this.options.map((elem, index) => ({
|
||||
index,
|
||||
fuseKeys: elem.fuseKeys,
|
||||
if (this.fuse === undefined || this.fusedOptions !== this.options) {
|
||||
this.fuse = new Fuse(this.options, this.fuseOptions)
|
||||
this.fusedOptions = this.options
|
||||
}
|
||||
|
||||
return this.fuse.search(query).map(result => ({
|
||||
option: result.item,
|
||||
score: result.score,
|
||||
}))
|
||||
this.fuse = new Fuse(searchThis, this.fuseOptions)
|
||||
return this.fuse.search(query).map(result => {
|
||||
// console.log(result, result.item, query)
|
||||
const index = toNumber(result.item.index)
|
||||
return {
|
||||
index,
|
||||
option: this.options[index],
|
||||
score: result.score as number,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Set option state by score
|
||||
|
|
@ -270,44 +296,35 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
focus the best match.
|
||||
*/
|
||||
setStateFromScore(scoredOpts: ScoredOption[], autoselect = false) {
|
||||
const matches = scoredOpts.map(res => res.index)
|
||||
const matches = new Set(scoredOpts.map(res => res.option))
|
||||
|
||||
const hidden_options = []
|
||||
for (const [index, option] of enumerate(this.options)) {
|
||||
if (matches.includes(index)) option.state = "normal"
|
||||
else {
|
||||
for (const option of this.options) {
|
||||
if (matches.has(option)) {
|
||||
option.state = "normal"
|
||||
} else {
|
||||
option.state = "hidden"
|
||||
hidden_options.push(option)
|
||||
}
|
||||
}
|
||||
|
||||
// ideally, this would not deselect anything unless it fell off the list of matches
|
||||
if (matches.length && autoselect) {
|
||||
this.select(this.options[matches[0]])
|
||||
if (scoredOpts.length && autoselect) {
|
||||
this.select(scoredOpts[0].option)
|
||||
} else {
|
||||
this.deselect()
|
||||
}
|
||||
|
||||
// sort this.options by score
|
||||
if (this.sortScoredOptions) {
|
||||
const sorted_options = matches.map(index => this.options[index])
|
||||
const sorted_options = scoredOpts.map(res => res.option)
|
||||
this.options = sorted_options.concat(hidden_options)
|
||||
this.fusedOptions = this.options
|
||||
}
|
||||
}
|
||||
|
||||
/** Call to replace the current display */
|
||||
updateDisplay() {
|
||||
const newContainer = this.optionContainer.cloneNode(
|
||||
false,
|
||||
) as HTMLElement
|
||||
|
||||
for (const option of this.options) {
|
||||
if (option.state !== "hidden")
|
||||
// This is probably slow: `.html` means the HTML parser will be invoked
|
||||
newContainer.appendChild(option.html)
|
||||
}
|
||||
this.optionContainer.replaceWith(newContainer)
|
||||
this.optionContainer = newContainer
|
||||
const visibleOptions = this.options.filter(o => o.state !== "hidden").map(o => o.html)
|
||||
this.optionContainer.replaceChildren(...visibleOptions)
|
||||
this.next(0)
|
||||
}
|
||||
|
||||
|
|
@ -322,7 +339,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
this.deselect()
|
||||
// visopts.length + 1 because we want an empty completion at the end
|
||||
const max = visopts.length + 1
|
||||
const opt = visopts[(currind + inc + max) % max]
|
||||
const opt = visopts[((currind + inc) % max + max) % max]
|
||||
if (opt) this.select(opt)
|
||||
return true
|
||||
})
|
||||
|
|
@ -332,7 +349,7 @@ export abstract class CompletionSourceFuse extends CompletionSource {
|
|||
/* abstract onUpdate(query: string, prefix: string, options: CompletionOptionFuse[]) */
|
||||
|
||||
// Lots of methods don't need this but some do
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars-experimental
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
||||
async onInput(exstr: string) {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import {
|
||||
excmdsFunctions,
|
||||
defaultConfigMembers,
|
||||
getDoc,
|
||||
memberDoc,
|
||||
} from "@src/.metadata.generated"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import * as config from "@src/lib/config"
|
||||
import { glossaryOptions } from "@src/completions/Glossary"
|
||||
|
||||
class AproposCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
class AproposCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
constructor(public name: string, doc: string, flag: string) {
|
||||
constructor(
|
||||
public name: string,
|
||||
doc: string,
|
||||
flag: string,
|
||||
) {
|
||||
super()
|
||||
this.value = `${flag} ${name}`
|
||||
this.html = html`<tr class="AproposCompletionOption option">
|
||||
|
|
@ -20,12 +29,21 @@ class AproposCompletionOption extends Completions.CompletionOptionHTML
|
|||
export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: AproposCompletionOption[]
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["apropos"], "AproposCompletionSource", "Apropos")
|
||||
constructor(
|
||||
private _parent,
|
||||
prefixes = ["apropos"],
|
||||
className = "AproposCompletionSource",
|
||||
title = "Apropos",
|
||||
) {
|
||||
super(prefixes, className, title)
|
||||
|
||||
this._parent.appendChild(this.node)
|
||||
}
|
||||
|
||||
protected createOption(name: string, doc: string, flag: string) {
|
||||
return new AproposCompletionOption(name, doc, flag)
|
||||
}
|
||||
|
||||
public async filter(exstr: string) {
|
||||
this.lastExstr = exstr
|
||||
this.completion = undefined
|
||||
|
|
@ -42,20 +60,13 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return
|
||||
}
|
||||
|
||||
const file = Metadata.everything.getFile("src/lib/config.ts")
|
||||
const default_config = file.getClass("default_config")
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
const fns = excmds.getFunctions()
|
||||
const settings = config.get()
|
||||
const exaliases = settings.exaliases
|
||||
const bindings = settings.nmaps
|
||||
if (
|
||||
fns === undefined ||
|
||||
exaliases === undefined ||
|
||||
bindings === undefined
|
||||
) {
|
||||
if (exaliases === undefined || bindings === undefined) {
|
||||
return
|
||||
}
|
||||
const fns = Object.entries(excmdsFunctions)
|
||||
|
||||
const flags = {
|
||||
"-a": (options, query) =>
|
||||
|
|
@ -65,17 +76,15 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
(
|
||||
alias +
|
||||
aliases.expandExstr(alias) +
|
||||
excmds.getFunction(aliases.expandExstr(alias))
|
||||
excmdsFunctions[aliases.expandExstr(alias)]
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
.map(alias => {
|
||||
const cmd = aliases.expandExstr(alias)
|
||||
const doc =
|
||||
(excmds.getFunction(cmd) || ({} as any)).doc ||
|
||||
""
|
||||
return new AproposCompletionOption(
|
||||
const doc = getDoc(excmdsFunctions[cmd])
|
||||
return this.createOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${doc}`,
|
||||
"-a",
|
||||
|
|
@ -92,7 +101,7 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
)
|
||||
.map(
|
||||
binding =>
|
||||
new AproposCompletionOption(
|
||||
this.createOption(
|
||||
binding,
|
||||
`Normal mode binding for \`${bindings[binding]}\``,
|
||||
"-b",
|
||||
|
|
@ -102,16 +111,14 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
"-e": (options, query) =>
|
||||
options.concat(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) =>
|
||||
!fn.hidden &&
|
||||
(name + fn.doc).toLowerCase().includes(query),
|
||||
.filter(([name, fn]) =>
|
||||
(name + getDoc(fn)).toLowerCase().includes(query),
|
||||
)
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new AproposCompletionOption(
|
||||
this.createOption(
|
||||
name,
|
||||
`Excmd. ${fn.doc}`,
|
||||
`Excmd. ${getDoc(fn)}`,
|
||||
"-e",
|
||||
),
|
||||
),
|
||||
|
|
@ -120,23 +127,23 @@ export class AproposCompletionSource extends Completions.CompletionSourceFuse {
|
|||
options.concat(
|
||||
Object.keys(settings)
|
||||
.filter(x =>
|
||||
(x + default_config.getMember(x)?.doc)
|
||||
(x + memberDoc(defaultConfigMembers[x]))
|
||||
.toLowerCase()
|
||||
.includes(query),
|
||||
)
|
||||
.map(setting => {
|
||||
const member = default_config.getMember(setting)
|
||||
let doc = ""
|
||||
if (member !== undefined) {
|
||||
doc = member.doc
|
||||
}
|
||||
return new AproposCompletionOption(
|
||||
const doc = memberDoc(defaultConfigMembers[setting])
|
||||
return this.createOption(
|
||||
setting,
|
||||
`Setting. ${doc}`,
|
||||
"-s",
|
||||
)
|
||||
}),
|
||||
),
|
||||
"-g": (options, query) =>
|
||||
options.concat(
|
||||
glossaryOptions(this.createOption.bind(this), query, false),
|
||||
),
|
||||
}
|
||||
|
||||
const args = query.split(" ")
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export class AutocmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
constructor(private _parent) {
|
||||
super(
|
||||
["autocmd", "autocmddelete"],
|
||||
["autocmd", "autocmddelete", "autocontaindelete"],
|
||||
"AutocmdCompletionSource",
|
||||
"Autocommands",
|
||||
)
|
||||
|
|
@ -59,7 +59,16 @@ export class AutocmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
this.state = "hidden"
|
||||
return
|
||||
}
|
||||
const is_autocmddelete = /del/.test(prefix)
|
||||
const command = this.canonicalisePrefix(prefix)
|
||||
if (command === "autocontaindelete") {
|
||||
this.options = Object.entries(config.get("autocontain"))
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([pattern, container]) =>
|
||||
new AutocmdCompletionOption(pattern, container, `-u ${pattern}`),
|
||||
)
|
||||
return this.updateChain()
|
||||
}
|
||||
const is_autocmddelete = command === "autocmddelete"
|
||||
const filter_defined_autocmds = is_autocmddelete
|
||||
const defined_autocmds = config.get("autocmds")
|
||||
// Config may contain empty dictionnaries if user deleted all patterns
|
||||
|
|
|
|||
|
|
@ -2,6 +2,17 @@ import * as Completions from "@src/completions"
|
|||
import * as config from "@src/lib/config"
|
||||
import * as Binding from "@src/lib/binding"
|
||||
|
||||
const modeDescriptions = new Map([
|
||||
["normal", "Default mode"],
|
||||
["ignore", "Almost all keys passed through to web pages except these binds"],
|
||||
["insert", "Editing text"],
|
||||
["input", "Bindings after `gi` focuses a text field"],
|
||||
["ex", "Command line binds"],
|
||||
["hint", "Binds accessible during hint mode"],
|
||||
["visual", "Text selection outside insert mode"],
|
||||
["browser", "Binds accessible everywhere, including while pages load. NB: some caveats, see :help bind"],
|
||||
])
|
||||
|
||||
class BindingsCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
|
@ -51,6 +62,7 @@ export class BindingsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
this.state = "hidden"
|
||||
return
|
||||
}
|
||||
prefix = this.canonicalisePrefix(prefix)
|
||||
|
||||
this.deselect()
|
||||
|
||||
|
|
@ -90,7 +102,7 @@ export class BindingsCompletionSource extends Completions.CompletionSourceFuse {
|
|||
options + "--mode=" + name,
|
||||
{
|
||||
name,
|
||||
value: "",
|
||||
value: modeDescriptions.get(name),
|
||||
mode: "Mode Name",
|
||||
},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class BmarkCompletionOption
|
|||
|
||||
constructor(
|
||||
public value: string,
|
||||
bmark: browser.bookmarks.BookmarkTreeNode,
|
||||
bmark: providers.Bookmark,
|
||||
) {
|
||||
super()
|
||||
if (!bmark.title) {
|
||||
|
|
@ -17,14 +17,14 @@ class BmarkCompletionOption
|
|||
}
|
||||
|
||||
// Push properties we want to fuzmatch on
|
||||
this.fuseKeys.push(bmark.title, bmark.url)
|
||||
this.fuseKeys.push(bmark.path, bmark.title, bmark.url)
|
||||
|
||||
this.html = html`<tr class="BmarkCompletionOption option">
|
||||
<td class="prefix">${"".padEnd(2)}</td>
|
||||
<td class="title">${bmark.title}</td>
|
||||
<td class="title">${bmark.path}${bmark.title}</td>
|
||||
<td class="content">
|
||||
<a class="url" target="_blank" href=${bmark.url}
|
||||
>${bmark.url}</a
|
||||
>${Completions.decodeUrlForDisplay(bmark.url)}</a
|
||||
>
|
||||
</td>
|
||||
</tr>`
|
||||
|
|
@ -70,6 +70,11 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
|
|||
option += " "
|
||||
query = args.slice(2).join(" ")
|
||||
}
|
||||
if (query.startsWith("-b")) {
|
||||
const args = query.split(" ")
|
||||
option += args.slice(0, 1).join(" ") + " "
|
||||
query = args.slice(1).join(" ")
|
||||
}
|
||||
|
||||
this.completion = undefined
|
||||
this.options = (await providers.getBookmarks(query))
|
||||
|
|
@ -96,14 +101,57 @@ export class BmarkCompletionSource extends Completions.CompletionSourceFuse {
|
|||
// Call concrete class
|
||||
return this.updateDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
select(option: Completions.CompletionOption) {
|
||||
if (this.lastExstr !== undefined && option !== undefined) {
|
||||
this.completion = "bmarks " + option.value
|
||||
option.state = "focused"
|
||||
this.lastFocused = option
|
||||
} else {
|
||||
throw new Error("lastExstr and option must be defined!")
|
||||
export class BookmarkFolderCompletionSource extends Completions.CompletionSourceFuse {
|
||||
constructor() {
|
||||
super(["bmark"], "BookmarkFolderCompletionSource", "Bookmark Folders", {
|
||||
trailingSpace: false,
|
||||
})
|
||||
}
|
||||
|
||||
async onInput(exstr: string) {
|
||||
const [_command, _url, path] = this.parseArgs(exstr)
|
||||
if (path == undefined) {
|
||||
this.options = undefined
|
||||
return
|
||||
}
|
||||
this.options = (await providers.getBookmarkFolders(path))
|
||||
.slice(0, 10)
|
||||
.map(path => new BookmarkFolderCompletionOption(path))
|
||||
}
|
||||
|
||||
splitOnPrefix(exstr: string): string[] {
|
||||
const [command, url, path] = this.parseArgs(exstr)
|
||||
return [`${command} ${url}`, path]
|
||||
}
|
||||
|
||||
private parseArgs(exstr: string): string[] {
|
||||
const [command, args] = super.splitOnPrefix(exstr)
|
||||
if (!args) {
|
||||
return [command]
|
||||
}
|
||||
const spaceIndex = args.search(/\s+/)
|
||||
const url = args.slice(0, spaceIndex)
|
||||
if (spaceIndex == -1) {
|
||||
return [command, url]
|
||||
}
|
||||
const path = args.slice(spaceIndex + 1)
|
||||
return [command, url, path]
|
||||
}
|
||||
}
|
||||
|
||||
class BookmarkFolderCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
fuseKeys = []
|
||||
|
||||
constructor(public value: string) {
|
||||
super()
|
||||
this.fuseKeys.push(value)
|
||||
this.html = html`<tr class="BookmarkFolderCompletionOption option">
|
||||
<td class="prefix">${"".padEnd(2)}</td>
|
||||
<td class="title">${value}</td>
|
||||
</tr>`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as ExcmdCompletions from "@src/completions/Excmd"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import { excmdsFunctions, getDoc } from "@src/.metadata.generated"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
|
||||
const PREFIX = "composite"
|
||||
const regex = new RegExp("^" + PREFIX + " ")
|
||||
|
||||
// Most of this is copied verbatim from Excmd.ts - would have liked to inherit but constructor posed difficulties
|
||||
export class CompositeCompletionSource extends Completions.CompletionSourceFuse {
|
||||
|
|
@ -27,7 +26,7 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
return this.updateOptions(exstr)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
updateChain(exstr = this.lastExstr, options = this.options) {
|
||||
if (this.options.length > 0) this.state = "normal"
|
||||
else this.state = "hidden"
|
||||
|
|
@ -35,6 +34,15 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
this.updateDisplay()
|
||||
}
|
||||
|
||||
completionForOption(option: ExcmdCompletions.ExcmdCompletionOption) {
|
||||
return (
|
||||
this.lastExstr.replace(
|
||||
new RegExp(this.getendexstr(this.lastExstr) + "$"),
|
||||
"",
|
||||
) + option.value
|
||||
)
|
||||
}
|
||||
|
||||
select(option: ExcmdCompletions.ExcmdCompletionOption) {
|
||||
this.completion =
|
||||
this.lastExstr.replace(
|
||||
|
|
@ -65,21 +73,15 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
return
|
||||
}
|
||||
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
if (!excmds) return
|
||||
const fns = excmds.getFunctions()
|
||||
|
||||
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
|
||||
this.options = this.scoreOptions(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) => !fn.hidden && name.startsWith(end_exstr),
|
||||
)
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([name]) => name.startsWith(end_exstr))
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new ExcmdCompletions.ExcmdCompletionOption(
|
||||
name,
|
||||
fn.doc,
|
||||
getDoc(fn),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -90,12 +92,12 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
)
|
||||
for (const alias of exaliases) {
|
||||
const cmd = aliases.expandExstr(alias)
|
||||
const fn = excmds.getFunction(cmd)
|
||||
const fn = excmdsFunctions[cmd]
|
||||
if (fn) {
|
||||
this.options.push(
|
||||
new ExcmdCompletions.ExcmdCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${fn.doc}`,
|
||||
`Alias for \`${cmd}\`. ${getDoc(fn)}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -118,8 +120,8 @@ export class CompositeCompletionSource extends Completions.CompletionSourceFuse
|
|||
}
|
||||
|
||||
private getendexstr(exstr) {
|
||||
return exstr
|
||||
.replace(regex, "")
|
||||
const [, query = exstr] = this.splitOnPrefix(exstr)
|
||||
return query
|
||||
.split("|")
|
||||
.slice(-1)[0]
|
||||
.split(";")
|
||||
|
|
|
|||
37
src/completions/Container.ts
Normal file
37
src/completions/Container.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Containers from "@src/lib/containers"
|
||||
|
||||
class ContainerCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
constructor(public value: string) {
|
||||
super()
|
||||
this.fuseKeys.push(value)
|
||||
this.html = html`<tr class="ContainerCompletionOption option">
|
||||
<td class="title">${value}</td>
|
||||
</tr>`
|
||||
}
|
||||
}
|
||||
|
||||
export class ContainerCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: ContainerCompletionOption[]
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["recontain", "containerclose", "containerdelete", "containerupdate"], "ContainerCompletionSource", "Containers")
|
||||
this._parent.appendChild(this.node)
|
||||
}
|
||||
|
||||
async onInput(exstr: string) {
|
||||
const [prefix, query] = this.splitOnPrefix(exstr)
|
||||
if (!prefix) return
|
||||
const command = this.canonicalisePrefix(prefix)
|
||||
this.options =
|
||||
command === "containerupdate" && /\s/u.test(query)
|
||||
? undefined
|
||||
: (await Containers.getAll())
|
||||
.filter(container => command !== "containerupdate" || !/\s/u.test(container.name))
|
||||
.map(container => new ContainerCompletionOption(container.name))
|
||||
}
|
||||
}
|
||||
30
src/completions/Dialog.ts
Normal file
30
src/completions/Dialog.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import { ABOUT_PAGES } from "@src/lib/about_pages"
|
||||
import "@src/lib/DANGEROUS-html-tagged-template"
|
||||
|
||||
class DialogCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys: string[]
|
||||
|
||||
constructor(public value: string, description: string) {
|
||||
super()
|
||||
this.fuseKeys = [value, description]
|
||||
this.html = html`<tr class="DialogCompletionOption option">
|
||||
<td class="title">${value}</td>
|
||||
<td class="description"></td>
|
||||
</tr>`
|
||||
this.html.querySelector(".description").textContent = description
|
||||
}
|
||||
}
|
||||
|
||||
export class DialogCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options = Object.entries(ABOUT_PAGES).map(
|
||||
([page, description]) => new DialogCompletionOption(page, description),
|
||||
)
|
||||
|
||||
constructor(parent: HTMLElement) {
|
||||
super(["dialog"], "DialogCompletionSource", "Firefox about pages")
|
||||
parent.appendChild(this.node)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import { excmdsFunctions, getDoc } from "@src/.metadata.generated"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
|
||||
export class ExcmdCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
export class ExcmdCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
constructor(public value: string, public documentation: string = "") {
|
||||
constructor(
|
||||
public value: string,
|
||||
public documentation = "",
|
||||
) {
|
||||
super()
|
||||
this.fuseKeys.push(this.value)
|
||||
this.fuseKeys.push(this.value, this.documentation)
|
||||
|
||||
// Create HTMLElement
|
||||
this.html = html`<tr class="ExcmdCompletionOption option">
|
||||
|
|
@ -37,7 +39,7 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
return this.updateOptions(exstr)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars-experimental
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
updateChain(exstr = this.lastExstr, options = this.options) {
|
||||
if (this.options.length > 0) this.state = "normal"
|
||||
else this.state = "hidden"
|
||||
|
|
@ -57,16 +59,15 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
private async updateOptions(exstr = "") {
|
||||
this.lastExstr = exstr
|
||||
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
if (!excmds) return
|
||||
const fns = excmds.getFunctions()
|
||||
this.node.querySelector(".sectionHeader").textContent = "ex commands"
|
||||
|
||||
// Add all excmds that start with exstr and that tridactyl has metadata about to completions
|
||||
this.options = this.scoreOptions(
|
||||
fns
|
||||
.filter(([name, fn]) => !fn.hidden && name.startsWith(exstr))
|
||||
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([name]) => name.startsWith(exstr))
|
||||
.map(
|
||||
([name, fn]) => new ExcmdCompletionOption(name, getDoc(fn)),
|
||||
),
|
||||
)
|
||||
|
||||
// Also narrow down aliases map to possible completions
|
||||
|
|
@ -80,13 +81,13 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
|
||||
for (const alias of Object.keys(exaliases)) {
|
||||
const cmd = aliases.expandExstr(alias, exaliases)
|
||||
const fn = excmds.getFunction(cmd)
|
||||
const fn = excmdsFunctions[cmd]
|
||||
|
||||
if (fn) {
|
||||
this.options.push(
|
||||
new ExcmdCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${fn.doc}`,
|
||||
`Alias for \`${cmd}\`. ${getDoc(fn)}`,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -100,15 +101,35 @@ export class ExcmdCompletionSource extends Completions.CompletionSourceFuse {
|
|||
// Add partial matched funcs like: 'conf' ~= 'viewconfig'
|
||||
const seen = new Set(this.options.map(o => o.value))
|
||||
const partial_options = this.scoreOptions(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) =>
|
||||
!fn.hidden && name.includes(exstr) && !seen.has(name),
|
||||
)
|
||||
.map(([name, fn]) => new ExcmdCompletionOption(name, fn.doc)),
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([name]) => name.includes(exstr) && !seen.has(name))
|
||||
.map(
|
||||
([name, fn]) => new ExcmdCompletionOption(name, getDoc(fn)),
|
||||
),
|
||||
)
|
||||
this.options = this.options.concat(partial_options)
|
||||
|
||||
const [command] = exstr.trim().split(/\s+/)
|
||||
if (
|
||||
this.options.length === 0 &&
|
||||
command &&
|
||||
!excmdsFunctions[command] &&
|
||||
exaliasesConfig[command] === undefined
|
||||
) {
|
||||
const query = exstr.toLowerCase()
|
||||
this.node.querySelector(".sectionHeader").textContent = "ex commands (no matches, falling back to doc search)"
|
||||
this.options = this.scoreOptions(
|
||||
Object.entries(excmdsFunctions)
|
||||
.filter(([, fn]) =>
|
||||
getDoc(fn).toLowerCase().includes(query),
|
||||
)
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new ExcmdCompletionOption(name, getDoc(fn)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
this.options.forEach(o => (o.state = "normal"))
|
||||
return this.updateChain()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class ExtensionsCompletionOption extends Completions.CompletionOptionHTML
|
|||
|
||||
constructor(public name: string, public optionsUrl: string) {
|
||||
super()
|
||||
this.value = name
|
||||
this.fuseKeys.push(this.name)
|
||||
|
||||
this.html = html`<tr class="option">
|
||||
|
|
@ -60,12 +61,6 @@ export class ExtensionsCompletionSource extends Completions.CompletionSourceFuse
|
|||
return this.updateDisplay()
|
||||
}
|
||||
|
||||
select(option: ExtensionsCompletionOption) {
|
||||
this.completion = "extoptions " + option.name
|
||||
option.state = "focused"
|
||||
this.lastFocused = option
|
||||
}
|
||||
|
||||
private scoreOptions(options: ExtensionsCompletionOption[]) {
|
||||
return options.sort((o1, o2) => o1.name.localeCompare(o2.name))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,10 @@ export class FileSystemCompletionSource extends Completions.CompletionSourceFuse
|
|||
}
|
||||
|
||||
let [cmd, path] = this.splitOnPrefix(exstr)
|
||||
if (cmd === undefined) {
|
||||
if (
|
||||
cmd === undefined ||
|
||||
(cmd === "source" && /^--url(?:\s|$)/.test(path))
|
||||
) {
|
||||
this.state = "hidden"
|
||||
return
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,116 +1,171 @@
|
|||
import { activeTabId } from "@src/lib/webext"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
import * as Completions from "../completions"
|
||||
import * as config from "@src/lib/config"
|
||||
import * as Messaging from "@src/lib/messaging"
|
||||
import { ownTabId } from "@src/lib/webext"
|
||||
|
||||
class FindCompletionOption
|
||||
extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
constructor(m, reverse = false) {
|
||||
super()
|
||||
this.value =
|
||||
(reverse ? "-? " : "") + ("-: " + m.index) + " " + m.rangeData.text
|
||||
this.fuseKeys.push(m.rangeData.text)
|
||||
public fuseKeys
|
||||
|
||||
// Create HTMLElement
|
||||
constructor(index: number, match, args: string) {
|
||||
super()
|
||||
this.value = [`--jump-to ${index}`, args].filter(Boolean).join(" ")
|
||||
this.fuseKeys = [match.text, match.precontext, match.postcontext, match.breadcrumbs]
|
||||
this.html = html`<tr class="FindCompletionOption option">
|
||||
<td class="breadcrumbs">${match.breadcrumbs}</td>
|
||||
<td class="content">
|
||||
${m.precontext}<span class="match">${m.rangeData.text}</span
|
||||
>${m.postcontext}
|
||||
${match.precontext}<span class="match">${match.text}</span
|
||||
>${match.postcontext}
|
||||
</td>
|
||||
<td class="position">${match.position}</td>
|
||||
</tr>`
|
||||
}
|
||||
}
|
||||
|
||||
export class FindCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: FindCompletionOption[]
|
||||
public prevCompletion = null
|
||||
public completionCount = 0
|
||||
public options: FindCompletionOption[] = []
|
||||
private session = Math.random()
|
||||
private tabId = ownTabId()
|
||||
private active = false
|
||||
private request = 0
|
||||
private findArgs = ""
|
||||
private pending: Promise<void> = Promise.resolve()
|
||||
private selection: Promise<void> = Promise.resolve()
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["find "], "FindCompletionSource", "Matches")
|
||||
|
||||
this._parent.appendChild(this.node)
|
||||
}
|
||||
|
||||
async onInput(exstr) {
|
||||
const id = this.completionCount++
|
||||
// If there's already a promise being executed, wait for it to finish
|
||||
await this.prevCompletion
|
||||
// Since we might have awaited for this.prevCompletion, we don't have a guarantee we're the last completion the user asked for anymore
|
||||
if (id === this.completionCount - 1) {
|
||||
// If we are the last completion
|
||||
this.prevCompletion = this.updateOptions(exstr)
|
||||
await this.prevCompletion
|
||||
}
|
||||
}
|
||||
|
||||
// Overriding this function is important, the default one has a tendency to hide options when you don't expect it
|
||||
setStateFromScore() {
|
||||
this.options.forEach(o => (o.state = "normal"))
|
||||
}
|
||||
|
||||
private async updateOptions(exstr?: string) {
|
||||
if (!exstr) return
|
||||
|
||||
// Flag parsing because -? should reverse completions
|
||||
const tokens = exstr.split(" ")
|
||||
const flagpos = tokens.indexOf("-?")
|
||||
const reverse = flagpos >= 0
|
||||
if (reverse) {
|
||||
tokens.splice(flagpos, 1)
|
||||
}
|
||||
|
||||
const query = tokens.slice(1).join(" ")
|
||||
const minincsearchlen = await config.getAsync("minincsearchlen")
|
||||
// No point if continuing if the user hasn't started searching yet
|
||||
if (query.length < minincsearchlen) return
|
||||
|
||||
let findresults = await config.getAsync("findresults")
|
||||
const incsearch = (await config.getAsync("incsearch")) === "true"
|
||||
if (findresults === 0 && !incsearch) return
|
||||
|
||||
let incsearchonly = false
|
||||
if (findresults === 0) {
|
||||
findresults = 1
|
||||
incsearchonly = true
|
||||
}
|
||||
|
||||
// Note: the use of activeTabId here might break completions if the user starts searching for a pattern in a really big page and then switches to another tab.
|
||||
// Getting the tabId should probably be done in the constructor but you can't have async constructors.
|
||||
const tabId = await activeTabId()
|
||||
const findings = await Messaging.messageTab(
|
||||
tabId,
|
||||
"finding_content",
|
||||
"find",
|
||||
[query, findresults, reverse],
|
||||
constructor(_parent?) {
|
||||
super(
|
||||
["find"],
|
||||
"FindCompletionSource",
|
||||
html`<table><tr>
|
||||
<td class="breadcrumbs">Breadcrumbs</td>
|
||||
<td class="content">Context</td>
|
||||
<td class="position">Position</td>
|
||||
</tr></table>`,
|
||||
)
|
||||
}
|
||||
|
||||
// If the search was successful
|
||||
if (findings.length > 0) {
|
||||
// Get match context
|
||||
const len = await config.getAsync("findcontextlen")
|
||||
const matches = await Messaging.messageTab(
|
||||
tabId,
|
||||
"finding_content",
|
||||
"getMatches",
|
||||
[findings, len],
|
||||
)
|
||||
|
||||
if (incsearch)
|
||||
Messaging.messageTab(tabId, "finding_content", "jumpToMatch", [
|
||||
query,
|
||||
false,
|
||||
0,
|
||||
])
|
||||
|
||||
if (!incsearchonly) {
|
||||
this.options = matches.map(
|
||||
m => new FindCompletionOption(m, reverse),
|
||||
filter(exstr: string) {
|
||||
if (exstr === this.lastExstr && this.completion) return Promise.resolve()
|
||||
this.lastExstr = exstr
|
||||
const [, argstr] = this.splitOnPrefix(exstr)
|
||||
if (argstr === undefined) return this.cancel()
|
||||
const request = ++this.request
|
||||
this.findArgs = argstr.trim()
|
||||
this.active = true
|
||||
this.options = []
|
||||
this.optionContainer.replaceChildren()
|
||||
this.state = "hidden"
|
||||
const optionArgs = this.findArgs.split(/(?:^|\s)--(?:\s|$)/, 1)[0]
|
||||
if (/(^|\s)(--jump-to|-:)=?$/.test(optionArgs)) return this.cancel()
|
||||
const hasJump = /(^|\s)(--jump-to(?:=|\s)|-:(?:\s|$))/.test(optionArgs)
|
||||
const regex = /(^|\s)(-r|--regex)(?=\s|$)/.test(optionArgs)
|
||||
const delay = regex
|
||||
? new Promise<void>(resolve => window.setTimeout(resolve, 250))
|
||||
: Promise.resolve()
|
||||
this.pending = delay
|
||||
.then(() => {
|
||||
if (request !== this.request) return
|
||||
return this.send(
|
||||
{ session: this.session, completions: !hasJump },
|
||||
...this.findArgs.split(/\s+/),
|
||||
)
|
||||
this.updateChain(exstr, this.options)
|
||||
}
|
||||
})
|
||||
.then(matches => {
|
||||
if (request !== this.request) return
|
||||
this.options = hasJump
|
||||
? []
|
||||
: (matches || []).map(
|
||||
match =>
|
||||
new FindCompletionOption(
|
||||
match.index,
|
||||
match,
|
||||
this.findArgs,
|
||||
),
|
||||
)
|
||||
if (this.options.length) this.updateChain(exstr, this.options)
|
||||
else this.state = "hidden"
|
||||
this.resize()
|
||||
})
|
||||
.catch(() => {
|
||||
if (request !== this.request) return
|
||||
this.options = []
|
||||
this.state = "hidden"
|
||||
this.resize()
|
||||
return this.send({ session: this.session, cancel: true })
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
setStateFromScore() {
|
||||
this.options.forEach(option => (option.state = "normal"))
|
||||
this.deselect()
|
||||
}
|
||||
|
||||
scoredOptions() {
|
||||
return []
|
||||
}
|
||||
|
||||
updateDisplay() {
|
||||
this.optionContainer.replaceChildren(
|
||||
...this.options
|
||||
.filter(option => option.state !== "hidden")
|
||||
.map(option => option.html),
|
||||
)
|
||||
this.next(0)
|
||||
}
|
||||
|
||||
select(option: FindCompletionOption) {
|
||||
super.select(option)
|
||||
this.selection = this.preview(option.value, true)
|
||||
}
|
||||
|
||||
async next(inc = 1) {
|
||||
if (!this.active) return false
|
||||
const pending = this.pending
|
||||
if (inc !== 0) await this.pending
|
||||
if (!this.active || pending !== this.pending) return false
|
||||
const moved = await super.next(inc)
|
||||
if (inc !== 0 && moved) {
|
||||
if (!this.completion) this.selection = this.preview(this.findArgs)
|
||||
await this.selection
|
||||
}
|
||||
return moved
|
||||
}
|
||||
|
||||
destroy() {
|
||||
return this.cancel()
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
if (!this.active) return Promise.resolve()
|
||||
++this.request
|
||||
this.active = false
|
||||
this.options = []
|
||||
this.state = "hidden"
|
||||
return this.send({ session: this.session, cancel: true }).catch(
|
||||
() => undefined,
|
||||
)
|
||||
}
|
||||
|
||||
private preview(args: string, selected = false) {
|
||||
return this.send(
|
||||
{ session: this.session, completions: false, selected },
|
||||
...args.split(/\s+/),
|
||||
)
|
||||
.then(() => undefined)
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
private send(preview, ...args: string[]) {
|
||||
return this.tabId.then(tabId =>
|
||||
Messaging.messageTab(tabId, "excmd_content", "find", [
|
||||
preview,
|
||||
...args,
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
private resize() {
|
||||
window.dispatchEvent(new Event("tridactyl-refresh-completions"))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
31
src/completions/Glossary.ts
Normal file
31
src/completions/Glossary.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import { ExcmdCompletionOption } from "@src/completions/Excmd"
|
||||
import glossary from "@src/.glossary.generated.json"
|
||||
|
||||
export function glossaryOptions(createOption, query: string, prefix: boolean) {
|
||||
const needle = query.toLowerCase()
|
||||
return glossary
|
||||
.filter(entry =>
|
||||
prefix
|
||||
? entry.word.toLowerCase().startsWith(needle)
|
||||
: (entry.word + entry.definition)
|
||||
.toLowerCase()
|
||||
.includes(needle),
|
||||
)
|
||||
.map(entry =>
|
||||
createOption(entry.word, `Glossary. ${entry.definition}`, "-g"),
|
||||
)
|
||||
}
|
||||
|
||||
export class GlossaryCompletionSource extends Completions.CompletionSourceFuse {
|
||||
public options: ExcmdCompletionOption[]
|
||||
|
||||
constructor(parent) {
|
||||
super(["define"], "GlossaryCompletionSource", "Glossary")
|
||||
this.options = glossary.map(
|
||||
entry => new ExcmdCompletionOption(entry.word, entry.definition),
|
||||
)
|
||||
this.sortScoredOptions = true
|
||||
parent.appendChild(this.node)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ class GotoCompletionOption
|
|||
this.fuseKeys.push(title)
|
||||
|
||||
this.html = html`<tr class="GotoCompletionOption option">
|
||||
<td class="title" style="padding-left: ${level * 4}ch">${title}</td>
|
||||
<td class="title">${Completions.treePrefix(level)}${title}</td>
|
||||
</tr>`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,23 @@
|
|||
import * as Completions from "@src/completions"
|
||||
import * as Metadata from "@src/.metadata.generated"
|
||||
import { AproposCompletionSource } from "@src/completions/Apropos"
|
||||
import {
|
||||
excmdsFunctions,
|
||||
defaultConfigMembers,
|
||||
getDoc,
|
||||
memberDoc,
|
||||
} from "@src/.metadata.generated"
|
||||
import * as aliases from "@src/lib/aliases"
|
||||
import * as config from "@src/lib/config"
|
||||
import { glossaryOptions } from "@src/completions/Glossary"
|
||||
|
||||
class HelpCompletionOption extends Completions.CompletionOptionHTML
|
||||
implements Completions.CompletionOptionFuse {
|
||||
class HelpCompletionOption extends Completions.CompletionOptionHTML implements Completions.CompletionOptionFuse {
|
||||
public fuseKeys = []
|
||||
|
||||
constructor(public name: string, doc: string, flag: string) {
|
||||
constructor(
|
||||
public name: string,
|
||||
doc: string,
|
||||
flag: string,
|
||||
) {
|
||||
super()
|
||||
this.value = `${flag} ${name}`
|
||||
this.html = html`<tr class="HelpCompletionOption option">
|
||||
|
|
@ -17,13 +27,15 @@ class HelpCompletionOption extends Completions.CompletionOptionHTML
|
|||
}
|
||||
}
|
||||
|
||||
export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
||||
export class HelpCompletionSource extends AproposCompletionSource {
|
||||
public options: HelpCompletionOption[]
|
||||
|
||||
constructor(private _parent) {
|
||||
super(["help"], "HelpCompletionSource", "Help")
|
||||
constructor(_parent) {
|
||||
super(_parent, ["help"], "HelpCompletionSource", "Help")
|
||||
}
|
||||
|
||||
this._parent.appendChild(this.node)
|
||||
protected createOption(name: string, doc: string, flag: string) {
|
||||
return new HelpCompletionOption(name, doc, flag)
|
||||
}
|
||||
|
||||
public async filter(exstr: string) {
|
||||
|
|
@ -41,21 +53,15 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
this.state = "hidden"
|
||||
return
|
||||
}
|
||||
this.node.querySelector(".sectionHeader").textContent = "Help (prefix matches)"
|
||||
|
||||
const file = Metadata.everything.getFile("src/lib/config.ts")
|
||||
const default_config = file.getClass("default_config")
|
||||
const excmds = Metadata.everything.getFile("src/excmds.ts")
|
||||
const fns = excmds.getFunctions()
|
||||
const settings = config.get()
|
||||
const exaliases = settings.exaliases
|
||||
const bindings = settings.nmaps
|
||||
if (
|
||||
fns === undefined ||
|
||||
exaliases === undefined ||
|
||||
bindings === undefined
|
||||
) {
|
||||
if (exaliases === undefined || bindings === undefined) {
|
||||
return
|
||||
}
|
||||
const fns = Object.entries(excmdsFunctions)
|
||||
|
||||
const flags = {
|
||||
"-a": (options, query) =>
|
||||
|
|
@ -64,9 +70,7 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
.filter(alias => alias.startsWith(query))
|
||||
.map(alias => {
|
||||
const cmd = aliases.expandExstr(alias)
|
||||
const doc =
|
||||
(excmds.getFunction(cmd) || ({} as any)).doc ||
|
||||
""
|
||||
const doc = getDoc(excmdsFunctions[cmd])
|
||||
return new HelpCompletionOption(
|
||||
alias,
|
||||
`Alias for \`${cmd}\`. ${doc}`,
|
||||
|
|
@ -90,15 +94,12 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
"-e": (options, query) =>
|
||||
options.concat(
|
||||
fns
|
||||
.filter(
|
||||
([name, fn]) =>
|
||||
!fn.hidden && name.startsWith(query),
|
||||
)
|
||||
.filter(([name]) => name.startsWith(query))
|
||||
.map(
|
||||
([name, fn]) =>
|
||||
new HelpCompletionOption(
|
||||
name,
|
||||
`Excmd. ${fn.doc}`,
|
||||
`Excmd. ${getDoc(fn)}`,
|
||||
"-e",
|
||||
),
|
||||
),
|
||||
|
|
@ -108,11 +109,7 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
Object.keys(settings)
|
||||
.filter(x => x.startsWith(query))
|
||||
.map(setting => {
|
||||
const member = default_config.getMember(setting)
|
||||
let doc = ""
|
||||
if (member !== undefined) {
|
||||
doc = member.doc
|
||||
}
|
||||
const doc = memberDoc(defaultConfigMembers[setting])
|
||||
return new HelpCompletionOption(
|
||||
setting,
|
||||
`Setting. ${doc}`,
|
||||
|
|
@ -120,6 +117,10 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
)
|
||||
}),
|
||||
),
|
||||
"-g": (options, query) =>
|
||||
options.concat(
|
||||
glossaryOptions(this.createOption.bind(this), query, true),
|
||||
),
|
||||
}
|
||||
|
||||
const args = query.split(" ")
|
||||
|
|
@ -133,18 +134,15 @@ export class HelpCompletionSource extends Completions.CompletionSourceFuse {
|
|||
)
|
||||
}
|
||||
|
||||
if (opts.length === 0) {
|
||||
this.node.querySelector(".sectionHeader").textContent = "Help (prefix match failed, showing :apropos matches)"
|
||||
return super.filter(exstr)
|
||||
}
|
||||
|
||||
this.options = opts
|
||||
this.options.sort((compopt1, compopt2) =>
|
||||
compopt1.name.localeCompare(compopt2.name),
|
||||
)
|
||||
return this.updateChain()
|
||||
}
|
||||
|
||||
updateChain() {
|
||||
// Options are pre-trimmed to the right length.
|
||||
this.options.forEach(option => (option.state = "normal"))
|
||||
|
||||
// Call concrete class
|
||||
return this.updateDisplay()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ class HistoryCompletionOption
|
|||
let preplain = page.bmark ? "B" : ""
|
||||
preplain += page.search ? "S" : ""
|
||||
let pre = preplain
|
||||
if (config.get("completions", "Tab", "statusstylepretty") === "true") {
|
||||
// Tab settings apply here to grandfather in people who set it before when there was a bug
|
||||
if ((config.get("completions", "Tab", "statusstylepretty") === "true") || (config.get("completions", "History", "statusstylepretty") === "true")) {
|
||||
pre = page.bmark ? "\u2B50" : ""
|
||||
pre += page.search ? "\u{1F50D}" : ""
|
||||
}
|
||||
|
|
@ -33,7 +34,7 @@ class HistoryCompletionOption
|
|||
<td class="title">${page.title}</td>
|
||||
<td class="content">
|
||||
${page.search ? "Search " : ""}
|
||||
<a class="url" target="_blank" href=${page.url}>${page.url}</a>
|
||||
<a class="url" target="_blank" href=${page.url}>${Completions.decodeUrlForDisplay(page.url)}</a>
|
||||
</td>
|
||||
</tr>`
|
||||
}
|
||||
|
|
@ -71,6 +72,7 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
|
|||
}
|
||||
|
||||
const headerPostfix = []
|
||||
prefix = this.canonicalisePrefix(prefix)
|
||||
|
||||
// Ignoring command-specific arguments
|
||||
// It's terrible but it's ok because it's just a stopgap until an actual commandline-parsing API is implemented
|
||||
|
|
@ -138,6 +140,13 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
|
|||
break
|
||||
}
|
||||
}
|
||||
if (
|
||||
this.completion === undefined &&
|
||||
this.options.length > 0 &&
|
||||
config.get("completions", "History", "autoselect") === "true"
|
||||
) {
|
||||
this.select(this.options[0])
|
||||
}
|
||||
|
||||
return this.updateDisplay()
|
||||
}
|
||||
|
|
@ -147,7 +156,10 @@ export class HistoryCompletionSource extends Completions.CompletionSourceFuse {
|
|||
updateChain() {}
|
||||
|
||||
private async scoreOptions(query: string, n: number) {
|
||||
if (!query || config.get("historyresults") === 0) {
|
||||
if (
|
||||
(!query && config.get("historysort") === "frequency") ||
|
||||
config.get("historyresults") === 0
|
||||
) {
|
||||
return (await providers.getTopSites()).slice(0, n)
|
||||
} else {
|
||||
return (await providers.getCombinedHistoryBmarks(query)).slice(0, n)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue