Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d44d331a78 | ||
|
|
595554f18a | ||
|
|
84d3c3f7eb | ||
|
|
890f55c9dc | ||
|
|
575a14e1f3 | ||
|
|
91074bebd6 | ||
|
|
3459a16b48 | ||
|
|
88c9b6849f | ||
|
|
d87bb25baf | ||
|
|
34cb796505 | ||
|
|
1eb34e0662 | ||
|
|
01d631033f | ||
|
|
84b2c07340 | ||
|
|
a86eeb636d | ||
|
|
417a5a8214 | ||
|
|
bcf40dde8c | ||
|
|
cad87484c7 | ||
|
|
b8ad641ed6 | ||
|
|
ae8197be8b | ||
|
|
eafb0274a7 | ||
|
|
71a23a4e02 | ||
|
|
26fd90dfa3 | ||
|
|
e393ebede2 | ||
|
|
2981bb3f19 | ||
|
|
6e9f05abb1 | ||
|
|
89bdc8ec75 | ||
|
|
ae25ade318 | ||
|
|
dd5e46a8a7 | ||
|
|
f7218aaa9e | ||
|
|
ee79bcfc44 | ||
|
|
2c2c174874 | ||
|
|
f57c49ce3a | ||
|
|
06121acfac | ||
|
|
2078ce7446 | ||
|
|
49f41abfdb | ||
|
|
70fcfe6d6c | ||
|
|
b060b4b9bf | ||
|
|
ee2135bfb3 | ||
|
|
7daa322441 | ||
|
|
74252cb66b | ||
|
|
88d3bba300 | ||
|
|
f5ee6f3537 | ||
|
|
afc77e7adc | ||
|
|
024f42fce6 | ||
|
|
8a5f12b97c | ||
|
|
954b21cf39 | ||
|
|
74d095774d | ||
|
|
17a2722e6d | ||
|
|
c843bddbfe | ||
|
|
3f22a49755 | ||
|
|
7af2ffcebf | ||
|
|
de90c959e0 | ||
|
|
9987dc1eb4 | ||
|
|
3efd575dd2 | ||
|
|
f4c7b17a87 | ||
|
|
16d80718cb | ||
|
|
ad228d53b7 | ||
|
|
15ee1e531b | ||
|
|
1c8fb3392a | ||
|
|
8647866a32 | ||
|
|
23351c4f1c | ||
|
|
1367c304cf | ||
|
|
26d6bcb074 | ||
|
|
b0d651ece1 | ||
|
|
b6d50ba6a4 | ||
|
|
b3ab6a9166 | ||
|
|
f39a5ac9c2 | ||
|
|
38a9a9240d | ||
|
|
241b22a465 | ||
|
|
741abc0366 | ||
|
|
7854775de5 | ||
|
|
e62eaa6b4b | ||
|
|
b4cce23ef4 | ||
|
|
2bcaf90fc8 | ||
|
|
96ffbe2f84 | ||
|
|
6f5acee1c3 | ||
|
|
54e491d8bf | ||
|
|
ab6374e278 | ||
|
|
2fda4c9f67 | ||
|
|
5138a45b0f | ||
|
|
b224d4fa8a | ||
|
|
a552e44483 | ||
|
|
0cf3bba118 | ||
|
|
2c48ea3508 | ||
|
|
b9b6212b75 | ||
|
|
b978aaec21 | ||
|
|
af704681d9 | ||
|
|
1443ddfe6c | ||
|
|
54457a3e1b | ||
|
|
bf180e6a2c | ||
|
|
864a5820c9 | ||
|
|
4d3ca49c3f | ||
|
|
c49c3cf7f0 | ||
|
|
5d5ab57469 | ||
|
|
31978d8de0 | ||
|
|
e8eb68bf24 | ||
|
|
9ea08f4fed | ||
|
|
fe078a5c5b | ||
|
|
61933954f3 | ||
|
|
4c243638cb | ||
|
|
02ba04b5d8 | ||
|
|
4f158a4829 | ||
|
|
177a22df59 | ||
|
|
6b0ca2966e | ||
|
|
aadfaf7150 | ||
|
|
b307b9a66b | ||
|
|
6c1ab6002d | ||
|
|
9638eefc91 | ||
|
|
9e9c4ad587 | ||
|
|
ce231431b9 | ||
|
|
06e1e14e02 | ||
|
|
416e7884f5 | ||
|
|
d579222007 | ||
|
|
30243c84cd | ||
|
|
3557a77645 | ||
|
|
97be28638b | ||
|
|
aba0826c38 | ||
|
|
f032228d0e | ||
|
|
6cf174c5ed | ||
|
|
c2109d245f | ||
|
|
6a9745171e | ||
|
|
f9a68e8b23 | ||
|
|
6e391df5ee | ||
|
|
f5edca94d3 | ||
|
|
60046abec3 | ||
|
|
cafc2060b8 | ||
|
|
b1f45752cf | ||
|
|
ed17551170 | ||
|
|
ef5adab638 | ||
|
|
fb653ff99d | ||
|
|
78fc47a9c4 | ||
|
|
2a124cea61 | ||
|
|
4c7cc563dc | ||
|
|
6114af4f93 | ||
|
|
8c31629655 | ||
|
|
03c8a8edb2 | ||
|
|
3eeaee154f | ||
|
|
8cf8fa7c80 | ||
|
|
6b4f6fc71e | ||
|
|
30c2680b6f | ||
|
|
fb7b1800cc | ||
|
|
ff573bf377 | ||
|
|
0a33bb861e | ||
|
|
728756289b | ||
|
|
56ccd3a0ac | ||
|
|
66f3f0ba07 | ||
|
|
af5e0d589e | ||
|
|
533dc99e7d | ||
|
|
fc5ca965ba | ||
|
|
9c4a46bcdb | ||
|
|
52658886e7 | ||
|
|
8174ab7616 | ||
|
|
2b6acedae1 | ||
|
|
d00fe9c5f4 | ||
|
|
88aa270728 | ||
|
|
4ae409c7f4 | ||
|
|
9a29c9abdd | ||
|
|
66d93ea037 | ||
|
|
6c0066dbfb | ||
|
|
4fde644733 | ||
|
|
e7841c60df | ||
|
|
94f647b24a | ||
|
|
630249d22a | ||
|
|
db99b4cb54 | ||
|
|
c77db23586 | ||
|
|
daf66bcec4 | ||
|
|
8caf36349f | ||
|
|
6934de58e5 | ||
|
|
54a5007c01 | ||
|
|
e25a455698 | ||
|
|
ab429dfeb6 | ||
|
|
c5289dc0e8 | ||
|
|
d191877002 | ||
|
|
4d979160c2 | ||
|
|
d00e8f6e19 | ||
|
|
91b77e5237 | ||
|
|
5b8c246d53 | ||
|
|
b374b9b91c | ||
|
|
403717117e | ||
|
|
027295d995 | ||
|
|
c9b7eccbc1 | ||
|
|
2b6d9348cd | ||
|
|
692f8c8454 | ||
|
|
6783355c4d | ||
|
|
fb9cca1e99 | ||
|
|
eb770ede1a | ||
|
|
2643e853af | ||
|
|
b79456e91b | ||
|
|
ac66c086f8 | ||
|
|
ebccf401dd | ||
|
|
66494845b7 | ||
|
|
e6c36ad602 | ||
|
|
26379182db | ||
|
|
bba03d14d4 | ||
|
|
23b728a762 | ||
|
|
819c5207d2 | ||
|
|
311358544e | ||
|
|
4480ecc96d | ||
|
|
6c5f70a205 | ||
|
|
99adfb4a9e | ||
|
|
7909cf4067 | ||
|
|
780ab20aeb | ||
|
|
73119bb7c5 | ||
|
|
f20f0f064e | ||
|
|
1b44ae098c | ||
|
|
453c230716 | ||
|
|
439d885ee1 | ||
|
|
43dee3ef76 | ||
|
|
c71ba6ff8d | ||
|
|
fb7a36eb73 | ||
|
|
e7f294a065 | ||
|
|
d5037f180e | ||
|
|
e90158809a | ||
|
|
0cb802ed9a | ||
|
|
d0b47204f4 | ||
|
|
351cb391e5 | ||
|
|
051be927cd | ||
|
|
8bad2c2113 | ||
|
|
2bcf6fb3eb | ||
|
|
d1ba0ed2b2 | ||
|
|
6e421e60c5 | ||
|
|
8385050804 | ||
|
|
bfe4f08232 | ||
|
|
132f914b0d | ||
|
|
97d82b03ab | ||
|
|
f06fa3f9b7 | ||
|
|
6337788a22 | ||
|
|
024db4358b | ||
|
|
173f35487e | ||
|
|
74cbe82dd1 | ||
|
|
a8c30d30a9 | ||
|
|
1b7a52d5af | ||
|
|
e5109789bf | ||
|
|
4d2b35e09d | ||
|
|
5c5177ec57 | ||
|
|
d93b00cd15 | ||
|
|
39ade59174 | ||
|
|
0309c815b9 | ||
|
|
ce613098db | ||
|
|
ab34044196 | ||
|
|
17f724748f | ||
|
|
f3a917b5e7 | ||
|
|
376011ea08 | ||
|
|
2ce7624c14 | ||
|
|
c62dddd5b9 | ||
|
|
490ba9f140 | ||
|
|
f76890cc56 | ||
|
|
e1c8f1fed9 | ||
|
|
6e19a4ab8b | ||
|
|
b156523a7f | ||
|
|
d20b745ae5 | ||
|
|
c51e9e6b2c | ||
|
|
1a31fa9067 | ||
|
|
558b8499af | ||
|
|
986c162988 | ||
|
|
770d5cd11c | ||
|
|
446d2ab3af | ||
|
|
2f9bf7f063 | ||
|
|
72ff9c0964 | ||
|
|
33b1a11d85 | ||
|
|
06dabf1e4e | ||
|
|
28d3691e0b | ||
|
|
ffa21fbfd2 | ||
|
|
db028665fd | ||
|
|
6bc83d9f27 | ||
|
|
790d832155 | ||
|
|
f477cecdeb | ||
|
|
e031389021 | ||
|
|
e00f826eb8 | ||
|
|
24f4e8ab99 | ||
|
|
1550d528bd | ||
|
|
40c041031e | ||
|
|
3e555ec9f1 | ||
|
|
5098f14aab | ||
|
|
a355a9c85e | ||
|
|
e7db2a8573 | ||
|
|
9b18bd0b48 | ||
|
|
f95ddd594e | ||
|
|
fe8168784f | ||
|
|
4046f143f6 | ||
|
|
e4e16999c8 | ||
|
|
10f3ba4ff4 | ||
|
|
3cd2be5081 | ||
|
|
c9359978f9 | ||
|
|
781c67b31a | ||
|
|
020bd129fb | ||
|
|
8086b6d78c | ||
|
|
48e14b36b8 | ||
|
|
b3c1c56579 | ||
|
|
bd0e932910 | ||
|
|
525e5f88ae | ||
|
|
005eeb0e0b | ||
|
|
d21bb015e8 | ||
|
|
7338838b0e | ||
|
|
8bb4803ff9 | ||
|
|
892b919cf3 | ||
|
|
572d81ecef | ||
|
|
a4ce7a4037 | ||
|
|
6eafb15cf9 | ||
|
|
e19fe5ce1c | ||
|
|
9d2017e598 | ||
|
|
f33c419ed9 | ||
|
|
f425fbaf9d | ||
|
|
bcc5ec897a | ||
|
|
f5f2fe3472 | ||
|
|
b7c3863882 | ||
|
|
d759f48ee8 | ||
|
|
3a37075e71 | ||
|
|
58366c0b87 | ||
|
|
2667ed13f1 | ||
|
|
34daffbc96 | ||
|
|
be81cd72af | ||
|
|
4ae00714d2 | ||
|
|
f24cbb6692 | ||
|
|
5a35c5b928 | ||
|
|
1880da6351 | ||
|
|
df93cb103c | ||
|
|
63b474a32c | ||
|
|
abdbf158d1 | ||
|
|
ee68d80d0a | ||
|
|
c8e6f3a627 | ||
|
|
dc941575fe | ||
|
|
e64103e5f2 | ||
|
|
f0ab03a9fb | ||
|
|
09965f1cc6 | ||
|
|
d2852bb34a | ||
|
|
b03c52a501 | ||
|
|
fd6633f536 | ||
|
|
320ac81f48 | ||
|
|
70bcd2428f | ||
|
|
71f5ada0a3 | ||
|
|
aab5141404 | ||
|
|
ff6b119f27 | ||
|
|
7ef4b7eeb8 | ||
|
|
4668be6e24 | ||
|
|
a211fc7c97 | ||
|
|
54af0b675d | ||
|
|
8c8171e774 | ||
|
|
0bb1790206 | ||
|
|
45fc84d8be | ||
|
|
f7500f4cad | ||
|
|
0cfd718d8a | ||
|
|
5f486d0f51 | ||
|
|
6e5a2f85a1 | ||
|
|
e66a89208d | ||
|
|
7f65c501c6 | ||
|
|
22c2382765 | ||
|
|
38e1a4febf | ||
|
|
8db554b377 | ||
|
|
5f5cc55331 | ||
|
|
2f8b39186f | ||
|
|
a3a724e2e6 | ||
|
|
73235c8699 | ||
|
|
7e5999e862 | ||
|
|
afa244dcb0 | ||
|
|
4ea5bb2390 | ||
|
|
e545d552f6 | ||
|
|
56fe7b3596 | ||
|
|
eb28ec866a | ||
|
|
4649454282 | ||
|
|
a45dc6efda | ||
|
|
9e56b9fbb5 | ||
|
|
5504994cb9 | ||
|
|
acff6d0432 | ||
|
|
eea9cb7c5b | ||
|
|
c0ddddfb1f | ||
|
|
f7dab3ca56 | ||
|
|
e11b4038a3 | ||
|
|
b635e64803 | ||
|
|
20a194b49a | ||
|
|
33a5fb8837 | ||
|
|
59dae640db | ||
|
|
a6ac75e97b | ||
|
|
cc5b33a8ec | ||
|
|
6e1a627b84 | ||
|
|
62d4c65e1c | ||
|
|
600134a3ac | ||
|
|
df14af7337 | ||
|
|
2f740b570d | ||
|
|
294d980b52 | ||
|
|
c9d9909d74 | ||
|
|
90eeabae7b | ||
|
|
a32c77c5f1 | ||
|
|
910846f2ce | ||
|
|
35d0438261 | ||
|
|
515af472ce | ||
|
|
f062f7f9fe | ||
|
|
f68378041f | ||
|
|
30ef3057ac | ||
|
|
48f3a934c9 | ||
|
|
38d1fde84f | ||
|
|
dd78670a4b | ||
|
|
b8a8190a43 | ||
|
|
2d0989342f | ||
|
|
15ff9b06a1 | ||
|
|
41b19f69de | ||
|
|
f2bd6f1fce | ||
|
|
ec94218a4b | ||
|
|
c2e5c7cf38 | ||
|
|
bac5734527 | ||
|
|
bef4033d94 | ||
|
|
64216cba67 | ||
|
|
a9d6167a9f | ||
|
|
6c6b40548f | ||
|
|
a916d88e85 | ||
|
|
ab2f0548a3 | ||
|
|
e30ba07285 | ||
|
|
1b336d973d | ||
|
|
7b1866737f | ||
|
|
eedc4ab648 | ||
|
|
5e429c7a94 | ||
|
|
57d23eb043 | ||
|
|
5ebd9be89a | ||
|
|
5a743779e2 | ||
|
|
0495062110 | ||
|
|
d522191f69 | ||
|
|
bbbdc8b7a6 | ||
|
|
c00ed8aa5a | ||
|
|
0beaa94068 | ||
|
|
96e76665d6 | ||
|
|
6423a29600 | ||
|
|
8bddf30dcf | ||
|
|
a6b2db182d | ||
|
|
b9f3f9d859 | ||
|
|
6331447dcd | ||
|
|
4213ee660f | ||
|
|
3bd9287c5d | ||
|
|
b23d3f7c8b | ||
|
|
f8d9e5e027 | ||
|
|
8fa7fb3b1f | ||
|
|
63d92a0872 | ||
|
|
509d83365e | ||
|
|
d6ab73c905 | ||
|
|
5a93ba05d5 | ||
|
|
2d2fdf0b1e | ||
|
|
cf96622261 | ||
|
|
47c8ccb0e5 | ||
|
|
02fdb8778b | ||
|
|
6bed7f0e66 | ||
|
|
7597d335b9 | ||
|
|
f32c5ba244 | ||
|
|
ab53ab21e2 | ||
|
|
acfe5dbb49 | ||
|
|
f9b91fa189 | ||
|
|
2462e68ba1 | ||
|
|
019b2a1681 | ||
|
|
e94a07b677 | ||
|
|
c058c29755 | ||
|
|
9e7c7ac163 | ||
|
|
fcee41f00d | ||
|
|
ff2c1bf8ed | ||
|
|
a2cf26e7ed | ||
|
|
71e6900375 | ||
|
|
3bdc98bd12 | ||
|
|
68d2a6e951 | ||
|
|
acf47ac947 | ||
|
|
fda24e5f5a | ||
|
|
05eca8e4d8 | ||
|
|
b915f3b1a9 | ||
|
|
ab58968f4d | ||
|
|
820d1e7570 | ||
|
|
a120c8cf98 | ||
|
|
5a35ea116f | ||
|
|
e72ad1f030 | ||
|
|
a17362437a | ||
|
|
8643dc43b1 | ||
|
|
e1c869a358 | ||
|
|
2f5b8d9abe | ||
|
|
db324f54eb | ||
|
|
3242efb1a2 | ||
|
|
30f9f1a021 | ||
|
|
3f58d05aa7 | ||
|
|
c611a51575 | ||
|
|
73458dcd28 | ||
|
|
23ebeb1cc0 | ||
|
|
a7ba9f1886 | ||
|
|
d5192e2244 | ||
|
|
7eb595170f | ||
|
|
bd576ca808 | ||
|
|
2f8146b11f | ||
|
|
7f321c89cb | ||
|
|
fa65f606b8 | ||
|
|
7a3285adaf | ||
|
|
8bf78fef10 | ||
|
|
78f97ce4df | ||
|
|
66ccbf70f8 | ||
|
|
cb48600b40 | ||
|
|
33ce235713 | ||
|
|
f1a68ece01 | ||
|
|
dd563360af | ||
|
|
7f6dac4271 | ||
|
|
178fd8e828 | ||
|
|
1641e02a7d | ||
|
|
cfd04e9bb4 | ||
|
|
38effaf740 | ||
|
|
a68eb4a73e | ||
|
|
c02990ef98 | ||
|
|
5aa7d5ffe9 | ||
|
|
4942b0fca5 | ||
|
|
aed1bdff5a | ||
|
|
929c840006 | ||
|
|
57aef1d3c2 | ||
|
|
c1734a094c | ||
|
|
99c46685ac | ||
|
|
066b08040a | ||
|
|
35de2334fb | ||
|
|
5564c966a5 | ||
|
|
0891b5e7b7 | ||
|
|
f3341f201f | ||
|
|
bf0dafabe2 | ||
|
|
60f6587169 | ||
|
|
a9bf32919e | ||
|
|
7633f9f88b | ||
|
|
9b56233938 | ||
|
|
65074264b8 | ||
|
|
4f6fceb87f | ||
|
|
659fdd1d37 | ||
|
|
8bce2ce040 | ||
|
|
55cc83a5d4 | ||
|
|
6a51e93ded | ||
|
|
fcf570e96e | ||
|
|
cbc184e953 | ||
|
|
98f9063352 | ||
|
|
c278f1af00 | ||
|
|
fbb17a0ba5 | ||
|
|
8637a9823e | ||
|
|
d717fe7e8c | ||
|
|
5e8dfe7267 | ||
|
|
aaa4fbcdbd | ||
|
|
150f0cf486 | ||
|
|
711b220a05 | ||
|
|
b615c485f7 | ||
|
|
2f3b6b29ae | ||
|
|
7aecd83c4a | ||
|
|
3b460ab91f | ||
|
|
661c0eb970 | ||
|
|
f4a1f1fdc8 | ||
|
|
b5432c3728 | ||
|
|
6d0625c409 | ||
|
|
2f1ea08b8a | ||
|
|
a092da1943 | ||
|
|
43ef3cc562 | ||
|
|
047774475c | ||
|
|
91b8504df5 | ||
|
|
a4fb2dfcf8 | ||
|
|
ba16ec02e5 | ||
|
|
55b7af2623 | ||
|
|
b428eff10e | ||
|
|
35aee15b6d | ||
|
|
6f53c1bfde | ||
|
|
db0230ed75 | ||
|
|
7fa3c1e12a | ||
|
|
3b5993652f | ||
|
|
be4ad8947f | ||
|
|
2ead15e78e | ||
|
|
aa0740ff94 | ||
|
|
6d3f837820 | ||
|
|
359b82c29c | ||
|
|
b54171bc2c | ||
|
|
936b2131e0 | ||
|
|
aaef6a9e9c | ||
|
|
9327006e61 | ||
|
|
5225c841ae | ||
|
|
8464e319fd | ||
|
|
98b8bd90ea | ||
|
|
ae94993b09 | ||
|
|
72f10aaed1 | ||
|
|
4a74bae8c7 | ||
|
|
5164aec37b | ||
|
|
1581e9b1cd | ||
|
|
536e3ffb11 | ||
|
|
d6103191ba | ||
|
|
49e37a19a5 | ||
|
|
d02431e260 | ||
|
|
f24f6ead92 | ||
|
|
b7bdb7ae50 | ||
|
|
2dcf8ac96f | ||
|
|
5421c94853 | ||
|
|
10f6bc092a | ||
|
|
be9ea4ea8e | ||
|
|
f5c6fa842a | ||
|
|
e0943ce905 | ||
|
|
61e7df77a7 | ||
|
|
a5434360bc | ||
|
|
ba29c66e3b | ||
|
|
b3a72d55ae | ||
|
|
c382ad1cc8 | ||
|
|
363e28f323 | ||
|
|
d695656b8c | ||
|
|
31c6cbc0a2 | ||
|
|
b93284bc2f | ||
|
|
99c855b01b | ||
|
|
c2eaedc959 | ||
|
|
a631cd67ec | ||
|
|
66fccd3c68 | ||
|
|
9a4ebf4daa | ||
|
|
91c14211c6 | ||
|
|
1ea07c458b | ||
|
|
591add6e0c | ||
|
|
b99fff66df | ||
|
|
f54ba05b00 | ||
|
|
6596fb00c7 | ||
|
|
df0b5a80dc | ||
|
|
8cdbb31dbe | ||
|
|
0be4b1222d | ||
|
|
18a6bfd73a | ||
|
|
5e2271b237 | ||
|
|
798999d490 | ||
|
|
0e68c5e8bc | ||
|
|
9694af82f4 | ||
|
|
28a4386975 | ||
|
|
a238f7beba | ||
|
|
75ba16281b | ||
|
|
ad65a278d4 | ||
|
|
a2320b3f8d | ||
|
|
ae12853ad0 | ||
|
|
68adf6dc2f | ||
|
|
f88989bd4b | ||
|
|
77ef3847ce | ||
|
|
b8291837fc | ||
|
|
423a7f951a | ||
|
|
557f5fe364 | ||
|
|
5308970ad8 | ||
|
|
6b368953f4 | ||
|
|
2d0b63c29d | ||
|
|
34e941c8cb | ||
|
|
76cf007fff | ||
|
|
321963be83 | ||
|
|
c733bda6c3 | ||
|
|
e67b50b976 | ||
|
|
9e3c080909 | ||
|
|
cb661aaebd | ||
|
|
573571978c | ||
|
|
dc492a2a0a | ||
|
|
e47747dd0e | ||
|
|
4de944b41e | ||
|
|
5f7a1fa5cd | ||
|
|
b8ad328cde | ||
|
|
3076fd19c1 | ||
|
|
fac437b8c1 | ||
|
|
697c25015e | ||
|
|
d83b404fc3 | ||
|
|
ab7313cbc4 | ||
|
|
1b8d70322b | ||
|
|
844ffab4ed | ||
|
|
f5d0513d1f | ||
|
|
557abd4285 |
@@ -5,3 +5,9 @@ charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Information**
|
||||
|
||||
- OS: [e.g. macOS]
|
||||
- Clash Nyanpasu Version: [e.g. 1.3.4]
|
||||
- Clash Core: [e.g. Clash or Clash Meta]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Alpha CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
debug:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-20.04, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Delete current release assets
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: alpha
|
||||
fail-if-no-assets: false
|
||||
fail-if-no-release: false
|
||||
assets: |
|
||||
*.zip
|
||||
*.gz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: alpha
|
||||
releaseName: "Clash Verge Alpha"
|
||||
releaseBody: "Alpha Version (include debug)"
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
includeDebug: ${{ github.event.inputs.debug }}
|
||||
|
||||
- name: Portable Bundle
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
run: |
|
||||
yarn build
|
||||
yarn run portable
|
||||
env:
|
||||
TAG_NAME: alpha
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
@@ -1,6 +1,10 @@
|
||||
name: Release CI
|
||||
|
||||
on: [push]
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
@@ -12,54 +16,38 @@ jobs:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: |
|
||||
startsWith(github.repository, 'zzzgydi') &&
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@ce325b60658c1b38465c06cc965b79baf32c1e72
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@0e558392ccadcb49bcc89e7df15a400e8f0c954d
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
# enable cache even though failed
|
||||
# continue-on-error: true
|
||||
env:
|
||||
@@ -73,38 +61,36 @@ jobs:
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
|
||||
- name: Green zip bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
- name: Portable Bundle
|
||||
if: startsWith(matrix.os, 'windows-')
|
||||
# rebuild with env settings
|
||||
run: |
|
||||
yarn build
|
||||
yarn run portable
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
|
||||
release-update:
|
||||
needs: release
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
startsWith(github.repository, 'zzzgydi') &&
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
|
||||
- name: Release updater file
|
||||
run: yarn run updater
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
name: Compatible CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# push:
|
||||
# tags:
|
||||
# - v**
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
targets:
|
||||
- tag: macOS-10.15
|
||||
os: macos-10.15
|
||||
- tag: Ubuntu18
|
||||
os: ubuntu-18.04
|
||||
- tag: Ubuntu22
|
||||
os: ubuntu-22.04
|
||||
|
||||
runs-on: ${{ matrix.targets.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
# - name: Install Dependencies (ubuntu18 only)
|
||||
# if: matrix.targets.os == 'ubuntu-18.04'
|
||||
# run: |
|
||||
# sudo apt-get update
|
||||
# sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libappindicator3-dev librsvg2-dev libayatana-appindicator3-dev
|
||||
|
||||
- name: Install Dependencies (ubuntu22 only)
|
||||
if: startsWith(matrix.targets.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: ${{ matrix.targets.tag }}
|
||||
releaseName: "Compatible For ${{ matrix.targets.tag }}"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
|
||||
# - name: Portable Bundle
|
||||
# if: matrix.os == 'windows-latest'
|
||||
# # rebuild with env settings
|
||||
# run: |
|
||||
# yarn build
|
||||
# yarn run portable
|
||||
# env:
|
||||
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
# TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
# VITE_WIN_PORTABLE: 1
|
||||
@@ -0,0 +1,97 @@
|
||||
name: Dev Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'keiko233')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Delete current release assets
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
tag: dev
|
||||
fail-if-no-assets: false
|
||||
fail-if-no-release: false
|
||||
assets: |
|
||||
*.zip
|
||||
*.gz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
|
||||
- name: Install Dependencies (Ubuntu Only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: dev
|
||||
releaseName: "Clash Nyanpasu Dev"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
tauriScript: pnpm
|
||||
args: -f default-meta
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
pnpm build -f default-meta
|
||||
pnpm portable
|
||||
env:
|
||||
TAG_NAME: dev
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Meta CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- v**
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Delete current release assets
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
uses: mknejp/delete-release-assets@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: meta
|
||||
fail-if-no-assets: false
|
||||
fail-if-no-release: false
|
||||
assets: |
|
||||
*.zip
|
||||
*.gz
|
||||
*.AppImage
|
||||
*.deb
|
||||
*.dmg
|
||||
*.msi
|
||||
*.sig
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: meta
|
||||
releaseName: "Clash Verge Meta"
|
||||
releaseBody: ""
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
args: -f default-meta
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
yarn build -f default-meta
|
||||
yarn run portable
|
||||
env:
|
||||
TAG_NAME: meta
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
@@ -0,0 +1,107 @@
|
||||
name: Release Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [windows-latest, ubuntu-latest, macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
if: startsWith(github.repository, 'keiko233')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Install Dependencies (Ubuntu Only)
|
||||
if: startsWith(matrix.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf openssl
|
||||
|
||||
- name: Pnpm install and check
|
||||
run: |
|
||||
pnpm i
|
||||
pnpm check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: v__VERSION__
|
||||
releaseName: "Clash Nyanpasu v__VERSION__"
|
||||
releaseBody: "More new features are now supported."
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
tauriScript: pnpm
|
||||
args: -f default-meta
|
||||
|
||||
- name: Portable Bundle
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
pnpm build -f default-meta
|
||||
pnpm portable
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
VITE_WIN_PORTABLE: 1
|
||||
|
||||
release-update:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
startsWith(github.repository, 'keiko233')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 8
|
||||
run_install: false
|
||||
|
||||
- name: Pnpm install
|
||||
run: pnpm i
|
||||
|
||||
- name: Release updater file
|
||||
run: pnpm updater
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Test CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
os:
|
||||
description: "Runs on OS"
|
||||
required: true
|
||||
default: windows-latest
|
||||
type: choice
|
||||
options:
|
||||
- windows-latest
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- ubuntu-18.04
|
||||
- ubuntu-20.04
|
||||
- ubuntu-22.04
|
||||
- macos-10.15
|
||||
- macos-11
|
||||
- macos-12
|
||||
- windows-2019
|
||||
- windows-2022
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: 0
|
||||
RUST_BACKTRACE: short
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ${{ github.event.inputs.os }}
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: System Version
|
||||
run: |
|
||||
echo ${{ github.event.inputs.os }}
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: src-tauri
|
||||
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install Dependencies (ubuntu only)
|
||||
if: startsWith(github.event.inputs.os, 'ubuntu-')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: Yarn install and check
|
||||
run: |
|
||||
yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
with:
|
||||
tagName: alpha
|
||||
releaseName: "Clash Verge Alpha"
|
||||
releaseBody: "Alpha Version (include debug)"
|
||||
releaseDraft: false
|
||||
includeUpdaterJson: false
|
||||
@@ -4,27 +4,20 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
release-update:
|
||||
runs-on: macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.repository, 'zzzgydi')
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get yarn cache dir path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- name: Yarn Cache
|
||||
uses: actions/cache@v2
|
||||
id: yarn-cache
|
||||
- name: Install Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
node-version: "16"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Yarn install
|
||||
run: yarn install
|
||||
run: yarn install --network-timeout 1000000 --frozen-lockfile
|
||||
|
||||
- name: Release updater file
|
||||
run: yarn run updater
|
||||
|
||||
@@ -4,3 +4,5 @@ dist
|
||||
dist-ssr
|
||||
*.local
|
||||
update.json
|
||||
scripts/_env.sh
|
||||
.vscode
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn pretty-quick --staged
|
||||
pnpm pretty-quick --staged
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<h1 align="center">
|
||||
<img src="./src/assets/image/logo.png" alt="Clash" width="128" />
|
||||
<br>
|
||||
Clash Verge
|
||||
Clash Nyanpasu
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
@@ -12,70 +12,61 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
||||
## Features
|
||||
|
||||
- Full `clash` config supported, Partial `clash premium` config supported.
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/issues/12)
|
||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/keiko233/clash-nyanpasu/wiki/%E4%BD%BF%E7%94%A8%E6%8C%87%E5%8D%97)
|
||||
- Simple UI and supports custom theme color.
|
||||
- Built-in support [Clash.Meta](https://github.com/MetaCubeX/Clash.Meta) core.
|
||||
- System proxy setting and guard.
|
||||
|
||||
## Install
|
||||
|
||||
Download from [release](https://github.com/zzzgydi/clash-verge/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
Download from [release](https://github.com/keiko233/clash-nyanpasu/releases). Supports Windows x64, Linux x86_64 and macOS 11+
|
||||
|
||||
Or you can build it yourself. Supports Windows, Linux and macOS 10.15+
|
||||
|
||||
Notes: If you could not start the app on Windows, please check that you have [Webview2](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section) installed.
|
||||
|
||||
### FAQ
|
||||
|
||||
#### 1. **macOS** "Clash Nyanpasu" is damaged and can't be opened
|
||||
|
||||
open the terminal and run `sudo xattr -r -d com.apple.quarantine /Applications/Clash\ Nyanpasu.app`
|
||||
|
||||
## Development
|
||||
|
||||
You should install Rust and Nodejs, see [here](https://tauri.studio/docs/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
|
||||
|
||||
```shell
|
||||
yarn install
|
||||
pnpm i
|
||||
```
|
||||
|
||||
Then download the clash binary... Or you can download it from [clash premium release](https://github.com/Dreamacro/clash/releases/tag/premium) and rename it according to [tauri config](https://tauri.studio/docs/api/config/#tauri.bundle.externalBin).
|
||||
|
||||
```shell
|
||||
yarn run check
|
||||
# force update to latest version
|
||||
# pnpm run check --force
|
||||
|
||||
pnpm run check
|
||||
```
|
||||
|
||||
Then run
|
||||
|
||||
```shell
|
||||
yarn dev
|
||||
pnpm dev
|
||||
|
||||
# run it in another way if app instance exists
|
||||
pnpm dev:diff
|
||||
```
|
||||
|
||||
Or you can build it
|
||||
|
||||
```shell
|
||||
yarn build
|
||||
pnpm build
|
||||
```
|
||||
|
||||
## Todos
|
||||
|
||||
> This keng is a little big...
|
||||
|
||||
## Screenshots
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/demo1.png" alt="demo1" width="32%" />
|
||||
<img src="./docs/demo2.png" alt="demo2" width="32%" />
|
||||
<img src="./docs/demo3.png" alt="demo3" width="32%" />
|
||||
<img src="./docs/demo4.png" alt="demo4" width="32%" />
|
||||
<img src="./docs/demo5.png" alt="demo5" width="32%" />
|
||||
<img src="./docs/demo6.png" alt="demo6" width="32%" />
|
||||
</div>
|
||||
|
||||
### Custom Theme
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/color1.png" alt="demo1" width="16%" />
|
||||
<img src="./docs/color2.png" alt="demo2" width="16%" />
|
||||
<img src="./docs/color3.png" alt="demo3" width="16%" />
|
||||
<img src="./docs/color4.png" alt="demo4" width="16%" />
|
||||
<img src="./docs/color5.png" alt="demo5" width="16%" />
|
||||
<img src="./docs/color6.png" alt="demo6" width="16%" />
|
||||
</div>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This is a learning project for Rust practice.
|
||||
@@ -86,10 +77,12 @@ Issue and PR welcome!
|
||||
|
||||
## Acknowledgement
|
||||
|
||||
Clash Verge was based on or inspired by these projects and so on:
|
||||
Clash Nyanpasu was based on or inspired by these projects and so on:
|
||||
|
||||
- [zzzgydi/clash-verge](https://github.com/zzzgydi/clash-verge): A Clash GUI based on tauri. Supports Windows, macOS and Linux.
|
||||
- [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
|
||||
- [Dreamacro/clash](https://github.com/Dreamacro/clash): A rule-based tunnel in Go.
|
||||
- [MetaCubeX/Clash.Meta](https://github.com/MetaCubeX/Clash.Meta): A rule-based tunnel in Go.
|
||||
- [Fndroid/clash_for_windows_pkg](https://github.com/Fndroid/clash_for_windows_pkg): A Windows/macOS GUI based on Clash.
|
||||
- [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
|
||||
|
||||
|
||||
@@ -1,3 +1,381 @@
|
||||
## v1.4.0
|
||||
|
||||
### Features
|
||||
|
||||
- Default use Meta Core.
|
||||
- Support copy PowerShell, CMD and sh env command.
|
||||
- Add Upload Traffic, Download Traffic and Active Connections to ConnectionsPage.
|
||||
- SettingPage use Grid layout.
|
||||
- Import LoadingButton & use when Download Profile.
|
||||
- New default theme color.
|
||||
- Add Nyanpasu Element. (Logo designer [@ReallySnow](https://github.com/ReallySnow))
|
||||
- Default enable unified-delay & tcp-concurrent.
|
||||
- Use Meta Country.mmdb.
|
||||
- Disable IPv6 by default.
|
||||
- Add Material You element.
|
||||
- Add Router switch transition.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix touchpad scrolling causes blank area to appear.
|
||||
|
||||
---
|
||||
|
||||
## v1.3.7
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
- profiles page add paste button
|
||||
- subscriptions url textfield use multi lines
|
||||
- set min window size
|
||||
- add check for updates buttons
|
||||
- add open dashboard to the hotkey list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix profiles page undefined exception
|
||||
|
||||
---
|
||||
|
||||
## v1.3.6
|
||||
|
||||
### Features
|
||||
|
||||
- add russian translation
|
||||
- support to show connection detail
|
||||
- support clash meta memory usage display
|
||||
- support proxy provider update ui
|
||||
- update geo data file from meta repo
|
||||
- adjust setting page
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- center the window when it is out of screen
|
||||
- use `sudo` when `pkexec` not found (Linux)
|
||||
- reconnect websocket when window focus
|
||||
|
||||
### Notes
|
||||
|
||||
- The current version of the Linux installation package is built by Ubuntu 20.04 (Github Action).
|
||||
|
||||
---
|
||||
|
||||
## v1.3.5
|
||||
|
||||
### Features
|
||||
|
||||
- update clash core
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix blurry system tray icon (Windows)
|
||||
- fix v1.3.4 wintun.dll not found (Windows)
|
||||
- fix v1.3.4 clash core not found (macOS, Linux)
|
||||
|
||||
---
|
||||
|
||||
## v1.3.4
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
- optimize traffic graph high CPU usage when window hidden
|
||||
- use polkit to elevate permission (Linux)
|
||||
- support app log level setting
|
||||
- support copy environment variable
|
||||
- overwrite resource file according to file modified
|
||||
- save window size and position
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- remove fallback group select status
|
||||
- enable context menu on editable element (Windows)
|
||||
|
||||
---
|
||||
|
||||
## v1.3.3
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
- show tray icon variants in different system proxy status (Windows)
|
||||
- close all connections when mode changed
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- encode controller secret into uri
|
||||
- error boundary for each page
|
||||
|
||||
---
|
||||
|
||||
## v1.3.2
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix import url issue
|
||||
- fix profile undefined issue
|
||||
|
||||
---
|
||||
|
||||
## v1.3.1
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta core
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix open url issue
|
||||
- fix appimage path panic
|
||||
- fix grant root permission in macOS
|
||||
- fix linux system proxy default bypass
|
||||
|
||||
---
|
||||
|
||||
## v1.3.0
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash meta
|
||||
- support opening dir on tray
|
||||
- support updating all profiles with one click
|
||||
- support granting root permission to clash core(Linux, macOS)
|
||||
- support enable/disable clash fields filter, feel free to experience the latest features of Clash Meta
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- deb add openssl depend(Linux)
|
||||
- fix the AppImage auto launch path(Linux)
|
||||
- fix get the default network service(macOS)
|
||||
- remove the esc key listener in macOS, cmd+w instead(macOS)
|
||||
- fix infinite retry when websocket error
|
||||
|
||||
---
|
||||
|
||||
## v1.2.3
|
||||
|
||||
### Features
|
||||
|
||||
- update clash
|
||||
- adjust macOS window style
|
||||
- profile supports UTF8 with BOM
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix selected proxy
|
||||
- fix error log
|
||||
|
||||
---
|
||||
|
||||
## v1.2.2
|
||||
|
||||
### Features
|
||||
|
||||
- update clash meta
|
||||
- recover clash core after panic
|
||||
- use system window decorations(Linux)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- flush system proxy settings(Windows)
|
||||
- fix parse log panic
|
||||
- fix ui bug
|
||||
|
||||
---
|
||||
|
||||
## v1.2.1
|
||||
|
||||
### Features
|
||||
|
||||
- update clash version
|
||||
- proxy groups support multi columns
|
||||
- optimize ui
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix ui websocket connection
|
||||
- adjust delay check concurrency
|
||||
- avoid setting login item repeatedly(macOS)
|
||||
|
||||
---
|
||||
|
||||
## v1.2.0
|
||||
|
||||
### Features
|
||||
|
||||
- update clash meta version
|
||||
- support to change external-controller
|
||||
- support to change default latency test URL
|
||||
- close all connections when proxy changed or profile changed
|
||||
- check the config by using the core
|
||||
- increase the robustness of the program
|
||||
- optimize windows service mode (need to reinstall)
|
||||
- optimize ui
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- invalid hotkey cause panic
|
||||
- invalid theme setting cause panic
|
||||
- fix some other glitches
|
||||
|
||||
---
|
||||
|
||||
## v1.1.2
|
||||
|
||||
### Features
|
||||
|
||||
- the system tray follows i18n
|
||||
- change the proxy group ui of global mode
|
||||
- support to update profile with the system proxy/clash proxy
|
||||
- check the remote profile more strictly
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- use app version as default user agent
|
||||
- the clash not exit in service mode
|
||||
- reset the system proxy when quit the app
|
||||
- fix some other glitches
|
||||
|
||||
---
|
||||
|
||||
## v1.1.1
|
||||
|
||||
### Features
|
||||
|
||||
- optimize clash config feedback
|
||||
- hide macOS dock icon
|
||||
- use clash meta compatible version (Linux)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix some other glitches
|
||||
|
||||
---
|
||||
|
||||
## v1.1.0
|
||||
|
||||
### Features
|
||||
|
||||
- add rule page
|
||||
- supports proxy providers delay check
|
||||
- add proxy delay check loading status
|
||||
- supports hotkey/shortcut management
|
||||
- supports displaying connections data in table layout(refer to yacd)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- supports yaml merge key in clash config
|
||||
- detect the network interface and set the system proxy(macOS)
|
||||
- fix some other glitches
|
||||
|
||||
---
|
||||
|
||||
## v1.0.6
|
||||
|
||||
### Features
|
||||
|
||||
- update clash and clash.meta
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- only script profile display console
|
||||
- automatic configuration update on demand at launch
|
||||
|
||||
---
|
||||
|
||||
## v1.0.5
|
||||
|
||||
### Features
|
||||
|
||||
- reimplement profile enhanced mode with quick-js
|
||||
- optimize the runtime config generation process
|
||||
- support web ui management
|
||||
- support clash field management
|
||||
- support viewing the runtime config
|
||||
- adjust some pages style
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix silent start
|
||||
- fix incorrectly reset system proxy on exit
|
||||
|
||||
---
|
||||
|
||||
## v1.0.4
|
||||
|
||||
### Features
|
||||
|
||||
- update clash core and clash meta version
|
||||
- support switch clash mode on system tray
|
||||
- theme mode support follows system
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- config load error on first use
|
||||
|
||||
---
|
||||
|
||||
## v1.0.3
|
||||
|
||||
### Features
|
||||
|
||||
- save some states such as URL test, filter, etc
|
||||
- update clash core and clash-meta core
|
||||
- new icon for macOS
|
||||
|
||||
---
|
||||
|
||||
## v1.0.2
|
||||
|
||||
### Features
|
||||
|
||||
- supports for switching clash core
|
||||
- supports release UI processes
|
||||
- supports script mode setting
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix service mode bug (Windows)
|
||||
|
||||
---
|
||||
|
||||
## v1.0.1
|
||||
|
||||
### Features
|
||||
|
||||
- adjust default theme settings
|
||||
- reduce gpu usage of traffic graph when hidden
|
||||
- supports more remote profile response header setting
|
||||
- check remote profile data format when imported
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- service mode install and start issue (Windows)
|
||||
- fix launch panic (Some Windows)
|
||||
|
||||
---
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### Features
|
||||
|
||||
- update clash core
|
||||
- optimize traffic graph animation
|
||||
- supports interval update profiles
|
||||
- supports service mode (Windows)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- reset system proxy when exit from dock (macOS)
|
||||
- adjust clash dns config process strategy
|
||||
|
||||
---
|
||||
|
||||
## v0.0.29
|
||||
|
||||
### Features
|
||||
@@ -10,6 +388,8 @@
|
||||
- system tray add tun mode toggle
|
||||
- enable to change the config dir (Windows only)
|
||||
|
||||
---
|
||||
|
||||
## v0.0.28
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "0.0.29",
|
||||
"name": "clash-nyanpasu",
|
||||
"version": "1.4.0",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build",
|
||||
"dev": "tauri dev -f default-meta",
|
||||
"dev:diff": "tauri dev -f verge-dev",
|
||||
"build": "tauri build -f default-meta",
|
||||
"tauri": "tauri",
|
||||
"web:dev": "vite",
|
||||
"web:build": "tsc && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"aarch": "node scripts/aarch.mjs",
|
||||
"check": "node scripts/check.mjs",
|
||||
"updater": "node scripts/updater.mjs",
|
||||
"publish": "node scripts/publish.mjs",
|
||||
@@ -16,44 +18,56 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.8.2",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mui/icons-material": "^5.6.1",
|
||||
"@mui/material": "^5.6.1",
|
||||
"@tauri-apps/api": "^1.0.0-rc.3",
|
||||
"ahooks": "^3.2.0",
|
||||
"axios": "^0.26.0",
|
||||
"dayjs": "^1.11.0",
|
||||
"i18next": "^21.6.14",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.6",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"react-virtuoso": "~2.7.2",
|
||||
"recoil": "^0.6.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@mui/icons-material": "^5.14.14",
|
||||
"@mui/lab": "5.0.0-alpha.149",
|
||||
"@mui/material": "^5.14.14",
|
||||
"@mui/x-data-grid": "^6.16.3",
|
||||
"@tauri-apps/api": "^1.3.0",
|
||||
"ahooks": "^3.7.2",
|
||||
"axios": "^1.1.3",
|
||||
"dayjs": "1.11.5",
|
||||
"i18next": "^22.0.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"monaco-editor": "^0.34.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-hook-form": "^7.39.5",
|
||||
"react-i18next": "^12.0.0",
|
||||
"react-router-dom": "^6.4.3",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"react-virtuoso": "^3.1.3",
|
||||
"recoil": "^0.7.6",
|
||||
"snarkdown": "^2.0.0",
|
||||
"swr": "^1.2.2"
|
||||
"swr": "^1.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@actions/github": "^5.0.0",
|
||||
"@tauri-apps/cli": "^1.0.0-rc.8",
|
||||
"@actions/github": "^5.0.3",
|
||||
"@tauri-apps/cli": "^1.3.1",
|
||||
"@types/fs-extra": "^9.0.13",
|
||||
"@types/js-cookie": "^3.0.1",
|
||||
"@types/js-cookie": "^3.0.2",
|
||||
"@types/lodash": "^4.14.180",
|
||||
"@types/react": "^17.0.0",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@vitejs/plugin-react": "^1.2.0",
|
||||
"@types/lodash-es": "^4.17.7",
|
||||
"@types/react": "^18.2.37",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@vitejs/plugin-react": "^4.1.0",
|
||||
"adm-zip": "^0.5.9",
|
||||
"cross-env": "^7.0.3",
|
||||
"fs-extra": "^10.0.0",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"husky": "^7.0.0",
|
||||
"node-fetch": "^3.2.0",
|
||||
"node-fetch": "^3.2.6",
|
||||
"prettier": "^2.7.1",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"sass": "^1.49.7",
|
||||
"typescript": "^4.5.5",
|
||||
"vite": "^2.8.6",
|
||||
"vite-plugin-monaco-editor": "^1.0.10",
|
||||
"vite-plugin-svgr": "^1.1.0"
|
||||
"sass": "^1.54.0",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.5.0",
|
||||
"vite-plugin-monaco-editor": "^1.1.0",
|
||||
"vite-plugin-svgr": "^4.1.0"
|
||||
},
|
||||
"prettier": {
|
||||
"tabWidth": 2,
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Build and upload assets
|
||||
* for macOS(aarch)
|
||||
*/
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { exit } from "process";
|
||||
import { execSync } from "child_process";
|
||||
import { createRequire } from "module";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
|
||||
// to `meta` tag
|
||||
const META = process.argv.includes("--meta");
|
||||
// to `alpha` tag
|
||||
const ALPHA = process.argv.includes("--alpha");
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
async function resolve() {
|
||||
if (!process.env.GITHUB_TOKEN) {
|
||||
throw new Error("GITHUB_TOKEN is required");
|
||||
}
|
||||
if (!process.env.GITHUB_REPOSITORY) {
|
||||
throw new Error("GITHUB_REPOSITORY is required");
|
||||
}
|
||||
if (!process.env.TAURI_PRIVATE_KEY) {
|
||||
throw new Error("TAURI_PRIVATE_KEY is required");
|
||||
}
|
||||
if (!process.env.TAURI_KEY_PASSWORD) {
|
||||
throw new Error("TAURI_KEY_PASSWORD is required");
|
||||
}
|
||||
|
||||
const { version } = require("../package.json");
|
||||
|
||||
const tag = META ? "meta" : ALPHA ? "alpha" : `v${version}`;
|
||||
const buildCmd = META ? `pnpm build -f default-meta` : `pnpm build`;
|
||||
|
||||
console.log(`[INFO]: Upload to tag "${tag}"`);
|
||||
console.log(`[INFO]: Building app. "${buildCmd}"`);
|
||||
|
||||
execSync(buildCmd);
|
||||
|
||||
const cwd = process.cwd();
|
||||
const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
|
||||
const join = (p) => path.join(bundlePath, p);
|
||||
|
||||
const appPathList = [
|
||||
join("macos/Clash Nyanpasu.aarch64.app.tar.gz"),
|
||||
join("macos/Clash Nyanpasu.aarch64.app.tar.gz.sig"),
|
||||
];
|
||||
|
||||
for (const appPath of appPathList) {
|
||||
if (fs.pathExistsSync(appPath)) {
|
||||
fs.removeSync(appPath);
|
||||
}
|
||||
}
|
||||
|
||||
fs.copyFileSync(join("macos/Clash Nyanpasu.app.tar.gz"), appPathList[0]);
|
||||
fs.copyFileSync(join("macos/Clash Nyanpasu.app.tar.gz.sig"), appPathList[1]);
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag,
|
||||
});
|
||||
|
||||
if (!release.id) throw new Error("failed to find the release");
|
||||
|
||||
await uploadAssets(release.id, [
|
||||
join(`dmg/Clash Nyanpasu_${version}_aarch64.dmg`),
|
||||
...appPathList,
|
||||
]);
|
||||
}
|
||||
|
||||
// From tauri-apps/tauri-action
|
||||
// https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts
|
||||
async function uploadAssets(releaseId, assets) {
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
// Determine content-length for header to upload asset
|
||||
const contentLength = (filePath) => fs.statSync(filePath).size;
|
||||
|
||||
for (const assetPath of assets) {
|
||||
const headers = {
|
||||
"content-type": "application/zip",
|
||||
"content-length": contentLength(assetPath),
|
||||
};
|
||||
|
||||
const ext = path.extname(assetPath);
|
||||
const filename = path.basename(assetPath).replace(ext, "");
|
||||
const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`)
|
||||
? `${filename}-debug${ext}`
|
||||
: `${filename}${ext}`;
|
||||
|
||||
console.log(`[INFO]: Uploading ${assetName}...`);
|
||||
|
||||
try {
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
headers,
|
||||
name: assetName,
|
||||
data: fs.readFileSync(assetPath),
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
release_id: releaseId,
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === "darwin" && process.arch === "arm64") {
|
||||
resolve();
|
||||
} else {
|
||||
console.error("invalid");
|
||||
exit(1);
|
||||
}
|
||||
@@ -3,93 +3,205 @@ import zlib from "zlib";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import fetch from "node-fetch";
|
||||
import proxyAgent from "https-proxy-agent";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
const cwd = process.cwd();
|
||||
const TEMP_DIR = path.join(cwd, "node_modules/.verge");
|
||||
const FORCE = process.argv.includes("--force");
|
||||
|
||||
const SIDECAR_HOST = execSync("rustc -vV")
|
||||
.toString()
|
||||
.match(/(?<=host: ).+(?=\s*)/g)[0];
|
||||
|
||||
/* ======= clash ======= */
|
||||
const CLASH_STORAGE_PREFIX = "https://release.dreamacro.workers.dev/";
|
||||
const CLASH_URL_PREFIX =
|
||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||
const CLASH_LATEST_DATE = "2023.08.17";
|
||||
|
||||
const CLASH_BACKUP_URL_PREFIX =
|
||||
"https://github.com/zhongfly/Clash-premium-backup/releases/download/";
|
||||
const CLASH_BACKUP_LATEST_DATE = "2023-09-05-gdcc8d87";
|
||||
|
||||
//https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-windows-amd64-2023-09-05-gdcc8d87.zip
|
||||
//https://github.com/zhongfly/Clash-premium-backup/releases/download/2023-09-05-gdcc8d87/clash-windows-amd64-n2023-09-05-gdcc8d87.zip
|
||||
|
||||
const CLASH_MAP = {
|
||||
"win32-x64": "clash-windows-amd64",
|
||||
"darwin-x64": "clash-darwin-amd64",
|
||||
"darwin-arm64": "clash-darwin-arm64",
|
||||
"linux-x64": "clash-linux-amd64",
|
||||
"linux-arm64": "clash-linux-arm64",
|
||||
};
|
||||
|
||||
/* ======= clash meta ======= */
|
||||
const META_URL_PREFIX = `https://github.com/MetaCubeX/Clash.Meta/releases/download/`;
|
||||
const META_VERSION = "v1.16.0";
|
||||
|
||||
const META_MAP = {
|
||||
"win32-x64": "clash.meta-windows-amd64-compatible",
|
||||
"darwin-x64": "clash.meta-darwin-amd64",
|
||||
"darwin-arm64": "clash.meta-darwin-arm64",
|
||||
"linux-x64": "clash.meta-linux-amd64-compatible",
|
||||
"linux-arm64": "clash.meta-linux-arm64",
|
||||
};
|
||||
|
||||
/**
|
||||
* get the correct clash release infomation
|
||||
* check available
|
||||
*/
|
||||
function resolveClash() {
|
||||
const { platform, arch } = process;
|
||||
|
||||
const CLASH_URL_PREFIX =
|
||||
"https://github.com/Dreamacro/clash/releases/download/premium/";
|
||||
const CLASH_LATEST_DATE = "2022.04.11";
|
||||
const { platform, arch } = process;
|
||||
if (!CLASH_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash unsupported platform "${platform}-${arch}"`);
|
||||
}
|
||||
if (!META_MAP[`${platform}-${arch}`]) {
|
||||
throw new Error(`clash meta unsupported platform "${platform}-${arch}"`);
|
||||
}
|
||||
|
||||
// todo
|
||||
const map = {
|
||||
"win32-x64": "clash-windows-amd64",
|
||||
"darwin-x64": "clash-darwin-amd64",
|
||||
"darwin-arm64": "clash-darwin-arm64",
|
||||
"linux-x64": "clash-linux-amd64",
|
||||
};
|
||||
|
||||
const name = map[`${platform}-${arch}`];
|
||||
|
||||
if (!name) {
|
||||
throw new Error(`unsupport platform "${platform}-${arch}"`);
|
||||
}
|
||||
function clash() {
|
||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const zip = isWin ? "zip" : "gz";
|
||||
const url = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${zip}`;
|
||||
const exefile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipfile = `${name}.${zip}`;
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${CLASH_URL_PREFIX}${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}.${urlExt}`;
|
||||
|
||||
return { url, zip, exefile, zipfile };
|
||||
return {
|
||||
name: "clash",
|
||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
function clashBackup() {
|
||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${CLASH_BACKUP_URL_PREFIX}${CLASH_BACKUP_LATEST_DATE}/${name}-n${CLASH_BACKUP_LATEST_DATE}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "clash",
|
||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
function clashS3() {
|
||||
const name = CLASH_MAP[`${platform}-${arch}`];
|
||||
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${CLASH_STORAGE_PREFIX}${CLASH_LATEST_DATE}/${name}-${CLASH_LATEST_DATE}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "clash",
|
||||
targetFile: `clash-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
function clashMeta() {
|
||||
const name = META_MAP[`${platform}-${arch}`];
|
||||
const isWin = platform === "win32";
|
||||
const urlExt = isWin ? "zip" : "gz";
|
||||
const downloadURL = `${META_URL_PREFIX}${META_VERSION}/${name}-${META_VERSION}.${urlExt}`;
|
||||
const exeFile = `${name}${isWin ? ".exe" : ""}`;
|
||||
const zipFile = `${name}-${META_VERSION}.${urlExt}`;
|
||||
|
||||
return {
|
||||
name: "clash-meta",
|
||||
targetFile: `clash-meta-${SIDECAR_HOST}${isWin ? ".exe" : ""}`,
|
||||
exeFile,
|
||||
zipFile,
|
||||
downloadURL,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* get the sidecar bin
|
||||
* download sidecar and rename
|
||||
*/
|
||||
async function resolveSidecar() {
|
||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||
async function resolveSidecar(binInfo) {
|
||||
const { name, targetFile, zipFile, exeFile, downloadURL } = binInfo;
|
||||
|
||||
const host = execSync("rustc -vV | grep host").toString().slice(6).trim();
|
||||
const ext = process.platform === "win32" ? ".exe" : "";
|
||||
const sidecarFile = `clash-${host}${ext}`;
|
||||
const sidecarPath = path.join(sidecarDir, sidecarFile);
|
||||
const sidecarDir = path.join(cwd, "src-tauri", "sidecar");
|
||||
const sidecarPath = path.join(sidecarDir, targetFile);
|
||||
|
||||
await fs.mkdirp(sidecarDir);
|
||||
if (!FORCE && (await fs.pathExists(sidecarPath))) return;
|
||||
|
||||
// download sidecar
|
||||
const binInfo = resolveClash();
|
||||
const tempDir = path.join(cwd, "pre-dev-temp");
|
||||
const tempZip = path.join(tempDir, binInfo.zipfile);
|
||||
const tempExe = path.join(tempDir, binInfo.exefile);
|
||||
const tempDir = path.join(TEMP_DIR, name);
|
||||
const tempZip = path.join(tempDir, zipFile);
|
||||
const tempExe = path.join(tempDir, exeFile);
|
||||
|
||||
await fs.mkdirp(tempDir);
|
||||
if (!(await fs.pathExists(tempZip))) await downloadFile(binInfo.url, tempZip);
|
||||
try {
|
||||
if (!(await fs.pathExists(tempZip))) {
|
||||
await downloadFile(downloadURL, tempZip);
|
||||
}
|
||||
|
||||
if (binInfo.zip === "zip") {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
console.log("[INFO]: entry name", entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
// save as sidecar
|
||||
await fs.rename(tempExe, sidecarPath);
|
||||
console.log(`[INFO]: unzip finished`);
|
||||
} else {
|
||||
// gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
readStream
|
||||
.pipe(zlib.createGunzip())
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
console.log(`[INFO]: gunzip finished`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
console.log(`[INFO]: chmod binary finished`);
|
||||
})
|
||||
.on("error", (error) => console.error(error));
|
||||
if (zipFile.endsWith(".zip")) {
|
||||
const zip = new AdmZip(tempZip);
|
||||
zip.getEntries().forEach((entry) => {
|
||||
console.log(`[DEBUG]: "${name}" entry name`, entry.entryName);
|
||||
});
|
||||
zip.extractAllTo(tempDir, true);
|
||||
await fs.rename(tempExe, sidecarPath);
|
||||
console.log(`[INFO]: "${name}" unzip finished`);
|
||||
} else {
|
||||
// gz
|
||||
const readStream = fs.createReadStream(tempZip);
|
||||
const writeStream = fs.createWriteStream(sidecarPath);
|
||||
await new Promise((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
console.error(`[ERROR]: "${name}" gz failed:`, error.message);
|
||||
reject(error);
|
||||
};
|
||||
readStream
|
||||
.pipe(zlib.createGunzip().on("error", onError))
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
console.log(`[INFO]: "${name}" gunzip finished`);
|
||||
execSync(`chmod 755 ${sidecarPath}`);
|
||||
console.log(`[INFO]: "${name}" chmod binary finished`);
|
||||
resolve();
|
||||
})
|
||||
.on("error", onError);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// 需要删除文件
|
||||
await fs.remove(sidecarPath);
|
||||
throw err;
|
||||
} finally {
|
||||
// delete temp dir
|
||||
await fs.remove(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
// delete temp dir
|
||||
await fs.remove(tempDir);
|
||||
/**
|
||||
* prepare clash core
|
||||
* if the core version is not updated in time, use S3 storage as a backup.
|
||||
*/
|
||||
async function resolveClash() {
|
||||
try {
|
||||
return await resolveSidecar(clash());
|
||||
} catch {
|
||||
console.log(`[WARN]: clash core needs to be updated`);
|
||||
return await resolveSidecar(clashS3());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +215,7 @@ async function resolveWintun() {
|
||||
|
||||
const url = "https://www.wintun.net/builds/wintun-0.14.1.zip";
|
||||
|
||||
const tempDir = path.join(cwd, "pre-dev-temp-1");
|
||||
const tempDir = path.join(TEMP_DIR, "wintun");
|
||||
const tempZip = path.join(tempDir, "wintun.zip");
|
||||
|
||||
const wintunPath = path.join(tempDir, "wintun/bin/amd64/wintun.dll");
|
||||
@@ -125,35 +237,47 @@ async function resolveWintun() {
|
||||
throw new Error(`path not found "${wintunPath}"`);
|
||||
}
|
||||
|
||||
// move wintun.dll to resources
|
||||
await fs.rename(wintunPath, targetPath);
|
||||
// delete temp dir
|
||||
await fs.remove(tempDir);
|
||||
|
||||
console.log(`[INFO]: resolve wintun.dll finished`);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the Country.mmdb (not required)
|
||||
* download the file to the resources dir
|
||||
*/
|
||||
async function resolveMmdb() {
|
||||
const url =
|
||||
"https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb";
|
||||
async function resolveResource(binInfo) {
|
||||
const { file, downloadURL } = binInfo;
|
||||
|
||||
const resDir = path.join(cwd, "src-tauri/resources");
|
||||
const targetPath = path.join(resDir, file);
|
||||
|
||||
if (!FORCE && (await fs.pathExists(targetPath))) return;
|
||||
|
||||
const resDir = path.join(cwd, "src-tauri", "resources");
|
||||
const resPath = path.join(resDir, "Country.mmdb");
|
||||
if (!FORCE && (await fs.pathExists(resPath))) return;
|
||||
await fs.mkdirp(resDir);
|
||||
await downloadFile(url, resPath);
|
||||
await downloadFile(downloadURL, targetPath);
|
||||
|
||||
console.log(`[INFO]: ${file} finished`);
|
||||
}
|
||||
|
||||
/**
|
||||
* download file and save to `path`
|
||||
*/
|
||||
async function downloadFile(url, path) {
|
||||
console.log(`[INFO]: downloading from "${url}"`);
|
||||
const options = {};
|
||||
|
||||
const httpProxy =
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy;
|
||||
|
||||
if (httpProxy) {
|
||||
options.agent = proxyAgent(httpProxy);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
});
|
||||
@@ -163,7 +287,71 @@ async function downloadFile(url, path) {
|
||||
console.log(`[INFO]: download finished "${url}"`);
|
||||
}
|
||||
|
||||
/// main
|
||||
resolveSidecar().catch(console.error);
|
||||
resolveWintun().catch(console.error);
|
||||
resolveMmdb().catch(console.error);
|
||||
/**
|
||||
* main
|
||||
*/
|
||||
const SERVICE_URL =
|
||||
"https://github.com/zzzgydi/clash-verge-service/releases/download/latest";
|
||||
|
||||
const resolveService = () =>
|
||||
resolveResource({
|
||||
file: "clash-verge-service.exe",
|
||||
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
|
||||
});
|
||||
const resolveInstall = () =>
|
||||
resolveResource({
|
||||
file: "install-service.exe",
|
||||
downloadURL: `${SERVICE_URL}/install-service.exe`,
|
||||
});
|
||||
const resolveUninstall = () =>
|
||||
resolveResource({
|
||||
file: "uninstall-service.exe",
|
||||
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
|
||||
});
|
||||
const resolveMmdb = () =>
|
||||
resolveResource({
|
||||
file: "Country.mmdb",
|
||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/country.mmdb`,
|
||||
});
|
||||
const resolveGeosite = () =>
|
||||
resolveResource({
|
||||
file: "geosite.dat",
|
||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat`,
|
||||
});
|
||||
const resolveGeoIP = () =>
|
||||
resolveResource({
|
||||
file: "geoip.dat",
|
||||
downloadURL: `https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.dat`,
|
||||
});
|
||||
|
||||
const tasks = [
|
||||
{ name: "clash", func: () => resolveSidecar(clashBackup()), retry: 5 },
|
||||
{ name: "clash-meta", func: () => resolveSidecar(clashMeta()), retry: 5 },
|
||||
{ name: "wintun", func: resolveWintun, retry: 5, winOnly: true },
|
||||
{ name: "service", func: resolveService, retry: 5, winOnly: true },
|
||||
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
|
||||
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
|
||||
{ name: "mmdb", func: resolveMmdb, retry: 5 },
|
||||
{ name: "geosite", func: resolveGeosite, retry: 5 },
|
||||
{ name: "geoip", func: resolveGeoIP, retry: 5 },
|
||||
];
|
||||
|
||||
async function runTask() {
|
||||
const task = tasks.shift();
|
||||
if (!task) return;
|
||||
if (task.winOnly && process.platform !== "win32") return runTask();
|
||||
|
||||
for (let i = 0; i < task.retry; i++) {
|
||||
try {
|
||||
await task.func();
|
||||
break;
|
||||
} catch (err) {
|
||||
console.error(`[ERROR]: task::${task.name} try ${i} ==`, err.message);
|
||||
if (i === task.retry - 1) throw err;
|
||||
}
|
||||
}
|
||||
return runTask();
|
||||
}
|
||||
|
||||
runTask();
|
||||
runTask();
|
||||
|
||||
@@ -17,15 +17,16 @@ async function resolvePortable() {
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Nyanpasu.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "clash.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "clash-meta.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require("../package.json");
|
||||
const { version } = packageJson;
|
||||
|
||||
const zipFile = `Clash.Verge_${version}_x64_portable.zip`;
|
||||
const zipFile = `Clash.Nyanpasu_${version}_x64_portable.zip`;
|
||||
zip.writeZip(zipFile);
|
||||
|
||||
console.log("[INFO]: create portable zip successfully");
|
||||
@@ -38,9 +39,11 @@ async function resolvePortable() {
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
console.log("[INFO]: upload to ", process.env.TAG_NAME || `v${version}`);
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: `v${version}`,
|
||||
tag: process.env.TAG_NAME || `v${version}`,
|
||||
});
|
||||
|
||||
console.log(release.name);
|
||||
|
||||
@@ -43,6 +43,7 @@ async function resolveUpdater() {
|
||||
darwin: { signature: "", url: "" }, // compatible with older formats
|
||||
"darwin-aarch64": { signature: "", url: "" },
|
||||
"darwin-intel": { signature: "", url: "" },
|
||||
"darwin-x86_64": { signature: "", url: "" },
|
||||
"linux-x86_64": { signature: "", url: "" },
|
||||
"windows-x86_64": { signature: "", url: "" },
|
||||
"windows-i686": { signature: "", url: "" }, // no supported
|
||||
@@ -53,12 +54,12 @@ async function resolveUpdater() {
|
||||
const { name, browser_download_url } = asset;
|
||||
|
||||
// win64 url
|
||||
if (name.endsWith(".msi.zip")) {
|
||||
if (name.endsWith(".msi.zip") && name.includes("en-US")) {
|
||||
updateData.platforms.win64.url = browser_download_url;
|
||||
updateData.platforms["windows-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// win64 signature
|
||||
if (name.endsWith(".msi.zip.sig")) {
|
||||
if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.win64.signature = sig;
|
||||
updateData.platforms["windows-x86_64"].signature = sig;
|
||||
@@ -68,20 +69,22 @@ async function resolveUpdater() {
|
||||
if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
|
||||
updateData.platforms.darwin.url = browser_download_url;
|
||||
updateData.platforms["darwin-intel"].url = browser_download_url;
|
||||
updateData.platforms["darwin-x86_64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (intel)
|
||||
if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms.darwin.signature = sig;
|
||||
updateData.platforms["darwin-intel"].signature = sig;
|
||||
updateData.platforms["darwin-x86_64"].signature = sig;
|
||||
}
|
||||
|
||||
// darwin url (aarch)
|
||||
if (name.endsWith("aarch.app.tar.gz")) {
|
||||
if (name.endsWith("aarch64.app.tar.gz")) {
|
||||
updateData.platforms["darwin-aarch64"].url = browser_download_url;
|
||||
}
|
||||
// darwin signature (aarch)
|
||||
if (name.endsWith("aarch.app.tar.gz.sig")) {
|
||||
if (name.endsWith("aarch64.app.tar.gz.sig")) {
|
||||
const sig = await getSignature(browser_download_url);
|
||||
updateData.platforms["darwin-aarch64"].signature = sig;
|
||||
}
|
||||
@@ -117,10 +120,7 @@ async function resolveUpdater() {
|
||||
|
||||
Object.entries(updateDataNew.platforms).forEach(([key, value]) => {
|
||||
if (value.url) {
|
||||
updateDataNew.platforms[key].url = value.url.replace(
|
||||
"https://github.com/",
|
||||
"https://hub.fastgit.xyz/"
|
||||
);
|
||||
updateDataNew.platforms[key].url = "https://ghproxy.com/" + value.url;
|
||||
} else {
|
||||
console.log(`[Error]: updateDataNew.platforms.${key} is null`);
|
||||
}
|
||||
|
||||
@@ -4,45 +4,63 @@ version = "0.1.0"
|
||||
description = "clash verge"
|
||||
authors = ["zzzgydi"]
|
||||
license = "GPL-3.0"
|
||||
repository = "https://github.com/zzzgydi/clash-verge.git"
|
||||
repository = "https://github.com/keiko233/clash-nyanpasu.git"
|
||||
default-run = "clash-verge"
|
||||
edition = "2021"
|
||||
build = "build.rs"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.0.0-rc.5", features = [] }
|
||||
tauri-build = { version = "1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
dirs = "4.0.0"
|
||||
open = "2.1.1"
|
||||
dunce = "1.0.2"
|
||||
nanoid = "0.4.0"
|
||||
chrono = "0.4.19"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "1.0.0-rc.6", features = ["process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
window-shadows = { git = "https://github.com/tauri-apps/window-shadows" }
|
||||
window-vibrancy = { git = "https://github.com/tauri-apps/window-vibrancy" }
|
||||
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
log = "0.4.14"
|
||||
log4rs = "1.0.0"
|
||||
warp = "0.3"
|
||||
which = "4.2.2"
|
||||
auto-launch = "0.2"
|
||||
anyhow = "1.0"
|
||||
dirs = "5.0.0"
|
||||
open = "4.0.1"
|
||||
log = "0.4.14"
|
||||
ctrlc = "3.2.3"
|
||||
dunce = "1.0.2"
|
||||
log4rs = "1.0.0"
|
||||
nanoid = "0.4.0"
|
||||
chrono = "0.4.19"
|
||||
sysinfo = "0.29"
|
||||
sysproxy = "0.3"
|
||||
rquickjs = "0.1.7"
|
||||
serde_json = "1.0"
|
||||
serde_yaml = "0.9"
|
||||
auto-launch = "0.5"
|
||||
once_cell = "1.14.0"
|
||||
port_scanner = "0.1.5"
|
||||
delay_timer = "0.11.1"
|
||||
parking_lot = "0.12.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
|
||||
tauri = { version = "1.2.4", features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all"] }
|
||||
window-vibrancy = { version = "0.3.0" }
|
||||
window-shadows = { version = "0.2.0" }
|
||||
wry = { version = "0.24.3" }
|
||||
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.10", features = ["transactions"] }
|
||||
runas = "1.1.0"
|
||||
deelevate = "0.2.0"
|
||||
winreg = { version = "0.50", features = ["transactions"] }
|
||||
windows-sys = { version = "0.48", features = ["Win32_System_LibraryLoader", "Win32_System_SystemInformation"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.tauri]
|
||||
features = ["global-shortcut-all", "icon-png", "process-all", "shell-all", "system-tray", "updater", "window-all"]
|
||||
|
||||
[target.'cfg(linux)'.dependencies.tauri]
|
||||
features = ["global-shortcut-all", "process-all", "shell-all", "system-tray", "updater", "window-all", "native-tls-vendored", "reqwest-native-tls-vendored"]
|
||||
|
||||
|
||||
[features]
|
||||
default = [ "custom-protocol" ]
|
||||
custom-protocol = [ "tauri/custom-protocol" ]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
verge-dev = []
|
||||
debug-yml = []
|
||||
default-meta = []
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
@@ -1,6 +1,6 @@
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
use_small_heuristics = "Default"
|
||||
reorder_imports = true
|
||||
|
||||
@@ -1,383 +1,269 @@
|
||||
use crate::{
|
||||
core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig},
|
||||
states::{ClashState, ProfilesState, VergeState},
|
||||
utils::{dirs, sysopt::SysProxyConfig},
|
||||
config::*,
|
||||
core::*,
|
||||
feat,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use crate::{ret_err, wrap_err};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use serde_yaml::Mapping;
|
||||
use std::process::Command;
|
||||
use tauri::{api, Manager, State};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use sysproxy::Sysproxy;
|
||||
|
||||
type CmdResult<T = ()> = Result<T, String>;
|
||||
|
||||
/// get all profiles from `profiles.yaml`
|
||||
#[tauri::command]
|
||||
pub fn get_profiles<'a>(profiles_state: State<'_, ProfilesState>) -> Result<Profiles, String> {
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
Ok(profiles.clone())
|
||||
pub fn get_profiles() -> CmdResult<IProfiles> {
|
||||
Ok(Config::profiles().data().clone())
|
||||
}
|
||||
|
||||
/// synchronize data irregularly
|
||||
#[tauri::command]
|
||||
pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.sync_file())
|
||||
pub async fn enhance_profiles() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// import the profile from url
|
||||
/// and save to `profiles.yaml`
|
||||
#[tauri::command]
|
||||
pub async fn import_profile(
|
||||
url: String,
|
||||
option: Option<PrfOption>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.append_item(item))
|
||||
pub async fn import_profile(url: String, option: Option<PrfOption>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// new a profile
|
||||
/// append a temp profile item file to the `profiles` dir
|
||||
/// view the temp profile file by using vscode or other editor
|
||||
#[tauri::command]
|
||||
pub async fn create_profile(
|
||||
item: PrfItem, // partial
|
||||
file_data: Option<String>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
wrap_err!(profiles.append_item(item))
|
||||
pub async fn create_profile(item: PrfItem, file_data: Option<String>) -> CmdResult {
|
||||
let item = wrap_err!(PrfItem::from(item, file_data).await)?;
|
||||
wrap_err!(Config::profiles().data().append_item(item))
|
||||
}
|
||||
|
||||
/// Update the profile
|
||||
#[tauri::command]
|
||||
pub async fn update_profile(
|
||||
index: String,
|
||||
option: Option<PrfOption>,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let (url, opt) = {
|
||||
// must release the lock here
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
pub async fn update_profile(index: String, option: Option<PrfOption>) -> CmdResult {
|
||||
wrap_err!(feat::update_profile(index, option).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn delete_profile(index: String) -> CmdResult {
|
||||
let should_update = wrap_err!({ Config::profiles().data().delete_item(index) })?;
|
||||
if should_update {
|
||||
wrap_err!(CoreManager::global().update_config().await)?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 修改profiles的
|
||||
#[tauri::command]
|
||||
pub async fn patch_profiles_config(profiles: IProfiles) -> CmdResult {
|
||||
wrap_err!({ Config::profiles().draft().patch_config(profiles) })?;
|
||||
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
Config::profiles().apply();
|
||||
wrap_err!(Config::profiles().data().save_file())?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::profiles().discard();
|
||||
log::error!(target: "app", "{err}");
|
||||
Err(format!("{err}"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改某个profile item的
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(index: String, profile: PrfItem) -> CmdResult {
|
||||
wrap_err!(Config::profiles().data().patch_item(index, profile))?;
|
||||
wrap_err!(timer::Timer::global().refresh())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn view_profile(index: String) -> CmdResult {
|
||||
let file = {
|
||||
wrap_err!(Config::profiles().latest().get_item(&index))?
|
||||
.file
|
||||
.clone()
|
||||
.ok_or("the file field is null")
|
||||
}?;
|
||||
|
||||
let path = wrap_err!(dirs::app_profiles_dir())?.join(file);
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
wrap_err!(help::open_file(path))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(index: String) -> CmdResult<String> {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
// check the profile type
|
||||
if let Some(typ) = item.itype.as_ref() {
|
||||
if *typ != "remote" {
|
||||
ret_err!(format!("could not update the `{typ}` profile"));
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(index: String, file_data: Option<String>) -> CmdResult {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if item.url.is_none() {
|
||||
ret_err!("failed to get the item url");
|
||||
}
|
||||
|
||||
(item.url.clone().unwrap(), item.option.clone())
|
||||
};
|
||||
|
||||
let fetch_opt = PrfOption::merge(opt, option);
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, fetch_opt).await)?;
|
||||
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.update_item(index.clone(), item))?;
|
||||
|
||||
// reactivate the profile
|
||||
if Some(index) == profiles.get_current() {
|
||||
let clash = clash_state.0.lock().unwrap();
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
}
|
||||
|
||||
/// change the current profile
|
||||
#[tauri::command]
|
||||
pub fn select_profile(
|
||||
index: String,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.put_current(index))?;
|
||||
|
||||
let clash = clash_state.0.lock().unwrap();
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||
pub fn get_clash_info() -> CmdResult<ClashInfo> {
|
||||
Ok(Config::clash().latest().get_client_info())
|
||||
}
|
||||
|
||||
/// change the profile chain
|
||||
#[tauri::command]
|
||||
pub fn change_profile_chain(
|
||||
chain: Option<Vec<String>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
profiles.put_chain(chain);
|
||||
clash.set_window(app_handle.get_window("main"));
|
||||
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||
pub fn get_runtime_config() -> CmdResult<Option<Mapping>> {
|
||||
Ok(Config::runtime().latest().config.clone())
|
||||
}
|
||||
|
||||
/// change the profile valid fields
|
||||
#[tauri::command]
|
||||
pub fn change_profile_valid(
|
||||
valid: Option<Vec<String>>,
|
||||
app_handle: tauri::AppHandle,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
profiles.put_valid(valid);
|
||||
clash.set_window(app_handle.get_window("main"));
|
||||
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||
pub fn get_runtime_yaml() -> CmdResult<String> {
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime.config.as_ref();
|
||||
wrap_err!(config
|
||||
.ok_or(anyhow::anyhow!("failed to parse config to yaml file"))
|
||||
.and_then(
|
||||
|config| serde_yaml::to_string(config).context("failed to convert config to yaml")
|
||||
))
|
||||
}
|
||||
|
||||
/// manually exec enhanced profile
|
||||
#[tauri::command]
|
||||
pub fn enhance_profiles(
|
||||
app_handle: tauri::AppHandle,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
clash.set_window(app_handle.get_window("main"));
|
||||
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||
pub fn get_runtime_exists() -> CmdResult<Vec<String>> {
|
||||
Ok(Config::runtime().latest().exists_keys.clone())
|
||||
}
|
||||
|
||||
/// delete profile item
|
||||
#[tauri::command]
|
||||
pub fn delete_profile(
|
||||
index: String,
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
if wrap_err!(profiles.delete_item(index))? {
|
||||
let clash = clash_state.0.lock().unwrap();
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
pub fn get_runtime_logs() -> CmdResult<HashMap<String, Vec<(String, String)>>> {
|
||||
Ok(Config::runtime().latest().chain_logs.clone())
|
||||
}
|
||||
|
||||
/// patch the profile config
|
||||
#[tauri::command]
|
||||
pub fn patch_profile(
|
||||
index: String,
|
||||
profile: PrfItem,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.patch_item(index, profile))
|
||||
pub async fn patch_clash_config(payload: Mapping) -> CmdResult {
|
||||
wrap_err!(feat::patch_clash(payload).await)
|
||||
}
|
||||
|
||||
/// run vscode command to edit the profile
|
||||
#[tauri::command]
|
||||
pub fn view_profile(index: String, profiles_state: State<'_, ProfilesState>) -> Result<(), String> {
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
|
||||
let file = item.file.clone();
|
||||
if file.is_none() {
|
||||
ret_err!("the file is null");
|
||||
}
|
||||
|
||||
let path = dirs::app_profiles_dir().join(file.unwrap());
|
||||
if !path.exists() {
|
||||
ret_err!("the file not found");
|
||||
}
|
||||
|
||||
// use vscode first
|
||||
if let Ok(code) = which::which("code") {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
if let Err(err) = Command::new(code)
|
||||
.creation_flags(0x08000000)
|
||||
.arg(path)
|
||||
.spawn()
|
||||
{
|
||||
log::error!("failed to open file by VScode for {err}");
|
||||
return Err("failed to open file by VScode".into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err(err) = Command::new(code).arg(path).spawn() {
|
||||
log::error!("failed to open file by VScode for {err}");
|
||||
return Err("failed to open file by VScode".into());
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
wrap_err!(open::that(path))
|
||||
pub fn get_verge_config() -> CmdResult<IVerge> {
|
||||
Ok(Config::verge().data().clone())
|
||||
}
|
||||
|
||||
/// read the profile item file data
|
||||
#[tauri::command]
|
||||
pub fn read_profile_file(
|
||||
index: String,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<String, String> {
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
let data = wrap_err!(item.read_file())?;
|
||||
|
||||
Ok(data)
|
||||
pub async fn patch_verge_config(payload: IVerge) -> CmdResult {
|
||||
wrap_err!(feat::patch_verge(payload).await)
|
||||
}
|
||||
|
||||
/// save the profile item file data
|
||||
#[tauri::command]
|
||||
pub fn save_profile_file(
|
||||
index: String,
|
||||
file_data: Option<String>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
if file_data.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
wrap_err!(item.save_file(file_data.unwrap()))
|
||||
pub async fn change_clash_core(clash_core: Option<String>) -> CmdResult {
|
||||
wrap_err!(CoreManager::global().change_core(clash_core).await)
|
||||
}
|
||||
|
||||
/// restart the sidecar
|
||||
#[tauri::command]
|
||||
pub fn restart_sidecar(
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
wrap_err!(clash.restart_sidecar(&mut profiles))
|
||||
pub async fn restart_sidecar() -> CmdResult {
|
||||
wrap_err!(CoreManager::global().run_core().await)
|
||||
}
|
||||
|
||||
/// get the clash core info from the state
|
||||
/// the caller can also get the infomation by clash's api
|
||||
#[tauri::command]
|
||||
pub fn get_clash_info(clash_state: State<'_, ClashState>) -> Result<ClashInfo, String> {
|
||||
let clash = clash_state.0.lock().unwrap();
|
||||
Ok(clash.info.clone())
|
||||
}
|
||||
pub fn grant_permission(_core: String) -> CmdResult {
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
return wrap_err!(manager::grant_permission(_core));
|
||||
|
||||
/// update the clash core config
|
||||
/// after putting the change to the clash core
|
||||
/// then we should save the latest config
|
||||
#[tauri::command]
|
||||
pub fn patch_clash_config(
|
||||
payload: Mapping,
|
||||
clash_state: State<'_, ClashState>,
|
||||
verge_state: State<'_, VergeState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(clash.patch_config(payload, &mut verge, &mut profiles))
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
return Err("Unsupported target".into());
|
||||
}
|
||||
|
||||
/// get the system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_sys_proxy() -> Result<SysProxyConfig, String> {
|
||||
wrap_err!(SysProxyConfig::get_sys())
|
||||
pub fn get_sys_proxy() -> CmdResult<Mapping> {
|
||||
let current = wrap_err!(Sysproxy::get_system_proxy())?;
|
||||
|
||||
let mut map = Mapping::new();
|
||||
map.insert("enable".into(), current.enable.into());
|
||||
map.insert(
|
||||
"server".into(),
|
||||
format!("{}:{}", current.host, current.port).into(),
|
||||
);
|
||||
map.insert("bypass".into(), current.bypass.into());
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
/// get the current proxy config
|
||||
/// which may not the same as system proxy
|
||||
#[tauri::command]
|
||||
pub fn get_cur_proxy(verge_state: State<'_, VergeState>) -> Result<Option<SysProxyConfig>, String> {
|
||||
let verge = verge_state.0.lock().unwrap();
|
||||
Ok(verge.cur_sysproxy.clone())
|
||||
pub fn get_clash_logs() -> CmdResult<VecDeque<String>> {
|
||||
Ok(logger::Logger::global().get_log())
|
||||
}
|
||||
|
||||
/// get the verge config
|
||||
#[tauri::command]
|
||||
pub fn get_verge_config(verge_state: State<'_, VergeState>) -> Result<VergeConfig, String> {
|
||||
let verge = verge_state.0.lock().unwrap();
|
||||
let mut config = verge.config.clone();
|
||||
|
||||
if config.system_proxy_bypass.is_none() && verge.cur_sysproxy.is_some() {
|
||||
config.system_proxy_bypass = Some(verge.cur_sysproxy.clone().unwrap().bypass)
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
pub fn open_app_dir() -> CmdResult<()> {
|
||||
let app_dir = wrap_err!(dirs::app_home_dir())?;
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// patch the verge config
|
||||
/// this command only save the config and not responsible for other things
|
||||
#[tauri::command]
|
||||
pub fn patch_verge_config(
|
||||
payload: VergeConfig,
|
||||
app_handle: tauri::AppHandle,
|
||||
clash_state: State<'_, ClashState>,
|
||||
verge_state: State<'_, VergeState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let tun_mode = payload.enable_tun_mode.clone();
|
||||
let system_proxy = payload.enable_system_proxy.clone();
|
||||
pub fn open_core_dir() -> CmdResult<()> {
|
||||
let core_dir = wrap_err!(tauri::utils::platform::current_exe())?;
|
||||
let core_dir = core_dir.parent().ok_or(format!("failed to get core dir"))?;
|
||||
wrap_err!(open::that(core_dir))
|
||||
}
|
||||
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
wrap_err!(verge.patch_config(payload))?;
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> CmdResult<()> {
|
||||
let log_dir = wrap_err!(dirs::app_logs_dir())?;
|
||||
wrap_err!(open::that(log_dir))
|
||||
}
|
||||
|
||||
// change tun mode
|
||||
if tun_mode.is_some() {
|
||||
#[cfg(target_os = "windows")]
|
||||
if *tun_mode.as_ref().unwrap() {
|
||||
let wintun_dll = dirs::app_home_dir().join("wintun.dll");
|
||||
if !wintun_dll.exists() {
|
||||
log::error!("failed to enable TUN for missing `wintun.dll`");
|
||||
return Err("failed to enable TUN for missing `wintun.dll`".into());
|
||||
}
|
||||
#[tauri::command]
|
||||
pub fn open_web_url(url: String) -> CmdResult<()> {
|
||||
wrap_err!(open::that(url))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
use crate::core::win_service;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
|
||||
wrap_err!(win_service::check_service().await)
|
||||
}
|
||||
|
||||
let clash = clash_state.0.lock().unwrap();
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
wrap_err!(win_service::install_service().await)
|
||||
}
|
||||
|
||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))?;
|
||||
}
|
||||
|
||||
// change system tray
|
||||
if system_proxy.is_some() || tun_mode.is_some() {
|
||||
verge.update_systray(&app_handle).unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
wrap_err!(win_service::uninstall_service().await)
|
||||
}
|
||||
}
|
||||
|
||||
/// kill all sidecars when update app
|
||||
#[tauri::command]
|
||||
pub fn kill_sidecars() {
|
||||
api::process::kill_children();
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
pub mod service {
|
||||
use super::*;
|
||||
|
||||
/// open app config dir
|
||||
#[tauri::command]
|
||||
pub fn open_app_dir() -> Result<(), String> {
|
||||
let app_dir = dirs::app_home_dir();
|
||||
wrap_err!(open::that(app_dir))
|
||||
}
|
||||
|
||||
/// open logs dir
|
||||
#[tauri::command]
|
||||
pub fn open_logs_dir() -> Result<(), String> {
|
||||
let log_dir = dirs::app_logs_dir();
|
||||
wrap_err!(open::that(log_dir))
|
||||
#[tauri::command]
|
||||
pub async fn check_service() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn install_service() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
#[tauri::command]
|
||||
pub async fn uninstall_service() -> CmdResult {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct IClashTemp(pub Mapping);
|
||||
|
||||
impl IClashTemp {
|
||||
pub fn new() -> Self {
|
||||
match dirs::clash_path().and_then(|path| help::read_merge_mapping(&path)) {
|
||||
Ok(map) => Self(Self::guard(map)),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
let mut map = Mapping::new();
|
||||
|
||||
map.insert("mixed-port".into(), 7890.into());
|
||||
map.insert("log-level".into(), "info".into());
|
||||
map.insert("allow-lan".into(), false.into());
|
||||
map.insert("mode".into(), "rule".into());
|
||||
map.insert("external-controller".into(), "127.0.0.1:9090".into());
|
||||
map.insert("secret".into(), "".into());
|
||||
#[cfg(feature = "default-meta")]
|
||||
map.insert("unified-delay".into(), true.into());
|
||||
#[cfg(feature = "default-meta")]
|
||||
map.insert("tcp-concurrent".into(), true.into());
|
||||
map.insert("ipv6".into(), false.into());
|
||||
|
||||
Self(map)
|
||||
}
|
||||
|
||||
fn guard(mut config: Mapping) -> Mapping {
|
||||
let port = Self::guard_mixed_port(&config);
|
||||
let ctrl = Self::guard_server_ctrl(&config);
|
||||
|
||||
config.insert("mixed-port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
config
|
||||
}
|
||||
|
||||
pub fn patch_config(&mut self, patch: Mapping) {
|
||||
for (key, value) in patch.into_iter() {
|
||||
self.0.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_config(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::clash_path()?,
|
||||
&self.0,
|
||||
Some("# Generated by Clash Nyanpasu"),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_mixed_port(&self) -> u16 {
|
||||
Self::guard_mixed_port(&self.0)
|
||||
}
|
||||
|
||||
pub fn get_client_info(&self) -> ClashInfo {
|
||||
let config = &self.0;
|
||||
|
||||
ClashInfo {
|
||||
port: Self::guard_mixed_port(&config),
|
||||
server: Self::guard_client_ctrl(&config),
|
||||
secret: config.get("secret").and_then(|value| match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||
Value::Number(val_num) => Some(val_num.to_string()),
|
||||
_ => None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn guard_mixed_port(config: &Mapping) -> u16 {
|
||||
let mut port = config
|
||||
.get("mixed-port")
|
||||
.and_then(|value| match value {
|
||||
Value::String(val_str) => val_str.parse().ok(),
|
||||
Value::Number(val_num) => val_num.as_u64().map(|u| u as u16),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(7890);
|
||||
if port == 0 {
|
||||
port = 7890;
|
||||
}
|
||||
port
|
||||
}
|
||||
|
||||
pub fn guard_server_ctrl(config: &Mapping) -> String {
|
||||
config
|
||||
.get("external-controller")
|
||||
.and_then(|value| match value.as_str() {
|
||||
Some(val_str) => {
|
||||
let val_str = val_str.trim();
|
||||
|
||||
let val = match val_str.starts_with(":") {
|
||||
true => format!("127.0.0.1{val_str}"),
|
||||
false => val_str.to_owned(),
|
||||
};
|
||||
|
||||
SocketAddr::from_str(val.as_str())
|
||||
.ok()
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
.unwrap_or("127.0.0.1:9090".into())
|
||||
}
|
||||
|
||||
pub fn guard_client_ctrl(config: &Mapping) -> String {
|
||||
let value = Self::guard_server_ctrl(config);
|
||||
match SocketAddr::from_str(value.as_str()) {
|
||||
Ok(mut socket) => {
|
||||
if socket.ip().is_unspecified() {
|
||||
socket.set_ip(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
|
||||
}
|
||||
socket.to_string()
|
||||
}
|
||||
Err(_) => "127.0.0.1:9090".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct ClashInfo {
|
||||
/// clash core port
|
||||
pub port: u16,
|
||||
/// same as `external-controller`
|
||||
pub server: String,
|
||||
/// clash secret
|
||||
pub secret: Option<String>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clash_info() {
|
||||
fn get_case<T: Into<Value>, D: Into<Value>>(mp: T, ec: D) -> ClashInfo {
|
||||
let mut map = Mapping::new();
|
||||
map.insert("mixed-port".into(), mp.into());
|
||||
map.insert("external-controller".into(), ec.into());
|
||||
|
||||
IClashTemp(IClashTemp::guard(map)).get_client_info()
|
||||
}
|
||||
|
||||
fn get_result<S: Into<String>>(port: u16, server: S) -> ClashInfo {
|
||||
ClashInfo {
|
||||
port,
|
||||
server: server.into(),
|
||||
secret: None,
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
IClashTemp(IClashTemp::guard(Mapping::new())).get_client_info(),
|
||||
get_result(7890, "127.0.0.1:9090")
|
||||
);
|
||||
|
||||
assert_eq!(get_case("", ""), get_result(7890, "127.0.0.1:9090"));
|
||||
|
||||
assert_eq!(get_case(65537, ""), get_result(1, "127.0.0.1:9090"));
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, "127.0.0.1:8888"),
|
||||
get_result(8888, "127.0.0.1:8888")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, " :98888 "),
|
||||
get_result(8888, "127.0.0.1:9090")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, "0.0.0.0:8080 "),
|
||||
get_result(8888, "127.0.0.1:8080")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, "0.0.0.0:8080"),
|
||||
get_result(8888, "127.0.0.1:8080")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, "[::]:8080"),
|
||||
get_result(8888, "127.0.0.1:8080")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, "192.168.1.1:8080"),
|
||||
get_result(8888, "192.168.1.1:8080")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
get_case(8888, "192.168.1.1:80800"),
|
||||
get_result(8888, "127.0.0.1:9090")
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClash {
|
||||
pub mixed_port: Option<u16>,
|
||||
pub allow_lan: Option<bool>,
|
||||
pub log_level: Option<String>,
|
||||
pub ipv6: Option<bool>,
|
||||
pub mode: Option<String>,
|
||||
pub external_controller: Option<String>,
|
||||
pub secret: Option<String>,
|
||||
pub dns: Option<IClashDNS>,
|
||||
pub tun: Option<IClashTUN>,
|
||||
pub interface_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashTUN {
|
||||
pub enable: Option<bool>,
|
||||
pub stack: Option<String>,
|
||||
pub auto_route: Option<bool>,
|
||||
pub auto_detect_interface: Option<bool>,
|
||||
pub dns_hijack: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashDNS {
|
||||
pub enable: Option<bool>,
|
||||
pub listen: Option<String>,
|
||||
pub default_nameserver: Option<Vec<String>>,
|
||||
pub enhanced_mode: Option<String>,
|
||||
pub fake_ip_range: Option<String>,
|
||||
pub use_hosts: Option<bool>,
|
||||
pub fake_ip_filter: Option<Vec<String>>,
|
||||
pub nameserver: Option<Vec<String>>,
|
||||
pub fallback: Option<Vec<String>>,
|
||||
pub fallback_filter: Option<IClashFallbackFilter>,
|
||||
pub nameserver_policy: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct IClashFallbackFilter {
|
||||
pub geoip: Option<bool>,
|
||||
pub geoip_code: Option<String>,
|
||||
pub ipcidr: Option<Vec<String>>,
|
||||
pub domain: Option<Vec<String>>,
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use super::{Draft, IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use crate::{
|
||||
enhance,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::{env::temp_dir, path::PathBuf};
|
||||
|
||||
pub const RUNTIME_CONFIG: &str = "clash-verge.yaml";
|
||||
pub const CHECK_CONFIG: &str = "clash-verge-check.yaml";
|
||||
|
||||
pub struct Config {
|
||||
clash_config: Draft<IClashTemp>,
|
||||
verge_config: Draft<IVerge>,
|
||||
profiles_config: Draft<IProfiles>,
|
||||
runtime_config: Draft<IRuntime>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn global() -> &'static Config {
|
||||
static CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
CONFIG.get_or_init(|| Config {
|
||||
clash_config: Draft::from(IClashTemp::new()),
|
||||
verge_config: Draft::from(IVerge::new()),
|
||||
profiles_config: Draft::from(IProfiles::new()),
|
||||
runtime_config: Draft::from(IRuntime::new()),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn clash() -> Draft<IClashTemp> {
|
||||
Self::global().clash_config.clone()
|
||||
}
|
||||
|
||||
pub fn verge() -> Draft<IVerge> {
|
||||
Self::global().verge_config.clone()
|
||||
}
|
||||
|
||||
pub fn profiles() -> Draft<IProfiles> {
|
||||
Self::global().profiles_config.clone()
|
||||
}
|
||||
|
||||
pub fn runtime() -> Draft<IRuntime> {
|
||||
Self::global().runtime_config.clone()
|
||||
}
|
||||
|
||||
/// 初始化配置
|
||||
pub fn init_config() -> Result<()> {
|
||||
crate::log_err!(Self::generate());
|
||||
if let Err(err) = Self::generate_file(ConfigType::Run) {
|
||||
log::error!(target: "app", "{err}");
|
||||
|
||||
let runtime_path = dirs::app_home_dir()?.join(RUNTIME_CONFIG);
|
||||
// 如果不存在就将默认的clash文件拿过来
|
||||
if !runtime_path.exists() {
|
||||
help::save_yaml(
|
||||
&runtime_path,
|
||||
&Config::clash().latest().0,
|
||||
Some("# Clash Nyanpasu Runtime"),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 将配置丢到对应的文件中
|
||||
pub fn generate_file(typ: ConfigType) -> Result<PathBuf> {
|
||||
let path = match typ {
|
||||
ConfigType::Run => dirs::app_home_dir()?.join(RUNTIME_CONFIG),
|
||||
ConfigType::Check => temp_dir().join(CHECK_CONFIG),
|
||||
};
|
||||
|
||||
let runtime = Config::runtime();
|
||||
let runtime = runtime.latest();
|
||||
let config = runtime
|
||||
.config
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("failed to get runtime config"))?;
|
||||
|
||||
help::save_yaml(&path, &config, Some("# Generated by Clash Nyanpasu"))?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// 生成配置存好
|
||||
pub fn generate() -> Result<()> {
|
||||
let (config, exists_keys, logs) = enhance::enhance();
|
||||
|
||||
*Config::runtime().draft() = IRuntime {
|
||||
config: Some(config),
|
||||
exists_keys,
|
||||
chain_logs: logs,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ConfigType {
|
||||
Run,
|
||||
Check,
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
use super::{IClashTemp, IProfiles, IRuntime, IVerge};
|
||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Draft<T: Clone + ToOwned> {
|
||||
inner: Arc<Mutex<(T, Option<T>)>>,
|
||||
}
|
||||
|
||||
macro_rules! draft_define {
|
||||
($id: ident) => {
|
||||
impl Draft<$id> {
|
||||
#[allow(unused)]
|
||||
pub fn data(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
|
||||
}
|
||||
|
||||
pub fn latest(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
&mut inner.0
|
||||
} else {
|
||||
inner.1.as_mut().unwrap()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> MappedMutexGuard<$id> {
|
||||
MutexGuard::map(self.inner.lock(), |inner| {
|
||||
if inner.1.is_none() {
|
||||
inner.1 = Some(inner.0.clone());
|
||||
}
|
||||
|
||||
inner.1.as_mut().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply(&self) -> Option<$id> {
|
||||
let mut inner = self.inner.lock();
|
||||
|
||||
match inner.1.take() {
|
||||
Some(draft) => {
|
||||
let old_value = inner.0.to_owned();
|
||||
inner.0 = draft.to_owned();
|
||||
Some(old_value)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discard(&self) -> Option<$id> {
|
||||
let mut inner = self.inner.lock();
|
||||
inner.1.take()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<$id> for Draft<$id> {
|
||||
fn from(data: $id) -> Self {
|
||||
Draft {
|
||||
inner: Arc::new(Mutex::new((data, None))),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// draft_define!(IClash);
|
||||
draft_define!(IClashTemp);
|
||||
draft_define!(IProfiles);
|
||||
draft_define!(IRuntime);
|
||||
draft_define!(IVerge);
|
||||
|
||||
#[test]
|
||||
fn test_draft() {
|
||||
let verge = IVerge {
|
||||
enable_auto_launch: Some(true),
|
||||
enable_tun_mode: Some(false),
|
||||
..IVerge::default()
|
||||
};
|
||||
|
||||
let draft = Draft::from(verge);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(false));
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(false);
|
||||
d.enable_tun_mode = Some(true);
|
||||
drop(d);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(true));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
|
||||
assert_eq!(draft.latest().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.latest().enable_tun_mode, Some(true));
|
||||
|
||||
assert!(draft.apply().is_some());
|
||||
assert!(draft.apply().is_none());
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.data().enable_tun_mode, Some(true));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
assert_eq!(draft.draft().enable_tun_mode, Some(true));
|
||||
|
||||
let mut d = draft.draft();
|
||||
d.enable_auto_launch = Some(true);
|
||||
drop(d);
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(true));
|
||||
|
||||
assert!(draft.discard().is_some());
|
||||
|
||||
assert_eq!(draft.data().enable_auto_launch, Some(false));
|
||||
|
||||
assert!(draft.discard().is_none());
|
||||
|
||||
assert_eq!(draft.draft().enable_auto_launch, Some(false));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
mod clash;
|
||||
mod config;
|
||||
mod draft;
|
||||
mod prfitem;
|
||||
mod profiles;
|
||||
mod runtime;
|
||||
mod verge;
|
||||
|
||||
pub use self::clash::*;
|
||||
pub use self::config::*;
|
||||
pub use self::draft::*;
|
||||
pub use self::prfitem::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::runtime::*;
|
||||
pub use self::verge::*;
|
||||
@@ -0,0 +1,374 @@
|
||||
use crate::utils::{dirs, help, tmpl};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use reqwest::StatusCode;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::fs;
|
||||
use sysproxy::Sysproxy;
|
||||
|
||||
use super::Config;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PrfItem {
|
||||
pub uid: Option<String>,
|
||||
|
||||
/// profile item type
|
||||
/// enum value: remote | local | script | merge
|
||||
#[serde(rename = "type")]
|
||||
pub itype: Option<String>,
|
||||
|
||||
/// profile name
|
||||
pub name: Option<String>,
|
||||
|
||||
/// profile file
|
||||
pub file: Option<String>,
|
||||
|
||||
/// profile description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub desc: Option<String>,
|
||||
|
||||
/// source url
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
|
||||
/// selected information
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<Vec<PrfSelected>>,
|
||||
|
||||
/// subscription user info
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra: Option<PrfExtra>,
|
||||
|
||||
/// updated time
|
||||
pub updated: Option<usize>,
|
||||
|
||||
/// some options of the item
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub option: Option<PrfOption>,
|
||||
|
||||
/// the file data
|
||||
#[serde(skip)]
|
||||
pub file_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PrfSelected {
|
||||
pub name: Option<String>,
|
||||
pub now: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct PrfExtra {
|
||||
pub upload: usize,
|
||||
pub download: usize,
|
||||
pub total: usize,
|
||||
pub expire: usize,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct PrfOption {
|
||||
/// for `remote` profile's http request
|
||||
/// see issue #13
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_agent: Option<String>,
|
||||
|
||||
/// for `remote` profile
|
||||
/// use system proxy
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub with_proxy: Option<bool>,
|
||||
|
||||
/// for `remote` profile
|
||||
/// use self proxy
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub self_proxy: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub update_interval: Option<u64>,
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||
match (one, other) {
|
||||
(Some(mut a), Some(b)) => {
|
||||
a.user_agent = b.user_agent.or(a.user_agent);
|
||||
a.with_proxy = b.with_proxy.or(a.with_proxy);
|
||||
a.self_proxy = b.self_proxy.or(a.self_proxy);
|
||||
a.update_interval = b.update_interval.or(a.update_interval);
|
||||
Some(a)
|
||||
}
|
||||
t @ _ => t.0.or(t.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrfItem {
|
||||
fn default() -> Self {
|
||||
PrfItem {
|
||||
uid: None,
|
||||
itype: None,
|
||||
name: None,
|
||||
desc: None,
|
||||
file: None,
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
updated: None,
|
||||
option: None,
|
||||
file_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
|
||||
match item.itype.unwrap().as_str() {
|
||||
"remote" => {
|
||||
if item.url.is_none() {
|
||||
bail!("url should not be null");
|
||||
}
|
||||
let url = item.url.as_ref().unwrap().as_str();
|
||||
let name = item.name;
|
||||
let desc = item.desc;
|
||||
PrfItem::from_url(url, name, desc, item.option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.unwrap_or("Local File".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_local(name, desc, file_data)
|
||||
}
|
||||
"merge" => {
|
||||
let name = item.name.unwrap_or("Merge".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_merge(name, desc)
|
||||
}
|
||||
"script" => {
|
||||
let name = item.name.unwrap_or("Script".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_script(name, desc)
|
||||
}
|
||||
typ @ _ => bail!("invalid profile item type \"{typ}\""),
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Local type
|
||||
/// create a new item from name/desc
|
||||
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("l");
|
||||
let file = format!("{uid}.yaml");
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
name: Some(name),
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Remote type
|
||||
/// create a new item from url
|
||||
pub async fn from_url(
|
||||
url: &str,
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
option: Option<PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let opt_ref = option.as_ref();
|
||||
let with_proxy = opt_ref.map_or(false, |o| o.with_proxy.unwrap_or(false));
|
||||
let self_proxy = opt_ref.map_or(false, |o| o.self_proxy.unwrap_or(false));
|
||||
let user_agent = opt_ref.map_or(None, |o| o.user_agent.clone());
|
||||
|
||||
let mut builder = reqwest::ClientBuilder::new().use_rustls_tls().no_proxy();
|
||||
|
||||
// 使用软件自己的代理
|
||||
if self_proxy {
|
||||
let port = Config::clash().data().get_mixed_port();
|
||||
|
||||
let proxy_scheme = format!("http://127.0.0.1:{port}");
|
||||
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
// 使用系统代理
|
||||
else if with_proxy {
|
||||
match Sysproxy::get_system_proxy() {
|
||||
Ok(p @ Sysproxy { enable: true, .. }) => {
|
||||
let proxy_scheme = format!("http://{}:{}", p.host, p.port);
|
||||
|
||||
if let Ok(proxy) = reqwest::Proxy::http(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::https(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
if let Ok(proxy) = reqwest::Proxy::all(&proxy_scheme) {
|
||||
builder = builder.proxy(proxy);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
let version = unsafe { dirs::APP_VERSION };
|
||||
let version = format!("clash-verge/{version}");
|
||||
builder = builder.user_agent(user_agent.unwrap_or(version));
|
||||
|
||||
let resp = builder.build()?.get(url).send().await?;
|
||||
|
||||
let status_code = resp.status();
|
||||
if !StatusCode::is_success(&status_code) {
|
||||
bail!("failed to fetch remote profile with status {status_code}")
|
||||
}
|
||||
|
||||
let header = resp.headers();
|
||||
|
||||
// parse the Subscription UserInfo
|
||||
let extra = match header.get("Subscription-Userinfo") {
|
||||
Some(value) => {
|
||||
let sub_info = value.to_str().unwrap_or("");
|
||||
|
||||
Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// parse the Content-Disposition
|
||||
let filename = match header.get("Content-Disposition") {
|
||||
Some(value) => {
|
||||
let filename = value.to_str().unwrap_or("");
|
||||
help::parse_str::<String>(filename, "filename=")
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// parse the profile-update-interval
|
||||
let option = match header.get("profile-update-interval") {
|
||||
Some(value) => match value.to_str().unwrap_or("").parse::<u64>() {
|
||||
Ok(val) => Some(PrfOption {
|
||||
update_interval: Some(val * 60), // hour -> min
|
||||
..PrfOption::default()
|
||||
}),
|
||||
Err(_) => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
let uid = help::get_uid("r");
|
||||
let file = format!("{uid}.yaml");
|
||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||
let data = resp.text_with_charset("utf-8").await?;
|
||||
|
||||
// process the charset "UTF-8 with BOM"
|
||||
let data = data.trim_start_matches('\u{feff}');
|
||||
|
||||
// check the data whether the valid yaml format
|
||||
let yaml = serde_yaml::from_str::<Mapping>(data)
|
||||
.context("the remote profile data is invalid yaml")?;
|
||||
|
||||
if !yaml.contains_key("proxies") && !yaml.contains_key("proxy-providers") {
|
||||
bail!("profile does not contain `proxies` or `proxy-providers`");
|
||||
}
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
name: Some(name),
|
||||
desc,
|
||||
file: Some(file),
|
||||
url: Some(url.into()),
|
||||
selected: None,
|
||||
extra,
|
||||
option,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(data.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Merge type (enhance)
|
||||
/// create the enhanced item by using `merge` rule
|
||||
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("m");
|
||||
let file = format!("{uid}.yaml");
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("merge".into()),
|
||||
name: Some(name),
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_MERGE.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Script type (enhance)
|
||||
/// create the enhanced item by using javascript quick.js
|
||||
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("s");
|
||||
let file = format!("{uid}.js"); // js ext
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("script".into()),
|
||||
name: Some(name),
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// get the file data
|
||||
pub fn read_file(&self) -> Result<String> {
|
||||
if self.file.is_none() {
|
||||
bail!("could not find the file");
|
||||
}
|
||||
|
||||
let file = self.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir()?.join(file);
|
||||
fs::read_to_string(path).context("failed to read the file")
|
||||
}
|
||||
|
||||
/// save the file data
|
||||
pub fn save_file(&self, data: String) -> Result<()> {
|
||||
if self.file.is_none() {
|
||||
bail!("could not find the file");
|
||||
}
|
||||
|
||||
let file = self.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir()?.join(file);
|
||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
use super::prfitem::PrfItem;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, io::Write};
|
||||
|
||||
/// Define the `profiles.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IProfiles {
|
||||
/// same as PrfConfig.current
|
||||
pub current: Option<String>,
|
||||
|
||||
/// same as PrfConfig.chain
|
||||
pub chain: Option<Vec<String>>,
|
||||
|
||||
/// record valid fields for clash
|
||||
pub valid: Option<Vec<String>>,
|
||||
|
||||
/// profile list
|
||||
pub items: Option<Vec<PrfItem>>,
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl IProfiles {
|
||||
pub fn new() -> Self {
|
||||
match dirs::profiles_path().and_then(|path| help::read_yaml::<Self>(&path)) {
|
||||
Ok(mut profiles) => {
|
||||
if profiles.items.is_none() {
|
||||
profiles.items = Some(vec![]);
|
||||
}
|
||||
// compatible with the old old old version
|
||||
profiles.items.as_mut().map(|items| {
|
||||
for item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
}
|
||||
});
|
||||
profiles
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
valid: Some(vec![
|
||||
"dns".into(),
|
||||
"unified-delay".into(),
|
||||
"tcp-concurrent".into(),
|
||||
]),
|
||||
items: Some(vec![]),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(
|
||||
&dirs::profiles_path()?,
|
||||
self,
|
||||
Some("# Profiles Config for Clash Nyanpasu"),
|
||||
)
|
||||
}
|
||||
|
||||
/// 只修改current,valid和chain
|
||||
pub fn patch_config(&mut self, patch: IProfiles) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
if let Some(current) = patch.current {
|
||||
let items = self.items.as_ref().unwrap();
|
||||
let some_uid = Some(current);
|
||||
|
||||
if items.iter().any(|e| e.uid == some_uid) {
|
||||
self.current = some_uid;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chain) = patch.chain {
|
||||
self.chain = Some(chain);
|
||||
}
|
||||
|
||||
if let Some(valid) = patch.valid {
|
||||
self.valid = Some(valid);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_current(&self) -> Option<String> {
|
||||
self.current.clone()
|
||||
}
|
||||
|
||||
/// get items ref
|
||||
pub fn get_items(&self) -> Option<&Vec<PrfItem>> {
|
||||
self.items.as_ref()
|
||||
}
|
||||
|
||||
/// find the item by the uid
|
||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
||||
if let Some(items) = self.items.as_ref() {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for each in items.iter() {
|
||||
if each.uid == some_uid {
|
||||
return Ok(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to get the profile item \"uid:{uid}\"");
|
||||
}
|
||||
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// then should save the data to file
|
||||
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
||||
if item.uid.is_none() {
|
||||
bail!("the uid should not be null");
|
||||
}
|
||||
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
if item.file.is_none() {
|
||||
bail!("the file should not be null");
|
||||
}
|
||||
|
||||
let file = item.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
self.items.as_mut().map(|items| items.push(item));
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// update the item value
|
||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file();
|
||||
}
|
||||
}
|
||||
|
||||
self.items = Some(items);
|
||||
bail!("failed to find the profile item \"uid:{uid}\"")
|
||||
}
|
||||
|
||||
/// be used to update the remote item
|
||||
/// only patch `updated` `extra` `file_data`
|
||||
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
// find the item
|
||||
let _ = self.get_item(&uid)?;
|
||||
|
||||
if let Some(items) = self.items.as_mut() {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for each in items.iter_mut() {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
let file = each.file.take();
|
||||
let file =
|
||||
file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
|
||||
|
||||
// the file must exists
|
||||
each.file = Some(file.clone());
|
||||
|
||||
let path = dirs::app_profiles_dir()?.join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.with_context(|| format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.with_context(|| format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// delete item
|
||||
/// if delete the current then return true
|
||||
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(&uid);
|
||||
let current = current.clone();
|
||||
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
let mut index = None;
|
||||
|
||||
// get the index
|
||||
for i in 0..items.len() {
|
||||
if items[i].uid == Some(uid.clone()) {
|
||||
index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = index {
|
||||
items.remove(index).file.map(|file| {
|
||||
let _ = dirs::app_profiles_dir().map(|path| {
|
||||
let path = path.join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// delete the original uid
|
||||
if current == uid {
|
||||
self.current = match items.len() > 0 {
|
||||
true => items[0].uid.clone(),
|
||||
false => None,
|
||||
};
|
||||
}
|
||||
|
||||
self.items = Some(items);
|
||||
self.save_file()?;
|
||||
Ok(current == uid)
|
||||
}
|
||||
|
||||
/// 获取current指向的配置内容
|
||||
pub fn current_mapping(&self) -> Result<Mapping> {
|
||||
match (self.current.as_ref(), self.items.as_ref()) {
|
||||
(Some(current), Some(items)) => {
|
||||
if let Some(item) = items.iter().find(|e| e.uid.as_ref() == Some(current)) {
|
||||
let file_path = match item.file.as_ref() {
|
||||
Some(file) => dirs::app_profiles_dir()?.join(file),
|
||||
None => bail!("failed to get the file field"),
|
||||
};
|
||||
return Ok(help::read_merge_mapping(&file_path)?);
|
||||
}
|
||||
bail!("failed to find the current profile \"uid:{current}\"");
|
||||
}
|
||||
_ => Ok(Mapping::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IRuntime {
|
||||
pub config: Option<Mapping>,
|
||||
// 记录在配置中(包括merge和script生成的)出现过的keys
|
||||
// 这些keys不一定都生效
|
||||
pub exists_keys: Vec<String>,
|
||||
pub chain_logs: HashMap<String, Vec<(String, String)>>,
|
||||
}
|
||||
|
||||
impl IRuntime {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
// 这里只更改 allow-lan | ipv6 | log-level
|
||||
pub fn patch_config(&mut self, patch: Mapping) {
|
||||
if let Some(config) = self.config.as_mut() {
|
||||
["allow-lan", "ipv6", "log-level"]
|
||||
.into_iter()
|
||||
.for_each(|key| {
|
||||
if let Some(value) = patch.get(key).to_owned() {
|
||||
config.insert(key.into(), value.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use log::LevelFilter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IVerge {
|
||||
/// app listening port for app singleton
|
||||
pub app_singleton_port: Option<u16>,
|
||||
|
||||
/// app log level
|
||||
/// silent | error | warn | info | debug | trace
|
||||
pub app_log_level: Option<String>,
|
||||
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
/// `light` or `dark` or `system`
|
||||
pub theme_mode: Option<String>,
|
||||
|
||||
/// enable blur mode
|
||||
/// maybe be able to set the alpha
|
||||
pub theme_blur: Option<bool>,
|
||||
|
||||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// show memory info (only for Clash Meta)
|
||||
pub enable_memory_usage: Option<bool>,
|
||||
|
||||
/// clash tun mode
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
|
||||
/// windows service mode
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enable_service_mode: Option<bool>,
|
||||
|
||||
/// can the app auto startup
|
||||
pub enable_auto_launch: Option<bool>,
|
||||
|
||||
/// not show the window on launch
|
||||
pub enable_silent_start: Option<bool>,
|
||||
|
||||
/// set system proxy
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
|
||||
/// enable proxy guard
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
|
||||
/// set system proxy bypass
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
|
||||
/// proxy guard duration
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
|
||||
/// theme setting
|
||||
pub theme_setting: Option<IVergeTheme>,
|
||||
|
||||
/// web ui list
|
||||
pub web_ui_list: Option<Vec<String>>,
|
||||
|
||||
/// clash core path
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub clash_core: Option<String>,
|
||||
|
||||
/// hotkey map
|
||||
/// format: {func},{key}
|
||||
pub hotkeys: Option<Vec<String>>,
|
||||
|
||||
/// 切换代理时自动关闭连接
|
||||
pub auto_close_connection: Option<bool>,
|
||||
|
||||
/// 默认的延迟测试连接
|
||||
pub default_latency_test: Option<String>,
|
||||
|
||||
/// 支持关闭字段过滤,避免meta的新字段都被过滤掉,默认为真
|
||||
pub enable_clash_fields: Option<bool>,
|
||||
|
||||
/// 是否使用内部的脚本支持,默认为真
|
||||
pub enable_builtin_enhanced: Option<bool>,
|
||||
|
||||
/// proxy 页面布局 列数
|
||||
pub proxy_layout_column: Option<i32>,
|
||||
|
||||
/// window size and position
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub window_size_position: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct IVergeTheme {
|
||||
pub primary_color: Option<String>,
|
||||
pub secondary_color: Option<String>,
|
||||
pub primary_text: Option<String>,
|
||||
pub secondary_text: Option<String>,
|
||||
|
||||
pub info_color: Option<String>,
|
||||
pub error_color: Option<String>,
|
||||
pub warning_color: Option<String>,
|
||||
pub success_color: Option<String>,
|
||||
|
||||
pub font_family: Option<String>,
|
||||
pub css_injection: Option<String>,
|
||||
}
|
||||
|
||||
impl IVerge {
|
||||
pub fn new() -> Self {
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{err}");
|
||||
Self::template()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn template() -> Self {
|
||||
Self {
|
||||
clash_core: match cfg!(feature = "default-meta") {
|
||||
false => Some("clash".into()),
|
||||
true => Some("clash-meta".into()),
|
||||
},
|
||||
language: match cfg!(feature = "default-meta") {
|
||||
false => Some("en".into()),
|
||||
true => Some("zh".into()),
|
||||
},
|
||||
theme_mode: Some("system".into()),
|
||||
theme_blur: Some(false),
|
||||
traffic_graph: Some(true),
|
||||
enable_memory_usage: Some(true),
|
||||
enable_auto_launch: Some(false),
|
||||
enable_silent_start: Some(false),
|
||||
enable_system_proxy: Some(false),
|
||||
enable_proxy_guard: Some(false),
|
||||
proxy_guard_duration: Some(30),
|
||||
auto_close_connection: Some(true),
|
||||
enable_builtin_enhanced: Some(true),
|
||||
enable_clash_fields: Some(true),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Save IVerge App Config
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
help::save_yaml(&dirs::verge_path()?, &self, Some("# Clash Nyanpasu Config"))
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
/// only save to file
|
||||
pub fn patch_config(&mut self, patch: IVerge) {
|
||||
macro_rules! patch {
|
||||
($key: tt) => {
|
||||
if patch.$key.is_some() {
|
||||
self.$key = patch.$key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
patch!(app_log_level);
|
||||
patch!(language);
|
||||
patch!(theme_mode);
|
||||
patch!(theme_blur);
|
||||
patch!(traffic_graph);
|
||||
patch!(enable_memory_usage);
|
||||
|
||||
patch!(enable_tun_mode);
|
||||
patch!(enable_service_mode);
|
||||
patch!(enable_auto_launch);
|
||||
patch!(enable_silent_start);
|
||||
patch!(enable_system_proxy);
|
||||
patch!(enable_proxy_guard);
|
||||
patch!(system_proxy_bypass);
|
||||
patch!(proxy_guard_duration);
|
||||
|
||||
patch!(theme_setting);
|
||||
patch!(web_ui_list);
|
||||
patch!(clash_core);
|
||||
patch!(hotkeys);
|
||||
|
||||
patch!(auto_close_connection);
|
||||
patch!(default_latency_test);
|
||||
patch!(enable_builtin_enhanced);
|
||||
patch!(proxy_layout_column);
|
||||
patch!(enable_clash_fields);
|
||||
|
||||
patch!(window_size_position);
|
||||
}
|
||||
|
||||
/// 在初始化前尝试拿到单例端口的值
|
||||
pub fn get_singleton_port() -> u16 {
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
const SERVER_PORT: u16 = 33331;
|
||||
#[cfg(feature = "verge-dev")]
|
||||
const SERVER_PORT: u16 = 11233;
|
||||
|
||||
match dirs::verge_path().and_then(|path| help::read_yaml::<IVerge>(&path)) {
|
||||
Ok(config) => config.app_singleton_port.unwrap_or(SERVER_PORT),
|
||||
Err(_) => SERVER_PORT, // 这里就不log错误了
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取日志等级
|
||||
pub fn get_log_level(&self) -> LevelFilter {
|
||||
if let Some(level) = self.app_log_level.as_ref() {
|
||||
match level.to_lowercase().as_str() {
|
||||
"silent" => LevelFilter::Off,
|
||||
"error" => LevelFilter::Error,
|
||||
"warn" => LevelFilter::Warn,
|
||||
"info" => LevelFilter::Info,
|
||||
"debug" => LevelFilter::Debug,
|
||||
"trace" => LevelFilter::Trace,
|
||||
_ => LevelFilter::Info,
|
||||
}
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,518 +0,0 @@
|
||||
use super::{PrfEnhancedResult, Profiles, Verge, VergeConfig};
|
||||
use crate::log_if_err;
|
||||
use crate::utils::{config, dirs, help};
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{collections::HashMap, time::Duration};
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use tauri::Window;
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ClashInfo {
|
||||
/// clash sidecar status
|
||||
pub status: String,
|
||||
|
||||
/// clash core port
|
||||
pub port: Option<String>,
|
||||
|
||||
/// same as `external-controller`
|
||||
pub server: Option<String>,
|
||||
|
||||
/// clash secret
|
||||
pub secret: Option<String>,
|
||||
}
|
||||
|
||||
pub struct Clash {
|
||||
/// maintain the clash config
|
||||
pub config: Mapping,
|
||||
|
||||
/// some info
|
||||
pub info: ClashInfo,
|
||||
|
||||
/// clash sidecar
|
||||
pub sidecar: Option<CommandChild>,
|
||||
|
||||
/// save the main window
|
||||
pub window: Option<Window>,
|
||||
}
|
||||
|
||||
impl Clash {
|
||||
pub fn new() -> Clash {
|
||||
let config = Clash::read_config();
|
||||
let info = Clash::get_info(&config);
|
||||
|
||||
Clash {
|
||||
config,
|
||||
info,
|
||||
sidecar: None,
|
||||
window: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// get clash config
|
||||
fn read_config() -> Mapping {
|
||||
config::read_yaml::<Mapping>(dirs::clash_path())
|
||||
}
|
||||
|
||||
/// save the clash config
|
||||
fn save_config(&self) -> Result<()> {
|
||||
config::save_yaml(
|
||||
dirs::clash_path(),
|
||||
&self.config,
|
||||
Some("# Default Config For Clash Core\n\n"),
|
||||
)
|
||||
}
|
||||
|
||||
/// parse the clash's config.yaml
|
||||
/// get some information
|
||||
fn get_info(clash_config: &Mapping) -> ClashInfo {
|
||||
let key_port_1 = Value::String("port".to_string());
|
||||
let key_port_2 = Value::String("mixed-port".to_string());
|
||||
let key_server = Value::String("external-controller".to_string());
|
||||
let key_secret = Value::String("secret".to_string());
|
||||
|
||||
let port = match clash_config.get(&key_port_1) {
|
||||
Some(value) => match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::Number(val_num) => Some(val_num.to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let port = match port {
|
||||
Some(_) => port,
|
||||
None => match clash_config.get(&key_port_2) {
|
||||
Some(value) => match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::Number(val_num) => Some(val_num.to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
},
|
||||
};
|
||||
|
||||
let server = match clash_config.get(&key_server) {
|
||||
Some(value) => match value {
|
||||
Value::String(val_str) => {
|
||||
// `external-controller` could be
|
||||
// "127.0.0.1:9090" or ":9090"
|
||||
// Todo: maybe it could support single port
|
||||
let server = val_str.clone();
|
||||
let server = match server.starts_with(":") {
|
||||
true => format!("127.0.0.1{server}"),
|
||||
false => server,
|
||||
};
|
||||
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
let secret = match clash_config.get(&key_secret) {
|
||||
Some(value) => match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::Bool(val_bool) => Some(val_bool.to_string()),
|
||||
Value::Number(val_num) => Some(val_num.to_string()),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
ClashInfo {
|
||||
status: "init".into(),
|
||||
port,
|
||||
server,
|
||||
secret,
|
||||
}
|
||||
}
|
||||
|
||||
/// save the main window
|
||||
pub fn set_window(&mut self, win: Option<Window>) {
|
||||
self.window = win;
|
||||
}
|
||||
|
||||
/// run clash sidecar
|
||||
pub fn run_sidecar(&mut self) -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir();
|
||||
let app_dir = app_dir.as_os_str().to_str().unwrap();
|
||||
|
||||
match Command::new_sidecar("clash") {
|
||||
Ok(cmd) => match cmd.args(["-d", app_dir]).spawn() {
|
||||
Ok((mut rx, cmd_child)) => {
|
||||
self.sidecar = Some(cmd_child);
|
||||
|
||||
// clash log
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => log::info!("[clash]: {}", line),
|
||||
CommandEvent::Stderr(err) => log::error!("[clash]: {}", err),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => bail!(err.to_string()),
|
||||
},
|
||||
Err(err) => bail!(err.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// drop clash sidecar
|
||||
pub fn drop_sidecar(&mut self) -> Result<()> {
|
||||
if let Some(sidecar) = self.sidecar.take() {
|
||||
sidecar.kill()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// restart clash sidecar
|
||||
/// should reactivate profile after restart
|
||||
pub fn restart_sidecar(&mut self, profiles: &mut Profiles) -> Result<()> {
|
||||
self.update_config();
|
||||
self.drop_sidecar()?;
|
||||
self.run_sidecar()?;
|
||||
self.activate(profiles)?;
|
||||
self.activate_enhanced(profiles, false, true)
|
||||
}
|
||||
|
||||
/// update the clash info
|
||||
pub fn update_config(&mut self) {
|
||||
self.config = Clash::read_config();
|
||||
self.info = Clash::get_info(&self.config);
|
||||
}
|
||||
|
||||
/// patch update the clash config
|
||||
pub fn patch_config(
|
||||
&mut self,
|
||||
patch: Mapping,
|
||||
verge: &mut Verge,
|
||||
profiles: &mut Profiles,
|
||||
) -> Result<()> {
|
||||
let mix_port_key = Value::from("mixed-port");
|
||||
let mut port = None;
|
||||
|
||||
for (key, value) in patch.into_iter() {
|
||||
let value = value.clone();
|
||||
|
||||
// check whether the mix_port is changed
|
||||
if key == mix_port_key {
|
||||
if value.is_number() {
|
||||
port = value.as_i64().as_ref().map(|n| n.to_string());
|
||||
} else {
|
||||
port = value.as_str().as_ref().map(|s| s.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
self.config.insert(key.clone(), value);
|
||||
}
|
||||
|
||||
self.save_config()?;
|
||||
|
||||
if let Some(port) = port {
|
||||
self.restart_sidecar(profiles)?;
|
||||
verge.init_sysproxy(Some(port));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// revise the `tun` and `dns` config
|
||||
fn _tun_mode(mut config: Mapping, enable: bool) -> Mapping {
|
||||
macro_rules! revise {
|
||||
($map: expr, $key: expr, $val: expr) => {
|
||||
let ret_key = Value::String($key.into());
|
||||
$map.insert(ret_key, Value::from($val));
|
||||
};
|
||||
}
|
||||
|
||||
// if key not exists then append value
|
||||
macro_rules! append {
|
||||
($map: expr, $key: expr, $val: expr) => {
|
||||
let ret_key = Value::String($key.into());
|
||||
if !$map.contains_key(&ret_key) {
|
||||
$map.insert(ret_key, Value::from($val));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// tun config
|
||||
let tun_val = config.get(&Value::from("tun"));
|
||||
let mut new_tun = Mapping::new();
|
||||
|
||||
if tun_val.is_some() && tun_val.as_ref().unwrap().is_mapping() {
|
||||
new_tun = tun_val.as_ref().unwrap().as_mapping().unwrap().clone();
|
||||
}
|
||||
|
||||
revise!(new_tun, "enable", enable);
|
||||
|
||||
if enable {
|
||||
append!(new_tun, "stack", "gvisor");
|
||||
append!(new_tun, "dns-hijack", vec!["198.18.0.2:53"]);
|
||||
append!(new_tun, "auto-route", true);
|
||||
append!(new_tun, "auto-detect-interface", true);
|
||||
}
|
||||
|
||||
revise!(config, "tun", new_tun);
|
||||
|
||||
// dns config
|
||||
let dns_val = config.get(&Value::from("dns"));
|
||||
let mut new_dns = Mapping::new();
|
||||
|
||||
if dns_val.is_some() && dns_val.as_ref().unwrap().is_mapping() {
|
||||
new_dns = dns_val.as_ref().unwrap().as_mapping().unwrap().clone();
|
||||
}
|
||||
|
||||
// 借鉴cfw的默认配置
|
||||
revise!(new_dns, "enable", enable);
|
||||
|
||||
if enable {
|
||||
append!(new_dns, "enhanced-mode", "fake-ip");
|
||||
append!(
|
||||
new_dns,
|
||||
"nameserver",
|
||||
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
|
||||
);
|
||||
append!(new_dns, "fallback", vec![] as Vec<&str>);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
append!(
|
||||
new_dns,
|
||||
"fake-ip-filter",
|
||||
vec![
|
||||
"dns.msftncsi.com",
|
||||
"www.msftncsi.com",
|
||||
"www.msftconnecttest.com"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
revise!(config, "dns", new_dns);
|
||||
config
|
||||
}
|
||||
|
||||
/// activate the profile
|
||||
/// generate a new profile to the temp_dir
|
||||
/// then put the path to the clash core
|
||||
fn _activate(info: ClashInfo, config: Mapping, window: Option<Window>) -> Result<()> {
|
||||
let verge_config = VergeConfig::new();
|
||||
let tun_enable = verge_config.enable_tun_mode.unwrap_or(false);
|
||||
|
||||
let config = Clash::_tun_mode(config, tun_enable);
|
||||
|
||||
let temp_path = dirs::profiles_temp_path();
|
||||
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let server = info.server.unwrap();
|
||||
let server = format!("http://{server}/configs");
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
|
||||
if let Some(secret) = info.secret.as_ref() {
|
||||
let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", temp_path.as_os_str().to_str().unwrap());
|
||||
|
||||
// retry 5 times
|
||||
for _ in 0..5 {
|
||||
match reqwest::ClientBuilder::new().no_proxy().build() {
|
||||
Ok(client) => {
|
||||
let builder = client.put(&server).headers(headers.clone()).json(&data);
|
||||
|
||||
match builder.send().await {
|
||||
Ok(resp) => {
|
||||
if resp.status() != 204 {
|
||||
log::error!("failed to activate clash for status \"{}\"", resp.status());
|
||||
}
|
||||
|
||||
// emit the window to update something
|
||||
if let Some(window) = window {
|
||||
window.emit("verge://refresh-clash-config", "yes").unwrap();
|
||||
}
|
||||
|
||||
// do not retry
|
||||
break;
|
||||
}
|
||||
Err(err) => log::error!("failed to activate for `{err}`"),
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!("failed to activate for `{err}`"),
|
||||
}
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// enhanced profiles mode
|
||||
/// - (sync) refresh config if enhance chain is null
|
||||
/// - (async) enhanced config
|
||||
pub fn activate_enhanced(&self, profiles: &Profiles, delay: bool, skip: bool) -> Result<()> {
|
||||
if self.window.is_none() {
|
||||
bail!("failed to get the main window");
|
||||
}
|
||||
|
||||
let event_name = help::get_uid("e");
|
||||
let event_name = format!("enhanced-cb-{event_name}");
|
||||
|
||||
// generate the payload
|
||||
let payload = profiles.gen_enhanced(event_name.clone())?;
|
||||
|
||||
let info = self.info.clone();
|
||||
|
||||
// do not run enhanced
|
||||
if payload.chain.len() == 0 {
|
||||
if skip {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut config = self.config.clone();
|
||||
let filter_data = Clash::strict_filter(payload.current);
|
||||
|
||||
for (key, value) in filter_data.into_iter() {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
return Clash::_activate(info, config, self.window.clone());
|
||||
}
|
||||
|
||||
let window = self.window.clone().unwrap();
|
||||
let window_move = self.window.clone();
|
||||
|
||||
window.once(&event_name, move |event| {
|
||||
if let Some(result) = event.payload() {
|
||||
let result: PrfEnhancedResult = serde_json::from_str(result).unwrap();
|
||||
|
||||
if let Some(data) = result.data {
|
||||
let mut config = Clash::read_config();
|
||||
let filter_data = Clash::loose_filter(data); // loose filter
|
||||
|
||||
for (key, value) in filter_data.into_iter() {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
log_if_err!(Clash::_activate(info, config, window_move));
|
||||
log::info!("profile enhanced status {}", result.status);
|
||||
}
|
||||
|
||||
result.error.map(|err| log::error!("{err}"));
|
||||
}
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// wait the window setup during resolve app
|
||||
if delay {
|
||||
sleep(Duration::from_secs(2)).await;
|
||||
}
|
||||
window.emit("script-handler", payload).unwrap();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// activate the profile
|
||||
/// auto activate enhanced profile
|
||||
pub fn activate(&self, profiles: &Profiles) -> Result<()> {
|
||||
let data = profiles.gen_activate()?;
|
||||
let data = Clash::strict_filter(data);
|
||||
|
||||
let info = self.info.clone();
|
||||
let mut config = self.config.clone();
|
||||
|
||||
for (key, value) in data.into_iter() {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
Clash::_activate(info, config, self.window.clone())
|
||||
}
|
||||
|
||||
/// only 5 default fields available (clash config fields)
|
||||
/// convert to lowercase
|
||||
fn strict_filter(config: Mapping) -> Mapping {
|
||||
// Only the following fields are allowed:
|
||||
// proxies/proxy-providers/proxy-groups/rule-providers/rules
|
||||
let valid_keys = vec![
|
||||
"proxies",
|
||||
"proxy-providers",
|
||||
"proxy-groups",
|
||||
"rules",
|
||||
"rule-providers",
|
||||
];
|
||||
|
||||
let mut new_config = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
key.as_str().map(|key_str| {
|
||||
// change to lowercase
|
||||
let mut key_str = String::from(key_str);
|
||||
key_str.make_ascii_lowercase();
|
||||
|
||||
// filter
|
||||
if valid_keys.contains(&&*key_str) {
|
||||
new_config.insert(Value::String(key_str), value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
new_config
|
||||
}
|
||||
|
||||
/// more clash config fields available
|
||||
/// convert to lowercase
|
||||
fn loose_filter(config: Mapping) -> Mapping {
|
||||
// all of these can not be revised by script or merge
|
||||
// http/https/socks port should be under control
|
||||
let not_allow = vec![
|
||||
"port",
|
||||
"socks-port",
|
||||
"mixed-port",
|
||||
"allow-lan",
|
||||
"mode",
|
||||
"external-controller",
|
||||
"secret",
|
||||
"log-level",
|
||||
];
|
||||
|
||||
let mut new_config = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
key.as_str().map(|key_str| {
|
||||
// change to lowercase
|
||||
let mut key_str = String::from(key_str);
|
||||
key_str.make_ascii_lowercase();
|
||||
|
||||
// filter
|
||||
if !not_allow.contains(&&*key_str) {
|
||||
new_config.insert(Value::String(key_str), value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
new_config
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Clash {
|
||||
fn default() -> Self {
|
||||
Clash::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Clash {
|
||||
fn drop(&mut self) {
|
||||
if let Err(err) = self.drop_sidecar() {
|
||||
log::error!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
use crate::config::Config;
|
||||
use anyhow::{bail, Result};
|
||||
use reqwest::header::HeaderMap;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// PUT /configs
|
||||
/// path 是绝对路径
|
||||
pub async fn put_configs(path: &str) -> Result<()> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/configs");
|
||||
|
||||
let mut data = HashMap::new();
|
||||
data.insert("path", path);
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.put(&url).headers(headers).json(&data);
|
||||
let response = builder.send().await?;
|
||||
|
||||
match response.status().as_u16() {
|
||||
204 => Ok(()),
|
||||
status @ _ => {
|
||||
bail!("failed to put configs with status \"{status}\"")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// PATCH /configs
|
||||
pub async fn patch_configs(config: &Mapping) -> Result<()> {
|
||||
let (url, headers) = clash_client_info()?;
|
||||
let url = format!("{url}/configs");
|
||||
|
||||
let client = reqwest::ClientBuilder::new().no_proxy().build()?;
|
||||
let builder = client.patch(&url).headers(headers.clone()).json(config);
|
||||
builder.send().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 根据clash info获取clash服务地址和请求头
|
||||
fn clash_client_info() -> Result<(String, HeaderMap)> {
|
||||
let client = { Config::clash().data().get_client_info() };
|
||||
|
||||
let server = format!("http://{}", client.server);
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse()?);
|
||||
|
||||
if let Some(secret) = client.secret {
|
||||
let secret = format!("Bearer {}", secret).parse()?;
|
||||
headers.insert("Authorization", secret);
|
||||
}
|
||||
|
||||
Ok((server, headers))
|
||||
}
|
||||
|
||||
/// 缩短clash的日志
|
||||
pub fn parse_log(log: String) -> String {
|
||||
if log.starts_with("time=") && log.len() > 33 {
|
||||
return (&log[33..]).to_owned();
|
||||
}
|
||||
if log.len() > 9 {
|
||||
return (&log[9..]).to_owned();
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
/// 缩短clash -t的错误输出
|
||||
/// 仅适配 clash p核 8-26、clash meta 1.13.1
|
||||
pub fn parse_check_output(log: String) -> String {
|
||||
let t = log.find("time=");
|
||||
let m = log.find("msg=");
|
||||
let mr = log.rfind('"');
|
||||
|
||||
if let (Some(_), Some(m), Some(mr)) = (t, m, mr) {
|
||||
let e = match log.find("level=error msg=") {
|
||||
Some(e) => e + 17,
|
||||
None => m + 5,
|
||||
};
|
||||
|
||||
if mr > m {
|
||||
return (&log[e..mr]).to_owned();
|
||||
}
|
||||
}
|
||||
|
||||
let l = log.find("error=");
|
||||
let r = log.find("path=").or(Some(log.len()));
|
||||
|
||||
if let (Some(l), Some(r)) = (l, r) {
|
||||
return (&log[(l + 6)..(r - 1)]).to_owned();
|
||||
}
|
||||
|
||||
log
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_check_output() {
|
||||
let str1 = r#"xxxx\n time="2022-11-18T20:42:58+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'""#;
|
||||
let str2 = r#"20:43:49 ERR [Config] configuration file test failed error=proxy 0: unsupport proxy type: hysteria path=xxx"#;
|
||||
let str3 = r#"
|
||||
"time="2022-11-18T21:38:01+08:00" level=info msg="Start initial configuration in progress"
|
||||
time="2022-11-18T21:38:01+08:00" level=error msg="proxy 0: 'alpn' expected type 'string', got unconvertible type '[]interface {}'"
|
||||
configuration file xxx\n
|
||||
"#;
|
||||
|
||||
let res1 = parse_check_output(str1.into());
|
||||
let res2 = parse_check_output(str2.into());
|
||||
let res3 = parse_check_output(str3.into());
|
||||
|
||||
println!("res1: {res1}");
|
||||
println!("res2: {res2}");
|
||||
println!("res3: {res3}");
|
||||
|
||||
assert_eq!(res1, res3);
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
use super::{clash_api, logger::Logger};
|
||||
use crate::log_err;
|
||||
use crate::{config::*, utils::dirs};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{fs, io::Write, sync::Arc, time::Duration};
|
||||
use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use tokio::time::sleep;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CoreManager {
|
||||
sidecar: Arc<Mutex<Option<CommandChild>>>,
|
||||
|
||||
#[allow(unused)]
|
||||
use_service_mode: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn global() -> &'static CoreManager {
|
||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||
|
||||
CORE_MANAGER.get_or_init(|| CoreManager {
|
||||
sidecar: Arc::new(Mutex::new(None)),
|
||||
use_service_mode: Arc::new(Mutex::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&self) -> Result<()> {
|
||||
// kill old clash process
|
||||
let _ = dirs::clash_pid_path()
|
||||
.and_then(|path| fs::read(path).map(|p| p.to_vec()).context(""))
|
||||
.and_then(|pid| String::from_utf8_lossy(&pid).parse().context(""))
|
||||
.map(|pid| {
|
||||
let mut system = System::new();
|
||||
system.refresh_all();
|
||||
system.process(Pid::from_u32(pid)).map(|proc| {
|
||||
if proc.name().contains("clash") {
|
||||
log::debug!(target: "app", "kill old clash process");
|
||||
proc.kill();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
tauri::async_runtime::spawn(async {
|
||||
// 启动clash
|
||||
log_err!(Self::global().run_core().await);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查配置是否正确
|
||||
pub fn check_config(&self) -> Result<()> {
|
||||
let config_path = Config::generate_file(ConfigType::Check)?;
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("clash".into());
|
||||
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir = dirs::path_to_str(&app_dir)?;
|
||||
|
||||
let output = Command::new_sidecar(clash_core)?
|
||||
.args(["-t", "-d", app_dir, "-f", config_path])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = clash_api::parse_check_output(output.stdout.clone());
|
||||
let error = match error.len() > 0 {
|
||||
true => error,
|
||||
false => output.stdout.clone(),
|
||||
};
|
||||
Logger::global().set_log(output.stdout);
|
||||
bail!("{error}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 启动核心
|
||||
pub async fn run_core(&self) -> Result<()> {
|
||||
let config_path = Config::generate_file(ConfigType::Run)?;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut should_kill = match self.sidecar.lock().take() {
|
||||
Some(child) => {
|
||||
log::debug!(target: "app", "stop the core by sidecar");
|
||||
let _ = child.kill();
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
log::debug!(target: "app", "stop the core by service");
|
||||
log_err!(super::win_service::stop_core_by_service().await);
|
||||
should_kill = true;
|
||||
}
|
||||
|
||||
// 这里得等一会儿
|
||||
if should_kill {
|
||||
sleep(Duration::from_millis(500)).await;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use super::win_service;
|
||||
|
||||
// 服务模式
|
||||
let enable = { Config::verge().latest().enable_service_mode.clone() };
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
*self.use_service_mode.lock() = enable;
|
||||
|
||||
if enable {
|
||||
// 服务模式启动失败就直接运行sidecar
|
||||
log::debug!(target: "app", "try to run core in service mode");
|
||||
|
||||
match (|| async {
|
||||
win_service::check_service().await?;
|
||||
win_service::run_core_by_service(&config_path).await
|
||||
})()
|
||||
.await
|
||||
{
|
||||
Ok(_) => return Ok(()),
|
||||
Err(err) => {
|
||||
// 修改这个值,免得stop出错
|
||||
*self.use_service_mode.lock() = false;
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let app_dir = dirs::path_to_str(&app_dir)?;
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("clash".into());
|
||||
let is_clash = clash_core == "clash";
|
||||
|
||||
let config_path = dirs::path_to_str(&config_path)?;
|
||||
|
||||
// fix #212
|
||||
let args = match clash_core.as_str() {
|
||||
"clash-meta" => vec!["-m", "-d", app_dir, "-f", config_path],
|
||||
_ => vec!["-d", app_dir, "-f", config_path],
|
||||
};
|
||||
|
||||
let cmd = Command::new_sidecar(clash_core)?;
|
||||
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
|
||||
|
||||
// 将pid写入文件中
|
||||
crate::log_err!((|| {
|
||||
let pid = cmd_child.pid();
|
||||
let path = dirs::clash_pid_path()?;
|
||||
fs::File::create(path)
|
||||
.context("failed to create the pid file")?
|
||||
.write(format!("{pid}").as_bytes())
|
||||
.context("failed to write pid to the file")?;
|
||||
<Result<()>>::Ok(())
|
||||
})());
|
||||
|
||||
let mut sidecar = self.sidecar.lock();
|
||||
*sidecar = Some(cmd_child);
|
||||
drop(sidecar);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
CommandEvent::Stdout(line) => {
|
||||
if is_clash {
|
||||
let stdout = clash_api::parse_log(line.clone());
|
||||
log::info!(target: "app", "[clash]: {stdout}");
|
||||
} else {
|
||||
log::info!(target: "app", "[clash]: {line}");
|
||||
};
|
||||
Logger::global().set_log(line);
|
||||
}
|
||||
CommandEvent::Stderr(err) => {
|
||||
// let stdout = clash_api::parse_log(err.clone());
|
||||
log::error!(target: "app", "[clash]: {err}");
|
||||
Logger::global().set_log(err);
|
||||
}
|
||||
CommandEvent::Error(err) => {
|
||||
log::error!(target: "app", "[clash]: {err}");
|
||||
Logger::global().set_log(err);
|
||||
}
|
||||
CommandEvent::Terminated(_) => {
|
||||
log::info!(target: "app", "clash core terminated");
|
||||
let _ = CoreManager::global().recover_core();
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 重启内核
|
||||
pub fn recover_core(&'static self) -> Result<()> {
|
||||
// 服务模式不管
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 清空原来的sidecar值
|
||||
if let Some(sidecar) = self.sidecar.lock().take() {
|
||||
let _ = sidecar.kill();
|
||||
}
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// 6秒之后再查看服务是否正常 (时间随便搞的)
|
||||
// terminated 可能是切换内核 (切换内核已经有500ms的延迟)
|
||||
sleep(Duration::from_millis(6666)).await;
|
||||
|
||||
if self.sidecar.lock().is_none() {
|
||||
log::info!(target: "app", "recover clash core");
|
||||
|
||||
// 重新启动app
|
||||
if let Err(err) = self.run_core().await {
|
||||
log::error!(target: "app", "failed to recover clash core");
|
||||
log::error!(target: "app", "{err}");
|
||||
|
||||
let _ = self.recover_core();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止核心运行
|
||||
pub fn stop_core(&self) -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
if *self.use_service_mode.lock() {
|
||||
log::debug!(target: "app", "stop the core by service");
|
||||
tauri::async_runtime::block_on(async move {
|
||||
log_err!(super::win_service::stop_core_by_service().await);
|
||||
});
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut sidecar = self.sidecar.lock();
|
||||
if let Some(child) = sidecar.take() {
|
||||
log::debug!(target: "app", "stop the core by sidecar");
|
||||
let _ = child.kill();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 切换核心
|
||||
pub async fn change_core(&self, clash_core: Option<String>) -> Result<()> {
|
||||
let clash_core = clash_core.ok_or(anyhow::anyhow!("clash core is null"))?;
|
||||
|
||||
if &clash_core != "clash" && &clash_core != "clash-meta" {
|
||||
bail!("invalid clash core name \"{clash_core}\"");
|
||||
}
|
||||
|
||||
log::debug!(target: "app", "change core to `{clash_core}`");
|
||||
|
||||
Config::verge().draft().clash_core = Some(clash_core);
|
||||
|
||||
// 更新配置
|
||||
Config::generate()?;
|
||||
|
||||
self.check_config()?;
|
||||
|
||||
// 清掉旧日志
|
||||
Logger::global().clear_log();
|
||||
|
||||
match self.run_core().await {
|
||||
Ok(_) => {
|
||||
Config::verge().apply();
|
||||
Config::runtime().apply();
|
||||
log_err!(Config::verge().latest().save_file());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Config::runtime().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新proxies那些
|
||||
/// 如果涉及端口和外部控制则需要重启
|
||||
pub async fn update_config(&self) -> Result<()> {
|
||||
log::debug!(target: "app", "try to update clash config");
|
||||
|
||||
// 更新配置
|
||||
Config::generate()?;
|
||||
|
||||
// 检查配置是否正常
|
||||
self.check_config()?;
|
||||
|
||||
// 更新运行时配置
|
||||
let path = Config::generate_file(ConfigType::Run)?;
|
||||
let path = dirs::path_to_str(&path)?;
|
||||
|
||||
// 发送请求 发送5次
|
||||
for i in 0..5 {
|
||||
match clash_api::put_configs(path).await {
|
||||
Ok(_) => break,
|
||||
Err(err) => {
|
||||
if i < 4 {
|
||||
log::info!(target: "app", "{err}");
|
||||
} else {
|
||||
bail!(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(250)).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use super::tray::Tray;
|
||||
use crate::log_err;
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, Manager, Window};
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Handle {
|
||||
pub app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||
}
|
||||
|
||||
impl Handle {
|
||||
pub fn global() -> &'static Handle {
|
||||
static HANDLE: OnceCell<Handle> = OnceCell::new();
|
||||
|
||||
HANDLE.get_or_init(|| Handle {
|
||||
app_handle: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&self, app_handle: AppHandle) {
|
||||
*self.app_handle.lock() = Some(app_handle);
|
||||
}
|
||||
|
||||
pub fn get_window(&self) -> Option<Window> {
|
||||
self.app_handle
|
||||
.lock()
|
||||
.as_ref()
|
||||
.map_or(None, |a| a.get_window("main"))
|
||||
}
|
||||
|
||||
pub fn refresh_clash() {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
log_err!(window.emit("verge://refresh-clash-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refresh_verge() {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
log_err!(window.emit("verge://refresh-verge-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn refresh_profiles() {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
log_err!(window.emit("verge://refresh-profiles-config", "yes"));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn notice_message<S: Into<String>, M: Into<String>>(status: S, msg: M) {
|
||||
if let Some(window) = Self::global().get_window() {
|
||||
log_err!(window.emit("verge://notice-message", (status.into(), msg.into())));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_systray() -> Result<()> {
|
||||
let app_handle = Self::global().app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("update_systray unhandled error");
|
||||
}
|
||||
Tray::update_systray(app_handle.as_ref().unwrap())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update the system tray state
|
||||
pub fn update_systray_part() -> Result<()> {
|
||||
let app_handle = Self::global().app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("update_systray unhandled error");
|
||||
}
|
||||
Tray::update_part(app_handle.as_ref().unwrap())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
use crate::{config::Config, feat, log_err};
|
||||
use anyhow::{bail, Result};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tauri::{AppHandle, GlobalShortcutManager};
|
||||
use wry::application::accelerator::Accelerator;
|
||||
|
||||
pub struct Hotkey {
|
||||
current: Arc<Mutex<Vec<String>>>, // 保存当前的热键设置
|
||||
|
||||
app_handle: Arc<Mutex<Option<AppHandle>>>,
|
||||
}
|
||||
|
||||
impl Hotkey {
|
||||
pub fn global() -> &'static Hotkey {
|
||||
static HOTKEY: OnceCell<Hotkey> = OnceCell::new();
|
||||
|
||||
HOTKEY.get_or_init(|| Hotkey {
|
||||
current: Arc::new(Mutex::new(Vec::new())),
|
||||
app_handle: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init(&self, app_handle: AppHandle) -> Result<()> {
|
||||
*self.app_handle.lock() = Some(app_handle);
|
||||
|
||||
let verge = Config::verge();
|
||||
|
||||
if let Some(hotkeys) = verge.latest().hotkeys.as_ref() {
|
||||
for hotkey in hotkeys.iter() {
|
||||
let mut iter = hotkey.split(',');
|
||||
let func = iter.next();
|
||||
let key = iter.next();
|
||||
|
||||
match (key, func) {
|
||||
(Some(key), Some(func)) => {
|
||||
log_err!(Self::check_key(key).and_then(|_| self.register(key, func)));
|
||||
}
|
||||
_ => {
|
||||
let key = key.unwrap_or("None");
|
||||
let func = func.unwrap_or("None");
|
||||
log::error!(target: "app", "invalid hotkey `{key}`:`{func}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
*self.current.lock() = hotkeys.clone();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 检查一个键是否合法
|
||||
fn check_key(hotkey: &str) -> Result<()> {
|
||||
// fix #287
|
||||
// tauri的这几个方法全部有Result expect,会panic,先检测一遍避免挂了
|
||||
if hotkey.parse::<Accelerator>().is_err() {
|
||||
bail!("invalid hotkey `{hotkey}`");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_manager(&self) -> Result<impl GlobalShortcutManager> {
|
||||
let app_handle = self.app_handle.lock();
|
||||
if app_handle.is_none() {
|
||||
bail!("failed to get the hotkey manager");
|
||||
}
|
||||
Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
|
||||
}
|
||||
|
||||
fn register(&self, hotkey: &str, func: &str) -> Result<()> {
|
||||
let mut manager = self.get_manager()?;
|
||||
|
||||
if manager.is_registered(hotkey)? {
|
||||
manager.unregister(hotkey)?;
|
||||
}
|
||||
|
||||
let f = match func.trim() {
|
||||
"open_dashboard" => || feat::open_dashboard(),
|
||||
"clash_mode_rule" => || feat::change_clash_mode("rule".into()),
|
||||
"clash_mode_global" => || feat::change_clash_mode("global".into()),
|
||||
"clash_mode_direct" => || feat::change_clash_mode("direct".into()),
|
||||
"clash_mode_script" => || feat::change_clash_mode("script".into()),
|
||||
"toggle_system_proxy" => || feat::toggle_system_proxy(),
|
||||
"enable_system_proxy" => || feat::enable_system_proxy(),
|
||||
"disable_system_proxy" => || feat::disable_system_proxy(),
|
||||
"toggle_tun_mode" => || feat::toggle_tun_mode(),
|
||||
"enable_tun_mode" => || feat::enable_tun_mode(),
|
||||
"disable_tun_mode" => || feat::disable_tun_mode(),
|
||||
|
||||
_ => bail!("invalid function \"{func}\""),
|
||||
};
|
||||
|
||||
manager.register(hotkey, f)?;
|
||||
log::info!(target: "app", "register hotkey {hotkey} {func}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn unregister(&self, hotkey: &str) -> Result<()> {
|
||||
self.get_manager()?.unregister(&hotkey)?;
|
||||
log::info!(target: "app", "unregister hotkey {hotkey}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&self, new_hotkeys: Vec<String>) -> Result<()> {
|
||||
let mut current = self.current.lock();
|
||||
let old_map = Self::get_map_from_vec(¤t);
|
||||
let new_map = Self::get_map_from_vec(&new_hotkeys);
|
||||
|
||||
let (del, add) = Self::get_diff(old_map, new_map);
|
||||
|
||||
// 先检查一遍所有新的热键是不是可以用的
|
||||
for (hotkey, _) in add.iter() {
|
||||
Self::check_key(hotkey)?;
|
||||
}
|
||||
|
||||
del.iter().for_each(|key| {
|
||||
let _ = self.unregister(key);
|
||||
});
|
||||
|
||||
add.iter().for_each(|(key, func)| {
|
||||
log_err!(self.register(key, func));
|
||||
});
|
||||
|
||||
*current = new_hotkeys;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_map_from_vec<'a>(hotkeys: &'a Vec<String>) -> HashMap<&'a str, &'a str> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
hotkeys.iter().for_each(|hotkey| {
|
||||
let mut iter = hotkey.split(',');
|
||||
let func = iter.next();
|
||||
let key = iter.next();
|
||||
|
||||
if func.is_some() && key.is_some() {
|
||||
let func = func.unwrap().trim();
|
||||
let key = key.unwrap().trim();
|
||||
map.insert(key, func);
|
||||
}
|
||||
});
|
||||
map
|
||||
}
|
||||
|
||||
fn get_diff<'a>(
|
||||
old_map: HashMap<&'a str, &'a str>,
|
||||
new_map: HashMap<&'a str, &'a str>,
|
||||
) -> (Vec<&'a str>, Vec<(&'a str, &'a str)>) {
|
||||
let mut del_list = vec![];
|
||||
let mut add_list = vec![];
|
||||
|
||||
old_map.iter().for_each(|(&key, func)| {
|
||||
match new_map.get(key) {
|
||||
Some(new_func) => {
|
||||
if new_func != func {
|
||||
del_list.push(key);
|
||||
add_list.push((key, *new_func));
|
||||
}
|
||||
}
|
||||
None => del_list.push(key),
|
||||
};
|
||||
});
|
||||
|
||||
new_map.iter().for_each(|(&key, &func)| {
|
||||
if old_map.get(key).is_none() {
|
||||
add_list.push((key, func));
|
||||
}
|
||||
});
|
||||
|
||||
(del_list, add_list)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Hotkey {
|
||||
fn drop(&mut self) {
|
||||
if let Ok(mut manager) = self.get_manager() {
|
||||
let _ = manager.unregister_all();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::{collections::VecDeque, sync::Arc};
|
||||
|
||||
const LOGS_QUEUE_LEN: usize = 100;
|
||||
|
||||
pub struct Logger {
|
||||
log_data: Arc<Mutex<VecDeque<String>>>,
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
pub fn global() -> &'static Logger {
|
||||
static LOGGER: OnceCell<Logger> = OnceCell::new();
|
||||
|
||||
LOGGER.get_or_init(|| Logger {
|
||||
log_data: Arc::new(Mutex::new(VecDeque::with_capacity(LOGS_QUEUE_LEN + 10))),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_log(&self) -> VecDeque<String> {
|
||||
self.log_data.lock().clone()
|
||||
}
|
||||
|
||||
pub fn set_log(&self, text: String) {
|
||||
let mut logs = self.log_data.lock();
|
||||
if logs.len() > LOGS_QUEUE_LEN {
|
||||
logs.pop_front();
|
||||
}
|
||||
logs.push_back(text);
|
||||
}
|
||||
|
||||
pub fn clear_log(&self) {
|
||||
let mut logs = self.log_data.lock();
|
||||
logs.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
/// 给clash内核的tun模式授权
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
pub fn grant_permission(core: String) -> anyhow::Result<()> {
|
||||
use std::process::Command;
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let path = current_exe()?.with_file_name(core).canonicalize()?;
|
||||
let path = path.display().to_string();
|
||||
|
||||
log::debug!("grant_permission path: {path}");
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let output = {
|
||||
// the path of clash /Applications/Clash Nyanpasu.app/Contents/MacOS/clash
|
||||
// https://apple.stackexchange.com/questions/82967/problem-with-empty-spaces-when-executing-shell-commands-in-applescript
|
||||
// let path = escape(&path);
|
||||
let path = path.replace(' ', "\\\\ ");
|
||||
let shell = format!("chown root:admin {path}\nchmod +sx {path}");
|
||||
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
|
||||
Command::new("osascript")
|
||||
.args(vec!["-e", &command])
|
||||
.output()?
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let output = {
|
||||
let path = path.replace(' ', "\\ "); // 避免路径中有空格
|
||||
let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}");
|
||||
|
||||
let sudo = match Command::new("which").arg("pkexec").output() {
|
||||
Ok(output) => {
|
||||
if output.stdout.is_empty() {
|
||||
"sudo"
|
||||
} else {
|
||||
"pkexec"
|
||||
}
|
||||
}
|
||||
Err(_) => "sudo",
|
||||
};
|
||||
|
||||
Command::new(sudo).arg("sh").arg("-c").arg(shell).output()?
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("");
|
||||
anyhow::bail!("{stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn escape<'a>(text: &'a str) -> Cow<'a, str> {
|
||||
let bytes = text.as_bytes();
|
||||
|
||||
let mut owned = None;
|
||||
|
||||
for pos in 0..bytes.len() {
|
||||
let special = match bytes[pos] {
|
||||
b' ' => Some(b' '),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(s) = special {
|
||||
if owned.is_none() {
|
||||
owned = Some(bytes[0..pos].to_owned());
|
||||
}
|
||||
owned.as_mut().unwrap().push(b'\\');
|
||||
owned.as_mut().unwrap().push(b'\\');
|
||||
owned.as_mut().unwrap().push(s);
|
||||
} else if let Some(owned) = owned.as_mut() {
|
||||
owned.push(bytes[pos]);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(owned) = owned {
|
||||
unsafe { Cow::Owned(String::from_utf8_unchecked(owned)) }
|
||||
} else {
|
||||
unsafe { Cow::Borrowed(std::str::from_utf8_unchecked(bytes)) }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,12 @@
|
||||
mod clash;
|
||||
mod profiles;
|
||||
mod verge;
|
||||
pub mod clash_api;
|
||||
mod core;
|
||||
pub mod handle;
|
||||
pub mod hotkey;
|
||||
pub mod logger;
|
||||
pub mod manager;
|
||||
pub mod sysopt;
|
||||
pub mod timer;
|
||||
pub mod tray;
|
||||
pub mod win_service;
|
||||
|
||||
pub use self::clash::*;
|
||||
pub use self::profiles::*;
|
||||
pub use self::verge::*;
|
||||
pub use self::core::*;
|
||||
|
||||
@@ -1,679 +0,0 @@
|
||||
use crate::utils::{config, dirs, help, tmpl};
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Mapping;
|
||||
use std::{fs, io::Write};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PrfItem {
|
||||
pub uid: Option<String>,
|
||||
|
||||
/// profile item type
|
||||
/// enum value: remote | local | script | merge
|
||||
#[serde(rename = "type")]
|
||||
pub itype: Option<String>,
|
||||
|
||||
/// profile name
|
||||
pub name: Option<String>,
|
||||
|
||||
/// profile description
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub desc: Option<String>,
|
||||
|
||||
/// profile file
|
||||
pub file: Option<String>,
|
||||
|
||||
/// source url
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
|
||||
/// selected infomation
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub selected: Option<Vec<PrfSelected>>,
|
||||
|
||||
/// subscription user info
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub extra: Option<PrfExtra>,
|
||||
|
||||
/// updated time
|
||||
pub updated: Option<usize>,
|
||||
|
||||
/// some options of the item
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub option: Option<PrfOption>,
|
||||
|
||||
/// the file data
|
||||
#[serde(skip)]
|
||||
pub file_data: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PrfSelected {
|
||||
pub name: Option<String>,
|
||||
pub now: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize)]
|
||||
pub struct PrfExtra {
|
||||
pub upload: usize,
|
||||
pub download: usize,
|
||||
pub total: usize,
|
||||
pub expire: usize,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PrfOption {
|
||||
/// for `remote` profile's http request
|
||||
/// see issue #13
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_agent: Option<String>,
|
||||
|
||||
/// for `remote` profile
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub with_proxy: Option<bool>,
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||
if one.is_some() && other.is_some() {
|
||||
let mut one = one.unwrap();
|
||||
let other = other.unwrap();
|
||||
|
||||
if let Some(val) = other.user_agent {
|
||||
one.user_agent = Some(val);
|
||||
}
|
||||
|
||||
if let Some(val) = other.with_proxy {
|
||||
one.with_proxy = Some(val);
|
||||
}
|
||||
|
||||
return Some(one);
|
||||
}
|
||||
|
||||
if one.is_none() {
|
||||
return other;
|
||||
}
|
||||
|
||||
return one;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrfItem {
|
||||
fn default() -> Self {
|
||||
PrfItem {
|
||||
uid: None,
|
||||
itype: None,
|
||||
name: None,
|
||||
desc: None,
|
||||
file: None,
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
updated: None,
|
||||
option: None,
|
||||
file_data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrfItem {
|
||||
/// From partial item
|
||||
/// must contain `itype`
|
||||
pub async fn from(item: PrfItem, file_data: Option<String>) -> Result<PrfItem> {
|
||||
if item.itype.is_none() {
|
||||
bail!("type should not be null");
|
||||
}
|
||||
|
||||
match item.itype.unwrap().as_str() {
|
||||
"remote" => {
|
||||
if item.url.is_none() {
|
||||
bail!("url should not be null");
|
||||
}
|
||||
let url = item.url.as_ref().unwrap().as_str();
|
||||
let name = item.name;
|
||||
let desc = item.desc;
|
||||
PrfItem::from_url(url, name, desc, item.option).await
|
||||
}
|
||||
"local" => {
|
||||
let name = item.name.unwrap_or("Local File".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_local(name, desc, file_data)
|
||||
}
|
||||
"merge" => {
|
||||
let name = item.name.unwrap_or("Merge".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_merge(name, desc)
|
||||
}
|
||||
"script" => {
|
||||
let name = item.name.unwrap_or("Script".into());
|
||||
let desc = item.desc.unwrap_or("".into());
|
||||
PrfItem::from_script(name, desc)
|
||||
}
|
||||
typ @ _ => bail!("invalid type \"{typ}\""),
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Local type
|
||||
/// create a new item from name/desc
|
||||
pub fn from_local(name: String, desc: String, file_data: Option<String>) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("l");
|
||||
let file = format!("{uid}.yaml");
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("local".into()),
|
||||
name: Some(name),
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(help::get_now()),
|
||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Remote type
|
||||
/// create a new item from url
|
||||
pub async fn from_url(
|
||||
url: &str,
|
||||
name: Option<String>,
|
||||
desc: Option<String>,
|
||||
option: Option<PrfOption>,
|
||||
) -> Result<PrfItem> {
|
||||
let with_proxy = match option.as_ref() {
|
||||
Some(opt) => opt.with_proxy.unwrap_or(false),
|
||||
None => false,
|
||||
};
|
||||
let user_agent = match option.as_ref() {
|
||||
Some(opt) => opt.user_agent.clone(),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let mut builder = reqwest::ClientBuilder::new();
|
||||
|
||||
if !with_proxy {
|
||||
builder = builder.no_proxy();
|
||||
}
|
||||
|
||||
builder = builder.user_agent(user_agent.unwrap_or("clash-verge/v0.1.0".into()));
|
||||
|
||||
let resp = builder.build()?.get(url).send().await?;
|
||||
let header = resp.headers();
|
||||
|
||||
// parse the Subscription Userinfo
|
||||
let extra = match header.get("Subscription-Userinfo") {
|
||||
Some(value) => {
|
||||
let sub_info = value.to_str().unwrap_or("");
|
||||
|
||||
Some(PrfExtra {
|
||||
upload: help::parse_str(sub_info, "upload=").unwrap_or(0),
|
||||
download: help::parse_str(sub_info, "download=").unwrap_or(0),
|
||||
total: help::parse_str(sub_info, "total=").unwrap_or(0),
|
||||
expire: help::parse_str(sub_info, "expire=").unwrap_or(0),
|
||||
})
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
let uid = help::get_uid("r");
|
||||
let file = format!("{uid}.yaml");
|
||||
let name = name.unwrap_or(uid.clone());
|
||||
let data = resp.text_with_charset("utf-8").await?;
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("remote".into()),
|
||||
name: Some(name),
|
||||
desc,
|
||||
file: Some(file),
|
||||
url: Some(url.into()),
|
||||
selected: None,
|
||||
extra,
|
||||
option,
|
||||
updated: Some(help::get_now()),
|
||||
file_data: Some(data),
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Merge type (enhance)
|
||||
/// create the enhanced item by using `merge` rule
|
||||
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("m");
|
||||
let file = format!("{uid}.yaml");
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("merge".into()),
|
||||
name: Some(name),
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(help::get_now()),
|
||||
file_data: Some(tmpl::ITEM_MERGE.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// ## Script type (enhance)
|
||||
/// create the enhanced item by using javascript(browserjs)
|
||||
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
|
||||
let uid = help::get_uid("s");
|
||||
let file = format!("{uid}.js"); // js ext
|
||||
|
||||
Ok(PrfItem {
|
||||
uid: Some(uid),
|
||||
itype: Some("script".into()),
|
||||
name: Some(name),
|
||||
desc: Some(desc),
|
||||
file: Some(file),
|
||||
url: None,
|
||||
selected: None,
|
||||
extra: None,
|
||||
option: None,
|
||||
updated: Some(help::get_now()),
|
||||
file_data: Some(tmpl::ITEM_SCRIPT.into()),
|
||||
})
|
||||
}
|
||||
|
||||
/// get the file data
|
||||
pub fn read_file(&self) -> Result<String> {
|
||||
if self.file.is_none() {
|
||||
bail!("could not find the file");
|
||||
}
|
||||
|
||||
let file = self.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir().join(file);
|
||||
fs::read_to_string(path).context("failed to read the file")
|
||||
}
|
||||
|
||||
/// save the file data
|
||||
pub fn save_file(&self, data: String) -> Result<()> {
|
||||
if self.file.is_none() {
|
||||
bail!("could not find the file");
|
||||
}
|
||||
|
||||
let file = self.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir().join(file);
|
||||
fs::write(path, data.as_bytes()).context("failed to save the file")
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// ## Profiles Config
|
||||
///
|
||||
/// Define the `profiles.yaml` schema
|
||||
///
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Profiles {
|
||||
/// same as PrfConfig.current
|
||||
current: Option<String>,
|
||||
|
||||
/// same as PrfConfig.chain
|
||||
chain: Option<Vec<String>>,
|
||||
|
||||
/// record valid fields for clash
|
||||
valid: Option<Vec<String>>,
|
||||
|
||||
/// profile list
|
||||
items: Option<Vec<PrfItem>>,
|
||||
}
|
||||
|
||||
macro_rules! patch {
|
||||
($lv: expr, $rv: expr, $key: tt) => {
|
||||
if ($rv.$key).is_some() {
|
||||
$lv.$key = $rv.$key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl Profiles {
|
||||
/// read the config from the file
|
||||
pub fn read_file() -> Self {
|
||||
let mut profiles = config::read_yaml::<Self>(dirs::profiles_path());
|
||||
|
||||
if profiles.items.is_none() {
|
||||
profiles.items = Some(vec![]);
|
||||
}
|
||||
|
||||
profiles.items.as_mut().map(|items| {
|
||||
for mut item in items.iter_mut() {
|
||||
if item.uid.is_none() {
|
||||
item.uid = Some(help::get_uid("d"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
profiles
|
||||
}
|
||||
|
||||
/// save the config to the file
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
config::save_yaml(
|
||||
dirs::profiles_path(),
|
||||
self,
|
||||
Some("# Profiles Config for Clash Verge\n\n"),
|
||||
)
|
||||
}
|
||||
|
||||
/// sync the config between file and memory
|
||||
pub fn sync_file(&mut self) -> Result<()> {
|
||||
let data = Self::read_file();
|
||||
if data.current.is_none() && data.items.is_none() {
|
||||
bail!("failed to read profiles.yaml");
|
||||
}
|
||||
|
||||
self.current = data.current;
|
||||
self.chain = data.chain;
|
||||
self.items = data.items;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get the current uid
|
||||
pub fn get_current(&self) -> Option<String> {
|
||||
self.current.clone()
|
||||
}
|
||||
|
||||
/// only change the main to the target id
|
||||
pub fn put_current(&mut self, uid: String) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
let items = self.items.as_ref().unwrap();
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for each in items.iter() {
|
||||
if each.uid == some_uid {
|
||||
self.current = some_uid;
|
||||
return self.save_file();
|
||||
}
|
||||
}
|
||||
|
||||
bail!("invalid uid \"{uid}\"");
|
||||
}
|
||||
|
||||
/// just change the `chain`
|
||||
pub fn put_chain(&mut self, chain: Option<Vec<String>>) {
|
||||
self.chain = chain;
|
||||
}
|
||||
|
||||
/// just change the `field`
|
||||
pub fn put_valid(&mut self, valid: Option<Vec<String>>) {
|
||||
self.valid = valid;
|
||||
}
|
||||
|
||||
/// find the item by the uid
|
||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
||||
if self.items.is_some() {
|
||||
let items = self.items.as_ref().unwrap();
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for each in items.iter() {
|
||||
if each.uid == some_uid {
|
||||
return Ok(each);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to get the item by \"{}\"", uid);
|
||||
}
|
||||
|
||||
/// append new item
|
||||
/// if the file_data is some
|
||||
/// then should save the data to file
|
||||
pub fn append_item(&mut self, mut item: PrfItem) -> Result<()> {
|
||||
if item.uid.is_none() {
|
||||
bail!("the uid should not be null");
|
||||
}
|
||||
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
if item.file.is_none() {
|
||||
bail!("the file should not be null");
|
||||
}
|
||||
|
||||
let file = item.file.clone().unwrap();
|
||||
let path = dirs::app_profiles_dir().join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.context(format!("failed to create file \"{}\"", file))?
|
||||
.write(file_data.as_bytes())
|
||||
.context(format!("failed to write to file \"{}\"", file))?;
|
||||
}
|
||||
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
self.items.as_mut().map(|items| items.push(item));
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// update the item's value
|
||||
pub fn patch_item(&mut self, uid: String, item: PrfItem) -> Result<()> {
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
if each.uid == Some(uid.clone()) {
|
||||
patch!(each, item, itype);
|
||||
patch!(each, item, name);
|
||||
patch!(each, item, desc);
|
||||
patch!(each, item, file);
|
||||
patch!(each, item, url);
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
patch!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file();
|
||||
}
|
||||
}
|
||||
|
||||
self.items = Some(items);
|
||||
bail!("failed to found the uid \"{uid}\"")
|
||||
}
|
||||
|
||||
/// be used to update the remote item
|
||||
/// only patch `updated` `extra` `file_data`
|
||||
pub fn update_item(&mut self, uid: String, mut item: PrfItem) -> Result<()> {
|
||||
if self.items.is_none() {
|
||||
self.items = Some(vec![]);
|
||||
}
|
||||
|
||||
// find the item
|
||||
let _ = self.get_item(&uid)?;
|
||||
|
||||
self.items.as_mut().map(|items| {
|
||||
let some_uid = Some(uid.clone());
|
||||
|
||||
for mut each in items.iter_mut() {
|
||||
if each.uid == some_uid {
|
||||
each.extra = item.extra;
|
||||
each.updated = item.updated;
|
||||
|
||||
// save the file data
|
||||
// move the field value after save
|
||||
if let Some(file_data) = item.file_data.take() {
|
||||
let file = each.file.take();
|
||||
let file = file.unwrap_or(item.file.take().unwrap_or(format!("{}.yaml", &uid)));
|
||||
|
||||
// the file must exists
|
||||
each.file = Some(file.clone());
|
||||
|
||||
let path = dirs::app_profiles_dir().join(&file);
|
||||
|
||||
fs::File::create(path)
|
||||
.unwrap()
|
||||
.write(file_data.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.save_file()
|
||||
}
|
||||
|
||||
/// delete item
|
||||
/// if delete the current then return true
|
||||
pub fn delete_item(&mut self, uid: String) -> Result<bool> {
|
||||
let current = self.current.as_ref().unwrap_or(&uid);
|
||||
let current = current.clone();
|
||||
|
||||
let mut items = self.items.take().unwrap_or(vec![]);
|
||||
let mut index = None;
|
||||
|
||||
// get the index
|
||||
for i in 0..items.len() {
|
||||
if items[i].uid == Some(uid.clone()) {
|
||||
index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = index {
|
||||
items.remove(index).file.map(|file| {
|
||||
let path = dirs::app_profiles_dir().join(file);
|
||||
if path.exists() {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// delete the original uid
|
||||
if current == uid {
|
||||
self.current = match items.len() > 0 {
|
||||
true => items[0].uid.clone(),
|
||||
false => None,
|
||||
};
|
||||
}
|
||||
|
||||
self.items = Some(items);
|
||||
self.save_file()?;
|
||||
Ok(current == uid)
|
||||
}
|
||||
|
||||
/// only generate config mapping
|
||||
pub fn gen_activate(&self) -> Result<Mapping> {
|
||||
let config = Mapping::new();
|
||||
|
||||
if self.current.is_none() || self.items.is_none() {
|
||||
return Ok(config);
|
||||
}
|
||||
|
||||
let current = self.current.clone().unwrap();
|
||||
|
||||
for item in self.items.as_ref().unwrap().iter() {
|
||||
if item.uid == Some(current.clone()) {
|
||||
let file_path = match item.file.clone() {
|
||||
Some(file) => dirs::app_profiles_dir().join(file),
|
||||
None => bail!("failed to get the file field"),
|
||||
};
|
||||
|
||||
if !file_path.exists() {
|
||||
bail!("failed to read the file \"{}\"", file_path.display());
|
||||
}
|
||||
|
||||
return Ok(config::read_yaml::<Mapping>(file_path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
bail!("failed to found the uid \"{current}\"");
|
||||
}
|
||||
|
||||
/// gen the enhanced profiles
|
||||
pub fn gen_enhanced(&self, callback: String) -> Result<PrfEnhanced> {
|
||||
let current = self.gen_activate()?;
|
||||
|
||||
let chain = match self.chain.as_ref() {
|
||||
Some(chain) => chain
|
||||
.iter()
|
||||
.map(|uid| self.get_item(uid))
|
||||
.filter(|item| item.is_ok())
|
||||
.map(|item| item.unwrap())
|
||||
.map(|item| PrfData::from_item(item))
|
||||
.filter(|o| o.is_some())
|
||||
.map(|o| o.unwrap())
|
||||
.collect::<Vec<PrfData>>(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let valid = self.valid.clone().unwrap_or(vec![]);
|
||||
|
||||
Ok(PrfEnhanced {
|
||||
current,
|
||||
chain,
|
||||
valid,
|
||||
callback,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PrfEnhanced {
|
||||
pub current: Mapping,
|
||||
|
||||
pub chain: Vec<PrfData>,
|
||||
|
||||
pub valid: Vec<String>,
|
||||
|
||||
pub callback: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PrfEnhancedResult {
|
||||
pub data: Option<Mapping>,
|
||||
|
||||
pub status: String,
|
||||
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PrfData {
|
||||
item: PrfItem,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
merge: Option<Mapping>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
script: Option<String>,
|
||||
}
|
||||
|
||||
impl PrfData {
|
||||
pub fn from_item(item: &PrfItem) -> Option<PrfData> {
|
||||
match item.itype.as_ref() {
|
||||
Some(itype) => {
|
||||
let file = item.file.clone()?;
|
||||
let path = dirs::app_profiles_dir().join(file);
|
||||
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match itype.as_str() {
|
||||
"script" => Some(PrfData {
|
||||
item: item.clone(),
|
||||
script: Some(fs::read_to_string(path).unwrap_or("".into())),
|
||||
merge: None,
|
||||
}),
|
||||
"merge" => Some(PrfData {
|
||||
item: item.clone(),
|
||||
merge: Some(config::read_yaml::<Mapping>(path)),
|
||||
script: None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
use crate::{config::Config, log_err};
|
||||
use anyhow::{anyhow, Result};
|
||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use sysproxy::Sysproxy;
|
||||
use tauri::{async_runtime::Mutex as TokioMutex, utils::platform::current_exe};
|
||||
|
||||
pub struct Sysopt {
|
||||
/// current system proxy setting
|
||||
cur_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
|
||||
|
||||
/// record the original system proxy
|
||||
/// recover it when exit
|
||||
old_sysproxy: Arc<Mutex<Option<Sysproxy>>>,
|
||||
|
||||
/// helps to auto launch the app
|
||||
auto_launch: Arc<Mutex<Option<AutoLaunch>>>,
|
||||
|
||||
/// record whether the guard async is running or not
|
||||
guard_state: Arc<TokioMutex<bool>>,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;192.168.*;10.*;172.16.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str = "127.0.0.1,192.168.0.0/16,10.0.0.0/8,172.16.0.0/12,localhost,*.local,*.crashlytics.com,<local>";
|
||||
|
||||
impl Sysopt {
|
||||
pub fn global() -> &'static Sysopt {
|
||||
static SYSOPT: OnceCell<Sysopt> = OnceCell::new();
|
||||
|
||||
SYSOPT.get_or_init(|| Sysopt {
|
||||
cur_sysproxy: Arc::new(Mutex::new(None)),
|
||||
old_sysproxy: Arc::new(Mutex::new(None)),
|
||||
auto_launch: Arc::new(Mutex::new(None)),
|
||||
guard_state: Arc::new(TokioMutex::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
pub fn init_sysproxy(&self) -> Result<()> {
|
||||
let port = { Config::clash().latest().get_mixed_port() };
|
||||
|
||||
let (enable, bypass) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let current = Sysproxy {
|
||||
enable,
|
||||
host: String::from("127.0.0.1"),
|
||||
port,
|
||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||
};
|
||||
|
||||
if enable {
|
||||
let old = Sysproxy::get_system_proxy().map_or(None, |p| Some(p));
|
||||
current.set_system_proxy()?;
|
||||
|
||||
*self.old_sysproxy.lock() = old;
|
||||
*self.cur_sysproxy.lock() = Some(current);
|
||||
}
|
||||
|
||||
// run the system proxy guard
|
||||
self.guard_proxy();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update the system proxy
|
||||
pub fn update_sysproxy(&self) -> Result<()> {
|
||||
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
||||
let old_sysproxy = self.old_sysproxy.lock();
|
||||
|
||||
if cur_sysproxy.is_none() || old_sysproxy.is_none() {
|
||||
drop(cur_sysproxy);
|
||||
drop(old_sysproxy);
|
||||
return self.init_sysproxy();
|
||||
}
|
||||
|
||||
let (enable, bypass) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
let mut sysproxy = cur_sysproxy.take().unwrap();
|
||||
|
||||
sysproxy.enable = enable;
|
||||
sysproxy.bypass = bypass.unwrap_or(DEFAULT_BYPASS.into());
|
||||
|
||||
sysproxy.set_system_proxy()?;
|
||||
*cur_sysproxy = Some(sysproxy);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// reset the sysproxy
|
||||
pub fn reset_sysproxy(&self) -> Result<()> {
|
||||
let mut cur_sysproxy = self.cur_sysproxy.lock();
|
||||
let mut old_sysproxy = self.old_sysproxy.lock();
|
||||
|
||||
let cur_sysproxy = cur_sysproxy.take();
|
||||
|
||||
if let Some(mut old) = old_sysproxy.take() {
|
||||
// 如果原代理和当前代理 端口一致,就disable关闭,否则就恢复原代理设置
|
||||
// 当前没有设置代理的时候,不确定旧设置是否和当前一致,全关了
|
||||
let port_same = cur_sysproxy.map_or(true, |cur| old.port == cur.port);
|
||||
|
||||
if old.enable && port_same {
|
||||
old.enable = false;
|
||||
log::info!(target: "app", "reset proxy by disabling the original proxy");
|
||||
} else {
|
||||
log::info!(target: "app", "reset proxy to the original proxy");
|
||||
}
|
||||
|
||||
old.set_system_proxy()?;
|
||||
} else if let Some(mut cur @ Sysproxy { enable: true, .. }) = cur_sysproxy {
|
||||
// 没有原代理,就按现在的代理设置disable即可
|
||||
log::info!(target: "app", "reset proxy by disabling the current proxy");
|
||||
cur.enable = false;
|
||||
cur.set_system_proxy()?;
|
||||
} else {
|
||||
log::info!(target: "app", "reset proxy with no action");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// init the auto launch
|
||||
pub fn init_launch(&self) -> Result<()> {
|
||||
let enable = { Config::verge().latest().enable_auto_launch.clone() };
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_name = app_exe
|
||||
.file_stem()
|
||||
.and_then(|f| f.to_str())
|
||||
.ok_or(anyhow!("failed to get file stem"))?;
|
||||
|
||||
let app_path = app_exe
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or(anyhow!("failed to get app_path"))?
|
||||
.to_string();
|
||||
|
||||
// fix issue #26
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
|
||||
// use the /Applications/Clash Nyanpasu.app path
|
||||
#[cfg(target_os = "macos")]
|
||||
let app_path = (|| -> Option<String> {
|
||||
let path = std::path::PathBuf::from(&app_path);
|
||||
let path = path.parent()?.parent()?.parent()?;
|
||||
let extension = path.extension()?.to_str()?;
|
||||
match extension == "app" {
|
||||
true => Some(path.as_os_str().to_str()?.to_string()),
|
||||
false => None,
|
||||
}
|
||||
})()
|
||||
.unwrap_or(app_path);
|
||||
|
||||
// fix #403
|
||||
#[cfg(target_os = "linux")]
|
||||
let app_path = {
|
||||
use crate::core::handle::Handle;
|
||||
use tauri::Manager;
|
||||
|
||||
let handle = Handle::global();
|
||||
match handle.app_handle.lock().as_ref() {
|
||||
Some(app_handle) => {
|
||||
let appimage = app_handle.env().appimage;
|
||||
appimage
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or(app_path)
|
||||
}
|
||||
None => app_path,
|
||||
}
|
||||
};
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(&app_path)
|
||||
.build()?;
|
||||
|
||||
// 避免在开发时将自启动关了
|
||||
#[cfg(feature = "verge-dev")]
|
||||
if !enable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
if enable && !auto.is_enabled().unwrap_or(false) {
|
||||
// 避免重复设置登录项
|
||||
let _ = auto.disable();
|
||||
auto.enable()?;
|
||||
} else if !enable {
|
||||
let _ = auto.disable();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
if enable {
|
||||
auto.enable()?;
|
||||
}
|
||||
|
||||
*self.auto_launch.lock() = Some(auto);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update the startup
|
||||
pub fn update_launch(&self) -> Result<()> {
|
||||
let auto_launch = self.auto_launch.lock();
|
||||
|
||||
if auto_launch.is_none() {
|
||||
drop(auto_launch);
|
||||
return self.init_launch();
|
||||
}
|
||||
let enable = { Config::verge().latest().enable_auto_launch.clone() };
|
||||
let enable = enable.unwrap_or(false);
|
||||
let auto_launch = auto_launch.as_ref().unwrap();
|
||||
|
||||
match enable {
|
||||
true => auto_launch.enable()?,
|
||||
false => log_err!(auto_launch.disable()), // 忽略关闭的错误
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// launch a system proxy guard
|
||||
/// read config from file directly
|
||||
pub fn guard_proxy(&self) {
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
let guard_state = self.guard_state.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// if it is running, exit
|
||||
let mut state = guard_state.lock().await;
|
||||
if *state {
|
||||
return;
|
||||
}
|
||||
*state = true;
|
||||
drop(state);
|
||||
|
||||
// default duration is 10s
|
||||
let mut wait_secs = 10u64;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_secs(wait_secs)).await;
|
||||
|
||||
let (enable, guard, guard_duration, bypass) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.enable_system_proxy.clone().unwrap_or(false),
|
||||
verge.enable_proxy_guard.clone().unwrap_or(false),
|
||||
verge.proxy_guard_duration.clone().unwrap_or(10),
|
||||
verge.system_proxy_bypass.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
// stop loop
|
||||
if !enable || !guard {
|
||||
break;
|
||||
}
|
||||
|
||||
// update duration
|
||||
wait_secs = guard_duration;
|
||||
|
||||
log::debug!(target: "app", "try to guard the system proxy");
|
||||
|
||||
let port = { Config::clash().latest().get_mixed_port() };
|
||||
|
||||
let sysproxy = Sysproxy {
|
||||
enable: true,
|
||||
host: "127.0.0.1".into(),
|
||||
port,
|
||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||
};
|
||||
|
||||
log_err!(sysproxy.set_system_proxy());
|
||||
}
|
||||
|
||||
let mut state = guard_state.lock().await;
|
||||
*state = false;
|
||||
drop(state);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
use crate::config::Config;
|
||||
use crate::feat;
|
||||
use anyhow::{Context, Result};
|
||||
use delay_timer::prelude::{DelayTimer, DelayTimerBuilder, TaskBuilder};
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
type TaskID = u64;
|
||||
|
||||
pub struct Timer {
|
||||
/// cron manager
|
||||
delay_timer: Arc<Mutex<DelayTimer>>,
|
||||
|
||||
/// save the current state
|
||||
timer_map: Arc<Mutex<HashMap<String, (TaskID, u64)>>>,
|
||||
|
||||
/// increment id
|
||||
timer_count: Arc<Mutex<TaskID>>,
|
||||
}
|
||||
|
||||
impl Timer {
|
||||
pub fn global() -> &'static Timer {
|
||||
static TIMER: OnceCell<Timer> = OnceCell::new();
|
||||
|
||||
TIMER.get_or_init(|| Timer {
|
||||
delay_timer: Arc::new(Mutex::new(DelayTimerBuilder::default().build())),
|
||||
timer_map: Arc::new(Mutex::new(HashMap::new())),
|
||||
timer_count: Arc::new(Mutex::new(1)),
|
||||
})
|
||||
}
|
||||
|
||||
/// restore timer
|
||||
pub fn init(&self) -> Result<()> {
|
||||
self.refresh()?;
|
||||
|
||||
let cur_timestamp = chrono::Local::now().timestamp();
|
||||
|
||||
let timer_map = self.timer_map.lock();
|
||||
let delay_timer = self.delay_timer.lock();
|
||||
|
||||
Config::profiles().latest().get_items().map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
// mins to seconds
|
||||
let interval = ((item.option.as_ref()?.update_interval?) as i64) * 60;
|
||||
let updated = item.updated? as i64;
|
||||
|
||||
if interval > 0 && cur_timestamp - updated >= interval {
|
||||
Some(item)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.for_each(|item| {
|
||||
if let Some(uid) = item.uid.as_ref() {
|
||||
if let Some((task_id, _)) = timer_map.get(uid) {
|
||||
crate::log_err!(delay_timer.advance_task(*task_id));
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Correctly update all cron tasks
|
||||
pub fn refresh(&self) -> Result<()> {
|
||||
let diff_map = self.gen_diff();
|
||||
|
||||
let mut timer_map = self.timer_map.lock();
|
||||
let mut delay_timer = self.delay_timer.lock();
|
||||
|
||||
for (uid, diff) in diff_map.into_iter() {
|
||||
match diff {
|
||||
DiffFlag::Del(tid) => {
|
||||
let _ = timer_map.remove(&uid);
|
||||
crate::log_err!(delay_timer.remove_task(tid));
|
||||
}
|
||||
DiffFlag::Add(tid, val) => {
|
||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||
}
|
||||
DiffFlag::Mod(tid, val) => {
|
||||
let _ = timer_map.insert(uid.clone(), (tid, val));
|
||||
crate::log_err!(delay_timer.remove_task(tid));
|
||||
crate::log_err!(self.add_task(&mut delay_timer, uid, tid, val));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// generate a uid -> update_interval map
|
||||
fn gen_map(&self) -> HashMap<String, u64> {
|
||||
let mut new_map = HashMap::new();
|
||||
|
||||
if let Some(items) = Config::profiles().latest().get_items() {
|
||||
for item in items.iter() {
|
||||
if item.option.is_some() {
|
||||
let option = item.option.as_ref().unwrap();
|
||||
let interval = option.update_interval.unwrap_or(0);
|
||||
|
||||
if interval > 0 {
|
||||
new_map.insert(item.uid.clone().unwrap(), interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_map
|
||||
}
|
||||
|
||||
/// generate the diff map for refresh
|
||||
fn gen_diff(&self) -> HashMap<String, DiffFlag> {
|
||||
let mut diff_map = HashMap::new();
|
||||
|
||||
let timer_map = self.timer_map.lock();
|
||||
|
||||
let new_map = self.gen_map();
|
||||
let cur_map = &timer_map;
|
||||
|
||||
cur_map.iter().for_each(|(uid, (tid, val))| {
|
||||
let new_val = new_map.get(uid).unwrap_or(&0);
|
||||
|
||||
if *new_val == 0 {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Del(*tid));
|
||||
} else if new_val != val {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Mod(*tid, *new_val));
|
||||
}
|
||||
});
|
||||
|
||||
let mut count = self.timer_count.lock();
|
||||
|
||||
new_map.iter().for_each(|(uid, val)| {
|
||||
if cur_map.get(uid).is_none() {
|
||||
diff_map.insert(uid.clone(), DiffFlag::Add(*count, *val));
|
||||
|
||||
*count += 1;
|
||||
}
|
||||
});
|
||||
|
||||
diff_map
|
||||
}
|
||||
|
||||
/// add a cron task
|
||||
fn add_task(
|
||||
&self,
|
||||
delay_timer: &mut DelayTimer,
|
||||
uid: String,
|
||||
tid: TaskID,
|
||||
minutes: u64,
|
||||
) -> Result<()> {
|
||||
let task = TaskBuilder::default()
|
||||
.set_task_id(tid)
|
||||
.set_maximum_parallel_runnable_num(1)
|
||||
.set_frequency_repeated_by_minutes(minutes)
|
||||
// .set_frequency_repeated_by_seconds(minutes) // for test
|
||||
.spawn_async_routine(move || Self::async_task(uid.to_owned()))
|
||||
.context("failed to create timer task")?;
|
||||
|
||||
delay_timer
|
||||
.add_task(task)
|
||||
.context("failed to add timer task")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// the task runner
|
||||
async fn async_task(uid: String) {
|
||||
log::info!(target: "app", "running timer task `{uid}`");
|
||||
crate::log_err!(feat::update_profile(uid, None).await);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DiffFlag {
|
||||
Del(TaskID),
|
||||
Add(TaskID, u64),
|
||||
Mod(TaskID, u64),
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
use crate::{cmds, config::Config, feat, utils::resolve};
|
||||
use anyhow::Result;
|
||||
use tauri::{
|
||||
api, AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
SystemTraySubmenu,
|
||||
};
|
||||
|
||||
pub struct Tray {}
|
||||
|
||||
impl Tray {
|
||||
pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
|
||||
let zh = { Config::verge().latest().language == Some("zh".into()) };
|
||||
|
||||
let version = app_handle.package_info().version.to_string();
|
||||
|
||||
macro_rules! t {
|
||||
($en: expr, $zh: expr) => {
|
||||
if zh {
|
||||
$zh
|
||||
} else {
|
||||
$en
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_window",
|
||||
t!("Dashboard", "打开面板"),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"rule_mode",
|
||||
t!("Rule Mode", "规则模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"global_mode",
|
||||
t!("Global Mode", "全局模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"direct_mode",
|
||||
t!("Direct Mode", "直连模式"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"script_mode",
|
||||
t!("Script Mode", "脚本模式"),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new(
|
||||
"system_proxy",
|
||||
t!("System Proxy", "系统代理"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new("tun_mode", t!("TUN Mode", "Tun 模式")))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env_sh",
|
||||
t!("Copy Env (sh)", "复制环境变量(sh)"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env_cmd",
|
||||
t!("Copy Env (CMD)", "复制环境变量(CMD)"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"copy_env_ps",
|
||||
t!("Copy Env (PS)", "复制环境变量(PS)"),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("Open Dir", "打开目录"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_app_dir",
|
||||
t!("App Dir", "应用目录"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_core_dir",
|
||||
t!("Core Dir", "内核目录"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"open_logs_dir",
|
||||
t!("Logs Dir", "日志目录"),
|
||||
)),
|
||||
))
|
||||
.add_submenu(SystemTraySubmenu::new(
|
||||
t!("More", "更多"),
|
||||
SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_clash",
|
||||
t!("Restart Clash", "重启 Clash"),
|
||||
))
|
||||
.add_item(CustomMenuItem::new(
|
||||
"restart_app",
|
||||
t!("Restart App", "重启应用"),
|
||||
))
|
||||
.add_item(
|
||||
CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
|
||||
),
|
||||
))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", t!("Quit", "退出")).accelerator("CmdOrControl+Q"))
|
||||
}
|
||||
|
||||
pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.set_menu(Tray::tray_menu(app_handle))?;
|
||||
Tray::update_part(app_handle)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_part(app_handle: &AppHandle) -> Result<()> {
|
||||
let mode = {
|
||||
Config::clash()
|
||||
.latest()
|
||||
.0
|
||||
.get("mode")
|
||||
.map(|val| val.as_str().unwrap_or("rule"))
|
||||
.unwrap_or("rule")
|
||||
.to_owned()
|
||||
};
|
||||
|
||||
let tray = app_handle.tray_handle();
|
||||
|
||||
let _ = tray.get_item("rule_mode").set_selected(mode == "rule");
|
||||
let _ = tray.get_item("global_mode").set_selected(mode == "global");
|
||||
let _ = tray.get_item("direct_mode").set_selected(mode == "direct");
|
||||
let _ = tray.get_item("script_mode").set_selected(mode == "script");
|
||||
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
let system_proxy = verge.enable_system_proxy.as_ref().unwrap_or(&false);
|
||||
let tun_mode = verge.enable_tun_mode.as_ref().unwrap_or(&false);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let indication_icon = if *system_proxy {
|
||||
include_bytes!("../../icons/win-tray-icon-activated.png").to_vec()
|
||||
} else {
|
||||
include_bytes!("../../icons/win-tray-icon.png").to_vec()
|
||||
};
|
||||
|
||||
let _ = tray.set_icon(tauri::Icon::Raw(indication_icon));
|
||||
}
|
||||
|
||||
let _ = tray.get_item("system_proxy").set_selected(*system_proxy);
|
||||
let _ = tray.get_item("tun_mode").set_selected(*tun_mode);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
|
||||
match event {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
mode @ ("rule_mode" | "global_mode" | "direct_mode" | "script_mode") => {
|
||||
let mode = &mode[0..mode.len() - 5];
|
||||
feat::change_clash_mode(mode.into());
|
||||
}
|
||||
|
||||
"open_window" => resolve::create_window(app_handle),
|
||||
"system_proxy" => feat::toggle_system_proxy(),
|
||||
"tun_mode" => feat::toggle_tun_mode(),
|
||||
"copy_env_sh" => feat::copy_clash_env("sh"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_cmd" => feat::copy_clash_env("cmd"),
|
||||
#[cfg(target_os = "windows")]
|
||||
"copy_env_ps" => feat::copy_clash_env("ps"),
|
||||
"open_app_dir" => crate::log_err!(cmds::open_app_dir()),
|
||||
"open_core_dir" => crate::log_err!(cmds::open_core_dir()),
|
||||
"open_logs_dir" => crate::log_err!(cmds::open_logs_dir()),
|
||||
"restart_clash" => feat::restart_clash_core(),
|
||||
"restart_app" => api::process::restart(&app_handle.env()),
|
||||
"quit" => {
|
||||
let _ = resolve::save_window_size_position(app_handle, true);
|
||||
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
#[cfg(target_os = "windows")]
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
resolve::create_window(app_handle);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
use crate::log_if_err;
|
||||
use crate::{
|
||||
core::Clash,
|
||||
utils::{config, dirs, sysopt::SysProxyConfig},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use auto_launch::{AutoLaunch, AutoLaunchBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use tauri::{async_runtime::Mutex, utils::platform::current_exe};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct VergeConfig {
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
/// `light` or `dark`
|
||||
pub theme_mode: Option<String>,
|
||||
|
||||
/// enable blur mode
|
||||
/// maybe be able to set the alpha
|
||||
pub theme_blur: Option<bool>,
|
||||
|
||||
/// enable traffic graph default is true
|
||||
pub traffic_graph: Option<bool>,
|
||||
|
||||
/// clash tun mode
|
||||
pub enable_tun_mode: Option<bool>,
|
||||
|
||||
/// can the app auto startup
|
||||
pub enable_auto_launch: Option<bool>,
|
||||
|
||||
/// not show the window on launch
|
||||
pub enable_silent_start: Option<bool>,
|
||||
|
||||
/// set system proxy
|
||||
pub enable_system_proxy: Option<bool>,
|
||||
|
||||
/// enable proxy guard
|
||||
pub enable_proxy_guard: Option<bool>,
|
||||
|
||||
/// set system proxy bypass
|
||||
pub system_proxy_bypass: Option<String>,
|
||||
|
||||
/// proxy guard duration
|
||||
pub proxy_guard_duration: Option<u64>,
|
||||
|
||||
/// theme setting
|
||||
pub theme_setting: Option<VergeTheme>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct VergeTheme {
|
||||
pub primary_color: Option<String>,
|
||||
pub secondary_color: Option<String>,
|
||||
pub primary_text: Option<String>,
|
||||
pub secondary_text: Option<String>,
|
||||
|
||||
pub info_color: Option<String>,
|
||||
pub error_color: Option<String>,
|
||||
pub warning_color: Option<String>,
|
||||
pub success_color: Option<String>,
|
||||
|
||||
pub font_family: Option<String>,
|
||||
pub css_injection: Option<String>,
|
||||
}
|
||||
|
||||
impl VergeConfig {
|
||||
pub fn new() -> Self {
|
||||
config::read_yaml::<VergeConfig>(dirs::verge_path())
|
||||
}
|
||||
|
||||
/// Save Verge App Config
|
||||
pub fn save_file(&self) -> Result<()> {
|
||||
config::save_yaml(
|
||||
dirs::verge_path(),
|
||||
self,
|
||||
Some("# The Config for Clash Verge App\n\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Verge App abilities
|
||||
#[derive(Debug)]
|
||||
pub struct Verge {
|
||||
/// manage the verge config
|
||||
pub config: VergeConfig,
|
||||
|
||||
/// current system proxy setting
|
||||
pub cur_sysproxy: Option<SysProxyConfig>,
|
||||
|
||||
/// record the original system proxy
|
||||
/// recover it when exit
|
||||
old_sysproxy: Option<SysProxyConfig>,
|
||||
|
||||
/// helps to auto launch the app
|
||||
auto_launch: Option<AutoLaunch>,
|
||||
|
||||
/// record whether the guard async is running or not
|
||||
guard_state: Arc<Mutex<bool>>,
|
||||
}
|
||||
|
||||
impl Default for Verge {
|
||||
fn default() -> Self {
|
||||
Verge::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Verge {
|
||||
pub fn new() -> Self {
|
||||
Verge {
|
||||
config: VergeConfig::new(),
|
||||
old_sysproxy: None,
|
||||
cur_sysproxy: None,
|
||||
auto_launch: None,
|
||||
guard_state: Arc::new(Mutex::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// init the sysproxy
|
||||
pub fn init_sysproxy(&mut self, port: Option<String>) {
|
||||
if let Some(port) = port {
|
||||
let enable = self.config.enable_system_proxy.clone().unwrap_or(false);
|
||||
|
||||
self.old_sysproxy = match SysProxyConfig::get_sys() {
|
||||
Ok(proxy) => Some(proxy),
|
||||
Err(_) => None,
|
||||
};
|
||||
|
||||
let bypass = self.config.system_proxy_bypass.clone();
|
||||
let sysproxy = SysProxyConfig::new(enable, port, bypass);
|
||||
|
||||
if enable {
|
||||
if sysproxy.set_sys().is_err() {
|
||||
log::error!("failed to set system proxy");
|
||||
}
|
||||
}
|
||||
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
}
|
||||
|
||||
// launchs the system proxy guard
|
||||
Verge::guard_proxy(self.guard_state.clone());
|
||||
}
|
||||
|
||||
/// reset the sysproxy
|
||||
pub fn reset_sysproxy(&mut self) {
|
||||
if let Some(sysproxy) = self.old_sysproxy.take() {
|
||||
match sysproxy.set_sys() {
|
||||
Ok(_) => self.cur_sysproxy = None,
|
||||
Err(_) => log::error!("failed to reset proxy"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// init the auto launch
|
||||
pub fn init_launch(&mut self) -> Result<()> {
|
||||
let app_exe = current_exe().unwrap();
|
||||
let app_exe = dunce::canonicalize(app_exe).unwrap();
|
||||
let app_name = app_exe.file_stem().unwrap().to_str().unwrap();
|
||||
let app_path = app_exe.as_os_str().to_str().unwrap();
|
||||
|
||||
// fix issue #26
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = format!("\"{app_path}\"");
|
||||
#[cfg(target_os = "windows")]
|
||||
let app_path = app_path.as_str();
|
||||
|
||||
let auto = AutoLaunchBuilder::new()
|
||||
.set_app_name(app_name)
|
||||
.set_app_path(app_path)
|
||||
.build();
|
||||
|
||||
if let Some(enable) = self.config.enable_auto_launch.as_ref() {
|
||||
// fix issue #26
|
||||
if *enable {
|
||||
auto.enable()?;
|
||||
}
|
||||
}
|
||||
|
||||
self.auto_launch = Some(auto);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update the startup
|
||||
fn update_launch(&mut self, enable: bool) -> Result<()> {
|
||||
let conf_enable = self.config.enable_auto_launch.clone().unwrap_or(false);
|
||||
|
||||
if enable == conf_enable {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let auto_launch = self.auto_launch.clone().unwrap();
|
||||
|
||||
match enable {
|
||||
true => auto_launch.enable()?,
|
||||
false => auto_launch.disable()?,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// patch verge config
|
||||
/// There should be only one update at a time here
|
||||
/// so call the save_file at the end is savely
|
||||
pub fn patch_config(&mut self, patch: VergeConfig) -> Result<()> {
|
||||
// only change it
|
||||
if patch.language.is_some() {
|
||||
self.config.language = patch.language;
|
||||
}
|
||||
if patch.theme_mode.is_some() {
|
||||
self.config.theme_mode = patch.theme_mode;
|
||||
}
|
||||
if patch.theme_blur.is_some() {
|
||||
self.config.theme_blur = patch.theme_blur;
|
||||
}
|
||||
if patch.traffic_graph.is_some() {
|
||||
self.config.traffic_graph = patch.traffic_graph;
|
||||
}
|
||||
if patch.enable_silent_start.is_some() {
|
||||
self.config.enable_silent_start = patch.enable_silent_start;
|
||||
}
|
||||
if patch.theme_setting.is_some() {
|
||||
self.config.theme_setting = patch.theme_setting;
|
||||
}
|
||||
|
||||
// should update system startup
|
||||
if patch.enable_auto_launch.is_some() {
|
||||
let enable = patch.enable_auto_launch.unwrap();
|
||||
self.update_launch(enable)?;
|
||||
self.config.enable_auto_launch = Some(enable);
|
||||
}
|
||||
|
||||
// should update system proxy
|
||||
if patch.enable_system_proxy.is_some() {
|
||||
let enable = patch.enable_system_proxy.unwrap();
|
||||
|
||||
if let Some(mut sysproxy) = self.cur_sysproxy.take() {
|
||||
sysproxy.enable = enable;
|
||||
if sysproxy.set_sys().is_err() {
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
|
||||
bail!("failed to set system proxy");
|
||||
}
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
}
|
||||
self.config.enable_system_proxy = Some(enable);
|
||||
}
|
||||
|
||||
// should update system proxy too
|
||||
if patch.system_proxy_bypass.is_some() {
|
||||
let bypass = patch.system_proxy_bypass.unwrap();
|
||||
|
||||
if let Some(mut sysproxy) = self.cur_sysproxy.take() {
|
||||
if sysproxy.enable {
|
||||
sysproxy.bypass = bypass.clone();
|
||||
|
||||
if sysproxy.set_sys().is_err() {
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
|
||||
bail!("failed to set system proxy");
|
||||
}
|
||||
}
|
||||
|
||||
self.cur_sysproxy = Some(sysproxy);
|
||||
}
|
||||
|
||||
self.config.system_proxy_bypass = Some(bypass);
|
||||
}
|
||||
|
||||
// proxy guard
|
||||
// only change it
|
||||
if patch.enable_proxy_guard.is_some() {
|
||||
self.config.enable_proxy_guard = patch.enable_proxy_guard;
|
||||
}
|
||||
if patch.proxy_guard_duration.is_some() {
|
||||
self.config.proxy_guard_duration = patch.proxy_guard_duration;
|
||||
}
|
||||
|
||||
// relaunch the guard
|
||||
if patch.enable_system_proxy.is_some() || patch.enable_proxy_guard.is_some() {
|
||||
Verge::guard_proxy(self.guard_state.clone());
|
||||
}
|
||||
|
||||
// handle the tun mode
|
||||
if patch.enable_tun_mode.is_some() {
|
||||
self.config.enable_tun_mode = patch.enable_tun_mode;
|
||||
}
|
||||
|
||||
self.config.save_file()
|
||||
}
|
||||
|
||||
/// update the system tray state
|
||||
pub fn update_systray(&self, app_handle: &AppHandle) -> Result<()> {
|
||||
// system proxy
|
||||
let system_proxy = self.config.enable_system_proxy.as_ref();
|
||||
system_proxy.map(|system_proxy| {
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.get_item("system_proxy")
|
||||
.set_selected(*system_proxy)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// tun mode
|
||||
let tun_mode = self.config.enable_tun_mode.as_ref();
|
||||
tun_mode.map(|tun_mode| {
|
||||
app_handle
|
||||
.tray_handle()
|
||||
.get_item("tun_mode")
|
||||
.set_selected(*tun_mode)
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
// update verge config
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
window.emit("verge://refresh-verge-config", "yes").unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Verge {
|
||||
/// launch a system proxy guard
|
||||
/// read config from file directly
|
||||
pub fn guard_proxy(guard_state: Arc<Mutex<bool>>) {
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// if it is running, exit
|
||||
let mut state = guard_state.lock().await;
|
||||
if *state {
|
||||
return;
|
||||
}
|
||||
*state = true;
|
||||
std::mem::drop(state);
|
||||
|
||||
// default duration is 10s
|
||||
let mut wait_secs = 10u64;
|
||||
|
||||
loop {
|
||||
sleep(Duration::from_secs(wait_secs)).await;
|
||||
|
||||
log::debug!("guard heartbeat detection");
|
||||
|
||||
let verge = Verge::new();
|
||||
|
||||
let enable_proxy = verge.config.enable_system_proxy.unwrap_or(false);
|
||||
let enable_guard = verge.config.enable_proxy_guard.unwrap_or(false);
|
||||
let guard_duration = verge.config.proxy_guard_duration.unwrap_or(10);
|
||||
|
||||
// update duration
|
||||
wait_secs = guard_duration;
|
||||
|
||||
// stop loop
|
||||
if !enable_guard || !enable_proxy {
|
||||
break;
|
||||
}
|
||||
|
||||
log::info!("try to guard proxy");
|
||||
|
||||
let clash = Clash::new();
|
||||
|
||||
match &clash.info.port {
|
||||
Some(port) => {
|
||||
let bypass = verge.config.system_proxy_bypass.clone();
|
||||
let sysproxy = SysProxyConfig::new(true, port.clone(), bypass);
|
||||
|
||||
log_if_err!(sysproxy.set_sys());
|
||||
}
|
||||
None => log::error!("fail to parse clash port"),
|
||||
}
|
||||
}
|
||||
|
||||
let mut state = guard_state.lock().await;
|
||||
*state = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{bail, Context, Result};
|
||||
use deelevate::{PrivilegeLevel, Token};
|
||||
use runas::Command as RunasCommand;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use std::{env::current_exe, process::Command as StdCommand};
|
||||
use tokio::time::sleep;
|
||||
|
||||
const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct ResponseBody {
|
||||
pub core_type: Option<String>,
|
||||
pub bin_path: String,
|
||||
pub config_dir: String,
|
||||
pub log_file: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct JsonResponse {
|
||||
pub code: u64,
|
||||
pub msg: String,
|
||||
pub data: Option<ResponseBody>,
|
||||
}
|
||||
|
||||
/// Install the Clash Nyanpasu Service
|
||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||
pub async fn install_service() -> Result<()> {
|
||||
let binary_path = dirs::service_path()?;
|
||||
let install_path = binary_path.with_file_name("install-service.exe");
|
||||
|
||||
if !install_path.exists() {
|
||||
bail!("installer exe not found");
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
|
||||
_ => StdCommand::new(install_path)
|
||||
.creation_flags(0x08000000)
|
||||
.status()?,
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to install service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uninstall the Clash Nyanpasu Service
|
||||
/// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
||||
pub async fn uninstall_service() -> Result<()> {
|
||||
let binary_path = dirs::service_path()?;
|
||||
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
|
||||
|
||||
if !uninstall_path.exists() {
|
||||
bail!("uninstaller exe not found");
|
||||
}
|
||||
|
||||
let token = Token::with_current_process()?;
|
||||
let level = token.privilege_level()?;
|
||||
|
||||
let status = match level {
|
||||
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
|
||||
_ => StdCommand::new(uninstall_path)
|
||||
.creation_flags(0x08000000)
|
||||
.status()?,
|
||||
};
|
||||
|
||||
if !status.success() {
|
||||
bail!(
|
||||
"failed to uninstall service with status {}",
|
||||
status.code().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// check the windows service status
|
||||
pub async fn check_service() -> Result<JsonResponse> {
|
||||
let url = format!("{SERVICE_URL}/get_clash");
|
||||
let response = reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()?
|
||||
.get(url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to connect to the Clash Nyanpasu Service")?
|
||||
.json::<JsonResponse>()
|
||||
.await
|
||||
.context("failed to parse the Clash Nyanpasu Service response")?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// start the clash by service
|
||||
pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
|
||||
let status = check_service().await?;
|
||||
|
||||
if status.code == 0 {
|
||||
stop_core_by_service().await?;
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
|
||||
let clash_core = { Config::verge().latest().clash_core.clone() };
|
||||
let clash_core = clash_core.unwrap_or("clash".into());
|
||||
|
||||
let clash_bin = format!("{clash_core}.exe");
|
||||
let bin_path = current_exe()?.with_file_name(clash_bin);
|
||||
let bin_path = dirs::path_to_str(&bin_path)?;
|
||||
|
||||
let config_dir = dirs::app_home_dir()?;
|
||||
let config_dir = dirs::path_to_str(&config_dir)?;
|
||||
|
||||
let log_path = dirs::service_log_file()?;
|
||||
let log_path = dirs::path_to_str(&log_path)?;
|
||||
|
||||
let config_file = dirs::path_to_str(config_file)?;
|
||||
|
||||
let mut map = HashMap::new();
|
||||
map.insert("core_type", clash_core.as_str());
|
||||
map.insert("bin_path", bin_path);
|
||||
map.insert("config_dir", config_dir);
|
||||
map.insert("config_file", config_file);
|
||||
map.insert("log_file", log_path);
|
||||
|
||||
let url = format!("{SERVICE_URL}/start_clash");
|
||||
let res = reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()?
|
||||
.post(url)
|
||||
.json(&map)
|
||||
.send()
|
||||
.await?
|
||||
.json::<JsonResponse>()
|
||||
.await
|
||||
.context("failed to connect to the Clash Nyanpasu Service")?;
|
||||
|
||||
if res.code != 0 {
|
||||
bail!(res.msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// stop the clash by service
|
||||
pub(super) async fn stop_core_by_service() -> Result<()> {
|
||||
let url = format!("{SERVICE_URL}/stop_clash");
|
||||
let res = reqwest::ClientBuilder::new()
|
||||
.no_proxy()
|
||||
.build()?
|
||||
.post(url)
|
||||
.send()
|
||||
.await?
|
||||
.json::<JsonResponse>()
|
||||
.await
|
||||
.context("failed to connect to the Clash Nyanpasu Service")?;
|
||||
|
||||
if res.code != 0 {
|
||||
bail!(res.msg);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
function main(params) {
|
||||
if (params.mode === "script") {
|
||||
params.mode = "rule";
|
||||
}
|
||||
return params;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
function main(params) {
|
||||
if (Array.isArray(params.proxies)) {
|
||||
params.proxies.forEach((p, i) => {
|
||||
if (p.type === "hysteria" && typeof p.alpn === "string") {
|
||||
params.proxies[i].alpn = [p.alpn];
|
||||
}
|
||||
});
|
||||
}
|
||||
return params;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use crate::{
|
||||
config::PrfItem,
|
||||
utils::{dirs, help},
|
||||
};
|
||||
use serde_yaml::Mapping;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainItem {
|
||||
pub uid: String,
|
||||
pub data: ChainType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChainType {
|
||||
Merge(Mapping),
|
||||
Script(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ChainSupport {
|
||||
Clash,
|
||||
ClashMeta,
|
||||
All,
|
||||
}
|
||||
|
||||
impl From<&PrfItem> for Option<ChainItem> {
|
||||
fn from(item: &PrfItem) -> Self {
|
||||
let itype = item.itype.as_ref()?.as_str();
|
||||
let file = item.file.clone()?;
|
||||
let uid = item.uid.clone().unwrap_or("".into());
|
||||
let path = dirs::app_profiles_dir().ok()?.join(file);
|
||||
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match itype {
|
||||
"script" => Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Script(fs::read_to_string(path).ok()?),
|
||||
}),
|
||||
"merge" => Some(ChainItem {
|
||||
uid,
|
||||
data: ChainType::Merge(help::read_merge_mapping(&path).ok()?),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainItem {
|
||||
/// 内建支持一些脚本
|
||||
pub fn builtin() -> Vec<(ChainSupport, ChainItem)> {
|
||||
// meta 的一些处理
|
||||
let meta_guard =
|
||||
ChainItem::to_script("verge_meta_guard", include_str!("./builtin/meta_guard.js"));
|
||||
|
||||
// meta 1.13.2 alpn string 转 数组
|
||||
let hy_alpn =
|
||||
ChainItem::to_script("verge_hy_alpn", include_str!("./builtin/meta_hy_alpn.js"));
|
||||
|
||||
vec![
|
||||
(ChainSupport::ClashMeta, hy_alpn),
|
||||
(ChainSupport::ClashMeta, meta_guard),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn to_script<U: Into<String>, D: Into<String>>(uid: U, data: D) -> Self {
|
||||
Self {
|
||||
uid: uid.into(),
|
||||
data: ChainType::Script(data.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainSupport {
|
||||
pub fn is_support(&self, core: Option<&String>) -> bool {
|
||||
match core {
|
||||
Some(core) => match (self, core.as_str()) {
|
||||
(ChainSupport::All, _) => true,
|
||||
(ChainSupport::Clash, "clash") => true,
|
||||
(ChainSupport::ClashMeta, "clash-meta") => true,
|
||||
_ => false,
|
||||
},
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::collections::HashSet;
|
||||
|
||||
pub const HANDLE_FIELDS: [&str; 9] = [
|
||||
"mode",
|
||||
"port",
|
||||
"socks-port",
|
||||
"mixed-port",
|
||||
"allow-lan",
|
||||
"log-level",
|
||||
"ipv6",
|
||||
"secret",
|
||||
"external-controller",
|
||||
];
|
||||
|
||||
pub const DEFAULT_FIELDS: [&str; 5] = [
|
||||
"proxies",
|
||||
"proxy-groups",
|
||||
"proxy-providers",
|
||||
"rules",
|
||||
"rule-providers",
|
||||
];
|
||||
|
||||
pub const OTHERS_FIELDS: [&str; 30] = [
|
||||
"dns",
|
||||
"tun",
|
||||
"ebpf",
|
||||
"hosts",
|
||||
"script",
|
||||
"profile",
|
||||
"payload",
|
||||
"tunnels",
|
||||
"auto-redir",
|
||||
"experimental",
|
||||
"interface-name",
|
||||
"routing-mark",
|
||||
"redir-port",
|
||||
"tproxy-port",
|
||||
"iptables",
|
||||
"external-ui",
|
||||
"bind-address",
|
||||
"authentication",
|
||||
"tls", // meta
|
||||
"sniffer", // meta
|
||||
"geox-url", // meta
|
||||
"listeners", // meta
|
||||
"sub-rules", // meta
|
||||
"geodata-mode", // meta
|
||||
"unified-delay", // meta
|
||||
"tcp-concurrent", // meta
|
||||
"enable-process", // meta
|
||||
"find-process-mode", // meta
|
||||
"external-controller-tls", // meta
|
||||
"global-client-fingerprint", // meta
|
||||
];
|
||||
|
||||
pub fn use_clash_fields() -> Vec<String> {
|
||||
DEFAULT_FIELDS
|
||||
.into_iter()
|
||||
.chain(HANDLE_FIELDS)
|
||||
.chain(OTHERS_FIELDS)
|
||||
.map(|s| s.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn use_valid_fields(mut valid: Vec<String>) -> Vec<String> {
|
||||
let others = Vec::from(OTHERS_FIELDS);
|
||||
|
||||
valid.iter_mut().for_each(|s| s.make_ascii_lowercase());
|
||||
valid
|
||||
.into_iter()
|
||||
.filter(|s| others.contains(&s.as_str()))
|
||||
.chain(DEFAULT_FIELDS.iter().map(|s| s.to_string()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn use_filter(config: Mapping, filter: &Vec<String>, enable: bool) -> Mapping {
|
||||
if !enable {
|
||||
return config;
|
||||
}
|
||||
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
if let Some(key) = key.as_str() {
|
||||
if filter.contains(&key.to_string()) {
|
||||
ret.insert(Value::from(key), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn use_lowercase(config: Mapping) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
for (key, value) in config.into_iter() {
|
||||
if let Some(key_str) = key.as_str() {
|
||||
let mut key_str = String::from(key_str);
|
||||
key_str.make_ascii_lowercase();
|
||||
ret.insert(Value::from(key_str), value);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn use_sort(config: Mapping, enable_filter: bool) -> Mapping {
|
||||
let mut ret = Mapping::new();
|
||||
|
||||
HANDLE_FIELDS
|
||||
.into_iter()
|
||||
.chain(OTHERS_FIELDS)
|
||||
.chain(DEFAULT_FIELDS)
|
||||
.for_each(|key| {
|
||||
let key = Value::from(key);
|
||||
config.get(&key).map(|value| {
|
||||
ret.insert(key, value.clone());
|
||||
});
|
||||
});
|
||||
|
||||
if !enable_filter {
|
||||
let supported_keys: HashSet<&str> = HANDLE_FIELDS
|
||||
.into_iter()
|
||||
.chain(OTHERS_FIELDS)
|
||||
.chain(DEFAULT_FIELDS)
|
||||
.collect();
|
||||
|
||||
let config_keys: HashSet<&str> = config
|
||||
.keys()
|
||||
.filter_map(|e| e.as_str())
|
||||
.into_iter()
|
||||
.collect();
|
||||
|
||||
config_keys.difference(&supported_keys).for_each(|&key| {
|
||||
let key = Value::from(key);
|
||||
config.get(&key).map(|value| {
|
||||
ret.insert(key, value.clone());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub fn use_keys(config: &Mapping) -> Vec<String> {
|
||||
config
|
||||
.iter()
|
||||
.filter_map(|(key, _)| key.as_str())
|
||||
.map(|s| {
|
||||
let mut s = s.to_string();
|
||||
s.make_ascii_lowercase();
|
||||
return s;
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use super::{use_filter, use_lowercase};
|
||||
use serde_yaml::{self, Mapping, Sequence, Value};
|
||||
|
||||
const MERGE_FIELDS: [&str; 6] = [
|
||||
"prepend-rules",
|
||||
"append-rules",
|
||||
"prepend-proxies",
|
||||
"append-proxies",
|
||||
"prepend-proxy-groups",
|
||||
"append-proxy-groups",
|
||||
];
|
||||
|
||||
pub fn use_merge(merge: Mapping, mut config: Mapping) -> Mapping {
|
||||
// 直接覆盖原字段
|
||||
use_lowercase(merge.clone())
|
||||
.into_iter()
|
||||
.for_each(|(key, value)| {
|
||||
config.insert(key, value);
|
||||
});
|
||||
|
||||
let merge_list = MERGE_FIELDS.iter().map(|s| s.to_string());
|
||||
let merge = use_filter(merge, &merge_list.collect(), true);
|
||||
|
||||
["rules", "proxies", "proxy-groups"]
|
||||
.iter()
|
||||
.for_each(|key_str| {
|
||||
let key_val = Value::from(key_str.to_string());
|
||||
|
||||
let mut list = Sequence::default();
|
||||
list = config.get(&key_val).map_or(list.clone(), |val| {
|
||||
val.as_sequence().map_or(list, |v| v.clone())
|
||||
});
|
||||
|
||||
let pre_key = Value::from(format!("prepend-{key_str}"));
|
||||
let post_key = Value::from(format!("append-{key_str}"));
|
||||
|
||||
if let Some(pre_val) = merge.get(&pre_key) {
|
||||
if pre_val.is_sequence() {
|
||||
let mut pre_val = pre_val.as_sequence().unwrap().clone();
|
||||
pre_val.extend(list);
|
||||
list = pre_val;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(post_val) = merge.get(&post_key) {
|
||||
if post_val.is_sequence() {
|
||||
list.extend(post_val.as_sequence().unwrap().clone());
|
||||
}
|
||||
}
|
||||
|
||||
config.insert(key_val, Value::from(list));
|
||||
});
|
||||
config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge() -> anyhow::Result<()> {
|
||||
let merge = r"
|
||||
prepend-rules:
|
||||
- prepend
|
||||
- 1123123
|
||||
append-rules:
|
||||
- append
|
||||
prepend-proxies:
|
||||
- 9999
|
||||
append-proxies:
|
||||
- 1111
|
||||
rules:
|
||||
- replace
|
||||
proxy-groups:
|
||||
- 123781923810
|
||||
tun:
|
||||
enable: true
|
||||
dns:
|
||||
enable: true
|
||||
";
|
||||
|
||||
let config = r"
|
||||
rules:
|
||||
- aaaaa
|
||||
script1: test
|
||||
";
|
||||
|
||||
let merge = serde_yaml::from_str::<Mapping>(merge)?;
|
||||
let config = serde_yaml::from_str::<Mapping>(config)?;
|
||||
|
||||
let result = serde_yaml::to_string(&use_merge(merge, config))?;
|
||||
|
||||
println!("{result}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
mod chain;
|
||||
mod field;
|
||||
mod merge;
|
||||
mod script;
|
||||
mod tun;
|
||||
|
||||
pub(self) use self::field::*;
|
||||
|
||||
use self::chain::*;
|
||||
use self::merge::*;
|
||||
use self::script::*;
|
||||
use self::tun::*;
|
||||
use crate::config::Config;
|
||||
use serde_yaml::Mapping;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
type ResultLog = Vec<(String, String)>;
|
||||
|
||||
/// Enhance mode
|
||||
/// 返回最终配置、该配置包含的键、和script执行的结果
|
||||
pub fn enhance() -> (Mapping, Vec<String>, HashMap<String, ResultLog>) {
|
||||
// config.yaml 的配置
|
||||
let clash_config = { Config::clash().latest().0.clone() };
|
||||
|
||||
let (clash_core, enable_tun, enable_builtin, enable_filter) = {
|
||||
let verge = Config::verge();
|
||||
let verge = verge.latest();
|
||||
(
|
||||
verge.clash_core.clone(),
|
||||
verge.enable_tun_mode.clone().unwrap_or(false),
|
||||
verge.enable_builtin_enhanced.clone().unwrap_or(true),
|
||||
verge.enable_clash_fields.clone().unwrap_or(true),
|
||||
)
|
||||
};
|
||||
|
||||
// 从profiles里拿东西
|
||||
let (mut config, chain, valid) = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
|
||||
let current = profiles.current_mapping().unwrap_or(Mapping::new());
|
||||
|
||||
let chain = match profiles.chain.as_ref() {
|
||||
Some(chain) => chain
|
||||
.iter()
|
||||
.filter_map(|uid| profiles.get_item(uid).ok())
|
||||
.filter_map(|item| <Option<ChainItem>>::from(item))
|
||||
.collect::<Vec<ChainItem>>(),
|
||||
None => vec![],
|
||||
};
|
||||
|
||||
let valid = profiles.valid.clone().unwrap_or(vec![]);
|
||||
|
||||
(current, chain, valid)
|
||||
};
|
||||
|
||||
let mut result_map = HashMap::new(); // 保存脚本日志
|
||||
let mut exists_keys = use_keys(&config); // 保存出现过的keys
|
||||
|
||||
let valid = use_valid_fields(valid);
|
||||
config = use_filter(config, &valid, enable_filter);
|
||||
|
||||
// 处理用户的profile
|
||||
chain.into_iter().for_each(|item| match item.data {
|
||||
ChainType::Merge(merge) => {
|
||||
exists_keys.extend(use_keys(&merge));
|
||||
config = use_merge(merge, config.to_owned());
|
||||
config = use_filter(config.to_owned(), &valid, enable_filter);
|
||||
}
|
||||
ChainType::Script(script) => {
|
||||
let mut logs = vec![];
|
||||
|
||||
match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, res_logs)) => {
|
||||
exists_keys.extend(use_keys(&res_config));
|
||||
config = use_filter(res_config, &valid, enable_filter);
|
||||
logs.extend(res_logs);
|
||||
}
|
||||
Err(err) => logs.push(("exception".into(), err.to_string())),
|
||||
}
|
||||
|
||||
result_map.insert(item.uid, logs);
|
||||
}
|
||||
});
|
||||
|
||||
// 合并默认的config
|
||||
for (key, value) in clash_config.into_iter() {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
let clash_fields = use_clash_fields();
|
||||
|
||||
// 内建脚本最后跑
|
||||
if enable_builtin {
|
||||
ChainItem::builtin()
|
||||
.into_iter()
|
||||
.filter(|(s, _)| s.is_support(clash_core.as_ref()))
|
||||
.map(|(_, c)| c)
|
||||
.for_each(|item| {
|
||||
log::debug!(target: "app", "run builtin script {}", item.uid);
|
||||
|
||||
match item.data {
|
||||
ChainType::Script(script) => match use_script(script, config.to_owned()) {
|
||||
Ok((res_config, _)) => {
|
||||
config = use_filter(res_config, &clash_fields, enable_filter);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "builtin script error `{err}`");
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
config = use_filter(config, &clash_fields, enable_filter);
|
||||
config = use_tun(config, enable_tun);
|
||||
config = use_sort(config, enable_filter);
|
||||
|
||||
let mut exists_set = HashSet::new();
|
||||
exists_set.extend(exists_keys.into_iter().filter(|s| clash_fields.contains(s)));
|
||||
exists_keys = exists_set.into_iter().collect();
|
||||
|
||||
(config, exists_keys, result_map)
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use super::use_lowercase;
|
||||
use anyhow::Result;
|
||||
use serde_yaml::Mapping;
|
||||
|
||||
pub fn use_script(script: String, config: Mapping) -> Result<(Mapping, Vec<(String, String)>)> {
|
||||
use rquickjs::{Context, Func, Runtime};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
let runtime = Runtime::new().unwrap();
|
||||
let context = Context::full(&runtime).unwrap();
|
||||
let outputs = Arc::new(Mutex::new(vec![]));
|
||||
|
||||
let copy_outputs = outputs.clone();
|
||||
let result = context.with(|ctx| -> Result<Mapping> {
|
||||
ctx.globals().set(
|
||||
"__verge_log__",
|
||||
Func::from(move |level: String, data: String| {
|
||||
let mut out = copy_outputs.lock().unwrap();
|
||||
out.push((level, data));
|
||||
}),
|
||||
)?;
|
||||
|
||||
ctx.eval(
|
||||
r#"var console = Object.freeze({
|
||||
log(data){__verge_log__("log",JSON.stringify(data))},
|
||||
info(data){__verge_log__("info",JSON.stringify(data))},
|
||||
error(data){__verge_log__("error",JSON.stringify(data))},
|
||||
debug(data){__verge_log__("debug",JSON.stringify(data))},
|
||||
});"#,
|
||||
)?;
|
||||
|
||||
let config = use_lowercase(config.clone());
|
||||
let config_str = serde_json::to_string(&config)?;
|
||||
|
||||
let code = format!(
|
||||
r#"try{{
|
||||
{script};
|
||||
JSON.stringify(main({config_str})||'')
|
||||
}} catch(err) {{
|
||||
`__error_flag__ ${{err.toString()}}`
|
||||
}}"#
|
||||
);
|
||||
let result: String = ctx.eval(code.as_str())?;
|
||||
if result.starts_with("__error_flag__") {
|
||||
anyhow::bail!(result[15..].to_owned());
|
||||
}
|
||||
if result == "\"\"" {
|
||||
anyhow::bail!("main function should return object");
|
||||
}
|
||||
return Ok(serde_json::from_str::<Mapping>(result.as_str())?);
|
||||
});
|
||||
|
||||
let mut out = outputs.lock().unwrap();
|
||||
match result {
|
||||
Ok(config) => Ok((use_lowercase(config), out.to_vec())),
|
||||
Err(err) => {
|
||||
out.push(("exception".into(), err.to_string()));
|
||||
Ok((config, out.to_vec()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script() {
|
||||
let script = r#"
|
||||
function main(config) {
|
||||
if (Array.isArray(config.rules)) {
|
||||
config.rules = [...config.rules, "add"];
|
||||
}
|
||||
console.log(config);
|
||||
config.proxies = ["111"];
|
||||
return config;
|
||||
}
|
||||
"#;
|
||||
|
||||
let config = r#"
|
||||
rules:
|
||||
- 111
|
||||
- 222
|
||||
tun:
|
||||
enable: false
|
||||
dns:
|
||||
enable: false
|
||||
"#;
|
||||
|
||||
let config = serde_yaml::from_str(config).unwrap();
|
||||
let (config, results) = use_script(script.into(), config).unwrap();
|
||||
|
||||
let config_str = serde_yaml::to_string(&config).unwrap();
|
||||
|
||||
println!("{config_str}");
|
||||
|
||||
dbg!(results);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use serde_yaml::{Mapping, Value};
|
||||
|
||||
macro_rules! revise {
|
||||
($map: expr, $key: expr, $val: expr) => {
|
||||
let ret_key = Value::String($key.into());
|
||||
$map.insert(ret_key, Value::from($val));
|
||||
};
|
||||
}
|
||||
|
||||
// if key not exists then append value
|
||||
macro_rules! append {
|
||||
($map: expr, $key: expr, $val: expr) => {
|
||||
let ret_key = Value::String($key.into());
|
||||
if !$map.contains_key(&ret_key) {
|
||||
$map.insert(ret_key, Value::from($val));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn use_tun(mut config: Mapping, enable: bool) -> Mapping {
|
||||
let tun_key = Value::from("tun");
|
||||
let tun_val = config.get(&tun_key);
|
||||
|
||||
if !enable && tun_val.is_none() {
|
||||
return config;
|
||||
}
|
||||
|
||||
let mut tun_val = tun_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
|
||||
revise!(tun_val, "enable", enable);
|
||||
if enable {
|
||||
append!(tun_val, "stack", "gvisor");
|
||||
append!(tun_val, "dns-hijack", vec!["any:53"]);
|
||||
append!(tun_val, "auto-route", true);
|
||||
append!(tun_val, "auto-detect-interface", true);
|
||||
}
|
||||
|
||||
revise!(config, "tun", tun_val);
|
||||
|
||||
if enable {
|
||||
use_dns_for_tun(config)
|
||||
} else {
|
||||
config
|
||||
}
|
||||
}
|
||||
|
||||
fn use_dns_for_tun(mut config: Mapping) -> Mapping {
|
||||
let dns_key = Value::from("dns");
|
||||
let dns_val = config.get(&dns_key);
|
||||
|
||||
let mut dns_val = dns_val.map_or(Mapping::new(), |val| {
|
||||
val.as_mapping().cloned().unwrap_or(Mapping::new())
|
||||
});
|
||||
|
||||
// 开启tun将同时开启dns
|
||||
revise!(dns_val, "enable", true);
|
||||
|
||||
append!(dns_val, "enhanced-mode", "fake-ip");
|
||||
append!(dns_val, "fake-ip-range", "198.18.0.1/16");
|
||||
append!(
|
||||
dns_val,
|
||||
"nameserver",
|
||||
vec!["114.114.114.114", "223.5.5.5", "8.8.8.8"]
|
||||
);
|
||||
append!(dns_val, "fallback", vec![] as Vec<&str>);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
append!(
|
||||
dns_val,
|
||||
"fake-ip-filter",
|
||||
vec![
|
||||
"dns.msftncsi.com",
|
||||
"www.msftncsi.com",
|
||||
"www.msftconnecttest.com"
|
||||
]
|
||||
);
|
||||
revise!(config, "dns", dns_val);
|
||||
config
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
//!
|
||||
//! feat mod 里的函数主要用于
|
||||
//! - hotkey 快捷键
|
||||
//! - timer 定时器
|
||||
//! - cmds 页面调用
|
||||
//!
|
||||
use crate::config::*;
|
||||
use crate::core::*;
|
||||
use crate::log_err;
|
||||
use crate::utils::resolve;
|
||||
use anyhow::{bail, Result};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use wry::application::clipboard::Clipboard;
|
||||
|
||||
// 打开面板
|
||||
pub fn open_dashboard() {
|
||||
let handle = handle::Handle::global();
|
||||
let app_handle = handle.app_handle.lock();
|
||||
if let Some(app_handle) = app_handle.as_ref() {
|
||||
resolve::create_window(app_handle);
|
||||
}
|
||||
}
|
||||
|
||||
// 重启clash
|
||||
pub fn restart_clash_core() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match CoreManager::global().run_core().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
log::error!(target:"app", "{err}");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换模式 rule/global/direct/script mode
|
||||
pub fn change_clash_mode(mode: String) {
|
||||
let mut mapping = Mapping::new();
|
||||
mapping.insert(Value::from("mode"), mode.clone().into());
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
log::debug!(target: "app", "change clash mode to {mode}");
|
||||
|
||||
match clash_api::patch_configs(&mapping).await {
|
||||
Ok(_) => {
|
||||
// 更新配置
|
||||
Config::clash().data().patch_config(mapping);
|
||||
|
||||
if Config::clash().data().save_config().is_ok() {
|
||||
handle::Handle::refresh_clash();
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
}
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换系统代理
|
||||
pub fn toggle_system_proxy() {
|
||||
let enable = Config::verge().draft().enable_system_proxy.clone();
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match patch_verge(IVerge {
|
||||
enable_system_proxy: Some(!enable),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开系统代理
|
||||
pub fn enable_system_proxy() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match patch_verge(IVerge {
|
||||
enable_system_proxy: Some(true),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭系统代理
|
||||
pub fn disable_system_proxy() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match patch_verge(IVerge {
|
||||
enable_system_proxy: Some(false),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 切换tun模式
|
||||
pub fn toggle_tun_mode() {
|
||||
let enable = Config::verge().data().enable_tun_mode.clone();
|
||||
let enable = enable.unwrap_or(false);
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match patch_verge(IVerge {
|
||||
enable_tun_mode: Some(!enable),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开tun模式
|
||||
pub fn enable_tun_mode() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match patch_verge(IVerge {
|
||||
enable_tun_mode: Some(true),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭tun模式
|
||||
pub fn disable_tun_mode() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
match patch_verge(IVerge {
|
||||
enable_tun_mode: Some(false),
|
||||
..IVerge::default()
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(_) => handle::Handle::refresh_verge(),
|
||||
Err(err) => log::error!(target: "app", "{err}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 修改clash的配置
|
||||
pub async fn patch_clash(patch: Mapping) -> Result<()> {
|
||||
Config::clash().draft().patch_config(patch.clone());
|
||||
|
||||
match {
|
||||
let mixed_port = patch.get("mixed-port");
|
||||
if mixed_port.is_some() {
|
||||
let changed = mixed_port != Config::clash().data().0.get("mixed-port");
|
||||
// 检查端口占用
|
||||
if changed {
|
||||
if let Some(port) = mixed_port.clone().unwrap().as_u64() {
|
||||
if !port_scanner::local_port_available(port as u16) {
|
||||
Config::clash().discard();
|
||||
bail!("port already in use");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 激活配置
|
||||
if mixed_port.is_some()
|
||||
|| patch.get("secret").is_some()
|
||||
|| patch.get("external-controller").is_some()
|
||||
{
|
||||
Config::generate()?;
|
||||
CoreManager::global().run_core().await?;
|
||||
handle::Handle::refresh_clash();
|
||||
}
|
||||
|
||||
// 更新系统代理
|
||||
if mixed_port.is_some() {
|
||||
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
||||
}
|
||||
|
||||
if patch.get("mode").is_some() {
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
}
|
||||
|
||||
Config::runtime().latest().patch_config(patch);
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
} {
|
||||
Ok(()) => {
|
||||
Config::clash().apply();
|
||||
Config::clash().data().save_config()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::clash().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改verge的配置
|
||||
/// 一般都是一个个的修改
|
||||
pub async fn patch_verge(patch: IVerge) -> Result<()> {
|
||||
Config::verge().draft().patch_config(patch.clone());
|
||||
|
||||
let tun_mode = patch.enable_tun_mode;
|
||||
let auto_launch = patch.enable_auto_launch;
|
||||
let system_proxy = patch.enable_system_proxy;
|
||||
let proxy_bypass = patch.system_proxy_bypass;
|
||||
let language = patch.language;
|
||||
|
||||
match {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let service_mode = patch.enable_service_mode;
|
||||
|
||||
if service_mode.is_some() {
|
||||
log::debug!(target: "app", "change service mode to {}", service_mode.unwrap());
|
||||
|
||||
Config::generate()?;
|
||||
CoreManager::global().run_core().await?;
|
||||
} else if tun_mode.is_some() {
|
||||
update_core_config().await?;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if tun_mode.is_some() {
|
||||
update_core_config().await?;
|
||||
}
|
||||
|
||||
if auto_launch.is_some() {
|
||||
sysopt::Sysopt::global().update_launch()?;
|
||||
}
|
||||
if system_proxy.is_some() || proxy_bypass.is_some() {
|
||||
sysopt::Sysopt::global().update_sysproxy()?;
|
||||
sysopt::Sysopt::global().guard_proxy();
|
||||
}
|
||||
|
||||
if let Some(true) = patch.enable_proxy_guard {
|
||||
sysopt::Sysopt::global().guard_proxy();
|
||||
}
|
||||
|
||||
if let Some(hotkeys) = patch.hotkeys {
|
||||
hotkey::Hotkey::global().update(hotkeys)?;
|
||||
}
|
||||
|
||||
if language.is_some() {
|
||||
handle::Handle::update_systray()?;
|
||||
} else if system_proxy.or(tun_mode).is_some() {
|
||||
handle::Handle::update_systray_part()?;
|
||||
}
|
||||
|
||||
<Result<()>>::Ok(())
|
||||
} {
|
||||
Ok(()) => {
|
||||
Config::verge().apply();
|
||||
Config::verge().data().save_file()?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
Config::verge().discard();
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新某个profile
|
||||
/// 如果更新当前配置就激活配置
|
||||
pub async fn update_profile(uid: String, option: Option<PrfOption>) -> Result<()> {
|
||||
let url_opt = {
|
||||
let profiles = Config::profiles();
|
||||
let profiles = profiles.latest();
|
||||
let item = profiles.get_item(&uid)?;
|
||||
let is_remote = item.itype.as_ref().map_or(false, |s| s == "remote");
|
||||
|
||||
if !is_remote {
|
||||
None // 直接更新
|
||||
} else if item.url.is_none() {
|
||||
bail!("failed to get the profile item url");
|
||||
} else {
|
||||
Some((item.url.clone().unwrap(), item.option.clone()))
|
||||
}
|
||||
};
|
||||
|
||||
let should_update = match url_opt {
|
||||
Some((url, opt)) => {
|
||||
let merged_opt = PrfOption::merge(opt, option);
|
||||
let item = PrfItem::from_url(&url, None, None, merged_opt).await?;
|
||||
|
||||
let profiles = Config::profiles();
|
||||
let mut profiles = profiles.latest();
|
||||
profiles.update_item(uid.clone(), item)?;
|
||||
|
||||
Some(uid) == profiles.get_current()
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
if should_update {
|
||||
update_core_config().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新配置
|
||||
async fn update_core_config() -> Result<()> {
|
||||
match CoreManager::global().update_config().await {
|
||||
Ok(_) => {
|
||||
handle::Handle::refresh_clash();
|
||||
handle::Handle::notice_message("set_config::ok", "ok");
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
handle::Handle::notice_message("set_config::error", format!("{err}"));
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// copy env variable
|
||||
pub fn copy_clash_env(option: &str) {
|
||||
let port = { Config::clash().data().get_client_info().port };
|
||||
let http_proxy = format!("http://127.0.0.1:{}", port);
|
||||
let socks5_proxy = format!("socks5://127.0.0.1:{}", port);
|
||||
|
||||
let sh = format!("export https_proxy={http_proxy} http_proxy={http_proxy} all_proxy={socks5_proxy}");
|
||||
let cmd: String = format!("set http_proxy={http_proxy} \n set https_proxy={http_proxy}");
|
||||
let ps: String = format!("$env:HTTP_PROXY=\"{http_proxy}\"; $env:HTTPS_PROXY=\"{http_proxy}\"");
|
||||
|
||||
let mut cliboard = Clipboard::new();
|
||||
|
||||
match option {
|
||||
"sh" => cliboard.write_text(sh),
|
||||
"cmd" => cliboard.write_text(cmd),
|
||||
"ps" => cliboard.write_text(ps),
|
||||
_ => log::error!(target: "app", "copy_clash_env: Invalid option! {option}"),
|
||||
}
|
||||
}
|
||||
@@ -1,175 +1,140 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
mod cmds;
|
||||
mod config;
|
||||
mod core;
|
||||
mod states;
|
||||
mod enhance;
|
||||
mod feat;
|
||||
mod utils;
|
||||
|
||||
use crate::{
|
||||
core::VergeConfig,
|
||||
utils::{dirs, resolve, server},
|
||||
};
|
||||
use tauri::{
|
||||
api, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
|
||||
};
|
||||
use crate::utils::{init, resolve, server};
|
||||
use tauri::{api, SystemTray};
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
if server::check_singleton().is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
}
|
||||
// 单例检测
|
||||
if server::check_singleton().is_err() {
|
||||
println!("app exists");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
dirs::init_portable_flag();
|
||||
}
|
||||
crate::log_err!(init::init_config());
|
||||
|
||||
let tray_menu = SystemTrayMenu::new()
|
||||
.add_item(CustomMenuItem::new("open_window", "Show"))
|
||||
.add_item(CustomMenuItem::new("system_proxy", "System Proxy"))
|
||||
.add_item(CustomMenuItem::new("tun_mode", "Tun Mode"))
|
||||
.add_item(CustomMenuItem::new("restart_clash", "Restart Clash"))
|
||||
.add_native_item(SystemTrayMenuItem::Separator)
|
||||
.add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"));
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.system_tray(SystemTray::new())
|
||||
.setup(|app| Ok(resolve::resolve_setup(app)))
|
||||
.on_system_tray_event(core::tray::Tray::on_system_tray_event)
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
cmds::get_sys_proxy,
|
||||
cmds::open_app_dir,
|
||||
cmds::open_logs_dir,
|
||||
cmds::open_web_url,
|
||||
cmds::open_core_dir,
|
||||
// cmds::kill_sidecar,
|
||||
cmds::restart_sidecar,
|
||||
cmds::grant_permission,
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::get_clash_logs,
|
||||
cmds::patch_clash_config,
|
||||
cmds::change_clash_core,
|
||||
cmds::get_runtime_config,
|
||||
cmds::get_runtime_yaml,
|
||||
cmds::get_runtime_exists,
|
||||
cmds::get_runtime_logs,
|
||||
// verge
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
// cmds::update_hotkeys,
|
||||
// profile
|
||||
cmds::get_profiles,
|
||||
cmds::enhance_profiles,
|
||||
cmds::patch_profiles_config,
|
||||
cmds::view_profile,
|
||||
cmds::patch_profile,
|
||||
cmds::create_profile,
|
||||
cmds::import_profile,
|
||||
cmds::update_profile,
|
||||
cmds::delete_profile,
|
||||
cmds::read_profile_file,
|
||||
cmds::save_profile_file,
|
||||
// service mode
|
||||
cmds::service::check_service,
|
||||
cmds::service::install_service,
|
||||
cmds::service::uninstall_service,
|
||||
]);
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.manage(states::VergeState::default())
|
||||
.manage(states::ClashState::default())
|
||||
.manage(states::ProfilesState::default())
|
||||
.setup(|app| Ok(resolve::resolve_setup(app)))
|
||||
.system_tray(SystemTray::new().with_menu(tray_menu))
|
||||
.on_system_tray_event(move |app_handle, event| match event {
|
||||
SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
|
||||
"open_window" => {
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
window.unminimize().unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::{Menu, MenuItem, Submenu};
|
||||
|
||||
builder = builder.menu(
|
||||
Menu::new().add_submenu(Submenu::new(
|
||||
"Edit",
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::Undo)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::SelectAll)
|
||||
.add_native_item(MenuItem::CloseWindow)
|
||||
.add_native_item(MenuItem::Quit),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
let app = builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
app.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::ExitRequested { api, .. } => {
|
||||
api.prevent_exit();
|
||||
}
|
||||
"system_proxy" => {
|
||||
let verge_state = app_handle.state::<states::VergeState>();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
|
||||
let old_value = verge.config.enable_system_proxy.clone().unwrap_or(false);
|
||||
let new_value = !old_value;
|
||||
|
||||
match verge.patch_config(VergeConfig {
|
||||
enable_system_proxy: Some(new_value),
|
||||
..VergeConfig::default()
|
||||
}) {
|
||||
Ok(_) => verge.update_systray(app_handle).unwrap(),
|
||||
Err(err) => log::error!("{err}"),
|
||||
}
|
||||
tauri::RunEvent::Exit => {
|
||||
resolve::resolve_reset();
|
||||
api::process::kill_children();
|
||||
app_handle.exit(0);
|
||||
}
|
||||
"tun_mode" => {
|
||||
let verge_state = app_handle.state::<states::VergeState>();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
use tauri::Manager;
|
||||
|
||||
let old_value = verge.config.enable_tun_mode.clone().unwrap_or(false);
|
||||
let new_value = !old_value;
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
api.prevent_close();
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
|
||||
match verge.patch_config(VergeConfig {
|
||||
enable_tun_mode: Some(new_value),
|
||||
..VergeConfig::default()
|
||||
}) {
|
||||
Ok(_) => verge.update_systray(app_handle).unwrap(),
|
||||
Err(err) => log::error!("{err}"),
|
||||
}
|
||||
app_handle.get_window("main").map(|win| {
|
||||
let _ = win.hide();
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
"restart_clash" => {
|
||||
let clash_state = app_handle.state::<states::ClashState>();
|
||||
let profiles_state = app_handle.state::<states::ProfilesState>();
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
|
||||
crate::log_if_err!(clash.restart_sidecar(&mut profiles));
|
||||
}
|
||||
"quit" => {
|
||||
resolve::resolve_reset(app_handle);
|
||||
api::process::kill_children();
|
||||
std::process::exit(0);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => {
|
||||
if label == "main" {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, true);
|
||||
}
|
||||
tauri::WindowEvent::Moved(_) | tauri::WindowEvent::Resized(_) => {
|
||||
let _ = resolve::save_window_size_position(&app_handle, false);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
#[cfg(target_os = "windows")]
|
||||
SystemTrayEvent::LeftClick { .. } => {
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
window.unminimize().unwrap();
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
// common
|
||||
cmds::restart_sidecar,
|
||||
cmds::get_sys_proxy,
|
||||
cmds::get_cur_proxy,
|
||||
cmds::kill_sidecars,
|
||||
cmds::open_app_dir,
|
||||
cmds::open_logs_dir,
|
||||
// clash
|
||||
cmds::get_clash_info,
|
||||
cmds::patch_clash_config,
|
||||
// verge
|
||||
cmds::get_verge_config,
|
||||
cmds::patch_verge_config,
|
||||
// profile
|
||||
cmds::view_profile,
|
||||
cmds::patch_profile,
|
||||
cmds::create_profile,
|
||||
cmds::import_profile,
|
||||
cmds::update_profile,
|
||||
cmds::delete_profile,
|
||||
cmds::select_profile,
|
||||
cmds::get_profiles,
|
||||
cmds::sync_profiles,
|
||||
cmds::enhance_profiles,
|
||||
cmds::change_profile_chain,
|
||||
cmds::change_profile_valid,
|
||||
cmds::read_profile_file,
|
||||
cmds::save_profile_file
|
||||
]);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::{Menu, MenuItem, Submenu};
|
||||
|
||||
let submenu_file = Submenu::new(
|
||||
"File",
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::Undo)
|
||||
.add_native_item(MenuItem::Redo)
|
||||
.add_native_item(MenuItem::Copy)
|
||||
.add_native_item(MenuItem::Paste)
|
||||
.add_native_item(MenuItem::Cut)
|
||||
.add_native_item(MenuItem::SelectAll),
|
||||
);
|
||||
builder = builder.menu(Menu::new().add_submenu(submenu_file));
|
||||
}
|
||||
|
||||
builder
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while running tauri application")
|
||||
.run(|app_handle, e| match e {
|
||||
tauri::RunEvent::WindowEvent { label, event, .. } => match event {
|
||||
tauri::WindowEvent::CloseRequested { api, .. } => {
|
||||
let app_handle = app_handle.clone();
|
||||
api.prevent_close();
|
||||
app_handle.get_window(&label).unwrap().hide().unwrap();
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
tauri::RunEvent::ExitRequested { .. } => {
|
||||
resolve::resolve_reset(app_handle);
|
||||
api::process::kill_children();
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
use crate::core::{Clash, Profiles, Verge};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ProfilesState(pub Arc<Mutex<Profiles>>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClashState(pub Arc<Mutex<Clash>>);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct VergeState(pub Arc<Mutex<Verge>>);
|
||||
@@ -1,23 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned + Default>(path: PathBuf) -> T {
|
||||
let yaml_str = fs::read_to_string(path).unwrap_or("".into());
|
||||
serde_yaml::from_str::<T>(&yaml_str).unwrap_or(T::default())
|
||||
}
|
||||
|
||||
/// save the data to the file
|
||||
/// can set `prefix` string to add some comments
|
||||
pub fn save_yaml<T: Serialize>(path: PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||
let data_str = serde_yaml::to_string(data)?;
|
||||
|
||||
let yaml_str = match prefix {
|
||||
Some(prefix) => format!("{prefix}{data_str}"),
|
||||
None => data_str,
|
||||
};
|
||||
|
||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||
fs::write(path, yaml_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::env::temp_dir;
|
||||
use anyhow::Result;
|
||||
use std::path::PathBuf;
|
||||
use tauri::utils::platform::current_exe;
|
||||
use tauri::{
|
||||
api::path::{home_dir, resource_dir},
|
||||
Env, PackageInfo,
|
||||
api::path::{home_dir, resource_dir},
|
||||
Env, PackageInfo,
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
@@ -14,77 +13,147 @@ static APP_DIR: &str = "clash-verge-dev";
|
||||
static CLASH_CONFIG: &str = "config.yaml";
|
||||
static VERGE_CONFIG: &str = "verge.yaml";
|
||||
static PROFILE_YAML: &str = "profiles.yaml";
|
||||
static PROFILE_TEMP: &str = "clash-verge-runtime.yaml";
|
||||
|
||||
static mut RESOURCE_DIR: Option<PathBuf> = None;
|
||||
|
||||
/// portable flag
|
||||
#[allow(unused)]
|
||||
static mut PORTABLE_FLAG: bool = false;
|
||||
|
||||
/// initialize portable flag
|
||||
pub unsafe fn init_portable_flag() {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let exe = current_exe().unwrap();
|
||||
let dir = exe.parent().unwrap();
|
||||
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||
pub static mut APP_VERSION: &str = "v1.2.0";
|
||||
|
||||
if dir.exists() {
|
||||
PORTABLE_FLAG = true;
|
||||
/// initialize portable flag
|
||||
#[cfg(target_os = "windows")]
|
||||
pub unsafe fn init_portable_flag() -> Result<()> {
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
let exe = current_exe()?;
|
||||
|
||||
if let Some(dir) = exe.parent() {
|
||||
let dir = PathBuf::from(dir).join(".config/PORTABLE");
|
||||
|
||||
if dir.exists() {
|
||||
PORTABLE_FLAG = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get the verge app home dir
|
||||
pub fn app_home_dir() -> PathBuf {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
if !PORTABLE_FLAG {
|
||||
home_dir().unwrap().join(".config").join(APP_DIR)
|
||||
} else {
|
||||
let app_exe = current_exe().unwrap();
|
||||
let app_exe = dunce::canonicalize(app_exe).unwrap();
|
||||
let app_dir = app_exe.parent().unwrap();
|
||||
PathBuf::from(app_dir).join(".config").join(APP_DIR)
|
||||
}
|
||||
}
|
||||
pub fn app_home_dir() -> Result<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
use tauri::utils::platform::current_exe;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
home_dir().unwrap().join(".config").join(APP_DIR)
|
||||
if !PORTABLE_FLAG {
|
||||
Ok(home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get app home dir"))?
|
||||
.join(".config")
|
||||
.join(APP_DIR))
|
||||
} else {
|
||||
let app_exe = current_exe()?;
|
||||
let app_exe = dunce::canonicalize(app_exe)?;
|
||||
let app_dir = app_exe
|
||||
.parent()
|
||||
.ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
|
||||
Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
Ok(home_dir()
|
||||
.ok_or(anyhow::anyhow!("failed to get the app home dir"))?
|
||||
.join(".config")
|
||||
.join(APP_DIR))
|
||||
}
|
||||
|
||||
/// get the resources dir
|
||||
pub fn app_resources_dir(package_info: &PackageInfo) -> PathBuf {
|
||||
resource_dir(package_info, &Env::default())
|
||||
.unwrap()
|
||||
.join("resources")
|
||||
pub fn app_resources_dir(package_info: &PackageInfo) -> Result<PathBuf> {
|
||||
let res_dir = resource_dir(package_info, &Env::default())
|
||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||
.join("resources");
|
||||
|
||||
unsafe {
|
||||
RESOURCE_DIR = Some(res_dir.clone());
|
||||
|
||||
let ver = package_info.version.to_string();
|
||||
let ver_str = format!("v{ver}");
|
||||
APP_VERSION = Box::leak(Box::new(ver_str));
|
||||
}
|
||||
|
||||
Ok(res_dir)
|
||||
}
|
||||
|
||||
/// profiles dir
|
||||
pub fn app_profiles_dir() -> PathBuf {
|
||||
app_home_dir().join("profiles")
|
||||
pub fn app_profiles_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("profiles"))
|
||||
}
|
||||
|
||||
/// logs dir
|
||||
pub fn app_logs_dir() -> PathBuf {
|
||||
app_home_dir().join("logs")
|
||||
pub fn app_logs_dir() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join("logs"))
|
||||
}
|
||||
|
||||
pub fn clash_path() -> PathBuf {
|
||||
app_home_dir().join(CLASH_CONFIG)
|
||||
pub fn clash_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
||||
}
|
||||
|
||||
pub fn verge_path() -> PathBuf {
|
||||
app_home_dir().join(VERGE_CONFIG)
|
||||
pub fn verge_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(VERGE_CONFIG))
|
||||
}
|
||||
|
||||
pub fn profiles_path() -> PathBuf {
|
||||
app_home_dir().join(PROFILE_YAML)
|
||||
pub fn profiles_path() -> Result<PathBuf> {
|
||||
Ok(app_home_dir()?.join(PROFILE_YAML))
|
||||
}
|
||||
|
||||
pub fn profiles_temp_path() -> PathBuf {
|
||||
#[cfg(not(feature = "debug-yml"))]
|
||||
return temp_dir().join(PROFILE_TEMP);
|
||||
#[allow(unused)]
|
||||
pub fn app_res_dir() -> Result<PathBuf> {
|
||||
unsafe {
|
||||
Ok(RESOURCE_DIR
|
||||
.clone()
|
||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug-yml")]
|
||||
return app_home_dir().join(PROFILE_TEMP);
|
||||
pub fn clash_pid_path() -> Result<PathBuf> {
|
||||
unsafe {
|
||||
Ok(RESOURCE_DIR
|
||||
.clone()
|
||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?
|
||||
.join("clash.pid"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn service_path() -> Result<PathBuf> {
|
||||
unsafe {
|
||||
let res_dir = RESOURCE_DIR
|
||||
.clone()
|
||||
.ok_or(anyhow::anyhow!("failed to get the resource dir"))?;
|
||||
Ok(res_dir.join("clash-verge-service.exe"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn service_log_file() -> Result<PathBuf> {
|
||||
use chrono::Local;
|
||||
|
||||
let log_dir = app_logs_dir()?.join("service");
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
let _ = std::fs::create_dir_all(&log_dir);
|
||||
|
||||
Ok(log_file)
|
||||
}
|
||||
|
||||
pub fn path_to_str(path: &PathBuf) -> Result<&str> {
|
||||
let path_str = path
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.ok_or(anyhow::anyhow!("failed to get path from {:?}", path))?;
|
||||
Ok(path_str)
|
||||
}
|
||||
|
||||
@@ -1,94 +1,172 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use nanoid::nanoid;
|
||||
use std::str::FromStr;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use serde_yaml::{Mapping, Value};
|
||||
use std::{fs, path::PathBuf, str::FromStr};
|
||||
|
||||
pub fn get_now() -> usize {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as _
|
||||
/// read data from yaml as struct T
|
||||
pub fn read_yaml<T: DeserializeOwned>(path: &PathBuf) -> Result<T> {
|
||||
if !path.exists() {
|
||||
bail!("file not found \"{}\"", path.display());
|
||||
}
|
||||
|
||||
let yaml_str = fs::read_to_string(&path)
|
||||
.with_context(|| format!("failed to read the file \"{}\"", path.display()))?;
|
||||
|
||||
serde_yaml::from_str::<T>(&yaml_str).with_context(|| {
|
||||
format!(
|
||||
"failed to read the file with yaml format \"{}\"",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// read mapping from yaml fix #165
|
||||
pub fn read_merge_mapping(path: &PathBuf) -> Result<Mapping> {
|
||||
let mut val: Value = read_yaml(path)?;
|
||||
val.apply_merge()
|
||||
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
|
||||
|
||||
Ok(val
|
||||
.as_mapping()
|
||||
.ok_or(anyhow!(
|
||||
"failed to transform to yaml mapping \"{}\"",
|
||||
path.display()
|
||||
))?
|
||||
.to_owned())
|
||||
}
|
||||
|
||||
/// save the data to the file
|
||||
/// can set `prefix` string to add some comments
|
||||
pub fn save_yaml<T: Serialize>(path: &PathBuf, data: &T, prefix: Option<&str>) -> Result<()> {
|
||||
let data_str = serde_yaml::to_string(data)?;
|
||||
|
||||
let yaml_str = match prefix {
|
||||
Some(prefix) => format!("{prefix}\n\n{data_str}"),
|
||||
None => data_str,
|
||||
};
|
||||
|
||||
let path_str = path.as_os_str().to_string_lossy().to_string();
|
||||
fs::write(path, yaml_str.as_bytes())
|
||||
.with_context(|| format!("failed to save file \"{path_str}\""))
|
||||
}
|
||||
|
||||
const ALPHABET: [char; 62] = [
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
||||
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
||||
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||
'V', 'W', 'X', 'Y', 'Z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
|
||||
'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B',
|
||||
'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
|
||||
'V', 'W', 'X', 'Y', 'Z',
|
||||
];
|
||||
|
||||
/// generate the uid
|
||||
pub fn get_uid(prefix: &str) -> String {
|
||||
let id = nanoid!(11, &ALPHABET);
|
||||
format!("{prefix}{id}")
|
||||
let id = nanoid!(11, &ALPHABET);
|
||||
format!("{prefix}{id}")
|
||||
}
|
||||
|
||||
/// parse the string
|
||||
/// xxx=123123; => 123123
|
||||
pub fn parse_str<T: FromStr>(target: &str, key: &str) -> Option<T> {
|
||||
match target.find(key) {
|
||||
Some(idx) => {
|
||||
let idx = idx + key.len();
|
||||
let value = &target[idx..];
|
||||
match match value.split(';').nth(0) {
|
||||
Some(value) => value.trim().parse(),
|
||||
None => value.trim().parse(),
|
||||
} {
|
||||
Ok(r) => Some(r),
|
||||
Err(_) => None,
|
||||
}
|
||||
target.find(key).and_then(|idx| {
|
||||
let idx = idx + key.len();
|
||||
let value = &target[idx..];
|
||||
|
||||
match value.split(';').nth(0) {
|
||||
Some(value) => value.trim().parse(),
|
||||
None => value.trim().parse(),
|
||||
}
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
/// open file
|
||||
/// use vscode by default
|
||||
pub fn open_file(path: PathBuf) -> Result<()> {
|
||||
#[cfg(target_os = "macos")]
|
||||
let code = "Visual Studio Code";
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let code = "code";
|
||||
|
||||
// use vscode first
|
||||
if let Err(err) = open::with(&path, code) {
|
||||
log::error!(target: "app", "failed to open file with VScode `{err}`");
|
||||
// default open
|
||||
open::that(path)?;
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_if_err {
|
||||
($result: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!("{err}");
|
||||
macro_rules! error {
|
||||
($result: expr) => {
|
||||
log::error!(target: "app", "{}", $result);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_err {
|
||||
($result: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::error!(target: "app", "{err}");
|
||||
}
|
||||
};
|
||||
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(_) = $result {
|
||||
log::error!(target: "app", "{}", $err_str);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace_err {
|
||||
($result: expr, $err_str: expr) => {
|
||||
if let Err(err) = $result {
|
||||
log::trace!(target: "app", "{}, err {}", $err_str, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// wrap the anyhow error
|
||||
/// transform the error to String
|
||||
#[macro_export]
|
||||
macro_rules! wrap_err {
|
||||
($stat: expr) => {
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!("{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
($stat: expr) => {
|
||||
match $stat {
|
||||
Ok(a) => Ok(a),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "{}", err.to_string());
|
||||
Err(format!("{}", err.to_string()))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// return the string literal error
|
||||
#[macro_export]
|
||||
macro_rules! ret_err {
|
||||
($str: expr) => {
|
||||
return Err($str.into())
|
||||
};
|
||||
($str: expr) => {
|
||||
return Err($str.into())
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_value() {
|
||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||
let test_2 = "attachment; filename=Clash.yaml";
|
||||
let test_1 = "upload=111; download=2222; total=3333; expire=444";
|
||||
let test_2 = "attachment; filename=Clash.yaml";
|
||||
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
||||
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
|
||||
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
||||
assert_eq!(
|
||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
||||
format!("Clash.yaml")
|
||||
);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload=").unwrap(), 111);
|
||||
assert_eq!(parse_str::<usize>(test_1, "download=").unwrap(), 2222);
|
||||
assert_eq!(parse_str::<usize>(test_1, "total=").unwrap(), 3333);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire=").unwrap(), 444);
|
||||
assert_eq!(
|
||||
parse_str::<String>(test_2, "filename=").unwrap(),
|
||||
format!("Clash.yaml")
|
||||
);
|
||||
|
||||
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "aaa="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "upload1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_1, "expire1="), None);
|
||||
assert_eq!(parse_str::<usize>(test_2, "attachment="), None);
|
||||
}
|
||||
|
||||
@@ -1,100 +1,175 @@
|
||||
use crate::utils::{dirs, tmpl};
|
||||
use crate::config::*;
|
||||
use crate::utils::{dirs, help};
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use log::LevelFilter;
|
||||
use log4rs::append::console::ConsoleAppender;
|
||||
use log4rs::append::file::FileAppender;
|
||||
use log4rs::config::{Appender, Config, Root};
|
||||
use log4rs::config::{Appender, Logger, Root};
|
||||
use log4rs::encode::pattern::PatternEncoder;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tauri::PackageInfo;
|
||||
|
||||
/// initialize this instance's log file
|
||||
fn init_log(log_dir: &PathBuf) {
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
let time_format = "{d(%Y-%m-%d %H:%M:%S)} - {m}{n}";
|
||||
let stdout = ConsoleAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(time_format)))
|
||||
.build();
|
||||
let tofile = FileAppender::builder()
|
||||
.encoder(Box::new(PatternEncoder::new(time_format)))
|
||||
.build(log_file)
|
||||
.unwrap();
|
||||
|
||||
let config = Config::builder()
|
||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||
.build(
|
||||
Root::builder()
|
||||
.appenders(["stdout", "file"])
|
||||
.build(LevelFilter::Info),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
log4rs::init_config(config).unwrap();
|
||||
}
|
||||
|
||||
/// Initialize all the files from resources
|
||||
fn init_config(app_dir: &PathBuf) -> std::io::Result<()> {
|
||||
// target path
|
||||
let clash_path = app_dir.join("config.yaml");
|
||||
let verge_path = app_dir.join("verge.yaml");
|
||||
let profile_path = app_dir.join("profiles.yaml");
|
||||
|
||||
if !clash_path.exists() {
|
||||
fs::File::create(clash_path)?.write(tmpl::CLASH_CONFIG)?;
|
||||
}
|
||||
if !verge_path.exists() {
|
||||
fs::File::create(verge_path)?.write(tmpl::VERGE_CONFIG)?;
|
||||
}
|
||||
if !profile_path.exists() {
|
||||
fs::File::create(profile_path)?.write(tmpl::PROFILES_CONFIG)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize app
|
||||
pub fn init_app(package_info: &PackageInfo) {
|
||||
// create app dir
|
||||
let app_dir = dirs::app_home_dir();
|
||||
let log_dir = dirs::app_logs_dir();
|
||||
let profiles_dir = dirs::app_profiles_dir();
|
||||
|
||||
let res_dir = dirs::app_resources_dir(package_info);
|
||||
|
||||
if !app_dir.exists() {
|
||||
fs::create_dir_all(&app_dir).unwrap();
|
||||
}
|
||||
if !log_dir.exists() {
|
||||
fs::create_dir_all(&log_dir).unwrap();
|
||||
}
|
||||
if !profiles_dir.exists() {
|
||||
fs::create_dir_all(&profiles_dir).unwrap();
|
||||
}
|
||||
|
||||
init_log(&log_dir);
|
||||
if let Err(err) = init_config(&app_dir) {
|
||||
log::error!("{err}");
|
||||
}
|
||||
|
||||
// copy the resource file
|
||||
let mmdb_path = app_dir.join("Country.mmdb");
|
||||
let mmdb_tmpl = res_dir.join("Country.mmdb");
|
||||
if !mmdb_path.exists() && mmdb_tmpl.exists() {
|
||||
fs::copy(mmdb_tmpl, mmdb_path).unwrap();
|
||||
}
|
||||
|
||||
// copy the wintun.dll
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let wintun_path = app_dir.join("wintun.dll");
|
||||
let wintun_tmpl = res_dir.join("wintun.dll");
|
||||
if !wintun_path.exists() && wintun_tmpl.exists() {
|
||||
fs::copy(wintun_tmpl, wintun_path).unwrap();
|
||||
fn init_log() -> Result<()> {
|
||||
let log_dir = dirs::app_logs_dir()?;
|
||||
if !log_dir.exists() {
|
||||
let _ = fs::create_dir_all(&log_dir);
|
||||
}
|
||||
}
|
||||
|
||||
let log_level = Config::verge().data().get_log_level();
|
||||
if log_level == LevelFilter::Off {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let local_time = Local::now().format("%Y-%m-%d-%H%M").to_string();
|
||||
let log_file = format!("{}.log", local_time);
|
||||
let log_file = log_dir.join(log_file);
|
||||
|
||||
let log_pattern = match log_level {
|
||||
LevelFilter::Trace => "{d(%Y-%m-%d %H:%M:%S)} {l} [{M}] - {m}{n}",
|
||||
_ => "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}",
|
||||
};
|
||||
|
||||
let encode = Box::new(PatternEncoder::new(log_pattern));
|
||||
|
||||
let stdout = ConsoleAppender::builder().encoder(encode.clone()).build();
|
||||
let tofile = FileAppender::builder().encoder(encode).build(log_file)?;
|
||||
|
||||
let mut logger_builder = Logger::builder();
|
||||
let mut root_builder = Root::builder();
|
||||
|
||||
let log_more = log_level == LevelFilter::Trace || log_level == LevelFilter::Debug;
|
||||
|
||||
#[cfg(feature = "verge-dev")]
|
||||
{
|
||||
logger_builder = logger_builder.appenders(["file", "stdout"]);
|
||||
if log_more {
|
||||
root_builder = root_builder.appenders(["file", "stdout"]);
|
||||
} else {
|
||||
root_builder = root_builder.appenders(["stdout"]);
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
{
|
||||
logger_builder = logger_builder.appenders(["file"]);
|
||||
if log_more {
|
||||
root_builder = root_builder.appenders(["file"]);
|
||||
}
|
||||
}
|
||||
|
||||
let (config, _) = log4rs::config::Config::builder()
|
||||
.appender(Appender::builder().build("stdout", Box::new(stdout)))
|
||||
.appender(Appender::builder().build("file", Box::new(tofile)))
|
||||
.logger(logger_builder.additive(false).build("app", log_level))
|
||||
.build_lossy(root_builder.build(log_level));
|
||||
|
||||
log4rs::init_config(config)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialize all the config files
|
||||
/// before tauri setup
|
||||
pub fn init_config() -> Result<()> {
|
||||
#[cfg(target_os = "windows")]
|
||||
unsafe {
|
||||
let _ = dirs::init_portable_flag();
|
||||
}
|
||||
|
||||
let _ = init_log();
|
||||
|
||||
crate::log_err!(dirs::app_home_dir().map(|app_dir| {
|
||||
if !app_dir.exists() {
|
||||
let _ = fs::create_dir_all(&app_dir);
|
||||
}
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::app_profiles_dir().map(|profiles_dir| {
|
||||
if !profiles_dir.exists() {
|
||||
let _ = fs::create_dir_all(&profiles_dir);
|
||||
}
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::clash_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IClashTemp::template().0, Some("# Clash Nyanpasuasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::verge_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IVerge::template(), Some("# Clash Nyanpasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
crate::log_err!(dirs::profiles_path().map(|path| {
|
||||
if !path.exists() {
|
||||
help::save_yaml(&path, &IProfiles::template(), Some("# Clash Nyanpasu"))?;
|
||||
}
|
||||
<Result<()>>::Ok(())
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// initialize app resources
|
||||
/// after tauri setup
|
||||
pub fn init_resources(package_info: &PackageInfo) -> Result<()> {
|
||||
let app_dir = dirs::app_home_dir()?;
|
||||
let res_dir = dirs::app_resources_dir(package_info)?;
|
||||
|
||||
if !app_dir.exists() {
|
||||
let _ = fs::create_dir_all(&app_dir);
|
||||
}
|
||||
if !res_dir.exists() {
|
||||
let _ = fs::create_dir_all(&res_dir);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat", "wintun.dll"];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let file_list = ["Country.mmdb", "geoip.dat", "geosite.dat"];
|
||||
|
||||
// copy the resource file
|
||||
// if the source file is newer than the destination file, copy it over
|
||||
for file in file_list.iter() {
|
||||
let src_path = res_dir.join(file);
|
||||
let dest_path = app_dir.join(file);
|
||||
|
||||
let handle_copy = || {
|
||||
match fs::copy(&src_path, &dest_path) {
|
||||
Ok(_) => log::debug!(target: "app", "resources copied '{file}'"),
|
||||
Err(err) => {
|
||||
log::error!(target: "app", "failed to copy resources '{file}', {err}")
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if src_path.exists() && !dest_path.exists() {
|
||||
handle_copy();
|
||||
continue;
|
||||
}
|
||||
|
||||
let src_modified = fs::metadata(&src_path).and_then(|m| m.modified());
|
||||
let dest_modified = fs::metadata(&dest_path).and_then(|m| m.modified());
|
||||
|
||||
match (src_modified, dest_modified) {
|
||||
(Ok(src_modified), Ok(dest_modified)) => {
|
||||
if src_modified > dest_modified {
|
||||
handle_copy();
|
||||
} else {
|
||||
log::debug!(target: "app", "skipping resource copy '{file}'");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::debug!(target: "app", "failed to get modified '{file}'");
|
||||
handle_copy();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
pub mod config;
|
||||
pub mod dirs;
|
||||
pub mod help;
|
||||
pub mod init;
|
||||
pub mod resolve;
|
||||
pub mod server;
|
||||
pub mod sysopt;
|
||||
pub mod tmpl;
|
||||
// mod winhelp;
|
||||
|
||||
@@ -1,88 +1,179 @@
|
||||
use super::{init, server};
|
||||
use crate::{core::Profiles, log_if_err, states};
|
||||
use crate::{config::Config, core::*, utils::init, utils::server};
|
||||
use crate::{log_err, trace_err};
|
||||
use anyhow::Result;
|
||||
use tauri::{App, AppHandle, Manager};
|
||||
|
||||
/// handle something when start app
|
||||
pub fn resolve_setup(app: &App) {
|
||||
// setup a simple http server for singleton
|
||||
server::embed_server(&app.handle());
|
||||
pub fn resolve_setup(app: &mut App) {
|
||||
#[cfg(target_os = "macos")]
|
||||
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
|
||||
|
||||
// init app config
|
||||
init::init_app(app.package_info());
|
||||
handle::Handle::global().init(app.app_handle());
|
||||
|
||||
// init states
|
||||
let clash_state = app.state::<states::ClashState>();
|
||||
let verge_state = app.state::<states::VergeState>();
|
||||
let profiles_state = app.state::<states::ProfilesState>();
|
||||
log_err!(init::init_resources(app.package_info()));
|
||||
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
// 启动核心
|
||||
log::trace!("init config");
|
||||
log_err!(Config::init_config());
|
||||
|
||||
log_if_err!(clash.run_sidecar());
|
||||
log::trace!("launch core");
|
||||
log_err!(CoreManager::global().init());
|
||||
|
||||
*profiles = Profiles::read_file();
|
||||
// setup a simple http server for singleton
|
||||
log::trace!("launch embed server");
|
||||
server::embed_server(app.app_handle());
|
||||
|
||||
clash.set_window(app.get_window("main"));
|
||||
log_if_err!(clash.activate(&profiles));
|
||||
log_if_err!(clash.activate_enhanced(&profiles, true, true));
|
||||
log::trace!("init system tray");
|
||||
log_err!(tray::Tray::update_systray(&app.app_handle()));
|
||||
|
||||
verge.init_sysproxy(clash.info.port.clone());
|
||||
let silent_start = { Config::verge().data().enable_silent_start.clone() };
|
||||
if !silent_start.unwrap_or(false) {
|
||||
create_window(&app.app_handle());
|
||||
}
|
||||
|
||||
log_if_err!(verge.init_launch());
|
||||
log_err!(sysopt::Sysopt::global().init_launch());
|
||||
log_err!(sysopt::Sysopt::global().init_sysproxy());
|
||||
|
||||
verge.config.enable_system_proxy.map(|enable| {
|
||||
log_if_err!(app
|
||||
.tray_handle()
|
||||
.get_item("system_proxy")
|
||||
.set_selected(enable));
|
||||
});
|
||||
|
||||
resolve_window(app, verge.config.enable_silent_start.clone());
|
||||
log_err!(handle::Handle::update_systray_part());
|
||||
log_err!(hotkey::Hotkey::global().init(app.app_handle()));
|
||||
log_err!(timer::Timer::global().init());
|
||||
}
|
||||
|
||||
/// reset system proxy
|
||||
pub fn resolve_reset(app_handle: &AppHandle) {
|
||||
let verge_state = app_handle.state::<states::VergeState>();
|
||||
let mut verge = verge_state.0.lock().unwrap();
|
||||
|
||||
verge.reset_sysproxy();
|
||||
pub fn resolve_reset() {
|
||||
log_err!(sysopt::Sysopt::global().reset_sysproxy());
|
||||
log_err!(CoreManager::global().stop_core());
|
||||
}
|
||||
|
||||
/// customize the window theme
|
||||
fn resolve_window(app: &App, hide: Option<bool>) {
|
||||
let window = app.get_window("main").unwrap();
|
||||
|
||||
// silent start
|
||||
hide.map(|hide| {
|
||||
if hide {
|
||||
window.hide().unwrap();
|
||||
/// create main window
|
||||
pub fn create_window(app_handle: &AppHandle) {
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use window_shadows::set_shadow;
|
||||
use window_vibrancy::apply_blur;
|
||||
let mut builder = tauri::window::WindowBuilder::new(
|
||||
app_handle,
|
||||
"main".to_string(),
|
||||
tauri::WindowUrl::App("index.html".into()),
|
||||
)
|
||||
.title("Clash Nyanpasu")
|
||||
.fullscreen(false)
|
||||
.min_inner_size(600.0, 520.0);
|
||||
|
||||
window.set_decorations(false).unwrap();
|
||||
set_shadow(&window, true).unwrap();
|
||||
apply_blur(&window, None).unwrap();
|
||||
}
|
||||
match Config::verge().latest().window_size_position.clone() {
|
||||
Some(size_pos) if size_pos.len() == 4 => {
|
||||
let size = (size_pos[0], size_pos[1]);
|
||||
let pos = (size_pos[2], size_pos[3]);
|
||||
let w = size.0.clamp(600.0, f64::INFINITY);
|
||||
let h = size.1.clamp(520.0, f64::INFINITY);
|
||||
builder = builder.inner_size(w, h).position(pos.0, pos.1);
|
||||
}
|
||||
_ => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 636.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::LogicalSize;
|
||||
use tauri::Size::Logical;
|
||||
window.set_decorations(true).unwrap();
|
||||
window
|
||||
.set_size(Logical(LogicalSize {
|
||||
width: 800.0,
|
||||
height: 610.0,
|
||||
}))
|
||||
.unwrap();
|
||||
// use tauri_plugin_vibrancy::MacOSVibrancy;
|
||||
// #[allow(deprecated)]
|
||||
// window.apply_vibrancy(MacOSVibrancy::AppearanceBased);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
builder = builder.inner_size(800.0, 642.0).center();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
match builder
|
||||
.decorations(false)
|
||||
.transparent(true)
|
||||
.visible(false)
|
||||
.build()
|
||||
{
|
||||
Ok(win) => {
|
||||
log::trace!("try to calculate the monitor size");
|
||||
let center = (|| -> Result<bool> {
|
||||
let mut center = false;
|
||||
let monitor = win.current_monitor()?.ok_or(anyhow::anyhow!(""))?;
|
||||
let size = monitor.size();
|
||||
let pos = win.outer_position()?;
|
||||
|
||||
if pos.x < -400
|
||||
|| pos.x > (size.width - 200).try_into()?
|
||||
|| pos.y < -200
|
||||
|| pos.y > (size.height - 200).try_into()?
|
||||
{
|
||||
center = true;
|
||||
}
|
||||
Ok(center)
|
||||
})();
|
||||
|
||||
if center.unwrap_or(true) {
|
||||
trace_err!(win.center(), "set win center");
|
||||
}
|
||||
|
||||
log::trace!("try to create window");
|
||||
let app_handle = app_handle.clone();
|
||||
|
||||
// 加点延迟避免界面闪一下
|
||||
tauri::async_runtime::spawn(async move {
|
||||
sleep(Duration::from_millis(888)).await;
|
||||
|
||||
if let Some(window) = app_handle.get_window("main") {
|
||||
trace_err!(set_shadow(&window, true), "set win shadow");
|
||||
trace_err!(window.show(), "set win visible");
|
||||
trace_err!(window.unminimize(), "set win unminimize");
|
||||
trace_err!(window.set_focus(), "set win focus");
|
||||
} else {
|
||||
log::error!(target: "app", "failed to create window, get_window is None")
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(err) => log::error!(target: "app", "failed to create window, {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::log_err!(builder
|
||||
.decorations(true)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(tauri::TitleBarStyle::Overlay)
|
||||
.build());
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
crate::log_err!(builder.decorations(true).transparent(false).build());
|
||||
}
|
||||
|
||||
/// save window size and position
|
||||
pub fn save_window_size_position(app_handle: &AppHandle, save_to_file: bool) -> Result<()> {
|
||||
let win = app_handle
|
||||
.get_window("main")
|
||||
.ok_or(anyhow::anyhow!("failed to get window"))?;
|
||||
|
||||
let scale = win.scale_factor()?;
|
||||
let size = win.inner_size()?;
|
||||
let size = size.to_logical::<f64>(scale);
|
||||
let pos = win.outer_position()?;
|
||||
let pos = pos.to_logical::<f64>(scale);
|
||||
|
||||
let verge = Config::verge();
|
||||
let mut verge = verge.latest();
|
||||
verge.window_size_position = Some(vec![size.width, size.height, pos.x, pos.y]);
|
||||
|
||||
if save_to_file {
|
||||
verge.save_file()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,41 +1,44 @@
|
||||
extern crate warp;
|
||||
|
||||
use super::resolve;
|
||||
use crate::config::IVerge;
|
||||
use anyhow::{bail, Result};
|
||||
use port_scanner::local_port_available;
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri::AppHandle;
|
||||
use warp::Filter;
|
||||
|
||||
#[cfg(not(feature = "verge-dev"))]
|
||||
const SERVER_PORT: u16 = 33333;
|
||||
#[cfg(feature = "verge-dev")]
|
||||
const SERVER_PORT: u16 = 11233;
|
||||
|
||||
/// check whether there is already exists
|
||||
pub fn check_singleton() -> Result<(), ()> {
|
||||
if !local_port_available(SERVER_PORT) {
|
||||
tauri::async_runtime::block_on(async {
|
||||
let url = format!("http://127.0.0.1:{}/commands/visible", SERVER_PORT);
|
||||
reqwest::get(url).await.unwrap();
|
||||
Err(())
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
pub fn check_singleton() -> Result<()> {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
if !local_port_available(port) {
|
||||
tauri::async_runtime::block_on(async {
|
||||
let url = format!("http://127.0.0.1:{port}/commands/visible");
|
||||
let resp = reqwest::get(url).await?.text().await?;
|
||||
|
||||
if &resp == "ok" {
|
||||
bail!("app exists");
|
||||
}
|
||||
|
||||
log::error!("failed to setup singleton listen server");
|
||||
Ok(())
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The embed server only be used to implement singleton process
|
||||
/// maybe it can be used as pac server later
|
||||
pub fn embed_server(app: &AppHandle) {
|
||||
let window = app.get_window("main").unwrap();
|
||||
pub fn embed_server(app_handle: AppHandle) {
|
||||
let port = IVerge::get_singleton_port();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let commands = warp::path!("commands" / "visible").map(move || {
|
||||
window.show().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
return format!("ok");
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let commands = warp::path!("commands" / "visible").map(move || {
|
||||
resolve::create_window(&app_handle);
|
||||
format!("ok")
|
||||
});
|
||||
|
||||
warp::serve(commands).bind(([127, 0, 0, 1], port)).await;
|
||||
});
|
||||
|
||||
warp::serve(commands)
|
||||
.bind(([127, 0, 0, 1], SERVER_PORT))
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
static DEFAULT_BYPASS: &str = "localhost;127.*;10.*;172.16.*;172.17.*;172.18.*;172.19.*;172.20.*;172.21.*;172.22.*;172.23.*;172.24.*;172.25.*;172.26.*;172.27.*;172.28.*;172.29.*;172.30.*;172.31.*;192.168.*;<local>";
|
||||
#[cfg(target_os = "linux")]
|
||||
static DEFAULT_BYPASS: &str = "localhost,127.0.0.1/8,::1";
|
||||
#[cfg(target_os = "macos")]
|
||||
static DEFAULT_BYPASS: &str =
|
||||
"192.168.0.0/16\n10.0.0.0/8\n172.16.0.0/12\n127.0.0.1\nlocalhost\n*.local\ntimestamp.apple.com\n";
|
||||
#[cfg(target_os = "macos")]
|
||||
static MACOS_SERVICE: &str = "Wi-Fi";
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||
pub struct SysProxyConfig {
|
||||
pub enable: bool,
|
||||
pub server: String,
|
||||
pub bypass: String,
|
||||
}
|
||||
|
||||
impl Default for SysProxyConfig {
|
||||
fn default() -> Self {
|
||||
SysProxyConfig {
|
||||
enable: false,
|
||||
server: String::from(""),
|
||||
bypass: String::from(""),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SysProxyConfig {
|
||||
pub fn new(enable: bool, port: String, bypass: Option<String>) -> Self {
|
||||
SysProxyConfig {
|
||||
enable,
|
||||
server: format!("127.0.0.1:{}", port),
|
||||
bypass: bypass.unwrap_or(DEFAULT_BYPASS.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl SysProxyConfig {
|
||||
/// Get the windows system proxy config
|
||||
pub fn get_sys() -> Result<Self> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let cur_var = hkcu.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||
KEY_READ,
|
||||
)?;
|
||||
|
||||
Ok(SysProxyConfig {
|
||||
enable: cur_var.get_value::<u32, _>("ProxyEnable")? == 1u32,
|
||||
server: cur_var.get_value("ProxyServer")?,
|
||||
bypass: cur_var.get_value("ProxyOverride")?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the windows system proxy config
|
||||
pub fn set_sys(&self) -> Result<()> {
|
||||
use winreg::enums::*;
|
||||
use winreg::RegKey;
|
||||
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||
let cur_var = hkcu.open_subkey_with_flags(
|
||||
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Internet Settings",
|
||||
KEY_SET_VALUE,
|
||||
)?;
|
||||
|
||||
let enable: u32 = if self.enable { 1u32 } else { 0u32 };
|
||||
|
||||
cur_var.set_value("ProxyEnable", &enable)?;
|
||||
cur_var.set_value("ProxyServer", &self.server)?;
|
||||
cur_var.set_value("ProxyOverride", &self.bypass)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
impl SysProxyConfig {
|
||||
/// Get the macos system proxy config
|
||||
pub fn get_sys() -> Result<Self> {
|
||||
use std::process::Command;
|
||||
|
||||
let http = macproxy::get_proxy(&["-getwebproxy", MACOS_SERVICE])?;
|
||||
let https = macproxy::get_proxy(&["-getsecurewebproxy", MACOS_SERVICE])?;
|
||||
let sock = macproxy::get_proxy(&["-getsocksfirewallproxy", MACOS_SERVICE])?;
|
||||
|
||||
let mut enable = false;
|
||||
let mut server = "".into();
|
||||
|
||||
if sock.0 == "Yes" {
|
||||
enable = true;
|
||||
server = sock.1;
|
||||
}
|
||||
if https.0 == "Yes" {
|
||||
enable = true;
|
||||
server = https.1;
|
||||
}
|
||||
if http.0 == "Yes" || !enable {
|
||||
enable = http.0 == "Yes";
|
||||
server = http.1;
|
||||
}
|
||||
|
||||
let bypass_output = Command::new("networksetup")
|
||||
.args(["-getproxybypassdomains", MACOS_SERVICE])
|
||||
.output()?;
|
||||
|
||||
// change the format to xxx,xxx
|
||||
let bypass = std::str::from_utf8(&bypass_output.stdout)
|
||||
.unwrap_or(DEFAULT_BYPASS)
|
||||
.to_string()
|
||||
.split('\n')
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
|
||||
Ok(SysProxyConfig {
|
||||
enable,
|
||||
server,
|
||||
bypass,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the macos system proxy config
|
||||
pub fn set_sys(&self) -> Result<()> {
|
||||
use std::process::Command;
|
||||
|
||||
let enable = self.enable;
|
||||
let server = self.server.as_str();
|
||||
let bypass = self.bypass.clone();
|
||||
macproxy::set_proxy("-setwebproxy", MACOS_SERVICE, enable, server)?;
|
||||
macproxy::set_proxy("-setsecurewebproxy", MACOS_SERVICE, enable, server)?;
|
||||
macproxy::set_proxy("-setsocksfirewallproxy", MACOS_SERVICE, enable, server)?;
|
||||
|
||||
let domains = bypass.split(",").collect::<Vec<_>>();
|
||||
Command::new("networksetup")
|
||||
.args([["-setproxybypassdomains", MACOS_SERVICE].to_vec(), domains].concat())
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macproxy {
|
||||
use super::*;
|
||||
use anyhow::bail;
|
||||
use std::process::Command;
|
||||
|
||||
/// use networksetup
|
||||
/// get the target proxy config
|
||||
pub(super) fn get_proxy(args: &[&str; 2]) -> Result<(String, String)> {
|
||||
let output = Command::new("networksetup").args(args).output()?;
|
||||
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
let enable = parse(stdout, "Enabled:");
|
||||
let server = parse(stdout, "Server:");
|
||||
let port = parse(stdout, "Port:");
|
||||
let server = format!("{server}:{port}");
|
||||
Ok((enable.into(), server))
|
||||
}
|
||||
|
||||
/// use networksetup
|
||||
/// set the target proxy config
|
||||
pub(super) fn set_proxy(
|
||||
target: &str, // like: -setwebproxy
|
||||
device: &str,
|
||||
enable: bool,
|
||||
server: &str,
|
||||
) -> Result<()> {
|
||||
let mut split = server.split(":");
|
||||
let host = split.next();
|
||||
let port = split.next();
|
||||
|
||||
// can not parse the field
|
||||
if host.is_none() || port.is_none() {
|
||||
bail!("failed to parse the server into host:port");
|
||||
}
|
||||
|
||||
let args = vec![target, device, host.unwrap(), port.unwrap()];
|
||||
Command::new("networksetup").args(&args).status()?;
|
||||
|
||||
let target_state = String::from(target) + "state";
|
||||
let enable = if enable { "on" } else { "off" };
|
||||
let args = vec![target_state.as_str(), device, enable];
|
||||
Command::new("networksetup").args(&args).status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// parse the networksetup output
|
||||
fn parse<'a>(target: &'a str, key: &'a str) -> &'a str {
|
||||
match target.find(key) {
|
||||
Some(idx) => {
|
||||
let idx = idx + key.len();
|
||||
let value = &target[idx..];
|
||||
let value = match value.find("\n") {
|
||||
Some(end) => &value[..end],
|
||||
None => value,
|
||||
};
|
||||
value.trim()
|
||||
}
|
||||
None => "",
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get() {
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("networksetup")
|
||||
.args(["-getwebproxy", "Wi-Fi"])
|
||||
.output();
|
||||
|
||||
let output = output.unwrap();
|
||||
let stdout = std::str::from_utf8(&output.stdout).unwrap();
|
||||
let enable = parse(stdout, "Enabled:");
|
||||
let server = parse(stdout, "Server:");
|
||||
let port = parse(stdout, "Port:");
|
||||
|
||||
println!("enable: {}, server: {}, port: {}", enable, server, port);
|
||||
|
||||
dbg!(SysProxyConfig::get_sys().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set() {
|
||||
let sysproxy = SysProxyConfig::new(true, "7890".into(), None);
|
||||
dbg!(sysproxy.set_sys().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Linux Desktop System Proxy Supports
|
||||
/// by using `gsettings`
|
||||
#[cfg(target_os = "linux")]
|
||||
impl SysProxyConfig {
|
||||
/// Get the system proxy config [http/https/socks]
|
||||
pub fn get_sys() -> Result<Self> {
|
||||
use std::process::Command;
|
||||
|
||||
let schema = "org.gnome.system.proxy";
|
||||
|
||||
// get enable
|
||||
let mode = Command::new("gsettings")
|
||||
.args(["get", schema, "mode"])
|
||||
.output()?;
|
||||
let mode = std::str::from_utf8(&mode.stdout)?;
|
||||
let enable = mode == "manual";
|
||||
|
||||
// get bypass
|
||||
// Todo: parse the ignore-hosts
|
||||
// ['aaa', 'bbb'] -> aaa,bbb
|
||||
let ignore = Command::new("gsettings")
|
||||
.args(["get", schema, "ignore-hosts"])
|
||||
.output()?;
|
||||
let ignore = std::str::from_utf8(&ignore.stdout)?;
|
||||
let bypass = ignore.to_string();
|
||||
|
||||
let http = Self::get_proxy("http")?;
|
||||
let https = Self::get_proxy("https")?;
|
||||
let socks = Self::get_proxy("socks")?;
|
||||
|
||||
let mut server = "".into();
|
||||
|
||||
if socks.len() > 0 {
|
||||
server = socks;
|
||||
}
|
||||
if https.len() > 0 {
|
||||
server = https;
|
||||
}
|
||||
if http.len() > 0 {
|
||||
server = http;
|
||||
}
|
||||
|
||||
Ok(SysProxyConfig {
|
||||
enable,
|
||||
server,
|
||||
bypass,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the system proxy config [http/https/socks]
|
||||
pub fn set_sys(&self) -> Result<()> {
|
||||
use anyhow::bail;
|
||||
use std::process::Command;
|
||||
|
||||
let enable = self.enable;
|
||||
let server = self.server.as_str();
|
||||
let bypass = self.bypass.clone();
|
||||
let schema = "org.gnome.system.proxy";
|
||||
|
||||
if enable {
|
||||
let mut split = server.split(":");
|
||||
let host = split.next();
|
||||
let port = split.next();
|
||||
|
||||
if port.is_none() {
|
||||
bail!("failed to parse the port");
|
||||
}
|
||||
|
||||
let host = format!("'{}'", host.unwrap_or("127.0.0.1"));
|
||||
let host = host.as_str();
|
||||
let port = port.unwrap();
|
||||
|
||||
let http = format!("{schema}.http");
|
||||
Command::new("gsettings")
|
||||
.args(["set", http.as_str(), "host", host])
|
||||
.status()?;
|
||||
Command::new("gsettings")
|
||||
.args(["set", http.as_str(), "port", port])
|
||||
.status()?;
|
||||
|
||||
let https = format!("{schema}.https");
|
||||
Command::new("gsettings")
|
||||
.args(["set", https.as_str(), "host", host])
|
||||
.status()?;
|
||||
Command::new("gsettings")
|
||||
.args(["set", https.as_str(), "port", port])
|
||||
.status()?;
|
||||
|
||||
let socks = format!("{schema}.socks");
|
||||
Command::new("gsettings")
|
||||
.args(["set", socks.as_str(), "host", host])
|
||||
.status()?;
|
||||
Command::new("gsettings")
|
||||
.args(["set", socks.as_str(), "port", port])
|
||||
.status()?;
|
||||
|
||||
// set bypass
|
||||
// Todo: parse the ignore-hosts
|
||||
// aaa,bbb,cccc -> ['aaa', 'bbb', 'ccc']
|
||||
Command::new("gsettings")
|
||||
.args(["set", schema, "ignore-hosts", bypass.as_str()]) // todo
|
||||
.status()?;
|
||||
}
|
||||
|
||||
let mode = if enable { "'manual'" } else { "'none'" };
|
||||
Command::new("gsettings")
|
||||
.args(["set", schema, "mode", mode])
|
||||
.status()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// help function
|
||||
fn get_proxy(typ: &str) -> Result<String> {
|
||||
use std::process::Command;
|
||||
|
||||
let schema = format!("org.gnome.system.proxy.{typ}");
|
||||
let schema = schema.as_str();
|
||||
|
||||
let host = Command::new("gsettings")
|
||||
.args(["get", schema, "host"])
|
||||
.output()?;
|
||||
let host = std::str::from_utf8(&host.stdout)?;
|
||||
|
||||
let port = Command::new("gsettings")
|
||||
.args(["get", schema, "port"])
|
||||
.output()?;
|
||||
let port = std::str::from_utf8(&port.stdout)?;
|
||||
|
||||
Ok(format!("{host}:{port}"))
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,5 @@
|
||||
///! Some config file template
|
||||
|
||||
/// template for clash core `config.yaml`
|
||||
pub const CLASH_CONFIG: &[u8] = br#"# Default Config For Clash Core
|
||||
|
||||
mixed-port: 7890
|
||||
log-level: info
|
||||
allow-lan: false
|
||||
external-controller: 127.0.0.1:9090
|
||||
mode: rule
|
||||
secret: ""
|
||||
"#;
|
||||
|
||||
/// template for `profiles.yaml`
|
||||
pub const PROFILES_CONFIG: &[u8] = b"# Profiles Config for Clash Verge
|
||||
|
||||
current: ~
|
||||
items: ~
|
||||
";
|
||||
|
||||
/// template for `verge.yaml`
|
||||
pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge
|
||||
|
||||
language: en
|
||||
theme_mode: light
|
||||
theme_blur: false
|
||||
traffic_graph: true
|
||||
enable_self_startup: false
|
||||
enable_system_proxy: false
|
||||
enable_proxy_guard: false
|
||||
proxy_guard_duration: 10
|
||||
system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
|
||||
";
|
||||
|
||||
/// template for new a profile item
|
||||
pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
|
||||
|
||||
@@ -60,10 +28,8 @@ append-proxy-groups:
|
||||
";
|
||||
|
||||
/// enhanced profile
|
||||
pub const ITEM_SCRIPT: &str = "// Should define the `main` function
|
||||
// The argument to this function is the clash config
|
||||
// or the result of the previous handler
|
||||
// so you should return the config after processing
|
||||
pub const ITEM_SCRIPT: &str = "// Define the `main` function
|
||||
|
||||
function main(params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
#![allow(non_snake_case)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
//!
|
||||
//! From https://github.com/tauri-apps/window-vibrancy/blob/dev/src/windows.rs
|
||||
//!
|
||||
|
||||
use windows_sys::Win32::{
|
||||
Foundation::*,
|
||||
System::{LibraryLoader::*, SystemInformation::*},
|
||||
};
|
||||
|
||||
fn get_function_impl(library: &str, function: &str) -> Option<FARPROC> {
|
||||
assert_eq!(library.chars().last(), Some('\0'));
|
||||
assert_eq!(function.chars().last(), Some('\0'));
|
||||
|
||||
let module = unsafe { LoadLibraryA(library.as_ptr()) };
|
||||
if module == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { GetProcAddress(module, function.as_ptr()) })
|
||||
}
|
||||
|
||||
macro_rules! get_function {
|
||||
($lib:expr, $func:ident) => {
|
||||
get_function_impl(concat!($lib, '\0'), concat!(stringify!($func), '\0')).map(|f| unsafe {
|
||||
std::mem::transmute::<::windows_sys::Win32::Foundation::FARPROC, $func>(f)
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a tuple of (major, minor, buildnumber)
|
||||
fn get_windows_ver() -> Option<(u32, u32, u32)> {
|
||||
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> i32;
|
||||
let handle = get_function!("ntdll.dll", RtlGetVersion);
|
||||
if let Some(rtl_get_version) = handle {
|
||||
unsafe {
|
||||
let mut vi = OSVERSIONINFOW {
|
||||
dwOSVersionInfoSize: 0,
|
||||
dwMajorVersion: 0,
|
||||
dwMinorVersion: 0,
|
||||
dwBuildNumber: 0,
|
||||
dwPlatformId: 0,
|
||||
szCSDVersion: [0; 128],
|
||||
};
|
||||
|
||||
let status = (rtl_get_version)(&mut vi as _);
|
||||
|
||||
if status >= 0 {
|
||||
Some((vi.dwMajorVersion, vi.dwMinorVersion, vi.dwBuildNumber))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_win11() -> bool {
|
||||
let v = get_windows_ver().unwrap_or_default();
|
||||
v.2 >= 22000
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
dbg!(get_windows_ver().unwrap_or_default());
|
||||
}
|
||||
@@ -1,44 +1,42 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "0.0.29"
|
||||
"productName": "Clash Nyanpasu",
|
||||
"version": "1.4.0"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:3000/",
|
||||
"beforeDevCommand": "yarn run web:dev",
|
||||
"beforeBuildCommand": "yarn run web:build"
|
||||
"beforeDevCommand": "pnpm run web:dev",
|
||||
"beforeBuildCommand": "pnpm run web:build"
|
||||
},
|
||||
"tauri": {
|
||||
"systemTray": {
|
||||
"iconPath": "icons/icon.png",
|
||||
"iconPath": "icons/tray-icon.ico",
|
||||
"iconAsTemplate": true
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "top.gydi.clashverge",
|
||||
"identifier": "moe.elaina.clash.nyanpasu",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon-new.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["resources"],
|
||||
"externalBin": ["sidecar/clash"],
|
||||
"externalBin": ["sidecar/clash", "sidecar/clash-meta"],
|
||||
"copyright": "© 2022 zzzgydi All Rights Reserved",
|
||||
"category": "DeveloperTool",
|
||||
"shortDescription": "A Clash GUI based on tauri.",
|
||||
"longDescription": "A Clash GUI based on tauri.",
|
||||
"deb": {
|
||||
"depends": [],
|
||||
"useBootstrapper": false
|
||||
"depends": ["openssl"]
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"useBootstrapper": false,
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": null,
|
||||
"entitlements": null
|
||||
@@ -46,17 +44,20 @@
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
"timestampUrl": "",
|
||||
"wix": {
|
||||
"language": ["zh-CN", "en-US", "ru-RU"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://github.com/zzzgydi/clash-verge/releases/download/updater/update.json",
|
||||
"https://hub.fastgit.xyz/zzzgydi/clash-verge/releases/download/updater/update-proxy.json"
|
||||
"https://ghproxy.com/https://github.com/keiko233/clash-nyanpasu/releases/download/updater/update-proxy.json",
|
||||
"https://github.com/keiko233/clash-nyanpasu/releases/download/updater/update.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDExNUFBNTBBN0FDNEFBRTUKUldUbHFzUjZDcVZhRVRJM25NS3NkSFlFVElxUkNZMzZ6bHUwRVJjb2F3alJXVzRaeDdSaTA2YWYK"
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlBMUM0NjMxREZCNDRGMjYKUldRbVQ3VGZNVVljbW43N0FlWjA4UkNrbTgxSWxSSXJQcExXNkZjUTlTQkIyYkJzL0tsSWF2d0cK"
|
||||
},
|
||||
"allowlist": {
|
||||
"shell": {
|
||||
@@ -67,21 +68,12 @@
|
||||
},
|
||||
"process": {
|
||||
"all": true
|
||||
},
|
||||
"globalShortcut": {
|
||||
"all": true
|
||||
}
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Clash Verge",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"decorations": false,
|
||||
"transparent": true,
|
||||
"minWidth": 600,
|
||||
"minHeight": 520
|
||||
}
|
||||
],
|
||||
"windows": [],
|
||||
"security": {
|
||||
"csp": "script-src 'unsafe-eval' 'self'; default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src data: 'self';"
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 100 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 72 KiB |
@@ -1,18 +1,36 @@
|
||||
<svg id="svg" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400"
|
||||
height="400" viewBox="0, 0, 400,400">
|
||||
<!--
|
||||
Copyright (C) 2022 The Project Nyanpasu
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
thanks: @ReallySnow
|
||||
-->
|
||||
|
||||
<svg width="420px" height="490px" viewBox="0 0 420 490" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<linearGradient id="Gradient2" x1="0" x2="-0.1" y1="0" y2="1">
|
||||
<stop offset="0%" stop-color="#5b5c9d" />
|
||||
<stop offset="80%" stop-color="rgb(63, 64, 109)" />
|
||||
<stop offset="100%" stop-color="rgb(63, 64, 109)" />
|
||||
</linearGradient>
|
||||
<style><![CDATA[
|
||||
#element {
|
||||
fill: #000000;
|
||||
}
|
||||
]]></style>
|
||||
</defs>
|
||||
<g id="svgg">
|
||||
<path id="path0"
|
||||
d="M118.124 39.858 C 116.645 40.182,114.562 42.930,113.283 46.247 C 111.908 49.812,110.887 54.542,107.205 74.400 C 106.736 76.930,105.926 81.160,105.404 83.800 C 104.527 88.237,103.654 92.889,102.177 101.000 C 101.856 102.760,101.413 105.100,101.192 106.200 C 100.754 108.375,100.784 108.199,98.436 122.200 C 96.675 132.702,95.947 137.215,94.984 143.600 C 94.636 145.910,94.098 149.420,93.789 151.400 C 92.649 158.697,91.961 164.413,92.180 164.768 C 92.304 164.968,92.223 165.192,92.000 165.266 C 91.777 165.340,96.852 165.347,103.277 165.282 C 116.520 165.149,115.721 165.379,116.358 161.519 C 122.839 122.279,179.334 121.580,186.617 160.650 C 186.981 162.602,187.463 164.425,187.689 164.700 C 188.328 165.481,229.717 165.423,230.367 164.640 C 230.622 164.332,231.013 162.937,231.235 161.540 C 237.531 121.949,295.239 121.807,301.620 161.367 C 302.274 165.421,301.855 165.252,310.962 165.117 C 315.273 165.052,318.861 165.090,318.935 165.200 C 319.009 165.310,319.005 165.152,318.927 164.848 C 318.849 164.545,318.519 162.610,318.193 160.548 C 317.867 158.487,317.330 155.135,317.000 153.100 C 316.162 147.934,315.312 142.663,314.823 139.600 C 314.595 138.170,314.226 136.010,314.004 134.800 C 313.781 133.590,312.423 125.400,310.985 116.600 C 304.278 75.545,304.008 74.156,298.145 50.400 C 295.961 41.553,294.621 39.584,290.800 39.611 C 287.927 39.631,283.053 43.395,279.420 48.400 C 277.509 51.032,261.566 79.109,256.960 87.953 C 253.991 93.654,253.647 93.789,245.600 92.402 C 218.757 87.774,194.454 87.780,170.478 92.420 C 162.868 93.893,163.987 94.596,157.579 84.306 C 133.879 46.247,126.566 38.009,118.124 39.858 M147.277 134.807 C 123.365 138.118,111.083 165.918,124.948 185.349 C 140.543 207.202,173.704 202.274,182.466 176.800 C 190.000 154.900,170.471 131.596,147.277 134.807 M262.051 134.810 C 235.780 138.440,224.730 170.720,243.356 189.422 C 264.398 210.548,299.874 195.275,298.689 165.600 C 297.927 146.523,280.892 132.207,262.051 134.810 M157.600 153.840 C 163.092 156.341,166.343 161.914,165.902 168.074 C 164.875 182.425,145.082 186.221,138.715 173.287 C 132.759 161.189,145.324 148.250,157.600 153.840 M273.193 153.597 C 279.788 156.926,283.287 165.159,280.844 171.600 C 275.229 186.406,253.594 183.910,252.135 168.287 C 251.125 157.467,263.609 148.758,273.193 153.597 M91.200 168.809 C 91.200 169.522,90.939 171.836,90.621 173.952 C 89.933 178.523,87.567 196.170,85.788 210.000 C 85.462 212.530,85.012 215.860,84.786 217.400 C 84.561 218.940,84.191 221.820,83.964 223.800 C 83.737 225.780,83.288 229.560,82.965 232.200 C 81.886 241.026,80.172 255.664,79.404 262.600 C 78.867 267.450,78.532 270.381,76.987 283.800 C 76.251 290.187,75.405 297.881,74.568 305.800 C 74.220 309.100,73.789 313.150,73.613 314.800 C 73.436 316.450,73.063 320.230,72.784 323.200 C 72.504 326.170,72.169 328.723,72.038 328.874 C 71.701 329.262,59.638 327.033,54.028 325.546 C 34.668 320.412,26.096 301.951,35.625 285.911 C 38.026 281.869,41.515 278.587,49.795 272.581 C 58.081 266.570,59.262 265.247,59.510 261.702 C 59.969 255.136,50.677 252.070,40.551 255.447 C -6.127 271.014,-3.894 337.227,43.806 351.951 C 50.541 354.030,58.050 355.239,67.760 355.807 C 72.410 356.079,75.202 356.542,82.181 358.199 C 88.822 359.777,100.215 360.425,156.000 362.398 C 170.725 362.918,255.192 362.921,267.760 362.401 C 273.062 362.182,281.900 361.820,287.400 361.597 C 302.851 360.972,326.558 359.339,333.200 358.444 C 343.055 357.116,343.889 354.966,341.839 336.200 C 341.502 333.120,340.948 327.900,340.607 324.600 C 340.266 321.300,339.709 315.990,339.370 312.800 C 339.030 309.610,338.499 304.570,338.189 301.600 C 337.022 290.393,335.650 278.160,334.622 269.800 C 333.105 257.460,332.638 254.027,330.408 238.800 C 329.716 234.070,328.983 229.030,328.781 227.600 C 328.579 226.170,328.231 223.920,328.008 222.600 C 327.785 221.280,326.695 214.260,325.587 207.000 C 324.478 199.740,323.124 191.010,322.578 187.600 C 322.032 184.190,321.325 179.780,321.007 177.800 C 320.689 175.820,320.203 172.711,319.925 170.892 C 319.508 168.155,319.315 167.635,318.810 167.885 C 318.475 168.051,314.780 168.279,310.600 168.393 C 301.870 168.631,302.159 168.535,301.810 171.300 C 299.468 189.825,283.036 203.371,264.230 202.279 C 247.552 201.310,233.532 188.465,231.203 172.020 C 230.651 168.125,232.337 168.400,209.000 168.400 C 185.703 168.400,187.432 168.114,186.814 172.064 C 180.639 211.474,123.556 212.329,116.559 173.117 C 115.684 168.218,116.787 168.629,103.881 168.400 C 97.676 168.290,92.285 168.046,91.900 167.857 C 91.307 167.567,91.200 167.712,91.200 168.809 M211.015 197.632 C 214.247 200.942,215.394 201.357,220.447 201.050 C 225.236 200.759,225.846 201.071,224.310 203.023 C 221.899 206.089,216.333 205.843,210.779 202.425 C 208.312 200.907,207.846 200.906,205.600 202.405 C 200.180 206.022,193.561 206.136,191.600 202.647 C 190.824 201.267,191.555 200.665,193.600 201.001 C 199.643 201.993,201.561 201.450,204.900 197.805 C 207.534 194.929,208.352 194.906,211.015 197.632 "
|
||||
stroke="none" fill="url(#Gradient2)" fill-rule="evenodd"></path>
|
||||
<path id="path1"
|
||||
d="M148.000 131.622 C 132.020 133.140,119.114 145.293,116.377 161.400 C 115.675 165.535,116.697 165.239,103.579 165.112 C 97.252 165.050,92.159 165.135,92.261 165.300 C 92.364 165.465,92.279 165.600,92.074 165.600 C 91.391 165.600,91.684 167.617,92.424 168.013 C 92.839 168.235,97.730 168.400,103.897 168.400 C 116.551 168.400,115.666 168.070,116.562 173.117 C 123.522 212.326,180.639 211.470,186.814 172.064 C 187.432 168.114,185.703 168.400,209.000 168.400 C 232.337 168.400,230.651 168.125,231.203 172.020 C 233.532 188.465,247.552 201.310,264.230 202.279 C 283.039 203.371,299.468 189.826,301.811 171.294 C 302.169 168.458,302.345 168.400,310.600 168.400 C 318.988 168.400,319.609 168.159,318.852 165.200 C 318.824 165.090,315.273 165.052,310.961 165.117 C 301.855 165.252,302.274 165.421,301.620 161.367 C 295.239 121.807,237.531 121.949,231.235 161.540 C 231.013 162.937,230.622 164.332,230.367 164.640 C 229.717 165.423,188.328 165.481,187.689 164.700 C 187.463 164.425,186.981 162.602,186.617 160.650 C 183.194 142.288,166.648 129.850,148.000 131.622 M159.886 135.622 C 183.880 141.759,192.035 172.033,174.435 189.636 C 156.404 207.671,125.892 198.962,119.944 174.084 C 114.368 150.761,136.343 129.601,159.886 135.622 M274.658 135.615 C 299.082 141.861,307.061 172.817,288.734 190.222 C 267.975 209.936,234.188 195.420,234.219 166.800 C 234.241 145.502,253.935 130.315,274.658 135.615 M148.542 152.793 C 139.499 154.706,134.598 164.925,138.715 173.287 C 145.082 186.221,164.875 182.425,165.902 168.074 C 166.591 158.446,157.916 150.811,148.542 152.793 M262.091 153.188 C 253.922 156.127,249.883 165.408,253.408 173.139 C 257.835 182.850,271.593 184.491,278.201 176.097 C 287.389 164.426,275.974 148.192,262.091 153.188 M204.900 197.805 C 201.561 201.450,199.643 201.993,193.600 201.001 C 191.555 200.665,190.824 201.267,191.600 202.647 C 193.561 206.136,200.180 206.022,205.600 202.405 C 207.846 200.906,208.312 200.907,210.779 202.425 C 216.333 205.843,221.899 206.089,224.310 203.023 C 225.846 201.071,225.236 200.759,220.447 201.050 C 215.394 201.357,214.247 200.942,211.015 197.632 C 208.352 194.906,207.534 194.929,204.900 197.805 "
|
||||
stroke="none" fill="#ffffff" fill-rule="evenodd"></path>
|
||||
|
||||
<g id="nyanpasu_logo" fill-rule="nonzero">
|
||||
<path
|
||||
d="M349.980734,470.820835 C362.906597,470.820835 375.380055,471.040616 387.808272,470.736801 C394.678368,470.562269 398.898662,465.526692 400.333433,457.394784 C402.20122,446.832354 400.417451,436.780592 395.809381,427.284747 C390.832924,417.03906 385.255414,407.090725 380.233716,396.903216 C360.528238,356.825451 356.463054,315.532425 371.896535,273.134028 C376.646789,260.076434 384.111475,248.479739 394.781775,239.255389 C400.869857,233.993566 405.684741,233.870747 409.911498,238.718864 C414.138255,243.56698 413.285148,248.415097 407.500824,253.663991 C390.535629,269.081003 384.285974,289.34613 382.230762,311.27901 C379.516331,340.367711 385.669042,367.633519 399.499715,393.373787 C407.100123,407.523823 416.070671,421.169656 418.88851,437.433471 C420.614112,447.317165 420.484854,457.129754 417.402035,466.787202 C412.613003,481.764651 401.244707,490.012913 385.513931,490 C273.567341,489.896558 161.622905,489.814679 49.6806226,489.754347 C42.5713979,489.754347 35.4621733,489.993521 28.605003,487.73753 C11.4136053,482.081394 -0.0322464095,467.259085 0,448.855634 C0.103475152,390.930336 0.374918274,333.011502 3.10227536,275.124988 C4.89250738,237.057576 7.30964376,199.054805 11.2067914,161.148996 C16.3771366,110.612228 23.6608604,60.405131 34.5315112,10.7604159 C35.0024707,8.67513302 35.7547542,6.66358964 36.7676855,4.78107191 C38.5191399,1.50374502 41.1689418,-0.513071536 45.1694964,0.113951558 C49.170051,0.740974653 49.3768648,4.11526388 49.9326769,6.9530282 C53.9913979,27.6706469 60.3056819,47.7612426 66.8590945,67.7742684 C68.9853989,74.238424 70.1875042,82.4543658 74.7891114,86.326395 C79.26146,90.0949977 87.6051045,87.3541957 94.2425352,87.3735882 C154.302557,87.5028713 214.369043,86.8176708 274.422602,88.3755323 C279.256875,88.4983513 281.615845,87.0827012 283.509484,82.6353621 C293.203881,59.8750702 301.967616,36.8044988 308.495177,12.925908 C309.309506,9.94593225 309.942874,6.778496 313.710763,6.46175237 C317.129653,6.17732953 319.346439,8.49796139 320.858765,11.1547293 C321.981523,13.2263513 322.777248,15.4591645 323.217735,17.7740247 C332.832735,64.3247661 340.022746,111.343788 344.758685,158.640904 C354.220417,251.724745 355.745669,345.112401 356.152834,438.538842 C356.152834,444.123872 356.217463,449.695975 355.370819,455.274541 C354.569415,460.646254 352.048872,465.38448 349.980734,470.820835 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M314.373914,51.8580359 C319.16418,79.1645649 322.474053,106.509891 325.189183,133.926344 C335.474355,237.585109 337,341.580114 337,445.633314 C337,462.193111 328.052999,470.974135 311.283837,470.980602 C222.356854,471.006466 133.429871,471.006466 44.5028879,470.980602 C27.8242301,470.980602 18.9741981,462.050856 19,445.491059 C19.0776317,397.460537 19.1422776,349.423548 20.816608,301.425356 C22.7559868,246.3726 25.665055,191.384506 31.4637977,136.538667 C34.2500385,110.221458 37.8831415,84.0335719 41.044329,57.7745583 C41.348165,55.2398296 42.0398768,52.7503639 42.8026991,49 C47.5541772,66.3034032 53.702008,81.848015 58.6926762,97.7676631 C60.6902363,104.123883 64.0001095,106.632747 70.7620769,106.632747 C141.816607,106.58964 212.868982,106.72974 283.919202,107.053047 C290.11875,107.053047 293.415694,104.679972 295.555475,99.1643504 C301.63866,83.3804912 308.652746,67.9328716 314.373914,51.8580359 Z"
|
||||
id="bg" fill="#ffffff" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M119.999315,204.118735 C120.083729,213.985193 112.350174,222.057169 102.850433,222 C94.0584657,221.961379 86.9547636,214.451375 87,205.249067 C87.0456702,196.148935 94.5909186,188.268541 103.493273,188.006712 C112.395627,187.744884 119.921396,195.178255 119.999315,204.118735 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M236,204.861743 C236,195.671258 243.035135,188.142475 251.816026,188.001869 C261.124552,187.861264 268.993481,195.671258 269,205.104607 C269.006509,214.537955 262.179823,221.772746 253.255623,221.996436 C244.16206,222.194562 236.026056,214.103357 236,204.861743 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
<path
|
||||
d="M190.493614,267.920884 C185.555364,268.142338 181.468093,262.430145 177.097162,262.137045 C172.945422,261.850458 168.716321,267.914371 162.875519,267.999044 C153.295574,268.096744 144.921179,260.691085 144.089542,251.064378 C143.851011,248.296212 143.844564,245.541072 147.312943,245.059085 C150.362279,244.622692 151.0263,246.902358 151.638746,249.481638 C153.327808,256.646305 157.286143,260.411011 162.701456,259.935538 C168.890385,259.394931 172.371657,255.649765 173.087252,249.468612 C173.383805,247.045652 174.234782,245.104678 176.819949,245.026518 C179.869286,244.935332 180.481732,247.208485 180.855647,249.813818 C181.751752,255.988458 185.426428,259.870405 191.570229,259.889945 C197.71403,259.909485 201.433834,255.981945 202.342833,249.872438 C202.774769,246.954465 203.516151,244.433805 206.907168,245.111192 C210.130569,245.762525 210.195037,248.634905 209.872697,251.383532 C208.750953,260.769245 200.621538,268.044638 190.493614,267.920884 Z"
|
||||
id="element" fill-rule="evenodd"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,21 @@
|
||||
.page-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.page-enter-active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
|
||||
.page-exit {
|
||||
opacity: 1;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
.page-exit-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
@@ -4,13 +4,21 @@ body {
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-main: #5b5c9d;
|
||||
--text-primary: #637381;
|
||||
--selection-color: #f5f5f5;
|
||||
--scroller-color: #90939980;
|
||||
--background-color: #ffffff;
|
||||
--background-color-alpha: rgba(24, 103, 192, 0.1);
|
||||
--border-radius: 12px;
|
||||
}
|
||||
|
||||
::selection {
|
||||
@@ -25,8 +33,26 @@ body {
|
||||
}
|
||||
*::-webkit-scrollbar-thumb {
|
||||
border-radius: 6px;
|
||||
background-color: rgba(#909399, 0.5);
|
||||
background-color: var(--scroller-color);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@import "./layout.scss";
|
||||
@import "./page.scss";
|
||||
@import "./anime.scss";
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
background-color: rgba(18, 18, 18, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.user-none {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||