mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-22 19:24:01 -04:00
Compare commits
695
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8865e1311 | ||
|
|
686eb1bfd6 | ||
|
|
4a87e8227d | ||
|
|
c0b26caf2b | ||
|
|
c6c40dbced | ||
|
|
02ddd010a0 | ||
|
|
6eb9df87cf | ||
|
|
0adbdfdde1 | ||
|
|
fb002d1977 | ||
|
|
8b7c9085ca | ||
|
|
8b34bbf71a | ||
|
|
fd468a6b78 | ||
|
|
7c04b61bcc | ||
|
|
bc4916a132 | ||
|
|
58a356bb05 | ||
|
|
3061e1301d | ||
|
|
e333da2013 | ||
|
|
307d05d76c | ||
|
|
10dbe73335 | ||
|
|
b6fee3a98f | ||
|
|
6166d51c7c | ||
|
|
d06fcccff9 | ||
|
|
d9cfeded47 | ||
|
|
c4ced3ac9d | ||
|
|
ce2ec80d80 | ||
|
|
a340eba493 | ||
|
|
2db7646fe3 | ||
|
|
097de493bb | ||
|
|
a7db0354ae | ||
|
|
2d2502cc67 | ||
|
|
402a8515ee | ||
|
|
4cf0f0c898 | ||
|
|
fd0564a3ec | ||
|
|
07f8675a7b | ||
|
|
cab2a1a3bc | ||
|
|
41dd91fe75 | ||
|
|
c074b220ee | ||
|
|
243d716703 | ||
|
|
fde2181c3b | ||
|
|
a5ffd18596 | ||
|
|
e0aa1f0177 | ||
|
|
e73b9c3ce1 | ||
|
|
69955286a7 | ||
|
|
d3a02f6342 | ||
|
|
86234e5b09 | ||
|
|
938fabe27d | ||
|
|
1b34aa0ce1 | ||
|
|
76ed5d68fa | ||
|
|
b69496dbcd | ||
|
|
ef179f9c92 | ||
|
|
b451d4d8e0 | ||
|
|
12f7675950 | ||
|
|
06cd88e990 | ||
|
|
aa4b99def4 | ||
|
|
4015f634c4 | ||
|
|
b2d1ec6690 | ||
|
|
1efc10dc7e | ||
|
|
94710cf643 | ||
|
|
3711cb11fa | ||
|
|
40a0eb6841 | ||
|
|
0a643028b3 | ||
|
|
0189eb0dbb | ||
|
|
ed26bdebdc | ||
|
|
2e2c74e355 | ||
|
|
c1a3be60c3 | ||
|
|
f8260513b1 | ||
|
|
ed1855946f | ||
|
|
c0bc1d8885 | ||
|
|
3f09d7b1c8 | ||
|
|
710f20d085 | ||
|
|
6261e580aa | ||
|
|
42912ebe74 | ||
|
|
e8a755b85d | ||
|
|
6713f05e02 | ||
|
|
5d39702136 | ||
|
|
166b82df1b | ||
|
|
bca1466162 | ||
|
|
75b8932b20 | ||
|
|
a1b51b191f | ||
|
|
5defc26929 | ||
|
|
71e8a85cd1 | ||
|
|
95b2750e5f | ||
|
|
7f1a423174 | ||
|
|
33e599100c | ||
|
|
cb85620777 | ||
|
|
faf25e98a5 | ||
|
|
d7b264701e | ||
|
|
c929a8952b | ||
|
|
82089c588a | ||
|
|
19a56d1835 | ||
|
|
8371fa8ef5 | ||
|
|
acca7fae68 | ||
|
|
a478c27fd0 | ||
|
|
094dcdd035 | ||
|
|
344c1e610c | ||
|
|
fdf8722818 | ||
|
|
f2114c1a01 | ||
|
|
6ed79d06df | ||
|
|
8ff677d2eb | ||
|
|
700f43a194 | ||
|
|
53a5582858 | ||
|
|
0bbe83380c | ||
|
|
6a4ce613bb | ||
|
|
ea0b3159dd | ||
|
|
ad30c2155a | ||
|
|
5142d576f5 | ||
|
|
592ca3ee8f | ||
|
|
6c54855fdd | ||
|
|
41c01a9319 | ||
|
|
f9a9e9baf8 | ||
|
|
053337d9b8 | ||
|
|
4693178345 | ||
|
|
0fc4aac964 | ||
|
|
ab1e57dda5 | ||
|
|
1d2ba798b0 | ||
|
|
78e3308f40 | ||
|
|
99e2368b1c | ||
|
|
c6c14576eb | ||
|
|
f022aa6796 | ||
|
|
0fe56c4bfa | ||
|
|
9378dfc44e | ||
|
|
f65e5ef2f2 | ||
|
|
81cbcef3bc | ||
|
|
e8082ad75d | ||
|
|
5ed5845eaf | ||
|
|
13fafa1dff | ||
|
|
6df71083ec | ||
|
|
863152a441 | ||
|
|
65ee46e330 | ||
|
|
0a46f84aaa | ||
|
|
de24b1515a | ||
|
|
a781b7b236 | ||
|
|
3effe6e7db | ||
|
|
875d32176d | ||
|
|
63ec4bd945 | ||
|
|
9a7177f929 | ||
|
|
4f0cbf5b03 | ||
|
|
95ee917e37 | ||
|
|
4d6d7e0d5f | ||
|
|
ed9e01e438 | ||
|
|
226868b6f0 | ||
|
|
dd068dbd13 | ||
|
|
5f4ba58acb | ||
|
|
881cfd2fd3 | ||
|
|
4f6841d8d2 | ||
|
|
823f5f85d5 | ||
|
|
7fa9c42f46 | ||
|
|
c0135b11e5 | ||
|
|
5bb6058751 | ||
|
|
0c51946f4f | ||
|
|
ef63de44b4 | ||
|
|
0b87df2608 | ||
|
|
ba04356d64 | ||
|
|
8fb83d251c | ||
|
|
3b990694c2 | ||
|
|
0540091a04 | ||
|
|
b32fe1e806 | ||
|
|
ae7ed119a6 | ||
|
|
fa445388e4 | ||
|
|
d23226f3ea | ||
|
|
99d36e9421 | ||
|
|
f273a64703 | ||
|
|
9ea8d2015b | ||
|
|
0604de881e | ||
|
|
16c098d09b | ||
|
|
99c5f96575 | ||
|
|
7cc0955358 | ||
|
|
285ce66d99 | ||
|
|
887509548b | ||
|
|
c580aa26c0 | ||
|
|
e1b2279a70 | ||
|
|
9eaaed3203 | ||
|
|
5c552f97e4 | ||
|
|
212271291c | ||
|
|
905726547d | ||
|
|
4624ceb8f0 | ||
|
|
7372b79c2d | ||
|
|
9522049881 | ||
|
|
ac46c7dd1b | ||
|
|
4cdfc53dcd | ||
|
|
393a54f7eb | ||
|
|
151aedbb68 | ||
|
|
9628628d38 | ||
|
|
c68b7e5333 | ||
|
|
9bd31e30f4 | ||
|
|
7f73a81c59 | ||
|
|
b8cceb7b3a | ||
|
|
ce879a6219 | ||
|
|
22ff24b0e4 | ||
|
|
e99813b07d | ||
|
|
08dde7f79a | ||
|
|
6ce86a07a5 | ||
|
|
f29a48f681 | ||
|
|
feab79df83 | ||
|
|
754040ca42 | ||
|
|
d272e7d9da | ||
|
|
104bbebbc2 | ||
|
|
3306772f97 | ||
|
|
8354113c34 | ||
|
|
bada28ac4b | ||
|
|
f0867798e6 | ||
|
|
2f19ccf707 | ||
|
|
ae3acb2462 | ||
|
|
f1bbb97b2a | ||
|
|
97b5641f67 | ||
|
|
9f136da2e1 | ||
|
|
93a7c17e59 | ||
|
|
618ad1135d | ||
|
|
7aa1b806d2 | ||
|
|
4aac96c845 | ||
|
|
fdabf949f1 | ||
|
|
4c90a36ecf | ||
|
|
f0753dcfd8 | ||
|
|
2c471e1c9e | ||
|
|
cef1eb962e | ||
|
|
00f7db98b5 | ||
|
|
07f89e314f | ||
|
|
54a64eb0b2 | ||
|
|
3604fc83a2 | ||
|
|
3538bdb9cd | ||
|
|
1486eafa75 | ||
|
|
d8ceb12164 | ||
|
|
23faed0bd4 | ||
|
|
95e617fa95 | ||
|
|
e620677f97 | ||
|
|
030974ce75 | ||
|
|
9255596b08 | ||
|
|
48e4709eb5 | ||
|
|
b97c9b0b9a | ||
|
|
17d535b7a4 | ||
|
|
52f393d821 | ||
|
|
c0c2c73e8f | ||
|
|
460013a77d | ||
|
|
8fabf22e3d | ||
|
|
f8cf761fa6 | ||
|
|
78352fd1fc | ||
|
|
dca1bd3591 | ||
|
|
35aa1329e8 | ||
|
|
ed60fb7e4f | ||
|
|
2e8257661c | ||
|
|
fdffc61d39 | ||
|
|
530c58b928 | ||
|
|
12df4bdb8f | ||
|
|
80c85bf929 | ||
|
|
fc42786fec | ||
|
|
be9aeaefbc | ||
|
|
8d36628df1 | ||
|
|
422dd2c9b0 | ||
|
|
d81aca468e | ||
|
|
9ed4acd4f8 | ||
|
|
42080db61b | ||
|
|
b7034fcadb | ||
|
|
d01a6d87d5 | ||
|
|
2934188296 | ||
|
|
25e95d597c | ||
|
|
0091e703a3 | ||
|
|
64d0e40baf | ||
|
|
c653378db7 | ||
|
|
3b527f1f7f | ||
|
|
662dffc141 | ||
|
|
67968aef79 | ||
|
|
f91b43b29d | ||
|
|
5b8f083fe5 | ||
|
|
ea0b158eb8 | ||
|
|
6feac1f79e | ||
|
|
34cf6e6346 | ||
|
|
c8ec045a3c | ||
|
|
d9462c5484 | ||
|
|
778efb832e | ||
|
|
195ec3bf6e | ||
|
|
7802572193 | ||
|
|
0bee6a2844 | ||
|
|
fb96661092 | ||
|
|
aabfb080e1 | ||
|
|
d951dbddbd | ||
|
|
4cf375f2fd | ||
|
|
d4069a778b | ||
|
|
ca7b165eb8 | ||
|
|
c9e6144de8 | ||
|
|
4f9b17503e | ||
|
|
1065bf07b6 | ||
|
|
20b86057d4 | ||
|
|
c31fad6381 | ||
|
|
eadb8cf9c7 | ||
|
|
744fa54ae0 | ||
|
|
4d9d3422d2 | ||
|
|
2ce790bcfe | ||
|
|
44e82c6e42 | ||
|
|
8bff5eb4fc | ||
|
|
2c44b794f1 | ||
|
|
5c9dd50c2e | ||
|
|
7109a52ed1 | ||
|
|
36cfd5faa1 | ||
|
|
60a8310002 | ||
|
|
c57fd7aad1 | ||
|
|
4447d77ba6 | ||
|
|
f89b21a0ef | ||
|
|
4a021a73bf | ||
|
|
b482e9b898 | ||
|
|
26fb129887 | ||
|
|
92d3550dc4 | ||
|
|
c6083d6177 | ||
|
|
45cd00bed3 | ||
|
|
523e0232d2 | ||
|
|
d22dae1494 | ||
|
|
a669253c58 | ||
|
|
124578b695 | ||
|
|
3630b46ee3 | ||
|
|
20aafebd87 | ||
|
|
90c5f65b0f | ||
|
|
589737e318 | ||
|
|
2f4afeb889 | ||
|
|
e6a5fae190 | ||
|
|
52244c01f7 | ||
|
|
44bae0934d | ||
|
|
c90b0cddab | ||
|
|
486febd4ce | ||
|
|
47fe1eb512 | ||
|
|
6a0197d252 | ||
|
|
7f7155b5d5 | ||
|
|
9b04b47e98 | ||
|
|
c694d9938c | ||
|
|
b6436196be | ||
|
|
0253f9daa5 | ||
|
|
521b56f1e1 | ||
|
|
0daf24095c | ||
|
|
7ebe072255 | ||
|
|
7028ea323d | ||
|
|
4147d81f01 | ||
|
|
0d2215e18c | ||
|
|
f6259d0ff7 | ||
|
|
d10ca38651 | ||
|
|
148d8440ec | ||
|
|
c67450cd2e | ||
|
|
754ebab48d | ||
|
|
fde8bb6487 | ||
|
|
afe0dbb104 | ||
|
|
396d490979 | ||
|
|
8bebd68068 | ||
|
|
101841585f | ||
|
|
c30ee19ec6 | ||
|
|
e78951d3c3 | ||
|
|
161c597c55 | ||
|
|
327aad210c | ||
|
|
084e6ce608 | ||
|
|
262acda336 | ||
|
|
5ca033d839 | ||
|
|
d83a0037f3 | ||
|
|
f44b7b9851 | ||
|
|
978203cd52 | ||
|
|
dce1095f8d | ||
|
|
c3fd526698 | ||
|
|
d7b19809aa | ||
|
|
a503e869cf | ||
|
|
0100c64d7a | ||
|
|
a56360d8ed | ||
|
|
9abaf4051e | ||
|
|
8485d66139 | ||
|
|
fc1c9f6c54 | ||
|
|
070090c35a | ||
|
|
d067959f1c | ||
|
|
52abc81011 | ||
|
|
d4fecfcd5c | ||
|
|
ed8fbf47b0 | ||
|
|
ba360213dd | ||
|
|
214de4a570 | ||
|
|
8527b5ed0e | ||
|
|
2ca2e53b6b | ||
|
|
c93767fdfe | ||
|
|
cb67a5c51a | ||
|
|
7d8532b075 | ||
|
|
fdf0d73b30 | ||
|
|
ef05dbed9b | ||
|
|
fb9f00a616 | ||
|
|
209cfe664f | ||
|
|
faeced373f | ||
|
|
6f1bcfd2c2 | ||
|
|
8d9c961605 | ||
|
|
70d0dd8f1d | ||
|
|
7b9b34b262 | ||
|
|
f83d6c7778 | ||
|
|
47311e9af7 | ||
|
|
b75a5f2bc1 | ||
|
|
87dc1b1ff4 | ||
|
|
f6d26251df | ||
|
|
6481f61028 | ||
|
|
2945b0d6f1 | ||
|
|
7974887295 | ||
|
|
373a3d3136 | ||
|
|
2f27eab949 | ||
|
|
c2faa0ea1c | ||
|
|
da664565b1 | ||
|
|
c3c9b13ef6 | ||
|
|
422406267e | ||
|
|
ed4cc1da7b | ||
|
|
db1ab5b7b4 | ||
|
|
b0b9b61593 | ||
|
|
80bad610ce | ||
|
|
b6d051a61f | ||
|
|
8e2168ecf8 | ||
|
|
0b526d9b05 | ||
|
|
bde4decc5d | ||
|
|
3c817dc0d0 | ||
|
|
fc90bddad1 | ||
|
|
a713485599 | ||
|
|
f16bdda969 | ||
|
|
cf4f48069f | ||
|
|
38cca3195f | ||
|
|
40a9ca348c | ||
|
|
a1ba5cf9ad | ||
|
|
915db2dce7 | ||
|
|
1dc0ceea1f | ||
|
|
1a621c3f88 | ||
|
|
b7ec57a534 | ||
|
|
a575363f89 | ||
|
|
3f6cd0b579 | ||
|
|
630e9bd3cd | ||
|
|
9013464312 | ||
|
|
dade416ca0 | ||
|
|
401872881e | ||
|
|
7cb97f4462 | ||
|
|
82b1b36b1a | ||
|
|
e203e76ccc | ||
|
|
adcafbcc8e | ||
|
|
9981fcf529 | ||
|
|
2baf048293 | ||
|
|
cffc33e14a | ||
|
|
0c811e3417 | ||
|
|
6ad46cf2c2 | ||
|
|
34626070af | ||
|
|
80b27b9a6a | ||
|
|
49f968d26b | ||
|
|
99b0dc596d | ||
|
|
158c0c12d8 | ||
|
|
e089948225 | ||
|
|
7f2ba56e06 | ||
|
|
6de5593216 | ||
|
|
365474b0d9 | ||
|
|
dc8a47644a | ||
|
|
27483e68dc | ||
|
|
400a18a8ed | ||
|
|
32ddf614c3 | ||
|
|
19d919ed5c | ||
|
|
594a2cde19 | ||
|
|
11287459c3 | ||
|
|
ef191babb7 | ||
|
|
fe64a342f9 | ||
|
|
e54be7d12d | ||
|
|
81c886784b | ||
|
|
6682bb120c | ||
|
|
5c02ec4789 | ||
|
|
de1e1757c3 | ||
|
|
43d331d6cf | ||
|
|
01832856d4 | ||
|
|
0033e3f0e0 | ||
|
|
2d3706321a | ||
|
|
5bb884db57 | ||
|
|
b42763ccbf | ||
|
|
f2a6d62d65 | ||
|
|
f66693df6b | ||
|
|
a710d6d7cc | ||
|
|
64461c534f | ||
|
|
73da4879f6 | ||
|
|
8594a414ec | ||
|
|
df396bfa43 | ||
|
|
33677150b1 | ||
|
|
c67b185076 | ||
|
|
bab2078dfd | ||
|
|
e9bc0169f6 | ||
|
|
59c83d19e4 | ||
|
|
9b7d3c64fe | ||
|
|
c367153bac | ||
|
|
1f94c3cbd4 | ||
|
|
564583951c | ||
|
|
eed3617a0d | ||
|
|
24cb3d19a0 | ||
|
|
3182c70857 | ||
|
|
8bb7396362 | ||
|
|
62648ed75a | ||
|
|
fa629fbf99 | ||
|
|
7222e072af | ||
|
|
f57f254855 | ||
|
|
fa094db127 | ||
|
|
77a357109a | ||
|
|
295657492d | ||
|
|
2bafc21229 | ||
|
|
ea03fb2788 | ||
|
|
0815e48465 | ||
|
|
42060366cb | ||
|
|
8af71036cc | ||
|
|
5854327c28 | ||
|
|
99f452c999 | ||
|
|
d818067bee | ||
|
|
5538913320 | ||
|
|
8188ced955 | ||
|
|
75eac83b47 | ||
|
|
ce548cb29c | ||
|
|
61510c2186 | ||
|
|
237fb57d5e | ||
|
|
fe561f2b5d | ||
|
|
1c01254e9b | ||
|
|
958672badd | ||
|
|
93c3030a1e | ||
|
|
53c729f685 | ||
|
|
e0480bf383 | ||
|
|
e63d210358 | ||
|
|
30d9a34011 | ||
|
|
f0bdaa7f72 | ||
|
|
fa6c36ad57 | ||
|
|
5de0339506 | ||
|
|
814871d982 | ||
|
|
68e60f036d | ||
|
|
d185135dd7 | ||
|
|
70ceee139b | ||
|
|
e58b41498c | ||
|
|
116cef0b3a | ||
|
|
dc75f1f01d | ||
|
|
367ad5f69a | ||
|
|
8577c6b815 | ||
|
|
3d9374d2b2 | ||
|
|
e300b14dde | ||
|
|
dc4916b9e0 | ||
|
|
fb65a23fac | ||
|
|
a5b5ed7e51 | ||
|
|
dfbbfe1793 | ||
|
|
a69acf171a | ||
|
|
1402d2312a | ||
|
|
d003683689 | ||
|
|
57fd39d84d | ||
|
|
6239e5a8e4 | ||
|
|
4c59d61f89 | ||
|
|
2a8b88c1f0 | ||
|
|
dbaf2e922c | ||
|
|
eb330f62e1 | ||
|
|
90291bd627 | ||
|
|
069df80b22 | ||
|
|
2846363f55 | ||
|
|
0cdb065739 | ||
|
|
2114ece0df | ||
|
|
4a90bd3fb1 | ||
|
|
698b66cf91 | ||
|
|
8fc38de0a1 | ||
|
|
235f0668b8 | ||
|
|
5dfd875b9e | ||
|
|
00b22df802 | ||
|
|
b22b1bef9b | ||
|
|
5cae48a85d | ||
|
|
4200270b55 | ||
|
|
c2c180d304 | ||
|
|
3938e60ce4 | ||
|
|
2986e354e8 | ||
|
|
bf12665adb | ||
|
|
f8fe16738e | ||
|
|
3c688cfbd3 | ||
|
|
92ba2bb57d | ||
|
|
504c132f78 | ||
|
|
bdaa6bcc9d | ||
|
|
a50bb2cab3 | ||
|
|
b17a90a412 | ||
|
|
0ffec5011b | ||
|
|
adbbd9b112 | ||
|
|
95e1d6b72e | ||
|
|
4cb09dcc1b | ||
|
|
ed44d02202 | ||
|
|
4352f49ae3 | ||
|
|
9ddea82063 | ||
|
|
3c0f2cbc48 | ||
|
|
6c45413d7a | ||
|
|
149e1e64b1 | ||
|
|
5589cfb783 | ||
|
|
ff71502083 | ||
|
|
1ef5371c3b | ||
|
|
3951cd6b18 | ||
|
|
9686f29246 | ||
|
|
6d206c9521 | ||
|
|
48e36ae146 | ||
|
|
7c24e85c19 | ||
|
|
fe1a783ec2 | ||
|
|
358496134c | ||
|
|
dc924618fb | ||
|
|
fb2dbced08 | ||
|
|
cdaedad969 | ||
|
|
bf408f8d00 | ||
|
|
b169fe0d77 | ||
|
|
9b67cedaa1 | ||
|
|
2edf70a144 | ||
|
|
72a71cacba | ||
|
|
0440e40a82 | ||
|
|
ec27d4643c | ||
|
|
f18d36f6c2 | ||
|
|
3254cc6a1e | ||
|
|
4af3225f21 | ||
|
|
3c5245914f | ||
|
|
89814a2c65 | ||
|
|
5d2093e54a | ||
|
|
3cd52ca327 | ||
|
|
5b41d699fa | ||
|
|
544599bf1c | ||
|
|
296b3a3d7e | ||
|
|
729a990fa7 | ||
|
|
c3fa7b2e1d | ||
|
|
2c5a1a2804 | ||
|
|
1973526c4e | ||
|
|
bba5502960 | ||
|
|
52740290b2 | ||
|
|
27703575bc | ||
|
|
e6504add7b | ||
|
|
f1e9121295 | ||
|
|
9ff751b82a | ||
|
|
21eaaef056 | ||
|
|
bb0be2b215 | ||
|
|
25847c3f03 | ||
|
|
7b5c25c50f | ||
|
|
7535b70fa6 | ||
|
|
e3034e4e94 | ||
|
|
3da19e5c15 | ||
|
|
ca89e12963 | ||
|
|
4fb6995796 | ||
|
|
3f5a54aa88 | ||
|
|
d379d251b9 | ||
|
|
7ab0e01573 | ||
|
|
2cb48aaf6b | ||
|
|
e4657aa5f9 | ||
|
|
197d17ac4e | ||
|
|
31ea83584b | ||
|
|
ea66b136ba | ||
|
|
06c0ea2afb | ||
|
|
f4f47c0bc5 | ||
|
|
8a0ed8a50f | ||
|
|
63eea01243 | ||
|
|
f590a2965a | ||
|
|
4ab03deded | ||
|
|
846d07d86a | ||
|
|
55d88d8dfb | ||
|
|
a803f96f41 | ||
|
|
da7cc5648b | ||
|
|
75443758f9 | ||
|
|
eaecff93a4 | ||
|
|
6e7c20c59c | ||
|
|
406dcfe110 | ||
|
|
e2b3a2e3ca | ||
|
|
71ab752e1b | ||
|
|
204ecd0461 | ||
|
|
0fdfac366e | ||
|
|
52123458c7 | ||
|
|
26b2955cf3 | ||
|
|
d82d86df5c | ||
|
|
c445597f83 | ||
|
|
6a58adfb29 | ||
|
|
05feb211ba | ||
|
|
a3b2167e58 | ||
|
|
0b69feaa1a | ||
|
|
494144a7c7 | ||
|
|
2a2c1ca9e6 | ||
|
|
56b7ecb008 | ||
|
|
867102b82c | ||
|
|
4bdb7d17b2 | ||
|
|
c44ffae751 | ||
|
|
ce1595d62d | ||
|
|
45f6232e32 | ||
|
|
9cf2ca7196 | ||
|
|
c0eeed4e89 | ||
|
|
d0a4c1c56e | ||
|
|
cb0dc9c68d | ||
|
|
0439d017b9 | ||
|
|
8008238ca0 | ||
|
|
a095d0ed90 | ||
|
|
a48cce59d4 | ||
|
|
4c806f83f1 | ||
|
|
bdfd565b72 | ||
|
|
f3b698590c | ||
|
|
7826d42ce4 | ||
|
|
6cc30c289b | ||
|
|
43cc8e69d1 | ||
|
|
a62ae336e0 | ||
|
|
e94af2a7aa | ||
|
|
bb9e69e0c1 | ||
|
|
1cc9218ff6 | ||
|
|
56712f46fa | ||
|
|
93168ee073 | ||
|
|
1724aedd49 | ||
|
|
d799175c07 | ||
|
|
ee6f7b4798 | ||
|
|
0511cd19df | ||
|
|
71b1901ab0 | ||
|
|
32e2d96e55 | ||
|
|
647766b9fa | ||
|
|
0cd8974110 | ||
|
|
0c4c5fc146 | ||
|
|
b447e16374 | ||
|
|
0bb8353a33 | ||
|
|
19a7dcf17d | ||
|
|
8a1acb63c9 | ||
|
|
6f298d3f52 | ||
|
|
3e481a566b |
@@ -514,6 +514,37 @@ Quickshell.execDetached(["dms", "cl", "copy", textToCopy])
|
|||||||
|
|
||||||
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
**Do NOT use** `globalThis.clipboard` or browser JavaScript APIs - they don't exist in the QML runtime.
|
||||||
|
|
||||||
|
## Translations (Optional)
|
||||||
|
|
||||||
|
Ship per-locale files in a `translations/` directory; DMS loads the one matching the active locale and re-reads it on locale changes:
|
||||||
|
|
||||||
|
```
|
||||||
|
MyPlugin/
|
||||||
|
└── translations/
|
||||||
|
├── es.json
|
||||||
|
└── zh_CN.json
|
||||||
|
```
|
||||||
|
|
||||||
|
File names follow DMS locale naming (`es.json`, `pt.json`, `zh_CN.json`); resolution tries the exact tag, then the hyphenated form, then the bare language. No `en.json` - the QML terms are the English strings.
|
||||||
|
|
||||||
|
Each file maps the English term to its translation, using the term as its own context bucket:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Cycle Speed": {
|
||||||
|
"Cycle Speed": "Velocidad de ciclo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In QML, call `I18n.trFor` with the plugin id as a literal string matching `plugin.json`:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
text: I18n.trFor("myPlugin", "Cycle Speed")
|
||||||
|
```
|
||||||
|
|
||||||
|
Lookup order: plugin file, then the global DMS catalog, then the English term. `ExampleEmojiPlugin` in `quickshell/PLUGINS/` demonstrates the full setup. Registry plugins can apply for central POEditor translation via the registry's CONTRIBUTING guide.
|
||||||
|
|
||||||
## Step 11: Validate and Test
|
## Step 11: Validate and Test
|
||||||
|
|
||||||
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
|
1. Validate `plugin.json` against the schema at [assets/plugin-schema.json](assets/plugin-schema.json)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Categories for GitHub's auto-generated release notes
|
||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
authors:
|
||||||
|
- github-actions[bot]
|
||||||
|
- dependabot[bot]
|
||||||
|
labels:
|
||||||
|
- internal
|
||||||
|
categories:
|
||||||
|
- title: Breaking Changes
|
||||||
|
labels: [breaking]
|
||||||
|
- title: Features
|
||||||
|
labels: [feature]
|
||||||
|
- title: Fixes
|
||||||
|
labels: [fix]
|
||||||
|
- title: Packaging
|
||||||
|
labels: [packaging]
|
||||||
|
- title: Internationalization
|
||||||
|
labels: [i18n]
|
||||||
|
- title: Documentation
|
||||||
|
labels: [docs]
|
||||||
|
- title: Other Changes
|
||||||
|
labels: ["*"]
|
||||||
Executable
+150
@@ -0,0 +1,150 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Prunes retired XBPS packages from the R2 bucket so it stays inside the
|
||||||
|
# Cloudflare free tier. Only objects under ARCHIVE_PREFIX are eligible; live
|
||||||
|
# repositories (<repo>/current/) are never touched.
|
||||||
|
#
|
||||||
|
# publish-void-r2.sh writes retired packages to archive/<repo>/<published_at>/.
|
||||||
|
# Snapshots are dropped oldest first:
|
||||||
|
# 1. snapshots older than KEEP_DAYS, keeping KEEP_MIN_SNAPSHOTS per repo
|
||||||
|
# 2. then more until the whole bucket is <= MAX_BUCKET_GB
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name
|
||||||
|
for name in "$@"; do
|
||||||
|
if [[ -z "${!name:-}" ]]; then
|
||||||
|
echo "error: $name is required" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env R2_BUCKET R2_ENDPOINT
|
||||||
|
|
||||||
|
ARCHIVE_PREFIX="${ARCHIVE_PREFIX:-archive/}"
|
||||||
|
MAX_BUCKET_GB="${MAX_BUCKET_GB:-8}"
|
||||||
|
KEEP_DAYS="${KEEP_DAYS:-30}"
|
||||||
|
KEEP_MIN_SNAPSHOTS="${KEEP_MIN_SNAPSHOTS:-2}"
|
||||||
|
DRY_RUN="${DRY_RUN:-false}"
|
||||||
|
|
||||||
|
case "$ARCHIVE_PREFIX" in
|
||||||
|
*/) ;;
|
||||||
|
*) echo "error: ARCHIVE_PREFIX must end with /" >&2; exit 2 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
aws_r2() {
|
||||||
|
aws --endpoint-url "$R2_ENDPOINT" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
human() {
|
||||||
|
numfmt --to=iec-i --suffix=B "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
objects="$(aws_r2 s3api list-objects-v2 \
|
||||||
|
--bucket "$R2_BUCKET" \
|
||||||
|
--output json \
|
||||||
|
--query 'Contents[].{key:Key,size:Size}')"
|
||||||
|
[[ -n "$objects" && "$objects" != null ]] || objects='[]'
|
||||||
|
|
||||||
|
total="$(jq 'map(.size) | add // 0' <<<"$objects")"
|
||||||
|
budget="$(awk -v g="$MAX_BUCKET_GB" 'BEGIN { printf "%d", g * 1073741824 }')"
|
||||||
|
now="$(date -u +%s)"
|
||||||
|
|
||||||
|
snapshots="$(jq -c --arg p "$ARCHIVE_PREFIX" '
|
||||||
|
map(select(.key | startswith($p)))
|
||||||
|
| map(.rel = (.key[($p | length):] | split("/")))
|
||||||
|
| map(select((.rel | length) >= 3))
|
||||||
|
| group_by(.rel[0:2])
|
||||||
|
| map({
|
||||||
|
repo: .[0].rel[0],
|
||||||
|
stamp: .[0].rel[1],
|
||||||
|
prefix: ($p + (.[0].rel[0:2] | join("/")) + "/"),
|
||||||
|
size: (map(.size) | add),
|
||||||
|
count: length
|
||||||
|
})
|
||||||
|
| sort_by(.stamp)
|
||||||
|
' <<<"$objects")"
|
||||||
|
|
||||||
|
to_delete="$(jq -c \
|
||||||
|
--argjson now "$now" \
|
||||||
|
--argjson keepDays "$KEEP_DAYS" \
|
||||||
|
--argjson keepMin "$KEEP_MIN_SNAPSHOTS" \
|
||||||
|
--argjson total "$total" \
|
||||||
|
--argjson budget "$budget" '
|
||||||
|
def epoch:
|
||||||
|
.stamp
|
||||||
|
| sub("T(?<h>[0-9]{2})-(?<m>[0-9]{2})-(?<s>[0-9]{2})Z$"; "T\(.h):\(.m):\(.s)Z")
|
||||||
|
| try fromdateiso8601 catch null;
|
||||||
|
(group_by(.repo) | map({key: .[0].repo, value: length}) | from_entries) as $perRepo
|
||||||
|
| reduce .[] as $s ({kept: $perRepo, total: $total, delete: []};
|
||||||
|
($s | epoch) as $e
|
||||||
|
| (.kept[$s.repo] > $keepMin and $e != null and ($now - $e) > $keepDays * 86400) as $expired
|
||||||
|
| (.total > $budget) as $over
|
||||||
|
| if $expired or $over then
|
||||||
|
.delete += [$s + {reason: (if $over then "budget" else "age" end)}]
|
||||||
|
| .total -= $s.size
|
||||||
|
| .kept[$s.repo] -= 1
|
||||||
|
else . end)
|
||||||
|
| .delete
|
||||||
|
' <<<"$snapshots")"
|
||||||
|
|
||||||
|
echo "bucket: $R2_BUCKET ($(human "$total") in $(jq length <<<"$objects") objects)"
|
||||||
|
echo "budget: $(human "$budget") (MAX_BUCKET_GB=$MAX_BUCKET_GB)"
|
||||||
|
echo "archive: $(jq length <<<"$snapshots") snapshots, $(human "$(jq 'map(.size) | add // 0' <<<"$snapshots")")"
|
||||||
|
echo "policy: KEEP_DAYS=$KEEP_DAYS KEEP_MIN_SNAPSHOTS=$KEEP_MIN_SNAPSHOTS DRY_RUN=$DRY_RUN"
|
||||||
|
|
||||||
|
deleted=0
|
||||||
|
freed=0
|
||||||
|
while IFS=$'\t' read -r prefix size count reason; do
|
||||||
|
[[ -n "$prefix" ]] || continue
|
||||||
|
# Belt and braces: never delete outside the archive or anything live.
|
||||||
|
case "$prefix" in
|
||||||
|
"$ARCHIVE_PREFIX"*) ;;
|
||||||
|
*) echo "error: refusing to delete $prefix (outside $ARCHIVE_PREFIX)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
case "$prefix" in
|
||||||
|
*/current/*) echo "error: refusing to delete live prefix $prefix" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
echo "would delete $prefix ($count objects, $(human "$size"), $reason)"
|
||||||
|
else
|
||||||
|
echo "deleting $prefix ($count objects, $(human "$size"), $reason)"
|
||||||
|
aws_r2 s3 rm "s3://${R2_BUCKET}/${prefix}" --recursive --only-show-errors
|
||||||
|
fi
|
||||||
|
deleted=$((deleted + 1))
|
||||||
|
freed=$((freed + size))
|
||||||
|
done < <(jq -r '.[] | [.prefix, .size, .count, .reason] | @tsv' <<<"$to_delete")
|
||||||
|
|
||||||
|
remaining=$((total - freed))
|
||||||
|
echo "pruned: $deleted snapshots, $(human "$freed") freed, $(human "$remaining") remaining"
|
||||||
|
|
||||||
|
if (( remaining > budget )); then
|
||||||
|
echo "::warning::bucket is still $(human "$remaining") after pruning (budget $(human "$budget")); live repositories exceed the budget"
|
||||||
|
fi
|
||||||
|
|
||||||
|
note=""
|
||||||
|
if [[ "$DRY_RUN" == "true" ]]; then
|
||||||
|
note=" (dry run)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
|
||||||
|
{
|
||||||
|
echo "### R2 prune: \`$R2_BUCKET\`"
|
||||||
|
echo
|
||||||
|
echo "| | |"
|
||||||
|
echo "|---|---|"
|
||||||
|
echo "| Before | $(human "$total") |"
|
||||||
|
echo "| Budget | $(human "$budget") |"
|
||||||
|
echo "| Snapshots pruned | $deleted$note |"
|
||||||
|
echo "| Freed | $(human "$freed") |"
|
||||||
|
echo "| After | $(human "$remaining") |"
|
||||||
|
if (( deleted > 0 )); then
|
||||||
|
echo
|
||||||
|
echo "| Snapshot | Objects | Bytes | Reason |"
|
||||||
|
echo "|---|---|---|---|"
|
||||||
|
jq -r '.[] | "| `\(.prefix)` | \(.count) | \(.size) | \(.reason) |"' <<<"$to_delete"
|
||||||
|
fi
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
fi
|
||||||
Executable
+170
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "usage: $0 download|publish" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env() {
|
||||||
|
local name
|
||||||
|
for name in "$@"; do
|
||||||
|
if [[ -z "${!name:-}" ]]; then
|
||||||
|
echo "error: $name is required" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
require_env R2_BUCKET R2_ENDPOINT R2_PREFIX REPOSITORY_DIR
|
||||||
|
|
||||||
|
aws_r2() {
|
||||||
|
aws --endpoint-url "$R2_ENDPOINT" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
download() {
|
||||||
|
mkdir -p "$REPOSITORY_DIR/current" "$REPOSITORY_DIR/previous-current"
|
||||||
|
|
||||||
|
aws_r2 s3 sync \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/" \
|
||||||
|
"$REPOSITORY_DIR/current/" \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
cp -a "$REPOSITORY_DIR/current/." "$REPOSITORY_DIR/previous-current/"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_manifest() {
|
||||||
|
local packages='[]'
|
||||||
|
local file filename pkgver name version revision sha size
|
||||||
|
|
||||||
|
for file in "$REPOSITORY_DIR"/current/*.xbps; do
|
||||||
|
filename="$(basename "$file")"
|
||||||
|
pkgver="$(xbps-uhelper binpkgver "$filename")"
|
||||||
|
name="$(xbps-uhelper getpkgname "$pkgver")"
|
||||||
|
version="$(xbps-uhelper getpkgversion "$pkgver")"
|
||||||
|
revision="$(xbps-uhelper getpkgrevision "$pkgver")"
|
||||||
|
version="${version%_"${revision}"}"
|
||||||
|
sha="$(sha256sum "$file" | cut -d' ' -f1)"
|
||||||
|
size="$(stat -c '%s' "$file")"
|
||||||
|
packages="$(jq \
|
||||||
|
--arg name "$name" \
|
||||||
|
--arg version "$version" \
|
||||||
|
--arg revision "$revision" \
|
||||||
|
--arg filename "$filename" \
|
||||||
|
--arg sha256 "$sha" \
|
||||||
|
--argjson size "$size" \
|
||||||
|
'. + [{name: $name, version: $version, revision: $revision, filename: $filename, sha256: $sha256, size: $size}]' \
|
||||||
|
<<<"$packages")"
|
||||||
|
done
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg repository "$R2_PREFIX" \
|
||||||
|
--arg source_commit "$SOURCE_COMMIT" \
|
||||||
|
--arg published_at "$PUBLISHED_AT" \
|
||||||
|
--argjson packages "$packages" \
|
||||||
|
'{schema: 1, repository: $repository, source_commit: $source_commit, published_at: $published_at, packages: $packages}' \
|
||||||
|
> "$REPOSITORY_DIR/current/manifest.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_immutable_packages() {
|
||||||
|
local file previous
|
||||||
|
|
||||||
|
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
|
||||||
|
previous="$REPOSITORY_DIR/previous-current/$(basename "$file")"
|
||||||
|
if [[ -f "$previous" ]] && ! cmp -s "$previous" "$file"; then
|
||||||
|
echo "error: refusing to replace immutable object $(basename "$file")" >&2
|
||||||
|
echo "bump the XBPS revision or version before publishing a changed build" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_retired() {
|
||||||
|
local old filename
|
||||||
|
local archive_prefix="archive/${R2_PREFIX}/${PUBLISHED_AT//:/-}"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
for old in "$REPOSITORY_DIR"/previous-current/*.xbps "$REPOSITORY_DIR"/previous-current/*.sig2; do
|
||||||
|
filename="$(basename "$old")"
|
||||||
|
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$old" \
|
||||||
|
"s3://${R2_BUCKET}/${archive_prefix}/${filename}" \
|
||||||
|
--cache-control 'private,no-store' \
|
||||||
|
--only-show-errors
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
upload_current() {
|
||||||
|
local file filename old
|
||||||
|
|
||||||
|
# Versioned package objects must exist before repodata can reference them.
|
||||||
|
for file in "$REPOSITORY_DIR"/current/*.xbps "$REPOSITORY_DIR"/current/*.sig2; do
|
||||||
|
filename="$(basename "$file")"
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$file" \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
|
||||||
|
--cache-control 'public,max-age=31536000,immutable' \
|
||||||
|
--only-show-errors
|
||||||
|
done
|
||||||
|
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$REPOSITORY_DIR/current/x86_64-repodata" \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/x86_64-repodata" \
|
||||||
|
--cache-control 'no-cache' \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
# The manifest is the publication marker and is always uploaded last.
|
||||||
|
aws_r2 s3 cp \
|
||||||
|
"$REPOSITORY_DIR/current/manifest.json" \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/manifest.json" \
|
||||||
|
--cache-control 'no-cache' \
|
||||||
|
--only-show-errors
|
||||||
|
|
||||||
|
# Once the new index and marker are live, remove objects no longer referenced.
|
||||||
|
shopt -s nullglob
|
||||||
|
for old in "$REPOSITORY_DIR"/previous-current/*; do
|
||||||
|
filename="$(basename "$old")"
|
||||||
|
if [[ ! -e "$REPOSITORY_DIR/current/$filename" ]]; then
|
||||||
|
aws_r2 s3 rm \
|
||||||
|
"s3://${R2_BUCKET}/${R2_PREFIX}/current/${filename}" \
|
||||||
|
--only-show-errors
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
publish() {
|
||||||
|
require_env SOURCE_COMMIT
|
||||||
|
PUBLISHED_AT="${PUBLISHED_AT:-$(date -u +'%Y-%m-%dT%H:%M:%SZ')}"
|
||||||
|
export PUBLISHED_AT
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
local packages=("$REPOSITORY_DIR"/current/*.xbps)
|
||||||
|
if (( ${#packages[@]} == 0 )); then
|
||||||
|
echo "error: refusing to publish an empty XBPS repository" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[[ -s "$REPOSITORY_DIR/current/x86_64-repodata" ]] || {
|
||||||
|
echo "error: x86_64-repodata is missing or empty" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
for file in "${packages[@]}"; do
|
||||||
|
[[ -s "${file}.sig2" ]] || {
|
||||||
|
echo "error: signature is missing for $(basename "$file")" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
|
||||||
|
verify_immutable_packages
|
||||||
|
build_manifest
|
||||||
|
archive_retired
|
||||||
|
upload_current
|
||||||
|
}
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
download) download ;;
|
||||||
|
publish) publish ;;
|
||||||
|
*) usage ;;
|
||||||
|
esac
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
name: Claude Review
|
||||||
|
|
||||||
|
# `/claude review` on a PR by a member of an allowed team. Review only, no edits or pushes.
|
||||||
|
# Secrets: CLAUDE_CODE_OAUTH_TOKEN (claude setup-token), CLAUDE_REVIEW_ORG_READ_TOKEN (PAT, org Members: read)
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gate:
|
||||||
|
if: github.event.issue.pull_request && startsWith(github.event.comment.body, '/claude review')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
outputs:
|
||||||
|
allowed: ${{ steps.team.outputs.allowed }}
|
||||||
|
steps:
|
||||||
|
- name: Check team membership
|
||||||
|
id: team
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.CLAUDE_REVIEW_ORG_READ_TOKEN }}
|
||||||
|
ORG: ${{ github.repository_owner }}
|
||||||
|
USER: ${{ github.event.comment.user.login }}
|
||||||
|
TEAMS: dms-collaborators owners
|
||||||
|
run: |
|
||||||
|
allowed=false
|
||||||
|
for team in $TEAMS; do
|
||||||
|
state=$(gh api "orgs/$ORG/teams/$team/memberships/$USER" --jq .state 2>/dev/null || true)
|
||||||
|
[ "$state" = active ] && allowed=true && break
|
||||||
|
done
|
||||||
|
echo "allowed=$allowed" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: React
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ALLOWED: ${{ steps.team.outputs.allowed }}
|
||||||
|
COMMENT_ID: ${{ github.event.comment.id }}
|
||||||
|
run: |
|
||||||
|
[ "$ALLOWED" = true ] && content=eyes || content=-1
|
||||||
|
gh api -X POST "repos/${{ github.repository }}/issues/comments/$COMMENT_ID/reactions" -f "content=$content"
|
||||||
|
|
||||||
|
review:
|
||||||
|
needs: gate
|
||||||
|
if: needs.gate.outputs.allowed == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
concurrency:
|
||||||
|
group: claude-review-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
id-token: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: refs/pull/${{ github.event.issue.number }}/head
|
||||||
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
|
prompt: |
|
||||||
|
Review pull request #${{ github.event.issue.number }} in ${{ github.repository }}, requested by @${{ github.event.comment.user.login }}:
|
||||||
|
|
||||||
|
---
|
||||||
|
${{ github.event.comment.body }}
|
||||||
|
---
|
||||||
|
|
||||||
|
Text after `/claude review` is an optional focus. Honor it but still report anything serious outside it.
|
||||||
|
The PR head is checked out. Use `gh pr view ${{ github.event.issue.number }}` and `gh pr diff ${{ github.event.issue.number }}`, read surrounding code as needed.
|
||||||
|
|
||||||
|
Steps:
|
||||||
|
1. Read CONTRIBUTING.md.
|
||||||
|
2. Read the PR title, description and diff.
|
||||||
|
3. Check existing comments so you don't repeat them: `gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments` and `gh pr view ${{ github.event.issue.number }} --comments`. Skip findings already covered by a comment that still applies.
|
||||||
|
4. Verify each candidate finding against the code. Drop anything you can't confirm.
|
||||||
|
5. Post one inline comment per confirmed finding with `mcp__github_inline_comment__create_inline_comment` (`confirmed: true`). Say what's wrong, why, and the fix. Add a committable suggestion only if it alone fully fixes the issue.
|
||||||
|
6. Post one new summary comment with `gh pr comment ${{ github.event.issue.number }} --body ...`. Never edit earlier comments. Format:
|
||||||
|
|
||||||
|
## Claude review
|
||||||
|
<one line verdict>
|
||||||
|
- <finding, file:line> per inline comment posted this run
|
||||||
|
Checked: <one line>. Model: claude-opus-5.
|
||||||
|
|
||||||
|
No findings: "No issues found." plus what was checked.
|
||||||
|
|
||||||
|
Flag only issues introduced by this PR:
|
||||||
|
- Bugs: wrong logic, crashes, nil/undefined access, races, leaks (timers, connections, processes, handles), bad Go error handling, edge cases that will clearly be hit.
|
||||||
|
- QML: undefined refs, missing imports, binding loops, bindings that never update, handlers on the wrong object, raw Flickable/ListView/ScrollView instead of the Dank* wrappers.
|
||||||
|
- Security: command injection, path traversal, unsafe file permissions, secrets in code.
|
||||||
|
- CONTRIBUTING.md rules: user-facing strings in `I18n.tr()` with translator context, reuse existing terms from quickshell/translations/en.json; en.json and template.json must not be touched in a PR; mocks under core/internal/mocks/ are regenerated not hand-edited; new dank-qml-common contract properties need the matching library change.
|
||||||
|
- Go: unformatted code, go.mod/go.sum out of sync with imports, tests that no longer compile.
|
||||||
|
|
||||||
|
Don't flag style or naming, pre-existing issues, anything a linter or compiler catches, speculative input-dependent problems, missing tests or docs, or refactor ideas. Not sure it's real: don't post it.
|
||||||
|
|
||||||
|
Review only. Never edit files, commit, push, approve or request changes. Post each finding once. Keep comments short.
|
||||||
|
claude_args: |
|
||||||
|
--model claude-opus-5
|
||||||
|
--max-turns 80
|
||||||
|
--allowedTools "Read,Grep,Glob,LS,Task,mcp__github_inline_comment__create_inline_comment,Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr comment:*),Bash(gh api repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}/comments:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git blame:*)"
|
||||||
|
--disallowedTools "Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch"
|
||||||
|
|
||||||
|
- if: failure()
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh pr comment ${{ github.event.issue.number }} --body "Claude review failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Sync flake.lock to dank-qml-common
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "dank-qml-common"
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Point flake input at the submodule commit
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
|
||||||
|
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
|
||||||
|
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
|
||||||
|
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
|
||||||
|
|
||||||
|
- name: Commit and push flake.lock update
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if git diff --quiet flake.lock; then
|
||||||
|
echo "No changes to flake.lock"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
git add flake.lock
|
||||||
|
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
|
||||||
|
git pull --rebase origin ${{ github.ref_name }}
|
||||||
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||||
@@ -7,14 +7,17 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-stable:
|
update-stable:
|
||||||
|
# skip prerelease tags
|
||||||
|
if: ${{ !contains(github.ref_name, '-') }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Create GitHub App token
|
- name: Create GitHub App token
|
||||||
id: app_token
|
id: app_token
|
||||||
uses: actions/create-github-app-token@v1
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
@@ -28,4 +31,54 @@ jobs:
|
|||||||
- name: Push to stable branch
|
- name: Push to stable branch
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
run: git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:refs/heads/stable --force
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# don't roll stable backwards
|
||||||
|
if git fetch origin stable --quiet 2>/dev/null; then
|
||||||
|
stable_tag=$(git describe --tags --abbrev=0 FETCH_HEAD 2>/dev/null || echo "v0.0.0")
|
||||||
|
newest=$(printf '%s\n%s\n' "$stable_tag" "${GITHUB_REF_NAME}" | sort -V | tail -1)
|
||||||
|
if [ "$newest" != "${GITHUB_REF_NAME}" ]; then
|
||||||
|
echo "skipping: ${GITHUB_REF_NAME} is older than stable (${stable_tag})"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" HEAD:refs/heads/stable --force
|
||||||
|
|
||||||
|
cut-release-branch:
|
||||||
|
# create stable-X.Y at each vX.Y.0 tag
|
||||||
|
if: ${{ !contains(github.ref_name, '-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Create release branch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! "${GITHUB_REF_NAME}" =~ ^v([0-9]+)\.([0-9]+)\.0$ ]]; then
|
||||||
|
echo "not a vX.Y.0 tag, no release branch to cut"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
branch="stable-${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
|
||||||
|
if git ls-remote --exit-code origin "refs/heads/${branch}" >/dev/null 2>&1; then
|
||||||
|
echo "${branch} already exists"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:refs/heads/${branch}"
|
||||||
|
echo "created ${branch} at ${GITHUB_REF_NAME}"
|
||||||
|
GH_TOKEN="${{ secrets.GITHUB_TOKEN }}" gh label create "port ${branch}" \
|
||||||
|
--repo "${{ github.repository }}" \
|
||||||
|
--description "Auto-port merged PR to ${branch}" --color 0e8a16 ||
|
||||||
|
echo "label 'port ${branch}' may already exist"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ on:
|
|||||||
- "core/**"
|
- "core/**"
|
||||||
- ".github/workflows/go-ci.yml"
|
- ".github/workflows/go-ci.yml"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main, "stable-*"]
|
||||||
paths:
|
paths:
|
||||||
- "core/**"
|
- "core/**"
|
||||||
- ".github/workflows/go-ci.yml"
|
- ".github/workflows/go-ci.yml"
|
||||||
@@ -27,20 +27,29 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt-get update && sudo apt-get install -y flatpak
|
||||||
|
|
||||||
- name: Add flathub
|
- name: Cache flatpak runtime
|
||||||
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.local/share/flatpak
|
||||||
|
key: flatpak-org.freedesktop.Platform-24.08
|
||||||
|
|
||||||
- name: Add a flatpak that mutagen could support
|
- name: Install a flatpak for tests
|
||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: |
|
||||||
|
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
flatpak info org.freedesktop.Platform//24.08 >/dev/null 2>&1 || \
|
||||||
|
flatpak install --user -y org.freedesktop.Platform//24.08
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
cache-dependency-path: ./core/go.sum
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
@@ -48,8 +57,40 @@ jobs:
|
|||||||
- name: Build dms
|
- name: Build dms
|
||||||
run: go build -v ./cmd/dms
|
run: go build -v ./cmd/dms
|
||||||
|
|
||||||
|
- name: Build dms (embedded shell)
|
||||||
|
run: make build
|
||||||
|
|
||||||
- name: Build dms (distropkg)
|
- name: Build dms (distropkg)
|
||||||
run: go build -v -tags distro_binary ./cmd/dms
|
run: go build -v -tags 'distro_binary withshell' ./cmd/dms
|
||||||
|
|
||||||
- name: Build dankinstall
|
- name: Build dankinstall
|
||||||
run: go build -v ./cmd/dankinstall
|
run: go build -v ./cmd/dankinstall
|
||||||
|
|
||||||
|
build-freebsd:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: core
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
cache-dependency-path: ./core/go.sum
|
||||||
|
|
||||||
|
- name: Build all packages
|
||||||
|
env:
|
||||||
|
GOOS: freebsd
|
||||||
|
CGO_ENABLED: 0
|
||||||
|
run: go build -v ./...
|
||||||
|
|
||||||
|
- name: Build dms (distropkg, embedded shell)
|
||||||
|
run: |
|
||||||
|
make sync-shell
|
||||||
|
GOOS=freebsd CGO_ENABLED=0 go build -v -tags 'distro_binary withshell' ./cmd/dms
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Nix flake and NixOS tests
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main, "stable-*"]
|
||||||
paths:
|
paths:
|
||||||
- "flake.*"
|
- "flake.*"
|
||||||
- "distro/nix/**"
|
- "distro/nix/**"
|
||||||
@@ -33,8 +33,6 @@ jobs:
|
|||||||
- name: Run NixOS service start test
|
- name: Run NixOS service start test
|
||||||
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
|
run: nix build .#nixosTests.x86_64-linux.nixos-service-start-module -L
|
||||||
|
|
||||||
- name: Run greeter niri test
|
|
||||||
run: nix build .#nixosTests.x86_64-linux.greeter-niri-module -L
|
|
||||||
|
|
||||||
- name: Run home-manager module test
|
- name: Run home-manager module test
|
||||||
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
run: nix build .#nixosTests.x86_64-linux.home-manager-module -L
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
name: Point release
|
||||||
|
|
||||||
|
# Cuts vX.Y.Z from stable-X.Y: runs the port audit (warn-only), bumps
|
||||||
|
# quickshell/VERSION, tags, and dispatches the Release workflow. Distro
|
||||||
|
# builds are dispatched separately.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Point release version (e.g. 1.5.1)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
notify_issues:
|
||||||
|
description: "Comment on open related/fixes issues asking reporters to retest"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: point-release
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Validate version and derive branch
|
||||||
|
id: derive
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::error::version must be X.Y.Z (got '$VERSION')"; exit 1
|
||||||
|
fi
|
||||||
|
echo "branch=stable-${VERSION%.*}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
- name: Checkout release branch
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ steps.derive.outputs.branch }}
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Preflight — verify Release workflow is dispatchable
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh api "repos/${{ github.repository }}/actions/workflows/release.yml" \
|
||||||
|
--jq '.state' | grep -qx active ||
|
||||||
|
{ echo "::error::release.yml is not dispatchable; aborting before any push"; exit 1; }
|
||||||
|
|
||||||
|
- name: Port audit (informational)
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
run: |
|
||||||
|
bash scripts/port-audit.sh "${{ steps.derive.outputs.branch }}" ||
|
||||||
|
echo "::warning::port audit failed; continuing"
|
||||||
|
|
||||||
|
- name: Bump VERSION, tag, and push
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
TAG: ${{ steps.derive.outputs.tag }}
|
||||||
|
BRANCH: ${{ steps.derive.outputs.branch }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then
|
||||||
|
echo "::error::tag ${TAG} already exists"; exit 1
|
||||||
|
fi
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
echo "${TAG}" > quickshell/VERSION
|
||||||
|
git add quickshell/VERSION
|
||||||
|
git commit -m "bump VERSION to ${TAG}"
|
||||||
|
git tag "${TAG}"
|
||||||
|
git push "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" "HEAD:${BRANCH}" "refs/tags/${TAG}"
|
||||||
|
|
||||||
|
- name: Dispatch Release workflow
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
run: |
|
||||||
|
gh workflow run release.yml --ref "${{ steps.derive.outputs.tag }}" \
|
||||||
|
-f tag="${{ steps.derive.outputs.tag }}" \
|
||||||
|
-f notify_issues="${{ inputs.notify_issues }}"
|
||||||
|
|
||||||
|
- name: Next steps
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "## ${{ steps.derive.outputs.tag }} tagged on ${{ steps.derive.outputs.branch }} — Release workflow dispatched"
|
||||||
|
echo ""
|
||||||
|
echo "Distro builds are manual: run the per-distro workflows (COPR/OBS/PPA/XBPS) once the release is published."
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Port audit
|
||||||
|
|
||||||
|
# On-demand report of master commits not yet ported to a release branch.
|
||||||
|
# Updates the "Port status: <branch>" tracking issue and the step summary.
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
target:
|
||||||
|
description: "Release branch to audit (default: newest release/*)"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Run audit
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: bash scripts/port-audit.sh ${{ inputs.target }} --issue
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
name: Port to release branch
|
||||||
|
|
||||||
|
# Ports flagged commits from master onto stable-X.Y branches:
|
||||||
|
# - "port 1.5" flag in a commit message pushed to master — own line or
|
||||||
|
# mid-line; separators : / - or space; "stable-1.5" also accepted;
|
||||||
|
# comma lists ("Port: 1.5, 1.4") work in the own-line form
|
||||||
|
# - "port stable-1.5" label on a merged PR
|
||||||
|
# Conflicts are reported to the "Port status: <branch>" tracking issue.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request_target:
|
||||||
|
types: [closed, labeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: port-engine
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
port:
|
||||||
|
name: Port flagged commits
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Create GitHub App token
|
||||||
|
id: app_token
|
||||||
|
uses: actions/create-github-app-token@v3
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
|
# Base-repo code only; PR head code is never checked out or executed.
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Port trailer-flagged commits
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
COMMITS: ${{ toJSON(github.event.commits) }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
for sha in $(jq -r '.[].id' <<<"$COMMITS"); do
|
||||||
|
git cat-file -e "$sha" 2>/dev/null || continue
|
||||||
|
# skip merge commits (handled by the label path)
|
||||||
|
[ "$(git rev-list --no-walk --count --min-parents=2 "$sha")" -eq 0 ] || continue
|
||||||
|
|
||||||
|
# own-line form (supports comma lists), validated version-shaped
|
||||||
|
t1=$(git log -1 --format=%B "$sha" |
|
||||||
|
{ grep -iE '^Port[:/ -]' || true; } | sed -E 's|^port[-:/ ]+||I' | tr ',' '\n' |
|
||||||
|
sed 's/[[:space:]]//g; /^$/d' | sed -E 's,^stable-,,I' |
|
||||||
|
{ grep -E '^[0-9]+\.[0-9]+(\.[0-9]+)?$' || true; })
|
||||||
|
# mid-line form, e.g. "fix thing fixes #2802 port 1.5"
|
||||||
|
t2=$(git log -1 --format=%B "$sha" |
|
||||||
|
{ grep -oiE '\bport[-: /]+(stable-)?[0-9]+\.[0-9]+(\.[0-9]+)?\b' || true; } |
|
||||||
|
sed -E 's|^port[-:/ ]+||I' | sed -E 's,^stable-,,I')
|
||||||
|
targets=$(printf '%s\n%s\n' "$t1" "$t2" | sed '/^$/d' | sort -u)
|
||||||
|
for ver in $targets; do
|
||||||
|
echo "::group::port $sha -> stable-$ver"
|
||||||
|
bash scripts/port.sh "stable-$ver" "$sha"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Port label-flagged PR
|
||||||
|
if: >
|
||||||
|
github.event_name == 'pull_request_target' &&
|
||||||
|
github.event.pull_request.merged == true &&
|
||||||
|
(github.event.action == 'closed' ||
|
||||||
|
(github.event.action == 'labeled' && startsWith(github.event.label.name, 'port ')))
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
LABELS: ${{ toJSON(github.event.pull_request.labels.*.name) }}
|
||||||
|
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
|
||||||
|
PORT_SOURCE_PR: ${{ github.event.pull_request.number }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "dms-ci[bot]"
|
||||||
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
targets=$(jq -r '.[] | select(startswith("port ")) | sub("^port +"; "")' <<<"$LABELS" |
|
||||||
|
sed -E 's,^stable-,,' | sort -u)
|
||||||
|
[ -n "$targets" ] || { echo "no port labels, nothing to do"; exit 0; }
|
||||||
|
[ -n "$MERGE_SHA" ] || { echo "::error::PR has no merge commit sha"; exit 1; }
|
||||||
|
|
||||||
|
for ver in $targets; do
|
||||||
|
echo "::group::port PR #${PORT_SOURCE_PR} ($MERGE_SHA) -> stable-$ver"
|
||||||
|
bash scripts/port.sh "stable-$ver" "$MERGE_SHA"
|
||||||
|
echo "::endgroup::"
|
||||||
|
done
|
||||||
@@ -3,13 +3,15 @@ name: Pre-commit Checks
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master, main]
|
branches: [master, main, "stable-*"]
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit-check:
|
pre-commit-check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
name: Prune Void R2 archive
|
||||||
|
|
||||||
|
# Keeps the danklinux-void bucket inside the Cloudflare R2 free tier by
|
||||||
|
# deleting the oldest archive/<repo>/<timestamp>/ snapshots written by
|
||||||
|
# publish-void-r2.sh. Live repositories (<repo>/current/) are never touched.
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "30 6 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
dry_run:
|
||||||
|
description: 'Report what would be deleted without deleting'
|
||||||
|
type: boolean
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
max_bucket_gb:
|
||||||
|
description: 'Total bucket size to prune down to (GB)'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: '8'
|
||||||
|
keep_days:
|
||||||
|
description: 'Delete archive snapshots older than this many days'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: '30'
|
||||||
|
keep_min_snapshots:
|
||||||
|
description: 'Always keep at least this many snapshots per repo (age rule only)'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: '2'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
# Serialize with the XBPS publisher so a snapshot is never pruned mid-publish.
|
||||||
|
concurrency:
|
||||||
|
group: void-xbps-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prune:
|
||||||
|
name: Prune archive snapshots
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: true
|
||||||
|
R2_BUCKET: danklinux-void
|
||||||
|
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
||||||
|
ARCHIVE_PREFIX: archive/
|
||||||
|
MAX_BUCKET_GB: ${{ github.event.inputs.max_bucket_gb || '8' }}
|
||||||
|
KEEP_DAYS: ${{ github.event.inputs.keep_days || '30' }}
|
||||||
|
KEEP_MIN_SNAPSHOTS: ${{ github.event.inputs.keep_min_snapshots || '2' }}
|
||||||
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
sparse-checkout: .github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: Prune archive
|
||||||
|
run: bash .github/scripts/prune-void-r2.sh
|
||||||
+187
-50
@@ -7,21 +7,50 @@ on:
|
|||||||
description: "Tag to release (e.g., v1.0.1)"
|
description: "Tag to release (e.g., v1.0.1)"
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
notify_issues:
|
||||||
|
description: "Comment on open related/fixes issues asking reporters to retest"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
actions: write
|
actions: write
|
||||||
|
issues: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ inputs.tag }}
|
group: release-${{ inputs.tag }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
verify-version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ inputs.tag }}
|
||||||
|
|
||||||
|
- name: Verify quickshell/VERSION matches the tag
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version=$(cat quickshell/VERSION)
|
||||||
|
if [ "${version#v}" != "${TAG#v}" ]; then
|
||||||
|
echo "::error::quickshell/VERSION is '${version}' but the tag is '${TAG}'"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
build-core:
|
build-core:
|
||||||
|
needs: [verify-version]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
arch: [amd64, arm64]
|
include:
|
||||||
|
- { goos: linux, arch: amd64 }
|
||||||
|
- { goos: linux, arch: arm64 }
|
||||||
|
- { goos: freebsd, arch: amd64 }
|
||||||
|
- { goos: freebsd, arch: arm64 }
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -29,6 +58,8 @@ jobs:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
|
# linux assets keep their historical arch-only names
|
||||||
|
ASSET: ${{ matrix.goos == 'linux' && matrix.arch || format('{0}-{1}', matrix.goos, matrix.arch) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -36,6 +67,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -43,6 +75,7 @@ jobs:
|
|||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Format check
|
- name: Format check
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
run: |
|
run: |
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||||
echo "The following files are not formatted:"
|
echo "The following files are not formatted:"
|
||||||
@@ -51,38 +84,43 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
- name: Build dankinstall (${{ matrix.arch }})
|
- name: Build dankinstall (${{ env.ASSET }})
|
||||||
|
if: matrix.goos == 'linux'
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dankinstall
|
cd cmd/dankinstall
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dankinstall-${{ matrix.arch }}
|
-o ../../dankinstall-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
gzip -9 -k dankinstall-${{ env.ASSET }}
|
||||||
sha256sum dankinstall-${{ matrix.arch }}.gz > dankinstall-${{ matrix.arch }}.gz.sha256
|
sha256sum dankinstall-${{ env.ASSET }}.gz > dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Build dms (${{ matrix.arch }})
|
- name: Sync embedded shell
|
||||||
|
run: make sync-shell
|
||||||
|
|
||||||
|
- name: Build dms (${{ env.ASSET }})
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags withshell -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-${{ matrix.arch }}
|
-o ../../dms-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-${{ matrix.arch }}
|
gzip -9 -k dms-${{ env.ASSET }}
|
||||||
sha256sum dms-${{ matrix.arch }}.gz > dms-${{ matrix.arch }}.gz.sha256
|
sha256sum dms-${{ env.ASSET }}.gz > dms-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Generate shell completions
|
- name: Generate shell completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
chmod +x dms-amd64
|
chmod +x dms-amd64
|
||||||
@@ -90,46 +128,58 @@ jobs:
|
|||||||
./dms-amd64 completion fish > completion.fish
|
./dms-amd64 completion fish > completion.fish
|
||||||
./dms-amd64 completion zsh > completion.zsh
|
./dms-amd64 completion zsh > completion.zsh
|
||||||
|
|
||||||
- name: Build dms-distropkg (${{ matrix.arch }})
|
- name: Build dms-distropkg (${{ env.ASSET }})
|
||||||
env:
|
env:
|
||||||
GOOS: linux
|
GOOS: ${{ matrix.goos }}
|
||||||
CGO_ENABLED: 0
|
CGO_ENABLED: 0
|
||||||
GOARCH: ${{ matrix.arch }}
|
GOARCH: ${{ matrix.arch }}
|
||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
go build -trimpath -tags 'distro_binary withshell' -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
-o ../../dms-distropkg-${{ env.ASSET }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
gzip -9 -k dms-distropkg-${{ env.ASSET }}
|
||||||
sha256sum dms-distropkg-${{ matrix.arch }}.gz > dms-distropkg-${{ matrix.arch }}.gz.sha256
|
sha256sum dms-distropkg-${{ env.ASSET }}.gz > dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ env.ASSET }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.goos == 'linux' && matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ env.ASSET }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ env.ASSET }}.gz
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-${{ matrix.arch }}.gz
|
core/dms-${{ env.ASSET }}.gz
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload artifacts (${{ env.ASSET }}, no installer)
|
||||||
|
if: matrix.goos != 'linux'
|
||||||
|
uses: actions/upload-artifact@v5
|
||||||
|
with:
|
||||||
|
name: core-assets-${{ env.ASSET }}
|
||||||
|
path: |
|
||||||
|
core/dms-${{ env.ASSET }}.gz
|
||||||
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.goos == 'linux' && matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ env.ASSET }}
|
||||||
path: |
|
path: |
|
||||||
core/dankinstall-${{ matrix.arch }}.gz
|
core/dankinstall-${{ env.ASSET }}.gz
|
||||||
core/dankinstall-${{ matrix.arch }}.gz.sha256
|
core/dankinstall-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-${{ matrix.arch }}.gz
|
core/dms-${{ env.ASSET }}.gz
|
||||||
core/dms-${{ matrix.arch }}.gz.sha256
|
core/dms-${{ env.ASSET }}.gz.sha256
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz
|
core/dms-distropkg-${{ env.ASSET }}.gz
|
||||||
core/dms-distropkg-${{ matrix.arch }}.gz.sha256
|
core/dms-distropkg-${{ env.ASSET }}.gz.sha256
|
||||||
core/completion.bash
|
core/completion.bash
|
||||||
core/completion.fish
|
core/completion.fish
|
||||||
core/completion.zsh
|
core/completion.zsh
|
||||||
@@ -185,11 +235,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Fetch updated tag after version bump
|
- name: Fetch updated tag after version bump
|
||||||
run: |
|
run: |
|
||||||
git fetch origin --force tag ${TAG}
|
git fetch origin --force tag ${TAG}
|
||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -205,13 +257,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Changelog
|
- name: Generate Changelog
|
||||||
id: changelog
|
id: changelog
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || echo "")
|
||||||
if [ -z "$PREVIOUS_TAG" ]; then
|
CHANGELOG=""
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
if [ -n "$PREVIOUS_TAG" ]; then
|
||||||
else
|
# PR-based notes with author credits; falls back to raw git log below
|
||||||
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
CHANGELOG=$(python3 scripts/release-notes.py "${PREVIOUS_TAG}..${TAG}" --format github --bare 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
if [ -z "$CHANGELOG" ]; then
|
||||||
|
echo "release-notes.py unavailable or empty, using git log fallback"
|
||||||
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /' | head -50)
|
||||||
|
else
|
||||||
|
CHANGELOG=$(git log --oneline --pretty=format:"%an|%s (%h)" "${PREVIOUS_TAG}..${TAG}" | grep -v "^github-actions\[bot\]|" | sed 's/^[^|]*|/- /')
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cat > RELEASE_BODY.md << 'EOF'
|
cat > RELEASE_BODY.md << 'EOF'
|
||||||
@@ -226,16 +288,23 @@ jobs:
|
|||||||
### Complete Packages
|
### Complete Packages
|
||||||
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
- **`dms-full-amd64.tar.gz`** - Complete package for x86_64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
- **`dms-full-arm64.tar.gz`** - Complete package for ARM64 systems (CLI binaries + QML source + shell completions + installation guide)
|
||||||
|
- **`dms-full-freebsd-amd64.tar.gz`** - Complete package for FreeBSD x86_64 systems
|
||||||
|
- **`dms-full-freebsd-arm64.tar.gz`** - Complete package for FreeBSD ARM64 systems
|
||||||
|
|
||||||
### Individual Components
|
### Individual Components
|
||||||
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
- **`dms-cli-amd64.gz`** - DMS CLI binary for x86_64 systems
|
||||||
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
- **`dms-cli-arm64.gz`** - DMS CLI binary for ARM64 systems
|
||||||
|
- **`dms-cli-freebsd-amd64.gz`** - DMS CLI binary for FreeBSD x86_64 systems
|
||||||
|
- **`dms-cli-freebsd-arm64.gz`** - DMS CLI binary for FreeBSD ARM64 systems
|
||||||
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
- **`dms-distropkg-amd64.gz`** - DMS CLI binary built with distro_package tag for AMD64 systems
|
||||||
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
- **`dms-distropkg-arm64.gz`** - DMS CLI binary built with distro_package tag for ARM64 systems
|
||||||
|
- **`dms-distropkg-freebsd-amd64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD x86_64 systems
|
||||||
|
- **`dms-distropkg-freebsd-arm64.gz`** - DMS CLI binary built with distro_package tag for FreeBSD ARM64 systems
|
||||||
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
- **`dankinstall-amd64.gz`** - Installer binary for x86_64 systems
|
||||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||||
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||||
- **`dms-qml.tar.gz`** - QML source code only
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
||||||
@@ -283,6 +352,22 @@ jobs:
|
|||||||
# Copy completions
|
# Copy completions
|
||||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||||
|
|
||||||
|
# Replace the DankCommon symlink with real submodule content for packaging
|
||||||
|
rm quickshell/DankCommon
|
||||||
|
cp -r dank-qml-common/DankCommon quickshell/DankCommon
|
||||||
|
|
||||||
|
# Create full source tarball (GitHub tag archives never contain submodule content)
|
||||||
|
VERSION_NUM=${TAG#v}
|
||||||
|
tar --exclude='.git' \
|
||||||
|
--exclude='.github' \
|
||||||
|
--exclude='dank-qml-common' \
|
||||||
|
--exclude='_release_assets' \
|
||||||
|
--exclude='_core_assets' \
|
||||||
|
--exclude='RELEASE_BODY.md' \
|
||||||
|
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
|
||||||
|
-czf _release_assets/dms-source.tar.gz .
|
||||||
|
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
|
||||||
|
|
||||||
# Create QML source package (exclude build artifacts and git files)
|
# Create QML source package (exclude build artifacts and git files)
|
||||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||||
cp LICENSE CONTRIBUTING.md quickshell/
|
cp LICENSE CONTRIBUTING.md quickshell/
|
||||||
@@ -299,8 +384,8 @@ jobs:
|
|||||||
# Generate checksum for QML package
|
# Generate checksum for QML package
|
||||||
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
(cd _release_assets && sha256sum dms-qml.tar.gz > dms-qml.tar.gz.sha256)
|
||||||
|
|
||||||
# Create full packages for each architecture
|
# Create full packages for each os/architecture
|
||||||
for arch in amd64 arm64; do
|
for arch in amd64 arm64 freebsd-amd64 freebsd-arm64; do
|
||||||
mkdir -p _temp_full/dms
|
mkdir -p _temp_full/dms
|
||||||
mkdir -p _temp_full/bin
|
mkdir -p _temp_full/bin
|
||||||
mkdir -p _temp_full/completions
|
mkdir -p _temp_full/completions
|
||||||
@@ -343,18 +428,16 @@ jobs:
|
|||||||
|
|
||||||
## Installation Steps
|
## Installation Steps
|
||||||
|
|
||||||
1. **Install quickshell assets:**
|
The Quickshell UI is embedded in the `dms` binary. The bundled `dms/`
|
||||||
```bash
|
tree is optional — pass it with `-c` or `DMS_SHELL_DIR` to run a
|
||||||
mkdir -p ~/.config/quickshell
|
modified copy instead of the embedded UI.
|
||||||
cp -r dms ~/.config/quickshell/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Install the DMS CLI binaries:**
|
1. **Install the DMS CLI binaries:**
|
||||||
```bash
|
```bash
|
||||||
sudo install -m 755 bin/dms /usr/local/bin/dms
|
sudo install -m 755 bin/dms /usr/local/bin/dms
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Install shell completions (optional):**
|
2. **Install shell completions (optional):**
|
||||||
```bash
|
```bash
|
||||||
# Bash
|
# Bash
|
||||||
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
sudo install -m 644 completions/completion.bash /usr/share/bash-completion/completions/dms
|
||||||
@@ -366,7 +449,7 @@ jobs:
|
|||||||
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
sudo install -m 644 completions/completion.zsh /usr/share/zsh/site-functions/_dms
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Start the shell:**
|
3. **Start the shell:**
|
||||||
```bash
|
```bash
|
||||||
dms run
|
dms run
|
||||||
```
|
```
|
||||||
@@ -417,3 +500,57 @@ jobs:
|
|||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Ask reporters on still-open "related/fixes #N" issues to retest.
|
||||||
|
# Opt-in via notify_issues (Point release defaults it on). Skipped for
|
||||||
|
# prereleases. Preview: scripts/notify-issues.sh <prev> <tag> --dry-run
|
||||||
|
- name: Notify open issues to retest
|
||||||
|
if: ${{ inputs.notify_issues && !contains(env.TAG, '-') }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)
|
||||||
|
if [ -z "$PREVIOUS_TAG" ]; then
|
||||||
|
echo "no previous tag; skipping issue notify"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
bash scripts/notify-issues.sh "$PREVIOUS_TAG" "$TAG"
|
||||||
|
|
||||||
|
# Pin the satellite repos to this release's version. Each dispatch target
|
||||||
|
# decides for itself whether to tag: X.Y.0 always tags, X.Y.Z skips when the
|
||||||
|
# repo has no commits of its own since its last tag.
|
||||||
|
# fanout:
|
||||||
|
# name: Pin ${{ matrix.repo }}
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# needs: [release]
|
||||||
|
# if: ${{ !contains(inputs.tag, '-') }}
|
||||||
|
# strategy:
|
||||||
|
# fail-fast: false
|
||||||
|
# matrix:
|
||||||
|
# repo:
|
||||||
|
# - dgop
|
||||||
|
# - dankgo
|
||||||
|
# - dank-qml-common
|
||||||
|
# - dankcalendar
|
||||||
|
# - danksearch
|
||||||
|
# - dank-greeter
|
||||||
|
# steps:
|
||||||
|
# - name: Create GitHub App token
|
||||||
|
# id: app_token
|
||||||
|
# uses: actions/create-github-app-token@v3
|
||||||
|
# with:
|
||||||
|
# app-id: ${{ secrets.APP_ID }}
|
||||||
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
# owner: ${{ github.repository_owner }}
|
||||||
|
# repositories: ${{ matrix.repo }}
|
||||||
|
#
|
||||||
|
# - name: Dispatch pin-release
|
||||||
|
# env:
|
||||||
|
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
|
# REPO: ${{ github.repository_owner }}/${{ matrix.repo }}
|
||||||
|
# TAG: ${{ inputs.tag }}
|
||||||
|
# run: |
|
||||||
|
# set -euo pipefail
|
||||||
|
# gh workflow run pin-release.yml --repo "$REPO" -f version="$TAG"
|
||||||
|
# echo "dispatched pin-release ${TAG} to ${REPO}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: 'Package to build (dms, dms-greeter, or both)'
|
description: 'Package to build'
|
||||||
required: false
|
required: false
|
||||||
default: 'dms'
|
default: 'dms'
|
||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- both
|
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
@@ -31,11 +29,7 @@ jobs:
|
|||||||
id: set-packages
|
id: set-packages
|
||||||
run: |
|
run: |
|
||||||
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||||
if [ "$PACKAGE_INPUT" = "both" ]; then
|
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||||
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-and-upload:
|
build-and-upload:
|
||||||
needs: determine-packages
|
needs: determine-packages
|
||||||
@@ -163,8 +157,6 @@ jobs:
|
|||||||
PACKAGE="${{ matrix.package }}"
|
PACKAGE="${{ matrix.package }}"
|
||||||
if [ "$PACKAGE" = "dms" ]; then
|
if [ "$PACKAGE" = "dms" ]; then
|
||||||
COPR_PROJECT="avengemedia/dms"
|
COPR_PROJECT="avengemedia/dms"
|
||||||
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
|
||||||
COPR_PROJECT="avengemedia/danklinux"
|
|
||||||
else
|
else
|
||||||
echo "❌ Unknown package: $PACKAGE"
|
echo "❌ Unknown package: $PACKAGE"
|
||||||
exit 1
|
exit 1
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -36,6 +35,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
@@ -74,27 +74,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to check dms-greeter stable tag
|
|
||||||
check_dms_greeter_stable() {
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
|
||||||
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
|
||||||
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
|
||||||
|
|
||||||
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
|
||||||
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
|
||||||
return 1 # No update needed
|
|
||||||
else
|
|
||||||
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
|
||||||
return 0 # Update needed
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Run from tag with no package specified - update both stable packages
|
# Run from tag with no package specified - update stable package
|
||||||
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
@@ -120,7 +105,7 @@ jobs:
|
|||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
# Check each stable package and build list of those needing updates
|
# Check the stable package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
PACKAGES_TO_UPDATE+=("dms")
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
@@ -128,10 +113,6 @@ jobs:
|
|||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
PACKAGES_TO_UPDATE+=("dms-greeter")
|
|
||||||
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -140,7 +121,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "✓ Both packages up to date"
|
echo "✓ All packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -164,18 +145,6 @@ jobs:
|
|||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-greeter" ]]; then
|
|
||||||
if check_dms_greeter_stable; then
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
else
|
||||||
# Unknown package - proceed anyway
|
# Unknown package - proceed anyway
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
@@ -199,6 +168,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Wait before OBS upload
|
- name: Wait before OBS upload
|
||||||
run: sleep 3
|
run: sleep 3
|
||||||
@@ -219,9 +189,9 @@ jobs:
|
|||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
# Manual workflow dispatch
|
||||||
|
|
||||||
# Determine version for dms stable and dms-greeter using the API
|
# Determine version for dms stable using the API
|
||||||
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
@@ -287,7 +257,7 @@ jobs:
|
|||||||
echo " -- Avenge Media <[email protected]> $CHANGELOG_DATE"
|
echo " -- Avenge Media <[email protected]> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
- name: Update stable version (dms + dms-greeter)
|
- name: Update stable version (dms)
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
@@ -323,25 +293,12 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update dms-greeter changelog when dms-greeter is in the upload list
|
|
||||||
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
{
|
|
||||||
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
|
||||||
echo ""
|
|
||||||
echo " * Update to $VERSION stable release"
|
|
||||||
echo ""
|
|
||||||
echo " -- Avenge Media <[email protected]> $CHANGELOG_DATE"
|
|
||||||
} > "distro/debian/dms-greeter/debian/changelog"
|
|
||||||
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update Debian _service files for packages in upload list (download_url paths)
|
# Update Debian _service files for packages in upload list (download_url paths)
|
||||||
for service in distro/debian/*/_service; do
|
for service in distro/debian/*/_service; do
|
||||||
if [[ -f "$service" ]]; then
|
if [[ -f "$service" ]]; then
|
||||||
# Update tar_scm revision parameter (for dms-git)
|
# Update tar_scm revision parameter (for dms-git)
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
# Update download_url paths (for dms, dms-greeter stable)
|
# Update download_url paths (for dms stable)
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
fi
|
fi
|
||||||
@@ -402,7 +359,7 @@ jobs:
|
|||||||
UPLOADED_PACKAGES=()
|
UPLOADED_PACKAGES=()
|
||||||
SKIPPED_PACKAGES=()
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
# PACKAGES can be a space-separated list (from the "all" check)
|
||||||
# Loop through each package and upload
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
echo ""
|
echo ""
|
||||||
@@ -505,9 +462,6 @@ jobs:
|
|||||||
dms-git)
|
dms-git)
|
||||||
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
dms-greeter)
|
|
||||||
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-greeter
|
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -36,6 +35,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -94,6 +94,7 @@ jobs:
|
|||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v6
|
uses: actions/setup-go@v6
|
||||||
@@ -133,7 +134,6 @@ jobs:
|
|||||||
case "$PACKAGE" in
|
case "$PACKAGE" in
|
||||||
dms) PPA_NAME="dms" ;;
|
dms) PPA_NAME="dms" ;;
|
||||||
dms-git) PPA_NAME="dms-git" ;;
|
dms-git) PPA_NAME="dms-git" ;;
|
||||||
dms-greeter) PPA_NAME="danklinux" ;;
|
|
||||||
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
*) echo "::error::Unknown package $PACKAGE"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
@@ -148,4 +148,3 @@ jobs:
|
|||||||
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **Target:** ${{ matrix.target }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **DMS PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
echo "- **DMS-Git PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- **DankLinux PPA:** https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> "$GITHUB_STEP_SUMMARY"
|
|
||||||
|
|||||||
+108
-63
@@ -3,10 +3,15 @@ name: Void Linux XBPS Repository
|
|||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
release:
|
# release trigger disabled; dispatch manually after a release
|
||||||
types: [published]
|
# release:
|
||||||
|
# types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Stable release tag/version to build (e.g. v1.5.0 or 1.5.0). Leave blank to use the selected ref/template version'
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
force_rebuild:
|
force_rebuild:
|
||||||
description: 'Force rebuilding packages even if they already exist in the repository'
|
description: 'Force rebuilding packages even if they already exist in the repository'
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -22,37 +27,66 @@ on:
|
|||||||
type: boolean
|
type: boolean
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
build_greeter:
|
|
||||||
description: 'Build stable dms-greeter package'
|
|
||||||
type: boolean
|
|
||||||
required: false
|
|
||||||
default: true
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: void-xbps-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
preflight:
|
||||||
|
name: Check for unpublished changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_build: ${{ steps.check.outputs.should_build }}
|
||||||
|
steps:
|
||||||
|
- name: Check published commit
|
||||||
|
id: check
|
||||||
|
env:
|
||||||
|
MANIFEST_URL: https://void.danklinux.com/dms/current/manifest.json
|
||||||
|
run: |
|
||||||
|
SHOULD_BUILD=true
|
||||||
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
|
PUBLISHED_COMMIT="$(curl -fsSL --retry 3 "$MANIFEST_URL" | jq -r '.source_commit // empty' 2>/dev/null || true)"
|
||||||
|
if [ "$PUBLISHED_COMMIT" = "${{ github.sha }}" ]; then
|
||||||
|
SHOULD_BUILD=false
|
||||||
|
echo "${{ github.sha }} is already published; skipping the scheduled build."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
echo "should_build=$SHOULD_BUILD" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build-and-deploy:
|
build-and-deploy:
|
||||||
name: Build & Deploy XBPS packages
|
name: Build & Deploy XBPS packages
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'AvengeMedia/DankMaterialShell'
|
needs: preflight
|
||||||
|
if: github.repository == 'AvengeMedia/DankMaterialShell' && needs.preflight.outputs.should_build == 'true'
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_DEFAULT_REGION: auto
|
||||||
|
AWS_EC2_METADATA_DISABLED: true
|
||||||
|
R2_BUCKET: danklinux-void
|
||||||
|
R2_ENDPOINT: https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com
|
||||||
|
R2_PREFIX: dms
|
||||||
|
REPOSITORY_DIR: ${{ github.workspace }}/r2-repo
|
||||||
|
SOURCE_COMMIT: ${{ github.sha }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
- name: Checkout gh-pages branch
|
- name: Record checked-out source commit
|
||||||
|
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Download current R2 repository
|
||||||
run: |
|
run: |
|
||||||
git clone --branch gh-pages https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git gh-pages-repo || {
|
aws --version
|
||||||
echo "⚠️ gh-pages branch not found or empty, initializing a new one..."
|
bash .github/scripts/publish-void-r2.sh download
|
||||||
mkdir gh-pages-repo
|
|
||||||
cd gh-pages-repo
|
|
||||||
git init
|
|
||||||
git checkout -b gh-pages
|
|
||||||
git remote add origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}.git
|
|
||||||
cd ..
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Install XBPS Static Utilities
|
- name: Install XBPS Static Utilities
|
||||||
run: |
|
run: |
|
||||||
@@ -67,7 +101,6 @@ jobs:
|
|||||||
- name: Inject templates
|
- name: Inject templates
|
||||||
run: |
|
run: |
|
||||||
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
|
cp -R distro/void/srcpkgs/dms void-packages/srcpkgs/
|
||||||
cp -R distro/void/srcpkgs/dms-greeter void-packages/srcpkgs/
|
|
||||||
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
|
cp -R distro/void/srcpkgs/dms-git void-packages/srcpkgs/
|
||||||
|
|
||||||
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
|
- name: Enable unprivileged user namespaces (Ubuntu 24.04)
|
||||||
@@ -126,45 +159,71 @@ jobs:
|
|||||||
- name: Configure repositories
|
- name: Configure repositories
|
||||||
run: |
|
run: |
|
||||||
# Append the repository to repos-remote templates so xbps-src translates it automatically
|
# Append the repository to repos-remote templates so xbps-src translates it automatically
|
||||||
echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
|
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote.conf
|
||||||
echo "repository=https://avengemedia.github.io/DankLinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
|
echo "repository=https://void.danklinux.com/danklinux/current" >> void-packages/etc/xbps.d/repos-remote-x86_64-multilib.conf
|
||||||
|
|
||||||
# Add any existing compiled packages to the build cache directory to avoid rebuilds
|
# Add any existing compiled packages to the build cache directory to avoid rebuilds
|
||||||
if [ -d "gh-pages-repo/current" ]; then
|
if [ -d "r2-repo/current" ]; then
|
||||||
mkdir -p void-packages/hostdir/binpkgs
|
mkdir -p void-packages/hostdir/binpkgs
|
||||||
cp -L gh-pages-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
|
cp -L r2-repo/current/*.xbps void-packages/hostdir/binpkgs/ 2>/dev/null || true
|
||||||
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
|
xbps-rindex -a void-packages/hostdir/binpkgs/*.xbps 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build packages
|
- name: Build packages
|
||||||
run: |
|
run: |
|
||||||
mkdir -p gh-pages-repo/current
|
mkdir -p r2-repo/current
|
||||||
|
|
||||||
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
|
BUILD_DMS="${{ github.event.inputs.build_dms || 'true' }}"
|
||||||
BUILD_GREETER="${{ github.event.inputs.build_greeter || 'true' }}"
|
|
||||||
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
|
BUILD_GIT="${{ github.event.inputs.build_git || 'true' }}"
|
||||||
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
|
FORCE_REBUILD="${{ github.event.inputs.force_rebuild || 'false' }}"
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "schedule" ]; then
|
if [ "${{ github.event_name }}" = "schedule" ]; then
|
||||||
BUILD_DMS="false"
|
BUILD_DMS="false"
|
||||||
BUILD_GREETER="false"
|
|
||||||
BUILD_GIT="true"
|
BUILD_GIT="true"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "${{ github.event_name }}" = "release" ]; then
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
BUILD_DMS="true"
|
BUILD_DMS="true"
|
||||||
BUILD_GREETER="true"
|
|
||||||
BUILD_GIT="false"
|
BUILD_GIT="false"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
RELEASE_TAG=""
|
||||||
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
|
RELEASE_TAG="${{ github.event.release.tag_name }}"
|
||||||
|
elif [ -n "${{ github.event.inputs.version || '' }}" ]; then
|
||||||
|
RELEASE_TAG="${{ github.event.inputs.version }}"
|
||||||
|
elif [ "${{ github.ref_type }}" = "tag" ]; then
|
||||||
|
RELEASE_TAG="${{ github.ref_name }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_TAG" ]; then
|
||||||
|
RELEASE_VER="${RELEASE_TAG#v}"
|
||||||
|
ARCHIVE_TAG="v${RELEASE_VER}"
|
||||||
|
echo "Stable release tag: $ARCHIVE_TAG"
|
||||||
|
else
|
||||||
|
RELEASE_VER=""
|
||||||
|
ARCHIVE_TAG=""
|
||||||
|
echo "Stable release tag: (template version)"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "=== Starting Builds ==="
|
echo "=== Starting Builds ==="
|
||||||
echo "DMS stable build enabled: $BUILD_DMS"
|
echo "DMS stable build enabled: $BUILD_DMS"
|
||||||
echo "Greeter stable build enabled: $BUILD_GREETER"
|
|
||||||
echo "Git build enabled: $BUILD_GIT"
|
echo "Git build enabled: $BUILD_GIT"
|
||||||
echo "Force rebuild: $FORCE_REBUILD"
|
echo "Force rebuild: $FORCE_REBUILD"
|
||||||
|
|
||||||
cd void-packages
|
cd void-packages
|
||||||
|
|
||||||
|
if [ -n "$RELEASE_VER" ] && [ "$BUILD_DMS" = "true" ]; then
|
||||||
|
echo "🔧 Updating stable template for $ARCHIVE_TAG"
|
||||||
|
TARBALL="$(mktemp)"
|
||||||
|
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
||||||
|
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
||||||
|
rm -f "$TARBALL"
|
||||||
|
|
||||||
|
sed -i "s/^version=.*/version=${RELEASE_VER}/" srcpkgs/dms/template
|
||||||
|
sed -i "s/^checksum=.*/checksum=${RELEASE_CHECKSUM}/" srcpkgs/dms/template
|
||||||
|
fi
|
||||||
|
|
||||||
# 1. Build dms-git (development package)
|
# 1. Build dms-git (development package)
|
||||||
if [ "$BUILD_GIT" = "true" ]; then
|
if [ "$BUILD_GIT" = "true" ]; then
|
||||||
# Calculate dynamic git version (tag.commits.hash)
|
# Calculate dynamic git version (tag.commits.hash)
|
||||||
@@ -176,9 +235,9 @@ jobs:
|
|||||||
# into $wrksrc (create_wrksrc=yes handles the rest).
|
# into $wrksrc (create_wrksrc=yes handles the rest).
|
||||||
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
||||||
mkdir -p "$SRC_CACHE"
|
mkdir -p "$SRC_CACHE"
|
||||||
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||||
--exclude=void-packages \
|
--exclude=void-packages \
|
||||||
--exclude=gh-pages-repo \
|
--exclude=r2-repo \
|
||||||
--exclude=.git \
|
--exclude=.git \
|
||||||
--exclude=danklinux \
|
--exclude=danklinux \
|
||||||
-C .. .
|
-C .. .
|
||||||
@@ -192,13 +251,13 @@ jobs:
|
|||||||
|
|
||||||
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
|
EXPECTED_GIT_FILE="dms-git-${GIT_VER}_1.x86_64.xbps"
|
||||||
|
|
||||||
if [ -f "../gh-pages-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
if [ -f "../r2-repo/current/$EXPECTED_GIT_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
|
echo "✅ $EXPECTED_GIT_FILE already exists, skipping build."
|
||||||
else
|
else
|
||||||
echo "🔨 Compiling dms-git..."
|
echo "🔨 Compiling dms-git..."
|
||||||
./xbps-src pkg dms-git
|
./xbps-src pkg dms-git
|
||||||
rm -f "../gh-pages-repo/current/${EXPECTED_GIT_FILE}"
|
rm -f "../r2-repo/current/${EXPECTED_GIT_FILE}"
|
||||||
cp -L hostdir/binpkgs/dms-git-*.xbps ../gh-pages-repo/current/
|
cp -L "hostdir/binpkgs/${EXPECTED_GIT_FILE}" ../r2-repo/current/
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -208,38 +267,21 @@ jobs:
|
|||||||
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
|
STABLE_REV=$(grep -E '^revision=' srcpkgs/dms/template | cut -d= -f2 | tr -d '"')
|
||||||
|
|
||||||
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
|
EXPECTED_DMS_FILE="dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps"
|
||||||
if [ -f "../gh-pages-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
if [ -f "../r2-repo/current/$EXPECTED_DMS_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
||||||
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
|
echo "✅ $EXPECTED_DMS_FILE already exists, skipping build."
|
||||||
else
|
else
|
||||||
echo "🔨 Compiling dms ($STABLE_VER)..."
|
echo "🔨 Compiling dms ($STABLE_VER)..."
|
||||||
./xbps-src pkg dms
|
./xbps-src pkg dms
|
||||||
rm -f "../gh-pages-repo/current/${EXPECTED_DMS_FILE}"
|
rm -f "../r2-repo/current/${EXPECTED_DMS_FILE}"
|
||||||
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../gh-pages-repo/current/
|
cp -L hostdir/binpkgs/dms-${STABLE_VER}_${STABLE_REV}.x86_64.xbps ../r2-repo/current/
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 3. Build stable dms-greeter package
|
|
||||||
if [ "$BUILD_GREETER" = "true" ]; then
|
|
||||||
GREETER_VER=$(grep -E '^version=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
|
|
||||||
GREETER_REV=$(grep -E '^revision=' srcpkgs/dms-greeter/template | cut -d= -f2 | tr -d '"')
|
|
||||||
|
|
||||||
EXPECTED_GREETER_FILE="dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps"
|
|
||||||
if [ -f "../gh-pages-repo/current/$EXPECTED_GREETER_FILE" ] && [ "$FORCE_REBUILD" != "true" ]; then
|
|
||||||
echo "✅ $EXPECTED_GREETER_FILE already exists, skipping build."
|
|
||||||
else
|
|
||||||
echo "🔨 Compiling dms-greeter ($GREETER_VER)..."
|
|
||||||
./xbps-src pkg dms-greeter
|
|
||||||
rm -f "../gh-pages-repo/current/${EXPECTED_GREETER_FILE}"
|
|
||||||
cp -L hostdir/binpkgs/dms-greeter-${GREETER_VER}_${GREETER_REV}.x86_64.xbps ../gh-pages-repo/current/
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Index and sign repository
|
- name: Index and sign repository
|
||||||
run: |
|
run: |
|
||||||
cd gh-pages-repo/current
|
cd r2-repo/current
|
||||||
|
|
||||||
# Clean up any stale or dangling signature files to prevent O_CREAT ENOENT errors
|
rm -f *.sig
|
||||||
rm -f *.sig2 *.sig
|
|
||||||
|
|
||||||
# Guard: nothing to index if no .xbps files exist
|
# Guard: nothing to index if no .xbps files exist
|
||||||
if ! ls *.xbps 1>/dev/null 2>&1; then
|
if ! ls *.xbps 1>/dev/null 2>&1; then
|
||||||
@@ -249,6 +291,14 @@ jobs:
|
|||||||
|
|
||||||
# Regenerate repo index
|
# Regenerate repo index
|
||||||
xbps-rindex -a $(pwd)/*.xbps
|
xbps-rindex -a $(pwd)/*.xbps
|
||||||
|
xbps-rindex --remove-obsoletes $(pwd)
|
||||||
|
|
||||||
|
# Remove signatures left behind by obsolete packages. Signatures for
|
||||||
|
# unchanged immutable packages are retained byte-for-byte.
|
||||||
|
for sig in *.sig2; do
|
||||||
|
[ -e "$sig" ] || break
|
||||||
|
[ -f "${sig%.sig2}" ] || rm -f "$sig"
|
||||||
|
done
|
||||||
|
|
||||||
# Sign repository
|
# Sign repository
|
||||||
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
echo "${{ secrets.XBPS_PRIVATE_KEY }}" > /tmp/xbps_privkey.pem
|
||||||
@@ -259,10 +309,5 @@ jobs:
|
|||||||
|
|
||||||
rm -f /tmp/xbps_privkey.pem
|
rm -f /tmp/xbps_privkey.pem
|
||||||
|
|
||||||
- name: Deploy to gh-pages branch
|
- name: Publish repository to R2
|
||||||
run: |
|
run: bash .github/scripts/publish-void-r2.sh publish
|
||||||
cd gh-pages-repo
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
||||||
git add current/
|
|
||||||
git diff --quiet && git diff --staged --quiet || (git commit -m "Update XBPS packages [skip ci]" && git push origin gh-pages)
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Create GitHub App token
|
- name: Create GitHub App token
|
||||||
id: app_token
|
id: app_token
|
||||||
uses: actions/create-github-app-token@v1
|
uses: actions/create-github-app-token@v3
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ core.*
|
|||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
quickshell/dms-plugins
|
quickshell/dms-plugins
|
||||||
|
quickshell/dms-plugins-external
|
||||||
__pycache__
|
__pycache__
|
||||||
|
|
||||||
.vscode/
|
.vscode/
|
||||||
@@ -126,3 +127,8 @@ __pycache__
|
|||||||
distro/void/temp/
|
distro/void/temp/
|
||||||
distro/void/hostdir/
|
distro/void/hostdir/
|
||||||
distro/void/masterdir*/
|
distro/void/masterdir*/
|
||||||
|
|
||||||
|
# Often gets built
|
||||||
|
core/dms
|
||||||
|
|
||||||
|
core/internal/shellembed/dist
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "dank-qml-common"]
|
||||||
|
path = dank-qml-common
|
||||||
|
url = https://github.com/AvengeMedia/dank-qml-common.git
|
||||||
|
branch = stable-1.6
|
||||||
@@ -26,7 +26,7 @@ repos:
|
|||||||
name: settings search index is up to date
|
name: settings search index is up to date
|
||||||
entry: bash -c 'python3 quickshell/translations/extract_settings_index.py >/dev/null || exit 1; if ! git diff --exit-code -- quickshell/translations/settings_search_index.json; then echo "settings_search_index.json is out of date; run quickshell/translations/extract_settings_index.py and stage the result" >&2; exit 1; fi'
|
entry: bash -c 'python3 quickshell/translations/extract_settings_index.py >/dev/null || exit 1; if ! git diff --exit-code -- quickshell/translations/settings_search_index.json; then echo "settings_search_index.json is out of date; run quickshell/translations/extract_settings_index.py and stage the result" >&2; exit 1; fi'
|
||||||
language: system
|
language: system
|
||||||
files: ^quickshell/(Modules/Settings/.*\.qml|Modals/Settings/SettingsSidebar\.qml|translations/extract_settings_index\.py)$
|
files: ^quickshell/(Modules/Settings/.*\.qml|Common/SettingsTabs\.qml|Services/SettingsSearchService\.qml|translations/(extract_settings_index\.py|en\.json))$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
@@ -36,6 +36,14 @@ repos:
|
|||||||
language: system
|
language: system
|
||||||
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
|
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: i18n-term-variants
|
||||||
|
name: i18n term variants (no case/punctuation duplicates)
|
||||||
|
entry: python3 quickshell/translations/check_term_variants.py
|
||||||
|
language: system
|
||||||
|
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
|
||||||
|
pass_filenames: false
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: no-console-in-qml
|
- id: no-console-in-qml
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
## What is this?
|
||||||
|
|
||||||
|
DMS is an open-source, MIT-licensed desktop shell for Wayland compositors on linux. Like KWin, gnome-shell, or cosmic-shell - but designed to work with many different compositors.
|
||||||
|
|
||||||
|
## Repo Structure:
|
||||||
|
|
||||||
|
There are 2 main apps in the repo, they are designed to operate together. Additionally there is a dank-qml-common submodule which contains common QML widgets, like a shared library, that is leveraged by other quickshell apps in the Dank Desktop suite.
|
||||||
|
|
||||||
|
There is a GO module (dankgo) that is used by core for certain common go functionality.
|
||||||
|
|
||||||
|
### 1- core:
|
||||||
|
|
||||||
|
Contains 2 apps:
|
||||||
|
|
||||||
|
1. cmd/dms - The parent process for quickshell (shell runner), and general purpose tools and utilities (such as screenshot, clipboard). It also runs the unix socket server the UI uses for functionality quickshell doesn't provide.
|
||||||
|
2. cmd/dankinstall - An entirely separate app, a TUI for quickly installing DMS
|
||||||
|
|
||||||
|
### 2- quickshell:
|
||||||
|
|
||||||
|
A quickshell application, the entirety of the UI for DMS. It uses quickshells own services supplemented by the core unix socket server for additional functionality.
|
||||||
|
|
||||||
|
Layout:
|
||||||
|
|
||||||
|
- Common/ - app-wide singletons and helpers (Theme, SettingsData, SessionData, I18n, PopoutManager)
|
||||||
|
- Services/ - headless singletons that talk to the system and to core
|
||||||
|
- Modules/ - the visible shell: DankBar, ControlCenter, Dock, Lock, Notifications, OSD, and so on
|
||||||
|
- Modals/ - large standalone surfaces: Settings, DankLauncherV2, Clipboard, Greeter
|
||||||
|
- Widgets/ - reusable Dank* components (DankListView, DankIcon, DankToggle, ...)
|
||||||
|
- PLUGINS/ - the plugin system; third-party plugins load from the user's config dir
|
||||||
|
- translations/ - POEditor-synced catalogs
|
||||||
|
|
||||||
|
### Other directories:
|
||||||
|
|
||||||
|
- docs/ - IPC reference, custom theme docs
|
||||||
|
- distro/ - packaging for debian, fedora, nix, opensuse, ubuntu, void
|
||||||
|
|
||||||
|
## General Rules:
|
||||||
|
|
||||||
|
- Keep it simple. Do not overcomplicate things.
|
||||||
|
- Follow each apps' own conventions. For example, QML widgets use Theme tokens instead of hardcoding colors, spacing, or other constants.
|
||||||
|
- Resource usage is extremely important to DMS. The shell runs 24/7 on every machine it's installed on - audit any change for idle CPU cost, extra processes, timers, and retained memory.
|
||||||
|
- `core` and `quickshell` are tightly coupled through the unix socket protocol. When you are changing one, be mindful of the consequences for the other.
|
||||||
|
- Use the Dank* wrappers in Widgets/ (DankListView, DankFlickable, and so on) instead of raw ListView/Flickable/ScrollView.
|
||||||
|
- All user-facing text goes through I18n.tr(). Prefer reusing existing catalog terms over adding new ones. Never edit the translation catalogs by hand, they are synced with POEditor.
|
||||||
|
- Do not start editing code in response to a question. We'll tell you when to edit code.
|
||||||
|
- Do not leave paragraphs of comments on top of the code. You should try to avoid them as much as possible with understandable function names and code. If they are necessary even then, make them concise. Remove such comments when you come by them in the codebase. Comments should always move with code, not be left behind.
|
||||||
|
- Use guard statement patterns in any code you write.
|
||||||
|
- Do not write any useless tests, tests should cover input output validation - not useless things like "main.go contains func main()"
|
||||||
|
- Do not edit generated code directly, mocks are generated with mockery. Not edited by hand
|
||||||
|
- If we are missing a glaring issue when we ask you to do something, do not hesitate to point it out.
|
||||||
|
- Reinvent the wheel but do not reinvent the car. If you are solving a simple problem do not introduce a library. If you are solving a complex but a common problem, there is likely a modern library for it, if so, use it.
|
||||||
|
- Never commit or push code unless explicitly asked to do so.
|
||||||
|
- Never make a PR unless explicitly asked to do so.
|
||||||
|
|
||||||
|
## Commit Messages
|
||||||
|
|
||||||
|
Commit messages start with the part of the system they touched, followed by a short lowercase explanation of the work:
|
||||||
|
|
||||||
|
network: report wifi state from the associated adapter
|
||||||
|
doctor: flag config files that fail to parse
|
||||||
|
|
||||||
|
The title should be concise. Description should explain the work in more detail (only if required) while still being concise. Use simple language, do not try to sound smart.
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
|
# Next
|
||||||
|
- Go core migrated to dankgo (shared log/paths/errdefs/ipc/shellapp)
|
||||||
|
- Quickshell UI embedded in the dms binary, -c / DMS_SHELL_DIR override - breaking for setups relying on ~/.config/quickshell/dms auto-discovery (XDG search removed)
|
||||||
|
- release binaries + all -git packages build with withshell; sockets/pidfiles/env names unchanged, IPC APIVersion 28 wire-compatible
|
||||||
|
- -git packages (AUR dms-shell-git, fedora, opensuse, void, debian, ubuntu) no longer install /usr/share/quickshell/dms; dms greeter install falls back to the embedded UI when the greeter package is present
|
||||||
|
- ping over the socket returns {"pong":true} instead of "pong"
|
||||||
|
- startup: embedded UI resolution is keyed by build-time .dankrev (no per-start content hashing)
|
||||||
|
|
||||||
# 1.5.0
|
# 1.5.0
|
||||||
- Overhauled shadows
|
- Overhauled shadows
|
||||||
- App ID changed to com.danklinux.dms - breaking for window rules
|
- App ID changed to com.danklinux.dms - breaking for window rules
|
||||||
|
|||||||
@@ -6,6 +6,20 @@ To contribute fork this repository, make your changes, and open a pull request.
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
# or, in an existing clone:
|
||||||
|
git submodule update --init
|
||||||
|
```
|
||||||
|
|
||||||
|
To have `git pull` keep the submodule in sync automatically (moving it to the commit this repo points at, no separate `git submodule update` step), set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config submodule.recurse true
|
||||||
|
```
|
||||||
|
|
||||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -28,6 +42,47 @@ This will provide:
|
|||||||
|
|
||||||
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||||
|
|
||||||
|
## Building and running
|
||||||
|
|
||||||
|
The Quickshell UI is embedded into the `dms` binary at build time. `make build` copies `quickshell/` into `core/internal/shellembed/dist/` (generated, never committed) and compiles with the `withshell` tag. `make dev` builds without the tag — that binary carries no UI and requires an explicit config dir.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build # embedded binary at core/bin/dms
|
||||||
|
make dev # untagged development build
|
||||||
|
make run # dev build, then launch against the live quickshell/ tree
|
||||||
|
```
|
||||||
|
|
||||||
|
The UI config dir resolves in order: `-c <dir>`, `DMS_SHELL_DIR`, the dir a running instance is using, then the embedded UI. Each candidate must contain `shell.qml`. `make run` uses `-c $(pwd)/quickshell`, so QML edits hot-reload from the working tree.
|
||||||
|
|
||||||
|
The Go core depends on [dankgo](https://github.com/AvengeMedia/dankgo) for logging, XDG paths, the IPC transport, and the quickshell process lifecycle. To develop against a local dankgo checkout, create a gitignored `go.work` at the repo root:
|
||||||
|
|
||||||
|
```
|
||||||
|
go 1.26.1
|
||||||
|
|
||||||
|
use (
|
||||||
|
./core
|
||||||
|
../dankgo
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shared widgets (dank-qml-common)
|
||||||
|
|
||||||
|
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
|
||||||
|
|
||||||
|
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
|
||||||
|
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
|
||||||
|
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
|
||||||
|
|
||||||
|
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git config --global url."[email protected]:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
|
||||||
|
```
|
||||||
|
|
||||||
|
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
|
||||||
|
|
||||||
|
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
|
||||||
|
|
||||||
## VSCode Setup
|
## VSCode Setup
|
||||||
|
|
||||||
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||||
@@ -104,6 +159,10 @@ Text {
|
|||||||
|
|
||||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
||||||
|
|
||||||
|
Don't re-extract the translations. `en.json` and `template.json` are synced with POEditor by a maintainer script, so running `extract_translations.py` in your PR just makes a diff that fights the next sync. Add your `I18n.tr()` calls and leave the catalogs alone. (`settings_search_index.json` is the exception, the pre-commit hook regenerates that one when you touch settings QML.)
|
||||||
|
|
||||||
|
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
|
||||||
|
|
||||||
### GO (`core` directory)
|
### GO (`core` directory)
|
||||||
|
|
||||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||||
@@ -112,6 +171,21 @@ Preferably, try to keep new terms to a minimum and re-use existing terms where p
|
|||||||
4. Run `go mod tidy`
|
4. Run `go mod tidy`
|
||||||
5. Open pull request
|
5. Open pull request
|
||||||
|
|
||||||
|
golangci-lint runs as a pre-commit hook and covers `go vet`, so there's nothing separate to run. If you run `go vet ./...` yourself you'll see complaints about the generated mocks, ignore them, `core/.golangci.yml` excludes that directory.
|
||||||
|
|
||||||
|
#### Mocks
|
||||||
|
|
||||||
|
Test mocks under `core/internal/mocks/` are generated with [mockery](https://vektra.github.io/mockery/) v3. Don't edit them by hand, regenerate after changing a mocked interface:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd core
|
||||||
|
go run github.com/vektra/mockery/v3@latest
|
||||||
|
# or with it installed (go install github.com/vektra/mockery/v3@latest):
|
||||||
|
mockery
|
||||||
|
```
|
||||||
|
|
||||||
|
`core/.mockery.yml` lists every mocked interface and where its mock goes (e.g. `network.Backend` -> `internal/mocks/network/mock_Backend.go`). To mock a new interface, add it there under its package and regenerate.
|
||||||
|
|
||||||
## Pull request
|
## Pull request
|
||||||
|
|
||||||
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
|
Include screenshots/video if applicable in your pull request if applicable, to visualize what your change is affecting.
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ BINARY_NAME=dms
|
|||||||
CORE_DIR=core
|
CORE_DIR=core
|
||||||
BUILD_DIR=$(CORE_DIR)/bin
|
BUILD_DIR=$(CORE_DIR)/bin
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
INSTALL_DIR=$(PREFIX)/bin
|
INSTALL_DIR=$(DESTDIR)$(PREFIX)/bin
|
||||||
DATA_DIR=$(PREFIX)/share
|
DATA_DIR=$(DESTDIR)$(PREFIX)/share
|
||||||
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
ICON_DIR=$(DATA_DIR)/icons/hicolor/scalable/apps
|
||||||
|
|
||||||
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
USER_HOME := $(if $(SUDO_USER),$(shell getent passwd $(SUDO_USER) | cut -d: -f6),$(HOME))
|
||||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
.PHONY: all build dev run clean lint-qml install install-bin install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -27,6 +27,12 @@ build:
|
|||||||
@$(MAKE) -C $(CORE_DIR) build
|
@$(MAKE) -C $(CORE_DIR) build
|
||||||
@echo "Build complete"
|
@echo "Build complete"
|
||||||
|
|
||||||
|
dev:
|
||||||
|
@$(MAKE) -C $(CORE_DIR) dev
|
||||||
|
|
||||||
|
run: dev
|
||||||
|
@$(BUILD_DIR)/$(BINARY_NAME) run -c $(CURDIR)/$(SHELL_DIR)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@@ -35,19 +41,18 @@ clean:
|
|||||||
lint-qml:
|
lint-qml:
|
||||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||||
|
|
||||||
|
# Pull the latest dank-qml-common and pin it everywhere it is consumed
|
||||||
|
# (submodule pointer + nix flake input). Commit both in one change.
|
||||||
|
update-common:
|
||||||
|
git submodule update --remote --merge dank-qml-common
|
||||||
|
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Binary installed"
|
@echo "Binary installed"
|
||||||
|
|
||||||
install-shell:
|
|
||||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
|
||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
|
||||||
@echo "Shell files installed"
|
|
||||||
|
|
||||||
install-completions:
|
install-completions:
|
||||||
@echo "Installing shell completions..."
|
@echo "Installing shell completions..."
|
||||||
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
@mkdir -p $(DATA_DIR)/bash-completion/completions
|
||||||
@@ -59,13 +64,17 @@ install-completions:
|
|||||||
@echo "Shell completions installed"
|
@echo "Shell completions installed"
|
||||||
|
|
||||||
install-systemd:
|
install-systemd:
|
||||||
|
ifneq ($(shell uname),Linux)
|
||||||
|
@echo "Skipping systemd user service (non-Linux); start the shell from your compositor config with 'dms run'"
|
||||||
|
else
|
||||||
@echo "Installing systemd user service..."
|
@echo "Installing systemd user service..."
|
||||||
@mkdir -p $(SYSTEMD_USER_DIR)
|
@mkdir -p $(SYSTEMD_USER_DIR)
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
@if [ -n "$(SUDO_USER)" ]; then chown -R $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR); fi
|
||||||
@sed 's|/usr/bin/dms|$(INSTALL_DIR)/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
@sed 's|/usr/bin/dms|$(PREFIX)/bin/dms|g' $(ASSETS_DIR)/systemd/dms.service > $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
@chmod 644 $(SYSTEMD_USER_DIR)/dms.service
|
||||||
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
@if [ -n "$(SUDO_USER)" ]; then chown $(SUDO_USER):"$(id -gn $SUDO_USER)" $(SYSTEMD_USER_DIR)/dms.service; fi
|
||||||
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
@echo "Systemd service installed to $(SYSTEMD_USER_DIR)/dms.service"
|
||||||
|
endif
|
||||||
|
|
||||||
install-icon:
|
install-icon:
|
||||||
@echo "Installing icon..."
|
@echo "Installing icon..."
|
||||||
@@ -81,7 +90,7 @@ install-desktop:
|
|||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entries installed"
|
@echo "Desktop entries installed"
|
||||||
|
|
||||||
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
install: install-bin install-completions install-systemd install-icon install-desktop
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -94,7 +103,7 @@ uninstall-bin:
|
|||||||
@echo "Binary removed"
|
@echo "Binary removed"
|
||||||
|
|
||||||
uninstall-shell:
|
uninstall-shell:
|
||||||
@echo "Removing shell files from $(SHELL_INSTALL_DIR)..."
|
@echo "Removing pre-1.6 shell files from $(SHELL_INSTALL_DIR)..."
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)
|
@rm -rf $(SHELL_INSTALL_DIR)
|
||||||
@echo "Shell files removed"
|
@echo "Shell files removed"
|
||||||
|
|
||||||
@@ -142,7 +151,6 @@ help:
|
|||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
@echo " install-bin - Install only the binary"
|
@echo " install-bin - Install only the binary"
|
||||||
@echo " install-shell - Install only shell files"
|
|
||||||
@echo " install-completions - Install only shell completions"
|
@echo " install-completions - Install only shell completions"
|
||||||
@echo " install-systemd - Install only systemd service"
|
@echo " install-systemd - Install only systemd service"
|
||||||
@echo " install-icon - Install only icon"
|
@echo " install-icon - Install only icon"
|
||||||
@@ -151,7 +159,7 @@ help:
|
|||||||
@echo "Uninstall:"
|
@echo "Uninstall:"
|
||||||
@echo " uninstall - Remove everything (requires sudo)"
|
@echo " uninstall - Remove everything (requires sudo)"
|
||||||
@echo " uninstall-bin - Remove only the binary"
|
@echo " uninstall-bin - Remove only the binary"
|
||||||
@echo " uninstall-shell - Remove only shell files"
|
@echo " uninstall-shell - Remove shell files left by pre-1.6 installs"
|
||||||
@echo " uninstall-completions - Remove only shell completions"
|
@echo " uninstall-completions - Remove only shell completions"
|
||||||
@echo " uninstall-systemd - Remove only systemd service"
|
@echo " uninstall-systemd - Remove only systemd service"
|
||||||
@echo " uninstall-icon - Remove only icon"
|
@echo " uninstall-icon - Remove only icon"
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ DankMaterialShell/
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/1200a739-7770-4601-8b85-695ca527819a
|
https://github.com/user-attachments/assets/d310418b-cc44-4ace-ac0d-2fc0c851291d
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ One command installs DMS and all dependencies on Arch, Fedora, Debian, Ubuntu, o
|
|||||||
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
|
Wallpaper-based color schemes that automatically theme GTK, Qt, terminals, editors (vscode, vscodium), and more using [matugen](https://github.com/InioX/matugen) and dank16.
|
||||||
|
|
||||||
**System Monitoring**
|
**System Monitoring**
|
||||||
Real-time CPU, RAM, GPU metrics and temperatures with [dgop](https://github.com/AvengeMedia/dgop). Process list with search and management.
|
Real-time CPU, RAM, GPU metrics and temperatures built into the dms daemon (powered by [dgop](https://github.com/AvengeMedia/dgop)). Process list with search and management.
|
||||||
|
|
||||||
**Powerful Launcher**
|
**Powerful Launcher**
|
||||||
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
|
Spotlight-style search for applications, files ([dsearch](https://github.com/AvengeMedia/danksearch)), emojis, running windows, calculator, and commands. Extensible with plugins.
|
||||||
@@ -98,10 +98,12 @@ Notification center with grouping, rich text support, and keyboard navigation.
|
|||||||
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
MPRIS player controls, calendar sync, weather widgets, and clipboard history with image previews.
|
||||||
|
|
||||||
**Session Management**
|
**Session Management**
|
||||||
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and greeter support.
|
Lock screen, idle detection, auto-lock/suspend with separate AC/battery settings, and a settings front-end for [dank-greeter](https://github.com/AvengeMedia/dank-greeter).
|
||||||
|
|
||||||
**Plugin System**
|
**Plugin System**
|
||||||
Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
Extend functionality with the [plugin registry](https://plugins.danklinux.com). DMS keeps
|
||||||
|
`~/.config/DankMaterialShell/plugins.lock.json` synchronized with managed plugin installs and
|
||||||
|
their exact Git commits, so the same plugins can be reproduced on another machine.
|
||||||
|
|
||||||
## Supported Compositors
|
## Supported Compositors
|
||||||
|
|
||||||
@@ -120,6 +122,8 @@ dms ipc call audio setvolume 50
|
|||||||
dms ipc call wallpaper set /path/to/image.jpg
|
dms ipc call wallpaper set /path/to/image.jpg
|
||||||
dms brightness list # List available displays
|
dms brightness list # List available displays
|
||||||
dms plugins search # Browse plugin registry
|
dms plugins search # Browse plugin registry
|
||||||
|
dms plugins lock # Refresh the portable plugin lockfile
|
||||||
|
dms plugins restore ~/plugins.lock.json
|
||||||
```
|
```
|
||||||
|
|
||||||
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
|
[Full CLI and IPC documentation](https://danklinux.com/docs/dankmaterialshell/keybinds-ipc)
|
||||||
@@ -132,6 +136,17 @@ dms plugins search # Browse plugin registry
|
|||||||
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
- **Plugins:** [Development guide](https://danklinux.com/docs/dankmaterialshell/plugins-overview)
|
||||||
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
- **Support:** [Ko-fi](https://ko-fi.com/avengemediallc)
|
||||||
|
|
||||||
|
## Dank Projects
|
||||||
|
|
||||||
|
DMS is one piece of the suite. The rest lives in its own repos:
|
||||||
|
|
||||||
|
- [dank-greeter](https://github.com/AvengeMedia/dank-greeter) - greetd login screen with the Dank Material aesthetic. The Greeter tab in DMS settings is the front-end for it.
|
||||||
|
- [dankcalendar](https://github.com/AvengeMedia/dankcalendar) - Local, Google, Microsoft, and CalDAV calendars for the dank desktop.
|
||||||
|
- [dgop](https://github.com/AvengeMedia/dgop) - System monitoring TUI and Go library; its library powers the process list and dashboard widgets inside the dms daemon.
|
||||||
|
- [dsearch](https://github.com/AvengeMedia/danksearch) - Fast file search that powers file results in the launcher.
|
||||||
|
- [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) - Shared QML widgets and components used by DMS, dank-greeter, and dankcalendar.
|
||||||
|
- [dankgo](https://github.com/AvengeMedia/dankgo) - Common Go modules behind the single binary apps.
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See component-specific documentation:
|
See component-specific documentation:
|
||||||
@@ -188,7 +203,7 @@ For documentation contributions, see [DankLinux-Docs](https://github.com/AvengeM
|
|||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://www.star-history.com/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
[](https://star-history.dera.page/#AvengeMedia/DankMaterialShell&type=date&legend=top-left)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ Requisite=graphical-session.target
|
|||||||
Type=dbus
|
Type=dbus
|
||||||
BusName=org.freedesktop.Notifications
|
BusName=org.freedesktop.Notifications
|
||||||
ExecStart=/usr/bin/dms run --session
|
ExecStart=/usr/bin/dms run --session
|
||||||
ExecReload=/usr/bin/pkill -USR1 -x dms
|
ExecReload=/bin/kill -USR1 $MAINPID
|
||||||
|
LimitNOFILE=16384:infinity
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
RestartForceExitStatus=TEMPFAIL
|
||||||
|
SuccessExitStatus=TEMPFAIL
|
||||||
RestartSec=1.23
|
RestartSec=1.23
|
||||||
|
TimeoutStartSec=90s
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
+5
-6
@@ -72,7 +72,6 @@ linters:
|
|||||||
- path: _test\.go
|
- path: _test\.go
|
||||||
linters:
|
linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
- govet
|
|
||||||
- unused
|
- unused
|
||||||
- ineffassign
|
- ineffassign
|
||||||
- staticcheck
|
- staticcheck
|
||||||
@@ -93,15 +92,15 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
|
text: "Error return value of `.+\\.AddMatchSignal` is not checked"
|
||||||
# Exclude wayland pkg from errcheck and ineffassign (generated code patterns)
|
|
||||||
- linters:
|
|
||||||
- errcheck
|
|
||||||
- ineffassign
|
|
||||||
path: pkg/go-wayland/
|
|
||||||
# Exclude proto pkg from ineffassign (generated protocol code)
|
# Exclude proto pkg from ineffassign (generated protocol code)
|
||||||
- linters:
|
- linters:
|
||||||
- ineffassign
|
- ineffassign
|
||||||
path: internal/proto/
|
path: internal/proto/
|
||||||
|
# Exclude proto pkg SA4006 (generated protocol code has trailing dead offset increments)
|
||||||
|
- linters:
|
||||||
|
- staticcheck
|
||||||
|
text: "SA4006:"
|
||||||
|
path: internal/proto/
|
||||||
# binary.Write/Read to bytes.Buffer can't fail
|
# binary.Write/Read to bytes.Buffer can't fail
|
||||||
- linters:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ packages:
|
|||||||
pkgname: mocks_version
|
pkgname: mocks_version
|
||||||
interfaces:
|
interfaces:
|
||||||
VersionFetcher: {}
|
VersionFetcher: {}
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
github.com/AvengeMedia/dankgo/wayland/client:
|
||||||
config:
|
config:
|
||||||
dir: internal/mocks/wlclient
|
dir: internal/mocks/wlclient
|
||||||
pkgname: mocks_wlclient
|
pkgname: mocks_wlclient
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.12.2
|
rev: v2.13.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
+52
-27
@@ -3,6 +3,8 @@ BINARY_NAME_INSTALL=dankinstall
|
|||||||
SOURCE_DIR=cmd/dms
|
SOURCE_DIR=cmd/dms
|
||||||
SOURCE_DIR_INSTALL=cmd/dankinstall
|
SOURCE_DIR_INSTALL=cmd/dankinstall
|
||||||
BUILD_DIR=bin
|
BUILD_DIR=bin
|
||||||
|
SHELL_SRC=../quickshell
|
||||||
|
EMBED_DIR=internal/shellembed/dist
|
||||||
PREFIX ?= /usr/local
|
PREFIX ?= /usr/local
|
||||||
INSTALL_DIR=$(PREFIX)/bin
|
INSTALL_DIR=$(PREFIX)/bin
|
||||||
|
|
||||||
@@ -21,17 +23,46 @@ BUILD_LDFLAGS=-ldflags='-s -w -X main.Version=$(VERSION) -X main.buildTime=$(BUI
|
|||||||
|
|
||||||
# Architecture to build for dist target (amd64, arm64, or all)
|
# Architecture to build for dist target (amd64, arm64, or all)
|
||||||
ARCH ?= all
|
ARCH ?= all
|
||||||
|
# Target OSes for dist builds
|
||||||
|
DIST_OSES ?= linux freebsd
|
||||||
|
|
||||||
.PHONY: all build dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
ifeq ($(ARCH),all)
|
||||||
|
DIST_ARCHS = amd64 arm64
|
||||||
|
else
|
||||||
|
DIST_ARCHS = $(ARCH)
|
||||||
|
endif
|
||||||
|
|
||||||
|
.PHONY: all build sync-shell dankinstall dist clean install install-all install-dankinstall uninstall uninstall-all uninstall-dankinstall install-config uninstall-config test fmt vet deps print-version help
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
|
# Copy the quickshell UI into the embed dir (gitignored) so tagged builds
|
||||||
|
# can bake it into the binary. Dev-only files are stripped; scripts/ is kept
|
||||||
|
# minus its dev entries, since Theme.qml and BluetoothService.qml run
|
||||||
|
# gtk.sh/qt.sh/bluez-card-profile.lua out of the resolved shell dir.
|
||||||
|
# tar -h dereferences the DankCommon submodule symlink; go:embed rejects
|
||||||
|
# symlinks. .qmlls.ini is excluded at copy time: it's a symlink into the
|
||||||
|
# quickshell runtime VFS and dereferencing it fails whenever the shell isn't
|
||||||
|
# running.
|
||||||
|
sync-shell:
|
||||||
|
@test -e $(SHELL_SRC)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
|
||||||
|
@rm -rf $(EMBED_DIR)
|
||||||
|
@mkdir -p $(EMBED_DIR)
|
||||||
|
@tar -C $(SHELL_SRC) --exclude=.qmlls.ini -chf - . | tar -C $(EMBED_DIR) -xf -
|
||||||
|
@rm -rf $(EMBED_DIR)/.git* $(EMBED_DIR)/.github
|
||||||
|
@find $(EMBED_DIR) -type d \( -name .claude -o -name .vscode \) -prune -exec rm -rf {} +
|
||||||
|
@rm -f $(EMBED_DIR)/AGENTS.md $(EMBED_DIR)/qmlformat-all.sh
|
||||||
|
@rm -f $(EMBED_DIR)/scripts/i18nsync.py $(EMBED_DIR)/scripts/build-vscode-vsix.sh $(EMBED_DIR)/scripts/qmllint-entrypoints.sh
|
||||||
|
@rm -f $(EMBED_DIR)/scripts/spam-notifications.sh $(EMBED_DIR)/scripts/verify-notifications.sh
|
||||||
|
@rm -f $(EMBED_DIR)/translations/*.py $(EMBED_DIR)/translations/WORKFLOW.md
|
||||||
|
@cd $(EMBED_DIR) && find . -type f -print0 | LC_ALL=C sort -z | xargs -0 sha256sum | sha256sum | cut -c1-16 > .dankrev
|
||||||
|
|
||||||
# Build the main binary (dms)
|
# Build the main binary (dms)
|
||||||
build:
|
build: sync-shell
|
||||||
@echo "Building $(BINARY_NAME)..."
|
@echo "Building $(BINARY_NAME)..."
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
CGO_ENABLED=0 $(GO) build -tags withshell $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) ./$(SOURCE_DIR)
|
||||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME)"
|
||||||
|
|
||||||
dankinstall:
|
dankinstall:
|
||||||
@@ -40,26 +71,18 @@ dankinstall:
|
|||||||
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
CGO_ENABLED=0 $(GO) build $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME_INSTALL) ./$(SOURCE_DIR_INSTALL)
|
||||||
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
@echo "Build complete: $(BUILD_DIR)/$(BINARY_NAME_INSTALL)"
|
||||||
|
|
||||||
# Build distro binaries for amd64 and arm64 (Linux only, no update/greeter support)
|
# Build distro binaries (no update/greeter support) for each DIST_OSES/DIST_ARCHS pair
|
||||||
dist:
|
dist: sync-shell
|
||||||
ifeq ($(ARCH),all)
|
@echo "Building $(BINARY_NAME) for distribution ($(DIST_OSES) x $(DIST_ARCHS))..."
|
||||||
@echo "Building $(BINARY_NAME) for distribution (amd64 and arm64)..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
@mkdir -p $(BUILD_DIR)
|
||||||
@echo "Building for linux/amd64..."
|
@for os in $(DIST_OSES); do \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64 ./$(SOURCE_DIR)
|
for arch in $(DIST_ARCHS); do \
|
||||||
@echo "Building for linux/arm64..."
|
echo "Building for $$os/$$arch..."; \
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64 ./$(SOURCE_DIR)
|
CGO_ENABLED=0 GOOS=$$os GOARCH=$$arch $(GO) build -tags 'distro_binary withshell' $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch ./$(SOURCE_DIR) || exit 1; \
|
||||||
@echo "Distribution builds complete:"
|
echo " $(BUILD_DIR)/$(BINARY_NAME)-$$os-$$arch"; \
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-amd64"
|
done; \
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-arm64"
|
done
|
||||||
else
|
@echo "Distribution builds complete"
|
||||||
@echo "Building $(BINARY_NAME) for distribution ($(ARCH))..."
|
|
||||||
@mkdir -p $(BUILD_DIR)
|
|
||||||
@echo "Building for linux/$(ARCH)..."
|
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH) $(GO) build -tags distro_binary $(BUILD_LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH) ./$(SOURCE_DIR)
|
|
||||||
@echo "Distribution build complete:"
|
|
||||||
@echo " $(BUILD_DIR)/$(BINARY_NAME)-linux-$(ARCH)"
|
|
||||||
endif
|
|
||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
@@ -99,7 +122,7 @@ uninstall-dankinstall:
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
@echo "Cleaning build artifacts..."
|
@echo "Cleaning build artifacts..."
|
||||||
@rm -rf $(BUILD_DIR)
|
@rm -rf $(BUILD_DIR) $(EMBED_DIR)
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
@@ -112,7 +135,8 @@ fmt:
|
|||||||
|
|
||||||
vet:
|
vet:
|
||||||
@echo "Running go vet..."
|
@echo "Running go vet..."
|
||||||
$(GO) vet ./...
|
$(GO) vet $$($(GO) list ./... | grep -v internal/highlight/lexers)
|
||||||
|
$(GO) vet -composites=false ./internal/highlight/lexers
|
||||||
|
|
||||||
deps:
|
deps:
|
||||||
@echo "Updating dependencies..."
|
@echo "Updating dependencies..."
|
||||||
@@ -141,10 +165,11 @@ print-version:
|
|||||||
help:
|
help:
|
||||||
@echo "Available targets:"
|
@echo "Available targets:"
|
||||||
@echo " all - Build the main binary (dms) (default)"
|
@echo " all - Build the main binary (dms) (default)"
|
||||||
@echo " build - Build the main binary (dms)"
|
@echo " build - Build the main binary (dms) with the embedded UI"
|
||||||
|
@echo " sync-shell - Copy quickshell/ into the embed dir (runs before tagged builds)"
|
||||||
@echo " dankinstall - Build dankinstall binary"
|
@echo " dankinstall - Build dankinstall binary"
|
||||||
@echo " dist - Build dms for linux amd64/arm64 (no update/greeter)"
|
@echo " dist - Build dms for linux/freebsd amd64/arm64 (no update/greeter)"
|
||||||
@echo " Use ARCH=amd64 or ARCH=arm64 to build only one"
|
@echo " Use ARCH=amd64 or ARCH=arm64 and/or DIST_OSES=linux to narrow"
|
||||||
@echo " build-all - Build both binaries"
|
@echo " build-all - Build both binaries"
|
||||||
@echo " install - Install dms to $(INSTALL_DIR)"
|
@echo " install - Install dms to $(INSTALL_DIR)"
|
||||||
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
@echo " install-all - Install both dms and dankinstall to $(INSTALL_DIR)"
|
||||||
|
|||||||
+28
-15
@@ -10,7 +10,7 @@ Go-based backend for DankMaterialShell providing system integration, IPC, and in
|
|||||||
Command-line interface and daemon for shell management and system control.
|
Command-line interface and daemon for shell management and system control.
|
||||||
|
|
||||||
**dankinstall**
|
**dankinstall**
|
||||||
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, and Gentoo. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
Distribution-aware installer for deploying DMS and compositor configurations on Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, and Void. Supports both an interactive TUI and a headless (unattended) mode via CLI flags.
|
||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
@@ -74,11 +74,11 @@ Custom IPC via unix socket (JSON API) for shell communication.
|
|||||||
- `dms run [-d]` - Start shell (optionally as daemon)
|
- `dms run [-d]` - Start shell (optionally as daemon)
|
||||||
- `dms restart` / `dms kill` - Manage running processes
|
- `dms restart` / `dms kill` - Manage running processes
|
||||||
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
- `dms ipc <command>` - Send IPC commands (toggle launcher, notifications, etc.)
|
||||||
- `dms plugins [install|browse|search]` - Plugin management
|
- `dms plugins [install|browse|search|lock|restore]` - Plugin management and portable exact-revision lockfiles
|
||||||
- `dms brightness [list|set]` - Control display/monitor brightness
|
- `dms brightness [list|set]` - Control display/monitor brightness
|
||||||
- `dms color pick` - Native color picker (see below)
|
- `dms color pick` - Native color picker (see below)
|
||||||
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
- `dms update` - Update DMS and dependencies (disabled in distro packages)
|
||||||
- `dms greeter install` - Install greetd greeter (disabled in distro packages)
|
- `dms greeter` - Deprecated; forwards to the standalone [dms-greeter](https://github.com/AvengeMedia/dank-greeter) binary
|
||||||
|
|
||||||
### Color Picker
|
### Color Picker
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ make test # Run tests
|
|||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update/greeter features
|
make dist # Build without update features
|
||||||
```
|
```
|
||||||
|
|
||||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
@@ -132,9 +132,11 @@ This runs gofmt, golangci-lint, tests, and builds before each commit when `core/
|
|||||||
|
|
||||||
**Regenerating Wayland Protocol Bindings:**
|
**Regenerating Wayland Protocol Bindings:**
|
||||||
|
|
||||||
|
Protocol XML files live in `internal/proto/xml/`. Bindings are generated with the [dankgo](https://github.com/AvengeMedia/dankgo) `go-wayland-scanner`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
go run github.com/AvengeMedia/dankgo/cmd/go-wayland-scanner@latest \
|
||||||
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
-i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
||||||
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
|
-pkg wlr_gamma_control -o internal/proto/wlr_gamma_control/gamma_control.go
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -155,26 +157,34 @@ curl -fsSL https://install.danklinux.com | sh
|
|||||||
|
|
||||||
**Headless (unattended):**
|
**Headless (unattended):**
|
||||||
|
|
||||||
Headless mode requires cached sudo credentials. Run `sudo -v` first:
|
Headless mode requires cached credentials or a passwordless rule for your privilege escalation tool (sudo, doas, or run0). With sudo, run `sudo -v` first:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
|
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty -y
|
||||||
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --include-deps dms-greeter -y
|
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c hyprland -t kitty --dms-greeter -y
|
||||||
|
sudo -v && curl -fsSL https://install.danklinux.com | sh -s -- -c niri -t ghostty --git-deps niri,quickshell --all-features -y
|
||||||
```
|
```
|
||||||
|
|
||||||
| Flag | Short | Description |
|
| Flag | Short | Description |
|
||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| `--compositor <niri|hyprland>` | `-c` | Compositor/WM to install (required for headless) |
|
| `--compositor <niri\|hyprland\|mango>` | `-c` | Compositor/WM to install (required for headless) |
|
||||||
| `--term <ghostty|kitty|alacritty>` | `-t` | Terminal emulator (required for headless) |
|
| `--term <ghostty\|kitty\|alacritty>` | `-t` | Terminal emulator (required for headless) |
|
||||||
|
| `--privesc <sudo\|doas\|run0>` | | Privilege escalation tool (default: autodetect) |
|
||||||
|
| `--git` | | Install the git version of every dependency that has one |
|
||||||
|
| `--git-deps <name,...>` | | Install the git version of specific dependencies (e.g. `niri,quickshell`) |
|
||||||
|
| `--all-features` | | Enable all optional dependencies (`dms-greeter`, `danksearch`, `dankcalendar`) |
|
||||||
|
| `--dms-greeter` | | Install dms-greeter |
|
||||||
|
| `--danksearch` | | Install danksearch and enable its user indexing service |
|
||||||
|
| `--dankcalendar` | | Install dankcalendar |
|
||||||
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
|
| `--include-deps <name,...>` | | Enable optional dependencies (e.g. `dms-greeter`) |
|
||||||
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
| `--exclude-deps <name,...>` | | Skip specific dependencies |
|
||||||
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
| `--replace-configs <name,...>` | | Replace specific configuration files (mutually exclusive with `--replace-configs-all`) |
|
||||||
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
| `--replace-configs-all` | | Replace all configuration files (mutually exclusive with `--replace-configs`) |
|
||||||
| `--yes` | `-y` | Required for headless mode — confirms installation without interactive prompts |
|
| `--yes` | `-y` | Required for headless mode - confirms installation without interactive prompts |
|
||||||
|
|
||||||
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
Headless mode requires `--yes` to proceed; without it, the installer exits with an error.
|
||||||
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
Configuration files are not replaced by default unless `--replace-configs` or `--replace-configs-all` is specified.
|
||||||
`dms-greeter` is disabled by default; use `--include-deps dms-greeter` to enable it.
|
Optional packages (`dms-greeter`, `danksearch`, `dankcalendar`) are disabled by default; enable them with their dedicated flags, `--include-deps`, or `--all-features`.
|
||||||
|
|
||||||
When no flags are provided, `dankinstall` launches the interactive TUI.
|
When no flags are provided, `dankinstall` launches the interactive TUI.
|
||||||
|
|
||||||
@@ -183,7 +193,7 @@ When no flags are provided, `dankinstall` launches the interactive TUI.
|
|||||||
Headless mode activates when `--compositor` or `--term` is provided.
|
Headless mode activates when `--compositor` or `--term` is provided.
|
||||||
|
|
||||||
- Both `--compositor` and `--term` are required; providing only one results in an error.
|
- Both `--compositor` and `--term` are required; providing only one results in an error.
|
||||||
- Headless-only flags (`--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`) are rejected in TUI mode.
|
- Headless-only flags (`--privesc`, `--git`, `--git-deps`, `--all-features`, `--include-deps`, `--exclude-deps`, `--replace-configs`, `--replace-configs-all`, `--yes`, `--danksearch`, `--dankcalendar`, `--dms-greeter`) are rejected in TUI mode.
|
||||||
- Positional arguments are not accepted.
|
- Positional arguments are not accepted.
|
||||||
|
|
||||||
### Log file location
|
### Log file location
|
||||||
@@ -193,7 +203,7 @@ Set the `DANKINSTALL_LOG_DIR` environment variable to override the log directory
|
|||||||
|
|
||||||
## Supported Distributions
|
## Supported Distributions
|
||||||
|
|
||||||
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo (and derivatives)
|
Arch, Fedora, Debian, Ubuntu, openSUSE, Gentoo, Void (and derivatives)
|
||||||
|
|
||||||
**Arch Linux**
|
**Arch Linux**
|
||||||
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
Uses `pacman` for system packages, builds AUR packages via `makepkg`, no AUR helper dependency.
|
||||||
@@ -206,7 +216,7 @@ Uses COPR repositories (`avengemedia/danklinux`, `avengemedia/dms`).
|
|||||||
Requires PPA support. Most packages built from source (slow first install).
|
Requires PPA support. Most packages built from source (slow first install).
|
||||||
|
|
||||||
**Debian**
|
**Debian**
|
||||||
Debian 13+ (Trixie). niri only, no Hyprland support. Builds from source.
|
Debian 13+ (Trixie), testing, and sid. Stable quickshell from Debian (trixie-backports on 13). Other companions via OBS.
|
||||||
|
|
||||||
**openSUSE**
|
**openSUSE**
|
||||||
Most packages available in standard repos. Minimal building required.
|
Most packages available in standard repos. Minimal building required.
|
||||||
@@ -214,4 +224,7 @@ Most packages available in standard repos. Minimal building required.
|
|||||||
**Gentoo**
|
**Gentoo**
|
||||||
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
Uses Portage with GURU overlay. Automatically configures USE flags. Variable success depending on system configuration.
|
||||||
|
|
||||||
|
**Void Linux**
|
||||||
|
Uses XBPS with the DMS and DankLinux self-hosted repositories.
|
||||||
|
|
||||||
See installer output for distribution-specific details during installation.
|
See installer output for distribution-specific details during installation.
|
||||||
|
|||||||
@@ -14,15 +14,21 @@ import (
|
|||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
// Flag variables bound via pflag
|
|
||||||
var (
|
var (
|
||||||
compositor string
|
compositor string
|
||||||
term string
|
term string
|
||||||
|
privescTool string
|
||||||
|
gitAll bool
|
||||||
|
gitDeps []string
|
||||||
|
allFeatures bool
|
||||||
includeDeps []string
|
includeDeps []string
|
||||||
excludeDeps []string
|
excludeDeps []string
|
||||||
replaceConfigs []string
|
replaceConfigs []string
|
||||||
replaceConfigsAll bool
|
replaceConfigsAll bool
|
||||||
yes bool
|
yes bool
|
||||||
|
danksearch bool
|
||||||
|
dankcalendar bool
|
||||||
|
dmsGreeter bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -33,8 +39,8 @@ var rootCmd = &cobra.Command{
|
|||||||
Without flags, it launches an interactive TUI. Providing either --compositor
|
Without flags, it launches an interactive TUI. Providing either --compositor
|
||||||
or --term activates headless (unattended) mode, which requires both flags.
|
or --term activates headless (unattended) mode, which requires both flags.
|
||||||
|
|
||||||
Headless mode requires cached sudo credentials. Run 'sudo -v' beforehand, or
|
Headless mode requires cached credentials or a passwordless rule for your
|
||||||
configure passwordless sudo for your user.`,
|
privilege escalation tool (sudo, doas, or run0).`,
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
RunE: runDankinstall,
|
RunE: runDankinstall,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
@@ -44,11 +50,18 @@ configure passwordless sudo for your user.`,
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
|
rootCmd.Flags().StringVarP(&compositor, "compositor", "c", "", "Compositor/WM to install: niri, hyprland, or mango (enables headless mode)")
|
||||||
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
rootCmd.Flags().StringVarP(&term, "term", "t", "", "Terminal emulator to install: ghostty, kitty, or alacritty (enables headless mode)")
|
||||||
|
rootCmd.Flags().StringVar(&privescTool, "privesc", "", "Privilege escalation tool: sudo, doas, or run0 (default: autodetect)")
|
||||||
|
rootCmd.Flags().BoolVar(&gitAll, "git", false, "Install the git version of every dep that has one")
|
||||||
|
rootCmd.Flags().StringSliceVar(&gitDeps, "git-deps", []string{}, "Deps to install the git version of (e.g. niri,quickshell)")
|
||||||
|
rootCmd.Flags().BoolVar(&allFeatures, "all-features", false, "Enable all optional deps (dms-greeter, danksearch, dankcalendar)")
|
||||||
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
rootCmd.Flags().StringSliceVar(&includeDeps, "include-deps", []string{}, "Optional deps to enable (e.g. dms-greeter)")
|
||||||
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
rootCmd.Flags().StringSliceVar(&excludeDeps, "exclude-deps", []string{}, "Deps to skip during installation")
|
||||||
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
rootCmd.Flags().StringSliceVar(&replaceConfigs, "replace-configs", []string{}, "Deploy only named configs (e.g. niri,ghostty)")
|
||||||
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
rootCmd.Flags().BoolVar(&replaceConfigsAll, "replace-configs-all", false, "Deploy and replace all configurations")
|
||||||
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
rootCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-confirm all prompts")
|
||||||
|
rootCmd.Flags().BoolVar(&danksearch, "danksearch", false, "Install danksearch and enable its user indexing service")
|
||||||
|
rootCmd.Flags().BoolVar(&dankcalendar, "dankcalendar", false, "Install dankcalendar")
|
||||||
|
rootCmd.Flags().BoolVar(&dmsGreeter, "dms-greeter", false, "Install dms-greeter")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -67,13 +80,19 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
|||||||
headlessMode := compositor != "" || term != ""
|
headlessMode := compositor != "" || term != ""
|
||||||
|
|
||||||
if !headlessMode {
|
if !headlessMode {
|
||||||
// Reject headless-only flags when running in TUI mode.
|
|
||||||
headlessOnly := []string{
|
headlessOnly := []string{
|
||||||
|
"privesc",
|
||||||
|
"git",
|
||||||
|
"git-deps",
|
||||||
|
"all-features",
|
||||||
"include-deps",
|
"include-deps",
|
||||||
"exclude-deps",
|
"exclude-deps",
|
||||||
"replace-configs",
|
"replace-configs",
|
||||||
"replace-configs-all",
|
"replace-configs-all",
|
||||||
"yes",
|
"yes",
|
||||||
|
"danksearch",
|
||||||
|
"dankcalendar",
|
||||||
|
"dms-greeter",
|
||||||
}
|
}
|
||||||
var set []string
|
var set []string
|
||||||
for _, name := range headlessOnly {
|
for _, name := range headlessOnly {
|
||||||
@@ -93,7 +112,6 @@ func runDankinstall(cmd *cobra.Command, args []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runHeadless() error {
|
func runHeadless() error {
|
||||||
// Validate required flags
|
|
||||||
if compositor == "" {
|
if compositor == "" {
|
||||||
return fmt.Errorf("--compositor is required for headless mode (niri, hyprland, or mango)")
|
return fmt.Errorf("--compositor is required for headless mode (niri, hyprland, or mango)")
|
||||||
}
|
}
|
||||||
@@ -104,16 +122,22 @@ func runHeadless() error {
|
|||||||
cfg := headless.Config{
|
cfg := headless.Config{
|
||||||
Compositor: compositor,
|
Compositor: compositor,
|
||||||
Terminal: term,
|
Terminal: term,
|
||||||
|
PrivescTool: privescTool,
|
||||||
|
GitAll: gitAll,
|
||||||
|
GitDeps: gitDeps,
|
||||||
|
AllFeatures: allFeatures,
|
||||||
IncludeDeps: includeDeps,
|
IncludeDeps: includeDeps,
|
||||||
ExcludeDeps: excludeDeps,
|
ExcludeDeps: excludeDeps,
|
||||||
ReplaceConfigs: replaceConfigs,
|
ReplaceConfigs: replaceConfigs,
|
||||||
ReplaceConfigsAll: replaceConfigsAll,
|
ReplaceConfigsAll: replaceConfigsAll,
|
||||||
Yes: yes,
|
Yes: yes,
|
||||||
|
DankSearch: danksearch,
|
||||||
|
DankCalendar: dankcalendar,
|
||||||
|
DmsGreeter: dmsGreeter,
|
||||||
}
|
}
|
||||||
|
|
||||||
runner := headless.NewRunner(cfg)
|
runner := headless.NewRunner(cfg)
|
||||||
|
|
||||||
// Set up file logging
|
|
||||||
fileLogger, err := log.NewFileLogger()
|
fileLogger, err := log.NewFileLogger()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Warning: Failed to create log file: %v\n", err)
|
||||||
@@ -128,10 +152,6 @@ func runHeadless() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
} else {
|
} else {
|
||||||
// Drain the log channel to prevent blocking sends from deadlocking
|
|
||||||
// downstream components (distros, config deployer) that write to it.
|
|
||||||
// Use an explicit stop signal because this code does not own the
|
|
||||||
// runner log channel and cannot assume it will be closed.
|
|
||||||
defer drainLogChan(runner.GetLogChan())()
|
defer drainLogChan(runner.GetLogChan())()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,10 +191,6 @@ func runTUI() error {
|
|||||||
if fileLogger != nil {
|
if fileLogger != nil {
|
||||||
fileLogger.StartListening(model.GetLogChan())
|
fileLogger.StartListening(model.GetLogChan())
|
||||||
} else {
|
} else {
|
||||||
// Drain the log channel to prevent blocking sends from deadlocking
|
|
||||||
// downstream components (distros, config deployer) that write to it.
|
|
||||||
// Use an explicit stop signal because this code does not own the
|
|
||||||
// model log channel and cannot assume it will be closed.
|
|
||||||
defer drainLogChan(model.GetLogChan())()
|
defer drainLogChan(model.GetLogChan())()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +208,9 @@ func runTUI() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// drainLogChan starts a goroutine that discards all messages from logCh,
|
// drainLogChan discards messages from logCh so blocking sends in downstream
|
||||||
// preventing blocking sends from deadlocking downstream components. It returns
|
// components cannot deadlock when no file logger is attached; the returned
|
||||||
// a cleanup function that signals the goroutine to stop and waits for it to
|
// cleanup stops the drain without assuming the channel will ever close.
|
||||||
// exit. Callers should defer the returned function.
|
|
||||||
func drainLogChan(logCh <-chan string) func() {
|
func drainLogChan(logCh <-chan string) func() {
|
||||||
drainStop := make(chan struct{})
|
drainStop := make(chan struct{})
|
||||||
drainDone := make(chan struct{})
|
drainDone := make(chan struct{})
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -12,7 +16,7 @@ import (
|
|||||||
var authCmd = &cobra.Command{
|
var authCmd = &cobra.Command{
|
||||||
Use: "auth",
|
Use: "auth",
|
||||||
Short: "Manage DMS authentication sync",
|
Short: "Manage DMS authentication sync",
|
||||||
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
Long: "Manage PAM/authentication setup for the DMS lock screen",
|
||||||
}
|
}
|
||||||
|
|
||||||
var authSyncCmd = &cobra.Command{
|
var authSyncCmd = &cobra.Command{
|
||||||
@@ -35,9 +39,142 @@ var authSyncCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var authResolveLockCmd = &cobra.Command{
|
||||||
|
Use: "resolve-lock",
|
||||||
|
Short: "Generate the lock-screen PAM config from the system auth stack",
|
||||||
|
Long: "Resolve the distribution's PAM auth stack into a self-contained lock-screen config under the user state directory.\n" +
|
||||||
|
"Runs unprivileged (reads /etc/pam.d, writes to the user's state dir) and is used by the shell as a fallback when /etc/pam.d/dankshell is not managed.\n" +
|
||||||
|
"Prints the path of the generated file.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
quiet, _ := cmd.Flags().GetBool("quiet")
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
if !quiet {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path, err := sharedpam.WriteUserLockscreenPamConfig(logFunc)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error resolving lock-screen PAM config: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(path)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var authListServicesCmd = &cobra.Command{
|
||||||
|
Use: "list-services",
|
||||||
|
Short: "List candidate lock-screen PAM services available on this system",
|
||||||
|
Long: "Enumerate the lock-screen PAM services that exist on this system and report their resolved auth stack (whether it has an auth directive and whether fingerprint/U2F modules appear inline).",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
services := sharedpam.ListLockscreenPamServices()
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
payload := struct {
|
||||||
|
Services []sharedpam.LockscreenPamServiceInfo `json:"services"`
|
||||||
|
}{Services: services}
|
||||||
|
data, err := json.MarshalIndent(payload, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error encoding services: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(services) == 0 {
|
||||||
|
fmt.Println("No candidate lock-screen PAM services found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, s := range services {
|
||||||
|
fmt.Printf("%-20s %-30s auth=%-5t fingerprint=%-5t u2f=%t\n", s.Name, s.Path, s.HasAuth, s.InlineFingerprint, s.InlineU2f)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var authValidateCmd = &cobra.Command{
|
||||||
|
Use: "validate",
|
||||||
|
Short: "Validate a PAM service file for use by the DMS lock screen",
|
||||||
|
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password or dedicated U2F stack. Exits 1 when the file is not usable.",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
path, _ := cmd.Flags().GetString("path")
|
||||||
|
service, _ := cmd.Flags().GetString("service")
|
||||||
|
purpose, _ := cmd.Flags().GetString("purpose")
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if (path == "") == (service == "") {
|
||||||
|
log.Fatalf("Error: exactly one of --path or --service is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if purpose != "password" && purpose != "u2f" {
|
||||||
|
log.Fatalf("Error: --purpose must be password or u2f")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result sharedpam.LockscreenPamValidation
|
||||||
|
switch {
|
||||||
|
case service != "":
|
||||||
|
if purpose == "u2f" {
|
||||||
|
result = sharedpam.ValidateLockscreenU2fPamService(service)
|
||||||
|
} else {
|
||||||
|
result = sharedpam.ValidateLockscreenPamService(service)
|
||||||
|
}
|
||||||
|
case !filepath.IsAbs(path):
|
||||||
|
result = sharedpam.LockscreenPamValidation{
|
||||||
|
Path: path,
|
||||||
|
MissingModules: []string{},
|
||||||
|
Warnings: []string{},
|
||||||
|
Errors: []string{"--path must be an absolute file path"},
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if purpose == "u2f" {
|
||||||
|
result = sharedpam.ValidateLockscreenU2fPamPath(path)
|
||||||
|
} else {
|
||||||
|
result = sharedpam.ValidateLockscreenPamPath(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
data, err := json.MarshalIndent(result, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error encoding validation: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
} else {
|
||||||
|
printLockscreenPamValidation(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !result.Valid {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func printLockscreenPamValidation(result sharedpam.LockscreenPamValidation) {
|
||||||
|
fmt.Printf("Path: %s\n", result.Path)
|
||||||
|
fmt.Printf("Valid: %t\n", result.Valid)
|
||||||
|
fmt.Printf("Has auth: %t\n", result.HasAuth)
|
||||||
|
fmt.Printf("Inline fingerprint: %t\n", result.InlineFingerprint)
|
||||||
|
fmt.Printf("Inline U2F: %t\n", result.InlineU2f)
|
||||||
|
if len(result.MissingModules) > 0 {
|
||||||
|
fmt.Printf("Missing modules: %s\n", strings.Join(result.MissingModules, ", "))
|
||||||
|
}
|
||||||
|
for _, w := range result.Warnings {
|
||||||
|
fmt.Println("⚠ " + w)
|
||||||
|
}
|
||||||
|
for _, e := range result.Errors {
|
||||||
|
fmt.Println("✗ " + e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
||||||
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
||||||
|
authResolveLockCmd.Flags().BoolP("quiet", "q", false, "Only print the resulting file path")
|
||||||
|
|
||||||
|
authListServicesCmd.Flags().Bool("json", false, "Output as JSON")
|
||||||
|
|
||||||
|
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
|
||||||
|
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
|
||||||
|
authValidateCmd.Flags().String("purpose", "password", "Validation purpose: password or u2f")
|
||||||
|
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncAuth(nonInteractive bool) error {
|
func syncAuth(nonInteractive bool) error {
|
||||||
@@ -75,3 +212,31 @@ func syncAuthInTerminal(nonInteractive bool) error {
|
|||||||
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
||||||
return runCommandInTerminal(shellCmd)
|
return runCommandInTerminal(shellCmd)
|
||||||
}
|
}
|
||||||
|
func runCommandInTerminal(shellCmd string) error {
|
||||||
|
terminals := []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
}{
|
||||||
|
{"gnome-terminal", []string{"--", "bash", "-c", shellCmd}},
|
||||||
|
{"konsole", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"xfce4-terminal", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||||
|
{"ghostty", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"wezterm", []string{"start", "--", "bash", "-c", shellCmd}},
|
||||||
|
{"alacritty", []string{"-e", "bash", "-c", shellCmd}},
|
||||||
|
{"kitty", []string{"bash", "-c", shellCmd}},
|
||||||
|
{"xterm", []string{"-e", "bash -c \"" + strings.ReplaceAll(shellCmd, `"`, `\"`) + "\""}},
|
||||||
|
}
|
||||||
|
for _, t := range terminals {
|
||||||
|
if _, err := exec.LookPath(t.name); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cmd := exec.Command(t.name, t.args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("no terminal emulator found (tried: gnome-terminal, konsole, xfce4-terminal, ghostty, wezterm, alacritty, kitty, xterm)")
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/backup"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var backupOutputPath string
|
||||||
|
|
||||||
|
var backupCmd = &cobra.Command{
|
||||||
|
Use: "backup",
|
||||||
|
Short: "Backup and restore DMS configuration",
|
||||||
|
}
|
||||||
|
|
||||||
|
var backupCreateCmd = &cobra.Command{
|
||||||
|
Use: "create",
|
||||||
|
Short: "Create a tar.gz backup of ~/.config/DankMaterialShell (settings, plugins, themes)",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: runBackupCreate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var backupRestoreCmd = &cobra.Command{
|
||||||
|
Use: "restore <archive>",
|
||||||
|
Short: "Restore a DMS configuration backup; the current configuration is moved aside first",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: runBackupRestore,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
backupCreateCmd.Flags().StringVarP(&backupOutputPath, "output", "o", "", "Output archive path (default dms-backup-<timestamp>.tar.gz)")
|
||||||
|
backupCmd.AddCommand(backupCreateCmd, backupRestoreCmd)
|
||||||
|
rootCmd.AddCommand(backupCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBackupCreate(cmd *cobra.Command, args []string) {
|
||||||
|
output := backupOutputPath
|
||||||
|
if output == "" {
|
||||||
|
output = backup.DefaultArchiveName()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := backup.Create(output); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBackupRestore(cmd *cobra.Command, args []string) {
|
||||||
|
previous, err := backup.Restore(args[0])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if previous != "" {
|
||||||
|
fmt.Printf("Previous configuration moved to %s\n", previous)
|
||||||
|
}
|
||||||
|
fmt.Println("Restore complete. Restart the shell to apply.")
|
||||||
|
}
|
||||||
@@ -8,13 +8,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/lexers"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/highlight/styles"
|
||||||
"github.com/alecthomas/chroma/v2"
|
"github.com/alecthomas/chroma/v2"
|
||||||
"github.com/alecthomas/chroma/v2/formatters/html"
|
"github.com/alecthomas/chroma/v2/formatters/html"
|
||||||
"github.com/alecthomas/chroma/v2/lexers"
|
|
||||||
"github.com/alecthomas/chroma/v2/styles"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
"github.com/yuin/goldmark/parser"
|
"github.com/yuin/goldmark/parser"
|
||||||
ghtml "github.com/yuin/goldmark/renderer/html"
|
ghtml "github.com/yuin/goldmark/renderer/html"
|
||||||
@@ -116,7 +116,7 @@ func getCachedStyle(name string) *chroma.Style {
|
|||||||
style := styles.Get(name)
|
style := styles.Get(name)
|
||||||
if style == nil {
|
if style == nil {
|
||||||
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
|
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
|
||||||
style = styles.Fallback
|
style = styles.Fallback()
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheMutex.Lock()
|
cacheMutex.Lock()
|
||||||
@@ -209,12 +209,10 @@ func runChroma(cmd *cobra.Command, args []string) {
|
|||||||
md := goldmark.New(
|
md := goldmark.New(
|
||||||
goldmark.WithExtensions(
|
goldmark.WithExtensions(
|
||||||
extension.GFM,
|
extension.GFM,
|
||||||
highlighting.NewHighlighting(
|
highlight.Markdown{
|
||||||
highlighting.WithStyle(chromaStyle),
|
Style: styles.Get(chromaStyle),
|
||||||
highlighting.WithFormatOptions(
|
Formatter: html.New(html.WithClasses(!chromaInline)),
|
||||||
html.WithClasses(!chromaInline),
|
},
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
goldmark.WithParserOptions(
|
goldmark.WithParserOptions(
|
||||||
parser.WithAutoHeadingID(),
|
parser.WithAutoHeadingID(),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/dankgo/wlclipboard"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -283,8 +284,8 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
|
func parseMultiOffers(data []byte) ([]wlclipboard.Offer, error) {
|
||||||
var offers []clipboard.Offer
|
var offers []wlclipboard.Offer
|
||||||
pos := 0
|
pos := 0
|
||||||
|
|
||||||
for pos < len(data) {
|
for pos < len(data) {
|
||||||
@@ -311,14 +312,14 @@ func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
|
|||||||
offerData := data[pos : pos+dataLen]
|
offerData := data[pos : pos+dataLen]
|
||||||
pos += dataLen
|
pos += dataLen
|
||||||
|
|
||||||
offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData})
|
offers = append(offers, wlclipboard.Offer{MimeType: mimeType, Data: offerData})
|
||||||
}
|
}
|
||||||
|
|
||||||
return offers, nil
|
return offers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClipPaste(cmd *cobra.Command, args []string) {
|
func runClipPaste(cmd *cobra.Command, args []string) {
|
||||||
data, _, err := clipboard.Paste()
|
data, _, err := wlclipboard.Paste()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("paste: %v", err)
|
log.Fatalf("paste: %v", err)
|
||||||
}
|
}
|
||||||
@@ -344,13 +345,13 @@ func runClipWatch(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) > 0:
|
case len(args) > 0:
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
|
||||||
runCommand(args, data)
|
runCommand(args, data)
|
||||||
}); err != nil && err != context.Canceled {
|
}); err != nil && err != context.Canceled {
|
||||||
log.Fatalf("Watch error: %v", err)
|
log.Fatalf("Watch error: %v", err)
|
||||||
}
|
}
|
||||||
case clipWatchStore:
|
case clipWatchStore:
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
|
||||||
if err := clipboard.Store(data, mimeType); err != nil {
|
if err := clipboard.Store(data, mimeType); err != nil {
|
||||||
log.Errorf("store: %v", err)
|
log.Errorf("store: %v", err)
|
||||||
}
|
}
|
||||||
@@ -382,7 +383,7 @@ func runClipWatch(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Watch error: %v", err)
|
log.Fatalf("Watch error: %v", err)
|
||||||
}
|
}
|
||||||
case clipJSONOutput:
|
case clipJSONOutput:
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
|
||||||
out := map[string]any{
|
out := map[string]any{
|
||||||
"data": string(data),
|
"data": string(data),
|
||||||
"mimeType": mimeType,
|
"mimeType": mimeType,
|
||||||
@@ -395,7 +396,7 @@ func runClipWatch(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Watch error: %v", err)
|
log.Fatalf("Watch error: %v", err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
if err := wlclipboard.Watch(ctx, func(data []byte, mimeType string) {
|
||||||
os.Stdout.Write(data)
|
os.Stdout.Write(data)
|
||||||
os.Stdout.WriteString("\n")
|
os.Stdout.WriteString("\n")
|
||||||
}); err != nil && err != context.Canceled {
|
}); err != nil && err != context.Canceled {
|
||||||
@@ -931,7 +932,7 @@ func downloadToTempFile(rawURL string) (string, error) {
|
|||||||
var contentType string
|
var contentType string
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
for attempt := 0; attempt < 3; attempt++ {
|
for attempt := range 3 {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
|
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
||||||
@@ -10,10 +11,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
colorOutputFmt string
|
colorOutputFmt string
|
||||||
colorAutocopy bool
|
colorAutocopy bool
|
||||||
colorNotify bool
|
colorNotify bool
|
||||||
colorLowercase bool
|
colorLowercase bool
|
||||||
|
colorAllowMulti bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var colorCmd = &cobra.Command{
|
var colorCmd = &cobra.Command{
|
||||||
@@ -53,7 +55,8 @@ Examples:
|
|||||||
dms color pick --rgb # Output as RGB
|
dms color pick --rgb # Output as RGB
|
||||||
dms color pick --json # Output all formats as JSON
|
dms color pick --json # Output all formats as JSON
|
||||||
dms color pick --hex -l # Output hex in lowercase
|
dms color pick --hex -l # Output hex in lowercase
|
||||||
dms color pick -a # Auto-copy result to clipboard`,
|
dms color pick -a # Auto-copy result to clipboard
|
||||||
|
dms color pick --allow-multiple # Skip the one-picker-at-a-time guard`,
|
||||||
Run: runColorPick,
|
Run: runColorPick,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +71,7 @@ func init() {
|
|||||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
|
colorPickCmd.Flags().BoolVar(&colorAllowMulti, "allow-multiple", false, "Open a picker even when another selection overlay is already open")
|
||||||
|
|
||||||
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
|
colorPickCmd.MarkFlagsMutuallyExclusive("hex", "rgb", "hsl", "hsv", "cmyk", "json")
|
||||||
|
|
||||||
@@ -89,13 +93,18 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config := colorpicker.Config{
|
config := colorpicker.Config{
|
||||||
Format: format,
|
Format: format,
|
||||||
CustomFormat: colorOutputFmt,
|
CustomFormat: colorOutputFmt,
|
||||||
Lowercase: colorLowercase,
|
Lowercase: colorLowercase,
|
||||||
Autocopy: colorAutocopy,
|
Autocopy: colorAutocopy,
|
||||||
Notify: colorNotify,
|
Notify: colorNotify,
|
||||||
|
AllowMultiple: colorAllowMulti,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-shot process holding a few screen-sized buffers: skip GC cycles while it runs.
|
||||||
|
debug.SetGCPercent(-1)
|
||||||
|
debug.SetMemoryLimit(1 << 30)
|
||||||
|
|
||||||
picker := colorpicker.New(config)
|
picker := colorpicker.New(config)
|
||||||
color, err := picker.Run()
|
color, err := picker.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import (
|
|||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/plugins"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,63 +19,6 @@ var versionCmd = &cobra.Command{
|
|||||||
Run: runVersion,
|
Run: runVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
var runCmd = &cobra.Command{
|
|
||||||
Use: "run",
|
|
||||||
Short: "Launch quickshell with DMS configuration",
|
|
||||||
Long: "Launch quickshell with DMS configuration (qs -c dms)",
|
|
||||||
PreRunE: findConfig,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
daemon, _ := cmd.Flags().GetBool("daemon")
|
|
||||||
session, _ := cmd.Flags().GetBool("session")
|
|
||||||
if v, _ := cmd.Flags().GetString("log-level"); v != "" {
|
|
||||||
if err := os.Setenv("DMS_LOG_LEVEL", v); err != nil {
|
|
||||||
log.Fatalf("Failed to set DMS_LOG_LEVEL: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if v, _ := cmd.Flags().GetString("log-file"); v != "" {
|
|
||||||
if err := os.Setenv("DMS_LOG_FILE", v); err != nil {
|
|
||||||
log.Fatalf("Failed to set DMS_LOG_FILE: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.ApplyEnvOverrides()
|
|
||||||
config.CleanupStrayHyprlandConfFile(log.Infof)
|
|
||||||
if daemon {
|
|
||||||
runShellDaemon(session)
|
|
||||||
} else {
|
|
||||||
runShellInteractive(session)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var restartCmd = &cobra.Command{
|
|
||||||
Use: "restart",
|
|
||||||
Short: "Restart quickshell with DMS configuration",
|
|
||||||
Long: "Kill existing DMS shell processes and restart quickshell with DMS configuration",
|
|
||||||
PreRunE: findConfig,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
restartShell()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var restartDetachedCmd = &cobra.Command{
|
|
||||||
Use: "restart-detached <pid>",
|
|
||||||
Hidden: true,
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
PreRunE: findConfig,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
runDetachedRestart(args[0])
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var killCmd = &cobra.Command{
|
|
||||||
Use: "kill",
|
|
||||||
Short: "Kill running DMS shell processes",
|
|
||||||
Long: "Kill all running quickshell processes with DMS configuration",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
killShell()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc",
|
Use: "ipc",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
@@ -108,6 +51,8 @@ func init() {
|
|||||||
})
|
})
|
||||||
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
|
pluginsUpdateCmd.Flags().BoolP("all", "a", false, "Update all installed plugins")
|
||||||
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
|
pluginsUpdateCmd.Flags().Bool("check", false, "Check for available updates without applying them")
|
||||||
|
pluginsLockCmd.Flags().StringP("output", "o", "", "Also write the lockfile to this path")
|
||||||
|
pluginsRestoreCmd.Flags().Bool("prune", false, "Remove managed plugins that are not in the lockfile")
|
||||||
}
|
}
|
||||||
|
|
||||||
var debugSrvCmd = &cobra.Command{
|
var debugSrvCmd = &cobra.Command{
|
||||||
@@ -235,6 +180,36 @@ var pluginsUpdateCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pluginsLockCmd = &cobra.Command{
|
||||||
|
Use: "lock",
|
||||||
|
Short: "Record installed plugins and their exact revisions",
|
||||||
|
Long: "Write a portable plugins.lock.json containing every managed user plugin and its current Git commit.",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
output, _ := cmd.Flags().GetString("output")
|
||||||
|
if err := lockPluginsCLI(output); err != nil {
|
||||||
|
log.Fatalf("Error writing plugin lockfile: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginsRestoreCmd = &cobra.Command{
|
||||||
|
Use: "restore [lockfile]",
|
||||||
|
Short: "Restore plugins from exact revisions in a lockfile",
|
||||||
|
Long: "Install or reset all plugins in a portable plugins.lock.json. Existing managed plugins are retained unless --prune is specified.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
path := ""
|
||||||
|
if len(args) == 1 {
|
||||||
|
path = args[0]
|
||||||
|
}
|
||||||
|
prune, _ := cmd.Flags().GetBool("prune")
|
||||||
|
if err := restorePluginsCLI(path, prune); err != nil {
|
||||||
|
log.Fatalf("Error restoring plugins: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
fmt.Printf("%s\n", formatVersion(Version))
|
fmt.Printf("%s\n", formatVersion(Version))
|
||||||
}
|
}
|
||||||
@@ -264,29 +239,41 @@ func formatVersion(version string) string {
|
|||||||
return fmt.Sprintf("dms %s", version)
|
return fmt.Sprintf("dms %s", version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var baseVersionRe = regexp.MustCompile(`^([\d.]+)`)
|
||||||
|
|
||||||
|
// Installed UI trees, for builds without an embedded UI.
|
||||||
|
var shellVersionPaths = []string{
|
||||||
|
"/usr/share/quickshell/dms/VERSION",
|
||||||
|
"/usr/local/share/quickshell/dms/VERSION",
|
||||||
|
"/etc/xdg/quickshell/dms/VERSION",
|
||||||
|
}
|
||||||
|
|
||||||
func getBaseVersion() string {
|
func getBaseVersion() string {
|
||||||
paths := []string{
|
if ver := parseBaseVersion(shellembed.Version()); ver != "" {
|
||||||
"/usr/share/quickshell/dms/VERSION",
|
return ver
|
||||||
"/usr/local/share/quickshell/dms/VERSION",
|
|
||||||
"/etc/xdg/quickshell/dms/VERSION",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, path := range paths {
|
for _, path := range shellVersionPaths {
|
||||||
if content, err := os.ReadFile(path); err == nil {
|
content, err := os.ReadFile(path)
|
||||||
ver := strings.TrimSpace(string(content))
|
if err != nil {
|
||||||
ver = strings.TrimPrefix(ver, "v")
|
continue
|
||||||
if re := regexp.MustCompile(`^([\d.]+)`); re.MatchString(ver) {
|
}
|
||||||
if matches := re.FindStringSubmatch(ver); matches != nil {
|
if ver := parseBaseVersion(string(content)); ver != "" {
|
||||||
return matches[1]
|
return ver
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
|
||||||
return "1.0.2"
|
return "1.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseBaseVersion(raw string) string {
|
||||||
|
matches := baseVersionRe.FindStringSubmatch(strings.TrimPrefix(strings.TrimSpace(raw), "v"))
|
||||||
|
if matches == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
server.CLIVersion = Version
|
server.CLIVersion = Version
|
||||||
return server.Start(true)
|
return server.Start(true)
|
||||||
@@ -478,6 +465,40 @@ func installPluginCLI(idOrName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lockPluginsCLI(outputPath string) error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
warnings, err := manager.WriteCurrentLockfile(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, warning := range warnings {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: %s\n", warning)
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin lockfile written: %s\n", manager.GetLockfilePath())
|
||||||
|
if outputPath != "" && outputPath != manager.GetLockfilePath() {
|
||||||
|
fmt.Printf("Plugin lockfile exported: %s\n", outputPath)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restorePluginsCLI(path string, prune bool) error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
|
}
|
||||||
|
if err := manager.RestoreFromLockfile(path, prune); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if path == "" {
|
||||||
|
path = manager.GetLockfilePath()
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugins restored from lockfile: %s\n", path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func getAvailablePluginIDs() []string {
|
func getAvailablePluginIDs() []string {
|
||||||
registry, err := plugins.NewRegistry()
|
registry, err := plugins.NewRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -743,15 +764,13 @@ func checkAllPluginsCLI() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCommonCommands() []*cobra.Command {
|
func getCommonCommands() []*cobra.Command {
|
||||||
return []*cobra.Command{
|
commands := shellApp.Commands()
|
||||||
|
return append(commands, []*cobra.Command{
|
||||||
versionCmd,
|
versionCmd,
|
||||||
runCmd,
|
|
||||||
restartCmd,
|
|
||||||
restartDetachedCmd,
|
|
||||||
killCmd,
|
|
||||||
ipcCmd,
|
ipcCmd,
|
||||||
debugSrvCmd,
|
debugSrvCmd,
|
||||||
pluginsCmd,
|
pluginsCmd,
|
||||||
|
registryCmd,
|
||||||
dank16Cmd,
|
dank16Cmd,
|
||||||
brightnessCmd,
|
brightnessCmd,
|
||||||
dpmsCmd,
|
dpmsCmd,
|
||||||
@@ -759,6 +778,7 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
greeterCmd,
|
greeterCmd,
|
||||||
setupCmd,
|
setupCmd,
|
||||||
colorCmd,
|
colorCmd,
|
||||||
|
qrCmd,
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
notifyCmd,
|
notifyCmd,
|
||||||
@@ -774,5 +794,5 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
trashCmd,
|
trashCmd,
|
||||||
systemCmd,
|
systemCmd,
|
||||||
switchUserCmd,
|
switchUserCmd,
|
||||||
}
|
}...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ var resolveIncludeCmd = &cobra.Command{
|
|||||||
"layout.kdl",
|
"layout.kdl",
|
||||||
"outputs.kdl",
|
"outputs.kdl",
|
||||||
"binds.kdl",
|
"binds.kdl",
|
||||||
|
"input.kdl",
|
||||||
"cursor.conf",
|
"cursor.conf",
|
||||||
"layout.conf",
|
"layout.conf",
|
||||||
"outputs.conf",
|
"outputs.conf",
|
||||||
@@ -149,9 +150,9 @@ func hyprlandFindIncludeHyprlang(filePath, target string, processed map[string]b
|
|||||||
}
|
}
|
||||||
|
|
||||||
baseDir := filepath.Dir(absPath)
|
baseDir := filepath.Dir(absPath)
|
||||||
lines := strings.Split(string(data), "\n")
|
lines := strings.SplitSeq(string(data), "\n")
|
||||||
|
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
continue
|
continue
|
||||||
@@ -228,7 +229,7 @@ func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
|||||||
baseDir := filepath.Dir(absPath)
|
baseDir := filepath.Dir(absPath)
|
||||||
content := string(data)
|
content := string(data)
|
||||||
|
|
||||||
for _, line := range strings.Split(content, "\n") {
|
for line := range strings.SplitSeq(content, "\n") {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
||||||
continue
|
continue
|
||||||
@@ -305,9 +306,9 @@ func mangowcFindInclude(filePath, target string, processed map[string]bool) bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
baseDir := filepath.Dir(absPath)
|
baseDir := filepath.Dir(absPath)
|
||||||
lines := strings.Split(string(data), "\n")
|
lines := strings.SplitSeq(string(data), "\n")
|
||||||
|
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -79,11 +79,11 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
primaryLight = primaryColor
|
primaryLight = primaryColor
|
||||||
}
|
}
|
||||||
variantOpts := dank16.VariantOptions{
|
variantOpts := dank16.VariantOptions{
|
||||||
PrimaryDark: primaryDark,
|
PrimaryDark: primaryDark,
|
||||||
PrimaryLight: primaryLight,
|
PrimaryLight: primaryLight,
|
||||||
Background: background,
|
BackgroundDark: background,
|
||||||
UseDPS: contrastAlgo == "dps",
|
UseDPS: contrastAlgo == "dps",
|
||||||
IsLightMode: isLight,
|
IsLightMode: isLight,
|
||||||
}
|
}
|
||||||
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
variantColors := dank16.GenerateVariantPalette(variantOpts)
|
||||||
fmt.Print(dank16.GenerateVariantJSON(variantColors))
|
fmt.Print(dank16.GenerateVariantJSON(variantColors))
|
||||||
|
|||||||
+546
-111
@@ -2,7 +2,9 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -15,6 +17,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/tui"
|
||||||
@@ -92,6 +95,8 @@ var (
|
|||||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
||||||
|
scrollVersionRegex = regexp.MustCompile(`scroll version (\d+\.\d+)`)
|
||||||
|
aqueousVersionRegex = regexp.MustCompile(`(?i)aqueous v?(\d+\.\d+(?:\.\d+)?)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -319,7 +324,16 @@ func checkSystemInfo() []checkResult {
|
|||||||
func checkEnvironmentVars() []checkResult {
|
func checkEnvironmentVars() []checkResult {
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
|
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME")...)
|
||||||
|
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")...)
|
||||||
|
results = append(results, checkEnvVar("QT_QPA_PLATFORMTHEME_QT6")...)
|
||||||
|
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") != os.Getenv("QT_QPA_PLATFORMTHEME") {
|
||||||
|
results = append(results, checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")...)
|
||||||
|
}
|
||||||
results = append(results, checkEnvVar("QS_ICON_THEME")...)
|
results = append(results, checkEnvVar("QS_ICON_THEME")...)
|
||||||
|
results = append(results, checkXDGMenuPrefix()...)
|
||||||
|
if matugen.QtengineActive() {
|
||||||
|
results = append(results, checkQtenginePlugin()...)
|
||||||
|
}
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +348,251 @@ func checkEnvVar(name string) []checkResult {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkXDGMenuPrefix() []checkResult {
|
||||||
|
menuPrefix := os.Getenv("XDG_MENU_PREFIX")
|
||||||
|
if menuPrefix != "" {
|
||||||
|
if checkXDGMenuFile(menuPrefix) {
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, menuPrefix, "", doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, fmt.Sprintf("%s (menu file not found)", menuPrefix), fmt.Sprintf("Dolphin 'Open with…' dialog may be empty. Ensure /etc/xdg/menus/%sapplications.menu exists.", menuPrefix), doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("keditfiletype"); err == nil {
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusWarn, "Not set", "Dolphin file associations and 'Open with…' dialog may be empty. Set XDG_MENU_PREFIX=plasma- in your compositor's environment block.", doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
if doctorVerbose {
|
||||||
|
return []checkResult{{catEnvironment, "XDG_MENU_PREFIX", statusInfo, "Not set", "", doctorDocsURL + "#xdg-menu-prefix"}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtQueryBinaries are the Qt build tools that can be asked where Qt looks for
|
||||||
|
// plugins, in probe order. A binary's name does not reliably say which Qt it
|
||||||
|
// belongs to — plain "qmake" is Qt5 on Arch and Qt6 elsewhere — so each one is
|
||||||
|
// asked for QT_VERSION and the major is taken from the answer.
|
||||||
|
var qtQueryBinaries = []struct{ bin, flag string }{
|
||||||
|
{"qmake6", "-query"},
|
||||||
|
{"qmake-qt6", "-query"},
|
||||||
|
{"qtpaths6", "--query"},
|
||||||
|
{"qtpaths-qt6", "--query"},
|
||||||
|
{"qmake-qt5", "-query"},
|
||||||
|
{"qtpaths-qt5", "--query"},
|
||||||
|
{"qmake", "-query"},
|
||||||
|
{"qtpaths", "--query"},
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkQtenginePlugin verifies the qtengine platform theme plugin is installed
|
||||||
|
// where Qt actually searches, for every Qt major that can be interrogated.
|
||||||
|
//
|
||||||
|
// Qt is asked where it looks rather than the filesystem searched, because a
|
||||||
|
// package can install the Qt5 plugin under a prefix Qt5 does not search, after
|
||||||
|
// which Qt5 apps silently fall back while Qt6 apps look correct. A search would
|
||||||
|
// find the plugin and report OK on exactly that bug.
|
||||||
|
func checkQtenginePlugin() []checkResult {
|
||||||
|
url := doctorDocsURL + "#environment-variables"
|
||||||
|
|
||||||
|
pluginRoots := qtPluginRoots()
|
||||||
|
|
||||||
|
// qmake/qtpaths ship in dev packages most users lack — never warn on a Qt
|
||||||
|
// that could not be interrogated, and never claim it is fine either.
|
||||||
|
if len(pluginRoots) == 0 {
|
||||||
|
return []checkResult{{
|
||||||
|
catEnvironment, "qtengine plugin", statusInfo, "Cannot verify (no qmake/qtpaths)",
|
||||||
|
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.",
|
||||||
|
url,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable.
|
||||||
|
envRoots := filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
for _, major := range slices.Sorted(maps.Keys(pluginRoots)) {
|
||||||
|
// qtengine only ships Qt5 and Qt6 plugins.
|
||||||
|
if major != "5" && major != "6" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
root := pluginRoots[major]
|
||||||
|
name := fmt.Sprintf("qtengine plugin (Qt%s)", major)
|
||||||
|
|
||||||
|
plugin := qtenginePluginPath(append([]string{root}, envRoots...), major)
|
||||||
|
if plugin == "" {
|
||||||
|
expected := qtenginePluginFile(root, major)
|
||||||
|
results = append(results, checkResult{
|
||||||
|
catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major),
|
||||||
|
fmt.Sprintf("Expected %s — Qt%s apps will silently fall back to an unthemed palette. The plugin is likely installed under a prefix Qt%s does not search.", expected, major, major),
|
||||||
|
url,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catEnvironment, name, statusOK, "Installed", plugin, url})
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkQtPlatformThemePlugin validates one of the QT_QPA_PLATFORMTHEME
|
||||||
|
// variables beyond mere presence: the value must name a platform theme whose
|
||||||
|
// plugin Qt can actually load. A wrong value fails silently at run time — Qt
|
||||||
|
// apps fall back to a built-in theme, icon lookups return nothing, and
|
||||||
|
// Quickshell tray menus draw a placeholder for every icon an app publishes.
|
||||||
|
func checkQtPlatformThemePlugin(varName string) []checkResult {
|
||||||
|
value := os.Getenv(varName)
|
||||||
|
// Unset is reported by checkEnvVar; qtengine has its own plugin check.
|
||||||
|
if value == "" || value == "qtengine" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url := doctorDocsURL + "#environment-variables"
|
||||||
|
|
||||||
|
// qt6ct-kde is the AUR package name, not the theme value: with it set,
|
||||||
|
// Qt finds no platform theme at all and every Qt app falls back silently.
|
||||||
|
if value == "qt6ct-kde" {
|
||||||
|
return []checkResult{{
|
||||||
|
catEnvironment, value, statusWarn, "Package name, not a theme",
|
||||||
|
fmt.Sprintf("qt6ct-kde is the AUR package name; the theme value Qt expects is qt6ct. Qt apps get no platform theme, so palettes stay unstyled and icon lookups fail. Set %s=qt6ct, then restart the shell.", varName),
|
||||||
|
url,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DMS themes Qt6 through these variables; qt5ct is the Qt5 counterpart.
|
||||||
|
major := "6"
|
||||||
|
if value == "qt5ct" {
|
||||||
|
major = "5"
|
||||||
|
}
|
||||||
|
files := qtPlatformThemePluginFiles(value)
|
||||||
|
name := fmt.Sprintf("%s plugin (Qt%s)", value, major)
|
||||||
|
|
||||||
|
root := qtPluginRoots()[major]
|
||||||
|
// qmake/qtpaths ship in dev packages most users lack — never warn on a Qt
|
||||||
|
// that could not be interrogated, and never claim it is fine either.
|
||||||
|
if root == "" {
|
||||||
|
return []checkResult{{
|
||||||
|
catEnvironment, name, statusInfo, "Cannot verify (no qmake/qtpaths)",
|
||||||
|
"Install a Qt development package to let dms doctor check the plugin against the path Qt actually searches.",
|
||||||
|
url,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Qt also searches QT_PLUGIN_PATH, so a plugin found there is loadable.
|
||||||
|
plugin := qtPluginPath(append([]string{root}, filepath.SplitList(os.Getenv("QT_PLUGIN_PATH"))...), files...)
|
||||||
|
if plugin == "" {
|
||||||
|
// plasma-integration ships the kde theme under a file name that has
|
||||||
|
// changed across versions; a miss says nothing reliable about the
|
||||||
|
// install, so degrade to info instead of warning at a working setup.
|
||||||
|
if value == "kde" {
|
||||||
|
return []checkResult{{
|
||||||
|
catEnvironment, name, statusInfo, "Cannot verify (plasma-integration)",
|
||||||
|
"The kde theme comes from plasma-integration, whose plugin file name varies across versions. Verify visually that Qt apps follow your theme.",
|
||||||
|
url,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
expected := filepath.Join(root, "platformthemes", files[0])
|
||||||
|
return []checkResult{{
|
||||||
|
catEnvironment, name, statusWarn, fmt.Sprintf("Not found in Qt%s plugin path", major),
|
||||||
|
fmt.Sprintf("Expected %s — Qt apps silently fall back to a built-in theme, breaking icon lookups and palettes. Install the package providing it, or correct %s.", expected, varName),
|
||||||
|
url,
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
return []checkResult{{catEnvironment, name, statusOK, "Installed", plugin, url}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtPlatformThemePluginFiles lists the plugin file names that can provide a
|
||||||
|
// QT_QPA_PLATFORMTHEME value, most conventional first. Qt resolves the value
|
||||||
|
// through plugin metadata keys rather than file names, so these are the names
|
||||||
|
// each integration is known to ship; unmapped values fall back to Qt's
|
||||||
|
// libq<value> naming convention.
|
||||||
|
func qtPlatformThemePluginFiles(value string) []string {
|
||||||
|
if files, ok := map[string][]string{
|
||||||
|
"qt6ct": {"libqt6ct.so"},
|
||||||
|
"qt5ct": {"libqt5ct.so"},
|
||||||
|
"gtk3": {"libqgtk3.so"},
|
||||||
|
"kde": {"libqkdeplatformtheme.so"},
|
||||||
|
}[value]; ok {
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
return []string{"libq" + value + ".so", "lib" + value + ".so"}
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtPluginRoots asks every available Qt build tool where its Qt looks for
|
||||||
|
// plugins, keyed by Qt major. A binary's name does not reliably say which Qt
|
||||||
|
// it belongs to — plain "qmake" is Qt5 on Arch and Qt6 elsewhere — so each one
|
||||||
|
// is asked for QT_VERSION and the major is taken from the answer.
|
||||||
|
func qtPluginRoots() map[string]string {
|
||||||
|
pluginRoots := make(map[string]string)
|
||||||
|
for _, q := range qtQueryBinaries {
|
||||||
|
if _, err := exec.LookPath(q.bin); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
major, _, _ := strings.Cut(qtQuery(q.bin, q.flag, "QT_VERSION"), ".")
|
||||||
|
if major == "" || pluginRoots[major] != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if root := qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"); root != "" {
|
||||||
|
pluginRoots[major] = root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pluginRoots
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtPluginPath returns the first of roots that holds any of files under
|
||||||
|
// platformthemes/, or "" if none does.
|
||||||
|
func qtPluginPath(roots []string, files ...string) string {
|
||||||
|
for _, root := range roots {
|
||||||
|
if root == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
path := filepath.Join(root, "platformthemes", file)
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtenginePluginPath returns the first of roots that holds the qtengine platform
|
||||||
|
// theme plugin for the given Qt major, or "" if none does.
|
||||||
|
func qtenginePluginPath(roots []string, major string) string {
|
||||||
|
return qtPluginPath(roots, fmt.Sprintf("libqt%sengine-plugin.so", major))
|
||||||
|
}
|
||||||
|
|
||||||
|
func qtenginePluginFile(root, major string) string {
|
||||||
|
return filepath.Join(root, "platformthemes", fmt.Sprintf("libqt%sengine-plugin.so", major))
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtQuery asks a Qt build tool for a single QLibraryInfo property.
|
||||||
|
func qtQuery(bin, flag, key string) string {
|
||||||
|
output, err := exec.Command(bin, flag, key).Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return qtQueryValue(string(output), key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtQueryValue extracts one property from a Qt query. Most tools print the bare
|
||||||
|
// value, but some ignore the requested property and dump every one as
|
||||||
|
// "KEY:value" lines; taking the whole output there would yield a bogus path.
|
||||||
|
func qtQueryValue(output, key string) string {
|
||||||
|
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if value, found := strings.CutPrefix(strings.TrimSpace(line), key+":"); found {
|
||||||
|
return strings.TrimSpace(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(lines) == 1 {
|
||||||
|
return strings.TrimSpace(lines[0])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkXDGMenuFile(prefix string) bool {
|
||||||
|
menuPath := fmt.Sprintf("/etc/xdg/menus/%sapplications.menu", prefix)
|
||||||
|
_, err := os.Stat(menuPath)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
func readOSRelease() map[string]string {
|
func readOSRelease() map[string]string {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
data, err := os.ReadFile("/etc/os-release")
|
data, err := os.ReadFile("/etc/os-release")
|
||||||
@@ -359,9 +618,8 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
|
|||||||
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
|
{catVersions, "DMS CLI", statusOK, formatVersion(Version), dmsCliDetails, doctorDocsURL + "#dms-cli"},
|
||||||
}
|
}
|
||||||
|
|
||||||
qsVersion, qsStatus, qsPath := getQuickshellVersionInfo(qsMissingFeatures)
|
qsVersion, qsStatus, qsPath, qsDetails := getQuickshellVersionInfo(qsMissingFeatures)
|
||||||
qsDetails := ""
|
if doctorVerbose && qsPath != "" && qsDetails == "" {
|
||||||
if doctorVerbose && qsPath != "" {
|
|
||||||
qsDetails = qsPath
|
qsDetails = qsPath
|
||||||
}
|
}
|
||||||
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
|
results = append(results, checkResult{catVersions, "Quickshell", qsStatus, qsVersion, qsDetails, doctorDocsURL + "#quickshell"})
|
||||||
@@ -377,12 +635,12 @@ func checkVersions(qsMissingFeatures bool) []checkResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDMSShellVersion() (version, path string) {
|
func getDMSShellVersion() (version, path string) {
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
||||||
versionFile := filepath.Join(configPath, "VERSION")
|
versionFile := filepath.Join(shellApp.ConfigPath(), "VERSION")
|
||||||
if data, err := os.ReadFile(versionFile); err == nil {
|
if data, err := os.ReadFile(versionFile); err == nil {
|
||||||
return strings.TrimSpace(string(data)), configPath
|
return strings.TrimSpace(string(data)), shellApp.ConfigPath()
|
||||||
}
|
}
|
||||||
return "installed", configPath
|
return "installed", shellApp.ConfigPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
if dmsPath, err := config.LocateDMSConfig(); err == nil {
|
||||||
@@ -396,41 +654,49 @@ func getDMSShellVersion() (version, path string) {
|
|||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string) {
|
func getQuickshellVersionInfo(missingFeatures bool) (string, status, string, string) {
|
||||||
if !utils.CommandExists("qs") {
|
if !utils.CommandExists("qs") {
|
||||||
return "Not installed", statusError, ""
|
return "Not installed", statusError, "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
qsPath, _ := exec.LookPath("qs")
|
qsPath, _ := exec.LookPath("qs")
|
||||||
|
|
||||||
output, err := exec.Command("qs", "--version").Output()
|
output, err := exec.Command("qs", "--version").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "Installed (version check failed)", statusWarn, qsPath
|
details := "Run 'qs --version' to inspect the failure."
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
details = quickshellVersionFailureDetails(string(exitErr.Stderr))
|
||||||
|
}
|
||||||
|
return "Installed (version check failed)", statusWarn, qsPath, details
|
||||||
}
|
}
|
||||||
|
|
||||||
fullVersion := strings.TrimSpace(string(output))
|
fullVersion := strings.TrimSpace(string(output))
|
||||||
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
|
if matches := quickshellVersionRegex.FindStringSubmatch(fullVersion); len(matches) >= 2 {
|
||||||
if version.CompareVersions(matches[1], "0.2.0") < 0 {
|
if version.CompareVersions(matches[1], "0.2.0") < 0 {
|
||||||
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath
|
return fmt.Sprintf("%s (needs >= 0.2.0)", fullVersion), statusError, qsPath, ""
|
||||||
}
|
}
|
||||||
if missingFeatures {
|
if missingFeatures {
|
||||||
return fullVersion, statusWarn, qsPath
|
return fullVersion, statusWarn, qsPath, ""
|
||||||
}
|
}
|
||||||
return fullVersion, statusOK, qsPath
|
return fullVersion, statusOK, qsPath, ""
|
||||||
}
|
}
|
||||||
|
|
||||||
return fullVersion, statusWarn, qsPath
|
return fullVersion, statusWarn, qsPath, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func quickshellVersionFailureDetails(stderr string) string {
|
||||||
|
if strings.Contains(stderr, "undefined symbol:") && strings.Contains(stderr, "Qt_6_PRIVATE_API") {
|
||||||
|
return "Quickshell is incompatible with the installed Qt libraries. Rebuild or reinstall Quickshell against the current Qt version."
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Run 'qs --version' to inspect the failure."
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkDMSInstallation() []checkResult {
|
func checkDMSInstallation() []checkResult {
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
|
|
||||||
dmsPath := ""
|
dmsPath := resolveDoctorShellPath()
|
||||||
if err := findConfig(nil, nil); err == nil && configPath != "" {
|
|
||||||
dmsPath = configPath
|
|
||||||
} else if path, err := config.LocateDMSConfig(); err == nil {
|
|
||||||
dmsPath = path
|
|
||||||
}
|
|
||||||
|
|
||||||
if dmsPath == "" {
|
if dmsPath == "" {
|
||||||
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
|
return []checkResult{{catInstallation, "DMS Configuration", statusError, "Not found", "shell.qml not found in any config path", doctorDocsURL + "#dms-configuration"}}
|
||||||
@@ -475,6 +741,8 @@ func checkWindowManagers() []checkResult {
|
|||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
||||||
|
{"Scroll", "scroll", "--version", scrollVersionRegex, []string{"scroll"}},
|
||||||
|
{"Aqueous", "aqueous", "-version", aqueousVersionRegex, []string{"aqueous"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -507,7 +775,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
"Install Hyprland, niri, Sway, River, Wayfire, labwc, mangowc, miracle-wm, Scroll, or Aqueous",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -516,11 +784,28 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("AQUEOUS_SOCKET") != "" {
|
||||||
|
results = append(results, checkAqueousConfigHelper())
|
||||||
|
}
|
||||||
|
|
||||||
results = append(results, checkCompositorBlurSupport())
|
results = append(results, checkCompositorBlurSupport())
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkAqueousConfigHelper() checkResult {
|
||||||
|
url := doctorDocsURL + "#compositor-checks"
|
||||||
|
path, err := exec.LookPath("aqueous-config")
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{catCompositor, "aqueous-config", statusWarn, "Not found", "Keybind and display settings edits from the shell need the aqueous-config helper", url}
|
||||||
|
}
|
||||||
|
details := "Keybind and display settings edits from the shell"
|
||||||
|
if doctorVerbose {
|
||||||
|
details = path
|
||||||
|
}
|
||||||
|
return checkResult{catCompositor, "aqueous-config", statusOK, "Installed", details, url}
|
||||||
|
}
|
||||||
|
|
||||||
func checkCompositorBlurSupport() checkResult {
|
func checkCompositorBlurSupport() checkResult {
|
||||||
supported, err := blur.ProbeSupport()
|
supported, err := blur.ProbeSupport()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -553,6 +838,8 @@ func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
|||||||
|
|
||||||
func detectRunningWM() string {
|
func detectRunningWM() string {
|
||||||
switch {
|
switch {
|
||||||
|
case os.Getenv("AQUEOUS_SOCKET") != "":
|
||||||
|
return "Aqueous"
|
||||||
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
|
case os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "":
|
||||||
return "Hyprland"
|
return "Hyprland"
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
@@ -647,7 +934,7 @@ ShellRoot {
|
|||||||
{"Polkit", "Authentication prompts"},
|
{"Polkit", "Authentication prompts"},
|
||||||
{"IdleMonitor", "Idle detection"},
|
{"IdleMonitor", "Idle detection"},
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri, Aqueous)"},
|
||||||
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -770,17 +1057,15 @@ func findQtPluginDirs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
||||||
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
for _, dir := range filepath.SplitList(os.Getenv("QT_PLUGIN_PATH")) {
|
||||||
for dir := range strings.SplitSeq(envPath, ":") {
|
addDir(dir)
|
||||||
addDir(dir)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try qtpaths
|
for _, q := range qtQueryBinaries {
|
||||||
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
if _, err := exec.LookPath(q.bin); err != nil {
|
||||||
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
continue
|
||||||
addDir(strings.TrimSpace(string(output)))
|
|
||||||
}
|
}
|
||||||
|
addDir(qtQuery(q.bin, q.flag, "QT_INSTALL_PLUGINS"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: common distro paths
|
// Fallback: common distro paths
|
||||||
@@ -809,6 +1094,8 @@ func detectNetworkBackend(stackResult *network.DetectResult) string {
|
|||||||
return "systemd-networkd"
|
return "systemd-networkd"
|
||||||
case network.BackendConnMan:
|
case network.BackendConnMan:
|
||||||
return "ConnMan"
|
return "ConnMan"
|
||||||
|
case network.BackendWpaSupplicant:
|
||||||
|
return "wpa_supplicant"
|
||||||
default:
|
default:
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -882,10 +1169,10 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
important bool
|
important bool
|
||||||
}{
|
}{
|
||||||
{"matugen", "matugen", "Dynamic theming", true},
|
{"matugen", "matugen", "Dynamic theming", true},
|
||||||
{"dgop", "dgop", "System monitoring", true},
|
|
||||||
{"cava", "cava", "Audio visualizer", true},
|
{"cava", "cava", "Audio visualizer", true},
|
||||||
{"khal", "khal", "Calendar events", false},
|
{"khal", "khal", "Calendar events", false},
|
||||||
{"danksearch", "dsearch", "File search", false},
|
{"danksearch", "dsearch", "File search", false},
|
||||||
|
{"dankcalendar", "dcal", "Calendar app", false},
|
||||||
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
{"fprintd", "fprintd-list", "Fingerprint auth", false},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,9 +1189,62 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results = append(results, checkAdwGtk3())
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mirrors the search in quickshell/scripts/gtk.sh: user theme dirs, then XDG_DATA_DIRS.
|
||||||
|
func checkAdwGtk3() checkResult {
|
||||||
|
optionalFeaturesURL := doctorDocsURL + "#optional-features"
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
dataHome := os.Getenv("XDG_DATA_HOME")
|
||||||
|
if dataHome == "" {
|
||||||
|
dataHome = filepath.Join(home, ".local", "share")
|
||||||
|
}
|
||||||
|
userRoots := []string{filepath.Join(dataHome, "themes"), filepath.Join(home, ".themes")}
|
||||||
|
|
||||||
|
dataDirs := os.Getenv("XDG_DATA_DIRS")
|
||||||
|
if dataDirs == "" {
|
||||||
|
dataDirs = "/usr/local/share:/usr/share"
|
||||||
|
}
|
||||||
|
var systemRoots []string
|
||||||
|
for _, d := range strings.Split(dataDirs, ":") {
|
||||||
|
if d != "" {
|
||||||
|
systemRoots = append(systemRoots, filepath.Join(d, "themes"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locate := func(roots []string, variant string) bool {
|
||||||
|
for _, root := range roots {
|
||||||
|
if info, err := os.Stat(filepath.Join(root, "adw-gtk3"+variant, "gtk-3.0")); err == nil && info.IsDir() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
userLight := locate(userRoots, "")
|
||||||
|
userDark := locate(userRoots, "-dark")
|
||||||
|
sysLight := locate(systemRoots, "")
|
||||||
|
sysDark := locate(systemRoots, "-dark")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case userLight && userDark:
|
||||||
|
return checkResult{catOptionalFeatures, "adw-gtk3", statusOK, "Installed", "GTK3 dynamic theming (user copy)", optionalFeaturesURL}
|
||||||
|
case sysLight && sysDark:
|
||||||
|
return checkResult{catOptionalFeatures, "adw-gtk3", statusOK, "Installed", "GTK3 dynamic theming (system copy, applied on next theme apply)", optionalFeaturesURL}
|
||||||
|
case userLight || userDark || sysLight || sysDark:
|
||||||
|
missing := "adw-gtk3-dark"
|
||||||
|
if userDark || sysDark {
|
||||||
|
missing = "adw-gtk3"
|
||||||
|
}
|
||||||
|
return checkResult{catOptionalFeatures, "adw-gtk3", statusWarn, "Missing " + missing, "GTK3 dynamic theming needs both variants", optionalFeaturesURL}
|
||||||
|
default:
|
||||||
|
return checkResult{catOptionalFeatures, "adw-gtk3", statusInfo, "Not installed", "GTK3 dynamic theming; without it the global CSS override applies", optionalFeaturesURL}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkConfigurationFiles() []checkResult {
|
func checkConfigurationFiles() []checkResult {
|
||||||
configDir, _ := os.UserConfigDir()
|
configDir, _ := os.UserConfigDir()
|
||||||
cacheDir, _ := os.UserCacheDir()
|
cacheDir, _ := os.UserCacheDir()
|
||||||
@@ -932,6 +1272,11 @@ func checkConfigurationFiles() []checkResult {
|
|||||||
status = statusWarn
|
status = statusWarn
|
||||||
message += " (read-only)"
|
message += " (read-only)"
|
||||||
}
|
}
|
||||||
|
// The shell falls back to defaults on a parse failure, so a broken file must not report ok.
|
||||||
|
if data, readErr := os.ReadFile(cf.path); readErr == nil && len(data) > 0 && !json.Valid(data) {
|
||||||
|
status = statusError
|
||||||
|
message = "Invalid JSON - the shell ignores this file and runs on defaults"
|
||||||
|
}
|
||||||
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
|
results = append(results, checkResult{catConfigFiles, cf.name, status, message, cf.path, doctorDocsURL + "#config-files"})
|
||||||
}
|
}
|
||||||
return results
|
return results
|
||||||
@@ -964,6 +1309,13 @@ func checkSystemdServices() []checkResult {
|
|||||||
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
|
results = append(results, checkResult{catServices, "dms.service", status, message, "", doctorDocsURL + "#services"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dmsState.exists && dmsState.enabled == "enabled" {
|
||||||
|
gsState := getServiceState("graphical-session.target", true)
|
||||||
|
if gsState.exists && gsState.active != "active" {
|
||||||
|
results = append(results, checkResult{catServices, "graphical-session.target", statusWarn, "Inactive", "Compositor session never activated it, so dms.service cannot autostart. On Hyprland, hyprland-session.target must exist and be started by the compositor.", doctorDocsURL + "#services"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
greetdState := getServiceState("greetd", false)
|
greetdState := getServiceState("greetd", false)
|
||||||
switch {
|
switch {
|
||||||
case greetdState.exists:
|
case greetdState.exists:
|
||||||
@@ -1142,99 +1494,182 @@ func formatResultsPlain(results []checkResult) string {
|
|||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDoctorFontFamily = "Inter Variable"
|
||||||
|
defaultDoctorMonoFontFamily = "Fira Code"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bundledFontRelPaths maps settings/default family names to font files shipped with
|
||||||
|
// the shell and loaded via Qt FontLoader (not registered with fontconfig).
|
||||||
|
var bundledFontRelPaths = map[string][]string{
|
||||||
|
"inter variable": {
|
||||||
|
"DankCommon/assets/fonts/inter/InterVariable.ttf",
|
||||||
|
"assets/fonts/inter/InterVariable.ttf",
|
||||||
|
},
|
||||||
|
"fira code": {
|
||||||
|
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
|
||||||
|
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
|
||||||
|
},
|
||||||
|
"firacode nerd font": {
|
||||||
|
"DankCommon/assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
|
||||||
|
"assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDoctorShellPath() string {
|
||||||
|
if err := shellApp.ResolveConfig(nil, nil); err == nil && shellApp.ConfigPath() != "" {
|
||||||
|
return shellApp.ConfigPath()
|
||||||
|
}
|
||||||
|
if path, err := config.LocateDMSConfig(); err == nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBundledFontFile(shellPath, family string) string {
|
||||||
|
if shellPath == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
relPaths, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
for _, rel := range relPaths {
|
||||||
|
path := filepath.Join(shellPath, rel)
|
||||||
|
if info, err := os.Stat(path); err == nil && !info.IsDir() {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBundledDefaultFont(family string) bool {
|
||||||
|
_, ok := bundledFontRelPaths[strings.ToLower(strings.TrimSpace(family))]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func fontInFcList(name, cacheLower string) bool {
|
||||||
|
target := strings.ToLower(strings.TrimSpace(name))
|
||||||
|
if target == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for line := range strings.SplitSeq(cacheLower, "\n") {
|
||||||
|
for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") {
|
||||||
|
if strings.TrimSpace(fam) == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkConfiguredFont(label, family, shellPath, fcCache string, fcListAvailable bool, url string) checkResult {
|
||||||
|
if bundled := findBundledFontFile(shellPath, family); bundled != "" {
|
||||||
|
details := "Bundled (Qt FontLoader)"
|
||||||
|
if doctorVerbose {
|
||||||
|
details = bundled
|
||||||
|
}
|
||||||
|
return checkResult{catFonts, label, statusOK, family, details, url}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isBundledDefaultFont(family) {
|
||||||
|
if shellPath == "" {
|
||||||
|
return checkResult{
|
||||||
|
catFonts, label, statusWarn,
|
||||||
|
fmt.Sprintf("'%s' not verified", family),
|
||||||
|
"Could not locate shell config to verify bundled font files.",
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return checkResult{
|
||||||
|
catFonts, label, statusWarn,
|
||||||
|
fmt.Sprintf("'%s' bundled file missing", family),
|
||||||
|
"Expected font file missing from shell install. Reinstall DMS or check DankCommon assets.",
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !fcListAvailable {
|
||||||
|
return checkResult{
|
||||||
|
catFonts, label, statusWarn,
|
||||||
|
fmt.Sprintf("'%s' not verified", family),
|
||||||
|
"fc-list not installed; cannot verify custom fonts in fontconfig.",
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fcCache == "" {
|
||||||
|
return checkResult{
|
||||||
|
catFonts, label, statusWarn,
|
||||||
|
fmt.Sprintf("'%s' not found", family),
|
||||||
|
"Fontconfig cache is empty or unreadable. Try running 'fc-cache -fv'.",
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fontInFcList(family, fcCache) {
|
||||||
|
return checkResult{catFonts, label, statusOK, family, "Available via fontconfig", url}
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkResult{
|
||||||
|
catFonts, label, statusWarn,
|
||||||
|
fmt.Sprintf("'%s' not found", family),
|
||||||
|
"Font is not registered with fontconfig. Try running 'fc-cache -fv' or install the font.",
|
||||||
|
url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func checkFonts() []checkResult {
|
func checkFonts() []checkResult {
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
url := doctorDocsURL + "#fonts"
|
url := doctorDocsURL + "#fonts"
|
||||||
|
|
||||||
configDir, err := os.UserConfigDir()
|
fontFamily := defaultDoctorFontFamily
|
||||||
if err != nil {
|
monoFontFamily := defaultDoctorMonoFontFamily
|
||||||
return nil
|
|
||||||
}
|
|
||||||
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
|
|
||||||
|
|
||||||
fontFamily := "Inter Variable"
|
if configDir, err := os.UserConfigDir(); err == nil {
|
||||||
monoFontFamily := "Fira Code"
|
settingsPath := filepath.Join(configDir, "DankMaterialShell", "settings.json")
|
||||||
|
if data, err := os.ReadFile(settingsPath); err == nil {
|
||||||
if data, err := os.ReadFile(settingsPath); err == nil {
|
var settings struct {
|
||||||
var settings struct {
|
FontFamily string `json:"fontFamily"`
|
||||||
FontFamily string `json:"fontFamily"`
|
MonoFontFamily string `json:"monoFontFamily"`
|
||||||
MonoFontFamily string `json:"monoFontFamily"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(data, &settings); err == nil {
|
|
||||||
if settings.FontFamily != "" {
|
|
||||||
fontFamily = settings.FontFamily
|
|
||||||
}
|
}
|
||||||
if settings.MonoFontFamily != "" {
|
if err := json.Unmarshal(data, &settings); err == nil {
|
||||||
monoFontFamily = settings.MonoFontFamily
|
if settings.FontFamily != "" {
|
||||||
}
|
fontFamily = settings.FontFamily
|
||||||
}
|
}
|
||||||
}
|
if settings.MonoFontFamily != "" {
|
||||||
|
monoFontFamily = settings.MonoFontFamily
|
||||||
if !utils.CommandExists("fc-list") {
|
|
||||||
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify if fonts are cached.", url})
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve font list
|
|
||||||
output, err := exec.Command("fc-list", ":", "family").Output()
|
|
||||||
if err != nil {
|
|
||||||
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
outStr := string(output)
|
|
||||||
if len(strings.TrimSpace(outStr)) == 0 {
|
|
||||||
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
lowerFonts := strings.ToLower(outStr)
|
|
||||||
|
|
||||||
// Helper to check if a font exists
|
|
||||||
hasFont := func(name string) bool {
|
|
||||||
target := strings.ToLower(strings.TrimSpace(name))
|
|
||||||
if target == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(lowerFonts, "\n") {
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Each line can have comma-separated families
|
|
||||||
families := strings.Split(line, ",")
|
|
||||||
for _, fam := range families {
|
|
||||||
if strings.TrimSpace(fam) == target {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal Font Check
|
shellPath := resolveDoctorShellPath()
|
||||||
if hasFont(fontFamily) {
|
needFontconfig := !isBundledDefaultFont(fontFamily) || !isBundledDefaultFont(monoFontFamily)
|
||||||
results = append(results, checkResult{catFonts, "Normal Font", statusOK, fontFamily, "Available", url})
|
|
||||||
} else {
|
fcListAvailable := utils.CommandExists("fc-list")
|
||||||
results = append(results, checkResult{
|
fcCache := ""
|
||||||
catFonts, "Normal Font", statusWarn,
|
|
||||||
fmt.Sprintf("'%s' not found", fontFamily),
|
if needFontconfig {
|
||||||
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
|
if !fcListAvailable {
|
||||||
url,
|
results = append(results, checkResult{catFonts, "Fontconfig Tools", statusWarn, "fc-list not installed", "Cannot verify custom fonts in fontconfig cache.", url})
|
||||||
})
|
} else {
|
||||||
|
output, err := exec.Command("fc-list", ":", "family").Output()
|
||||||
|
if err != nil {
|
||||||
|
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Failed to query font list", "Fontconfig cache query failed. Try running 'fc-cache -fv'.", url})
|
||||||
|
} else {
|
||||||
|
fcCache = strings.ToLower(string(output))
|
||||||
|
if len(strings.TrimSpace(fcCache)) == 0 {
|
||||||
|
results = append(results, checkResult{catFonts, "Fontconfig Cache", statusError, "Cache is empty", "No fonts found in fontconfig cache. Try running 'fc-cache -fv'.", url})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monospace Font Check
|
results = append(results,
|
||||||
if hasFont(monoFontFamily) {
|
checkConfiguredFont("Normal Font", fontFamily, shellPath, fcCache, fcListAvailable, url),
|
||||||
results = append(results, checkResult{catFonts, "Monospace Font", statusOK, monoFontFamily, "Available", url})
|
checkConfiguredFont("Monospace Font", monoFontFamily, shellPath, fcCache, fcListAvailable, url),
|
||||||
} else {
|
)
|
||||||
results = append(results, checkResult{
|
|
||||||
catFonts, "Monospace Font", statusWarn,
|
|
||||||
fmt.Sprintf("'%s' not found", monoFontFamily),
|
|
||||||
"Font is not registered. Try running 'fc-cache -fv' or install the font.",
|
|
||||||
url,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQuickshellVersionFailureDetails(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
stderr string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Qt private API symbol mismatch",
|
||||||
|
stderr: "qs: symbol lookup error: qs: undefined symbol: _ZN23QUntypedPropertyBindingC1EP23QPropertyBindingPrivate, version Qt_6_PRIVATE_API",
|
||||||
|
want: "Quickshell is incompatible with the installed Qt libraries. Rebuild or reinstall Quickshell against the current Qt version.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "other failure",
|
||||||
|
stderr: "qs: unsupported option --version",
|
||||||
|
want: "Run 'qs --version' to inspect the failure.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := quickshellVersionFailureDetails(tc.stderr); got != tc.want {
|
||||||
|
t.Fatalf("quickshellVersionFailureDetails(%q) = %q, want %q", tc.stderr, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsing a full property dump as a bare value would yield a bogus plugin root,
|
||||||
|
// making the qtengine check warn at a user whose install is fine.
|
||||||
|
func TestQtQueryValue(t *testing.T) {
|
||||||
|
const dumpAll = `QT_INSTALL_PREFIX:/usr
|
||||||
|
QT_INSTALL_PLUGINS:/usr/lib/qt/plugins
|
||||||
|
QT_VERSION:5.15.19`
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
output string
|
||||||
|
key string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "bare value as qmake prints it",
|
||||||
|
output: "/usr/lib/qt6/plugins\n",
|
||||||
|
key: "QT_INSTALL_PLUGINS",
|
||||||
|
want: "/usr/lib/qt6/plugins",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key:value line among a full property dump",
|
||||||
|
output: dumpAll,
|
||||||
|
key: "QT_INSTALL_PLUGINS",
|
||||||
|
want: "/usr/lib/qt/plugins",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "a different key from the same dump",
|
||||||
|
output: dumpAll,
|
||||||
|
key: "QT_VERSION",
|
||||||
|
want: "5.15.19",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "key absent from a multi line dump yields nothing",
|
||||||
|
output: dumpAll,
|
||||||
|
key: "QT_INSTALL_LIBEXECS",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty output yields nothing",
|
||||||
|
output: "",
|
||||||
|
key: "QT_INSTALL_PLUGINS",
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "surrounding whitespace is trimmed",
|
||||||
|
output: " QT_INSTALL_PLUGINS: /usr/lib/qt/plugins \n",
|
||||||
|
key: "QT_INSTALL_PLUGINS",
|
||||||
|
want: "/usr/lib/qt/plugins",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
if got := qtQueryValue(tc.output, tc.key); got != tc.want {
|
||||||
|
t.Fatalf("qtQueryValue(%q, %q) = %q, want %q", tc.output, tc.key, got, tc.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stubQtQueryTool writes a fake Qt build tool answering QT_VERSION and
|
||||||
|
// QT_INSTALL_PLUGINS from the given values, and points PATH at its directory
|
||||||
|
// alone so the real toolchain cannot interfere.
|
||||||
|
func stubQtQueryTool(t *testing.T, bin, version, plugins string) string {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
script := "#!/bin/sh\ncase \"$2\" in\n QT_VERSION) echo " + version +
|
||||||
|
" ;;\n QT_INSTALL_PLUGINS) echo " + plugins + " ;;\nesac\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, bin), []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", dir)
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
func writePlugin(t *testing.T, pluginsDir, file string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Join(pluginsDir, "platformthemes"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(pluginsDir, "platformthemes", file), nil, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// qtenginePluginFile("", major) is a path with a directory component, and
|
||||||
|
// qtPluginPath joins platformthemes/ again — a regression here silently warns
|
||||||
|
// at every correctly installed system, so pin the full path down.
|
||||||
|
func TestQtenginePluginPath(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if got := qtenginePluginPath([]string{root}, "6"); got != "" {
|
||||||
|
t.Fatalf("missing plugin should yield an empty path, got %q", got)
|
||||||
|
}
|
||||||
|
want := filepath.Join(root, "platformthemes", "libqt6engine-plugin.so")
|
||||||
|
if err := os.MkdirAll(filepath.Join(root, "platformthemes"), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(want, nil, 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := qtenginePluginPath([]string{root}, "6"); got != want {
|
||||||
|
t.Fatalf("qtenginePluginPath = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckQtPlatformThemePlugin(t *testing.T) {
|
||||||
|
t.Run("qt6ct-kde is flagged as a package name, not a theme", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct-kde")
|
||||||
|
t.Setenv("PATH", t.TempDir()) // no Qt tools reachable
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Fatalf("got %d results, want 1: %+v", len(results), results)
|
||||||
|
}
|
||||||
|
if results[0].status != statusWarn {
|
||||||
|
t.Fatalf("status = %q, want warn", results[0].status)
|
||||||
|
}
|
||||||
|
if !strings.Contains(results[0].details, "package name") || !strings.Contains(results[0].details, "qt6ct") {
|
||||||
|
t.Fatalf("details should name the package/value mixup, got %q", results[0].details)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("installed plugin reports OK", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
|
||||||
|
plugins := t.TempDir()
|
||||||
|
writePlugin(t, plugins, "libqt6ct.so")
|
||||||
|
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
|
||||||
|
if len(results) != 1 || results[0].status != statusOK {
|
||||||
|
t.Fatalf("got %+v, want one OK result", results)
|
||||||
|
}
|
||||||
|
if !strings.Contains(results[0].details, "libqt6ct.so") {
|
||||||
|
t.Fatalf("details should point at the plugin, got %q", results[0].details)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing plugin warns with the expected path", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
|
||||||
|
plugins := t.TempDir()
|
||||||
|
stubQtQueryTool(t, "qtpaths6", "6.11.2", plugins)
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
|
||||||
|
if len(results) != 1 || results[0].status != statusWarn {
|
||||||
|
t.Fatalf("got %+v, want one warn result", results)
|
||||||
|
}
|
||||||
|
want := filepath.Join(plugins, "platformthemes", "libqt6ct.so")
|
||||||
|
if !strings.Contains(results[0].details, want) {
|
||||||
|
t.Fatalf("details should contain expected path %s, got %q", want, results[0].details)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no Qt build tools degrades to info", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "qt6ct")
|
||||||
|
t.Setenv("PATH", t.TempDir())
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
|
||||||
|
if len(results) != 1 || results[0].status != statusInfo {
|
||||||
|
t.Fatalf("got %+v, want one info result", results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unset and qtengine are left to their own checks", func(t *testing.T) {
|
||||||
|
t.Setenv("PATH", t.TempDir())
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "")
|
||||||
|
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
|
||||||
|
t.Fatalf("unset should yield no results, got %+v", results)
|
||||||
|
}
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "qtengine")
|
||||||
|
if results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME"); results != nil {
|
||||||
|
t.Fatalf("qtengine should yield no results, got %+v", results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("qt6 variable is not satisfied by a Qt5 tool alone", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME_QT6", "qt6ct")
|
||||||
|
plugins := t.TempDir()
|
||||||
|
stubQtQueryTool(t, "qmake-qt5", "5.15.19", plugins)
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME_QT6")
|
||||||
|
if len(results) != 1 || results[0].status != statusInfo {
|
||||||
|
t.Fatalf("got %+v, want one info result", results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("kde plugin miss degrades to info", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "kde")
|
||||||
|
plugins := t.TempDir()
|
||||||
|
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
|
||||||
|
if len(results) != 1 || results[0].status != statusInfo {
|
||||||
|
t.Fatalf("got %+v, want one info result", results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("gtk3 resolves through the libq convention", func(t *testing.T) {
|
||||||
|
t.Setenv("QT_QPA_PLATFORMTHEME", "gtk3")
|
||||||
|
plugins := t.TempDir()
|
||||||
|
writePlugin(t, plugins, "libqgtk3.so")
|
||||||
|
stubQtQueryTool(t, "qmake6", "6.11.2", plugins)
|
||||||
|
results := checkQtPlatformThemePlugin("QT_QPA_PLATFORMTHEME")
|
||||||
|
if len(results) != 1 || results[0].status != statusOK {
|
||||||
|
t.Fatalf("got %+v, want one OK result", results)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,19 +3,19 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var dlOutput string
|
var dlOutput string
|
||||||
var dlUserAgent string
|
var dlUserAgent string
|
||||||
|
var dlHeaders []string
|
||||||
var dlTimeout int
|
var dlTimeout int
|
||||||
|
var dlConnectTimeout int
|
||||||
var dlIPv4Only bool
|
var dlIPv4Only bool
|
||||||
|
|
||||||
var dlCmd = &cobra.Command{
|
var dlCmd = &cobra.Command{
|
||||||
@@ -33,65 +33,52 @@ var dlCmd = &cobra.Command{
|
|||||||
func init() {
|
func init() {
|
||||||
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
|
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
|
||||||
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
|
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
|
||||||
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
|
dlCmd.Flags().StringArrayVarP(&dlHeaders, "header", "H", nil, "Extra request header as 'Name: value' (repeatable)")
|
||||||
|
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Total request timeout in seconds")
|
||||||
|
dlCmd.Flags().IntVar(&dlConnectTimeout, "connect-timeout", 5, "Connection timeout in seconds")
|
||||||
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
|
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDownload(url string) error {
|
func parseHeaders(raw []string) (map[string]string, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
|
if len(raw) == 0 {
|
||||||
defer cancel()
|
return nil, nil
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("invalid request: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
headers := make(map[string]string, len(raw))
|
||||||
case dlUserAgent != "":
|
for _, entry := range raw {
|
||||||
req.Header.Set("User-Agent", dlUserAgent)
|
name, value, found := strings.Cut(entry, ":")
|
||||||
default:
|
if !found {
|
||||||
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
|
return nil, fmt.Errorf("invalid header %q: expected 'Name: value'", entry)
|
||||||
}
|
|
||||||
|
|
||||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
|
||||||
transport := &http.Transport{DialContext: dialer.DialContext}
|
|
||||||
if dlIPv4Only {
|
|
||||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, "tcp4", addr)
|
|
||||||
}
|
}
|
||||||
|
name = strings.TrimSpace(name)
|
||||||
|
if name == "" {
|
||||||
|
return nil, fmt.Errorf("invalid header %q: empty name", entry)
|
||||||
|
}
|
||||||
|
headers[name] = strings.TrimSpace(value)
|
||||||
}
|
}
|
||||||
client := &http.Client{Transport: transport}
|
return headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
func runDownload(url string) error {
|
||||||
|
headers, err := parseHeaders(dlHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("download failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
||||||
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dlOutput == "" {
|
|
||||||
_, err = io.Copy(os.Stdout, resp.Body)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if dir := filepath.Dir(dlOutput); dir != "." {
|
opts := netfetch.Options{
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
Headers: headers,
|
||||||
return fmt.Errorf("mkdir failed: %w", err)
|
UserAgent: dlUserAgent,
|
||||||
}
|
Timeout: time.Duration(dlTimeout) * time.Second,
|
||||||
|
ConnectTimeout: time.Duration(dlConnectTimeout) * time.Second,
|
||||||
|
IPv4Only: dlIPv4Only,
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := os.Create(dlOutput)
|
if dlOutput == "" {
|
||||||
if err != nil {
|
return netfetch.ToWriter(context.Background(), url, opts, os.Stdout)
|
||||||
return fmt.Errorf("create failed: %w", err)
|
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
if err := netfetch.ToFile(context.Background(), url, opts, dlOutput); err != nil {
|
||||||
os.Remove(dlOutput)
|
return err
|
||||||
return fmt.Errorf("write failed: %w", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println(dlOutput)
|
fmt.Println(dlOutput)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/netfetch"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
@@ -26,7 +27,7 @@ var updateCmd = &cobra.Command{
|
|||||||
Use: "update",
|
Use: "update",
|
||||||
Short: "Update DankMaterialShell to the latest version",
|
Short: "Update DankMaterialShell to the latest version",
|
||||||
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
|
Long: "Update DankMaterialShell to the latest version using the appropriate package manager for your distribution",
|
||||||
PreRunE: findConfig,
|
PreRunE: shellApp.ResolveConfig,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
runUpdate()
|
runUpdate()
|
||||||
},
|
},
|
||||||
@@ -98,7 +99,7 @@ func runUpdate() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Update complete! Restarting DMS...")
|
log.Info("Update complete! Restarting DMS...")
|
||||||
restartShell()
|
shellApp.Restart()
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateArchLinux() error {
|
func updateArchLinux() error {
|
||||||
@@ -250,6 +251,10 @@ func updateOtherDistros() error {
|
|||||||
return fmt.Errorf("failed to fetch changes: %w", err)
|
return fmt.Errorf("failed to fetch changes: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateSubmodules(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if currentTag != "" {
|
if currentTag != "" {
|
||||||
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
||||||
latestTagOutput, err := latestTagCmd.Output()
|
latestTagOutput, err := latestTagCmd.Output()
|
||||||
@@ -291,6 +296,10 @@ func updateOtherDistros() error {
|
|||||||
return fmt.Errorf("update cancelled")
|
return fmt.Errorf("update cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateSubmodules(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -320,10 +329,21 @@ func updateOtherDistros() error {
|
|||||||
return fmt.Errorf("update cancelled")
|
return fmt.Errorf("update cancelled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := updateSubmodules(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nUpdate complete!")
|
fmt.Println("\nUpdate complete!")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateSubmodules() error {
|
||||||
|
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||||
|
submoduleCmd.Stdout = os.Stdout
|
||||||
|
submoduleCmd.Stderr = os.Stderr
|
||||||
|
return submoduleCmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
func offerReclone(dmsPath string) bool {
|
func offerReclone(dmsPath string) bool {
|
||||||
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
||||||
reader := bufio.NewReader(os.Stdin)
|
reader := bufio.NewReader(os.Stdin)
|
||||||
@@ -342,7 +362,7 @@ func offerReclone(dmsPath string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Cloning fresh copy...")
|
fmt.Println("Cloning fresh copy...")
|
||||||
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||||
cloneCmd.Stdout = os.Stdout
|
cloneCmd.Stdout = os.Stdout
|
||||||
cloneCmd.Stderr = os.Stderr
|
cloneCmd.Stderr = os.Stderr
|
||||||
if err := cloneCmd.Run(); err != nil {
|
if err := cloneCmd.Run(); err != nil {
|
||||||
@@ -393,27 +413,11 @@ func updateDMSBinary() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Fetching latest release version...")
|
fmt.Println("Fetching latest release version...")
|
||||||
cmd := exec.Command("curl", "-s", "https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest")
|
version, err := netfetch.LatestReleaseTag(context.Background())
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch latest release: %w", err)
|
return fmt.Errorf("failed to fetch latest release: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
version := ""
|
|
||||||
for line := range strings.SplitSeq(string(output), "\n") {
|
|
||||||
if strings.Contains(line, "\"tag_name\"") {
|
|
||||||
parts := strings.Split(line, "\"")
|
|
||||||
if len(parts) >= 4 {
|
|
||||||
version = parts[3]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if version == "" {
|
|
||||||
return fmt.Errorf("could not determine latest version")
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("Latest version: %s\n", version)
|
fmt.Printf("Latest version: %s\n", version)
|
||||||
|
|
||||||
tempDir, err := os.MkdirTemp("", "dms-update-*")
|
tempDir, err := os.MkdirTemp("", "dms-update-*")
|
||||||
@@ -429,14 +433,12 @@ func updateDMSBinary() error {
|
|||||||
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
|
checksumPath := filepath.Join(tempDir, "dms.gz.sha256")
|
||||||
|
|
||||||
fmt.Println("Downloading dms binary...")
|
fmt.Println("Downloading dms binary...")
|
||||||
downloadCmd := exec.Command("curl", "-L", binaryURL, "-o", binaryPath)
|
if err := netfetch.ToFile(context.Background(), binaryURL, netfetch.Options{Timeout: 5 * time.Minute}, binaryPath); err != nil {
|
||||||
if err := downloadCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to download binary: %w", err)
|
return fmt.Errorf("failed to download binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Downloading checksum...")
|
fmt.Println("Downloading checksum...")
|
||||||
downloadCmd = exec.Command("curl", "-L", checksumURL, "-o", checksumPath)
|
if err := netfetch.ToFile(context.Background(), checksumURL, netfetch.Options{Timeout: 30 * time.Second}, checksumPath); err != nil {
|
||||||
if err := downloadCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to download checksum: %w", err)
|
return fmt.Errorf("failed to download checksum: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+27
-2079
File diff suppressed because it is too large
Load Diff
@@ -1,143 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
var calls []string
|
|
||||||
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
|
||||||
if dmsPath != "/tmp/dms" {
|
|
||||||
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
|
||||||
}
|
|
||||||
if compositor != "niri" {
|
|
||||||
t.Fatalf("unexpected compositor %q", compositor)
|
|
||||||
}
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
calls = append(calls, "configs")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var gotOptions sharedpam.SyncAuthOptions
|
|
||||||
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
|
||||||
if sudoPassword != "" {
|
|
||||||
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
|
||||||
}
|
|
||||||
gotOptions = options
|
|
||||||
calls = append(calls, "auth")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
|
||||||
ForceGreeterAuth: true,
|
|
||||||
}, func() {
|
|
||||||
calls = append(calls, "before-auth")
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wantCalls := []string{"configs", "before-auth", "auth"}
|
|
||||||
if !reflect.DeepEqual(calls, wantCalls) {
|
|
||||||
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
|
||||||
}
|
|
||||||
if !gotOptions.ForceGreeterAuth {
|
|
||||||
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
|
||||||
origGreeterConfigSyncFn := greeterConfigSyncFn
|
|
||||||
origSharedAuthSyncFn := sharedAuthSyncFn
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterConfigSyncFn = origGreeterConfigSyncFn
|
|
||||||
sharedAuthSyncFn = origSharedAuthSyncFn
|
|
||||||
})
|
|
||||||
|
|
||||||
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
|
||||||
return errors.New("config sync failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
authCalled := false
|
|
||||||
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
|
||||||
authCalled = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
|
||||||
if err == nil || err.Error() != "config sync failed" {
|
|
||||||
t.Fatalf("expected config sync error, got %v", err)
|
|
||||||
}
|
|
||||||
if authCalled {
|
|
||||||
t.Fatal("expected auth sync not to run after config sync failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGreeterStatusStateDirUsesNixOSDefault(t *testing.T) {
|
|
||||||
if got := greeterStatusStateDir("", true); got != nixOSGreeterStateDir {
|
|
||||||
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, nixOSGreeterStateDir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGreeterStatusStateDirHonorsExplicitOverrideOnNixOS(t *testing.T) {
|
|
||||||
command := "dms-greeter --cache-dir /srv/dms-greeter --command niri"
|
|
||||||
if got := greeterStatusStateDir(command, true); got != "/srv/dms-greeter" {
|
|
||||||
t.Fatalf("greeterStatusStateDir() = %q, want %q", got, "/srv/dms-greeter")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRejectNixOSGreeterMutationBlocksImperativeCommands(t *testing.T) {
|
|
||||||
origGreeterIsNixOSFn := greeterIsNixOSFn
|
|
||||||
greeterIsNixOSFn = func() bool { return true }
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterIsNixOSFn = origGreeterIsNixOSFn
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, commandName := range []string{"install", "enable", "sync", "uninstall"} {
|
|
||||||
t.Run(commandName, func(t *testing.T) {
|
|
||||||
root := &cobra.Command{Use: "dms"}
|
|
||||||
greeterCommand := &cobra.Command{Use: "greeter"}
|
|
||||||
mutationCommand := &cobra.Command{Use: commandName}
|
|
||||||
root.AddCommand(greeterCommand)
|
|
||||||
greeterCommand.AddCommand(mutationCommand)
|
|
||||||
|
|
||||||
err := rejectNixOSGreeterMutation(mutationCommand)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected NixOS greeter %s to be rejected", commandName)
|
|
||||||
}
|
|
||||||
if !strings.Contains(err.Error(), "dms greeter "+commandName+" is disabled on NixOS") {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "/var/cache/dms-greeter") {
|
|
||||||
t.Fatalf("NixOS remediation should not recommend the non-NixOS cache path: %v", err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRejectNixOSGreeterMutationAllowsOtherDistros(t *testing.T) {
|
|
||||||
origGreeterIsNixOSFn := greeterIsNixOSFn
|
|
||||||
greeterIsNixOSFn = func() bool { return false }
|
|
||||||
t.Cleanup(func() {
|
|
||||||
greeterIsNixOSFn = origGreeterIsNixOSFn
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := rejectNixOSGreeterMutation(&cobra.Command{Use: "sync"}); err != nil {
|
|
||||||
t.Fatalf("expected non-NixOS greeter command to be allowed, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -46,7 +47,7 @@ var keybindsSetCmd = &cobra.Command{
|
|||||||
Short: "Set a keybind override",
|
Short: "Set a keybind override",
|
||||||
Long: "Create or update a keybind override for the specified provider",
|
Long: "Create or update a keybind override for the specified provider",
|
||||||
Args: cobra.ExactArgs(3),
|
Args: cobra.ExactArgs(3),
|
||||||
Run: runKeybindsSet,
|
Run: runKeybindsEdit,
|
||||||
}
|
}
|
||||||
|
|
||||||
var keybindsRemoveCmd = &cobra.Command{
|
var keybindsRemoveCmd = &cobra.Command{
|
||||||
@@ -54,7 +55,7 @@ var keybindsRemoveCmd = &cobra.Command{
|
|||||||
Short: "Remove a keybind",
|
Short: "Remove a keybind",
|
||||||
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
|
Long: "Remove a keybind. For Hyprland this writes a negative override to dms/binds-user.lua so the key stays unbound across DMS updates. For other providers it deletes the entry from the managed file.",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runKeybindsRemove,
|
Run: runKeybindsEdit,
|
||||||
}
|
}
|
||||||
|
|
||||||
var keybindsResetCmd = &cobra.Command{
|
var keybindsResetCmd = &cobra.Command{
|
||||||
@@ -62,10 +63,14 @@ var keybindsResetCmd = &cobra.Command{
|
|||||||
Short: "Reset a keybind override to its DMS default",
|
Short: "Reset a keybind override to its DMS default",
|
||||||
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
|
Long: "Drop the user override for the given key so the DMS default re-applies. For providers without a separate default file (Niri, MangoWC) this is equivalent to remove.",
|
||||||
Args: cobra.ExactArgs(2),
|
Args: cobra.ExactArgs(2),
|
||||||
Run: runKeybindsReset,
|
Run: runKeybindsEdit,
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
keybindsCmd.PersistentFlags().String("expected-generation", "", "Configuration generation observed when editing Aqueous bindings")
|
||||||
|
for _, command := range []*cobra.Command{keybindsSetCmd, keybindsRemoveCmd, keybindsResetCmd} {
|
||||||
|
command.Flags().Bool("json", false, "Return structured mutation results, including errors")
|
||||||
|
}
|
||||||
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
keybindsListCmd.Flags().BoolP("json", "j", false, "Output as JSON")
|
||||||
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
keybindsShowCmd.Flags().String("path", "", "Override config path for the provider")
|
||||||
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
keybindsSetCmd.Flags().String("desc", "", "Description for hotkey overlay")
|
||||||
@@ -93,6 +98,9 @@ func initializeProviders() {
|
|||||||
registry := keybinds.GetDefaultRegistry()
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
|
||||||
hyprlandProvider := providers.NewHyprlandProvider("")
|
hyprlandProvider := providers.NewHyprlandProvider("")
|
||||||
|
if err := registry.Register(providers.NewAqueousProvider()); err != nil {
|
||||||
|
log.Warnf("Failed to register Aqueous provider: %v", err)
|
||||||
|
}
|
||||||
if err := registry.Register(hyprlandProvider); err != nil {
|
if err := registry.Register(hyprlandProvider); err != nil {
|
||||||
log.Warnf("Failed to register Hyprland provider: %v", err)
|
log.Warnf("Failed to register Hyprland provider: %v", err)
|
||||||
}
|
}
|
||||||
@@ -206,85 +214,92 @@ func runKeybindsShow(cmd *cobra.Command, args []string) {
|
|||||||
printCheatSheet(provider)
|
printCheatSheet(provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getWritableProvider(name string) keybinds.WritableProvider {
|
func keybindEditFailure(err error) map[string]any {
|
||||||
provider, err := keybinds.GetDefaultRegistry().Get(name)
|
code := "command_failed"
|
||||||
|
var helperError *providers.AqueousError
|
||||||
|
if errors.As(err, &helperError) {
|
||||||
|
code = helperError.Code
|
||||||
|
}
|
||||||
|
return map[string]any{"success": false, "code": code, "message": err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runKeybindsEdit(cmd *cobra.Command, args []string) {
|
||||||
|
result, err := editKeybind(cmd, args)
|
||||||
|
if err == nil {
|
||||||
|
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
structured, _ := cmd.Flags().GetBool("json")
|
||||||
|
if !structured {
|
||||||
|
log.Fatalf("Failed to save keybind: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(cmd.OutOrStdout()).Encode(keybindEditFailure(err))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func editKeybind(cmd *cobra.Command, args []string) (map[string]any, error) {
|
||||||
|
provider, err := keybinds.GetDefaultRegistry().Get(args[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error: %v", err)
|
return nil, err
|
||||||
|
}
|
||||||
|
key := args[1]
|
||||||
|
if aqueous, ok := provider.(*providers.AqueousProvider); ok {
|
||||||
|
generation, _ := cmd.Flags().GetString("expected-generation")
|
||||||
|
edit := providers.AqueousBindEdit{Generation: generation, Key: key, Remove: cmd.Name() != "set"}
|
||||||
|
if !edit.Remove {
|
||||||
|
for _, flag := range []string{"desc", "allow-when-locked", "cooldown-ms", "no-repeat", "no-inhibiting", "flags"} {
|
||||||
|
if cmd.Flags().Changed(flag) {
|
||||||
|
return nil, fmt.Errorf("aqueous does not support --%s in this provider", flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edit.Action = args[2]
|
||||||
|
edit.OriginalKey, _ = cmd.Flags().GetString("replace-key")
|
||||||
|
}
|
||||||
|
result, err := aqueous.Edit(cmd.Context(), edit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return map[string]any{"success": true, "code": "applied", "generation": result.String("generation"), "key": key}, nil
|
||||||
}
|
}
|
||||||
writable, ok := provider.(keybinds.WritableProvider)
|
writable, ok := provider.(keybinds.WritableProvider)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Fatalf("Provider %s does not support writing keybinds", name)
|
return nil, fmt.Errorf("provider %s does not support writing keybinds", args[0])
|
||||||
}
|
}
|
||||||
return writable
|
result := map[string]any{"success": true, "key": key}
|
||||||
}
|
switch cmd.Name() {
|
||||||
|
case "set":
|
||||||
func runKeybindsSet(cmd *cobra.Command, args []string) {
|
options := make(map[string]any)
|
||||||
providerName, key, action := args[0], args[1], args[2]
|
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
||||||
writable := getWritableProvider(providerName)
|
options["allow-when-locked"] = true
|
||||||
|
}
|
||||||
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
||||||
_ = writable.RemoveBind(replaceKey)
|
options["cooldown-ms"] = v
|
||||||
}
|
}
|
||||||
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
options := make(map[string]any)
|
options["repeat"] = false
|
||||||
if v, _ := cmd.Flags().GetBool("allow-when-locked"); v {
|
}
|
||||||
options["allow-when-locked"] = true
|
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||||
}
|
options["allow-inhibiting"] = false
|
||||||
if v, _ := cmd.Flags().GetInt("cooldown-ms"); v > 0 {
|
}
|
||||||
options["cooldown-ms"] = v
|
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||||
}
|
options["flags"] = v
|
||||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
}
|
||||||
options["repeat"] = false
|
if replaceKey, _ := cmd.Flags().GetString("replace-key"); replaceKey != "" && replaceKey != key {
|
||||||
}
|
_ = writable.RemoveBind(replaceKey)
|
||||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
}
|
||||||
options["allow-inhibiting"] = false
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
}
|
err = writable.SetBind(key, args[2], desc, options)
|
||||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
result["action"] = args[2]
|
||||||
options["flags"] = v
|
result["path"] = writable.GetOverridePath()
|
||||||
}
|
case "remove":
|
||||||
|
err = writable.RemoveBind(key)
|
||||||
desc, _ := cmd.Flags().GetString("desc")
|
result["removed"] = true
|
||||||
if err := writable.SetBind(key, action, desc, options); err != nil {
|
case "reset":
|
||||||
log.Fatalf("Error setting keybind: %v", err)
|
err = writable.ResetBind(key)
|
||||||
}
|
result["reset"] = true
|
||||||
|
default:
|
||||||
output, _ := json.MarshalIndent(map[string]any{
|
return nil, fmt.Errorf("unsupported keybind operation: %s", cmd.Name())
|
||||||
"success": true,
|
}
|
||||||
"key": key,
|
return result, err
|
||||||
"action": action,
|
|
||||||
"path": writable.GetOverridePath(),
|
|
||||||
}, "", " ")
|
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runKeybindsRemove(_ *cobra.Command, args []string) {
|
|
||||||
providerName, key := args[0], args[1]
|
|
||||||
writable := getWritableProvider(providerName)
|
|
||||||
|
|
||||||
if err := writable.RemoveBind(key); err != nil {
|
|
||||||
log.Fatalf("Error removing keybind: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output, _ := json.MarshalIndent(map[string]any{
|
|
||||||
"success": true,
|
|
||||||
"key": key,
|
|
||||||
"removed": true,
|
|
||||||
}, "", " ")
|
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func runKeybindsReset(_ *cobra.Command, args []string) {
|
|
||||||
providerName, key := args[0], args[1]
|
|
||||||
writable := getWritableProvider(providerName)
|
|
||||||
|
|
||||||
if err := writable.ResetBind(key); err != nil {
|
|
||||||
log.Fatalf("Error resetting keybind: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
output, _ := json.MarshalIndent(map[string]any{
|
|
||||||
"success": true,
|
|
||||||
"key": key,
|
|
||||||
"reset": true,
|
|
||||||
}, "", " ")
|
|
||||||
fmt.Fprintln(os.Stdout, string(output))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestKeybindEditFailure(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
err error
|
||||||
|
code string
|
||||||
|
}{
|
||||||
|
{&providers.AqueousError{Code: "external_change", Message: "changed"}, "external_change"},
|
||||||
|
{fmt.Errorf("helper: %w", &providers.AqueousError{Code: "read_only", Message: "denied"}), "read_only"},
|
||||||
|
{errors.New("transport failed"), "command_failed"},
|
||||||
|
} {
|
||||||
|
result := keybindEditFailure(tc.err)
|
||||||
|
if result["success"] != false || result["code"] != tc.code || result["message"] != tc.err.Error() {
|
||||||
|
t.Fatalf("unexpected failure result: %#v", result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,11 +44,18 @@ var matugenPreviewCmd = &cobra.Command{
|
|||||||
Run: runMatugenPreview,
|
Run: runMatugenPreview,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matugenQtengineCmd = &cobra.Command{
|
||||||
|
Use: "qtengine",
|
||||||
|
Short: "Sync qtengine's config with the current DMS theme",
|
||||||
|
Run: runMatugenQtengine,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
matugenCmd.AddCommand(matugenQueueCmd)
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
matugenCmd.AddCommand(matugenCheckCmd)
|
matugenCmd.AddCommand(matugenCheckCmd)
|
||||||
matugenCmd.AddCommand(matugenPreviewCmd)
|
matugenCmd.AddCommand(matugenPreviewCmd)
|
||||||
|
matugenCmd.AddCommand(matugenQtengineCmd)
|
||||||
|
|
||||||
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
for _, cmd := range []*cobra.Command{matugenGenerateCmd, matugenQueueCmd} {
|
||||||
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
cmd.Flags().String("state-dir", "", "State directory for cache files")
|
||||||
@@ -56,21 +63,25 @@ func init() {
|
|||||||
cmd.Flags().String("config-dir", "", "User config directory")
|
cmd.Flags().String("config-dir", "", "User config directory")
|
||||||
cmd.Flags().String("kind", "image", "Source type: image or hex")
|
cmd.Flags().String("kind", "image", "Source type: image or hex")
|
||||||
cmd.Flags().String("value", "", "Wallpaper path or hex color")
|
cmd.Flags().String("value", "", "Wallpaper path or hex color")
|
||||||
cmd.Flags().String("mode", "dark", "Color mode: dark or light")
|
cmd.Flags().String("mode", "dark", "Color mode: dark, light, or smart (smart requires matugen 4.2+ and an image source)")
|
||||||
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
|
cmd.Flags().String("icon-theme", "System Default", "Icon theme name")
|
||||||
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type")
|
cmd.Flags().String("matugen-type", "scheme-tonal-spot", "Matugen scheme type (supports scheme-smart on matugen 4.2+)")
|
||||||
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
|
cmd.Flags().Bool("run-user-templates", true, "Run user matugen templates")
|
||||||
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
cmd.Flags().String("stock-colors", "", "Stock theme colors JSON")
|
||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||||
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||||
|
cmd.Flags().String("source-mode", "", "Source color selection: dominant, colorful, darkness, lightness, saturation, less-saturation, value")
|
||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
||||||
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
|
matugenPreviewCmd.Flags().String("source-color", "", "Source color used to generate previews")
|
||||||
|
matugenPreviewCmd.Flags().String("image", "", "Wallpaper image used to resolve the scheme-smart preview")
|
||||||
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
matugenPreviewCmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||||
|
matugenQtengineCmd.Flags().String("config-dir", "", "User config directory")
|
||||||
|
matugenQtengineCmd.Flags().String("icon-theme", "", "Icon theme name")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
@@ -88,6 +99,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
|
sourceMode, _ := cmd.Flags().GetString("source-mode")
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -104,6 +116,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
TerminalsAlwaysDark: terminalsAlwaysDark,
|
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||||
SkipTemplates: skipTemplates,
|
SkipTemplates: skipTemplates,
|
||||||
|
SourceMode: sourceMode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +154,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
"skipTemplates": opts.SkipTemplates,
|
||||||
"contrast": opts.Contrast,
|
"contrast": opts.Contrast,
|
||||||
|
"sourceMode": opts.SourceMode,
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -212,8 +226,9 @@ func runMatugenCheck(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
func runMatugenPreview(cmd *cobra.Command, args []string) {
|
func runMatugenPreview(cmd *cobra.Command, args []string) {
|
||||||
sourceColor, _ := cmd.Flags().GetString("source-color")
|
sourceColor, _ := cmd.Flags().GetString("source-color")
|
||||||
|
imagePath, _ := cmd.Flags().GetString("image")
|
||||||
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
previews, err := matugen.PreviewSchemes(sourceColor, contrast)
|
previews, err := matugen.PreviewSchemes(sourceColor, contrast, imagePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to generate Matugen previews: %v", err)
|
log.Fatalf("Failed to generate Matugen previews: %v", err)
|
||||||
}
|
}
|
||||||
@@ -223,3 +238,12 @@ func runMatugenPreview(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runMatugenQtengine(cmd *cobra.Command, args []string) {
|
||||||
|
configDir, _ := cmd.Flags().GetString("config-dir")
|
||||||
|
iconTheme, _ := cmd.Flags().GetString("icon-theme")
|
||||||
|
|
||||||
|
if err := matugen.SyncQtengineConfigAt(configDir, iconTheme); err != nil {
|
||||||
|
log.Fatalf("Failed to sync qtengine config: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatugenQtengineCommandAcceptsConfigDir(t *testing.T) {
|
||||||
|
flag := matugenQtengineCmd.Flags().Lookup("config-dir")
|
||||||
|
require.NotNil(t, flag)
|
||||||
|
assert.Equal(t, "", flag.DefValue)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,8 +62,27 @@ func runNotify(cmd *cobra.Command, args []string) {
|
|||||||
Timeout: int32(notifyTimeout),
|
Timeout: int32(notifyTimeout),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := notify.Send(n); err != nil {
|
id, err := notify.Send(n)
|
||||||
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if n.FilePath == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
watchNotificationAction(id, n.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func watchNotificationAction(id uint32, path string) {
|
||||||
|
if id == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, ok := tryServerRequest(models.Request{
|
||||||
|
Method: "notify.watchAction",
|
||||||
|
Params: map[string]any{"id": id, "path": path},
|
||||||
|
})
|
||||||
|
if ok && resp.Error == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notify.SpawnActionListener(id, path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,265 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/qrcode"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
qrEcc string
|
||||||
|
qrVersion int
|
||||||
|
qrOutput string
|
||||||
|
qrStdout bool
|
||||||
|
qrClipboard bool
|
||||||
|
qrCopyText bool
|
||||||
|
qrRender bool
|
||||||
|
qrNoRender bool
|
||||||
|
qrInvert bool
|
||||||
|
qrQuietZone int
|
||||||
|
qrModuleSize int
|
||||||
|
qrFg string
|
||||||
|
qrBg string
|
||||||
|
qrTransparent bool
|
||||||
|
qrBorder int
|
||||||
|
qrShape string
|
||||||
|
qrLogo string
|
||||||
|
qrLogoScale int
|
||||||
|
|
||||||
|
qrWifiPassword string
|
||||||
|
qrWifiSecurity string
|
||||||
|
qrWifiHidden bool
|
||||||
|
)
|
||||||
|
|
||||||
|
var qrCmd = &cobra.Command{
|
||||||
|
Use: "qr [text]",
|
||||||
|
Short: "Generate QR codes",
|
||||||
|
Long: `Generate a QR code from text, stdin, or a WiFi network.
|
||||||
|
|
||||||
|
By default the code is rendered to the terminal when stdout is a TTY, or
|
||||||
|
written as PNG bytes to stdout when piped. Use flags to also copy to the
|
||||||
|
clipboard, save a PNG, or tune encoding and colors.
|
||||||
|
|
||||||
|
Input:
|
||||||
|
dms qr "https://example.com" # encode an argument
|
||||||
|
echo -n "data" | dms qr # encode stdin
|
||||||
|
dms qr - # encode stdin explicitly
|
||||||
|
|
||||||
|
Output (combine freely):
|
||||||
|
dms qr "text" --clipboard # copy PNG image to clipboard
|
||||||
|
dms qr "text" --copy-text # copy the source text to clipboard
|
||||||
|
dms qr "text" -o code.png # save a PNG file
|
||||||
|
dms qr "text" > code.png # PNG to stdout (piped)
|
||||||
|
dms qr "text" --render # force terminal render
|
||||||
|
|
||||||
|
Encoding & style:
|
||||||
|
--ecc L|M|Q|H # error correction (default M)
|
||||||
|
--qr-version 10 # force symbol version (1-40)
|
||||||
|
--module-size 12 --fg '#000' ... # PNG sizing and colors
|
||||||
|
--shape circle # round modules
|
||||||
|
--logo icon.png # center logo (bumps --ecc to H)
|
||||||
|
--invert # flip colors for light terminals
|
||||||
|
|
||||||
|
WiFi:
|
||||||
|
dms qr wifi MySSID -p secret # build from an explicit password
|
||||||
|
dms qr wifi MySSID # pull the saved secret from the shell`,
|
||||||
|
Args: cobra.ArbitraryArgs,
|
||||||
|
Run: runQR,
|
||||||
|
}
|
||||||
|
|
||||||
|
var qrWifiCmd = &cobra.Command{
|
||||||
|
Use: "wifi <ssid>",
|
||||||
|
Short: "Generate a WiFi QR code",
|
||||||
|
Long: `Generate a QR code that joins a WiFi network when scanned.
|
||||||
|
|
||||||
|
With --password the code is built entirely offline. Without it, the saved
|
||||||
|
credentials are fetched from the running DMS shell (like the network panel).`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: runQRWifi,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrEcc, "ecc", "", "Error correction level: L, M, Q, H (default M, or H with --logo)")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrVersion, "qr-version", 0, "Force QR symbol version 1-40 (0 = auto)")
|
||||||
|
qrCmd.PersistentFlags().StringVarP(&qrOutput, "output", "o", "", "Write a PNG to this file")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrStdout, "stdout", false, "Write PNG bytes to stdout")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrClipboard, "clipboard", false, "Copy the PNG image to the clipboard")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrCopyText, "copy-text", false, "Copy the source text to the clipboard")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrRender, "render", false, "Force terminal rendering")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrNoRender, "no-render", false, "Never render to the terminal")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrInvert, "invert", false, "Swap colors (for light terminals)")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrQuietZone, "quiet-zone", 2, "Terminal margin in modules")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrModuleSize, "module-size", 0, "PNG pixels per module (0 = auto)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrFg, "fg", "", "Dark module color (#RGB or #RRGGBB)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrBg, "bg", "", "Light module color (#RGB or #RRGGBB)")
|
||||||
|
qrCmd.PersistentFlags().BoolVar(&qrTransparent, "transparent", false, "Transparent PNG background")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrBorder, "border", -1, "PNG border in pixels (-1 = auto)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrShape, "shape", "square", "PNG module shape (square, circle)")
|
||||||
|
qrCmd.PersistentFlags().StringVar(&qrLogo, "logo", "", "Center a PNG/JPEG logo on the PNG output")
|
||||||
|
qrCmd.PersistentFlags().IntVar(&qrLogoScale, "logo-scale", 0, "Max logo size as 1/N of the code (0 = library default of 5)")
|
||||||
|
|
||||||
|
qrWifiCmd.Flags().StringVarP(&qrWifiPassword, "password", "p", "", "WiFi password (offline build)")
|
||||||
|
qrWifiCmd.Flags().StringVar(&qrWifiSecurity, "security", "WPA", "Security type (WPA, WEP, nopass)")
|
||||||
|
qrWifiCmd.Flags().BoolVar(&qrWifiHidden, "hidden", false, "Mark the network as hidden")
|
||||||
|
|
||||||
|
qrCmd.AddCommand(qrWifiCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQR(cmd *cobra.Command, args []string) {
|
||||||
|
text := strings.Join(args, " ")
|
||||||
|
if text == "" || text == "-" {
|
||||||
|
data, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Error reading stdin: %v", err)
|
||||||
|
}
|
||||||
|
text = strings.TrimRight(string(data), "\n")
|
||||||
|
}
|
||||||
|
if text == "" {
|
||||||
|
fatalf("Error: no input (provide text, pipe stdin, or use a subcommand)")
|
||||||
|
}
|
||||||
|
emitQR(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runQRWifi(cmd *cobra.Command, args []string) {
|
||||||
|
ssid := args[0]
|
||||||
|
if qrWifiPassword != "" || strings.EqualFold(qrWifiSecurity, "nopass") {
|
||||||
|
emitQR(qrcode.WiFiString(qrWifiSecurity, ssid, qrWifiPassword, qrWifiHidden))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := fetchWifiQRContent(ssid)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "Hint: pass --password to build the code without the shell.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if qrWifiHidden {
|
||||||
|
content = strings.TrimSuffix(content, ";") + "H:true;;"
|
||||||
|
}
|
||||||
|
emitQR(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitQR(text string) {
|
||||||
|
renderTerm := shouldRenderTerminal()
|
||||||
|
pngToStdout := qrStdout || (!renderTerm && qrOutput == "" && !qrClipboard && !qrCopyText)
|
||||||
|
|
||||||
|
if pngToStdout || qrOutput != "" || qrClipboard {
|
||||||
|
png, err := qrcode.RenderPNG(text, qrcode.ImageOptions{
|
||||||
|
ECC: effectiveEcc(),
|
||||||
|
Version: qrVersion,
|
||||||
|
ModuleSize: qrModuleSize,
|
||||||
|
Fg: qrFg,
|
||||||
|
Bg: qrBg,
|
||||||
|
Transparent: qrTransparent,
|
||||||
|
Border: qrBorder,
|
||||||
|
Shape: qrShape,
|
||||||
|
Logo: qrLogo,
|
||||||
|
LogoScale: qrLogoScale,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Error encoding QR: %v", err)
|
||||||
|
}
|
||||||
|
emitPNG(png, pngToStdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if qrCopyText {
|
||||||
|
if err := clipboard.CopyText(text); err != nil {
|
||||||
|
fatalf("Error copying text: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !renderTerm {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := qrcode.RenderTerminal(text, qrcode.TermOptions{
|
||||||
|
ECC: effectiveEcc(),
|
||||||
|
Version: qrVersion,
|
||||||
|
QuietZone: qrQuietZone,
|
||||||
|
Invert: qrInvert,
|
||||||
|
Fg: qrFg,
|
||||||
|
Bg: qrBg,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
fatalf("Error rendering QR: %v", err)
|
||||||
|
}
|
||||||
|
dst := os.Stdout
|
||||||
|
if pngToStdout {
|
||||||
|
dst = os.Stderr
|
||||||
|
}
|
||||||
|
fmt.Fprint(dst, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func emitPNG(png []byte, toStdout bool) {
|
||||||
|
if qrOutput != "" {
|
||||||
|
if err := os.WriteFile(qrOutput, png, 0o644); err != nil {
|
||||||
|
fatalf("Error writing file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintln(os.Stderr, qrOutput)
|
||||||
|
}
|
||||||
|
if toStdout {
|
||||||
|
os.Stdout.Write(png)
|
||||||
|
}
|
||||||
|
if qrClipboard {
|
||||||
|
if err := clipboard.Copy(png, "image/png"); err != nil {
|
||||||
|
fatalf("Error copying image: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldRenderTerminal() bool {
|
||||||
|
switch {
|
||||||
|
case qrNoRender:
|
||||||
|
return false
|
||||||
|
case qrRender:
|
||||||
|
return true
|
||||||
|
case qrStdout, qrOutput != "", qrClipboard, qrCopyText:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return isatty.IsTerminal(os.Stdout.Fd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectiveEcc() string {
|
||||||
|
switch {
|
||||||
|
case qrEcc != "":
|
||||||
|
return qrEcc
|
||||||
|
case qrLogo != "":
|
||||||
|
return "H"
|
||||||
|
default:
|
||||||
|
return "M"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fatalf(format string, args ...any) {
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWifiQRContent(ssid string) (string, error) {
|
||||||
|
resp, err := sendServerRequest(models.Request{
|
||||||
|
ID: 1,
|
||||||
|
Method: "network.qrcode-content",
|
||||||
|
Params: map[string]any{"ssid": ssid},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp.Error != "" {
|
||||||
|
return "", fmt.Errorf("%s", resp.Error)
|
||||||
|
}
|
||||||
|
if resp.Result == nil {
|
||||||
|
return "", fmt.Errorf("empty response")
|
||||||
|
}
|
||||||
|
content, ok := (*resp.Result).(string)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("unexpected response format")
|
||||||
|
}
|
||||||
|
return content, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/registries"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var registryCmd = &cobra.Command{
|
||||||
|
Use: "registry",
|
||||||
|
Short: "Manage plugin and theme registries",
|
||||||
|
Long: "Manage the registries DMS fetches plugins and themes from. The official registry is always active; additional registries can be added by name and git URL.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var registryListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "List configured registries",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for _, s := range registries.Load(afero.NewOsFs()) {
|
||||||
|
suffix := ""
|
||||||
|
if s.Official() {
|
||||||
|
suffix = " (official)"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s%s\n %s\n", s.Name, suffix, s.URL)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var registryAddCmd = &cobra.Command{
|
||||||
|
Use: "add <name> <url>",
|
||||||
|
Short: "Add a registry",
|
||||||
|
Long: "Add a registry by name and git URL. The repository must contain a plugins/ or themes/ directory in the registry format.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := registries.Add(afero.NewOsFs(), args[0], args[1]); err != nil {
|
||||||
|
log.Fatalf("Error adding registry: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Registry added: %s\n", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var registryRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <name>",
|
||||||
|
Short: "Remove a registry",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, s := range registries.Load(afero.NewOsFs()) {
|
||||||
|
if !s.Official() {
|
||||||
|
names = append(names, s.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return names, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := registries.Remove(afero.NewOsFs(), args[0]); err != nil {
|
||||||
|
log.Fatalf("Error removing registry: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Registry removed: %s\n", args[0])
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,19 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var customConfigPath string
|
|
||||||
var configPath string
|
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "dms",
|
Use: "dms",
|
||||||
Short: "dms CLI",
|
Short: "dms CLI",
|
||||||
@@ -21,54 +11,5 @@ var rootCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
rootCmd.PersistentFlags().StringVarP(shellApp.CustomConfigVar(), "config", "c", "", "Path to a UI config dir (containing shell.qml) to use instead of the embedded UI (env: DMS_SHELL_DIR)")
|
||||||
}
|
|
||||||
|
|
||||||
func findConfig(cmd *cobra.Command, args []string) error {
|
|
||||||
if customConfigPath != "" {
|
|
||||||
log.Debug("Custom config path provided via -c flag: %s", customConfigPath)
|
|
||||||
shellPath := filepath.Join(customConfigPath, "shell.qml")
|
|
||||||
|
|
||||||
info, statErr := os.Stat(shellPath)
|
|
||||||
|
|
||||||
if statErr == nil && !info.IsDir() {
|
|
||||||
configPath = customConfigPath
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if statErr != nil {
|
|
||||||
return fmt.Errorf("custom config path error: %w", statErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("path is a directory, not a file: %s", shellPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
|
||||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
|
||||||
if len(getAllDMSPIDs()) == 0 {
|
|
||||||
os.Remove(configStateFile)
|
|
||||||
} else {
|
|
||||||
statePath := strings.TrimSpace(string(data))
|
|
||||||
shellPath := filepath.Join(statePath, "shell.qml")
|
|
||||||
|
|
||||||
if info, statErr := os.Stat(shellPath); statErr == nil && !info.IsDir() {
|
|
||||||
log.Debug("Using config from active session state file: %s", statePath)
|
|
||||||
configPath = statePath
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
os.Remove(configStateFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("No custom path or active session, searching default XDG locations...")
|
|
||||||
var err error
|
|
||||||
configPath, err = config.LocateDMSConfig()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("Using config from: %s", configPath)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,25 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/screenshot"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ssOutputName string
|
ssOutputName string
|
||||||
|
ssSeat string
|
||||||
ssCursor string
|
ssCursor string
|
||||||
ssFormat string
|
ssFormat string
|
||||||
ssQuality int
|
ssQuality int
|
||||||
@@ -27,8 +32,24 @@ var (
|
|||||||
ssNoConfirm bool
|
ssNoConfirm bool
|
||||||
ssReset bool
|
ssReset bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
|
ssJSON bool
|
||||||
|
ssGeometry bool
|
||||||
|
ssAllowMulti bool
|
||||||
|
ssHUD string
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type screenshotMetadata struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
X *int `json:"x,omitempty"`
|
||||||
|
Y *int `json:"y,omitempty"`
|
||||||
|
Width int `json:"width,omitempty"`
|
||||||
|
Height int `json:"height,omitempty"`
|
||||||
|
Scale float64 `json:"scale,omitempty"`
|
||||||
|
Mime string `json:"mime,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
var screenshotCmd = &cobra.Command{
|
var screenshotCmd = &cobra.Command{
|
||||||
Use: "screenshot",
|
Use: "screenshot",
|
||||||
Short: "Capture screenshots",
|
Short: "Capture screenshots",
|
||||||
@@ -39,8 +60,9 @@ Modes:
|
|||||||
full - Capture the focused output
|
full - Capture the focused output
|
||||||
all - Capture all outputs combined
|
all - Capture all outputs combined
|
||||||
output - Capture a specific output by name
|
output - Capture a specific output by name
|
||||||
window - Capture the focused window (Hyprland/Mango)
|
window - Capture the focused window (Hyprland/Mango/niri/Aqueous)
|
||||||
last - Capture the last selected region
|
last - Capture the last selected region
|
||||||
|
scroll - Select a region, then scroll to capture a stitched tall image
|
||||||
|
|
||||||
Output format (--format):
|
Output format (--format):
|
||||||
png - PNG format (default)
|
png - PNG format (default)
|
||||||
@@ -52,14 +74,19 @@ Examples:
|
|||||||
dms screenshot full # Full screen of focused output
|
dms screenshot full # Full screen of focused output
|
||||||
dms screenshot all # All screens combined
|
dms screenshot all # All screens combined
|
||||||
dms screenshot output -o DP-1 # Specific output
|
dms screenshot output -o DP-1 # Specific output
|
||||||
dms screenshot window # Focused window (Hyprland)
|
dms screenshot window # Focused window
|
||||||
dms screenshot last # Last region (pre-selected)
|
dms screenshot last # Last region (pre-selected)
|
||||||
dms screenshot --reset # Reset last region pre-selection
|
dms screenshot --reset # Reset last region pre-selection
|
||||||
dms screenshot --no-clipboard # Save file only
|
dms screenshot --no-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
dms screenshot --no-confirm # Region capture on mouse release
|
dms screenshot --no-confirm # Region capture on mouse release
|
||||||
dms screenshot --cursor=on # Include cursor
|
dms screenshot --cursor=on # Include cursor
|
||||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
dms screenshot -f jpg -q 85 # JPEG with quality 85
|
||||||
|
dms screenshot --json # Print capture metadata as JSON
|
||||||
|
dms screenshot --allow-multiple # Skip the one-selector-at-a-time guard
|
||||||
|
dms screenshot -g # Print selected region geometry (X,Y WxH) to stdout
|
||||||
|
dms screenshot scroll # Scroll capture, Enter finishes / Esc cancels
|
||||||
|
dms screenshot scroll --interval 250`,
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssRegionCmd = &cobra.Command{
|
var ssRegionCmd = &cobra.Command{
|
||||||
@@ -97,10 +124,33 @@ If no previous region exists, falls back to interactive selection.`,
|
|||||||
var ssWindowCmd = &cobra.Command{
|
var ssWindowCmd = &cobra.Command{
|
||||||
Use: "window",
|
Use: "window",
|
||||||
Short: "Capture the focused window",
|
Short: "Capture the focused window",
|
||||||
Long: `Capture the currently focused window. Supported on Hyprland and Mango.`,
|
Long: `Capture the currently focused window. Supported on Hyprland, Mango, niri, and Aqueous. Aqueous requires a running DMS shell and crops the output including borders and overlapping windows.`,
|
||||||
Run: runScreenshotWindow,
|
Run: runScreenshotWindow,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ssScrollInterval int
|
||||||
|
|
||||||
|
var ssScrollCmd = &cobra.Command{
|
||||||
|
Use: "scroll",
|
||||||
|
Short: "Capture a scrolling region stitched into one tall image",
|
||||||
|
Long: `Select a region, then scroll the content beneath with the mouse wheel or
|
||||||
|
touchpad while frames are captured and stitched vertically. Finish with the
|
||||||
|
on-screen done button; cancel with the cancel button. Enter and Esc work
|
||||||
|
everywhere: most compositors hold the keyboard on the overlay (keyboard
|
||||||
|
scrolling does not reach the app there), while Hyprland leaves the keyboard
|
||||||
|
with the application — keyboard scrolling works, and Enter/Esc act through
|
||||||
|
temporary global binds for the session. The cursor is never included in
|
||||||
|
frames.
|
||||||
|
|
||||||
|
Frames are stitched continuously while scrolling, and revisited content is
|
||||||
|
never duplicated — scrolling up past the starting point extends the image
|
||||||
|
upward. Content jumped past faster than capture can follow is skipped rather
|
||||||
|
than stitched incorrectly.
|
||||||
|
|
||||||
|
Rotated outputs are not supported.`,
|
||||||
|
Run: runScreenshotScroll,
|
||||||
|
}
|
||||||
|
|
||||||
var ssListCmd = &cobra.Command{
|
var ssListCmd = &cobra.Command{
|
||||||
Use: "list",
|
Use: "list",
|
||||||
Short: "List available outputs",
|
Short: "List available outputs",
|
||||||
@@ -111,12 +161,13 @@ var notifyActionCmd = &cobra.Command{
|
|||||||
Use: "notify-action",
|
Use: "notify-action",
|
||||||
Hidden: true,
|
Hidden: true,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
screenshot.RunNotifyActionListener(args)
|
notify.RunActionListener(args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputName, "output", "o", "", "Output name for 'output' mode")
|
||||||
|
screenshotCmd.PersistentFlags().StringVar(&ssSeat, "seat", "", "Seat for Aqueous capture (required with multiple seats)")
|
||||||
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
|
screenshotCmd.PersistentFlags().StringVar(&ssCursor, "cursor", "off", "Include cursor in screenshot (on/off)")
|
||||||
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
screenshotCmd.PersistentFlags().StringVarP(&ssFormat, "format", "f", "png", "Output format (png, jpg, ppm)")
|
||||||
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
screenshotCmd.PersistentFlags().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||||
@@ -128,8 +179,15 @@ func init() {
|
|||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssJSON, "json", false, "Print capture metadata as JSON")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVarP(&ssGeometry, "geometry", "g", false, "Print selected region geometry (X,Y WxH) to stdout without capturing an image")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssAllowMulti, "allow-multiple", false, "Open a selector even when another one is already open")
|
||||||
|
screenshotCmd.PersistentFlags().StringVar(&ssHUD, "hud", "auto", "HUD overlay scale in region selector (auto, off/0, or integer scale 1-4)")
|
||||||
|
|
||||||
|
ssScrollCmd.Flags().IntVar(&ssScrollInterval, "interval", 45, "Capture interval in milliseconds (30-1000)")
|
||||||
|
|
||||||
screenshotCmd.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
|
screenshotCmd.AddCommand(ssScrollCmd)
|
||||||
screenshotCmd.AddCommand(ssFullCmd)
|
screenshotCmd.AddCommand(ssFullCmd)
|
||||||
screenshotCmd.AddCommand(ssAllCmd)
|
screenshotCmd.AddCommand(ssAllCmd)
|
||||||
screenshotCmd.AddCommand(ssOutputCmd)
|
screenshotCmd.AddCommand(ssOutputCmd)
|
||||||
@@ -144,6 +202,7 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config := screenshot.DefaultConfig()
|
config := screenshot.DefaultConfig()
|
||||||
config.Mode = mode
|
config.Mode = mode
|
||||||
config.OutputName = ssOutputName
|
config.OutputName = ssOutputName
|
||||||
|
config.Seat = ssSeat
|
||||||
if strings.EqualFold(ssCursor, "on") {
|
if strings.EqualFold(ssCursor, "on") {
|
||||||
config.Cursor = screenshot.CursorOn
|
config.Cursor = screenshot.CursorOn
|
||||||
}
|
}
|
||||||
@@ -153,6 +212,15 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
config.NoConfirm = ssNoConfirm
|
config.NoConfirm = ssNoConfirm
|
||||||
config.Reset = ssReset
|
config.Reset = ssReset
|
||||||
config.Stdout = ssStdout
|
config.Stdout = ssStdout
|
||||||
|
config.Geometry = ssGeometry
|
||||||
|
config.AllowMultiple = ssAllowMulti
|
||||||
|
config.HUD = ssHUD
|
||||||
|
|
||||||
|
if ssGeometry {
|
||||||
|
config.Clipboard = false
|
||||||
|
config.SaveFile = false
|
||||||
|
config.Notify = false
|
||||||
|
}
|
||||||
|
|
||||||
if ssOutputDir != "" {
|
if ssOutputDir != "" {
|
||||||
config.OutputDir = ssOutputDir
|
config.OutputDir = ssOutputDir
|
||||||
@@ -181,58 +249,118 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
|||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select. Best-effort.
|
// setPopoutScreenshotMode toggles the shell handshake so popouts drop their keyboard grab during region select.
|
||||||
|
// Best-effort and not awaited: qs takes ~20ms to come up, longer than the selector needs to show.
|
||||||
func setPopoutScreenshotMode(begin bool) {
|
func setPopoutScreenshotMode(begin bool) {
|
||||||
fn := "end"
|
fn := "end"
|
||||||
if begin {
|
if begin {
|
||||||
fn = "begin"
|
fn = "begin"
|
||||||
}
|
}
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
if pid, ok := getFirstDMSPID(); ok {
|
if pid, ok := shellApp.SessionPID(); ok {
|
||||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
} else {
|
} else {
|
||||||
if err := findConfig(nil, nil); err != nil {
|
if err := shellApp.ResolveConfig(nil, nil); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if qsHasAnyDisplay() {
|
if qsHasAnyDisplay() {
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
|
cmdArgs = append(cmdArgs, "call", "screenshot", fn)
|
||||||
_ = exec.Command("qs", cmdArgs...).Run()
|
_ = exec.Command("qs", cmdArgs...).Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeScreenshotJSON(meta screenshotMetadata) {
|
||||||
|
_ = json.NewEncoder(os.Stdout).Encode(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitScreenshotError(context string, err error) {
|
||||||
|
if ssJSON {
|
||||||
|
writeScreenshotJSON(screenshotMetadata{Status: "error", Error: err.Error()})
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "Error%s: %v\n", context, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatMime(format screenshot.Format) string {
|
||||||
|
switch format {
|
||||||
|
case screenshot.FormatJPEG:
|
||||||
|
return "image/jpeg"
|
||||||
|
case screenshot.FormatPPM:
|
||||||
|
return "image/x-portable-pixmap"
|
||||||
|
default:
|
||||||
|
return "image/png"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runScreenshot(config screenshot.Config) {
|
func runScreenshot(config screenshot.Config) {
|
||||||
// Region select needs the keyboard; drop popout grabs for its duration.
|
if ssJSON && config.Stdout {
|
||||||
result, err := func() (*screenshot.CaptureResult, error) {
|
fmt.Fprintln(os.Stderr, "Error: --json cannot be combined with --stdout")
|
||||||
interactive := config.Mode == screenshot.ModeRegion || config.Mode == screenshot.ModeLastRegion
|
|
||||||
if interactive {
|
|
||||||
setPopoutScreenshotMode(true)
|
|
||||||
defer setPopoutScreenshotMode(false)
|
|
||||||
}
|
|
||||||
return screenshot.New(config).Run()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if config.Geometry {
|
||||||
|
if config.Stdout {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: --geometry cannot be combined with --stdout")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if config.Mode == screenshot.ModeScroll {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: --geometry cannot be combined with scroll mode")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short-lived process over a few tens of MB: let the heap grow instead of paying GC cycles mid-capture.
|
||||||
|
debug.SetGCPercent(-1)
|
||||||
|
debug.SetMemoryLimit(1 << 30)
|
||||||
|
|
||||||
|
// Region select needs the keyboard; drop popout grabs for its duration.
|
||||||
|
config.SelectorHook = setPopoutScreenshotMode
|
||||||
|
result, err := screenshot.New(config).Run()
|
||||||
|
if err != nil {
|
||||||
|
exitScreenshotError("", err)
|
||||||
|
}
|
||||||
|
|
||||||
if result == nil {
|
if result == nil {
|
||||||
|
if ssJSON {
|
||||||
|
writeScreenshotJSON(screenshotMetadata{Status: "aborted", Error: "User cancelled selection"})
|
||||||
|
}
|
||||||
|
if config.Geometry {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer result.Buffer.Close()
|
if config.Geometry {
|
||||||
|
if ssJSON {
|
||||||
|
x := int(result.Region.X)
|
||||||
|
y := int(result.Region.Y)
|
||||||
|
writeScreenshotJSON(screenshotMetadata{
|
||||||
|
Status: "success",
|
||||||
|
X: &x,
|
||||||
|
Y: &y,
|
||||||
|
Width: int(result.Region.Width),
|
||||||
|
Height: int(result.Region.Height),
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
fmt.Println(result.Region.GeometryString())
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
if result.YInverted {
|
if result.Buffer != nil {
|
||||||
result.Buffer.FlipVertical()
|
defer result.Buffer.Close()
|
||||||
|
|
||||||
|
if result.YInverted {
|
||||||
|
result.Buffer.FlipVertical()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Stdout {
|
if config.Stdout {
|
||||||
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
if err := writeImageToStdout(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error writing to stdout: %v\n", err)
|
exitScreenshotError(" writing to stdout", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -251,50 +379,64 @@ func runScreenshot(config screenshot.Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filePath = filepath.Join(outputDir, filename)
|
filePath = filepath.Join(outputDir, filename)
|
||||||
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format); err != nil {
|
if err := screenshot.WriteToFileWithFormat(result.Buffer, filePath, config.Format, config.Quality, result.Format, result.CICP); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error writing file: %v\n", err)
|
exitScreenshotError(" writing file", err)
|
||||||
os.Exit(1)
|
}
|
||||||
|
if !ssJSON {
|
||||||
|
fmt.Println(filePath)
|
||||||
}
|
}
|
||||||
fmt.Println(filePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Clipboard {
|
if config.Clipboard {
|
||||||
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format); err != nil {
|
if err := copyImageToClipboard(result.Buffer, config.Format, config.Quality, result.Format, result.CICP); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Error copying to clipboard: %v\n", err)
|
exitScreenshotError(" copying to clipboard", err)
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
if !config.SaveFile {
|
if !ssJSON && !config.SaveFile {
|
||||||
fmt.Println("Copied to clipboard")
|
fmt.Println("Copied to clipboard")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ssJSON {
|
||||||
|
scale := result.Scale
|
||||||
|
if scale <= 0 {
|
||||||
|
scale = 1.0
|
||||||
|
}
|
||||||
|
writeScreenshotJSON(screenshotMetadata{
|
||||||
|
Status: "success",
|
||||||
|
Path: filePath,
|
||||||
|
Width: result.Buffer.Width,
|
||||||
|
Height: result.Buffer.Height,
|
||||||
|
Scale: scale,
|
||||||
|
Mime: formatMime(config.Format),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if config.Notify {
|
if config.Notify {
|
||||||
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
thumbData, thumbW, thumbH := bufferToRGBThumbnail(result.Buffer, 256, result.Format)
|
||||||
screenshot.SendNotification(screenshot.NotifyResult{
|
id := screenshot.SendNotification(screenshot.NotifyResult{
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
Clipboard: config.Clipboard,
|
Clipboard: config.Clipboard,
|
||||||
ImageData: thumbData,
|
ImageData: thumbData,
|
||||||
Width: thumbW,
|
Width: thumbW,
|
||||||
Height: thumbH,
|
Height: thumbH,
|
||||||
})
|
})
|
||||||
|
watchNotificationAction(id, filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32, cicp *screenshot.CICP) error {
|
||||||
var mimeType string
|
var mimeType string
|
||||||
var data bytes.Buffer
|
var data bytes.Buffer
|
||||||
|
|
||||||
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
case screenshot.FormatJPEG:
|
case screenshot.FormatJPEG:
|
||||||
mimeType = "image/jpeg"
|
mimeType = "image/jpeg"
|
||||||
if err := screenshot.EncodeJPEG(&data, img, quality); err != nil {
|
if err := screenshot.EncodeBufferJPEG(&data, buf, pixelFormat, quality); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
mimeType = "image/png"
|
mimeType = "image/png"
|
||||||
if err := screenshot.EncodePNG(&data, img); err != nil {
|
if err := screenshot.EncodeBufferPNG(&data, buf, pixelFormat, cicp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,14 +444,12 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
|
|||||||
return clipboard.Copy(data.Bytes(), mimeType)
|
return clipboard.Copy(data.Bytes(), mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32) error {
|
func writeImageToStdout(buf *screenshot.ShmBuffer, format screenshot.Format, quality int, pixelFormat uint32, cicp *screenshot.CICP) error {
|
||||||
img := screenshot.BufferToImageWithFormat(buf, pixelFormat)
|
|
||||||
|
|
||||||
switch format {
|
switch format {
|
||||||
case screenshot.FormatJPEG:
|
case screenshot.FormatJPEG:
|
||||||
return screenshot.EncodeJPEG(os.Stdout, img, quality)
|
return screenshot.EncodeBufferJPEG(os.Stdout, buf, pixelFormat, quality)
|
||||||
default:
|
default:
|
||||||
return screenshot.EncodePNG(os.Stdout, img)
|
return screenshot.EncodeBufferPNG(os.Stdout, buf, pixelFormat, cicp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +476,12 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
|
|||||||
data := buf.Data()
|
data := buf.Data()
|
||||||
rgb := make([]byte, dstW*dstH*3)
|
rgb := make([]byte, dstW*dstH*3)
|
||||||
|
|
||||||
|
is10Bit := screenshot.PixelFormat(pixelFormat).Is10Bit()
|
||||||
|
|
||||||
var swapRB bool
|
var swapRB bool
|
||||||
switch pixelFormat {
|
switch pixelFormat {
|
||||||
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
|
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888),
|
||||||
|
uint32(screenshot.FormatABGR2101010), uint32(screenshot.FormatXBGR2101010):
|
||||||
swapRB = false
|
swapRB = false
|
||||||
default:
|
default:
|
||||||
swapRB = true
|
swapRB = true
|
||||||
@@ -359,11 +502,19 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
|
|||||||
if si+3 >= len(data) {
|
if si+3 >= len(data) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if swapRB {
|
switch {
|
||||||
|
case is10Bit:
|
||||||
|
v := binary.LittleEndian.Uint32(data[si:])
|
||||||
|
c0, c1, c2 := uint8(v>>2), uint8(v>>12), uint8(v>>22)
|
||||||
|
if swapRB {
|
||||||
|
c0, c2 = c2, c0
|
||||||
|
}
|
||||||
|
rgb[di+0], rgb[di+1], rgb[di+2] = c0, c1, c2
|
||||||
|
case swapRB:
|
||||||
rgb[di+0] = data[si+2]
|
rgb[di+0] = data[si+2]
|
||||||
rgb[di+1] = data[si+1]
|
rgb[di+1] = data[si+1]
|
||||||
rgb[di+2] = data[si+0]
|
rgb[di+2] = data[si+0]
|
||||||
} else {
|
default:
|
||||||
rgb[di+0] = data[si+0]
|
rgb[di+0] = data[si+0]
|
||||||
rgb[di+1] = data[si+1]
|
rgb[di+1] = data[si+1]
|
||||||
rgb[di+2] = data[si+2]
|
rgb[di+2] = data[si+2]
|
||||||
@@ -378,6 +529,12 @@ func runScreenshotRegion(cmd *cobra.Command, args []string) {
|
|||||||
runScreenshot(config)
|
runScreenshot(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runScreenshotScroll(cmd *cobra.Command, args []string) {
|
||||||
|
config := getScreenshotConfig(screenshot.ModeScroll)
|
||||||
|
config.IntervalMs = min(max(ssScrollInterval, 30), 1000)
|
||||||
|
runScreenshot(config)
|
||||||
|
}
|
||||||
|
|
||||||
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
func runScreenshotFull(cmd *cobra.Command, args []string) {
|
||||||
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
config := getScreenshotConfig(screenshot.ModeFullScreen)
|
||||||
runScreenshot(config)
|
runScreenshot(config)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ func showSession(id string) (sessionInfo, error) {
|
|||||||
return sessionInfo{}, err
|
return sessionInfo{}, err
|
||||||
}
|
}
|
||||||
fields := map[string]string{}
|
fields := map[string]string{}
|
||||||
for _, line := range strings.Split(string(out), "\n") {
|
for line := range strings.SplitSeq(string(out), "\n") {
|
||||||
idx := strings.IndexByte(line, '=')
|
idx := strings.IndexByte(line, '=')
|
||||||
if idx <= 0 {
|
if idx <= 0 {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
@@ -143,6 +143,10 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{
|
|||||||
niriFile: "alttab.kdl",
|
niriFile: "alttab.kdl",
|
||||||
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
||||||
},
|
},
|
||||||
|
"input": {
|
||||||
|
niriFile: "input.kdl",
|
||||||
|
niriContent: func(_ string) string { return config.NiriInputConfig },
|
||||||
|
},
|
||||||
"outputs": {
|
"outputs": {
|
||||||
niriFile: "outputs.kdl",
|
niriFile: "outputs.kdl",
|
||||||
hyprFile: "outputs.lua",
|
hyprFile: "outputs.lua",
|
||||||
@@ -204,7 +208,12 @@ func detectTerminal() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func detectCompositorForSetup() (string, error) {
|
func detectCompositorForSetup() (string, error) {
|
||||||
compositors := greeter.DetectCompositors()
|
var compositors []string
|
||||||
|
for _, candidate := range []string{"niri", "Hyprland", "mango"} {
|
||||||
|
if utils.CommandExists(candidate) {
|
||||||
|
compositors = append(compositors, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch len(compositors) {
|
switch len(compositors) {
|
||||||
case 0:
|
case 0:
|
||||||
@@ -213,11 +222,20 @@ func detectCompositorForSetup() (string, error) {
|
|||||||
return strings.ToLower(compositors[0]), nil
|
return strings.ToLower(compositors[0]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
selected, err := greeter.PromptCompositorChoice(compositors)
|
fmt.Println("Multiple compositors detected:")
|
||||||
if err != nil {
|
for i, compositor := range compositors {
|
||||||
return "", err
|
fmt.Printf("%d) %s\n", i+1, compositor)
|
||||||
}
|
}
|
||||||
return strings.ToLower(selected), nil
|
fmt.Printf("\nChoice (1-%d): ", len(compositors))
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
choice := 0
|
||||||
|
fmt.Sscanf(strings.TrimSpace(response), "%d", &choice)
|
||||||
|
if choice < 1 || choice > len(compositors) {
|
||||||
|
return "", fmt.Errorf("invalid choice")
|
||||||
|
}
|
||||||
|
return strings.ToLower(compositors[choice-1]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSetupDmsConfig(name string) error {
|
func runSetupDmsConfig(name string) error {
|
||||||
@@ -298,6 +316,9 @@ func runSetup() error {
|
|||||||
if wmSelected {
|
if wmSelected {
|
||||||
if wm == deps.WindowManagerMango {
|
if wm == deps.WindowManagerMango {
|
||||||
useSystemd = false
|
useSystemd = false
|
||||||
|
} else if isVoidSetup() {
|
||||||
|
useSystemd = false
|
||||||
|
fmt.Println("\nVoid Linux detected; deploying non-systemd session config.")
|
||||||
} else {
|
} else {
|
||||||
useSystemd = promptSystemd()
|
useSystemd = promptSystemd()
|
||||||
}
|
}
|
||||||
@@ -335,22 +356,18 @@ func runSetup() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
var results []config.DeploymentResult
|
var results []config.DeploymentResult
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if wmSelected && terminalSelected {
|
if wmSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
|
var result config.DeploymentResult
|
||||||
} else if wmSelected {
|
result, err = deployer.DeployCompositor(wm, terminal, useSystemd)
|
||||||
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
|
results = append(results, result)
|
||||||
if len(results) > 1 {
|
}
|
||||||
results = results[:1]
|
if err == nil && terminalSelected {
|
||||||
}
|
var terminalResults []config.DeploymentResult
|
||||||
} else if terminalSelected {
|
terminalResults, err = deployer.DeployTerminal(terminal)
|
||||||
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
|
results = append(results, terminalResults...)
|
||||||
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
|
||||||
results = results[1:]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(logChan)
|
close(logChan)
|
||||||
@@ -372,6 +389,15 @@ func runSetup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isVoidSetup() bool {
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||||
|
return exists && config.Family == distros.FamilyVoid
|
||||||
|
}
|
||||||
|
|
||||||
// Add user to the input group for the evdev manager for inut state tracking.
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
// Caps Lock OSD and the Caps Lock bar indicator.
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
func ensureInputGroup() {
|
func ensureInputGroup() {
|
||||||
@@ -465,49 +491,54 @@ func promptSystemd() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
|
func checkExistingConfigs(wm deps.WindowManager, wmSelected bool, terminal deps.Terminal, terminalSelected bool) bool {
|
||||||
homeDir := os.Getenv("HOME")
|
if wmSelected && firstExistingPath(compositorPrimaryPaths(wm)) != "" {
|
||||||
willBackup := false
|
return true
|
||||||
|
|
||||||
if wmSelected {
|
|
||||||
var configPaths []string
|
|
||||||
switch wm {
|
|
||||||
case deps.WindowManagerNiri:
|
|
||||||
configPaths = []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
|
|
||||||
case deps.WindowManagerHyprland:
|
|
||||||
configPaths = []string{
|
|
||||||
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
|
||||||
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
|
||||||
}
|
|
||||||
case deps.WindowManagerMango:
|
|
||||||
configPaths = []string{
|
|
||||||
filepath.Join(homeDir, ".config", "mango", "config.conf"),
|
|
||||||
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, configPath := range configPaths {
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
|
||||||
willBackup = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return terminalSelected && firstExistingPath([]string{terminalPrimaryPath(terminal)}) != ""
|
||||||
|
}
|
||||||
|
|
||||||
if terminalSelected {
|
func compositorPrimaryPaths(wm deps.WindowManager) []string {
|
||||||
var configPath string
|
homeDir := os.Getenv("HOME")
|
||||||
switch terminal {
|
switch wm {
|
||||||
case deps.TerminalGhostty:
|
case deps.WindowManagerNiri:
|
||||||
configPath = filepath.Join(homeDir, ".config", "ghostty", "config")
|
return []string{filepath.Join(homeDir, ".config", "niri", "config.kdl")}
|
||||||
case deps.TerminalKitty:
|
case deps.WindowManagerHyprland:
|
||||||
configPath = filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
|
return []string{
|
||||||
case deps.TerminalAlacritty:
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.lua"),
|
||||||
configPath = filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
|
filepath.Join(homeDir, ".config", "hypr", "hyprland.conf"),
|
||||||
}
|
}
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
return []string{
|
||||||
|
filepath.Join(homeDir, ".config", "mango", "config.conf"),
|
||||||
|
filepath.Join(homeDir, ".config", "mango", "mango.conf"),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func terminalPrimaryPath(terminal deps.Terminal) string {
|
||||||
|
homeDir := os.Getenv("HOME")
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
return filepath.Join(homeDir, ".config", "ghostty", "config")
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
return filepath.Join(homeDir, ".config", "kitty", "kitty.conf")
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
return filepath.Join(homeDir, ".config", "alacritty", "alacritty.toml")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := os.Stat(configPath); err == nil {
|
func firstExistingPath(paths []string) string {
|
||||||
willBackup = true
|
for _, path := range paths {
|
||||||
|
if path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return path
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
return willBackup
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var setupHeadlessCmd = &cobra.Command{
|
||||||
|
Use: "headless",
|
||||||
|
Short: "Deploy DMS configurations without prompts",
|
||||||
|
Long: "Deploy compositor and terminal configurations non-interactively, for installers and scripts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
compositor, _ := cmd.Flags().GetString("compositor")
|
||||||
|
terminal, _ := cmd.Flags().GetString("terminal")
|
||||||
|
noSystemd, _ := cmd.Flags().GetBool("no-systemd")
|
||||||
|
force, _ := cmd.Flags().GetBool("force")
|
||||||
|
skipExisting, _ := cmd.Flags().GetBool("skip-existing")
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
return runSetupHeadless(compositor, terminal, noSystemd, force, skipExisting)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
setupHeadlessCmd.Flags().String("compositor", "", "Compositor to configure: niri, hyprland, or mango")
|
||||||
|
setupHeadlessCmd.Flags().String("terminal", "", "Also deploy a terminal config: ghostty, kitty, or alacritty")
|
||||||
|
setupHeadlessCmd.Flags().Bool("no-systemd", false, "Deploy session config without systemd integration")
|
||||||
|
setupHeadlessCmd.Flags().Bool("force", false, "Overwrite existing configs (timestamped backups are created)")
|
||||||
|
setupHeadlessCmd.Flags().Bool("skip-existing", false, "Warn and skip instead of failing when a config already exists")
|
||||||
|
_ = setupHeadlessCmd.MarkFlagRequired("compositor")
|
||||||
|
setupHeadlessCmd.MarkFlagsMutuallyExclusive("force", "skip-existing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSetupHeadless(compositor, terminal string, noSystemd, force, skipExisting bool) error {
|
||||||
|
wm, err := parseHeadlessCompositor(compositor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
term, termSelected, err := parseHeadlessTerminal(terminal)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deployCompositor := true
|
||||||
|
deployTerminal := termSelected
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
if existing := firstExistingPath(compositorPrimaryPaths(wm)); existing != "" {
|
||||||
|
if !skipExisting {
|
||||||
|
return fmt.Errorf("%s config already exists: %s (use --force to overwrite with backup, or --skip-existing)", compositor, existing)
|
||||||
|
}
|
||||||
|
fmt.Printf("⚠ Skipping %s config, already exists: %s\n", compositor, existing)
|
||||||
|
deployCompositor = false
|
||||||
|
}
|
||||||
|
if deployTerminal {
|
||||||
|
if existing := firstExistingPath([]string{terminalPrimaryPath(term)}); existing != "" {
|
||||||
|
if !skipExisting {
|
||||||
|
return fmt.Errorf("%s config already exists: %s (use --force to overwrite with backup, or --skip-existing)", terminal, existing)
|
||||||
|
}
|
||||||
|
fmt.Printf("⚠ Skipping %s config, already exists: %s\n", terminal, existing)
|
||||||
|
deployTerminal = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !deployCompositor && !deployTerminal {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
logDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
for msg := range logChan {
|
||||||
|
fmt.Println(" " + msg)
|
||||||
|
}
|
||||||
|
close(logDone)
|
||||||
|
}()
|
||||||
|
deployer := config.NewConfigDeployer(logChan)
|
||||||
|
|
||||||
|
var results []config.DeploymentResult
|
||||||
|
var deployErr error
|
||||||
|
if deployCompositor {
|
||||||
|
result, err := deployer.DeployCompositor(wm, term, headlessUseSystemd(wm, noSystemd))
|
||||||
|
results = append(results, result)
|
||||||
|
deployErr = err
|
||||||
|
}
|
||||||
|
if deployErr == nil && deployTerminal {
|
||||||
|
terminalResults, err := deployer.DeployTerminal(term)
|
||||||
|
results = append(results, terminalResults...)
|
||||||
|
deployErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
close(logChan)
|
||||||
|
<-logDone
|
||||||
|
|
||||||
|
for _, result := range results {
|
||||||
|
if !result.Deployed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ %s: %s\n", result.ConfigType, result.Path)
|
||||||
|
if result.BackupPath != "" {
|
||||||
|
fmt.Printf(" Backup: %s\n", result.BackupPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployErr != nil {
|
||||||
|
return fmt.Errorf("deployment failed: %w", deployErr)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeadlessCompositor(name string) (deps.WindowManager, error) {
|
||||||
|
switch name {
|
||||||
|
case "niri":
|
||||||
|
return deps.WindowManagerNiri, nil
|
||||||
|
case "hyprland":
|
||||||
|
return deps.WindowManagerHyprland, nil
|
||||||
|
case "mango":
|
||||||
|
return deps.WindowManagerMango, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown compositor %q (expected niri, hyprland, or mango)", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHeadlessTerminal(name string) (deps.Terminal, bool, error) {
|
||||||
|
switch name {
|
||||||
|
case "":
|
||||||
|
return deps.TerminalGhostty, false, nil
|
||||||
|
case "ghostty":
|
||||||
|
return deps.TerminalGhostty, true, nil
|
||||||
|
case "kitty":
|
||||||
|
return deps.TerminalKitty, true, nil
|
||||||
|
case "alacritty":
|
||||||
|
return deps.TerminalAlacritty, true, nil
|
||||||
|
default:
|
||||||
|
return 0, false, fmt.Errorf("unknown terminal %q (expected ghostty, kitty, or alacritty)", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func headlessUseSystemd(wm deps.WindowManager, noSystemd bool) bool {
|
||||||
|
if noSystemd || wm == deps.WindowManagerMango {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !isVoidSetup()
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ var (
|
|||||||
sysUpdateJSON bool
|
sysUpdateJSON bool
|
||||||
sysUpdateNoFlatpak bool
|
sysUpdateNoFlatpak bool
|
||||||
sysUpdateNoAUR bool
|
sysUpdateNoAUR bool
|
||||||
|
sysUpdateIgnore []string
|
||||||
sysUpdateIntervalS int
|
sysUpdateIntervalS int
|
||||||
sysUpdateListPmTime = 5 * time.Minute
|
sysUpdateListPmTime = 5 * time.Minute
|
||||||
)
|
)
|
||||||
@@ -57,7 +58,8 @@ func init() {
|
|||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateDry, "dry", false, "Simulate the upgrade without applying changes")
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateJSON, "json", false, "Output as JSON (with --check)")
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoFlatpak, "no-flatpak", false, "Skip the Flatpak overlay")
|
||||||
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip the AUR (paru/yay only)")
|
systemUpdateCmd.Flags().BoolVar(&sysUpdateNoAUR, "no-aur", false, "Skip AUR updates")
|
||||||
|
systemUpdateCmd.Flags().StringSliceVar(&sysUpdateIgnore, "ignore", nil, "Skip specific packages (repeatable or comma-separated)")
|
||||||
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
systemUpdateCmd.Flags().IntVar(&sysUpdateIntervalS, "interval", -1, "Set the DMS server poll interval in seconds and exit (requires running server)")
|
||||||
|
|
||||||
systemCmd.AddCommand(systemUpdateCmd)
|
systemCmd.AddCommand(systemUpdateCmd)
|
||||||
@@ -192,6 +194,7 @@ func runSystemUpdateApply() {
|
|||||||
Targets: pkgs,
|
Targets: pkgs,
|
||||||
IncludeFlatpak: !sysUpdateNoFlatpak,
|
IncludeFlatpak: !sysUpdateNoFlatpak,
|
||||||
IncludeAUR: !sysUpdateNoAUR,
|
IncludeAUR: !sysUpdateNoAUR,
|
||||||
|
Ignored: sysUpdateIgnore,
|
||||||
DryRun: sysUpdateDry,
|
DryRun: sysUpdateDry,
|
||||||
UseSudo: true,
|
UseSudo: true,
|
||||||
}
|
}
|
||||||
@@ -234,12 +237,19 @@ func collectUpdates(ctx context.Context, backends []sysupdate.Backend) ([]sysupd
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
func filterUpdateTargets(pkgs []sysupdate.Package) []sysupdate.Package {
|
||||||
if !sysUpdateNoAUR {
|
if !sysUpdateNoAUR && len(sysUpdateIgnore) == 0 {
|
||||||
return pkgs
|
return pkgs
|
||||||
}
|
}
|
||||||
|
ignored := make(map[string]bool, len(sysUpdateIgnore))
|
||||||
|
for _, name := range sysUpdateIgnore {
|
||||||
|
ignored[name] = true
|
||||||
|
}
|
||||||
out := pkgs[:0]
|
out := pkgs[:0]
|
||||||
for _, p := range pkgs {
|
for _, p := range pkgs {
|
||||||
if p.Repo == sysupdate.RepoAUR {
|
if sysUpdateNoAUR && p.Repo == sysupdate.RepoAUR {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ignored[p.Name] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
out = append(out, p)
|
out = append(out, p)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_power"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type cmd struct {
|
type cmd struct {
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
|
||||||
)
|
|
||||||
|
|
||||||
// runit (Void Linux) service helpers. Services live in /etc/sv and are "enabled"
|
|
||||||
// by symlinking them into the /var/service supervision dir, so the greeter
|
|
||||||
// commands branch on isRunit() instead of shelling systemctl.
|
|
||||||
|
|
||||||
const (
|
|
||||||
runitSvDir = "/etc/sv"
|
|
||||||
runitServiceDir = "/var/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
// isRunit reports whether this system is supervised by runit (Void Linux).
|
|
||||||
func isRunit() bool {
|
|
||||||
if fi, err := os.Stat("/run/runit"); err == nil && fi.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if _, err := os.Stat("/run/systemd/system"); err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if fi, err := os.Stat(runitServiceDir); err == nil && fi.IsDir() {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func runitServiceInstalled(name string) bool {
|
|
||||||
fi, err := os.Stat(runitSvDir + "/" + name)
|
|
||||||
return err == nil && fi.IsDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runitServiceEnabled(name string) bool {
|
|
||||||
_, err := os.Lstat(runitServiceDir + "/" + name)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// enableRunitService links a service into /var/service (idempotent).
|
|
||||||
func enableRunitService(name string) error {
|
|
||||||
if !runitServiceInstalled(name) {
|
|
||||||
return fmt.Errorf("runit service %q not found in %s", name, runitSvDir)
|
|
||||||
}
|
|
||||||
if runitServiceEnabled(name) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return privesc.Run(context.Background(), "", "ln", "-sf",
|
|
||||||
runitSvDir+"/"+name, runitServiceDir+"/"+name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// disableRunitService removes a service's supervision symlink.
|
|
||||||
func disableRunitService(name string) error {
|
|
||||||
if !runitServiceEnabled(name) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return privesc.Run(context.Background(), "", "rm", "-f",
|
|
||||||
runitServiceDir+"/"+name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureRunitSeat sets up the seat access a Wayland greeter needs on runit (the
|
|
||||||
// equivalent of logind on systemd): enables seatd and adds the greeter user to
|
|
||||||
// the seat/video/input groups. Failures are reported but non-fatal.
|
|
||||||
func ensureRunitSeat(greeterUser string) {
|
|
||||||
if runitServiceInstalled("seatd") {
|
|
||||||
if err := enableRunitService("seatd"); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not enable seatd: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println(" ✓ seatd enabled")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println(" ⚠ seatd not installed — the greeter compositor needs it for GPU/seat access")
|
|
||||||
}
|
|
||||||
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "_seatd,video,input", greeterUser); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not add %s to seat groups: %v\n", greeterUser, err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf(" ✓ %s added to seat groups (_seatd, video, input)\n", greeterUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensureGreetdPamRundir adds pam_rundir to the greetd PAM stack so the post-login
|
|
||||||
// session gets an XDG_RUNTIME_DIR on systems without logind (Void with seatd).
|
|
||||||
// Appended outside DMS's managed auth block so it survives `dms greeter sync`.
|
|
||||||
func ensureGreetdPamRundir() {
|
|
||||||
const pamPath = "/etc/pam.d/greetd"
|
|
||||||
data, err := os.ReadFile(pamPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not read %s: %v\n", pamPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(string(data), "pam_rundir") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
line := "session optional pam_rundir.so"
|
|
||||||
if err := privesc.Run(context.Background(), "", "sh", "-c",
|
|
||||||
fmt.Sprintf("printf '%%s\\n' %q >> %s", line, pamPath)); err != nil {
|
|
||||||
fmt.Printf(" ⚠ could not add pam_rundir to %s: %v\n", pamPath, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Println(" ✓ pam_rundir added to greetd PAM (provides XDG_RUNTIME_DIR for the session)")
|
|
||||||
}
|
|
||||||
|
|
||||||
// startGreeterHint returns the init-appropriate "start greetd now" command.
|
|
||||||
func startGreeterHint() string {
|
|
||||||
if isRunit() {
|
|
||||||
return " sudo sv up greetd"
|
|
||||||
}
|
|
||||||
return " sudo systemctl start greetd"
|
|
||||||
}
|
|
||||||
+5
-11
@@ -12,18 +12,11 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
setupCmd.AddCommand(setupHeadlessCmd, setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
|
||||||
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
|
|
||||||
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
|
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
|
||||||
authCmd.AddCommand(authSyncCmd)
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd)
|
||||||
|
registryCmd.AddCommand(registryListCmd, registryAddCmd, registryRemoveCmd)
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(authCmd)
|
rootCmd.AddCommand(authCmd)
|
||||||
@@ -33,6 +26,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
disableMemProfilingUnlessRequested()
|
||||||
clipboard.MaybeServeAndExit()
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
|
|||||||
@@ -12,17 +12,9 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
authCmd.AddCommand(authSyncCmd, authResolveLockCmd, authListServicesCmd, authValidateCmd)
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
setupCmd.AddCommand(setupHeadlessCmd, setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd, pluginsLockCmd, pluginsRestoreCmd)
|
||||||
runCmd.Flags().String("log-level", "", "Log level: debug, info, warn, error, fatal (overrides DMS_LOG_LEVEL)")
|
|
||||||
runCmd.Flags().String("log-file", "", "Append logs to this file in addition to stderr (overrides DMS_LOG_FILE)")
|
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
|
||||||
|
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd, greeterLaunchSessionCmd)
|
|
||||||
authCmd.AddCommand(authSyncCmd)
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
rootCmd.AddCommand(authCmd)
|
rootCmd.AddCommand(authCmd)
|
||||||
|
|
||||||
@@ -30,6 +22,7 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
disableMemProfilingUnlessRequested()
|
||||||
clipboard.MaybeServeAndExit()
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type randrOutput struct {
|
type randrOutput struct {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
|
// maxIPCMessageSize allows room for a 50 MB clipboard entry plus JSON/base64
|
||||||
@@ -96,16 +97,10 @@ func tryServerRequest(req models.Request) (*models.Response[any], bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getServerSocketPath() string {
|
func getServerSocketPath() string {
|
||||||
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
runtimeDir := utils.RuntimeDir()
|
||||||
if runtimeDir == "" {
|
|
||||||
runtimeDir = os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
if parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY")); ok {
|
if sessionSock, ok := shellApp.SessionSocketPath(); ok {
|
||||||
sessionSock := filepath.Join(runtimeDir, fmt.Sprintf("danklinux-%d.sock", parentPID))
|
return sessionSock
|
||||||
if _, err := os.Stat(sessionSock); err == nil {
|
|
||||||
return sessionSock
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries, err := os.ReadDir(runtimeDir)
|
entries, err := os.ReadDir(runtimeDir)
|
||||||
|
|||||||
+33
-690
@@ -4,583 +4,21 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/qsipc"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ipcTargets map[string]map[string][]string
|
type ipcTargets map[string]map[string][]string
|
||||||
|
|
||||||
// getProcessExitCode returns the exit code from a ProcessState.
|
|
||||||
// For normal exits, returns the exit code directly.
|
|
||||||
// For signal termination, returns 128 + signal number (Unix convention).
|
|
||||||
func getProcessExitCode(state *os.ProcessState) int {
|
|
||||||
if state == nil {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if code := state.ExitCode(); code != -1 {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
// Process was killed by signal - extract signal number
|
|
||||||
if status, ok := state.Sys().(syscall.WaitStatus); ok {
|
|
||||||
if status.Signaled() {
|
|
||||||
return 128 + int(status.Signal())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var isSessionManaged bool
|
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
|
||||||
selfPath, err := os.Executable()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(selfPath, "restart-detached", strconv.Itoa(targetPID))
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setsid: true,
|
|
||||||
}
|
|
||||||
cmd.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
func runDetachedRestart(targetPIDStr string) {
|
|
||||||
targetPID, err := strconv.Atoi(targetPIDStr)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(200 * time.Millisecond)
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(targetPID)
|
|
||||||
if err == nil {
|
|
||||||
proc.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
|
|
||||||
killShell()
|
|
||||||
runShellDaemon(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getRuntimeDir() string {
|
|
||||||
if runtime := os.Getenv("XDG_RUNTIME_DIR"); runtime != "" {
|
|
||||||
return runtime
|
|
||||||
}
|
|
||||||
return os.TempDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendLogEnv(env []string) []string {
|
|
||||||
if v := os.Getenv("DMS_LOG_LEVEL"); v != "" {
|
|
||||||
env = append(env, "DMS_LOG_LEVEL="+v)
|
|
||||||
}
|
|
||||||
if v := os.Getenv("DMS_LOG_FILE"); v != "" {
|
|
||||||
env = append(env, "DMS_LOG_FILE="+v)
|
|
||||||
}
|
|
||||||
return env
|
|
||||||
}
|
|
||||||
|
|
||||||
func hasSystemdRun() bool {
|
|
||||||
_, err := exec.LookPath("systemd-run")
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPIDFilePath() string {
|
|
||||||
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSessionFilePath() string {
|
|
||||||
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.session", os.Getpid()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func writePIDFile(childPID int) error {
|
|
||||||
pidFile := getPIDFilePath()
|
|
||||||
if display := os.Getenv("WAYLAND_DISPLAY"); display != "" {
|
|
||||||
if err := os.WriteFile(getSessionFilePath(), []byte(display), 0o644); err != nil {
|
|
||||||
log.Warnf("Failed to write session file: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return os.WriteFile(pidFile, []byte(strconv.Itoa(childPID)), 0o644)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removePIDFile() {
|
|
||||||
os.Remove(getPIDFilePath())
|
|
||||||
os.Remove(getSessionFilePath())
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllDMSPIDs() []int {
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var pids []int
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pidFile := filepath.Join(dir, entry.Name())
|
|
||||||
data, err := os.ReadFile(pidFile)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
childPID, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(pidFile)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(childPID)
|
|
||||||
if err != nil {
|
|
||||||
os.Remove(pidFile)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
|
||||||
os.Remove(pidFile)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pids = append(pids, childPID)
|
|
||||||
|
|
||||||
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
|
||||||
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
|
||||||
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
|
||||||
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
|
||||||
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
|
||||||
pids = append(pids, parentPID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pids
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellInteractive(session bool) {
|
|
||||||
isSessionManaged = session
|
|
||||||
go printASCII()
|
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
socketPath := server.GetSocketPath()
|
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(configStateFile)
|
|
||||||
|
|
||||||
errChan := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
server.CLIVersion = Version
|
|
||||||
if err := server.Start(false); err != nil {
|
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ensureFontCache()
|
|
||||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
|
||||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
|
||||||
if !strings.HasPrefix(configPath, homeDir) {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
|
||||||
}
|
|
||||||
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = appendLogEnv(cmd.Env)
|
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
tracker := &stderrTracker{parent: os.Stderr}
|
|
||||||
cmd.Stderr = tracker
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Fatalf("Error starting quickshell: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write PID file for the quickshell child process
|
|
||||||
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
|
||||||
log.Warnf("Failed to write PID file: %v", err)
|
|
||||||
}
|
|
||||||
defer removePIDFile()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
|
||||||
} else {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sig := <-sigChan:
|
|
||||||
if sig == syscall.SIGUSR1 {
|
|
||||||
if isSessionManaged {
|
|
||||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
|
||||||
execDetachedRestart(os.Getpid())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
|
||||||
select {
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
os.Remove(socketPath)
|
|
||||||
exitCode := getProcessExitCode(cmd.ProcessState)
|
|
||||||
logStartupFailure(startTime, exitCode, tracker)
|
|
||||||
os.Exit(exitCode)
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
return
|
|
||||||
|
|
||||||
case err := <-errChan:
|
|
||||||
log.Error(err)
|
|
||||||
cancel()
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
os.Remove(socketPath)
|
|
||||||
exitCode := getProcessExitCode(cmd.ProcessState)
|
|
||||||
logStartupFailure(startTime, exitCode, tracker)
|
|
||||||
os.Exit(exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func restartShell() {
|
|
||||||
pids := getAllDMSPIDs()
|
|
||||||
|
|
||||||
if len(pids) == 0 {
|
|
||||||
log.Info("No running DMS shell instances found. Starting daemon...")
|
|
||||||
runShellDaemon(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPid := os.Getpid()
|
|
||||||
uniquePids := make(map[int]bool)
|
|
||||||
|
|
||||||
for _, pid := range pids {
|
|
||||||
if pid != currentPid {
|
|
||||||
uniquePids[pid] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for pid := range uniquePids {
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error finding process %d: %v", pid, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.SIGUSR1); err != nil {
|
|
||||||
log.Errorf("Error sending SIGUSR1 to process %d: %v", pid, err)
|
|
||||||
} else {
|
|
||||||
log.Infof("Sent SIGUSR1 to DMS process with PID %d", pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func killShell() {
|
|
||||||
pids := getAllDMSPIDs()
|
|
||||||
|
|
||||||
if len(pids) == 0 {
|
|
||||||
log.Info("No running DMS shell instances found.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPid := os.Getpid()
|
|
||||||
uniquePids := make(map[int]bool)
|
|
||||||
|
|
||||||
for _, pid := range pids {
|
|
||||||
if pid != currentPid {
|
|
||||||
uniquePids[pid] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for pid := range uniquePids {
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error finding process %d: %v", pid, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proc.Kill(); err != nil {
|
|
||||||
log.Errorf("Error killing process %d: %v", pid, err)
|
|
||||||
} else {
|
|
||||||
log.Infof("Killed DMS process with PID %d", pid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(entry.Name(), ".pid") || strings.HasSuffix(entry.Name(), ".session") {
|
|
||||||
os.Remove(filepath.Join(dir, entry.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellDaemon(session bool) {
|
|
||||||
isSessionManaged = session
|
|
||||||
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
|
|
||||||
|
|
||||||
if !isDaemonChild {
|
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
|
||||||
|
|
||||||
cmd := exec.Command(os.Args[0], "run", "-d", "--daemon-child")
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
|
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
||||||
Setsid: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Fatalf("Error starting daemon: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("DMS shell daemon started (PID: %d)", cmd.Process.Pid)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
socketPath := server.GetSocketPath()
|
|
||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
|
||||||
if err := os.WriteFile(configStateFile, []byte(configPath), 0o644); err != nil {
|
|
||||||
log.Warnf("Failed to write config state file: %v", err)
|
|
||||||
}
|
|
||||||
defer os.Remove(configStateFile)
|
|
||||||
|
|
||||||
errChan := make(chan error, 2)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
server.CLIVersion = Version
|
|
||||||
if err := server.Start(false); err != nil {
|
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
ensureFontCache()
|
|
||||||
log.Infof("Spawning quickshell with -p %s", configPath)
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
|
||||||
if os.Getenv("QT_LOGGING_RULES") == "" {
|
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
|
||||||
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
|
||||||
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
|
|
||||||
if !strings.HasPrefix(configPath, homeDir) {
|
|
||||||
cmd.Env = append(cmd.Env, "DMS_DISABLE_HOT_RELOAD=1")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORMTHEME_QT6") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
|
||||||
}
|
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
|
||||||
}
|
|
||||||
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
|
|
||||||
cmd.Env = append(cmd.Env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Env = appendLogEnv(cmd.Env)
|
|
||||||
|
|
||||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error opening /dev/null: %v", err)
|
|
||||||
}
|
|
||||||
defer devNull.Close()
|
|
||||||
|
|
||||||
cmd.Stdin = devNull
|
|
||||||
cmd.Stdout = devNull
|
|
||||||
tracker := &stderrTracker{parent: devNull}
|
|
||||||
cmd.Stderr = tracker
|
|
||||||
|
|
||||||
startTime := time.Now()
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
log.Fatalf("Error starting daemon: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write PID file for the quickshell child process
|
|
||||||
if err := writePIDFile(cmd.Process.Pid); err != nil {
|
|
||||||
log.Warnf("Failed to write PID file: %v", err)
|
|
||||||
}
|
|
||||||
defer removePIDFile()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sigChan := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
if err := cmd.Wait(); err != nil {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited: %w", err)
|
|
||||||
} else {
|
|
||||||
errChan <- fmt.Errorf("quickshell exited")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case sig := <-sigChan:
|
|
||||||
if sig == syscall.SIGUSR1 {
|
|
||||||
if isSessionManaged {
|
|
||||||
log.Infof("Received SIGUSR1, exiting for systemd restart...")
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
|
||||||
execDetachedRestart(os.Getpid())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if qs already crashed before we got SIGTERM (systemd sends SIGTERM when D-Bus name is released)
|
|
||||||
select {
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
os.Remove(socketPath)
|
|
||||||
exitCode := getProcessExitCode(cmd.ProcessState)
|
|
||||||
logStartupFailure(startTime, exitCode, tracker)
|
|
||||||
os.Exit(exitCode)
|
|
||||||
case <-time.After(500 * time.Millisecond):
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel()
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
os.Remove(socketPath)
|
|
||||||
return
|
|
||||||
|
|
||||||
case <-errChan:
|
|
||||||
cancel()
|
|
||||||
if cmd.Process != nil {
|
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
|
||||||
}
|
|
||||||
os.Remove(socketPath)
|
|
||||||
exitCode := getProcessExitCode(cmd.ProcessState)
|
|
||||||
logStartupFailure(startTime, exitCode, tracker)
|
|
||||||
os.Exit(exitCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -624,17 +62,17 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
|||||||
|
|
||||||
func buildQsIPCBaseArgs() ([]string, error) {
|
func buildQsIPCBaseArgs() ([]string, error) {
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
switch pid, ok := getSessionDMSPID(); {
|
switch pid, ok := shellApp.SessionPID(); {
|
||||||
case ok:
|
case ok:
|
||||||
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
default:
|
default:
|
||||||
if err := findConfig(nil, nil); err != nil {
|
if err := shellApp.ResolveConfig(nil, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if qsHasAnyDisplay() {
|
if qsHasAnyDisplay() {
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
cmdArgs = append(cmdArgs, "-p", shellApp.ConfigPath())
|
||||||
}
|
}
|
||||||
return cmdArgs, nil
|
return cmdArgs, nil
|
||||||
}
|
}
|
||||||
@@ -691,101 +129,6 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFirstDMSPID() (int, bool) {
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if proc.Signal(syscall.Signal(0)) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func sessionParentPID(display string) (int, bool) {
|
|
||||||
if display == "" {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := getRuntimeDir()
|
|
||||||
entries, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, entry := range entries {
|
|
||||||
name := entry.Name()
|
|
||||||
if !strings.HasPrefix(name, "danklinux-") || !strings.HasSuffix(name, ".session") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(dir, name))
|
|
||||||
if err != nil || strings.TrimSpace(string(data)) != display {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
parentStr := strings.TrimSuffix(strings.TrimPrefix(name, "danklinux-"), ".session")
|
|
||||||
parentPID, err := strconv.Atoi(parentStr)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return parentPID, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSessionDMSPID() (int, bool) {
|
|
||||||
parentPID, ok := sessionParentPID(os.Getenv("WAYLAND_DISPLAY"))
|
|
||||||
if !ok {
|
|
||||||
return getFirstDMSPID()
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", parentPID)))
|
|
||||||
if err != nil {
|
|
||||||
return getFirstDMSPID()
|
|
||||||
}
|
|
||||||
|
|
||||||
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
|
||||||
if err != nil {
|
|
||||||
return getFirstDMSPID()
|
|
||||||
}
|
|
||||||
|
|
||||||
proc, err := os.FindProcess(pid)
|
|
||||||
if err != nil || proc.Signal(syscall.Signal(0)) != nil {
|
|
||||||
return getFirstDMSPID()
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -795,6 +138,9 @@ func runShellIPCCommand(args []string) {
|
|||||||
if args[0] != "call" {
|
if args[0] != "call" {
|
||||||
args = append([]string{"call"}, args...)
|
args = append([]string{"call"}, args...)
|
||||||
}
|
}
|
||||||
|
if tryDirectIPCCall(args) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
baseArgs, err := buildQsIPCBaseArgs()
|
baseArgs, err := buildQsIPCBaseArgs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -811,6 +157,27 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tryDirectIPCCall invokes the shell over its IPC socket without spawning qs.
|
||||||
|
// Any failure falls back to the qs child process path, which owns error output.
|
||||||
|
func tryDirectIPCCall(args []string) bool {
|
||||||
|
if len(args) < 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pid, ok := shellApp.SessionPID()
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
result, isVoid, err := qsipc.Call(qsipc.SocketPathForPID(pid), args[1], args[2], args[3:])
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("Direct Quickshell IPC failed, falling back to qs: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !isVoid {
|
||||||
|
fmt.Println(result)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func printIPCHelp() {
|
func printIPCHelp() {
|
||||||
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
|
fmt.Println("Usage: dms ipc call <target> <function> [args...]")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -919,8 +286,8 @@ func ensureFontCache() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fontInCache(target, cache string) bool {
|
func fontInCache(target, cache string) bool {
|
||||||
for _, line := range strings.Split(cache, "\n") {
|
for line := range strings.SplitSeq(cache, "\n") {
|
||||||
for _, fam := range strings.Split(strings.TrimSpace(line), ",") {
|
for fam := range strings.SplitSeq(strings.TrimSpace(line), ",") {
|
||||||
if strings.TrimSpace(fam) == target {
|
if strings.TrimSpace(fam) == target {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -938,36 +305,12 @@ func rebuildFontCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type stderrTracker struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
buf strings.Builder
|
|
||||||
parent io.Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stderrTracker) Write(p []byte) (n int, err error) {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
if s.buf.Len() < 8192 {
|
|
||||||
s.buf.Write(p)
|
|
||||||
}
|
|
||||||
if s.parent != nil {
|
|
||||||
return s.parent.Write(p)
|
|
||||||
}
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stderrTracker) String() string {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
|
// logStartupFailure logs diagnostic advice if qs crashes within 5s of launch.
|
||||||
func logStartupFailure(startTime time.Time, exitCode int, tracker *stderrTracker) {
|
func logStartupFailure(exitCode int, uptime time.Duration, stderrTail string) {
|
||||||
if time.Since(startTime) >= 5*time.Second || exitCode == 0 || exitCode > 128 {
|
if uptime >= 5*time.Second || exitCode == 0 || exitCode > 128 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if containsFontCrashSignature(tracker.String()) {
|
if containsFontCrashSignature(stderrTail) {
|
||||||
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
|
log.Errorf("DMS startup failed due to a potential font/rendering crash. Try running 'fc-cache -fv' and restarting DMS.")
|
||||||
} else {
|
} else {
|
||||||
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
|
log.Errorf("DMS startup failed (exit code %d). Run 'dms doctor' for more diagnostics.", exitCode)
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/gpu"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/shellembed"
|
||||||
|
"github.com/AvengeMedia/dankgo/shellapp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var shellApp = shellapp.New(shellapp.Config{
|
||||||
|
ID: "danklinux",
|
||||||
|
EnvPrefix: "DMS",
|
||||||
|
QSAppID: "com.danklinux.dms",
|
||||||
|
Version: Version,
|
||||||
|
Embedded: embeddedShell{},
|
||||||
|
Boot: bootBackend,
|
||||||
|
PreLaunch: preLaunch,
|
||||||
|
ExtraEnv: dmsExtraEnv,
|
||||||
|
OnUIExit: logStartupFailure,
|
||||||
|
SessionRestartExitCode: dmsSessionRestartExitCode,
|
||||||
|
TryManagedRestart: trySystemdRestart,
|
||||||
|
})
|
||||||
|
|
||||||
|
type embeddedShell struct{}
|
||||||
|
|
||||||
|
func (embeddedShell) Available() bool { return shellembed.Available() }
|
||||||
|
|
||||||
|
func (embeddedShell) Extract(baseDir string) (string, error) { return shellembed.Extract(baseDir) }
|
||||||
|
|
||||||
|
func (embeddedShell) Prune(baseDir, keep string) { shellembed.Prune(baseDir, keep) }
|
||||||
|
|
||||||
|
type dmsBackend struct {
|
||||||
|
srv *server.Server
|
||||||
|
done chan error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *dmsBackend) SocketPath() string { return b.srv.SocketPath() }
|
||||||
|
|
||||||
|
func (b *dmsBackend) Close() { b.srv.Close() }
|
||||||
|
|
||||||
|
func (b *dmsBackend) Done() <-chan error { return b.done }
|
||||||
|
|
||||||
|
func bootBackend(ctx context.Context) (shellapp.Backend, error) {
|
||||||
|
config.CleanupStrayHyprlandConfFile(log.Infof)
|
||||||
|
server.CLIVersion = Version
|
||||||
|
|
||||||
|
srv := server.New()
|
||||||
|
if err := srv.Listen(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
backend := &dmsBackend{srv: srv, done: make(chan error, 1)}
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
backend.done <- fmt.Errorf("server panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
backend.done <- srv.Serve(false)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return backend, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func preLaunch() {
|
||||||
|
go printASCII()
|
||||||
|
ensureFontCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
func dmsExtraEnv(string) []string {
|
||||||
|
var env []string
|
||||||
|
if selfPath, err := os.Executable(); err == nil {
|
||||||
|
env = append(env, "DMS_EXECUTABLE="+selfPath)
|
||||||
|
}
|
||||||
|
if os.Getenv("QSG_USE_SIMPLE_ANIMATION_DRIVER") == "" {
|
||||||
|
env = append(env, "QSG_USE_SIMPLE_ANIMATION_DRIVER=1")
|
||||||
|
}
|
||||||
|
if _, set := os.LookupEnv("MALLOC_CONF"); !set {
|
||||||
|
env = append(env, "MALLOC_CONF=thp:never,narenas:4,dirty_decay_ms:3000")
|
||||||
|
}
|
||||||
|
env = append(env, gpu.EGLVendorEnv()...)
|
||||||
|
return env
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dmsSystemdUnit = "dms.service"
|
||||||
|
const dmsSessionRestartExitCode = 75 // EX_TEMPFAIL
|
||||||
|
|
||||||
|
var runSystemctl = func(args ...string) error {
|
||||||
|
output, err := exec.Command("systemctl", args...).CombinedOutput()
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if detail := strings.TrimSpace(string(output)); detail != "" {
|
||||||
|
return fmt.Errorf("%s: %w", detail, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func trySystemdRestart() (bool, error) {
|
||||||
|
if err := runSystemctl("--user", "is-active", "--quiet", dmsSystemdUnit); err != nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runSystemctl("--user", "restart", dmsSystemdUnit); err != nil {
|
||||||
|
return true, fmt.Errorf("restarting %s: %w", dmsSystemdUnit, err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
+6
-6
@@ -15,12 +15,12 @@ func getThemedASCII() string {
|
|||||||
theme := tui.TerminalTheme()
|
theme := tui.TerminalTheme()
|
||||||
|
|
||||||
logo := `
|
logo := `
|
||||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
██████╗ ███╗ ███╗███████╗
|
||||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
██╔══██╗████╗ ████║██╔════╝
|
||||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
██║ ██║██╔████╔██║███████╗
|
||||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
██║ ██║██║╚██╔╝██║╚════██║
|
||||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
██████╔╝██║ ╚═╝ ██║███████║
|
||||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
╚═════╝ ╚═╝ ╚═╝╚══════╝`
|
||||||
|
|
||||||
style := lipgloss.NewStyle().
|
style := lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color(theme.Primary)).
|
Foreground(lipgloss.Color(theme.Primary)).
|
||||||
|
|||||||
+9
-61
@@ -1,12 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"slices"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func disableMemProfilingUnlessRequested() {
|
||||||
|
if os.Getenv("DMS_PPROF") != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
runtime.MemProfileRate = 0
|
||||||
|
}
|
||||||
|
|
||||||
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||||
// safe to run as root (e.g. shell completion, help).
|
// safe to run as root (e.g. shell completion, help).
|
||||||
func isReadOnlyCommand(args []string) bool {
|
func isReadOnlyCommand(args []string) bool {
|
||||||
@@ -28,62 +35,3 @@ func isArchPackageInstalled(packageName string) bool {
|
|||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type systemdServiceState struct {
|
|
||||||
Name string
|
|
||||||
EnabledState string
|
|
||||||
NeedsDisable bool
|
|
||||||
Exists bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkSystemdServiceEnabled returns (state, should_disable, error) for a systemd service
|
|
||||||
func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
|
|
||||||
cmd := exec.Command("systemctl", "is-enabled", serviceName)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
|
|
||||||
stateStr := strings.TrimSpace(string(output))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
|
||||||
isKnownState := slices.Contains(knownStates, stateStr)
|
|
||||||
|
|
||||||
if !isKnownState {
|
|
||||||
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldDisable := false
|
|
||||||
switch stateStr {
|
|
||||||
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
|
||||||
shouldDisable = true
|
|
||||||
case "disabled", "masked", "masked-runtime", "not-found":
|
|
||||||
shouldDisable = false
|
|
||||||
default:
|
|
||||||
shouldDisable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return stateStr, shouldDisable, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSystemdServiceState(serviceName string) (*systemdServiceState, error) {
|
|
||||||
state := &systemdServiceState{
|
|
||||||
Name: serviceName,
|
|
||||||
Exists: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
enabledState, needsDisable, err := checkSystemdServiceEnabled(serviceName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check enabled state: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
state.EnabledState = enabledState
|
|
||||||
state.NeedsDisable = needsDisable
|
|
||||||
|
|
||||||
if enabledState == "not-found" {
|
|
||||||
state.Exists = false
|
|
||||||
return state, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
state.Exists = true
|
|
||||||
return state, nil
|
|
||||||
}
|
|
||||||
|
|||||||
+45
-36
@@ -1,29 +1,31 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287
|
||||||
|
github.com/Nadim147c/material/v3 v3.1.1
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1
|
github.com/alecthomas/chroma/v2 v2.27.0
|
||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v1.0.0
|
github.com/dlclark/regexp2/v2 v2.5.1
|
||||||
github.com/fsnotify/fsnotify v1.10.1
|
github.com/fsnotify/fsnotify v1.10.1
|
||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
|
github.com/holoplot/go-evdev v0.0.0-20260504100651-66d1748fe847
|
||||||
|
github.com/klauspost/compress v1.19.2
|
||||||
github.com/pilebones/go-udev v0.9.1
|
github.com/pilebones/go-udev v0.9.1
|
||||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.12.1
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||||
github.com/yuin/goldmark v1.8.2
|
github.com/yuin/goldmark v1.8.4
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
go.etcd.io/bbolt v1.5.0
|
||||||
go.etcd.io/bbolt v1.4.3
|
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745
|
||||||
golang.org/x/image v0.39.0
|
golang.org/x/image v0.44.0
|
||||||
tailscale.com v1.96.5
|
tailscale.com v1.102.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -31,70 +33,77 @@ require (
|
|||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
github.com/ProtonMail/go-crypto v1.4.1 // indirect
|
||||||
github.com/akutz/memconn v0.1.0 // indirect
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
|
github.com/charmbracelet/log v1.0.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.3 // indirect
|
github.com/cloudflare/circl v1.6.4 // indirect
|
||||||
github.com/coder/websocket v1.8.14 // indirect
|
github.com/coder/websocket v1.8.15 // indirect
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/danielgtaylor/huma/v2 v2.39.1 // indirect
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d // indirect
|
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
|
||||||
github.com/dlclark/regexp2 v1.12.0 // indirect
|
github.com/ebitengine/purego v0.10.2 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fogleman/gg v1.3.0 // indirect
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
github.com/go-git/gcfg/v2 v2.0.2 // indirect
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 // indirect
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/google/go-cmp v0.7.0 // indirect
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
github.com/kevinburke/ssh_config v1.6.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
|
||||||
github.com/mdlayher/netlink v1.11.1 // indirect
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 // indirect
|
||||||
github.com/mdlayher/socket v0.6.0 // indirect
|
github.com/mdlayher/netlink v1.11.2 // indirect
|
||||||
|
github.com/mdlayher/socket v0.6.1 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
github.com/pjbgf/sha1cd v0.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.7 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.12.0 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/crypto v0.50.0 // indirect
|
golang.org/x/crypto v0.54.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa // indirect
|
||||||
golang.org/x/net v0.53.0 // indirect
|
golang.org/x/net v0.57.0 // indirect
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
golang.org/x/sync v0.22.0 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea
|
||||||
github.com/atotto/clipboard v0.1.4 // indirect
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/x/ansi v0.11.8 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0
|
github.com/lucasb-eyer/go-colorful v1.4.1
|
||||||
github.com/mattn/go-isatty v0.0.22
|
github.com/mattn/go-isatty v0.0.24
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
github.com/mattn/go-runewidth v0.0.28 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/afero v1.15.0
|
github.com/spf13/afero v1.15.0
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.43.0
|
golang.org/x/sys v0.47.0
|
||||||
golang.org/x/text v0.36.0
|
golang.org/x/text v0.41.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+100
-79
@@ -1,7 +1,13 @@
|
|||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea h1:A2IuXIzhTQwvBZiS8pxU5RmWSszMd16Oh11gUlJ9OeU=
|
||||||
|
github.com/AvengeMedia/dankgo v1.6.1-0.20260908011626-07e1ef7caaea/go.mod h1:w1W/vDrWJCLdjVI4E/mURiKUQLMCmiGiPLxZEeP5kp8=
|
||||||
|
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287 h1:wzQqP16zxfUcfFltJelKg6pTdRyiLY2l2NJLT0Par+Q=
|
||||||
|
github.com/AvengeMedia/dgop v1.6.1-0.20260916132520-8a12dbc6e287/go.mod h1:lwzO5Y4BO5z0I7cLMp5A7sw/THliwGz1TNv6xoUehY4=
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
|
github.com/Nadim147c/material/v3 v3.1.1 h1:3ZuTPWBJWUpzzWW1rtqe28RMZXhA4f0/mg/Jk3JG94Y=
|
||||||
|
github.com/Nadim147c/material/v3 v3.1.1/go.mod h1:ThN+dRdC+CDN2qXSGMn32KqtcXwYtb/trDM82cdZI6g=
|
||||||
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
|
||||||
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||||
@@ -10,10 +16,8 @@ github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
|||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1 h1:m5ffpfZbIb++k8AqFEKy9uVgY12xIQtBsQlc6DfZJQM=
|
github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8=
|
||||||
github.com/alecthomas/chroma/v2 v2.24.1/go.mod h1:l+ohZ9xRXIbGe7cIW+YZgOGbvuVLjMps/FYN/CwuabI=
|
|
||||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
@@ -30,14 +34,12 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
github.com/charmbracelet/log v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
|
||||||
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7 h1:kzv1kJvjg2S3r9KHo8hDdHFQLEqn4RBCb39dAYC84jI=
|
github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ=
|
||||||
github.com/charmbracelet/x/ansi v0.11.7/go.mod h1:9qGpnAVYz+8ACONkZBUWPtL7lulP9No6p1epAihUZwQ=
|
github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
@@ -48,25 +50,23 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE
|
|||||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
github.com/cloudflare/circl v1.6.4 h1:pOXuDTCEYyzydgUpQ0CQz3LsinKjiSk6nNP5Lt5K64U=
|
||||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
github.com/cloudflare/circl v1.6.4/go.mod h1:YxarevkLlbaHuWsxG6vmYNWBEsSp4pnp7j+4VljMavY=
|
||||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
github.com/creachadair/taskgroup v0.13.2 h1:3KyqakBuFsm3KkXi/9XIb0QcA8tEzLHLgaoidf0MdVc=
|
||||||
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
github.com/creachadair/taskgroup v0.13.2/go.mod h1:i3V1Zx7H8RjwljUEeUWYT30Lmb9poewSb2XI1yTwD0g=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/danielgtaylor/huma/v2 v2.39.1 h1:0kwF4ltQoYZ+IU55VPy+BcGekzgF44R64daTGde1H+g=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/danielgtaylor/huma/v2 v2.39.1/go.mod h1:zcnQ38duIJ3VUHwFaBoZ6x8T+KN/mr33oyqxcj0HTug=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca h1:h1Awca4lQOspNR/2eeo04Ricn5NixDX9mb17WSAgLhQ=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca/go.mod h1:2TGl1jRJrRpbzykmg7asHm3h08TqutUgQqY5v9k/g3c=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d h1:QRKpU+9ZBDs62LyBfwhZkJdB5DJX2Sm3p4kUh7l1aA0=
|
github.com/dlclark/regexp2/v2 v2.5.1 h1:E5Ug7Dh264W1ymdySmiHNcDG7fmsR307APCE5R07a20=
|
||||||
github.com/dblohm7/wingoes v0.0.0-20250822163801-6d8e6105c62d/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
|
github.com/dlclark/regexp2/v2 v2.5.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU=
|
||||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
github.com/ebitengine/purego v0.10.2 h1:W809HbnvzAxgdm+aOvlSekrM16wGCdT/e76+9tS7gzE=
|
||||||
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/ebitengine/purego v0.10.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
|
||||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
||||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
@@ -83,18 +83,23 @@ github.com/gaissmai/bart v0.26.1 h1:+w4rnLGNlA2GDVn382Tfe3jOsK5vOr5n4KmigJ9lbTo=
|
|||||||
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
github.com/gaissmai/bart v0.26.1/go.mod h1:GREWQfTLRWz/c5FTOsIw+KkscuFkIV5t8Rp7Nd1Td5c=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-chi/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY=
|
||||||
|
github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
|
||||||
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266 h1:wH21vHuv323v9x78JNFNJ6P7HEAsdwr9yq2k9/o4zEE=
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1 h1:xVjAR4oUvrKy7/Xuw/lLlV3gkxR3KO2H8W+MamuVVsQ=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20260504142752-cb8e9d337266/go.mod h1:CdBVp7CXl9l3sOyNEog46cP1Pvx/hjCe9AD0mtaIUYU=
|
github.com/go-git/go-billy/v6 v6.0.0-alpha.1/go.mod h1:eaCUpHbedW7//EwcYmUDfJe2N6sJC9O12AT0OTqJR1E=
|
||||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0 h1:XoTsdvaghuVfIr7HpNTmFDLu2nz3I2iGqyn6Uk6MkJc=
|
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1 h1:gmqi2jvsreu0s8JMLylYDFq4sbjHwwlhktMw0DUg3mA=
|
||||||
github.com/go-git/go-git-fixtures/v6 v6.0.0-20260405195209-b16dd39735e0/go.mod h1:1Lr7/vYEYyl6Ir9Ku0tKrCIRreM5zovv0Jdx2MPSM4s=
|
github.com/go-git/go-git-fixtures/v6 v6.0.0-alpha.1/go.mod h1:ECf1MqJlBdYpKggBrOXjo/0EnvRZx6D++I86UYjPgAQ=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2 h1:T3loNtDuAixNzXtlQxZhnYiYpaQ3CA4vn9RssAniEeI=
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4 h1:aDTc2UGanmaE7FkGLSlBEB9nohMnQ+RKXcfq/D+esDQ=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-alpha.2/go.mod h1:oCD3i19CTz7gBpeb11ZZqL91WzqbMq9avn5KpUYy/Ak=
|
github.com/go-git/go-git/v6 v6.0.0-alpha.4/go.mod h1:4ODa/G7hPWrh4Y+7lmt59Ij3zW38IEfvRoAZxLYYBhc=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4 h1:2WmHkJINIjgXXYDGik8d3oJvFA3DAwPy00csDJ3vo+o=
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
|
||||||
github.com/go-json-experiment/json v0.0.0-20260430182902-b6187a392ed4/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
|
||||||
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
|
||||||
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
@@ -105,6 +110,8 @@ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUv
|
|||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
@@ -117,8 +124,10 @@ github.com/jsimonetti/rtnetlink v1.4.2 h1:Df9w9TZ3npHTyDn0Ev9e1uzmN2odmXd0QX+J5G
|
|||||||
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
github.com/jsimonetti/rtnetlink v1.4.2/go.mod h1:92s6LJdE+1iOrw+F2/RO7LYI2Qd8pPpFNNUYW06gcoM=
|
||||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
github.com/klauspost/compress v1.19.2 h1:hMRETovs/pu/dVWN7zIT1PGG8t509MwT6bO7XSi26R8=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/compress v1.19.2/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
@@ -126,18 +135,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss=
|
||||||
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5 h1:eveIIGn4BGM3qknO74omf6HYr30/exH+eVUTuAgwjZ0=
|
||||||
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
github.com/lufia/plan9stats v0.0.0-20260802145828-341c2f0c90b5/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||||
|
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
github.com/mattn/go-runewidth v0.0.28 h1:rPyg2ybwEKPebvpzVWe1gKBkH8EQFkxO4Y0hjBeLaBU=
|
||||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.28/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
|
||||||
github.com/mdlayher/netlink v1.11.1 h1:T136gDS6Gkt+hLncaBwKdW5GpEC8Z0ykqimOebVoal0=
|
github.com/mdlayher/netlink v1.11.2 h1:HKh2jqe+omdSWcQ88nrT7INE61B0NXfiSPFdgL4YbNI=
|
||||||
github.com/mdlayher/netlink v1.11.1/go.mod h1:ao4LjamyK4Uq9L8+fQzqFYpAncbeCdwbvd9Edv/pYnc=
|
github.com/mdlayher/netlink v1.11.2/go.mod h1:uT2Yc/QLaZubzDpZIBi9d4GoeLwtp3x1AMeqSRrK2sA=
|
||||||
github.com/mdlayher/socket v0.6.0 h1:ScZPaAGyO1icQnbFrhPM8mnXyMu9qukC1K4ZoM2IQKU=
|
github.com/mdlayher/socket v0.6.1 h1:M7uj2NtuujUY4mYr1C57NmfNiRHbkKpnBxO856lsc3A=
|
||||||
github.com/mdlayher/socket v0.6.0/go.mod h1:q7vozUAnxSqnjHc12Fik5yUKIzfZ8ITCfMkhOtE9z18=
|
github.com/mdlayher/socket v0.6.1/go.mod h1:+/SGtqc9V+5dAuRgQsU0fGBI+oRDiW7O2Obx10OIWfg=
|
||||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
@@ -153,8 +164,8 @@ github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H
|
|||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6 h1:jL3a8soXdzuTCcRnKhOmtcsVOObdDTFf4O2B403HPRU=
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/power-devops/perfstat v0.0.0-20260805114148-88456608a4f6/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
@@ -164,6 +175,8 @@ github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4Xl
|
|||||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||||
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.7 h1:IXzpHz/dkMRYAhKkOXr1HB6SuzWU3eoyyeWe7g3bNZc=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.7/go.mod h1:5O9FjBiXoTDFatIWjZZosqj4pV0DRtLx598xGbBehzM=
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
@@ -175,54 +188,63 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
|||||||
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da h1:jVRUZPRs9sqyKlYHHzHjAqKN+6e/Vog6NpHYeNPJqOw=
|
github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0 h1:CnIEL2n7Xql6Ux1k+Vu5S5ubDHCT/kxFgkKCY8FjefU=
|
||||||
github.com/tailscale/wireguard-go v0.0.0-20250716170648-1d0488a3d7da/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
|
github.com/tailscale/wireguard-go v0.0.0-20260715223240-2e01ba5b00f0/go.mod h1:6SerzcvHWQchKO2BfNdmquA77CHSECZuFl+D9fp4RnI=
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0 h1:7H0uAN+7RkwWRaxhYXDLqa5V3LPrJeV8wmD9dRUgPQU=
|
||||||
|
github.com/tklauser/go-sysconf v0.4.0/go.mod h1:8mTNWyog7H+MpKijp4VmKJAd2bbYQ2zuUwkYRbUArPI=
|
||||||
|
github.com/tklauser/numcpus v0.12.0 h1:NR85qdvHA9pFse3x3weVZ0r0ST8R6l5RHbZrlRaqob4=
|
||||||
|
github.com/tklauser/numcpus v0.12.0/go.mod h1:ABHeXzJnr/qqwguhClkZKT1/8VABcYrsyUiUGobwWJg=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v1.0.0 h1:2ZpYzqWzyyytjk3TP6aJVDhkMAkc99/1xKQdA3TDTBY=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v1.0.0/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||||
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||||
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
||||||
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA=
|
||||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
go.etcd.io/bbolt v1.5.0 h1:S7GAl7Fxv12yohbwFfIbQCGDWbQbtDGPET4P/bD4lxU=
|
||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.5.0/go.mod h1:mkltfYE5aUHQxUct9N9V+Kp7aSjFqjgrhcXIS70Lrdk=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa h1:QSyA8ishJCyT21kER9KwNt0b7BM3iRK4x9QXhjN5Fdk=
|
||||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
golang.org/x/exp v0.0.0-20260824195058-e88cd73687aa/go.mod h1:zeBbvyFKDaLwa7CH/zI8KXt7gTl14SF7sO08Pl5jBCM=
|
||||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I=
|
||||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY=
|
||||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
golang.org/x/mod v0.39.0 h1:UF5zwQdCRRUpHfyPwr7d4UrGiVeldIsogtzWVnczL74=
|
||||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
golang.org/x/mod v0.39.0/go.mod h1:bvIbwjQ0HUFFf5AKukeeYQG4ZBUG9yxQbR9aEweIwYY=
|
||||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||||
|
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||||
|
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
|
||||||
|
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||||
@@ -232,8 +254,7 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
tailscale.com v1.96.5 h1:gNkfA/KSZAl6jCH9cj8urq00HRWItDDTtGsyATI89jA=
|
tailscale.com v1.102.0 h1:yAYrykMZWlgp7Qq2ECDrDnxeySWBhNXNMkXdDmVMyrE=
|
||||||
tailscale.com v1.96.5/go.mod h1:/3lnZBYb2UEwnN0MNu2SDXUtT06AGd5k0s+OWx3WmcY=
|
tailscale.com v1.102.0/go.mod h1:NdndXfkSklVPbRUZMAzzRuDNaR5B+YWGbAyE1HpSQZg=
|
||||||
|
|||||||
+2
-7
@@ -47,6 +47,7 @@ printf "%bInstalling Dankinstall %s for %s...%b\n" "$GREEN" "$LATEST_VERSION" "$
|
|||||||
|
|
||||||
# Download and install
|
# Download and install
|
||||||
TEMP_DIR=$(mktemp -d)
|
TEMP_DIR=$(mktemp -d)
|
||||||
|
trap 'cd / && rm -rf "$TEMP_DIR"' EXIT
|
||||||
cd "$TEMP_DIR" || exit 1
|
cd "$TEMP_DIR" || exit 1
|
||||||
|
|
||||||
# Download the gzipped binary and its checksum
|
# Download the gzipped binary and its checksum
|
||||||
@@ -67,8 +68,6 @@ if [ "$EXPECTED_CHECKSUM" != "$ACTUAL_CHECKSUM" ]; then
|
|||||||
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
printf "Expected: %s\n" "$EXPECTED_CHECKSUM"
|
||||||
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
printf "Got: %s\n" "$ACTUAL_CHECKSUM"
|
||||||
printf "The downloaded file may be corrupted or tampered with\n"
|
printf "The downloaded file may be corrupted or tampered with\n"
|
||||||
cd - >/dev/null
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -79,8 +78,4 @@ chmod +x installer
|
|||||||
|
|
||||||
# Execute the installer
|
# Execute the installer
|
||||||
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
||||||
./installer
|
./installer "$@"
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
cd - >/dev/null
|
|
||||||
rm -rf "$TEMP_DIR"
|
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const dirName = "DankMaterialShell"
|
||||||
|
|
||||||
|
func ConfigDir() (string, error) {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(configDir, dirName), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultArchiveName() string {
|
||||||
|
return fmt.Sprintf("dms-backup-%s.tar.gz", time.Now().Format("20060102-150405"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Create(outputPath string) error {
|
||||||
|
srcDir, err := ConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(srcDir); err != nil {
|
||||||
|
return fmt.Errorf("no DMS configuration found at %s: %w", srcDir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
gz := gzip.NewWriter(out)
|
||||||
|
defer gz.Close()
|
||||||
|
tw := tar.NewWriter(gz)
|
||||||
|
defer tw.Close()
|
||||||
|
|
||||||
|
return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rel, err := filepath.Rel(srcDir, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if rel == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var link string
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
if link, err = os.Readlink(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header, err := tar.FileInfoHeader(info, link)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
header.Name = filepath.ToSlash(filepath.Join(dirName, rel))
|
||||||
|
|
||||||
|
if err := tw.WriteHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = io.Copy(tw, f)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func Restore(archivePath string) (string, error) {
|
||||||
|
dstDir, err := ConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateArchive(archivePath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
previous := ""
|
||||||
|
if _, err := os.Stat(dstDir); err == nil {
|
||||||
|
previous = dstDir + ".pre-restore-" + time.Now().Format("20060102-150405")
|
||||||
|
if err := os.Rename(dstDir, previous); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to move existing configuration aside: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := extract(archivePath, filepath.Dir(dstDir)); err != nil {
|
||||||
|
if previous != "" {
|
||||||
|
os.RemoveAll(dstDir)
|
||||||
|
os.Rename(previous, dstDir)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return previous, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateArchive(archivePath string) error {
|
||||||
|
hasSettings := false
|
||||||
|
err := walkArchive(archivePath, func(header *tar.Header, _ *tar.Reader) error {
|
||||||
|
name := filepath.ToSlash(filepath.Clean(header.Name))
|
||||||
|
if strings.HasPrefix(name, "..") || filepath.IsAbs(header.Name) {
|
||||||
|
return fmt.Errorf("unsafe path in archive: %s", header.Name)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(name, dirName+"/") && name != dirName {
|
||||||
|
return fmt.Errorf("not a DMS backup: unexpected entry %s", header.Name)
|
||||||
|
}
|
||||||
|
if name == dirName+"/settings.json" {
|
||||||
|
hasSettings = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !hasSettings {
|
||||||
|
return fmt.Errorf("not a DMS backup: settings.json missing from archive")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extract(archivePath, destParent string) error {
|
||||||
|
return walkArchive(archivePath, func(header *tar.Header, tr *tar.Reader) error {
|
||||||
|
target := filepath.Join(destParent, filepath.Clean(header.Name))
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
return os.MkdirAll(target, os.FileMode(header.Mode))
|
||||||
|
case tar.TypeSymlink:
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Symlink(header.Linkname, target)
|
||||||
|
case tar.TypeReg:
|
||||||
|
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.FileMode(header.Mode))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
_, err = io.Copy(f, tr)
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func walkArchive(archivePath string, visit func(*tar.Header, *tar.Reader) error) error {
|
||||||
|
f, err := os.Open(archivePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
gz, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("not a gzip archive: %w", err)
|
||||||
|
}
|
||||||
|
defer gz.Close()
|
||||||
|
|
||||||
|
tr := tar.NewReader(gz)
|
||||||
|
for {
|
||||||
|
header, err := tr.Next()
|
||||||
|
if err == io.EOF {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := visit(header, tr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ package blur
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
client "github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/dankgo/wayland/ext_data_control"
|
||||||
|
"github.com/AvengeMedia/dankgo/wlclipboard"
|
||||||
)
|
)
|
||||||
|
|
||||||
const envServe = "_DMS_CLIPBOARD_SERVE"
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
@@ -16,37 +17,6 @@ const envMime = "_DMS_CLIPBOARD_MIME"
|
|||||||
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
type Offer struct {
|
|
||||||
MimeType string
|
|
||||||
Data []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
// textMimeAliases are offered alongside plain-text content so legacy X11
|
|
||||||
// clients bridged through XWayland find a target they can convert.
|
|
||||||
var textMimeAliases = []string{
|
|
||||||
"text/plain",
|
|
||||||
"text/plain;charset=utf-8",
|
|
||||||
"UTF8_STRING",
|
|
||||||
"STRING",
|
|
||||||
"TEXT",
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExpandOffers turns raw clipboard data into the full offer list to serve,
|
|
||||||
// adding the standard alias set for text content.
|
|
||||||
func ExpandOffers(data []byte, mimeType string) []Offer {
|
|
||||||
offers := []Offer{{MimeType: mimeType, Data: data}}
|
|
||||||
if mimeType != "text/plain" && mimeType != "text/plain;charset=utf-8" {
|
|
||||||
return offers
|
|
||||||
}
|
|
||||||
for _, alias := range textMimeAliases {
|
|
||||||
if alias == mimeType {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
offers = append(offers, Offer{MimeType: alias, Data: data})
|
|
||||||
}
|
|
||||||
return offers
|
|
||||||
}
|
|
||||||
|
|
||||||
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
// child. Reads source data into memory, deletes any cache file, then serves.
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
func MaybeServeAndExit() {
|
func MaybeServeAndExit() {
|
||||||
@@ -74,7 +44,7 @@ func MaybeServeAndExit() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := serveOffers(ExpandOffers(data, mimeType), pasteOnce); err != nil {
|
if err := serveOffers(wlclipboard.ExpandOffers(data, mimeType), pasteOnce); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -91,7 +61,7 @@ func CopyText(text string) error {
|
|||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return serveOffers(ExpandOffers(data, mimeType), pasteOnce)
|
return serveOffers(wlclipboard.ExpandOffers(data, mimeType), pasteOnce)
|
||||||
}
|
}
|
||||||
return copyForkCached(data, mimeType, pasteOnce)
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
@@ -104,10 +74,10 @@ func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read source: %w", err)
|
return fmt.Errorf("read source: %w", err)
|
||||||
}
|
}
|
||||||
return serveOffers(ExpandOffers(buf, mimeType), pasteOnce)
|
return serveOffers(wlclipboard.ExpandOffers(buf, mimeType), pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
func CopyMulti(offers []wlclipboard.Offer, foreground, pasteOnce bool) error {
|
||||||
if foreground {
|
if foreground {
|
||||||
return serveOffers(offers, pasteOnce)
|
return serveOffers(offers, pasteOnce)
|
||||||
}
|
}
|
||||||
@@ -206,7 +176,7 @@ func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
func copyMultiFork(offers []wlclipboard.Offer, pasteOnce bool) error {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
args = append(args, "--paste-once")
|
args = append(args, "--paste-once")
|
||||||
@@ -268,7 +238,7 @@ func createClipboardCacheFile() (*os.File, error) {
|
|||||||
|
|
||||||
// serveOffers owns the Wayland selection until cancelled (or first paste when
|
// serveOffers owns the Wayland selection until cancelled (or first paste when
|
||||||
// pasteOnce is set), answering every offered mime type with its data.
|
// pasteOnce is set), answering every offered mime type with its data.
|
||||||
func serveOffers(offers []Offer, pasteOnce bool) error {
|
func serveOffers(offers []wlclipboard.Offer, pasteOnce bool) error {
|
||||||
if len(offers) == 0 {
|
if len(offers) == 0 {
|
||||||
return fmt.Errorf("no offers to serve")
|
return fmt.Errorf("no offers to serve")
|
||||||
}
|
}
|
||||||
@@ -348,111 +318,6 @@ func serveOffers(offers []Offer, pasteOnce bool) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Paste() ([]byte, string, error) {
|
|
||||||
s, err := connectSession()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
dataControlMgr, err := s.requireDataControl()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(s.seat)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
|
||||||
|
|
||||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerMimeTypes[e.Id] = nil
|
|
||||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
|
||||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
var selectionOffer *ext_data_control.ExtDataControlOfferV1
|
|
||||||
gotSelection := false
|
|
||||||
|
|
||||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
|
||||||
selectionOffer = e.Id
|
|
||||||
gotSelection = true
|
|
||||||
})
|
|
||||||
|
|
||||||
s.display.Roundtrip()
|
|
||||||
s.display.Roundtrip()
|
|
||||||
|
|
||||||
if !gotSelection || selectionOffer == nil {
|
|
||||||
return nil, "", fmt.Errorf("no clipboard data")
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedMime := selectPreferredMimeType(offerMimeTypes[selectionOffer])
|
|
||||||
if selectedMime == "" {
|
|
||||||
return nil, "", fmt.Errorf("no supported mime type")
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("create pipe: %w", err)
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
if err := selectionOffer.Receive(selectedMime, int(w.Fd())); err != nil {
|
|
||||||
w.Close()
|
|
||||||
return nil, "", fmt.Errorf("receive: %w", err)
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
s.display.Roundtrip()
|
|
||||||
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", fmt.Errorf("read: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, selectedMime, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func PasteText() (string, error) {
|
|
||||||
data, _, err := Paste()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func selectPreferredMimeType(mimes []string) string {
|
|
||||||
preferred := []string{
|
|
||||||
"text/plain;charset=utf-8",
|
|
||||||
"text/plain",
|
|
||||||
"UTF8_STRING",
|
|
||||||
"STRING",
|
|
||||||
"TEXT",
|
|
||||||
"image/png",
|
|
||||||
"image/jpeg",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, pref := range preferred {
|
|
||||||
for _, mime := range mimes {
|
|
||||||
if mime == pref {
|
|
||||||
return mime
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(mimes) > 0 {
|
|
||||||
return mimes[0]
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,35 +2,26 @@ package clipboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||||
|
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
xkbKeymapFormatV1 = 1
|
|
||||||
|
|
||||||
keyStateReleased = 0
|
keyStateReleased = 0
|
||||||
keyStatePressed = 1
|
keyStatePressed = 1
|
||||||
|
|
||||||
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
|
// xkb real modifier bit positions are fixed: Shift=0, Lock=1, Control=2
|
||||||
shiftModMask = 1 << 0
|
shiftModMask = 1 << 0
|
||||||
ctrlModMask = 1 << 2
|
ctrlModMask = 1 << 2
|
||||||
|
|
||||||
// evdev fallbacks for a standard pc105 map
|
|
||||||
fallbackCtrlKey = 29 // KEY_LEFTCTRL
|
|
||||||
fallbackShiftKey = 42 // KEY_LEFTSHIFT
|
|
||||||
fallbackVKey = 47 // KEY_V
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
|
// SendPasteKeystroke emulates a paste shortcut via zwp_virtual_keyboard_v1
|
||||||
// using the seat's own keymap, so keycodes stay valid for XWayland clients
|
// using the seat's own keymap, so keycodes stay valid for XWayland clients
|
||||||
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
|
// (a synthetic wtype-style keymap breaks X11 apps like Steam). withShift
|
||||||
// selects ctrl+shift+v for terminal targets.
|
// selects ctrl+shift+v for terminal targets.
|
||||||
func SendPasteKeystroke(withShift bool) error {
|
func SendPasteKeystroke(withShift bool) (err error) {
|
||||||
s, err := connectSession()
|
s, err := connectSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -50,25 +41,25 @@ func SendPasteKeystroke(withShift bool) error {
|
|||||||
}
|
}
|
||||||
defer keyboard.Release()
|
defer keyboard.Release()
|
||||||
|
|
||||||
var keymap *wlclient.KeyboardKeymapEvent
|
var seatKeymap *wlclient.KeyboardKeymapEvent
|
||||||
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||||
if keymap == nil {
|
if seatKeymap == nil {
|
||||||
keymap = &e
|
seatKeymap = &e
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
s.display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
if keymap == nil || keymap.Format != xkbKeymapFormatV1 {
|
if seatKeymap == nil || seatKeymap.Format != keymap.FormatXkbV1 {
|
||||||
return fmt.Errorf("no xkb keymap from seat")
|
return fmt.Errorf("no xkb keymap from seat")
|
||||||
}
|
}
|
||||||
defer unix.Close(keymap.Fd)
|
defer unix.Close(seatKeymap.Fd)
|
||||||
|
|
||||||
keymapText, err := readKeymap(keymap.Fd, keymap.Size)
|
keymapText, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("read keymap: %w", err)
|
return fmt.Errorf("read keymap: %w", err)
|
||||||
}
|
}
|
||||||
keys := resolveKeycodes(keymapText)
|
keys := keymap.Parse(keymapText)
|
||||||
|
|
||||||
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
|
vk, err := s.virtualKeyboardMgr.CreateVirtualKeyboard(s.seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -76,131 +67,53 @@ func SendPasteKeystroke(withShift bool) error {
|
|||||||
}
|
}
|
||||||
defer vk.Destroy()
|
defer vk.Destroy()
|
||||||
|
|
||||||
if err := vk.Keymap(xkbKeymapFormatV1, keymap.Fd, keymap.Size); err != nil {
|
if err := vk.Keymap(keymap.FormatXkbV1, seatKeymap.Fd, seatKeymap.Size); err != nil {
|
||||||
return fmt.Errorf("set keymap: %w", err)
|
return fmt.Errorf("set keymap: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
mods := uint32(ctrlModMask)
|
mods := uint32(ctrlModMask)
|
||||||
held := []uint32{keys.ctrl}
|
held := []uint32{keys.Keycode("Control_L")}
|
||||||
if withShift {
|
if withShift {
|
||||||
mods |= shiftModMask
|
mods |= shiftModMask
|
||||||
held = append(held, keys.shift)
|
held = append(held, keys.Keycode("Shift_L"))
|
||||||
}
|
}
|
||||||
|
|
||||||
t := uint32(0)
|
t := uint32(0)
|
||||||
press := func(key, state uint32) error {
|
var pressed []uint32
|
||||||
|
press := func(key uint32) error {
|
||||||
t++
|
t++
|
||||||
return vk.Key(t, key, state)
|
if err := vk.Key(t, key, keyStatePressed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pressed = append(pressed, key)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// all releases happen here so error paths can't leave keys held or
|
||||||
|
// modifiers latched for the compositor to interpret on destroy
|
||||||
|
defer func() {
|
||||||
|
for i := len(pressed) - 1; i >= 0; i-- {
|
||||||
|
t++
|
||||||
|
if e := vk.Key(t, pressed[i], keyStateReleased); e != nil && err == nil {
|
||||||
|
err = fmt.Errorf("key release: %w", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e := vk.Modifiers(0, 0, 0, 0); e != nil && err == nil {
|
||||||
|
err = fmt.Errorf("clear modifiers: %w", e)
|
||||||
|
}
|
||||||
|
s.display.Roundtrip()
|
||||||
|
}()
|
||||||
|
|
||||||
for _, key := range held {
|
for _, key := range held {
|
||||||
if err := press(key, keyStatePressed); err != nil {
|
if err := press(key); err != nil {
|
||||||
return fmt.Errorf("key press: %w", err)
|
return fmt.Errorf("key press: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
|
if err := vk.Modifiers(mods, 0, 0, 0); err != nil {
|
||||||
return fmt.Errorf("set modifiers: %w", err)
|
return fmt.Errorf("set modifiers: %w", err)
|
||||||
}
|
}
|
||||||
if err := press(keys.v, keyStatePressed); err != nil {
|
if err := press(keys.Keycode("v")); err != nil {
|
||||||
return fmt.Errorf("key press: %w", err)
|
return fmt.Errorf("key press: %w", err)
|
||||||
}
|
}
|
||||||
if err := press(keys.v, keyStateReleased); err != nil {
|
|
||||||
return fmt.Errorf("key release: %w", err)
|
|
||||||
}
|
|
||||||
for i := len(held) - 1; i >= 0; i-- {
|
|
||||||
if err := press(held[i], keyStateReleased); err != nil {
|
|
||||||
return fmt.Errorf("key release: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := vk.Modifiers(0, 0, 0, 0); err != nil {
|
|
||||||
return fmt.Errorf("clear modifiers: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.display.Roundtrip()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readKeymap(fd int, size uint32) (string, error) {
|
|
||||||
data, err := unix.Mmap(fd, 0, int(size), unix.PROT_READ, unix.MAP_PRIVATE)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
text := strings.TrimRight(string(data), "\x00")
|
|
||||||
return text, unix.Munmap(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type pasteKeycodes struct {
|
|
||||||
ctrl uint32
|
|
||||||
shift uint32
|
|
||||||
v uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
keycodeDefRe = regexp.MustCompile(`<([A-Za-z0-9+_-]+)>\s*=\s*(\d+)`)
|
|
||||||
keySymbolsRe = regexp.MustCompile(`key\s*<([A-Za-z0-9+_-]+)>\s*\{([^}]*)\}`)
|
|
||||||
groupIndexRe = regexp.MustCompile(`\w+\[\d+\]\s*=`)
|
|
||||||
symbolListRe = regexp.MustCompile(`\[([^\]]*)\]`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// xkbcommon may serialize keysyms as hex escapes instead of names
|
|
||||||
// (e.g. "0x76" for v, "0xffe3" for Control_L).
|
|
||||||
var keysymNames = map[uint32]string{
|
|
||||||
0x76: "v",
|
|
||||||
0xffe3: "Control_L",
|
|
||||||
0xffe1: "Shift_L",
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalKeysym(sym string) string {
|
|
||||||
if !strings.HasPrefix(sym, "0x") && !strings.HasPrefix(sym, "0X") {
|
|
||||||
return sym
|
|
||||||
}
|
|
||||||
value, err := strconv.ParseUint(sym[2:], 16, 32)
|
|
||||||
if err != nil {
|
|
||||||
return sym
|
|
||||||
}
|
|
||||||
if name, ok := keysymNames[uint32(value)]; ok {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return sym
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolveKeycodes finds the evdev keycodes producing the keysyms we need in
|
|
||||||
// the seat keymap's first group, falling back to pc105 positions.
|
|
||||||
func resolveKeycodes(keymap string) pasteKeycodes {
|
|
||||||
codes := map[string]uint32{}
|
|
||||||
for _, m := range keycodeDefRe.FindAllStringSubmatch(keymap, -1) {
|
|
||||||
if code, err := strconv.Atoi(m[2]); err == nil {
|
|
||||||
codes[m[1]] = uint32(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}
|
|
||||||
want := map[string]*uint32{
|
|
||||||
"Control_L": &keys.ctrl,
|
|
||||||
"Shift_L": &keys.shift,
|
|
||||||
"v": &keys.v,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, m := range keySymbolsRe.FindAllStringSubmatch(keymap, -1) {
|
|
||||||
group := symbolListRe.FindStringSubmatch(groupIndexRe.ReplaceAllString(m[2], ""))
|
|
||||||
if group == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
xkbCode, ok := codes[m[1]]
|
|
||||||
if !ok || xkbCode < 8 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
level1 := canonicalKeysym(strings.TrimSpace(strings.Split(group[1], ",")[0]))
|
|
||||||
target, wanted := want[level1]
|
|
||||||
if !wanted {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
*target = xkbCode - 8
|
|
||||||
delete(want, level1)
|
|
||||||
if len(want) == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||||
|
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,20 +33,20 @@ func TestLiveSeatKeymapResolution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer keyboard.Release()
|
defer keyboard.Release()
|
||||||
|
|
||||||
var keymap *wlclient.KeyboardKeymapEvent
|
var seatKeymap *wlclient.KeyboardKeymapEvent
|
||||||
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
keyboard.SetKeymapHandler(func(e wlclient.KeyboardKeymapEvent) {
|
||||||
if keymap == nil {
|
if seatKeymap == nil {
|
||||||
keymap = &e
|
seatKeymap = &e
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
if keymap == nil {
|
if seatKeymap == nil {
|
||||||
t.Fatal("no keymap event")
|
t.Fatal("no keymap event")
|
||||||
}
|
}
|
||||||
defer unix.Close(keymap.Fd)
|
defer unix.Close(seatKeymap.Fd)
|
||||||
|
|
||||||
text, err := readKeymap(keymap.Fd, keymap.Size)
|
text, err := keymap.Read(seatKeymap.Fd, seatKeymap.Size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read keymap: %v", err)
|
t.Fatalf("read keymap: %v", err)
|
||||||
}
|
}
|
||||||
@@ -56,10 +57,7 @@ func TestLiveSeatKeymapResolution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := resolveKeycodes(text)
|
keys := keymap.Parse(text)
|
||||||
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", keymap.Size, keys.ctrl, keys.shift, keys.v)
|
t.Logf("keymap size=%d resolved ctrl=%d shift=%d v=%d", seatKeymap.Size,
|
||||||
|
keys.Keycode("Control_L"), keys.Keycode("Shift_L"), keys.Keycode("v"))
|
||||||
if keys.ctrl == fallbackCtrlKey && keys.shift == fallbackShiftKey && keys.v == fallbackVKey {
|
|
||||||
t.Log("all keycodes are fallbacks - parsing may not have matched the live keymap")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
package clipboard
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
const qwertyKeymap = `xkb_keymap {
|
|
||||||
xkb_keycodes "(unnamed)" {
|
|
||||||
minimum = 8;
|
|
||||||
maximum = 708;
|
|
||||||
<ESC> = 9;
|
|
||||||
<AB04> = 55;
|
|
||||||
<LCTL> = 37;
|
|
||||||
<LFSH> = 50;
|
|
||||||
alias <AL01> = <AC01>;
|
|
||||||
indicator 1 = "Caps Lock";
|
|
||||||
};
|
|
||||||
xkb_types "(unnamed)" {
|
|
||||||
type "ALPHABETIC" {
|
|
||||||
modifiers = Shift+Lock;
|
|
||||||
map[Shift] = Level2;
|
|
||||||
level_name[Level1] = "Base";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
xkb_symbols "(unnamed)" {
|
|
||||||
key <ESC> { [ Escape ] };
|
|
||||||
key <AB04> { type= "ALPHABETIC", [ v, V ] };
|
|
||||||
key <LCTL> { [ Control_L ] };
|
|
||||||
key <LFSH> { [ Shift_L ] };
|
|
||||||
};
|
|
||||||
};`
|
|
||||||
|
|
||||||
const hexKeymap = `xkb_keymap {
|
|
||||||
xkb_keycodes "(unnamed)" {
|
|
||||||
<AB04> = 56;
|
|
||||||
<LCTL> = 38;
|
|
||||||
<LFSH> = 51;
|
|
||||||
};
|
|
||||||
xkb_symbols "(unnamed)" {
|
|
||||||
key <LCTL> { [ 0xffe3 ] };
|
|
||||||
key <LFSH> {
|
|
||||||
type= "PC_ALT_LEVEL2",
|
|
||||||
symbols[1]= [ 0xffe1, 0xfe08 ]
|
|
||||||
};
|
|
||||||
key <AB04> { [ 0x76, 0x56 ] };
|
|
||||||
};
|
|
||||||
};`
|
|
||||||
|
|
||||||
const dvorakKeymap = `xkb_keymap {
|
|
||||||
xkb_keycodes "(unnamed)" {
|
|
||||||
<AB09> = 60;
|
|
||||||
<LCTL> = 37;
|
|
||||||
<LFSH> = 50;
|
|
||||||
};
|
|
||||||
xkb_symbols "(unnamed)" {
|
|
||||||
key <AB09> { [ v, V ] };
|
|
||||||
key <LCTL> { [ Control_L ] };
|
|
||||||
key <LFSH> { [ Shift_L ] };
|
|
||||||
};
|
|
||||||
};`
|
|
||||||
|
|
||||||
func TestResolveKeycodes(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
keymap string
|
|
||||||
want pasteKeycodes
|
|
||||||
}{
|
|
||||||
{"qwerty", qwertyKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 47}},
|
|
||||||
{"dvorak", dvorakKeymap, pasteKeycodes{ctrl: 29, shift: 42, v: 52}},
|
|
||||||
{"hex keysyms", hexKeymap, pasteKeycodes{ctrl: 30, shift: 43, v: 48}},
|
|
||||||
{"empty falls back", "", pasteKeycodes{ctrl: fallbackCtrlKey, shift: fallbackShiftKey, v: fallbackVKey}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
got := resolveKeycodes(tt.keymap)
|
|
||||||
if got != tt.want {
|
|
||||||
t.Errorf("resolveKeycodes() = %+v, want %+v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestExpandOffers(t *testing.T) {
|
|
||||||
text := ExpandOffers([]byte("hi"), "text/plain;charset=utf-8")
|
|
||||||
if len(text) != 5 {
|
|
||||||
t.Fatalf("expected 5 text offers, got %d", len(text))
|
|
||||||
}
|
|
||||||
seen := map[string]bool{}
|
|
||||||
for _, o := range text {
|
|
||||||
if string(o.Data) != "hi" {
|
|
||||||
t.Errorf("offer %s has wrong data", o.MimeType)
|
|
||||||
}
|
|
||||||
if seen[o.MimeType] {
|
|
||||||
t.Errorf("duplicate offer %s", o.MimeType)
|
|
||||||
}
|
|
||||||
seen[o.MimeType] = true
|
|
||||||
}
|
|
||||||
if !seen["UTF8_STRING"] || !seen["STRING"] || !seen["TEXT"] || !seen["text/plain"] {
|
|
||||||
t.Errorf("missing X11 alias offers: %v", seen)
|
|
||||||
}
|
|
||||||
|
|
||||||
img := ExpandOffers([]byte{1}, "image/png")
|
|
||||||
if len(img) != 1 || img[0].MimeType != "image/png" {
|
|
||||||
t.Errorf("non-text mime should not expand, got %+v", img)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,7 +47,15 @@ func Store(data []byte, mimeType string) error {
|
|||||||
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) (err error) {
|
||||||
|
defer func() {
|
||||||
|
r := recover()
|
||||||
|
if r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = fmt.Errorf("clipboard db panic: %v", r)
|
||||||
|
}()
|
||||||
|
|
||||||
if len(data) == 0 {
|
if len(data) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,191 +8,24 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/dankgo/wayland/ext_data_control"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClipboardChange struct {
|
// WatchAll subscribes to clipboard selection changes like wlclipboard.Watch,
|
||||||
Data []byte
|
// additionally reporting every mime type each selection offers.
|
||||||
MimeType string
|
|
||||||
MimeTypes []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
|
||||||
display, err := wlclient.Connect("")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
|
||||||
}
|
|
||||||
defer display.Destroy()
|
|
||||||
|
|
||||||
wlCtx := display.Context()
|
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get registry: %w", err)
|
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(wlCtx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("get data device: %w", err)
|
|
||||||
}
|
|
||||||
defer device.Destroy()
|
|
||||||
|
|
||||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
|
||||||
|
|
||||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
offerMimeTypes[e.Id] = nil
|
|
||||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
|
||||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
|
||||||
if e.Id == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mimes := offerMimeTypes[e.Id]
|
|
||||||
selectedMime := selectPreferredMimeType(mimes)
|
|
||||||
if selectedMime == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
r, w, err := os.Pipe()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
|
||||||
w.Close()
|
|
||||||
r.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Close()
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer r.Close()
|
|
||||||
data, err := io.ReadAll(r)
|
|
||||||
if err != nil || len(data) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callback(data, selectedMime)
|
|
||||||
}()
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
|
||||||
}
|
|
||||||
if err := wlCtx.Dispatch(); err != nil {
|
|
||||||
if isTimeoutError(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return fmt.Errorf("dispatch: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
|
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
|
||||||
display, err := wlclient.Connect("")
|
s, err := connectSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
defer display.Destroy()
|
defer s.Close()
|
||||||
|
|
||||||
wlCtx := display.Context()
|
dataControlMgr, err := s.requireDataControl()
|
||||||
registry, err := display.GetRegistry()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get registry: %w", err)
|
return err
|
||||||
}
|
|
||||||
defer registry.Destroy()
|
|
||||||
|
|
||||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
|
||||||
var seat *wlclient.Seat
|
|
||||||
var bindErr error
|
|
||||||
|
|
||||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
|
||||||
switch e.Interface {
|
|
||||||
case "ext_data_control_manager_v1":
|
|
||||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
case "wl_seat":
|
|
||||||
if seat != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seat = wlclient.NewSeat(wlCtx)
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
|
||||||
bindErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
display.Roundtrip()
|
|
||||||
display.Roundtrip()
|
|
||||||
|
|
||||||
if bindErr != nil {
|
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
|
||||||
}
|
|
||||||
if dataControlMgr == nil {
|
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
|
||||||
}
|
|
||||||
defer dataControlMgr.Destroy()
|
|
||||||
if seat == nil {
|
|
||||||
return fmt.Errorf("no seat available")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
device, err := dataControlMgr.GetDataDevice(seat)
|
device, err := dataControlMgr.GetDataDevice(s.seat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get data device: %w", err)
|
return fmt.Errorf("get data device: %w", err)
|
||||||
}
|
}
|
||||||
@@ -246,18 +79,18 @@ func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, a
|
|||||||
}()
|
}()
|
||||||
})
|
})
|
||||||
|
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
display.Roundtrip()
|
s.display.Roundtrip()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
default:
|
default:
|
||||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
if err := s.ctx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
return fmt.Errorf("set read deadline: %w", err)
|
||||||
}
|
}
|
||||||
if err := wlCtx.Dispatch(); err != nil {
|
if err := s.ctx.Dispatch(); err != nil {
|
||||||
if isTimeoutError(err) {
|
if isTimeoutError(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -267,6 +100,31 @@ func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectPreferredMimeType(mimes []string) string {
|
||||||
|
preferred := []string{
|
||||||
|
"text/plain;charset=utf-8",
|
||||||
|
"text/plain",
|
||||||
|
"UTF8_STRING",
|
||||||
|
"STRING",
|
||||||
|
"TEXT",
|
||||||
|
"image/png",
|
||||||
|
"image/jpeg",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pref := range preferred {
|
||||||
|
for _, mime := range mimes {
|
||||||
|
if mime == pref {
|
||||||
|
return mime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mimes) > 0 {
|
||||||
|
return mimes[0]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func isTimeoutError(err error) bool {
|
func isTimeoutError(err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
@@ -279,25 +137,3 @@ func isTimeoutError(err error) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
|
||||||
ch := make(chan ClipboardChange, 16)
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer close(ch)
|
|
||||||
err := Watch(ctx, func(data []byte, mimeType string) {
|
|
||||||
select {
|
|
||||||
case ch <- ClipboardChange{Data: data, MimeType: mimeType}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if err != nil && err != context.Canceled {
|
|
||||||
errCh <- err
|
|
||||||
}
|
|
||||||
close(errCh)
|
|
||||||
}()
|
|
||||||
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
return ch, errCh
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package clipboard
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/virtual_keyboard"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
|
"github.com/AvengeMedia/dankgo/wayland/ext_data_control"
|
||||||
)
|
)
|
||||||
|
|
||||||
type session struct {
|
type session struct {
|
||||||
|
|||||||
@@ -8,17 +8,21 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/keyboard_shortcuts_inhibit"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_layer_shell"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_screencopy"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_color_management"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wp_viewporter"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/keymap"
|
||||||
|
"github.com/AvengeMedia/dankgo/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Format OutputFormat
|
Format OutputFormat
|
||||||
CustomFormat string
|
CustomFormat string
|
||||||
Lowercase bool
|
Lowercase bool
|
||||||
Autocopy bool
|
Autocopy bool
|
||||||
Notify bool
|
Notify bool
|
||||||
|
AllowMultiple bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Output struct {
|
type Output struct {
|
||||||
@@ -34,18 +38,19 @@ type Output struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LayerSurface struct {
|
type LayerSurface struct {
|
||||||
output *Output
|
output *Output
|
||||||
state *SurfaceState
|
state *SurfaceState
|
||||||
wlSurface *client.Surface
|
wlSurface *client.Surface
|
||||||
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
layerSurf *wlr_layer_shell.ZwlrLayerSurfaceV1
|
||||||
viewport *wp_viewporter.WpViewport
|
viewport *wp_viewporter.WpViewport
|
||||||
wlPools [2]*client.ShmPool
|
wlPools [2]*client.ShmPool
|
||||||
wlBuffers [2]*client.Buffer
|
wlBuffers [2]*client.Buffer
|
||||||
slotBusy [2]bool
|
slotBusy [2]bool
|
||||||
needsRedraw bool
|
needsRedraw bool
|
||||||
scopyBuffer *client.Buffer
|
framePending bool
|
||||||
configured bool
|
scopyBuffer *client.Buffer
|
||||||
hidden bool
|
configured bool
|
||||||
|
hidden bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Picker struct {
|
type Picker struct {
|
||||||
@@ -60,9 +65,13 @@ type Picker struct {
|
|||||||
seat *client.Seat
|
seat *client.Seat
|
||||||
pointer *client.Pointer
|
pointer *client.Pointer
|
||||||
keyboard *client.Keyboard
|
keyboard *client.Keyboard
|
||||||
|
keymap *keymap.Keymap
|
||||||
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
layerShell *wlr_layer_shell.ZwlrLayerShellV1
|
||||||
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
screencopy *wlr_screencopy.ZwlrScreencopyManagerV1
|
||||||
viewporter *wp_viewporter.WpViewporter
|
viewporter *wp_viewporter.WpViewporter
|
||||||
|
colorMgr *wp_color_management.WpColorManagerV1
|
||||||
|
|
||||||
|
compositorVersion uint32
|
||||||
|
|
||||||
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
shortcutsInhibitMgr *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1
|
||||||
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
shortcutsInhibitor *keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitorV1
|
||||||
@@ -86,6 +95,14 @@ func New(config Config) *Picker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) Run() (*Color, error) {
|
func (p *Picker) Run() (*Color, error) {
|
||||||
|
if !p.config.AllowMultiple {
|
||||||
|
lock, err := utils.LockSelectionOverlay()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer lock.Release()
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.connect(); err != nil {
|
if err := p.connect(); err != nil {
|
||||||
return nil, fmt.Errorf("wayland connect: %w", err)
|
return nil, fmt.Errorf("wayland connect: %w", err)
|
||||||
}
|
}
|
||||||
@@ -164,9 +181,10 @@ func (p *Picker) checkDone() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flushRedraws paints queued surfaces, at most once per compositor frame each.
|
||||||
func (p *Picker) flushRedraws() {
|
func (p *Picker) flushRedraws() {
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
if !ls.needsRedraw {
|
if !ls.needsRedraw || ls.framePending {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
p.redrawSurface(ls)
|
p.redrawSurface(ls)
|
||||||
@@ -213,6 +231,7 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
compositor := client.NewCompositor(p.ctx)
|
compositor := client.NewCompositor(p.ctx)
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, compositor); err == nil {
|
||||||
p.compositor = compositor
|
p.compositor = compositor
|
||||||
|
p.compositorVersion = e.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
case client.ShmInterfaceName:
|
case client.ShmInterfaceName:
|
||||||
@@ -263,6 +282,12 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
p.viewporter = viewporter
|
p.viewporter = viewporter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case wp_color_management.WpColorManagerV1InterfaceName:
|
||||||
|
mgr := wp_color_management.NewWpColorManagerV1(p.ctx)
|
||||||
|
if err := p.registry.Bind(e.Name, e.Interface, 1, mgr); err == nil {
|
||||||
|
p.colorMgr = mgr
|
||||||
|
}
|
||||||
|
|
||||||
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
case keyboard_shortcuts_inhibit.ZwpKeyboardShortcutsInhibitManagerV1InterfaceName:
|
||||||
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
|
mgr := keyboard_shortcuts_inhibit.NewZwpKeyboardShortcutsInhibitManagerV1(p.ctx)
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, e.Version, mgr); err == nil {
|
||||||
@@ -321,16 +346,38 @@ func (p *Picker) createSurfaces() error {
|
|||||||
p.outputsMu.Unlock()
|
p.outputsMu.Unlock()
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
|
refLum, pq := p.outputPQEncoding(output)
|
||||||
ls, err := p.createLayerSurface(output)
|
ls, err := p.createLayerSurface(output)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("output %s: %w", output.name, err)
|
return fmt.Errorf("output %s: %w", output.name, err)
|
||||||
}
|
}
|
||||||
|
if pq {
|
||||||
|
ls.state.SetPQEncoding(refLum)
|
||||||
|
}
|
||||||
p.surfaces = append(p.surfaces, ls)
|
p.surfaces = append(p.surfaces, ls)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Picker) outputPQEncoding(output *Output) (float64, bool) {
|
||||||
|
if p.colorMgr == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
desc, ok := wp_color_management.QueryOutputDescription(p.colorMgr, output.wlOutput, p.roundtrip)
|
||||||
|
if !ok {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
pq := desc.TF == uint32(wp_color_management.WpColorManagerV1TransferFunctionSt2084Pq) &&
|
||||||
|
desc.Primaries == uint32(wp_color_management.WpColorManagerV1PrimariesBt2020)
|
||||||
|
if !pq {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return float64(desc.ReferenceLum), true
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
|
func (p *Picker) createLayerSurface(output *Output) (*LayerSurface, error) {
|
||||||
surface, err := p.compositor.CreateSurface()
|
surface, err := p.compositor.CreateSurface()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -517,17 +564,18 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
|
|
||||||
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
func (p *Picker) redrawSurface(ls *LayerSurface) {
|
||||||
slot := ls.state.FrontIndex()
|
slot := ls.state.FrontIndex()
|
||||||
if ls.slotBusy[slot] {
|
if ls.slotBusy[slot] || ls.framePending {
|
||||||
ls.needsRedraw = true
|
ls.needsRedraw = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var renderBuf *ShmBuffer
|
var renderBuf *ShmBuffer
|
||||||
|
var damage []rect
|
||||||
switch {
|
switch {
|
||||||
case ls.hidden:
|
case ls.hidden:
|
||||||
renderBuf = ls.state.RedrawScreenOnly()
|
renderBuf, damage = ls.state.RedrawScreenOnly()
|
||||||
default:
|
default:
|
||||||
renderBuf = ls.state.Redraw()
|
renderBuf, damage = ls.state.Redraw()
|
||||||
}
|
}
|
||||||
if renderBuf == nil {
|
if renderBuf == nil {
|
||||||
return
|
return
|
||||||
@@ -574,7 +622,25 @@ func (p *Picker) redrawSurface(ls *LayerSurface) {
|
|||||||
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
_ = ls.wlSurface.SetBufferScale(bufferScale)
|
||||||
}
|
}
|
||||||
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
|
_ = ls.wlSurface.Attach(ls.wlBuffers[slot], 0, 0)
|
||||||
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
switch {
|
||||||
|
case p.compositorVersion < 4:
|
||||||
|
_ = ls.wlSurface.Damage(0, 0, int32(logicalW), int32(logicalH))
|
||||||
|
default:
|
||||||
|
for _, r := range damage {
|
||||||
|
r = r.clip(renderBuf.Width, renderBuf.Height)
|
||||||
|
if r.w <= 0 || r.h <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
_ = ls.wlSurface.DamageBuffer(int32(r.x), int32(r.y), int32(r.w), int32(r.h))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cb, err := ls.wlSurface.Frame(); err == nil {
|
||||||
|
ls.framePending = true
|
||||||
|
cb.SetDoneHandler(func(client.CallbackDoneEvent) {
|
||||||
|
_ = cb.Destroy()
|
||||||
|
ls.framePending = false
|
||||||
|
})
|
||||||
|
}
|
||||||
_ = ls.wlSurface.Commit()
|
_ = ls.wlSurface.Commit()
|
||||||
|
|
||||||
ls.state.SwapBuffers()
|
ls.state.SwapBuffers()
|
||||||
@@ -674,9 +740,14 @@ func (p *Picker) setupPointerHandlers() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *Picker) setupKeyboardHandlers() {
|
func (p *Picker) setupKeyboardHandlers() {
|
||||||
|
p.keyboard.SetKeymapHandler(func(e client.KeyboardKeymapEvent) {
|
||||||
|
p.keymap = keymap.FromEvent(e)
|
||||||
|
})
|
||||||
|
|
||||||
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
p.keyboard.SetKeyHandler(func(e client.KeyboardKeyEvent) {
|
||||||
|
sym := p.keymap.Keysym(e.Key)
|
||||||
for _, ls := range p.surfaces {
|
for _, ls := range p.surfaces {
|
||||||
ls.state.OnKey(e.Key, e.State)
|
ls.state.OnKey(sym, e.State)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -726,6 +797,10 @@ func (p *Picker) cleanup() {
|
|||||||
p.viewporter.Destroy()
|
p.viewporter.Destroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if p.colorMgr != nil {
|
||||||
|
p.colorMgr.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
if p.screencopy != nil {
|
if p.screencopy != nil {
|
||||||
p.screencopy.Destroy()
|
p.screencopy.Destroy()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ type SurfaceState struct {
|
|||||||
|
|
||||||
screenBuf *ShmBuffer
|
screenBuf *ShmBuffer
|
||||||
screenFormat PixelFormat
|
screenFormat PixelFormat
|
||||||
|
pqEncoded bool
|
||||||
|
refLum float64
|
||||||
yInverted bool
|
yInverted bool
|
||||||
|
|
||||||
logicalW int
|
logicalW int
|
||||||
@@ -32,6 +34,10 @@ type SurfaceState struct {
|
|||||||
|
|
||||||
renderBufs [2]*ShmBuffer
|
renderBufs [2]*ShmBuffer
|
||||||
front int
|
front int
|
||||||
|
primed [2]bool
|
||||||
|
drawn [2][]rect
|
||||||
|
shown []rect
|
||||||
|
fullDamage bool
|
||||||
|
|
||||||
scale int32
|
scale int32
|
||||||
scaleX float64
|
scaleX float64
|
||||||
@@ -56,6 +62,13 @@ func NewSurfaceState(format OutputFormat, lowercase bool) *SurfaceState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SurfaceState) SetPQEncoding(refLum float64) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.pqEncoded = true
|
||||||
|
s.refLum = refLum
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) SetScale(scale int32) {
|
func (s *SurfaceState) SetScale(scale int32) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
@@ -152,6 +165,17 @@ func (s *SurfaceState) OnScreencopyReady() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Magnifier rendering and pixel picking work on 8-bit pixels only
|
||||||
|
if s.screenFormat.Is10Bit() {
|
||||||
|
switch {
|
||||||
|
case s.pqEncoded:
|
||||||
|
s.screenBuf.ConvertPQ2020ToSRGB(s.refLum)
|
||||||
|
default:
|
||||||
|
s.screenBuf.Convert10To8()
|
||||||
|
}
|
||||||
|
s.screenFormat = s.screenBuf.Format
|
||||||
|
}
|
||||||
|
|
||||||
s.recomputeScale()
|
s.recomputeScale()
|
||||||
s.ensureRenderBuffers()
|
s.ensureRenderBuffers()
|
||||||
s.readyForDisplay = true
|
s.readyForDisplay = true
|
||||||
@@ -192,6 +216,10 @@ func (s *SurfaceState) ensureRenderBuffers() {
|
|||||||
if s.screenBuf == nil {
|
if s.screenBuf == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.primed = [2]bool{}
|
||||||
|
s.drawn = [2][]rect{}
|
||||||
|
s.shown = nil
|
||||||
|
s.fullDamage = true
|
||||||
|
|
||||||
width := s.screenBuf.Width
|
width := s.screenBuf.Width
|
||||||
height := s.screenBuf.Height
|
height := s.screenBuf.Height
|
||||||
@@ -238,7 +266,7 @@ func (s *SurfaceState) OnPointerButton(button, state uint32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) OnKey(key, state uint32) {
|
func (s *SurfaceState) OnKey(sym string, state uint32) {
|
||||||
if state != 1 {
|
if state != 1 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -246,10 +274,10 @@ func (s *SurfaceState) OnKey(key, state uint32) {
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
switch key {
|
switch sym {
|
||||||
case 1: // KEY_ESC
|
case "Escape":
|
||||||
s.cancelled = true
|
s.cancelled = true
|
||||||
case 28: // KEY_ENTER
|
case "Return", "KP_Enter":
|
||||||
if s.readyForDisplay && s.screenBuf != nil {
|
if s.readyForDisplay && s.screenBuf != nil {
|
||||||
s.colorPicked = true
|
s.colorPicked = true
|
||||||
}
|
}
|
||||||
@@ -286,21 +314,70 @@ func (s *SurfaceState) SwapBuffers() {
|
|||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) Redraw() *ShmBuffer {
|
// rect is a buffer-pixel area, clipped on use.
|
||||||
|
type rect struct{ x, y, w, h int }
|
||||||
|
|
||||||
|
func (r rect) clip(w, h int) rect {
|
||||||
|
x1, y1 := max(r.x, 0), max(r.y, 0)
|
||||||
|
x2, y2 := min(r.x+r.w, w), min(r.y+r.h, h)
|
||||||
|
return rect{x1, y1, max(x2-x1, 0), max(y2-y1, 0)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func magnifierRect(cx, cy int) rect {
|
||||||
|
const reach = magnifierRadius + 2
|
||||||
|
return rect{cx - reach, cy - reach, 2*reach + 1, 2*reach + 1}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore puts screen pixels back under the front buffer's previous overlay and
|
||||||
|
// records cur as its new footprint. It returns what differs from the frame on screen.
|
||||||
|
func (s *SurfaceState) restore(dst *ShmBuffer, cur []rect) []rect {
|
||||||
|
i := s.front
|
||||||
|
switch {
|
||||||
|
case !s.primed[i]:
|
||||||
|
dst.CopyFrom(s.screenBuf)
|
||||||
|
s.primed[i] = true
|
||||||
|
default:
|
||||||
|
for _, r := range s.drawn[i] {
|
||||||
|
s.copyRect(dst, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.drawn[i] = cur
|
||||||
|
|
||||||
|
damage := append(append([]rect(nil), s.shown...), cur...)
|
||||||
|
if s.fullDamage {
|
||||||
|
s.fullDamage = false
|
||||||
|
damage = []rect{{0, 0, dst.Width, dst.Height}}
|
||||||
|
}
|
||||||
|
s.shown = cur
|
||||||
|
return damage
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SurfaceState) copyRect(dst *ShmBuffer, r rect) {
|
||||||
|
r = r.clip(min(dst.Width, s.screenBuf.Width), min(dst.Height, s.screenBuf.Height))
|
||||||
|
if r.w <= 0 || r.h <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
src := s.screenBuf
|
||||||
|
for y := r.y; y < r.y+r.h; y++ {
|
||||||
|
copy(dst.Data()[y*dst.Stride+r.x*4:][:r.w*4], src.Data()[y*src.Stride+r.x*4:][:r.w*4])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw draws the magnifier and preview at the pointer into the front buffer,
|
||||||
|
// touching only the previous and new overlay areas; the rects are the damage.
|
||||||
|
func (s *SurfaceState) Redraw() (*ShmBuffer, []rect) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
if !s.readyForDisplay || s.screenBuf == nil {
|
if !s.readyForDisplay || s.screenBuf == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := s.renderBufs[s.front]
|
dst := s.renderBufs[s.front]
|
||||||
if dst == nil {
|
if dst == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dst.CopyFrom(s.screenBuf)
|
|
||||||
|
|
||||||
px := int(math.Round(float64(s.pointerX) * s.scaleX))
|
px := int(math.Round(float64(s.pointerX) * s.scaleX))
|
||||||
py := int(math.Round(float64(s.pointerY) * s.scaleY))
|
py := int(math.Round(float64(s.pointerY) * s.scaleY))
|
||||||
|
|
||||||
@@ -313,6 +390,8 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
picked := GetPixelColorWithFormat(s.screenBuf, px, sampleY, s.screenFormat)
|
picked := GetPixelColorWithFormat(s.screenBuf, px, sampleY, s.screenFormat)
|
||||||
|
text := formatColorForPreview(picked, s.displayFormat, s.lowercase)
|
||||||
|
damage := s.restore(dst, []rect{magnifierRect(px, py), previewRect(dst.Width, dst.Height, px, py, text)})
|
||||||
|
|
||||||
drawMagnifierWithInversion(
|
drawMagnifierWithInversion(
|
||||||
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
||||||
@@ -322,26 +401,25 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
|
|
||||||
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
|
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
|
||||||
|
|
||||||
return dst
|
return dst, damage
|
||||||
}
|
}
|
||||||
|
|
||||||
// RedrawScreenOnly renders just the screenshot without any overlay (magnifier, preview).
|
// RedrawScreenOnly renders just the screenshot without any overlay (magnifier, preview).
|
||||||
// Used for when pointer leaves the surface.
|
// Used for when pointer leaves the surface.
|
||||||
func (s *SurfaceState) RedrawScreenOnly() *ShmBuffer {
|
func (s *SurfaceState) RedrawScreenOnly() (*ShmBuffer, []rect) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
if !s.readyForDisplay || s.screenBuf == nil {
|
if !s.readyForDisplay || s.screenBuf == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dst := s.renderBufs[s.front]
|
dst := s.renderBufs[s.front]
|
||||||
if dst == nil {
|
if dst == nil {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
dst.CopyFrom(s.screenBuf)
|
return dst, s.restore(dst, nil)
|
||||||
return dst
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) PickColor() (Color, bool) {
|
func (s *SurfaceState) PickColor() (Color, bool) {
|
||||||
@@ -422,6 +500,8 @@ func blendColors(bg, fg Color, alpha float64) Color {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const magnifierRadius = 80
|
||||||
|
|
||||||
func drawMagnifierWithInversion(
|
func drawMagnifierWithInversion(
|
||||||
dst []byte, dstStride, dstW, dstH int,
|
dst []byte, dstStride, dstW, dstH int,
|
||||||
src []byte, srcStride, srcW, srcH int,
|
src []byte, srcStride, srcW, srcH int,
|
||||||
@@ -435,7 +515,7 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
outerRadius = 80
|
outerRadius = magnifierRadius
|
||||||
borderThickness = 4
|
borderThickness = 4
|
||||||
aaWidth = 1.5
|
aaWidth = 1.5
|
||||||
zoom = 8.0
|
zoom = 8.0
|
||||||
@@ -530,7 +610,7 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
|
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff]}
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -543,12 +623,11 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
|
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius)
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawMagnifierCrosshair(
|
func drawMagnifierCrosshair(
|
||||||
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
|
data []byte, stride, width, height, cx, cy, radius, thickness, innerRadius int,
|
||||||
format PixelFormat,
|
|
||||||
) {
|
) {
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return
|
return
|
||||||
@@ -1046,30 +1125,28 @@ var fontGlyphs = map[rune][fontH]uint8{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
|
const (
|
||||||
text := formatColorForPreview(c, format, lowercase)
|
previewPaddingX = 8
|
||||||
|
previewPaddingY = 4
|
||||||
|
previewSpace = 2
|
||||||
|
previewOffset = 88
|
||||||
|
)
|
||||||
|
|
||||||
|
// previewRect places the color label beside the magnifier, flipping to the left
|
||||||
|
// and clamping so it stays on the buffer.
|
||||||
|
func previewRect(width, height, cx, cy int, text string) rect {
|
||||||
if len(text) == 0 {
|
if len(text) == 0 {
|
||||||
return
|
return rect{}
|
||||||
}
|
}
|
||||||
|
textW := len(text)*(fontW+previewSpace) - previewSpace
|
||||||
|
boxW := textW + previewPaddingX*2
|
||||||
|
boxH := fontH + previewPaddingY*2
|
||||||
|
|
||||||
const (
|
x := cx + previewOffset
|
||||||
paddingX = 8
|
|
||||||
paddingY = 4
|
|
||||||
space = 2
|
|
||||||
offset = 88
|
|
||||||
)
|
|
||||||
|
|
||||||
textW := len(text)*(fontW+space) - space
|
|
||||||
textH := fontH
|
|
||||||
|
|
||||||
boxW := textW + paddingX*2
|
|
||||||
boxH := textH + paddingY*2
|
|
||||||
|
|
||||||
x := cx + offset
|
|
||||||
y := cy - boxH/2
|
y := cy - boxH/2
|
||||||
|
|
||||||
if x+boxW >= width {
|
if x+boxW >= width {
|
||||||
x = cx - boxW - offset
|
x = cx - boxW - previewOffset
|
||||||
}
|
}
|
||||||
if x < 0 {
|
if x < 0 {
|
||||||
x = 0
|
x = 0
|
||||||
@@ -1080,8 +1157,18 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
|
|||||||
if y+boxH >= height {
|
if y+boxH >= height {
|
||||||
y = height - boxH
|
y = height - boxH
|
||||||
}
|
}
|
||||||
|
return rect{x, y, boxW, boxH}
|
||||||
|
}
|
||||||
|
|
||||||
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
|
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
|
||||||
|
text := formatColorForPreview(c, format, lowercase)
|
||||||
|
if len(text) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
box := previewRect(width, height, cx, cy, text)
|
||||||
|
x, y := box.x, box.y
|
||||||
|
drawFilledRect(data, stride, width, height, x, y, box.w, box.h, c, pixelFormat)
|
||||||
|
|
||||||
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
|
lum := 0.299*float64(c.R) + 0.587*float64(c.G) + 0.114*float64(c.B)
|
||||||
var fg Color
|
var fg Color
|
||||||
@@ -1090,7 +1177,7 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
|
|||||||
} else {
|
} else {
|
||||||
fg = Color{R: 255, G: 255, B: 255, A: 255}
|
fg = Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
}
|
}
|
||||||
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
|
drawText(data, stride, width, height, x+previewPaddingX, y+previewPaddingY, text, fg, pixelFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
|
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
|
||||||
|
|||||||
@@ -45,14 +45,12 @@ func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 2 {
|
for range goroutines / 2 {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
scale := s.Scale()
|
scale := s.Scale()
|
||||||
assert.GreaterOrEqual(t, scale, int32(1))
|
assert.GreaterOrEqual(t, scale, int32(1))
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -76,15 +74,13 @@ func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 2 {
|
for range goroutines / 2 {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
w, h := s.LogicalSize()
|
w, h := s.LogicalSize()
|
||||||
_ = w
|
_ = w
|
||||||
_ = h
|
_ = h
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -98,35 +94,29 @@ func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
|||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for range goroutines / 3 {
|
for range goroutines / 3 {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
s.OnPointerButton(0x110, 1)
|
s.OnPointerButton(0x110, 1)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 3 {
|
for range goroutines / 3 {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
s.OnKey(1, 1)
|
s.OnKey("Escape", 1)
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for range goroutines / 3 {
|
for range goroutines / 3 {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
picked, cancelled := s.IsDone()
|
picked, cancelled := s.IsDone()
|
||||||
_ = picked
|
_ = picked
|
||||||
_ = cancelled
|
_ = cancelled
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -140,13 +130,11 @@ func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
|||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for range goroutines {
|
for range goroutines {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
_ = s.IsReady()
|
_ = s.IsReady()
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -160,13 +148,11 @@ func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
|||||||
const iterations = 100
|
const iterations = 100
|
||||||
|
|
||||||
for range goroutines {
|
for range goroutines {
|
||||||
wg.Add(1)
|
wg.Go(func() {
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
for range iterations {
|
for range iterations {
|
||||||
s.SwapBuffers()
|
s.SwapBuffers()
|
||||||
}
|
}
|
||||||
}()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -210,13 +196,13 @@ func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
|
|||||||
|
|
||||||
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
|
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
|
||||||
s := NewSurfaceState(FormatHex, false)
|
s := NewSurfaceState(FormatHex, false)
|
||||||
buf := s.Redraw()
|
buf, _ := s.Redraw()
|
||||||
assert.Nil(t, buf)
|
assert.Nil(t, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
|
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
|
||||||
s := NewSurfaceState(FormatHex, false)
|
s := NewSurfaceState(FormatHex, false)
|
||||||
buf := s.RedrawScreenOnly()
|
buf, _ := s.RedrawScreenOnly()
|
||||||
assert.Nil(t, buf)
|
assert.Nil(t, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,3 +298,99 @@ func TestBlendColors(t *testing.T) {
|
|||||||
result = blendColors(bg, fg, 2.0)
|
result = blendColors(bg, fg, 2.0)
|
||||||
assert.Equal(t, fg.R, result.R)
|
assert.Equal(t, fg.R, result.R)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newReadyState(t *testing.T, w, h int) *SurfaceState {
|
||||||
|
t.Helper()
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
if err := s.OnScreencopyBuffer(FormatXRGB8888, w, h, w*4+16); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(s.Destroy)
|
||||||
|
data := s.ScreenBuffer().Data()
|
||||||
|
for i := range data {
|
||||||
|
data[i] = byte(i*31 + i/7)
|
||||||
|
}
|
||||||
|
if err := s.OnLayerConfigure(w, h); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s.OnScreencopyReady()
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_RedrawIncremental(t *testing.T) {
|
||||||
|
const w, h = 400, 300
|
||||||
|
s := newReadyState(t, w, h)
|
||||||
|
screen := s.ScreenBuffer()
|
||||||
|
|
||||||
|
reference := func(px, py int, overlay bool) []byte {
|
||||||
|
ref, err := CreateShmBuffer(w, h, screen.Stride)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer ref.Close()
|
||||||
|
ref.CopyFrom(screen)
|
||||||
|
if overlay {
|
||||||
|
c := GetPixelColorWithFormat(screen, px, py, s.screenFormat)
|
||||||
|
drawMagnifierWithInversion(ref.Data(), ref.Stride, w, h, screen.Data(), screen.Stride, w, h, px, py, c, false, s.screenFormat)
|
||||||
|
drawColorPreview(ref.Data(), ref.Stride, w, h, px, py, c, FormatHex, false, s.screenFormat)
|
||||||
|
}
|
||||||
|
return append([]byte(nil), ref.Data()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
inDamage := func(x, y int, damage []rect) bool {
|
||||||
|
for _, r := range damage {
|
||||||
|
if x >= r.x && x < r.x+r.w && y >= r.y && y < r.y+r.h {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var onScreen []byte
|
||||||
|
positions := [][2]int{{10, 10}, {200, 150}, {205, 152}, {399, 299}, {0, 299}, {-1, -1}, {120, 40}, {125, 40}, {300, 290}}
|
||||||
|
for i, pos := range positions {
|
||||||
|
hidden := pos[0] < 0
|
||||||
|
var buf *ShmBuffer
|
||||||
|
var damage []rect
|
||||||
|
px, py := pos[0], pos[1]
|
||||||
|
switch {
|
||||||
|
case hidden:
|
||||||
|
buf, damage = s.RedrawScreenOnly()
|
||||||
|
default:
|
||||||
|
s.OnPointerMotion(float64(px), float64(py))
|
||||||
|
buf, damage = s.Redraw()
|
||||||
|
}
|
||||||
|
if buf == nil {
|
||||||
|
t.Fatalf("step %d: no buffer", i)
|
||||||
|
}
|
||||||
|
want := reference(px, py, !hidden)
|
||||||
|
if !bytesEqual(buf.Data(), want) {
|
||||||
|
t.Fatalf("step %d: incremental frame differs from full render", i)
|
||||||
|
}
|
||||||
|
if onScreen != nil {
|
||||||
|
for y := range h {
|
||||||
|
for x := range w {
|
||||||
|
off := y*buf.Stride + x*4
|
||||||
|
if bytesEqual(want[off:off+4], onScreen[off:off+4]) || inDamage(x, y, damage) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Fatalf("step %d: pixel %d,%d changed outside damage %v", i, x, y, damage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onScreen = want
|
||||||
|
s.SwapBuffers()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesEqual(a, b []byte) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range a {
|
||||||
|
if a[i] != b[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -38,30 +38,15 @@ func (cd *ConfigDeployer) log(message string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeployConfigurations deploys all necessary configurations based on the chosen window manager
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurations(ctx context.Context, wm deps.WindowManager) ([]DeploymentResult, error) {
|
|
||||||
return cd.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeployConfigurationsWithTerminal deploys all necessary configurations based on chosen window manager and terminal
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal) ([]DeploymentResult, error) {
|
|
||||||
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeployConfigurationsWithSystemd deploys configurations with systemd option
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsWithSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) ([]DeploymentResult, error) {
|
|
||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, nil, nil, nil, useSystemd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsSelective(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool) ([]DeploymentResult, error) {
|
|
||||||
return cd.DeployConfigurationsSelectiveWithReinstalls(ctx, wm, terminal, installedDeps, replaceConfigs, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool) ([]DeploymentResult, error) {
|
||||||
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstallsAndSystemd(ctx context.Context, wm deps.WindowManager, terminal deps.Terminal, installedDeps []deps.Dependency, replaceConfigs map[string]bool, reinstallItems map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
|
return cd.deployConfigurationsInternal(ctx, wm, terminal, installedDeps, replaceConfigs, reinstallItems, useSystemd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) deployConfigurationsInternal(_ context.Context, wm deps.WindowManager, terminal deps.Terminal, _ []deps.Dependency, replaceConfigs map[string]bool, _ map[string]bool, useSystemd bool) ([]DeploymentResult, error) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
// Primary config file paths used to detect fresh installs.
|
// Primary config file paths used to detect fresh installs.
|
||||||
@@ -170,6 +155,32 @@ func (cd *ConfigDeployer) deployConfigurationsInternal(ctx context.Context, wm d
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) DeployCompositor(wm deps.WindowManager, terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
|
switch wm {
|
||||||
|
case deps.WindowManagerNiri:
|
||||||
|
return cd.deployNiriConfig(terminal, useSystemd)
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
return cd.deployHyprlandConfig(terminal, useSystemd)
|
||||||
|
case deps.WindowManagerMango:
|
||||||
|
return cd.deployMangoConfig(terminal, useSystemd)
|
||||||
|
default:
|
||||||
|
return DeploymentResult{}, fmt.Errorf("unsupported window manager")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) DeployTerminal(terminal deps.Terminal) ([]DeploymentResult, error) {
|
||||||
|
switch terminal {
|
||||||
|
case deps.TerminalGhostty:
|
||||||
|
return cd.deployGhosttyConfig()
|
||||||
|
case deps.TerminalKitty:
|
||||||
|
return cd.deployKittyConfig()
|
||||||
|
case deps.TerminalAlacritty:
|
||||||
|
return cd.deployAlacrittyConfig()
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported terminal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Niri",
|
ConfigType: "Niri",
|
||||||
@@ -260,6 +271,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
{"layout.kdl", NiriLayoutConfig},
|
{"layout.kdl", NiriLayoutConfig},
|
||||||
{"alttab.kdl", NiriAlttabConfig},
|
{"alttab.kdl", NiriAlttabConfig},
|
||||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
|
{"input.kdl", NiriInputConfig},
|
||||||
{"outputs.kdl", ""},
|
{"outputs.kdl", ""},
|
||||||
{"cursor.kdl", ""},
|
{"cursor.kdl", ""},
|
||||||
{"windowrules.kdl", ""},
|
{"windowrules.kdl", ""},
|
||||||
@@ -281,7 +293,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployMangoConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployMangoConfig(terminal deps.Terminal, _ bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Mango",
|
ConfigType: "Mango",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "mango", "config.conf"),
|
||||||
@@ -567,7 +579,13 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||||
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
var existingOutputs []string
|
||||||
|
for _, output := range outputRegex.FindAllString(existingConfig, -1) {
|
||||||
|
if strings.Contains(NiriConfig, output) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
existingOutputs = append(existingOutputs, output)
|
||||||
|
}
|
||||||
|
|
||||||
if len(existingOutputs) == 0 {
|
if len(existingOutputs) == 0 {
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
@@ -643,9 +661,12 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
if existingData != "" {
|
if existingData != "" {
|
||||||
existingConfig = existingData
|
existingConfig = existingData
|
||||||
cd.log(fmt.Sprintf("Found existing Hyprland configuration at %s", existingPath))
|
cd.log(fmt.Sprintf("Found existing Hyprland configuration at %s", existingPath))
|
||||||
|
}
|
||||||
result.BackupPath = filepath.Join(backupDir, filepath.Base(existingPath))
|
// A legacy hyprland.conf is only backed up once hyprland.lua is in place; Hyprland
|
||||||
if err := backupHyprlandConfigFile(existingPath, result.BackupPath, []byte(existingData), strings.EqualFold(filepath.Ext(existingPath), ".conf")); err != nil {
|
// watches the live config and regenerates a stub one if it disappears first.
|
||||||
|
if existingData != "" && !strings.EqualFold(filepath.Ext(existingPath), ".conf") {
|
||||||
|
result.BackupPath, err = backupHyprlandConfigFile(filepath.Join(backupDir, filepath.Base(existingPath)), []byte(existingData))
|
||||||
|
if err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
result.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -685,11 +706,23 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
movedLegacy, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
|
if useSystemd {
|
||||||
|
if targetPath, err := EnsureHyprlandSessionTarget(); err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: failed to write hyprland-session.target: %v", err))
|
||||||
|
} else {
|
||||||
|
cd.log(fmt.Sprintf("Ensured hyprland session target at %s", targetPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
movedLegacy, mainConfBackup, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Error = fmt.Errorf("failed to back up legacy hyprlang configs: %w", err)
|
result.Error = fmt.Errorf("failed to back up legacy hyprlang configs: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
if mainConfBackup != "" && result.BackupPath == "" {
|
||||||
|
result.BackupPath = mainConfBackup
|
||||||
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainConfBackup))
|
||||||
|
}
|
||||||
if movedLegacy > 0 {
|
if movedLegacy > 0 {
|
||||||
if result.BackupPath == "" {
|
if result.BackupPath == "" {
|
||||||
result.BackupPath = backupDir
|
result.BackupPath = backupDir
|
||||||
@@ -711,42 +744,53 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func backupHyprlandConfigFile(src, dst string, data []byte, removeSource bool) error {
|
func uniqueBackupPath(dst string) string {
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
if _, err := os.Lstat(dst); err != nil {
|
||||||
return err
|
return dst
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
for i := 1; i < 1000; i++ {
|
||||||
return err
|
candidate := fmt.Sprintf("%s.%d", dst, i)
|
||||||
}
|
if _, err := os.Lstat(candidate); err != nil {
|
||||||
if removeSource {
|
return candidate
|
||||||
if err := os.Remove(src); err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return fmt.Sprintf("%s.%d", dst, time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
|
|
||||||
func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, error) {
|
func backupHyprlandConfigFile(dst string, data []byte) (string, error) {
|
||||||
legacyPaths := []string{filepath.Join(configDir, "hyprland.conf")}
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
dst = uniqueBackupPath(dst)
|
||||||
|
if err := os.WriteFile(dst, data, 0o644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dst, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, string, error) {
|
||||||
|
mainConf := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
legacyPaths := []string{mainConf}
|
||||||
dmsConfPaths, err := filepath.Glob(filepath.Join(dmsDir, "*.conf"))
|
dmsConfPaths, err := filepath.Glob(filepath.Join(dmsDir, "*.conf"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
legacyPaths = append(legacyPaths, dmsConfPaths...)
|
legacyPaths = append(legacyPaths, dmsConfPaths...)
|
||||||
backupPaths, err := adjacentHyprlandBackupFiles(configDir, dmsDir)
|
backupPaths, err := adjacentHyprlandBackupFiles(configDir, dmsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, "", err
|
||||||
}
|
}
|
||||||
legacyPaths = append(legacyPaths, backupPaths...)
|
legacyPaths = append(legacyPaths, backupPaths...)
|
||||||
|
|
||||||
moved := 0
|
moved := 0
|
||||||
|
mainConfBackup := ""
|
||||||
for _, src := range legacyPaths {
|
for _, src := range legacyPaths {
|
||||||
info, err := os.Lstat(src)
|
info, err := os.Lstat(src)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return moved, err
|
return moved, mainConfBackup, err
|
||||||
}
|
}
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
continue
|
continue
|
||||||
@@ -756,21 +800,28 @@ func backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir string) (int, er
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
rel = filepath.Base(src)
|
rel = filepath.Base(src)
|
||||||
}
|
}
|
||||||
dst := filepath.Join(backupDir, rel)
|
dst, err := moveHyprlandConfigFile(src, filepath.Join(backupDir, rel))
|
||||||
if err := moveHyprlandConfigFile(src, dst); err != nil {
|
if err != nil {
|
||||||
return moved, err
|
return moved, mainConfBackup, err
|
||||||
|
}
|
||||||
|
if src == mainConf {
|
||||||
|
mainConfBackup = dst
|
||||||
}
|
}
|
||||||
moved++
|
moved++
|
||||||
}
|
}
|
||||||
|
|
||||||
return moved, nil
|
return moved, mainConfBackup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveHyprlandConfigFile(src, dst string) error {
|
func moveHyprlandConfigFile(src, dst string) (string, error) {
|
||||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
return os.Rename(src, dst)
|
dst = uniqueBackupPath(dst)
|
||||||
|
if err := os.Rename(src, dst); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dst, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func adjacentHyprlandBackupFiles(configDir, dmsDir string) ([]string, error) {
|
func adjacentHyprlandBackupFiles(configDir, dmsDir string) ([]string, error) {
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ func TestKittyConfigStructure(t *testing.T) {
|
|||||||
func TestKittyThemeConfigStructure(t *testing.T) {
|
func TestKittyThemeConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
|
assert.Contains(t, KittyThemeConfig, "foreground #e0e2e8")
|
||||||
assert.Contains(t, KittyThemeConfig, "background #101418")
|
assert.Contains(t, KittyThemeConfig, "background #101418")
|
||||||
assert.Contains(t, KittyThemeConfig, "cursor #e0e2e8")
|
assert.Contains(t, KittyThemeConfig, "cursor #9dcbfb")
|
||||||
assert.Contains(t, KittyThemeConfig, "color0 #101418")
|
assert.Contains(t, KittyThemeConfig, "color0 #101418")
|
||||||
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
|
assert.Contains(t, KittyThemeConfig, "color15 #ffffff")
|
||||||
}
|
}
|
||||||
@@ -890,3 +890,72 @@ func TestShouldReplaceConfigDeployIfMissing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBackupLegacyHyprlandConfFilesKeepsExistingBackups(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
require.NoError(t, os.MkdirAll(dmsDir, 0o755))
|
||||||
|
|
||||||
|
userConfig := "# the config the user actually wrote\nmonitor = DP-1, 1920x1080@144, 0x0, 1\n"
|
||||||
|
backupDir := filepath.Join(configDir, hyprlandBackupDirName, "2026-01-02_03-04-05")
|
||||||
|
require.NoError(t, os.MkdirAll(backupDir, 0o755))
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(backupDir, "hyprland.conf"), []byte(userConfig), 0o644))
|
||||||
|
|
||||||
|
autogenerated := "autogenerated = 1\n$mainMod = SUPER\n"
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(configDir, "hyprland.conf"), []byte(autogenerated), 0o644))
|
||||||
|
|
||||||
|
moved, mainConfBackup, err := backupLegacyHyprlandConfFiles(configDir, dmsDir, backupDir)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, moved)
|
||||||
|
assert.Equal(t, filepath.Join(backupDir, "hyprland.conf.1"), mainConfBackup)
|
||||||
|
|
||||||
|
kept, err := os.ReadFile(filepath.Join(backupDir, "hyprland.conf"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, userConfig, string(kept))
|
||||||
|
|
||||||
|
stray, err := os.ReadFile(mainConfBackup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, autogenerated, string(stray))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupHyprlandConfigFileKeepsExistingBackups(t *testing.T) {
|
||||||
|
backupDir := t.TempDir()
|
||||||
|
dst := filepath.Join(backupDir, "hyprland.lua")
|
||||||
|
require.NoError(t, os.WriteFile(dst, []byte("-- first\n"), 0o644))
|
||||||
|
|
||||||
|
written, err := backupHyprlandConfigFile(dst, []byte("-- second\n"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, dst+".1", written)
|
||||||
|
|
||||||
|
first, err := os.ReadFile(dst)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "-- first\n", string(first))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeployHyprlandConfigWritesLuaBeforeRemovingLegacyConf(t *testing.T) {
|
||||||
|
td := t.TempDir()
|
||||||
|
t.Setenv("HOME", td)
|
||||||
|
configDir := filepath.Join(td, ".config", "hypr")
|
||||||
|
require.NoError(t, os.MkdirAll(configDir, 0o755))
|
||||||
|
confPath := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
userConfig := "# user config\nmonitor = DP-1, 1920x1080@144, 0x0, 1\n"
|
||||||
|
require.NoError(t, os.WriteFile(confPath, []byte(userConfig), 0o644))
|
||||||
|
|
||||||
|
logChan := make(chan string, 100)
|
||||||
|
cd := NewConfigDeployer(logChan)
|
||||||
|
go func() {
|
||||||
|
for range logChan {
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer close(logChan)
|
||||||
|
|
||||||
|
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.NoFileExists(t, confPath)
|
||||||
|
backup, err := os.ReadFile(result.BackupPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, userConfig, string(backup))
|
||||||
|
assert.Equal(t, filepath.Join(filepath.Dir(result.BackupPath), "hyprland.conf"), result.BackupPath)
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ mouse-hide-while-typing = true
|
|||||||
copy-on-select = false
|
copy-on-select = false
|
||||||
confirm-close-surface = false
|
confirm-close-surface = false
|
||||||
|
|
||||||
# Disable annoying copied to clipboard
|
# Disable in-app Ghostty toast notifications
|
||||||
app-notifications = no-clipboard-copy,no-config-reload
|
app-notifications = false
|
||||||
|
|
||||||
# Key bindings for common actions
|
# Key bindings for common actions
|
||||||
#keybind = ctrl+c=copy_to_clipboard
|
#keybind = ctrl+c=copy_to_clipboard
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ hl.bind("SUPER + M", hl.dsp.exec_cmd("dms ipc call processlist focusOrToggle"))
|
|||||||
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
hl.bind("SUPER + comma", hl.dsp.exec_cmd("dms ipc call settings focusOrToggle"))
|
||||||
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
hl.bind("SUPER + N", hl.dsp.exec_cmd("dms ipc call notifications toggle"))
|
||||||
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("dms ipc call notepad toggle"))
|
||||||
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dankdash wallpaper"))
|
hl.bind("SUPER + Y", hl.dsp.exec_cmd("dms ipc call dash toggle wallpaper"))
|
||||||
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
hl.bind("SUPER + TAB", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
hl.bind("SUPER + O", hl.dsp.exec_cmd("dms ipc call hypr toggleOverview"))
|
||||||
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
hl.bind("SUPER + X", hl.dsp.exec_cmd("dms ipc call powermenu toggle"))
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ end)
|
|||||||
|
|
||||||
hl.config({
|
hl.config({
|
||||||
input = {
|
input = {
|
||||||
kb_layout = "us",
|
-- empty inherits XKB_DEFAULT_LAYOUT (libxkbcommon), falls back to "us"
|
||||||
|
kb_layout = "",
|
||||||
numlock_by_default = true,
|
numlock_by_default = true,
|
||||||
follow_mouse = 0,
|
follow_mouse = 0,
|
||||||
touchpad = {
|
touchpad = {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
cursor #e0e2e8
|
cursor #9dcbfb
|
||||||
cursor_text_color #c2c7cf
|
cursor_text_color #003354
|
||||||
|
|
||||||
foreground #e0e2e8
|
foreground #e0e2e8
|
||||||
background #101418
|
background #101418
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ bind=SUPER,n,spawn,dms ipc call notifications toggle
|
|||||||
# Notepad
|
# Notepad
|
||||||
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
bind=SUPER+SHIFT,n,spawn,dms ipc call notepad toggle
|
||||||
# Browse Wallpapers
|
# Browse Wallpapers
|
||||||
bind=SUPER,y,spawn,dms ipc call dankdash wallpaper
|
bind=SUPER,y,spawn,dms ipc call dash toggle wallpaper
|
||||||
# Power Menu
|
# Power Menu
|
||||||
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
bind=SUPER,x,spawn,dms ipc call powermenu toggle
|
||||||
# Cycle Display Profile
|
# Cycle Display Profile
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ binds {
|
|||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
Mod+Y hotkey-overlay-title="Browse Wallpapers" {
|
||||||
spawn "dms" "ipc" "call" "dankdash" "wallpaper";
|
spawn "dms" "ipc" "call" "dash" "toggle" "wallpaper";
|
||||||
}
|
}
|
||||||
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
Mod+N hotkey-overlay-title="Notification Center" { spawn "dms" "ipc" "call" "notifications" "toggle"; }
|
||||||
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
Mod+Shift+N hotkey-overlay-title="Notepad" { spawn "dms" "ipc" "call" "notepad" "toggle"; }
|
||||||
@@ -51,24 +51,24 @@ binds {
|
|||||||
XF86AudioMicMute allow-when-locked=true {
|
XF86AudioMicMute allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "audio" "micmute";
|
spawn "dms" "ipc" "call" "audio" "micmute";
|
||||||
}
|
}
|
||||||
XF86AudioPause allow-when-locked=true {
|
XF86AudioPause allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
}
|
}
|
||||||
XF86AudioPlay allow-when-locked=true {
|
XF86AudioPlay allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "playPause";
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
}
|
}
|
||||||
XF86AudioPrev allow-when-locked=true {
|
XF86AudioPrev allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "previous";
|
spawn "dms" "ipc" "call" "mpris" "previous";
|
||||||
}
|
}
|
||||||
XF86AudioNext allow-when-locked=true {
|
XF86AudioNext allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "next";
|
spawn "dms" "ipc" "call" "mpris" "next";
|
||||||
}
|
}
|
||||||
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
||||||
}
|
}
|
||||||
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Brightness Controls ===
|
// === Brightness Controls ===
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
@@ -212,12 +212,12 @@ binds {
|
|||||||
Mod+Shift+Equal { set-window-height "+10%"; }
|
Mod+Shift+Equal { set-window-height "+10%"; }
|
||||||
|
|
||||||
// === Screenshots ===
|
// === Screenshots ===
|
||||||
XF86Launch1 { screenshot; }
|
XF86Launch1 hotkey-overlay-title="Screenshot: Region" { spawn "dms" "screenshot"; }
|
||||||
Ctrl+XF86Launch1 { screenshot-screen; }
|
Ctrl+XF86Launch1 hotkey-overlay-title="Screenshot: Full Screen" { spawn "dms" "screenshot" "full"; }
|
||||||
Alt+XF86Launch1 { screenshot-window; }
|
Alt+XF86Launch1 hotkey-overlay-title="Screenshot: Window" { spawn "dms" "screenshot" "window"; }
|
||||||
Print { screenshot; }
|
Print hotkey-overlay-title="Screenshot: Region" { spawn "dms" "screenshot"; }
|
||||||
Ctrl+Print { screenshot-screen; }
|
Ctrl+Print hotkey-overlay-title="Screenshot: Full Screen" { spawn "dms" "screenshot" "full"; }
|
||||||
Alt+Print { screenshot-window; }
|
Alt+Print hotkey-overlay-title="Screenshot: Window" { spawn "dms" "screenshot" "window"; }
|
||||||
// === Display Profiles ===
|
// === Display Profiles ===
|
||||||
Mod+P hotkey-overlay-title="Cycle Display Profile" {
|
Mod+P hotkey-overlay-title="Cycle Display Profile" {
|
||||||
spawn "dms" "ipc" "outputs" "cycleProfile";
|
spawn "dms" "ipc" "outputs" "cycleProfile";
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
hotkey-overlay {
|
|
||||||
skip-at-startup
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
DMS_RUN_GREETER "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
gestures {
|
|
||||||
hot-corners {
|
|
||||||
off
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layout {
|
|
||||||
background-color "#000000"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
|
input {
|
||||||
|
mouse {
|
||||||
|
accel-speed 0.0
|
||||||
|
}
|
||||||
|
touchpad {
|
||||||
|
tap
|
||||||
|
accel-speed 0.0
|
||||||
|
natural-scroll
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,3 +20,10 @@ window-rule {
|
|||||||
tiled-state true
|
tiled-state true
|
||||||
draw-border-with-background false
|
draw-border-with-background false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layer-rule {
|
||||||
|
exclude namespace="^dms:bar$"
|
||||||
|
background-effect {
|
||||||
|
xray false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user