mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-03 02:52:07 -04:00
Compare commits
989 Commits
marcus/add
...
02a274ebe2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02a274ebe2 | ||
|
|
fc7b61c20b | ||
|
|
5880043f56 | ||
|
|
fee3b7f2a7 | ||
|
|
c0b0339fca | ||
|
|
26c1e62204 | ||
|
|
7b2d4dbe30 | ||
|
|
78c5d46c6b | ||
|
|
3fb85df504 | ||
|
|
227dd24726 | ||
|
|
ae6a656899 | ||
|
|
a4055e0f01 | ||
|
|
6d98c229ef | ||
|
|
71d93ad85e | ||
|
|
4ec21fcd3d | ||
|
|
0a2fe03fee | ||
|
|
4f4745609b | ||
|
|
a69cd515fb | ||
|
|
06c4b97a6b | ||
|
|
a6cf71a190 | ||
|
|
21750156dc | ||
|
|
f9b737f543 | ||
|
|
246b59f3b9 | ||
|
|
dcda81ea64 | ||
|
|
9909b665cd | ||
|
|
4bcd786be3 | ||
|
|
64c9222000 | ||
|
|
12acf2dd51 | ||
|
|
fea97b4aad | ||
|
|
c6d398eeac | ||
|
|
7a74be83d7 | ||
|
|
67a6427418 | ||
|
|
18b20d3225 | ||
|
|
8a76885fb6 | ||
|
|
69b1e61ab7 | ||
|
|
3f24cf37ca | ||
|
|
01218f34cb | ||
|
|
9da58d8296 | ||
|
|
af0038e634 | ||
|
|
05c312b9eb | ||
|
|
89d5c958c4 | ||
|
|
e4d86ad595 | ||
|
|
532b54a028 | ||
|
|
504d027c3f | ||
|
|
e8f95f4533 | ||
|
|
b83256c83a | ||
|
|
8e2cd21be8 | ||
|
|
c5413608da | ||
|
|
586bcad442 | ||
|
|
3b3d10f730 | ||
|
|
4834891b36 | ||
|
|
f60e65aecb | ||
|
|
01387b0123 | ||
|
|
1476658c23 | ||
|
|
7861c6e316 | ||
|
|
d2247d7b24 | ||
|
|
2ff78d4a02 | ||
|
|
785243ce5f | ||
|
|
0e1b868384 | ||
|
|
2b08e800e8 | ||
|
|
74e4f8ea1e | ||
|
|
9c58569b4c | ||
|
|
29de677e00 | ||
|
|
fae4944845 | ||
|
|
07a0ac4b7d | ||
|
|
b2d8f4d73b | ||
|
|
fe58c45233 | ||
|
|
3ea4e389eb | ||
|
|
7276f295fc | ||
|
|
93ed96a789 | ||
|
|
bea325e94c | ||
|
|
2f8f1c30ad | ||
|
|
f859a14173 | ||
|
|
153f39da48 | ||
|
|
e4accdd1c7 | ||
|
|
a2c89e0a8c | ||
|
|
e282831c2e | ||
|
|
5c5ff6195a | ||
|
|
c4bbf54679 | ||
|
|
98acafb4b8 | ||
|
|
da20681fc0 | ||
|
|
b38cb961b2 | ||
|
|
7a0bb07518 | ||
|
|
403e3e90a2 | ||
|
|
50b91f14b6 | ||
|
|
b3df47fce0 | ||
|
|
09bd65d746 | ||
|
|
020d56ab7f | ||
|
|
f3bee65da9 | ||
|
|
b14b0946e2 | ||
|
|
ca44205f1c | ||
|
|
2d39e8fd2a | ||
|
|
6d4df6e927 | ||
|
|
b8ab86e6c0 | ||
|
|
837329a6d8 | ||
|
|
8c6c2ffd23 | ||
|
|
ad3c8b6755 | ||
|
|
03a8e1e0d5 | ||
|
|
4d4d3c20a1 | ||
|
|
cef16d6bc9 | ||
|
|
aafaad1791 | ||
|
|
7906fdc2b0 | ||
|
|
397650ca52 | ||
|
|
826207006a | ||
|
|
58c2fcd31c | ||
|
|
b2a2b425ec | ||
|
|
942c9c9609 | ||
|
|
46d6e1cff3 | ||
|
|
a4137c57c1 | ||
|
|
1ad8b627f1 | ||
|
|
58a02ce290 | ||
|
|
8e1ad1a2be | ||
|
|
68cd7ab32c | ||
|
|
f649ce9a8e | ||
|
|
c4df242f07 | ||
|
|
26846c8d55 | ||
|
|
31b44a667c | ||
|
|
4f3b73ee21 | ||
|
|
4cfae91f02 | ||
|
|
8d947a6e95 | ||
|
|
1e84d4252c | ||
|
|
76072e1d4c | ||
|
|
6408dce4a9 | ||
|
|
0b2e1cca38 | ||
|
|
c1bfd8c0b7 | ||
|
|
90ffa5833b | ||
|
|
169c669286 | ||
|
|
f8350deafc | ||
|
|
0286a1b80b | ||
|
|
7c3e6c1f02 | ||
|
|
d2d72db3c9 | ||
|
|
f81f861408 | ||
|
|
af494543f5 | ||
|
|
db4de55338 | ||
|
|
37ecbbbbde | ||
|
|
d6a6d2a438 | ||
|
|
bf1c6eec74 | ||
|
|
0ddae80584 | ||
|
|
5c96c03bfa | ||
|
|
dfe36e47d8 | ||
|
|
63e1b75e57 | ||
|
|
29efdd8598 | ||
|
|
34d03cf11b | ||
|
|
c339389d44 | ||
|
|
af5f6eb656 | ||
|
|
a6d28e2553 | ||
|
|
6213267908 | ||
|
|
d084114149 | ||
|
|
f6d99eca0d | ||
|
|
722eb3289e | ||
|
|
b7f2bdcb2d | ||
|
|
11c20db6e6 | ||
|
|
8a4e3f8bb1 | ||
|
|
bc8fe97c13 | ||
|
|
47262155aa | ||
|
|
dd4c41a6b2 | ||
|
|
92a25fdb6a | ||
|
|
d6650be008 | ||
|
|
2646e7b19a | ||
|
|
4133f11d82 | ||
|
|
22ed740394 | ||
|
|
063299a434 | ||
|
|
44d836c975 | ||
|
|
da437e77fb | ||
|
|
34a6bbfb32 | ||
|
|
9ed53bac9e | ||
|
|
3a6752c3d2 | ||
|
|
ef19568dd7 | ||
|
|
f280cd9d3b | ||
|
|
cf4ce3c476 | ||
|
|
ae59e53c4c | ||
|
|
7e0d661f63 | ||
|
|
0b33d3f905 | ||
|
|
d62bdda56b | ||
|
|
5841b38cd9 | ||
|
|
83e2b5a7a6 | ||
|
|
2f863f64ee | ||
|
|
1a8b397cfd | ||
|
|
196c421b75 | ||
|
|
8399d64c2d | ||
|
|
c530eab303 | ||
|
|
45b6362dd3 | ||
|
|
50b77dcfc3 | ||
|
|
be8f3adf01 | ||
|
|
75a8c171ea | ||
|
|
466ff59573 | ||
|
|
053bb91927 | ||
|
|
2c9b22c016 | ||
|
|
a9ee91586e | ||
|
|
81bce74612 | ||
|
|
f2a6d2c7da | ||
|
|
0a9a34912e | ||
|
|
abff670814 | ||
|
|
0d49acaaa8 | ||
|
|
ebe1785411 | ||
|
|
f9f0192b22 | ||
|
|
e5cdbf4cf5 | ||
|
|
13ef1efa7b | ||
|
|
fbd9301a2d | ||
|
|
24e3024b57 | ||
|
|
52d5af11ba | ||
|
|
44a45b00cf | ||
|
|
2b78fe5b9f | ||
|
|
14f92669c6 | ||
|
|
124106de87 | ||
|
|
bb8e0d384f | ||
|
|
59d37847ec | ||
|
|
acdc531dca | ||
|
|
ce75dac81b | ||
|
|
b8d40761ff | ||
|
|
3a7430f6da | ||
|
|
242660c51d | ||
|
|
8a6c1e45ce | ||
|
|
b8e5f9f3b1 | ||
|
|
d60e70f9cc | ||
|
|
cdb70fadb3 | ||
|
|
7867deef60 | ||
|
|
a77c1adb32 | ||
|
|
da14d75a3b | ||
|
|
7c66a34931 | ||
|
|
425715e0f0 | ||
|
|
a3baf8ce31 | ||
|
|
605e03b065 | ||
|
|
0e9b21d359 | ||
|
|
ba5bf0cabc | ||
|
|
96b9d7aab3 | ||
|
|
750e4c4527 | ||
|
|
7417e26444 | ||
|
|
00e1099912 | ||
|
|
bd46d29ff0 | ||
|
|
1a9d7684b9 | ||
|
|
0133c19276 | ||
|
|
46bb3b613b | ||
|
|
5839a5de30 | ||
|
|
535d0bb0f0 | ||
|
|
4d316007af | ||
|
|
3c2d60d8e1 | ||
|
|
9c4f4cbd0d | ||
|
|
a337585b00 | ||
|
|
1cdec5d687 | ||
|
|
081b15e24c | ||
|
|
b04cb7b3cc | ||
|
|
e2c3ff00fb | ||
|
|
c783ff3dcf | ||
|
|
2c360dc3e8 | ||
|
|
5342647bfb | ||
|
|
46a2f6f0d8 | ||
|
|
f8af8fc171 | ||
|
|
3d0ee9d72b | ||
|
|
5a0bb260b4 | ||
|
|
9a7f1f5f2f | ||
|
|
d88b04fc30 | ||
|
|
6fe4cc98b9 | ||
|
|
b9bcfd8d2c | ||
|
|
e3bd31bb52 | ||
|
|
0922e3e459 | ||
|
|
a168b12bb2 | ||
|
|
8c01deba86 | ||
|
|
b645487e79 | ||
|
|
91569affd7 | ||
|
|
1ed44ee6f3 | ||
|
|
fce120fa31 | ||
|
|
a02b0c0c3c | ||
|
|
c86999f389 | ||
|
|
2b546967d2 | ||
|
|
591d2ba4d4 | ||
|
|
37cc4ab197 | ||
|
|
d775974a90 | ||
|
|
cc62aa4a9e | ||
|
|
5b8b7b04be | ||
|
|
b4a8853591 | ||
|
|
4220dfe2a5 | ||
|
|
4557426c28 | ||
|
|
8ee7fe8e66 | ||
|
|
c4a41f994a | ||
|
|
fa639424f5 | ||
|
|
e618a8390c | ||
|
|
393e9ed2e4 | ||
|
|
e1ea441215 | ||
|
|
654661fd66 | ||
|
|
c5a21f8da0 | ||
|
|
ca5b168117 | ||
|
|
aa88eb42ee | ||
|
|
ac84cadd77 | ||
|
|
81d5235b9f | ||
|
|
8944762c76 | ||
|
|
3d05c34673 | ||
|
|
c2ee41c844 | ||
|
|
6b537f30a5 | ||
|
|
a3ae95df09 | ||
|
|
4349d68f87 | ||
|
|
7d5c20125a | ||
|
|
2583dbd3f2 | ||
|
|
a103b93583 | ||
|
|
fff018eafb | ||
|
|
60b824e7a4 | ||
|
|
e27e904157 | ||
|
|
fe15667986 | ||
|
|
bd9029e533 | ||
|
|
fa71d563ea | ||
|
|
143918bc5e | ||
|
|
961680af8c | ||
|
|
6e3b3ce888 | ||
|
|
44292c3b55 | ||
|
|
c024c1b8e4 | ||
|
|
13adfdec11 | ||
|
|
e9ec28aab7 | ||
|
|
0af4d1d6e3 | ||
|
|
3ef0e63533 | ||
|
|
f4dad69ccd | ||
|
|
b811316d0c | ||
|
|
f59aeb2782 | ||
|
|
24ce41935e | ||
|
|
3c4749ead0 | ||
|
|
22ab5b9660 | ||
|
|
22f16f1da3 | ||
|
|
a97409dfd7 | ||
|
|
eaa6a664c8 | ||
|
|
d934b3b3b4 | ||
|
|
546cbfb3ca | ||
|
|
39b70a53a0 | ||
|
|
795f84adce | ||
|
|
3d80a9dd0f | ||
|
|
9669e9bc87 | ||
|
|
5f2a5a5d7d | ||
|
|
ecfd721fc0 | ||
|
|
07242a00b3 | ||
|
|
4602442feb | ||
|
|
a90717b20c | ||
|
|
02edce2999 | ||
|
|
f2d9066f90 | ||
|
|
f6f7b1ed72 | ||
|
|
803bc1cb7f | ||
|
|
67d3aa9da3 | ||
|
|
9fbff5e833 | ||
|
|
c371140a97 | ||
|
|
c755a3719d | ||
|
|
4f153f3026 | ||
|
|
f2b1dbd256 | ||
|
|
be0ca993ff | ||
|
|
ed87e1b00b | ||
|
|
ac509933d7 | ||
|
|
f49f98ff85 | ||
|
|
10923346d7 | ||
|
|
f27bffc387 | ||
|
|
36b43f93a3 | ||
|
|
2deeab9d08 | ||
|
|
f00854879c | ||
|
|
75fd62865b | ||
|
|
757054e140 | ||
|
|
eda59b348c | ||
|
|
d19e81ffac | ||
|
|
60c6872aec | ||
|
|
a9cb2fe912 | ||
|
|
a168a8160c | ||
|
|
78662f9613 | ||
|
|
d9d7bb8dcc | ||
|
|
3136f48b30 | ||
|
|
0c46711b01 | ||
|
|
68159b5c41 | ||
|
|
6557d66f94 | ||
|
|
9553cb06d3 | ||
|
|
122fb16dfb | ||
|
|
511502220f | ||
|
|
8bfe7439c0 | ||
|
|
8499033221 | ||
|
|
705d5b04dd | ||
|
|
17eaa761f8 | ||
|
|
1cdbd01748 | ||
|
|
08cc076a4c | ||
|
|
2a02d5594c | ||
|
|
2263338878 | ||
|
|
26bc5425d3 | ||
|
|
38b4d1dc95 | ||
|
|
3aaca7ff39 | ||
|
|
83d9808536 | ||
|
|
ad458dfece | ||
|
|
8f6fe7ed2b | ||
|
|
419a692593 | ||
|
|
03fdf795e0 | ||
|
|
832807a217 | ||
|
|
f7df3b2a68 | ||
|
|
0d03e73595 | ||
|
|
c5ae1a77d3 | ||
|
|
5f16624000 | ||
|
|
80025804ab | ||
|
|
028d3b4e61 | ||
|
|
9cce5ccfe6 | ||
|
|
a260b8060e | ||
|
|
f945307232 | ||
|
|
8f44d52cb2 | ||
|
|
3413cb7b89 | ||
|
|
4e3b24ffbb | ||
|
|
03cfa55e0b | ||
|
|
a887e60f40 | ||
|
|
816819bf9f | ||
|
|
78f3bb3812 | ||
|
|
01d7ed5dd8 | ||
|
|
50311db280 | ||
|
|
01b1a276c5 | ||
|
|
6d4c31492c | ||
|
|
f8c5f07e9f | ||
|
|
11e23feb0e | ||
|
|
b4ba2dac37 | ||
|
|
d013c3b718 | ||
|
|
b3ea28c5c4 | ||
|
|
775b381987 | ||
|
|
3a41f2f1ed | ||
|
|
972fc534a4 | ||
|
|
808ee66e11 | ||
|
|
3936a516f8 | ||
|
|
15dc91f779 | ||
|
|
dd3d2908a2 | ||
|
|
0857023dba | ||
|
|
1edc8f468e | ||
|
|
2681fe87bb | ||
|
|
3f0d0f4d95 | ||
|
|
f24ecf1b99 | ||
|
|
acdd1d2ec4 | ||
|
|
d08496f237 | ||
|
|
27b4e0221b | ||
|
|
496ace0cd4 | ||
|
|
f61ed8b8a6 | ||
|
|
41ee88a3cf | ||
|
|
6bf1438ef1 | ||
|
|
b819306ab6 | ||
|
|
b140afca8e | ||
|
|
6735989455 | ||
|
|
db37ac24c7 | ||
|
|
0231270f9e | ||
|
|
b5194aa9e1 | ||
|
|
ea0ffaacb0 | ||
|
|
3b1f084a13 | ||
|
|
39a9e3a89f | ||
|
|
7a7af775c2 | ||
|
|
6ac2a305f7 | ||
|
|
3507c6cec3 | ||
|
|
3ff00768ac | ||
|
|
556d253ea8 | ||
|
|
3922070488 | ||
|
|
eebb4827c4 | ||
|
|
fd2c6a0784 | ||
|
|
417bf37515 | ||
|
|
132e799265 | ||
|
|
bdc864781b | ||
|
|
a343bc7562 | ||
|
|
1f2e231386 | ||
|
|
0e7f628c4a | ||
|
|
553f5257b3 | ||
|
|
80ce6aa19c | ||
|
|
2b2977de4a | ||
|
|
1d5d876e16 | ||
|
|
3c39162016 | ||
|
|
d38767fb5a | ||
|
|
f2be6cfeb1 | ||
|
|
65486ed3cf | ||
|
|
cc30e2a9e4 | ||
|
|
ac68451cdf | ||
|
|
0f6ae11c3d | ||
|
|
7cb39f00ad | ||
|
|
f313d03348 | ||
|
|
1adbf3937b | ||
|
|
a685d9da52 | ||
|
|
13dededcc9 | ||
|
|
3bed2d9feb | ||
|
|
7241877995 | ||
|
|
340d79000c | ||
|
|
162ec909da | ||
|
|
53f5240d41 | ||
|
|
27f0df07af | ||
|
|
ad940b5884 | ||
|
|
ec8ab47462 | ||
|
|
35cbfeb008 | ||
|
|
7036362b9b | ||
|
|
2bcb33e85c | ||
|
|
76ac036f85 | ||
|
|
581073394a | ||
|
|
d7b7086b21 | ||
|
|
59be179821 | ||
|
|
1cf2f6b946 | ||
|
|
a57a9c2121 | ||
|
|
67568c3746 | ||
|
|
afce792b80 | ||
|
|
f5c7493dbb | ||
|
|
f9b9d98638 | ||
|
|
2a97e03fa6 | ||
|
|
d6dacc2975 | ||
|
|
aab4b6765d | ||
|
|
3539aca1f7 | ||
|
|
81fbe9eaba | ||
|
|
f9dc6de485 | ||
|
|
012022d370 | ||
|
|
993216e157 | ||
|
|
c992f2b582 | ||
|
|
3243adebca | ||
|
|
baccef57d4 | ||
|
|
a823095372 | ||
|
|
172a743de4 | ||
|
|
623eec3689 | ||
|
|
53a033fe35 | ||
|
|
c490ee24f4 | ||
|
|
cc1e49294e | ||
|
|
e6fa46ae26 | ||
|
|
35fe774a1b | ||
|
|
1e6a0f9423 | ||
|
|
cc1877aadb | ||
|
|
f1eb1fa9ba | ||
|
|
bdd01e335d | ||
|
|
4b7baf82cd | ||
|
|
15c88ce1d2 | ||
|
|
8891c388d0 | ||
|
|
cd9d92d884 | ||
|
|
1b69a5e62b | ||
|
|
61d311b157 | ||
|
|
6b76b86930 | ||
|
|
dcfb947c36 | ||
|
|
59893b7f44 | ||
|
|
d2c62f5533 | ||
|
|
2bbe9a0c45 | ||
|
|
4e2ce82c0a | ||
|
|
104762186f | ||
|
|
f1233ab1e3 | ||
|
|
d6b407ec37 | ||
|
|
022b4b4bb3 | ||
|
|
49b322582d | ||
|
|
1280bd047d | ||
|
|
6f206d7523 | ||
|
|
2e58283859 | ||
|
|
99a5721fe8 | ||
|
|
5302ebd840 | ||
|
|
fa427ea1ac | ||
|
|
7027bd1646 | ||
|
|
3c38e17472 | ||
|
|
510ea5d2e4 | ||
|
|
bb2234d328 | ||
|
|
edbdeb0fb8 | ||
|
|
19541fc573 | ||
|
|
7c936cacfb | ||
|
|
c60cd3a341 | ||
|
|
e37135f80d | ||
|
|
aac937cbcc | ||
|
|
4b46d022af | ||
|
|
7f0181b310 | ||
|
|
6a109274f8 | ||
|
|
0f09cc693a | ||
|
|
af0166a553 | ||
|
|
a283017f26 | ||
|
|
5ae2cd1dfb | ||
|
|
eece811fb0 | ||
|
|
1ff1f3a7f2 | ||
|
|
a21a846bf5 | ||
|
|
f5f21e738a | ||
|
|
033e62418a | ||
|
|
3c69e8b1cc | ||
|
|
118be27796 | ||
|
|
721d35d417 | ||
|
|
7bc3d5910d | ||
|
|
ccc7047be0 | ||
|
|
a5e107c89d | ||
|
|
646d60dcbf | ||
|
|
5dc7c0d797 | ||
|
|
db1de9df38 | ||
|
|
3dd21382ba | ||
|
|
ec2b3d0d4b | ||
|
|
a205df1bd6 | ||
|
|
e822fa73da | ||
|
|
634e75b80c | ||
|
|
ec5b507efc | ||
|
|
e6d289d48c | ||
|
|
745d7f26ce | ||
|
|
ad43053b94 | ||
|
|
721700190b | ||
|
|
8c9c936d0e | ||
|
|
842bf6e3ff | ||
|
|
c1fbeb3f5e | ||
|
|
c45eb2cccf | ||
|
|
1b5abca83a | ||
|
|
45818b202f | ||
|
|
1c8ce46f25 | ||
|
|
f762f9ae49 | ||
|
|
4484f6bd61 | ||
|
|
0076c45496 | ||
|
|
ab071e12aa | ||
|
|
8386b40c50 | ||
|
|
03a985228d | ||
|
|
ef7d7ec13d | ||
|
|
824792cca7 | ||
|
|
850e5b6572 | ||
|
|
64310854a6 | ||
|
|
4005a55bf2 | ||
|
|
0236fe3276 | ||
|
|
c1d95a3086 | ||
|
|
9b027df1d5 | ||
|
|
5e03afe7f0 | ||
|
|
145a974b6d | ||
|
|
d23fc9f2df | ||
|
|
7ac5191e8d | ||
|
|
29d27ebd6d | ||
|
|
e45075dd84 | ||
|
|
80bc87e76b | ||
|
|
76d88517ec | ||
|
|
151d695212 | ||
|
|
2e1bed5fb5 | ||
|
|
f163b97c17 | ||
|
|
436c99927e | ||
|
|
aa72eacae7 | ||
|
|
913bb2ff67 | ||
|
|
3bb2696263 | ||
|
|
166843ded4 | ||
|
|
02166a4ca5 | ||
|
|
f0f2e6ef72 | ||
|
|
8d8d5de5fd | ||
|
|
6d76f0b476 | ||
|
|
f3f720bb37 | ||
|
|
2bf85bc4dd | ||
|
|
faddc46185 | ||
|
|
2991aac82e | ||
|
|
e1817027b1 | ||
|
|
ba2d51bcbb | ||
|
|
7f10d6a9b8 | ||
|
|
405749aa98 | ||
|
|
77681fd387 | ||
|
|
8253ec4496 | ||
|
|
a1e001e640 | ||
|
|
3a65ea21ba | ||
|
|
7d761c4c9a | ||
|
|
4cb90c5367 | ||
|
|
1c7d15db0b | ||
|
|
7268a3fe7f | ||
|
|
d2c4391514 | ||
|
|
69b1d0c2da | ||
|
|
ba28767492 | ||
|
|
6cff5f1146 | ||
|
|
3e1c6534bd | ||
|
|
c1d57946d9 | ||
|
|
5e111d89a5 | ||
|
|
1a98da22b2 | ||
|
|
618ccbcb2f | ||
|
|
d3a79a055e | ||
|
|
bae32e51ff | ||
|
|
edfda965e9 | ||
|
|
a547966b23 | ||
|
|
f6279b1b2e | ||
|
|
957c89a85d | ||
|
|
571a9dabcd | ||
|
|
51ca9a7686 | ||
|
|
c141ad1e34 | ||
|
|
37f972d075 | ||
|
|
7d8de6e6f0 | ||
|
|
7ff751f8a2 | ||
|
|
651672afe2 | ||
|
|
2dbadfe1b5 | ||
|
|
621710bd86 | ||
|
|
1edecb05bb | ||
|
|
f1a876301b | ||
|
|
97a07c399a | ||
|
|
18f095cb23 | ||
|
|
d95d516d64 | ||
|
|
45ba64ab02 | ||
|
|
9501d66af6 | ||
|
|
2127fc339a | ||
|
|
7962fee0bd | ||
|
|
d5c7b5c0cc | ||
|
|
5f77d69dd8 | ||
|
|
60034be06a | ||
|
|
518a5d38aa | ||
|
|
2eeaf8ff62 | ||
|
|
cffee0fae6 | ||
|
|
f08e2ef5b8 | ||
|
|
2b0070c31a | ||
|
|
ae82716afa | ||
|
|
c281bf3b53 | ||
|
|
45b8b2a895 | ||
|
|
7b9ba840fb | ||
|
|
84fb567ff5 | ||
|
|
bbd57e0673 | ||
|
|
b1632a0a03 | ||
|
|
7aff1182c8 | ||
|
|
fbe362cd20 | ||
|
|
e7f94c94cc | ||
|
|
7523190b16 | ||
|
|
da37e16b6e | ||
|
|
5c420ab50b | ||
|
|
4493b7c231 | ||
|
|
40a96c6eaf | ||
|
|
23a93082c6 | ||
|
|
f7650b5e1f | ||
|
|
3ebdd5631c | ||
|
|
6c4caf121a | ||
|
|
89788e9ca7 | ||
|
|
0787c63fed | ||
|
|
9fc0d5efff | ||
|
|
6611dfbe05 | ||
|
|
8a71ead51d | ||
|
|
d9d6ab5776 | ||
|
|
d6fe7bea27 | ||
|
|
1194f3ffb8 | ||
|
|
5ac81e6dd6 | ||
|
|
987856a1de | ||
|
|
ef52ce0990 | ||
|
|
06b14a5869 | ||
|
|
fd839059c0 | ||
|
|
ec6db7962a | ||
|
|
adf92cbc46 | ||
|
|
6b6f51cd1f | ||
|
|
df6c60213f | ||
|
|
6303304a10 | ||
|
|
8e76789119 | ||
|
|
10e81cfdd3 | ||
|
|
03fd3a4f16 | ||
|
|
8fdc748ed2 | ||
|
|
6c56d23b93 | ||
|
|
45d34dcb5b | ||
|
|
d7ac0d50fa | ||
|
|
1d4d145187 | ||
|
|
a5b9ff98c0 | ||
|
|
6feaecd92e | ||
|
|
b066a25308 | ||
|
|
777a552b57 | ||
|
|
7dbe608c28 | ||
|
|
61630e447b | ||
|
|
91385e7c83 | ||
|
|
04648fcca7 | ||
|
|
080fc7e44e | ||
|
|
0b60da3d6d | ||
|
|
a4492b90e7 | ||
|
|
c9331b7338 | ||
|
|
4982ea53dd | ||
|
|
c703cb6504 | ||
|
|
a7494971fd | ||
|
|
c548255bfc | ||
|
|
9656c7afd7 | ||
|
|
414b8c8272 | ||
|
|
b4f83d09d4 | ||
|
|
67ee74ac20 | ||
|
|
93539d2b6b | ||
|
|
524d967745 | ||
|
|
0effbebbb6 | ||
|
|
dca07a70f8 | ||
|
|
02936c97fd | ||
|
|
8f7e732827 | ||
|
|
5ffe563b7d | ||
|
|
6ef08c3d54 | ||
|
|
908b4b58cd | ||
|
|
f2611e0de0 | ||
|
|
ea75a9d351 | ||
|
|
3a744d7d68 | ||
|
|
195d312ae2 | ||
|
|
76006a7377 | ||
|
|
11536da512 | ||
|
|
2a91bc41f7 | ||
|
|
baf23157fc | ||
|
|
83b81be825 | ||
|
|
4aefa0f1f7 | ||
|
|
e53a7cee97 | ||
|
|
8437e1aa7b | ||
|
|
632f40cc0a | ||
|
|
7d81445341 | ||
|
|
78a5f401d7 | ||
|
|
8745f98c95 | ||
|
|
f0f5bcc630 | ||
|
|
8a3c513605 | ||
|
|
145d2636dd | ||
|
|
f2b9dc8988 | ||
|
|
2e4d56728b | ||
|
|
18231ed324 | ||
|
|
d0b61d8ed1 | ||
|
|
d385a44949 | ||
|
|
d97392d46e | ||
|
|
6abb2c73fd | ||
|
|
7e141c6b36 | ||
|
|
53553c1f62 | ||
|
|
523ccc6bf8 | ||
|
|
811e89fcfa | ||
|
|
5d5be4d9d7 | ||
|
|
88457ab139 | ||
|
|
0034926df7 | ||
|
|
d082d41ab9 | ||
|
|
b7911475b6 | ||
|
|
672754b0b5 | ||
|
|
0d1553123b | ||
|
|
ba6c51c102 | ||
|
|
d64206a9ff | ||
|
|
d9a1089039 | ||
|
|
55fe463405 | ||
|
|
e84210e962 | ||
|
|
ff506548d3 | ||
|
|
f6b09751e9 | ||
|
|
3d863979c4 | ||
|
|
2947ff4131 | ||
|
|
b8fca10896 | ||
|
|
33e45794d2 | ||
|
|
42cc88ca65 | ||
|
|
0b7f2416ca | ||
|
|
5d5c745ee5 | ||
|
|
e0429e4c60 | ||
|
|
0bece5287e | ||
|
|
60b5e47836 | ||
|
|
aa75b44790 | ||
|
|
769f58caa9 | ||
|
|
e7facf740d | ||
|
|
04921eef62 | ||
|
|
8863c42879 | ||
|
|
2745116ac5 | ||
|
|
bafe1c5fee | ||
|
|
306d7b2ce0 | ||
|
|
e9f6583c60 | ||
|
|
42a2835929 | ||
|
|
c2c90c680e | ||
|
|
cd01f6378c | ||
|
|
6033075de6 | ||
|
|
79794d3441 | ||
|
|
031f86b417 | ||
|
|
891f53cf6f | ||
|
|
848991cf5b | ||
|
|
d37ddd1d41 | ||
|
|
00d12acd5e | ||
|
|
3bbc78a44f | ||
|
|
b0a6652cc6 | ||
|
|
cb710b2e5f | ||
|
|
ca5fe6f7db | ||
|
|
fb75f4c68b | ||
|
|
5e2a418485 | ||
|
|
24fe215067 | ||
|
|
ab2e8875ac | ||
|
|
dec5740c74 | ||
|
|
208266dfa3 | ||
|
|
32f218d58c | ||
|
|
6fdaab2ccd | ||
|
|
d336866f44 | ||
|
|
b40df5f1c4 | ||
|
|
3c9886ad1b | ||
|
|
ea205ebd12 | ||
|
|
30dad46c94 | ||
|
|
fbf79e62e9 | ||
|
|
efcf72bc08 | ||
|
|
3b511e2f55 | ||
|
|
e4e20fb43a | ||
|
|
48ccff67a6 | ||
|
|
a783d6507b | ||
|
|
fd94e60797 | ||
|
|
a1bcb7ea30 | ||
|
|
31b67164c7 | ||
|
|
786c13f892 | ||
|
|
c652659d54 | ||
|
|
ca39196f13 | ||
|
|
f02dd8fd4b | ||
|
|
0f89886ce7 | ||
|
|
1118404192 | ||
|
|
f011ea6cce | ||
|
|
b2ac9c6c1a | ||
|
|
fbab41abd6 | ||
|
|
82f881af5b | ||
|
|
68de9b437d | ||
|
|
830a715b6d | ||
|
|
ce4aca9a72 | ||
|
|
7641171a01 | ||
|
|
119e084e52 | ||
|
|
7c6d52913e | ||
|
|
f63ab5cf7c | ||
|
|
50f1bc5017 | ||
|
|
c3ab409b6a | ||
|
|
44f6ab4878 | ||
|
|
5fda6e0f12 | ||
|
|
38068e78c9 | ||
|
|
66d22727e9 | ||
|
|
db2f68e35d | ||
|
|
352277ec15 | ||
|
|
d6043e64f2 | ||
|
|
d3f5b8f32e | ||
|
|
6c3c722674 | ||
|
|
5b8edb13d8 | ||
|
|
c595727b94 | ||
|
|
d46302588a | ||
|
|
0ff9fdb365 | ||
|
|
e95f7ce367 | ||
|
|
df1a8f4066 | ||
|
|
32e6c1660f | ||
|
|
d6b9b72e9b | ||
|
|
179ad03fa4 | ||
|
|
c3cb82c84e | ||
|
|
4b52e2ed9e | ||
|
|
77fd61f81e | ||
|
|
c3ffb7f83b | ||
|
|
89dcd72d70 | ||
|
|
5c3346aa9d | ||
|
|
7c4b383477 | ||
|
|
bdc0e8e0fc | ||
|
|
6d66f93565 | ||
|
|
9cac93b724 | ||
|
|
0709f263af | ||
|
|
4e4effd8b1 | ||
|
|
f9632cba61 | ||
|
|
38db6a41d5 | ||
|
|
7c6f0432c8 | ||
|
|
56ff9368be | ||
|
|
597e21d44d | ||
|
|
5bf54632be | ||
|
|
3a8d3ee515 | ||
|
|
1c1cf866e2 | ||
|
|
ccc1df75f1 | ||
|
|
d2c3f87656 | ||
|
|
6d62229b5f | ||
|
|
7c88865d67 | ||
|
|
c8cfe0cb5a | ||
|
|
e573bdba92 | ||
|
|
d8cd15d361 | ||
|
|
1db3907838 | ||
|
|
72cfd37ab7 | ||
|
|
1e67ee995e | ||
|
|
6c26b4080c | ||
|
|
0dbd59b223 | ||
|
|
b2066c60d1 | ||
|
|
8d7ae324ff | ||
|
|
c0d3c4f875 | ||
|
|
27a771648a | ||
|
|
86affc7304 | ||
|
|
d939b99628 | ||
|
|
1fcf777f3d | ||
|
|
7a8e23faa9 | ||
|
|
73a4dd3321 | ||
|
|
13ce873a69 | ||
|
|
406dc64aba | ||
|
|
af5d6a2015 | ||
|
|
61c6f509ae | ||
|
|
98769ecd88 | ||
|
|
8615950bd6 | ||
|
|
1bec8dfc48 | ||
|
|
460486fe25 | ||
|
|
318c50bc6c | ||
|
|
3e08bac7f3 | ||
|
|
c3d64ab185 | ||
|
|
2b73077b50 | ||
|
|
f953bd5488 | ||
|
|
f94011cf05 | ||
|
|
aeacf109eb | ||
|
|
e307de83e2 | ||
|
|
85968ec417 | ||
|
|
993f14a31f | ||
|
|
566d617508 | ||
|
|
542a279fcb | ||
|
|
e784bb89e1 | ||
|
|
f680ace258 | ||
|
|
7aa5976e07 | ||
|
|
f88f1ea951 | ||
|
|
da4561cb35 | ||
|
|
1f89ae9813 | ||
|
|
5647323449 | ||
|
|
bc27253cbf | ||
|
|
0672b711f3 | ||
|
|
ed9ee6e347 | ||
|
|
7ad23ad4a2 | ||
|
|
8a83f03cc1 | ||
|
|
0be9ac4097 | ||
|
|
ba5be6b516 | ||
|
|
c4aea6d326 | ||
|
|
858c6407a9 | ||
|
|
c4313395b5 | ||
|
|
a32aec3d59 | ||
|
|
696bcfe8fa | ||
|
|
2f3a253c6a | ||
|
|
e41fbe0188 | ||
|
|
ef9d28597b | ||
|
|
6f3c4c89ab | ||
|
|
60c577a61e | ||
|
|
f3276c3039 | ||
|
|
37a843323d | ||
|
|
95c780ca8c | ||
|
|
d60d5b154a | ||
|
|
0435a805c7 | ||
|
|
f406a977e0 | ||
|
|
18db1e1ecb | ||
|
|
6bd1beb719 | ||
|
|
1293aecbca | ||
|
|
8a10c2e112 | ||
|
|
c21d777269 | ||
|
|
d864094f48 | ||
|
|
deaac3fdf0 | ||
|
|
b7062fe40c | ||
|
|
64d5e99b9d | ||
|
|
f9d8a7d22b | ||
|
|
52fcd3ad98 | ||
|
|
9d1e0ee29b | ||
|
|
de62f48f50 | ||
|
|
f47b19274c | ||
|
|
bb7f7083b9 | ||
|
|
cd580090dc | ||
|
|
ddb74b598d | ||
|
|
29571fc3aa |
57
.agents/settings.local.json
Normal file
57
.agents/settings.local.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(cat:*)",
|
||||||
|
"Bash(git -C /home/purian23/dms diff --stat .github/workflows/)",
|
||||||
|
"Bash(git -C /home/purian23/projects/danklinux diff --stat .github/workflows/)",
|
||||||
|
"Bash(git -C /home/purian23/dms diff .github/workflows/)",
|
||||||
|
"Bash(git -C /home/purian23/dms diff .github/workflows/run-ppa.yml)",
|
||||||
|
"Bash(osc cat:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(git show-ref:*)",
|
||||||
|
"Bash(git tag:*)",
|
||||||
|
"Bash(bash -c 'ALL_PATHS=$(grep -A 5 \"\"<service name=\\\"\"download_url\\\"\">\"\" distro/debian/dms/_service | grep \"\"<param name=\\\"\"path\\\"\">\"\" | sed \"\"s/.*<param name=\\\"\"path\\\"\">\\(.*\\)<\\/param>.*/\\1/\"\"); SOURCE_PATH=\"\"\"\"; for path in $ALL_PATHS; do if echo \"\"$path\"\" | grep -qE \"\"(source|archive|\\.tar\\.(gz|xz|bz2))\"\" && ! echo \"\"$path\"\" | grep -qE \"\"(distropkg|binary)\"\"; then SOURCE_PATH=\"\"$path\"\"; break; fi; done; echo \"\"Selected path: $SOURCE_PATH\"\"')",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(tar:*)",
|
||||||
|
"Bash(git -C /home/purian23/dms log:*)",
|
||||||
|
"Bash(osc status:*)",
|
||||||
|
"Bash(osc commit:*)",
|
||||||
|
"Bash(osc up:*)",
|
||||||
|
"Bash(osc results:*)",
|
||||||
|
"Bash(osc api:*)",
|
||||||
|
"Bash(systemctl:*)",
|
||||||
|
"Bash(dms version:*)",
|
||||||
|
"Bash(git describe:*)",
|
||||||
|
"Bash(qmlsc:*)",
|
||||||
|
"Bash(qmllint-qt6:*)",
|
||||||
|
"Bash(make fmt:*)",
|
||||||
|
"Bash(make test:*)",
|
||||||
|
"Bash(dms chroma list-styles:*)",
|
||||||
|
"Bash(python3:*)",
|
||||||
|
"Bash(time dms chroma:*)",
|
||||||
|
"Bash(dms chroma:*)",
|
||||||
|
"Bash(make build:*)",
|
||||||
|
"Bash(pgrep:*)",
|
||||||
|
"Bash(go build:*)",
|
||||||
|
"Bash(/tmp/dms-test chroma:*)",
|
||||||
|
"Bash(1)",
|
||||||
|
"Bash(go install:*)",
|
||||||
|
"Bash(grep:*)",
|
||||||
|
"Bash(journalctl:*)",
|
||||||
|
"Bash(qdbus:*)",
|
||||||
|
"Bash(TZ='Asia/Tokyo' date:*)",
|
||||||
|
"Bash(dms --help:*)",
|
||||||
|
"Bash(dms run:*)",
|
||||||
|
"Bash(dms status:*)",
|
||||||
|
"Bash(dms kill:*)",
|
||||||
|
"Bash(tee:*)",
|
||||||
|
"Bash(qmlscene:*)",
|
||||||
|
"Bash(quickshell --version:*)",
|
||||||
|
"WebFetch(domain:forum.qt.io)",
|
||||||
|
"Bash(gh api:*)",
|
||||||
|
"WebFetch(domain:github.com)",
|
||||||
|
"WebFetch(domain:raw.githubusercontent.com)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[*.sh]
|
||||||
|
# like -i=4
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
[*.nix]
|
||||||
|
# like -i=4
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
REPO_ROOT="$(cd "$HOOK_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Go CI checks (when core/ files are staged)
|
|
||||||
# =============================================================================
|
|
||||||
STAGED_CORE_FILES=$(git diff --cached --name-only --diff-filter=ACMR | grep '^core/' || true)
|
|
||||||
|
|
||||||
if [[ -n "$STAGED_CORE_FILES" ]]; then
|
|
||||||
echo "Go files staged in core/, running CI checks..."
|
|
||||||
cd "$REPO_ROOT/core"
|
|
||||||
|
|
||||||
# Format check
|
|
||||||
echo " Checking gofmt..."
|
|
||||||
UNFORMATTED=$(gofmt -s -l . 2>/dev/null || true)
|
|
||||||
if [[ -n "$UNFORMATTED" ]]; then
|
|
||||||
echo "The following files are not formatted:"
|
|
||||||
echo "$UNFORMATTED"
|
|
||||||
echo ""
|
|
||||||
echo "Run: cd core && gofmt -s -w ."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# golangci-lint
|
|
||||||
if command -v golangci-lint &>/dev/null; then
|
|
||||||
echo " Running golangci-lint..."
|
|
||||||
golangci-lint run ./...
|
|
||||||
else
|
|
||||||
echo " Warning: golangci-lint not installed, skipping lint"
|
|
||||||
echo " Install: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Tests
|
|
||||||
echo " Running tests..."
|
|
||||||
if ! go test ./... >/dev/null 2>&1; then
|
|
||||||
echo "Tests failed! Run 'go test ./...' for details."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build checks
|
|
||||||
echo " Building..."
|
|
||||||
mkdir -p bin
|
|
||||||
go build -buildvcs=false -o bin/dms ./cmd/dms
|
|
||||||
go build -buildvcs=false -o bin/dms-distro -tags distro_binary ./cmd/dms
|
|
||||||
go build -buildvcs=false -o bin/dankinstall ./cmd/dankinstall
|
|
||||||
|
|
||||||
echo "All Go CI checks passed!"
|
|
||||||
cd "$REPO_ROOT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# i18n sync check (DISABLED for now)
|
|
||||||
# =============================================================================
|
|
||||||
# if [[ -n "${POEDITOR_API_TOKEN:-}" ]] && [[ -n "${POEDITOR_PROJECT_ID:-}" ]]; then
|
|
||||||
# if command -v python3 &>/dev/null; then
|
|
||||||
# if ! python3 scripts/i18nsync.py check &>/dev/null; then
|
|
||||||
# echo "Translations out of sync"
|
|
||||||
# echo "Run: python3 scripts/i18nsync.py sync"
|
|
||||||
# exit 1
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
# fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
65
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,65 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug Report
|
|
||||||
about: Crashes or unexpected behaviors
|
|
||||||
title: ""
|
|
||||||
labels: "bug"
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- If your issue is related to ICONS
|
|
||||||
- Purple and black checkerboards are QT's way of signalling an icon doesn't exist
|
|
||||||
- FIX: Configure a QT6 or Icon Pack in DMS Settings that has the icon you want
|
|
||||||
- Follow the [THEMING](https://danklinux.com/docs/dankmaterialshell/icon-theming) section to ensure your QT environment variable is configured correctly for themes.
|
|
||||||
- Once done, configure an icon theme - either however you normally do with gtk3 or qt6ct, or through the built-in settings modal. -->
|
|
||||||
|
|
||||||
## Compositor
|
|
||||||
|
|
||||||
- [ ] niri
|
|
||||||
- [ ] Hyprland
|
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
- [ ] Other (specify)
|
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
<!-- Arch, Fedora, Debian, etc. -->
|
|
||||||
|
|
||||||
## dms version
|
|
||||||
|
|
||||||
<!-- Output of dms version command -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- Brief description of the issue -->
|
|
||||||
|
|
||||||
## Expected Behavior
|
|
||||||
|
|
||||||
<!-- Describe what you expected to happen -->
|
|
||||||
|
|
||||||
## Steps to Reproduce
|
|
||||||
|
|
||||||
<!-- Please provide detailed steps to reproduce the issue -->
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
3.
|
|
||||||
|
|
||||||
## Error Messages/Logs
|
|
||||||
|
|
||||||
<!-- Please include any error messages, stack traces, or relevant logs -->
|
|
||||||
<!-- you can get a log file with the following steps:
|
|
||||||
dms kill
|
|
||||||
mkdir ~/dms_logs
|
|
||||||
nohup dms run > ~/dms_logs/dms-$(date +%s).txt 2>&1 &
|
|
||||||
|
|
||||||
Then trigger your issue, and share the contents of ~/dms_logs/dms-<timestamp>.txt
|
|
||||||
|
|
||||||
-->
|
|
||||||
|
|
||||||
```
|
|
||||||
Paste error messages or logs here
|
|
||||||
```
|
|
||||||
|
|
||||||
## Screenshots/Recordings
|
|
||||||
|
|
||||||
<!-- If applicable, add screenshots or screen recordings -->
|
|
||||||
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
129
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
name: Bug Report
|
||||||
|
description: Crashes or unexpected behaviors
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## DankMaterialShell Bug Report
|
||||||
|
Limit your report to one issue per submission unless similarly related
|
||||||
|
- type: dropdown
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor
|
||||||
|
options:
|
||||||
|
- Niri
|
||||||
|
- Hyprland
|
||||||
|
- MangoWC (dwl)
|
||||||
|
- Sway
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: distribution
|
||||||
|
attributes:
|
||||||
|
label: Distribution
|
||||||
|
options:
|
||||||
|
- Arch Linux
|
||||||
|
- CachyOS
|
||||||
|
- Fedora
|
||||||
|
- NixOS
|
||||||
|
- Debian
|
||||||
|
- Ubuntu
|
||||||
|
- Gentoo
|
||||||
|
- OpenSUSE
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: distribution_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: installation_method
|
||||||
|
attributes:
|
||||||
|
label: Select your Installation Method
|
||||||
|
options:
|
||||||
|
- DankInstaller
|
||||||
|
- Distro Packaging
|
||||||
|
- Source
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: original_installation_method
|
||||||
|
attributes:
|
||||||
|
label: Was this your original Installation method?
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- No (specify below)
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: original_installation_method_specify
|
||||||
|
attributes:
|
||||||
|
label: If no, specify
|
||||||
|
placeholder: e.g., Distro Packaging, then Source
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: dms_doctor
|
||||||
|
attributes:
|
||||||
|
label: dms doctor -vC
|
||||||
|
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
|
||||||
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Brief description of the issue
|
||||||
|
placeholder: What happened?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: expected_behavior
|
||||||
|
attributes:
|
||||||
|
label: Expected Behavior
|
||||||
|
description: What did you expect to happen?
|
||||||
|
placeholder: Describe the expected behavior
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: steps_to_reproduce
|
||||||
|
attributes:
|
||||||
|
label: Steps to Reproduce
|
||||||
|
description: Please provide detailed steps to reproduce the issue
|
||||||
|
placeholder: |
|
||||||
|
1. ...
|
||||||
|
2. ...
|
||||||
|
3. ...
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Error Messages/Logs
|
||||||
|
description: Please include any error messages, stack traces, or relevant logs
|
||||||
|
placeholder: |
|
||||||
|
Paste error messages or logs here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots/Recordings
|
||||||
|
description: If applicable, add screenshots or screen recordings
|
||||||
|
placeholder: Attach images or videos here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
33
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,33 +0,0 @@
|
|||||||
---
|
|
||||||
name: Request a Feature
|
|
||||||
about: New widgets, new widget behavior, etc.
|
|
||||||
title: ""
|
|
||||||
labels: "enhancement"
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Description
|
|
||||||
|
|
||||||
<!-- Brief description of the feature requested -->
|
|
||||||
|
|
||||||
## Use Case
|
|
||||||
|
|
||||||
<!-- Explain the purpose of this feature/why it'd be useful to you -->
|
|
||||||
|
|
||||||
## Compositor
|
|
||||||
|
|
||||||
Is this feature specific to one compositor?
|
|
||||||
|
|
||||||
- [ ] All compositors
|
|
||||||
- [ ] niri
|
|
||||||
- [ ] Hyprland
|
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
|
|
||||||
## Proposed Solution
|
|
||||||
|
|
||||||
<!-- If you have any ideas for how to implement this, please share! -->
|
|
||||||
|
|
||||||
## Alternatives/Existing Solutions
|
|
||||||
|
|
||||||
<!-- Include any similar/pre-existing products that solve this problem -->
|
|
||||||
62
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
62
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
name: Feature Request
|
||||||
|
description: Suggest a new feature or improvement for DMS. Keep features focused on a single topic with clear benefits, examples, etc. Avoid vague or broad requests, they will be closed.
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## DankMaterialShell Feature Request
|
||||||
|
- type: textarea
|
||||||
|
id: feature_description
|
||||||
|
attributes:
|
||||||
|
label: Feature Description
|
||||||
|
description: Brief description of the feature requested
|
||||||
|
placeholder: What feature would you like to see?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: use_case
|
||||||
|
attributes:
|
||||||
|
label: Use Case
|
||||||
|
description: Explain the purpose of this feature/why it'd be useful to you
|
||||||
|
placeholder: Why is this feature important?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor(s)
|
||||||
|
description: Is this feature specific to one or more compositors?
|
||||||
|
options:
|
||||||
|
- All compositors
|
||||||
|
- Niri
|
||||||
|
- Hyprland
|
||||||
|
- MangoWC (dwl)
|
||||||
|
- Sway
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: compositor_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., Wayfire, Mutter, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: proposed_solution
|
||||||
|
attributes:
|
||||||
|
label: Proposed Solution
|
||||||
|
description: If you have any ideas for how to implement this, please share!
|
||||||
|
placeholder: Suggest a solution or approach
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Alternatives/Existing Solutions
|
||||||
|
description: Include any similar/pre-existing products that solve this problem
|
||||||
|
placeholder: List alternatives or existing solutions
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
40
.github/ISSUE_TEMPLATE/support_request.md
vendored
@@ -1,40 +0,0 @@
|
|||||||
---
|
|
||||||
name: Request Assistance or Support
|
|
||||||
about: Help with installation, usage, or general questions.
|
|
||||||
title: ""
|
|
||||||
labels: "support"
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
## Compositor
|
|
||||||
|
|
||||||
- [ ] niri
|
|
||||||
- [ ] Hyprland
|
|
||||||
- [ ] dwl (MangoWC)
|
|
||||||
- [ ] sway
|
|
||||||
- [ ] other
|
|
||||||
|
|
||||||
## Distribution
|
|
||||||
|
|
||||||
<!-- Arch, Fedora, Debian, etc. -->
|
|
||||||
|
|
||||||
## dms version
|
|
||||||
|
|
||||||
<!-- Output of dms version command -->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
<!-- Brief description of the support needed -->
|
|
||||||
|
|
||||||
## Solutions Tried
|
|
||||||
|
|
||||||
<!-- Describe what you've tried so far -->
|
|
||||||
<!-- Outlining what you've tried so far helps us make improvements to the user experience and documentation to avoid recurrent issues -->
|
|
||||||
|
|
||||||
## Configuration Details
|
|
||||||
|
|
||||||
<!-- Include any configuration if relevant -->
|
|
||||||
|
|
||||||
## Screenshots/Recordings
|
|
||||||
|
|
||||||
<!-- If applicable, add screenshots or screen recordings -->
|
|
||||||
124
.github/ISSUE_TEMPLATE/support_request.yml
vendored
Normal file
124
.github/ISSUE_TEMPLATE/support_request.yml
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
name: Support Request
|
||||||
|
description: Help with installation, usage, or general questions about DankMaterialShell
|
||||||
|
labels:
|
||||||
|
- support
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
## DankMaterialShell Support Request
|
||||||
|
- type: dropdown
|
||||||
|
id: compositor
|
||||||
|
attributes:
|
||||||
|
label: Compositor
|
||||||
|
options:
|
||||||
|
- Niri
|
||||||
|
- Hyprland
|
||||||
|
- MangoWC (dwl)
|
||||||
|
- Sway
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: compositor_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., Wayfire, Mutter, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: distribution
|
||||||
|
attributes:
|
||||||
|
label: Distribution
|
||||||
|
options:
|
||||||
|
- Arch Linux
|
||||||
|
- CachyOS
|
||||||
|
- Fedora
|
||||||
|
- NixOS
|
||||||
|
- Debian
|
||||||
|
- Ubuntu
|
||||||
|
- Gentoo
|
||||||
|
- OpenSUSE
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: distribution_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: installation_method
|
||||||
|
attributes:
|
||||||
|
label: Select your Installation Method
|
||||||
|
options:
|
||||||
|
- DankInstaller
|
||||||
|
- Distro Packaging
|
||||||
|
- Source
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: original_installation_method_different
|
||||||
|
attributes:
|
||||||
|
label: Was your original Installation method different?
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- No (specify below)
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: original_installation_method_specify
|
||||||
|
attributes:
|
||||||
|
label: If no, specify
|
||||||
|
placeholder: e.g., Distro Packaging, then Source
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: dms_doctor
|
||||||
|
attributes:
|
||||||
|
label: dms doctor -vC
|
||||||
|
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||||
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Brief description of the support needed
|
||||||
|
placeholder: What do you need help with?
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: textarea
|
||||||
|
id: solutions_tried
|
||||||
|
attributes:
|
||||||
|
label: Solutions Tried
|
||||||
|
description: Describe what you've tried so far (commands, documentation, etc.)
|
||||||
|
placeholder: List steps or resources you've already tried
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: configuration
|
||||||
|
attributes:
|
||||||
|
label: Configuration Details
|
||||||
|
description: Include any relevant configuration if relevant
|
||||||
|
placeholder: Add configuration or environment info
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: textarea
|
||||||
|
id: screenshots
|
||||||
|
attributes:
|
||||||
|
label: Screenshots/Recordings
|
||||||
|
description: If applicable, add screenshots or screen recordings
|
||||||
|
placeholder: Attach images or videos here
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
31
.github/workflows/dms-stable.yml
vendored
Normal file
31
.github/workflows/dms-stable.yml
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
name: Update stable branch
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-stable:
|
||||||
|
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@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
|
|
||||||
|
- name: Push to stable branch
|
||||||
|
env:
|
||||||
|
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
|
||||||
23
.github/workflows/go-ci.yml
vendored
23
.github/workflows/go-ci.yml
vendored
@@ -28,25 +28,20 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install flatpak
|
||||||
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
|
|
||||||
|
- name: Add flathub
|
||||||
|
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
|
- name: Add a flatpak that mutagen could support
|
||||||
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Format check
|
|
||||||
run: |
|
|
||||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
|
||||||
echo "The following files are not formatted:"
|
|
||||||
gofmt -s -l .
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@v9
|
|
||||||
with:
|
|
||||||
version: v2.6
|
|
||||||
working-directory: core
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: go test -v ./...
|
run: go test -v ./...
|
||||||
|
|
||||||
|
|||||||
23
.github/workflows/nix-pr-check.yml
vendored
Normal file
23
.github/workflows/nix-pr-check.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: Check nix flake
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
paths:
|
||||||
|
- "flake.*"
|
||||||
|
- "distro/nix/**"
|
||||||
|
jobs:
|
||||||
|
check-flake:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Nix
|
||||||
|
uses: cachix/install-nix-action@v31
|
||||||
|
|
||||||
|
- name: Check the flake
|
||||||
|
run: nix flake check
|
||||||
29
.github/workflows/prek.yml
vendored
Normal file
29
.github/workflows/prek.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
name: Pre-commit Checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
branches: [master, main]
|
||||||
|
jobs:
|
||||||
|
pre-commit-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install flatpak
|
||||||
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
|
|
||||||
|
- name: Add flathub
|
||||||
|
run: sudo flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
|
|
||||||
|
- name: Add a flatpak that mutagen could support
|
||||||
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
|
- name: run pre-commit hooks
|
||||||
|
uses: j178/prek-action@v1
|
||||||
429
.github/workflows/release.yml
vendored
429
.github/workflows/release.yml
vendored
@@ -1,16 +1,19 @@
|
|||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
workflow_dispatch:
|
||||||
tags:
|
inputs:
|
||||||
- 'v*'
|
tag:
|
||||||
|
description: "Tag to release (e.g., v1.0.1)"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
actions: write
|
actions: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: release-${{ github.ref_name }}
|
group: release-${{ inputs.tag }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -24,10 +27,14 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: core
|
working-directory: core
|
||||||
|
|
||||||
|
env:
|
||||||
|
TAG: ${{ inputs.tag }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
@@ -54,7 +61,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dankinstall
|
cd cmd/dankinstall
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dankinstall-${{ matrix.arch }}
|
-o ../../dankinstall-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dankinstall-${{ matrix.arch }}
|
gzip -9 -k dankinstall-${{ matrix.arch }}
|
||||||
@@ -68,7 +75,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
go build -trimpath -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-${{ matrix.arch }}
|
-o ../../dms-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-${{ matrix.arch }}
|
gzip -9 -k dms-${{ matrix.arch }}
|
||||||
@@ -91,7 +98,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -eux
|
set -eux
|
||||||
cd cmd/dms
|
cd cmd/dms
|
||||||
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${GITHUB_REF#refs/tags/}" \
|
go build -trimpath -tags distro_binary -ldflags "-s -w -X main.Version=${TAG}" \
|
||||||
-o ../../dms-distropkg-${{ matrix.arch }}
|
-o ../../dms-distropkg-${{ matrix.arch }}
|
||||||
cd ../..
|
cd ../..
|
||||||
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
gzip -9 -k dms-distropkg-${{ matrix.arch }}
|
||||||
@@ -128,60 +135,66 @@ jobs:
|
|||||||
core/completion.zsh
|
core/completion.zsh
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
update-versions:
|
# update-versions:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
needs: build-core
|
# needs: build-core
|
||||||
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@v1
|
||||||
with:
|
# with:
|
||||||
app-id: ${{ secrets.APP_ID }}
|
# app-id: ${{ secrets.APP_ID }}
|
||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
# - name: Checkout
|
||||||
uses: actions/checkout@v4
|
# uses: actions/checkout@v4
|
||||||
with:
|
# with:
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
fetch-depth: 0
|
# fetch-depth: 0
|
||||||
|
|
||||||
- name: Update VERSION
|
# - name: Update VERSION
|
||||||
env:
|
# env:
|
||||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
# GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||||
run: |
|
# run: |
|
||||||
set -euo pipefail
|
# set -euo pipefail
|
||||||
git config user.name "dms-ci[bot]"
|
# git config user.name "dms-ci[bot]"
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
# git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
version="${GITHUB_REF#refs/tags/}"
|
# version="${GITHUB_REF#refs/tags/}"
|
||||||
echo "Updating to version: $version"
|
# echo "Updating to version: $version"
|
||||||
echo "${version}" > quickshell/VERSION
|
# echo "${version}" > quickshell/VERSION
|
||||||
git add quickshell/VERSION
|
# git add quickshell/VERSION
|
||||||
|
|
||||||
if ! git diff --cached --quiet; then
|
# if ! git diff --cached --quiet; then
|
||||||
git commit -m "chore: bump version to $version"
|
# git commit -m "chore: bump version to $version"
|
||||||
git pull --rebase origin master
|
# git pull --rebase origin master
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
# git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
||||||
fi
|
# fi
|
||||||
|
|
||||||
git tag -f "${version}"
|
# git tag -f "${version}"
|
||||||
git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
# git push -f https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git "${version}"
|
||||||
|
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs: [build-core, update-versions]
|
needs: [build-core] #, update-versions]
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Fetch updated tag after version bump
|
- name: Fetch updated tag after version bump
|
||||||
run: |
|
run: |
|
||||||
git fetch origin --force tag ${{ github.ref_name }}
|
git fetch origin --force tag ${TAG}
|
||||||
git checkout ${{ github.ref_name }}
|
git checkout ${TAG}
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
@@ -221,6 +234,7 @@ jobs:
|
|||||||
- **`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
|
||||||
- **`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-qml.tar.gz`** - QML source code only
|
- **`dms-qml.tar.gz`** - QML source code only
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
@@ -273,6 +287,9 @@ jobs:
|
|||||||
# 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/
|
||||||
|
|
||||||
|
# Copy root assets directory to quickshell for systemd service and desktop file
|
||||||
|
cp -r assets quickshell/
|
||||||
|
|
||||||
# Tar the CONTENTS of quickshell/, not the directory itself
|
# Tar the CONTENTS of quickshell/, not the directory itself
|
||||||
(cd quickshell && tar --exclude='.git' \
|
(cd quickshell && tar --exclude='.git' \
|
||||||
--exclude='.github' \
|
--exclude='.github' \
|
||||||
@@ -376,6 +393,19 @@ jobs:
|
|||||||
rm -rf _temp_full
|
rm -rf _temp_full
|
||||||
done
|
done
|
||||||
|
|
||||||
|
- name: Generate vendored source tarball
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
VERSION_NUM=${TAG#v}
|
||||||
|
cd core
|
||||||
|
go mod vendor
|
||||||
|
cd ..
|
||||||
|
tar czf "_release_assets/dms-cli-${VERSION_NUM}.tar.gz" \
|
||||||
|
--transform "s,^core/,dms-cli-${VERSION_NUM}/," \
|
||||||
|
--exclude='core/.git' \
|
||||||
|
core/
|
||||||
|
(cd _release_assets && sha256sum "dms-cli-${VERSION_NUM}.tar.gz" > "dms-cli-${VERSION_NUM}.tar.gz.sha256")
|
||||||
|
|
||||||
- name: Create GitHub Release
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
@@ -387,314 +417,3 @@ jobs:
|
|||||||
prerelease: ${{ contains(env.TAG, '-') }}
|
prerelease: ${{ contains(env.TAG, '-') }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
trigger-obs-update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install OSC
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y osc
|
|
||||||
|
|
||||||
mkdir -p ~/.config/osc
|
|
||||||
cat > ~/.config/osc/oscrc << EOF
|
|
||||||
[general]
|
|
||||||
apiurl = https://api.opensuse.org
|
|
||||||
|
|
||||||
[https://api.opensuse.org]
|
|
||||||
user = ${{ secrets.OBS_USERNAME }}
|
|
||||||
pass = ${{ secrets.OBS_PASSWORD }}
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/osc/oscrc
|
|
||||||
|
|
||||||
- name: Update OBS packages
|
|
||||||
run: |
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
cd distro
|
|
||||||
bash scripts/obs-upload.sh dms "Update to $VERSION"
|
|
||||||
|
|
||||||
trigger-ppa-update:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
debhelper \
|
|
||||||
devscripts \
|
|
||||||
dput \
|
|
||||||
lftp \
|
|
||||||
build-essential \
|
|
||||||
fakeroot \
|
|
||||||
dpkg-dev
|
|
||||||
|
|
||||||
- name: Configure GPG
|
|
||||||
env:
|
|
||||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
|
||||||
run: |
|
|
||||||
echo "$GPG_KEY" | gpg --import
|
|
||||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
|
||||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Upload to PPA
|
|
||||||
run: |
|
|
||||||
VERSION="${{ github.ref_name }}"
|
|
||||||
cd distro/ubuntu/ppa
|
|
||||||
bash create-and-upload.sh ../dms dms questing
|
|
||||||
|
|
||||||
copr-build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: release
|
|
||||||
env:
|
|
||||||
TAG: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Determine version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
VERSION="${TAG#v}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Building DMS stable version: $VERSION"
|
|
||||||
|
|
||||||
- name: Setup build environment
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y rpm wget curl jq gzip
|
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
|
||||||
|
|
||||||
- name: Download release assets
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
cd ~/rpmbuild/SOURCES
|
|
||||||
|
|
||||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
|
||||||
echo "Failed to download dms-qml.tar.gz for v${VERSION}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Generate stable spec file
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
|
||||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
|
||||||
|
|
||||||
%global debug_package %{nil}
|
|
||||||
%global version VERSION_PLACEHOLDER
|
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
|
||||||
|
|
||||||
Name: dms
|
|
||||||
Version: %{version}
|
|
||||||
Release: 1%{?dist}
|
|
||||||
Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
BuildRequires: gzip
|
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
|
||||||
Requires: accountsservice
|
|
||||||
Requires: dms-cli
|
|
||||||
Requires: dgop
|
|
||||||
|
|
||||||
Recommends: cava
|
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
%package -n dms-cli
|
|
||||||
Summary: DankMaterialShell CLI tool
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%package -n dgop
|
|
||||||
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/dgop
|
|
||||||
Provides: dgop
|
|
||||||
|
|
||||||
%description -n dgop
|
|
||||||
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
|
||||||
network statistics. Designed for integration with DankMaterialShell but can be
|
|
||||||
used standalone. This package always includes the latest stable dgop release.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
|
||||||
case "%{_arch}" in
|
|
||||||
x86_64)
|
|
||||||
ARCH_SUFFIX="amd64"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dgop for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
|
||||||
chmod +x %{_builddir}/dgop
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
|
||||||
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
install -Dm644 assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
install -Dm644 assets/dms-open.desktop %{buildroot}%{_datadir}/applications/dms-open.desktop
|
|
||||||
install -Dm644 assets/danklogo.svg %{buildroot}%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
echo "%{version}" > %{buildroot}%{_datadir}/quickshell/dms/VERSION
|
|
||||||
|
|
||||||
%posttrans
|
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$1" -ge 2 ]; then
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
%files
|
|
||||||
%license LICENSE
|
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
%{_datadir}/applications/dms-open.desktop
|
|
||||||
%{_datadir}/icons/hicolor/scalable/apps/danklogo.svg
|
|
||||||
|
|
||||||
%files -n dms-cli
|
|
||||||
%{_bindir}/dms
|
|
||||||
%{_datadir}/bash-completion/completions/dms
|
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%files -n dgop
|
|
||||||
%{_bindir}/dgop
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-1
|
|
||||||
- Stable release VERSION_PLACEHOLDER
|
|
||||||
- Built from GitHub release
|
|
||||||
- Includes latest dms-cli and dgop binaries
|
|
||||||
SPECEOF
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
- name: Build SRPM
|
|
||||||
id: build
|
|
||||||
run: |
|
|
||||||
cd ~/rpmbuild/SPECS
|
|
||||||
rpmbuild -bs dms.spec
|
|
||||||
|
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
|
||||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
|
||||||
echo "SRPM built: $SRPM_NAME"
|
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
|
||||||
retention-days: 90
|
|
||||||
|
|
||||||
- name: Install Copr CLI
|
|
||||||
run: |
|
|
||||||
sudo apt-get install -y python3-pip
|
|
||||||
pip3 install copr-cli
|
|
||||||
|
|
||||||
mkdir -p ~/.config
|
|
||||||
cat > ~/.config/copr << EOF
|
|
||||||
[copr-cli]
|
|
||||||
login = ${{ secrets.COPR_LOGIN }}
|
|
||||||
username = avengemedia
|
|
||||||
token = ${{ secrets.COPR_TOKEN }}
|
|
||||||
copr_url = https://copr.fedorainfracloud.org
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/copr
|
|
||||||
|
|
||||||
- name: Upload to Copr
|
|
||||||
run: |
|
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
|
||||||
|
|
||||||
echo "Uploading SRPM to avengemedia/dms..."
|
|
||||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
|
||||||
echo "$BUILD_OUTPUT"
|
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
|
||||||
echo "Build submitted: https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
|
||||||
fi
|
|
||||||
|
|||||||
291
.github/workflows/run-copr.yml
vendored
291
.github/workflows/run-copr.yml
vendored
@@ -3,8 +3,17 @@ name: DMS Copr Stable Release
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
|
package:
|
||||||
|
description: 'Package to build (dms, dms-greeter, or both)'
|
||||||
|
required: false
|
||||||
|
default: 'dms'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- dms
|
||||||
|
- dms-greeter
|
||||||
|
- both
|
||||||
version:
|
version:
|
||||||
description: 'Versioning (e.g., 0.1.14, leave empty for latest release)'
|
description: 'Versioning (e.g., 1.0.3, leave empty for latest release)'
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ''
|
||||||
release:
|
release:
|
||||||
@@ -13,13 +22,32 @@ on:
|
|||||||
default: '1'
|
default: '1'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-upload:
|
determine-packages:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
packages: ${{ steps.set-packages.outputs.packages }}
|
||||||
|
steps:
|
||||||
|
- name: Set package list
|
||||||
|
id: set-packages
|
||||||
|
run: |
|
||||||
|
PACKAGE_INPUT="${{ github.event.inputs.package || 'dms' }}"
|
||||||
|
if [ "$PACKAGE_INPUT" = "both" ]; then
|
||||||
|
echo 'packages=["dms","dms-greeter"]' >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=[\"$PACKAGE_INPUT\"]" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
build-and-upload:
|
||||||
|
needs: determine-packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
package: ${{ fromJSON(needs.determine-packages.outputs.packages) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
run: |
|
run: |
|
||||||
@@ -39,237 +67,84 @@ jobs:
|
|||||||
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
echo "release=$RELEASE" >> $GITHUB_OUTPUT
|
||||||
echo "✅ Building DMS hotfix version: $VERSION-$RELEASE"
|
echo "✅ Building ${{ matrix.package }} version: $VERSION-$RELEASE"
|
||||||
|
|
||||||
- name: Setup build environment
|
- name: Setup build environment
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y rpm wget curl jq gzip
|
sudo apt-get install -y rpm wget curl jq gzip
|
||||||
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||||
echo "✅ RPM build environment ready"
|
echo "✅ RPM build environment ready"
|
||||||
|
|
||||||
- name: Download release assets
|
- name: Download release assets
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
cd ~/rpmbuild/SOURCES
|
cd ~/rpmbuild/SOURCES
|
||||||
|
|
||||||
echo "📦 Downloading DMS QML source for v${VERSION}..."
|
echo "📦 Downloading DMS QML source for v${VERSION}..."
|
||||||
|
|
||||||
# Download DMS QML source
|
# Download DMS QML source
|
||||||
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
wget "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-qml.tar.gz" || {
|
||||||
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
|
echo "❌ Failed to download dms-qml.tar.gz for v${VERSION}"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "✅ Source downloaded"
|
echo "✅ Source downloaded"
|
||||||
echo "Note: dms-cli and dgop binaries will be downloaded during build based on target architecture"
|
echo "Note: dms-cli binary will be downloaded during build based on target architecture"
|
||||||
ls -lh
|
ls -lh
|
||||||
|
|
||||||
- name: Generate stable spec file
|
- name: Generate stable spec file
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
RELEASE="${{ steps.version.outputs.release }}"
|
RELEASE="${{ steps.version.outputs.release }}"
|
||||||
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
CHANGELOG_DATE="$(date '+%a %b %d %Y')"
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
|
||||||
cat > ~/rpmbuild/SPECS/dms.spec <<'SPECEOF'
|
# Copy spec file from repository
|
||||||
# Spec for DMS stable releases - Generated by GitHub Actions
|
cp distro/fedora/${PACKAGE}.spec ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
|
||||||
%global debug_package %{nil}
|
# Replace placeholders with actual values
|
||||||
%global version VERSION_PLACEHOLDER
|
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
%global pkg_summary DankMaterialShell - Material 3 inspired shell for Wayland compositors
|
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
|
||||||
Name: dms
|
echo "✅ Spec file generated for ${PACKAGE} v${VERSION}-${RELEASE}"
|
||||||
Version: %{version}
|
|
||||||
Release: RELEASE_PLACEHOLDER%{?dist}
|
|
||||||
Summary: %{pkg_summary}
|
|
||||||
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
Source0: dms-qml.tar.gz
|
|
||||||
|
|
||||||
BuildRequires: gzip
|
|
||||||
BuildRequires: wget
|
|
||||||
BuildRequires: systemd-rpm-macros
|
|
||||||
|
|
||||||
Requires: (quickshell or quickshell-git)
|
|
||||||
Requires: accountsservice
|
|
||||||
Requires: dms-cli
|
|
||||||
Requires: dgop
|
|
||||||
|
|
||||||
Recommends: cava
|
|
||||||
Recommends: cliphist
|
|
||||||
Recommends: danksearch
|
|
||||||
Recommends: hyprpicker
|
|
||||||
Recommends: matugen
|
|
||||||
Recommends: wl-clipboard
|
|
||||||
Recommends: NetworkManager
|
|
||||||
Recommends: qt6-qtmultimedia
|
|
||||||
Suggests: qt6ct
|
|
||||||
|
|
||||||
%description
|
|
||||||
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
|
|
||||||
and optimized for the niri and hyprland compositors. Features notifications,
|
|
||||||
app launcher, wallpaper customization, and fully customizable with plugins.
|
|
||||||
|
|
||||||
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
|
|
||||||
process monitoring, notification center, clipboard history, dock, control center,
|
|
||||||
lock screen, and comprehensive plugin system.
|
|
||||||
|
|
||||||
%package -n dms-cli
|
|
||||||
Summary: DankMaterialShell CLI tool
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/DankMaterialShell
|
|
||||||
|
|
||||||
%description -n dms-cli
|
|
||||||
Command-line interface for DankMaterialShell configuration and management.
|
|
||||||
Provides native DBus bindings, NetworkManager integration, and system utilities.
|
|
||||||
|
|
||||||
%package -n dgop
|
|
||||||
Summary: Stateless CPU/GPU monitor for DankMaterialShell
|
|
||||||
License: MIT
|
|
||||||
URL: https://github.com/AvengeMedia/dgop
|
|
||||||
Provides: dgop
|
|
||||||
|
|
||||||
%description -n dgop
|
|
||||||
DGOP is a stateless system monitoring tool that provides CPU, GPU, memory, and
|
|
||||||
network statistics. Designed for integration with DankMaterialShell but can be
|
|
||||||
used standalone. This package always includes the latest stable dgop release.
|
|
||||||
|
|
||||||
%prep
|
|
||||||
%setup -q -c -n dms-qml
|
|
||||||
|
|
||||||
# Download architecture-specific binaries during build
|
|
||||||
# This ensures the correct architecture is used for each build target
|
|
||||||
case "%{_arch}" in
|
|
||||||
x86_64)
|
|
||||||
ARCH_SUFFIX="amd64"
|
|
||||||
;;
|
|
||||||
aarch64)
|
|
||||||
ARCH_SUFFIX="arm64"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unsupported architecture: %{_arch}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Download dms-cli for target architecture
|
|
||||||
wget -O %{_builddir}/dms-cli.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dms-cli for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dms-cli.gz > %{_builddir}/dms-cli
|
|
||||||
chmod +x %{_builddir}/dms-cli
|
|
||||||
|
|
||||||
# Download dgop for target architecture
|
|
||||||
wget -O %{_builddir}/dgop.gz "https://github.com/AvengeMedia/dgop/releases/latest/download/dgop-linux-${ARCH_SUFFIX}.gz" || {
|
|
||||||
echo "Failed to download dgop for architecture %{_arch}"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
gunzip -c %{_builddir}/dgop.gz > %{_builddir}/dgop
|
|
||||||
chmod +x %{_builddir}/dgop
|
|
||||||
|
|
||||||
%build
|
|
||||||
|
|
||||||
%install
|
|
||||||
install -Dm755 %{_builddir}/dms-cli %{buildroot}%{_bindir}/dms
|
|
||||||
install -Dm755 %{_builddir}/dgop %{buildroot}%{_bindir}/dgop
|
|
||||||
|
|
||||||
# Shell completions
|
|
||||||
install -d %{buildroot}%{_datadir}/bash-completion/completions
|
|
||||||
install -d %{buildroot}%{_datadir}/zsh/site-functions
|
|
||||||
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
|
|
||||||
%{_builddir}/dms-cli completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
|
|
||||||
%{_builddir}/dms-cli completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
|
|
||||||
%{_builddir}/dms-cli completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
|
|
||||||
|
|
||||||
install -Dm644 %{_builddir}/dms-qml/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
|
|
||||||
cp -r %{_builddir}/dms-qml/* %{buildroot}%{_datadir}/quickshell/dms/
|
|
||||||
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
|
|
||||||
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
|
|
||||||
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
|
|
||||||
|
|
||||||
%posttrans
|
|
||||||
# Clean up old installation path from previous versions (only if empty)
|
|
||||||
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
|
|
||||||
# Remove directories only if empty (preserves any user-added files)
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
|
|
||||||
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Restart DMS for active users after upgrade
|
|
||||||
if [ "$1" -ge 2 ]; then
|
|
||||||
pkill -USR1 -x dms >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
%files
|
|
||||||
%license LICENSE
|
|
||||||
%doc README.md CONTRIBUTING.md
|
|
||||||
%{_datadir}/quickshell/dms/
|
|
||||||
%{_userunitdir}/dms.service
|
|
||||||
|
|
||||||
%files -n dms-cli
|
|
||||||
%{_bindir}/dms
|
|
||||||
%{_datadir}/bash-completion/completions/dms
|
|
||||||
%{_datadir}/zsh/site-functions/_dms
|
|
||||||
%{_datadir}/fish/vendor_completions.d/dms.fish
|
|
||||||
|
|
||||||
%files -n dgop
|
|
||||||
%{_bindir}/dgop
|
|
||||||
|
|
||||||
%changelog
|
|
||||||
* CHANGELOG_DATE_PLACEHOLDER AvengeMedia <contact@avengemedia.com> - VERSION_PLACEHOLDER-RELEASE_PLACEHOLDER
|
|
||||||
- Stable release VERSION_PLACEHOLDER
|
|
||||||
- Built from GitHub release
|
|
||||||
- Includes latest dms-cli and dgop binaries
|
|
||||||
SPECEOF
|
|
||||||
|
|
||||||
sed -i "s/VERSION_PLACEHOLDER/${VERSION}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/RELEASE_PLACEHOLDER/${RELEASE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" ~/rpmbuild/SPECS/dms.spec
|
|
||||||
|
|
||||||
echo "✅ Spec file generated for v${VERSION}-${RELEASE}"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Spec file preview ==="
|
echo "=== Spec file preview ==="
|
||||||
head -40 ~/rpmbuild/SPECS/dms.spec
|
head -40 ~/rpmbuild/SPECS/${PACKAGE}.spec
|
||||||
|
|
||||||
- name: Build SRPM
|
- name: Build SRPM
|
||||||
id: build
|
id: build
|
||||||
run: |
|
run: |
|
||||||
cd ~/rpmbuild/SPECS
|
cd ~/rpmbuild/SPECS
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
echo "🔨 Building SRPM..."
|
|
||||||
rpmbuild -bs dms.spec
|
echo "🔨 Building SRPM for ${PACKAGE}..."
|
||||||
|
rpmbuild -bs ${PACKAGE}.spec
|
||||||
SRPM=$(ls ~/rpmbuild/SRPMS/*.src.rpm | tail -n 1)
|
|
||||||
|
SRPM=$(ls ~/rpmbuild/SRPMS/${PACKAGE}-*.src.rpm | tail -n 1)
|
||||||
SRPM_NAME=$(basename "$SRPM")
|
SRPM_NAME=$(basename "$SRPM")
|
||||||
|
|
||||||
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
echo "srpm_path=$SRPM" >> $GITHUB_OUTPUT
|
||||||
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
echo "srpm_name=$SRPM_NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
echo "✅ SRPM built: $SRPM_NAME"
|
echo "✅ SRPM built: $SRPM_NAME"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== SRPM Info ==="
|
echo "=== SRPM Info ==="
|
||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: dms-stable-srpm-${{ steps.version.outputs.version }}
|
name: ${{ matrix.package }}-stable-srpm-${{ steps.version.outputs.version }}
|
||||||
path: ${{ steps.build.outputs.srpm_path }}
|
path: ${{ steps.build.outputs.srpm_path }}
|
||||||
retention-days: 90
|
retention-days: 90
|
||||||
|
|
||||||
- name: Install Copr CLI
|
- name: Install Copr CLI
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get install -y python3-pip
|
sudo apt-get install -y python3-pip
|
||||||
pip3 install copr-cli
|
pip3 install copr-cli
|
||||||
|
|
||||||
mkdir -p ~/.config
|
mkdir -p ~/.config
|
||||||
cat > ~/.config/copr << EOF
|
cat > ~/.config/copr << EOF
|
||||||
[copr-cli]
|
[copr-cli]
|
||||||
@@ -279,37 +154,57 @@ jobs:
|
|||||||
copr_url = https://copr.fedorainfracloud.org
|
copr_url = https://copr.fedorainfracloud.org
|
||||||
EOF
|
EOF
|
||||||
chmod 600 ~/.config/copr
|
chmod 600 ~/.config/copr
|
||||||
|
|
||||||
echo "✅ Copr CLI configured"
|
echo "✅ Copr CLI configured"
|
||||||
|
|
||||||
|
- name: Determine Copr project
|
||||||
|
id: copr_project
|
||||||
|
run: |
|
||||||
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
if [ "$PACKAGE" = "dms" ]; then
|
||||||
|
COPR_PROJECT="avengemedia/dms"
|
||||||
|
elif [ "$PACKAGE" = "dms-greeter" ]; then
|
||||||
|
COPR_PROJECT="avengemedia/danklinux"
|
||||||
|
else
|
||||||
|
echo "❌ Unknown package: $PACKAGE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "copr_project=$COPR_PROJECT" >> $GITHUB_OUTPUT
|
||||||
|
echo "✅ Copr project: $COPR_PROJECT"
|
||||||
|
|
||||||
- name: Upload to Copr
|
- name: Upload to Copr
|
||||||
run: |
|
run: |
|
||||||
SRPM="${{ steps.build.outputs.srpm_path }}"
|
SRPM="${{ steps.build.outputs.srpm_path }}"
|
||||||
VERSION="${{ steps.version.outputs.version }}"
|
VERSION="${{ steps.version.outputs.version }}"
|
||||||
|
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||||
echo "🚀 Uploading SRPM to avengemedia/dms..."
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
|
||||||
|
echo "🚀 Uploading ${PACKAGE} SRPM to ${COPR_PROJECT}..."
|
||||||
echo " SRPM: $(basename $SRPM)"
|
echo " SRPM: $(basename $SRPM)"
|
||||||
echo " Version: $VERSION"
|
echo " Version: $VERSION"
|
||||||
|
|
||||||
BUILD_OUTPUT=$(copr-cli build avengemedia/dms "$SRPM" --nowait 2>&1)
|
BUILD_OUTPUT=$(copr-cli build "$COPR_PROJECT" "$SRPM" --nowait 2>&1)
|
||||||
echo "$BUILD_OUTPUT"
|
echo "$BUILD_OUTPUT"
|
||||||
|
|
||||||
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
BUILD_ID=$(echo "$BUILD_OUTPUT" | grep -oP 'Build was added to.*\K[0-9]+' || echo "unknown")
|
||||||
|
|
||||||
if [ "$BUILD_ID" != "unknown" ]; then
|
if [ "$BUILD_ID" != "unknown" ]; then
|
||||||
echo "✅ Build submitted successfully!"
|
echo "✅ Build submitted successfully!"
|
||||||
echo "🔗 https://copr.fedorainfracloud.org/coprs/avengemedia/dms/build/$BUILD_ID/"
|
echo "🔗 https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/build/$BUILD_ID/"
|
||||||
else
|
else
|
||||||
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
echo "⚠️ Could not extract build ID, but upload may have succeeded"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build summary
|
- name: Build summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "### 🎉 DMS Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
PACKAGE="${{ matrix.package }}"
|
||||||
|
COPR_PROJECT="${{ steps.copr_project.outputs.copr_project }}"
|
||||||
|
echo "### 🎉 ${PACKAGE} Stable Build Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "- **Package:** ${PACKAGE}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **Version:** ${{ steps.version.outputs.version }}-${{ steps.version.outputs.release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
echo "- **SRPM:** ${{ steps.build.outputs.srpm_name }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/avengemedia/dms/" >> $GITHUB_STEP_SUMMARY
|
echo "- **Project:** https://copr.fedorainfracloud.org/coprs/${COPR_PROJECT}/" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
echo "Stable release has been built and uploaded to Copr!" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|||||||
533
.github/workflows/run-obs.yml
vendored
533
.github/workflows/run-obs.yml
vendored
@@ -4,107 +4,186 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: 'Package to update (dms, dms-git, or all)'
|
description: "Package to update"
|
||||||
required: false
|
required: true
|
||||||
default: 'all'
|
type: choice
|
||||||
|
options:
|
||||||
|
- dms
|
||||||
|
- dms-git
|
||||||
|
- dms-greeter
|
||||||
|
- all
|
||||||
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
|
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ""
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
name: Check for updates
|
name: Check for updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
has_updates: ${{ steps.check.outputs.has_updates }}
|
||||||
packages: ${{ steps.check.outputs.packages }}
|
packages: ${{ steps.check.outputs.packages }}
|
||||||
version: ${{ steps.check.outputs.version }}
|
version: ${{ steps.check.outputs.version }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Install OSC
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y osc
|
|
||||||
|
|
||||||
mkdir -p ~/.config/osc
|
|
||||||
cat > ~/.config/osc/oscrc << EOF
|
|
||||||
[general]
|
|
||||||
apiurl = https://api.opensuse.org
|
|
||||||
|
|
||||||
[https://api.opensuse.org]
|
|
||||||
user = ${{ secrets.OBS_USERNAME }}
|
|
||||||
pass = ${{ secrets.OBS_PASSWORD }}
|
|
||||||
EOF
|
|
||||||
chmod 600 ~/.config/osc/oscrc
|
|
||||||
|
|
||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
|
env:
|
||||||
|
OBS_USERNAME: ${{ secrets.OBS_USERNAME }}
|
||||||
|
OBS_PASSWORD: ${{ secrets.OBS_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
# Helper function to check dms-git commit
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
check_dms_git() {
|
||||||
|
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:dms-git/dms-git/dms-git.spec" 2>/dev/null || echo "")
|
||||||
|
local OBS_COMMIT=$(echo "$OBS_SPEC" | grep "^Version:" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$OBS_COMMIT" && "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
||||||
|
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 dms-git: New commit $CURRENT_COMMIT (OBS has ${OBS_COMMIT:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to check dms stable tag
|
||||||
|
# Sets LATEST_TAG variable in parent scope if update needed
|
||||||
|
check_dms_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:dms/dms/dms.spec" 2>/dev/null || echo "")
|
||||||
|
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs || echo "")
|
||||||
|
|
||||||
|
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$OBS_VERSION" ]]; then
|
||||||
|
echo "📋 dms: Tag $LATEST_TAG already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 dms: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
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
|
||||||
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||||
|
# Run from tag with no package specified - update both stable packages
|
||||||
|
echo "packages=dms dms-greeter" >> $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
|
||||||
echo "Triggered by tag: $VERSION (always update)"
|
echo "Triggered by tag: $VERSION (always update)"
|
||||||
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
# Scheduled run - check dms-git only
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
echo "Checking if dms-git source has changed..."
|
if check_dms_git; then
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
# Get current commit hash (8 chars to match spec format)
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
# Manual workflow trigger
|
||||||
|
PKG="${{ github.event.inputs.package }}"
|
||||||
|
|
||||||
# Check OBS for last uploaded commit
|
if [[ -n "$REBUILD" ]]; then
|
||||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
# Rebuild requested - always proceed
|
||||||
mkdir -p "$OBS_BASE"
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
# Check each package and build list of those needing updates
|
||||||
osc up -q 2>/dev/null || true
|
PACKAGES_TO_UPDATE=()
|
||||||
|
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||||
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
if check_dms_stable; then
|
||||||
if [[ -f "dms-git.spec" ]]; then
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
if [[ -n "$OBS_COMMIT" ]]; then
|
|
||||||
if [[ "$CURRENT_COMMIT" == "$OBS_COMMIT" ]]; then
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Commit $CURRENT_COMMIT already uploaded to OBS, skipping"
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 New commit detected: $CURRENT_COMMIT (OBS has $OBS_COMMIT)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Could not extract OBS commit, proceeding with update"
|
|
||||||
fi
|
fi
|
||||||
else
|
fi
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
if check_dms_greeter_stable; then
|
||||||
echo "📋 No spec file in OBS, proceeding with update"
|
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||||
|
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ All packages up to date"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
|
if check_dms_git; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms" ]]; then
|
||||||
|
if check_dms_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
|
||||||
|
|
||||||
|
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
|
fi
|
||||||
|
|
||||||
cd "${{ github.workspace }}"
|
|
||||||
else
|
else
|
||||||
|
# Unknown package - proceed anyway
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
echo "📋 First upload to OBS, update needed"
|
echo "Manual trigger: $PKG"
|
||||||
fi
|
fi
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
else
|
else
|
||||||
|
# Fallback - proceed
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
echo "packages=all" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
@@ -113,119 +192,162 @@ jobs:
|
|||||||
name: Upload to OBS
|
name: Upload to OBS
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: |
|
if: needs.check-updates.outputs.has_updates == 'true'
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
# Use check-updates outputs when available
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
# Use version from check-updates job
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by tag: $VERSION"
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using version from check-updates: ${{ needs.check-updates.outputs.version }}"
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
# Scheduled run - dms-git only
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "Triggered by schedule: updating git package"
|
echo "Triggered by schedule: updating git package"
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
# Manual workflow dispatch
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
|
# Determine version for dms stable and dms-greeter using the API
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
echo "Using latest release from API: $LATEST_TAG"
|
||||||
|
else
|
||||||
|
echo "ERROR: Could not fetch latest release from API"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||||
|
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: all (filtered to: ${{ needs.check-updates.outputs.packages }})"
|
||||||
|
else
|
||||||
|
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
- name: Update dms-git spec version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
run: |
|
run: |
|
||||||
# Get commit info for dms-git versioning
|
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
||||||
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
echo "📦 Updating dms-git.spec to version: $NEW_VERSION"
|
||||||
|
|
||||||
# Update version in spec
|
|
||||||
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
sed -i "s/^Version:.*/Version: $NEW_VERSION/" distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
# Add changelog entry
|
# Single changelog entry (git snapshots don't need history)
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
CHANGELOG_ENTRY="* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1\n- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms-git.spec)
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
{
|
||||||
|
echo "$LOCAL_SPEC_HEAD"
|
||||||
|
echo "%changelog"
|
||||||
|
echo "* $DATE_STR Avenge Media <AvengeMedia.US@gmail.com> - ${NEW_VERSION}-1"
|
||||||
|
echo "- Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
|
} > distro/opensuse/dms-git.spec
|
||||||
|
|
||||||
- name: Update Debian dms-git changelog version
|
- name: Update Debian dms-git changelog version
|
||||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||||
run: |
|
run: |
|
||||||
# Get commit info for dms-git versioning
|
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||||
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "0.6.2")
|
BASE_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1 || echo "1.0.2")
|
||||||
|
|
||||||
# Debian version format: 0.6.2+git2256.9162e314
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
||||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
||||||
|
|
||||||
|
# Single changelog entry (git snapshots don't need history)
|
||||||
CHANGELOG_DATE=$(date -R)
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
{
|
||||||
|
echo "dms-git (${NEW_VERSION}db1) nightly; urgency=medium"
|
||||||
|
echo ""
|
||||||
|
echo " * Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)"
|
||||||
|
echo ""
|
||||||
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
- name: Update stable version (dms + dms-greeter)
|
||||||
|
|
||||||
# Get current version from changelog
|
|
||||||
CURRENT_VERSION=$(head -1 "$CHANGELOG_FILE" | sed 's/.*(\([^)]*\)).*/\1/')
|
|
||||||
|
|
||||||
echo "Current Debian version: $CURRENT_VERSION"
|
|
||||||
echo "New version: $NEW_VERSION"
|
|
||||||
|
|
||||||
# Only update if version changed
|
|
||||||
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
|
|
||||||
# Create new changelog entry at top
|
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$TEMP_CHANGELOG" << EOF
|
|
||||||
dms-git ($NEW_VERSION) nightly; urgency=medium
|
|
||||||
|
|
||||||
* Git snapshot (commit $COMMIT_COUNT: $COMMIT_HASH)
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Prepend to existing changelog
|
|
||||||
cat "$CHANGELOG_FILE" >> "$TEMP_CHANGELOG"
|
|
||||||
mv "$TEMP_CHANGELOG" "$CHANGELOG_FILE"
|
|
||||||
|
|
||||||
echo "✓ Updated Debian changelog: $CURRENT_VERSION → $NEW_VERSION"
|
|
||||||
else
|
|
||||||
echo "✓ Debian changelog already at version $NEW_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update dms stable version
|
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
VERSION_NO_V="${VERSION#v}"
|
VERSION_NO_V="${VERSION#v}"
|
||||||
echo "Updating packaging to version $VERSION_NO_V"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||||
# Update openSUSE dms spec (stable only)
|
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
# Update dms spec and changelog when dms is in the upload list
|
||||||
|
if [[ "$PACKAGES" == *"dms"* ]]; then
|
||||||
# Update Debian _service files
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||||
|
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
|
||||||
|
|
||||||
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
|
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||||
|
{
|
||||||
|
echo "$LOCAL_SPEC_HEAD"
|
||||||
|
echo "%changelog"
|
||||||
|
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
||||||
|
echo "- Update to stable $VERSION release"
|
||||||
|
} > distro/opensuse/dms.spec
|
||||||
|
|
||||||
|
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
{
|
||||||
|
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||||
|
echo ""
|
||||||
|
echo " * Update to $VERSION stable release"
|
||||||
|
echo ""
|
||||||
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
|
} > "distro/debian/dms/debian/changelog"
|
||||||
|
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
||||||
|
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 <AvengeMedia.US@gmail.com> $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)
|
||||||
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)
|
||||||
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)
|
||||||
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Install OSC
|
- name: Install OSC
|
||||||
run: |
|
run: |
|
||||||
@@ -244,33 +366,148 @@ jobs:
|
|||||||
chmod 600 ~/.config/osc/oscrc
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
- name: Upload to OBS
|
- name: Upload to OBS
|
||||||
|
id: upload
|
||||||
env:
|
env:
|
||||||
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}
|
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
|
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||||
run: |
|
run: |
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
|
||||||
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
|
echo "✓ No packages need uploading. All up to date!"
|
||||||
|
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
MESSAGE="Automated update from GitHub Actions"
|
MESSAGE="Automated update from GitHub Actions"
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
||||||
|
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
UPLOADED_PACKAGES=()
|
||||||
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
SKIPPED_PACKAGES=()
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
|
||||||
else
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||||
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
# Loop through each package and upload
|
||||||
fi
|
for PKG in $PACKAGES; do
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Uploading $PKG to OBS..."
|
||||||
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
|
echo "🔄 Using rebuild release number: db$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
LOG_FILE=$(mktemp)
|
||||||
|
set +e
|
||||||
|
if [[ "$PKG" == "dms-git" ]]; then
|
||||||
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
|
||||||
|
else
|
||||||
|
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
||||||
|
fi
|
||||||
|
STATUS=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cat "$LOG_FILE"
|
||||||
|
|
||||||
|
if [[ $STATUS -ne 0 ]]; then
|
||||||
|
rm -f "$LOG_FILE"
|
||||||
|
echo "❌ Upload failed for $PKG"
|
||||||
|
exit $STATUS
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
|
||||||
|
echo "ℹ️ $PKG is already up to date. Skipped."
|
||||||
|
SKIPPED_PACKAGES+=("$PKG")
|
||||||
|
else
|
||||||
|
UPLOADED_PACKAGES+=("$PKG")
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$LOG_FILE"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||||
|
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
echo "### OBS Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
|
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
|
||||||
|
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
|
||||||
|
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
|
||||||
|
UPLOADED_COUNT=0
|
||||||
|
SKIPPED_COUNT=0
|
||||||
|
if [[ -n "$UPLOADED_PACKAGES" ]]; then
|
||||||
|
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
|
||||||
|
fi
|
||||||
|
if [[ -n "$SKIPPED_PACKAGES" ]]; then
|
||||||
|
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
|
||||||
|
fi
|
||||||
|
in_list() {
|
||||||
|
local item="$1"
|
||||||
|
local list="$2"
|
||||||
|
[[ " $list " == *" $item "* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${{ job.status }}" == "success" ]]; then
|
||||||
|
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
for PKG in $PACKAGES; do
|
||||||
|
STATUS_ICON="✅"
|
||||||
|
STATUS_TEXT="uploaded"
|
||||||
|
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
|
||||||
|
STATUS_ICON="ℹ️"
|
||||||
|
STATUS_TEXT="up to date (skipped)"
|
||||||
|
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
|
||||||
|
STATUS_ICON="❌"
|
||||||
|
STATUS_TEXT="failed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$PKG" in
|
||||||
|
dms)
|
||||||
|
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
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
|
||||||
|
;;
|
||||||
|
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
|
||||||
|
done
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
|
echo "**Rebuild Number:** db${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
||||||
|
echo "**Version:** ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Monitor build progress on [OBS project page](https://build.opensuse.org/project/show/home:AvengeMedia)." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
|
||||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|||||||
292
.github/workflows/run-ppa.yml
vendored
292
.github/workflows/run-ppa.yml
vendored
@@ -4,15 +4,15 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: 'Package to upload (dms, dms-git, dms-greeter, or all)'
|
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||||
required: false
|
required: false
|
||||||
default: 'dms-git'
|
default: "dms-git"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
|
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||||
required: false
|
required: false
|
||||||
default: ''
|
default: ""
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
|
- cron: "0 2,5,14,17,20,23 * * *" # 9am, 12pm, 3pm, 6pm, 9pm, 12am EST (UTC times shown)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-updates:
|
check-updates:
|
||||||
@@ -32,41 +32,113 @@ jobs:
|
|||||||
- name: Check for updates
|
- name: Check for updates
|
||||||
id: check
|
id: check
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
# Helper function to check dms-git commit
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
check_dms_git() {
|
||||||
echo "Checking if dms-git source has changed..."
|
local CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
||||||
|
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/dms-git?ws.op=getPublishedSources&source_name=dms-git&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
|
local PPA_COMMIT=$(echo "$PPA_VERSION" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
||||||
|
|
||||||
# Get current commit hash (8 chars to match changelog format)
|
if [[ -n "$PPA_COMMIT" && "$CURRENT_COMMIT" == "$PPA_COMMIT" ]]; then
|
||||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
echo "📋 dms-git: Commit $CURRENT_COMMIT already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
# Extract commit hash from changelog
|
|
||||||
# Format: dms-git (0.6.2+git2264.c5c5ce84) questing; urgency=medium
|
|
||||||
CHANGELOG_FILE="distro/ubuntu/dms-git/debian/changelog"
|
|
||||||
|
|
||||||
if [[ -f "$CHANGELOG_FILE" ]]; then
|
|
||||||
CHANGELOG_COMMIT=$(head -1 "$CHANGELOG_FILE" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
|
||||||
|
|
||||||
if [[ -n "$CHANGELOG_COMMIT" ]]; then
|
|
||||||
if [[ "$CURRENT_COMMIT" == "$CHANGELOG_COMMIT" ]]; then
|
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Commit $CURRENT_COMMIT already in changelog, skipping upload"
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 New commit detected: $CURRENT_COMMIT (changelog has $CHANGELOG_COMMIT)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Could not extract commit from changelog, proceeding with upload"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "📋 dms-git: New commit $CURRENT_COMMIT (PPA has ${PPA_COMMIT:-none})"
|
||||||
echo "📋 No changelog file found, proceeding with upload"
|
return 0 # Update needed
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to check stable package tag
|
||||||
|
check_stable_package() {
|
||||||
|
local PKG="$1"
|
||||||
|
local PPA_NAME="$2"
|
||||||
|
# Use git ls-remote to find the latest tag, sorted by version (descending)
|
||||||
|
local LATEST_TAG=$(git ls-remote --tags --refs --sort='-v:refname' https://github.com/AvengeMedia/DankMaterialShell.git | head -n1 | awk -F/ '{print $NF}' | sed 's/^v//')
|
||||||
|
local PPA_VERSION=$(curl -s "https://api.launchpad.net/1.0/~avengemedia/+archive/ubuntu/$PPA_NAME?ws.op=getPublishedSources&source_name=$PKG&status=Published" | grep -oP '"source_package_version":\s*"\K[^"]+' | head -1 || echo "")
|
||||||
|
local PPA_BASE_VERSION=$(echo "$PPA_VERSION" | sed 's/ppa[0-9]*$//')
|
||||||
|
|
||||||
|
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "$PPA_BASE_VERSION" ]]; then
|
||||||
|
echo "📋 $PKG: Tag $LATEST_TAG already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 $PKG: New tag ${LATEST_TAG:-unknown} (PPA has ${PPA_BASE_VERSION:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main logic
|
||||||
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
|
# Scheduled run - check dms-git only
|
||||||
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
|
if check_dms_git; then
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
# Manual workflow trigger
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
PKG="${{ github.event.inputs.package }}"
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
|
if [[ -n "$REBUILD" ]]; then
|
||||||
|
# Rebuild requested - always proceed
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "🔄 Manual rebuild requested: $PKG (ppa$REBUILD)"
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
|
# Check each package and build list of those needing updates
|
||||||
|
PACKAGES_TO_UPDATE=()
|
||||||
|
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||||
|
check_stable_package "dms" "dms" && PACKAGES_TO_UPDATE+=("dms")
|
||||||
|
check_stable_package "dms-greeter" "danklinux" && PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||||
|
|
||||||
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ Packages to update: ${PACKAGES_TO_UPDATE[*]}"
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
echo "✓ All packages up to date"
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
|
if check_dms_git; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms" ]]; then
|
||||||
|
if check_stable_package "dms" "dms"; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||||
|
if check_stable_package "dms-greeter" "danklinux"; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
# Unknown package - proceed anyway
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
echo "Manual trigger: $PKG"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
|
# Fallback
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
@@ -75,21 +147,19 @@ jobs:
|
|||||||
name: Upload to PPA
|
name: Upload to PPA
|
||||||
needs: check-updates
|
needs: check-updates
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: |
|
if: needs.check-updates.outputs.has_updates == 'true'
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -102,7 +172,7 @@ jobs:
|
|||||||
build-essential \
|
build-essential \
|
||||||
fakeroot \
|
fakeroot \
|
||||||
dpkg-dev
|
dpkg-dev
|
||||||
|
|
||||||
- name: Configure GPG
|
- name: Configure GPG
|
||||||
env:
|
env:
|
||||||
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||||
@@ -110,79 +180,101 @@ jobs:
|
|||||||
echo "$GPG_KEY" | gpg --import
|
echo "$GPG_KEY" | gpg --import
|
||||||
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
|
||||||
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Determine packages to upload
|
- name: Determine packages to upload
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
|
# Use packages determined by check-updates job
|
||||||
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by schedule: uploading git package"
|
echo "Triggered by schedule: uploading git package"
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
echo "Manual trigger: ${{ needs.check-updates.outputs.packages }}"
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
else
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload to PPA
|
- name: Upload to PPA
|
||||||
env:
|
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
|
||||||
run: |
|
run: |
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
|
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
|
echo "✓ No packages need uploading. All up to date!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Export REBUILD_RELEASE so ppa-build.sh can use it
|
||||||
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
|
export REBUILD_RELEASE
|
||||||
|
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||||
|
# Loop through each package and upload
|
||||||
|
for PKG in $PACKAGES; do
|
||||||
|
# Map package to PPA name
|
||||||
|
case "$PKG" in
|
||||||
|
dms)
|
||||||
|
PPA_NAME="dms"
|
||||||
|
;;
|
||||||
|
dms-git)
|
||||||
|
PPA_NAME="dms-git"
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
PPA_NAME="danklinux"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "⚠️ Unknown package: $PKG, skipping"
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
echo "Uploading dms to PPA..."
|
echo "Uploading $PKG to PPA $PPA_NAME..."
|
||||||
if [ -n "$REBUILD_RELEASE" ]; then
|
if [[ -n "$REBUILD_RELEASE" ]]; then
|
||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms" dms questing
|
bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
||||||
|
done
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-git to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-git" dms-git questing
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-greeter to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/dms-greeter" danklinux questing
|
|
||||||
else
|
|
||||||
PPA_NAME="$PACKAGES"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading $PACKAGES to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "distro/ubuntu/$PACKAGES" "$PPA_NAME" questing
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|
||||||
if [[ "${{ needs.check-updates.outputs.has_updates }}" == "false" ]]; then
|
- name: Summary
|
||||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
if: always()
|
||||||
fi
|
run: |
|
||||||
|
echo "### PPA Package Upload Summary" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
echo "**Status:** ✅ All packages up to date (no uploads needed)" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
elif [[ "$PACKAGES" == "dms" ]]; then
|
echo "All packages are current. Run will complete successfully." >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
else
|
||||||
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
for PKG in $PACKAGES; do
|
||||||
|
case "$PKG" in
|
||||||
|
dms)
|
||||||
|
echo "- ✅ **dms** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
dms-git)
|
||||||
|
echo "- ✅ **dms-git** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
echo "- ✅ **dms-greeter** → [View builds](https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
|
echo "**Rebuild Number:** ppa${{ github.event.inputs.rebuild_release }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
|
|
||||||
|
|||||||
4
.github/workflows/update-vendor-hash.yml
vendored
4
.github/workflows/update-vendor-hash.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
echo "Attempting nix build to get new vendorHash..."
|
echo "Attempting nix build to get new vendorHash..."
|
||||||
if output=$(nix build .#dmsCli 2>&1); then
|
if output=$(nix build .#dms-shell 2>&1); then
|
||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
sed -i "s|vendorHash = \"$current_hash\"|vendorHash = \"$new_hash\"|" flake.nix
|
||||||
echo "Verifying build with new vendorHash..."
|
echo "Verifying build with new vendorHash..."
|
||||||
nix build .#dmsCli
|
nix build .#dms-shell
|
||||||
echo "vendorHash updated successfully!"
|
echo "vendorHash updated successfully!"
|
||||||
|
|
||||||
- name: Commit and push vendorHash update
|
- name: Commit and push vendorHash update
|
||||||
|
|||||||
13
.gitignore
vendored
13
.gitignore
vendored
@@ -56,6 +56,8 @@ UNUSED
|
|||||||
|
|
||||||
CLAUDE-activeContext.md
|
CLAUDE-activeContext.md
|
||||||
CLAUDE-temp.md
|
CLAUDE-temp.md
|
||||||
|
AGENTS-activeContext.md
|
||||||
|
AGENTS-temp.md
|
||||||
|
|
||||||
# Auto-generated theme files
|
# Auto-generated theme files
|
||||||
*.generated.*
|
*.generated.*
|
||||||
@@ -96,20 +98,17 @@ go.work
|
|||||||
go.work.sum
|
go.work.sum
|
||||||
|
|
||||||
# env file
|
# env file
|
||||||
.env
|
.env*
|
||||||
|
|
||||||
# Editor/IDE
|
# Editor/IDE
|
||||||
# .idea/
|
# .idea/
|
||||||
# .vscode/
|
# .vscode/
|
||||||
|
vim/
|
||||||
|
|
||||||
bin/
|
bin/
|
||||||
|
|
||||||
# Extracted source trees in Ubuntu package directories
|
|
||||||
distro/ubuntu/*/dms-git-repo/
|
|
||||||
distro/ubuntu/*/DankMaterialShell-*/
|
|
||||||
distro/ubuntu/danklinux/*/dsearch-*/
|
|
||||||
distro/ubuntu/danklinux/*/dgop-*/
|
|
||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv/
|
.direnv/
|
||||||
|
quickshell/dms-plugins
|
||||||
|
__pycache__
|
||||||
|
|||||||
22
.pre-commit-config.yaml
Normal file
22
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v6.0.0
|
||||||
|
hooks:
|
||||||
|
- id: trailing-whitespace
|
||||||
|
- id: check-yaml
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: shellcheck
|
||||||
|
name: shellcheck
|
||||||
|
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
|
||||||
|
language: system
|
||||||
|
types: [shell]
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: go-mod-tidy
|
||||||
|
name: go mod tidy
|
||||||
|
entry: bash -c 'cd core && go mod tidy'
|
||||||
|
language: system
|
||||||
|
files: ^core/.*\.(go|mod|sum)$
|
||||||
|
pass_filenames: false
|
||||||
33
CHANGELOG.MD
Normal file
33
CHANGELOG.MD
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
|
# 1.4.0
|
||||||
|
|
||||||
|
- Overhauled system monitor, graphs, styling
|
||||||
|
- dbus API for plugins, KDEConnect
|
||||||
|
- new dank16 algorithm
|
||||||
|
- launcher actions, customize env, args, name, icon
|
||||||
|
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||||
|
- dock on bar
|
||||||
|
- window rule manager, with IPC - #TODO verify RTL layout (niri only)
|
||||||
|
|
||||||
|
# 1.2.0
|
||||||
|
|
||||||
|
- Added clipboard and clipboard history integration
|
||||||
|
- Added swipe to dismiss notification popups and from center
|
||||||
|
- Added paste from clipboard history view - requires wtype
|
||||||
|
- Optimize surface damage of OSD & Toast
|
||||||
|
- Add monitor configurator (niri, Hyprland, MangoWC)
|
||||||
|
- **BREAKING** ghostty theme changed to ~/.config/ghostty/themes/danktheme
|
||||||
|
- requires intervention and doc update
|
||||||
|
- Added desktop widget plugins
|
||||||
|
- dev guidance available
|
||||||
|
- builtin clock & dgop widgets
|
||||||
|
- new IPC targets
|
||||||
|
- Initial RTL support/i18n
|
||||||
|
- Theme registry
|
||||||
|
- Notification persistence & history
|
||||||
|
- **BREAKING** vscode theme needs re-installed
|
||||||
|
- dms doctor cmd
|
||||||
|
- niri/hypr/mango gaps/window/border overrides
|
||||||
|
- settings search
|
||||||
|
- notification display ops on lock screen
|
||||||
@@ -6,10 +6,10 @@ To contribute fork this repository, make your changes, and open a pull request.
|
|||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
Enable pre-commit hooks to catch CI failures before pushing:
|
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git config core.hooksPath .githooks
|
prek install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Nix Development Shell
|
### Nix Development Shell
|
||||||
@@ -21,7 +21,8 @@ nix develop
|
|||||||
```
|
```
|
||||||
|
|
||||||
This will provide:
|
This will provide:
|
||||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
|
||||||
|
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
- Quickshell and required QML packages
|
- Quickshell and required QML packages
|
||||||
- Properly configured QML2_IMPORT_PATH
|
- Properly configured QML2_IMPORT_PATH
|
||||||
|
|
||||||
@@ -36,10 +37,43 @@ This is a monorepo, the easiest thing to do is to open an editor in either `quic
|
|||||||
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
|
||||||
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
|
||||||
|
|
||||||
|
**Note:** Paths may vary by distribution. Below are examples for Arch Linux and Fedora.
|
||||||
|
|
||||||
|
**Arch Linux:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
"[qml]": {
|
||||||
|
"editor.defaultFormatter": "qt-project.qmlls",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
"qt-qml.doNotAskForQmllsDownload": true,
|
"qt-qml.doNotAskForQmllsDownload": true,
|
||||||
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
|
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls",
|
||||||
|
"qt-core.additionalQtPaths": [
|
||||||
|
{
|
||||||
|
"name": "Qt-6.x-linux-g++",
|
||||||
|
"path": "/usr/bin/qmake"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fedora:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"[qml]": {
|
||||||
|
"editor.defaultFormatter": "qt-project.qmlls",
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"qt-qml.doNotAskForQmllsDownload": true,
|
||||||
|
"qt-qml.qmlls.customExePath": "/usr/bin/qmlls",
|
||||||
|
"qt-core.additionalQtPaths": [
|
||||||
|
{
|
||||||
|
"name": "Qt-6.x-Fedora-linux-g++",
|
||||||
|
"path": "/usr/bin/qmake6"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -54,6 +88,20 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
5. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
|
### I18n/Localization
|
||||||
|
|
||||||
|
When adding user-facing strings, ensure they are wrapped in `I18n.tr()` with context, for example.
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: I18n.tr("Hello World", "<This is context for the translators, example> Hello world greeting that appears on the lock screen")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
### 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)
|
||||||
|
|||||||
5
Makefile
5
Makefile
@@ -43,7 +43,6 @@ install-shell:
|
|||||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||||
@$(MAKE) --no-print-directory -C $(CORE_DIR) print-version > $(SHELL_INSTALL_DIR)/VERSION
|
|
||||||
@echo "Shell files installed"
|
@echo "Shell files installed"
|
||||||
|
|
||||||
install-completions:
|
install-completions:
|
||||||
@@ -59,10 +58,10 @@ install-completions:
|
|||||||
install-systemd:
|
install-systemd:
|
||||||
@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):$(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|$(INSTALL_DIR)/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):$(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"
|
||||||
|
|
||||||
install-icon:
|
install-icon:
|
||||||
|
|||||||
17
README.md
17
README.md
@@ -5,21 +5,21 @@
|
|||||||
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
<img src="assets/danklogo.svg" alt="DankMaterialShell" width="200">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
### A modern desktop shell for Wayland
|
### A modern desktop shell for Wayland
|
||||||
|
|
||||||
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||||
|
|
||||||
[](https://danklinux.com/docs)
|
[](https://danklinux.com/docs)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
[](https://github.com/AvengeMedia/DankMaterialShell/stargazers)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
[](https://github.com/AvengeMedia/DankMaterialShell/blob/master/LICENSE)
|
||||||
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
[](https://github.com/AvengeMedia/DankMaterialShell/releases)
|
||||||
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
[](https://aur.archlinux.org/packages/dms-shell-bin)
|
||||||
[)](https://aur.archlinux.org/packages/dms-shell-git)
|
[>)](https://aur.archlinux.org/packages/dms-shell-git)
|
||||||
[](https://ko-fi.com/danklinux)
|
[](https://ko-fi.com/danklinux)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
DankMaterialShell is a complete desktop shell for [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [Sway](https://swaywm.org), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), [Miracle WM](https://github.com/miracle-wm-org/miracle-wm), and other Wayland compositors. It replaces waybar, swaylock, swayidle, mako, fuzzel, polkit, and everything else you'd normally stitch together to make a desktop.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
|||||||
|
|
||||||
## Supported Compositors
|
## Supported Compositors
|
||||||
|
|
||||||
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), and [labwc](https://labwc.github.io/) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
Works best with [niri](https://github.com/YaLTeR/niri), [Hyprland](https://hyprland.org/), [Sway](https://swaywm.org/), [MangoWC](https://github.com/DreamMaoMao/mangowc), [labwc](https://labwc.github.io/), [Scroll](https://github.com/dawsers/scroll), and [Miracle WM](https://github.com/miracle-wm-org/miracle-wm) with full workspace switching, overview integration, and monitor management. Other Wayland compositors work with reduced features.
|
||||||
|
|
||||||
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
[Compositor configuration guide](https://danklinux.com/docs/dankmaterialshell/compositors)
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ dms plugins search # Browse plugin registry
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **Website:** [danklinux.com](https://danklinux.com)
|
- **Website:** [danklinux.com](https://danklinux.com)
|
||||||
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs)
|
- **Docs:** [danklinux.com/docs](https://danklinux.com/docs/)
|
||||||
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
- **Theming:** [Application themes](https://danklinux.com/docs/dankmaterialshell/application-themes) | [Custom themes](https://danklinux.com/docs/dankmaterialshell/custom-themes)
|
||||||
- **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)
|
||||||
@@ -143,6 +143,7 @@ See component-specific documentation:
|
|||||||
### Building from Source
|
### Building from Source
|
||||||
|
|
||||||
**Core + Dankinstall:**
|
**Core + Dankinstall:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd core
|
cd core
|
||||||
make # Build dms CLI
|
make # Build dms CLI
|
||||||
@@ -150,17 +151,19 @@ make dankinstall # Build installer
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Shell:**
|
**Shell:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
quickshell -p quickshell/
|
quickshell -p quickshell/
|
||||||
```
|
```
|
||||||
|
|
||||||
**NixOS:**
|
**NixOS:**
|
||||||
|
|
||||||
```nix
|
```nix
|
||||||
{
|
{
|
||||||
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
inputs.dms.url = "github:AvengeMedia/DankMaterialShell";
|
||||||
|
|
||||||
# Use in home-manager or NixOS configuration
|
# Use in home-manager or NixOS configuration
|
||||||
imports = [ inputs.dms.homeModules.dankMaterialShell.default ];
|
imports = [ inputs.dms.homeModules.dank-material-shell ];
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
indentation = "FourSpaces"
|
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=DMS Application Picker
|
Name=DMS
|
||||||
Comment=Select an application to open links and files
|
Comment=Select an application to open links and files
|
||||||
Exec=dms open %u
|
Exec=dms open %u
|
||||||
Icon=danklogo
|
Icon=danklogo
|
||||||
Terminal=false
|
Terminal=false
|
||||||
NoDisplay=true
|
NoDisplay=true
|
||||||
MimeType=x-scheme-handler/http;x-scheme-handler/https;text/html;application/xhtml+xml;
|
MimeType=x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/dms;text/html;application/xhtml+xml;
|
||||||
Categories=Utility;
|
Categories=Utility;
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ 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=/usr/bin/pkill -USR1 -x dms
|
||||||
Restart=always
|
Restart=on-failure
|
||||||
RestartSec=1.23
|
RestartSec=1.23
|
||||||
TimeoutStopSec=10
|
TimeoutStopSec=10
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=graphical-session.target
|
WantedBy=graphical-session.target
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ linters:
|
|||||||
- (*os.Process).Signal
|
- (*os.Process).Signal
|
||||||
- (*os.Process).Kill
|
- (*os.Process).Kill
|
||||||
- syscall.Kill
|
- syscall.Kill
|
||||||
|
# Seek on memfd (reset position before passing fd)
|
||||||
|
- syscall.Seek
|
||||||
# DBus cleanup
|
# DBus cleanup
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
- (*github.com/godbus/dbus/v5.Conn).RemoveMatchSignal
|
||||||
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
- (*github.com/godbus/dbus/v5.Conn).RemoveSignal
|
||||||
@@ -100,7 +102,11 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- ineffassign
|
- ineffassign
|
||||||
path: internal/proto/
|
path: internal/proto/
|
||||||
# binary.Write to bytes.Buffer can't fail
|
# binary.Write/Read to bytes.Buffer can't fail
|
||||||
- linters:
|
- linters:
|
||||||
- errcheck
|
- errcheck
|
||||||
text: "Error return value of `binary\\.Write` is not checked"
|
text: "Error return value of `binary\\.(Write|Read)` is not checked"
|
||||||
|
# bytes.Reader.Read can't fail (reads from memory)
|
||||||
|
- linters:
|
||||||
|
- errcheck
|
||||||
|
text: "Error return value of `buf\\.Read` is not checked"
|
||||||
|
|||||||
@@ -56,3 +56,21 @@ packages:
|
|||||||
outpkg: mocks_version
|
outpkg: mocks_version
|
||||||
interfaces:
|
interfaces:
|
||||||
VersionFetcher:
|
VersionFetcher:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/wlcontext"
|
||||||
|
outpkg: mocks_wlcontext
|
||||||
|
interfaces:
|
||||||
|
WaylandContext:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/wlclient"
|
||||||
|
outpkg: mocks_wlclient
|
||||||
|
interfaces:
|
||||||
|
WaylandDisplay:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/utils:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/utils"
|
||||||
|
outpkg: mocks_utils
|
||||||
|
interfaces:
|
||||||
|
AppChecker:
|
||||||
|
|||||||
16
core/.pre-commit-config.yaml
Normal file
16
core/.pre-commit-config.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
|
rev: v2.9.0
|
||||||
|
hooks:
|
||||||
|
- id: golangci-lint-fmt
|
||||||
|
require_serial: true
|
||||||
|
- id: golangci-lint-full
|
||||||
|
- id: golangci-lint-config-verify
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: go-test
|
||||||
|
name: go test
|
||||||
|
entry: go test ./...
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [go]
|
||||||
@@ -14,34 +14,63 @@ Distribution-aware installer with TUI for deploying DMS and compositor configura
|
|||||||
|
|
||||||
## System Integration
|
## System Integration
|
||||||
|
|
||||||
**Wayland Protocols**
|
### Wayland Protocols (Client)
|
||||||
- `wlr-gamma-control-unstable-v1` - Night mode and gamma control
|
|
||||||
- `wlr-screencopy-unstable-v1` - Screen capture for color picker
|
|
||||||
- `wlr-layer-shell-unstable-v1` - Overlay surfaces for color picker
|
|
||||||
- `wp-viewporter` - Fractional scaling support
|
|
||||||
- `dwl-ipc-unstable-v2` - dwl/MangoWC workspace integration
|
|
||||||
- `ext-workspace-v1` - Workspace protocol support
|
|
||||||
- `wlr-output-management-unstable-v1` - Display configuration
|
|
||||||
|
|
||||||
**DBus Interfaces**
|
All Wayland protocols are consumed as a client - connecting to the compositor.
|
||||||
- NetworkManager/iwd - Network management
|
|
||||||
- logind - Session control and inhibit locks
|
|
||||||
- accountsservice - User account information
|
|
||||||
- CUPS - Printer management
|
|
||||||
- Custom IPC via unix socket (JSON API)
|
|
||||||
|
|
||||||
**Hardware Control**
|
| Protocol | Purpose |
|
||||||
- DDC/CI protocol - External monitor brightness control (like `ddcutil`)
|
| ----------------------------------------- | ----------------------------------------------------------- |
|
||||||
- Backlight control - Internal display brightness via `login1` or sysfs
|
| `wlr-gamma-control-unstable-v1` | Night mode color temperature control |
|
||||||
- LED control - Keyboard/device LED management
|
| `wlr-screencopy-unstable-v1` | Screen capture for color picker/screenshot |
|
||||||
- evdev input monitoring - Keyboard state tracking (caps lock, etc.)
|
| `wlr-layer-shell-unstable-v1` | Overlay surfaces for color picker UI/screenshot |
|
||||||
|
| `wlr-output-management-unstable-v1` | Display configuration |
|
||||||
|
| `wlr-output-power-management-unstable-v1` | DPMS on/off CLI |
|
||||||
|
| `wp-viewporter` | Fractional scaling support (color picker/screenshot UIs) |
|
||||||
|
| `keyboard-shortcuts-inhibit-unstable-v1` | Inhibit compositor shortcuts during color picker/screenshot |
|
||||||
|
| `ext-data-control-v1` | Clipboard history and persistence |
|
||||||
|
| `ext-workspace-v1` | Workspace integration |
|
||||||
|
| `dwl-ipc-unstable-v2` | dwl/MangoWC IPC for tags, outputs, etc. |
|
||||||
|
|
||||||
|
### DBus Interfaces
|
||||||
|
|
||||||
|
**Client (consuming external services):**
|
||||||
|
|
||||||
|
| Interface | Purpose |
|
||||||
|
| -------------------------------- | --------------------------------------------- |
|
||||||
|
| `org.bluez` | Bluetooth management with pairing agent |
|
||||||
|
| `org.freedesktop.NetworkManager` | Network management |
|
||||||
|
| `net.connman.iwd` | iwd Wi-Fi backend |
|
||||||
|
| `org.freedesktop.network1` | systemd-networkd integration |
|
||||||
|
| `org.freedesktop.login1` | Session control, sleep inhibitors, brightness |
|
||||||
|
| `org.freedesktop.Accounts` | User account information |
|
||||||
|
| `org.freedesktop.portal.Desktop` | Desktop appearance settings (color scheme) |
|
||||||
|
| CUPS via IPP + D-Bus | Printer management with job notifications |
|
||||||
|
|
||||||
|
**Server (implementing interfaces):**
|
||||||
|
|
||||||
|
| Interface | Purpose |
|
||||||
|
| ----------------------------- | -------------------------------------- |
|
||||||
|
| `org.freedesktop.ScreenSaver` | Screensaver inhibit for video playback |
|
||||||
|
|
||||||
|
Custom IPC via unix socket (JSON API) for shell communication.
|
||||||
|
|
||||||
|
### Hardware Control
|
||||||
|
|
||||||
|
| Subsystem | Method | Purpose |
|
||||||
|
| --------- | ------------------- | ---------------------------------- |
|
||||||
|
| DDC/CI | I2C direct | External monitor brightness |
|
||||||
|
| Backlight | logind or sysfs | Internal display brightness |
|
||||||
|
| evdev | `/dev/input/event*` | Keyboard state (caps lock LED) |
|
||||||
|
| udev | netlink monitor | Backlight device updates (for OSD) |
|
||||||
|
|
||||||
|
### Plugin System
|
||||||
|
|
||||||
**Plugin System**
|
|
||||||
- Plugin registry integration
|
- Plugin registry integration
|
||||||
- Plugin lifecycle management
|
- Plugin lifecycle management
|
||||||
- Settings persistence
|
- Settings persistence
|
||||||
|
|
||||||
## CLI Commands
|
## CLI Commands
|
||||||
|
|
||||||
- `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.)
|
||||||
@@ -67,9 +96,10 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires Go 1.24+
|
Requires Go 1.25+
|
||||||
|
|
||||||
**Development build:**
|
**Development build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make # Build dms CLI
|
make # Build dms CLI
|
||||||
make dankinstall # Build installer
|
make dankinstall # Build installer
|
||||||
@@ -77,6 +107,7 @@ make test # Run tests
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Distribution build:**
|
**Distribution build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make dist # Build without update/greeter features
|
make dist # Build without update/greeter features
|
||||||
```
|
```
|
||||||
@@ -84,6 +115,7 @@ make dist # Build without update/greeter features
|
|||||||
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
Produces `bin/dms-linux-amd64` and `bin/dms-linux-arm64`
|
||||||
|
|
||||||
**Installation:**
|
**Installation:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo make install # Install to /usr/local/bin/dms
|
sudo make install # Install to /usr/local/bin/dms
|
||||||
```
|
```
|
||||||
@@ -91,6 +123,7 @@ sudo make install # Install to /usr/local/bin/dms
|
|||||||
## Development
|
## Development
|
||||||
|
|
||||||
**Setup pre-commit hooks:**
|
**Setup pre-commit hooks:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git config core.hooksPath .githooks
|
git config core.hooksPath .githooks
|
||||||
```
|
```
|
||||||
@@ -98,6 +131,7 @@ git config core.hooksPath .githooks
|
|||||||
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
This runs gofmt, golangci-lint, tests, and builds before each commit when `core/` files are staged.
|
||||||
|
|
||||||
**Regenerating Wayland Protocol Bindings:**
|
**Regenerating Wayland Protocol Bindings:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
go install github.com/rajveermalviya/go-wayland/cmd/go-wayland-scanner@latest
|
||||||
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
||||||
@@ -105,6 +139,7 @@ go-wayland-scanner -i internal/proto/xml/wlr-gamma-control-unstable-v1.xml \
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Module Structure:**
|
**Module Structure:**
|
||||||
|
|
||||||
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
- `cmd/` - Binary entrypoints (dms, dankinstall)
|
||||||
- `internal/distros/` - Distribution-specific installation logic
|
- `internal/distros/` - Distribution-specific installation logic
|
||||||
- `internal/proto/` - Wayland protocol bindings
|
- `internal/proto/` - Wayland protocol bindings
|
||||||
@@ -139,4 +174,4 @@ 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.
|
||||||
|
|
||||||
See installer output for distribution-specific details during installation.
|
See installer output for distribution-specific details during installation.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<rect x="0" y="29" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="0" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="20" y="29" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="20" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="0" y="37" width="24" height="8" fill="#CCBEFF"/>
|
<rect x="0" y="37" width="24" height="8" fill="#CCBEFF"/>
|
||||||
|
|
||||||
<!-- A -->
|
<!-- A -->
|
||||||
<rect x="36" y="5" width="20" height="8" fill="#CCBEFF"/>
|
<rect x="36" y="5" width="20" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="32" y="13" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="32" y="13" width="8" height="8" fill="#CCBEFF"/>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
<rect x="52" y="29" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="52" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="32" y="37" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="32" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="52" y="37" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="52" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||||
|
|
||||||
<!-- N -->
|
<!-- N -->
|
||||||
<rect x="64" y="5" width="12" height="8" fill="#CCBEFF"/>
|
<rect x="64" y="5" width="12" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="92" y="5" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="92" y="5" width="8" height="8" fill="#CCBEFF"/>
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<rect x="92" y="29" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="92" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="64" y="37" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="64" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="84" y="37" width="16" height="8" fill="#CCBEFF"/>
|
<rect x="84" y="37" width="16" height="8" fill="#CCBEFF"/>
|
||||||
|
|
||||||
<!-- K -->
|
<!-- K -->
|
||||||
<rect x="104" y="5" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="104" y="5" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="124" y="5" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="124" y="5" width="8" height="8" fill="#CCBEFF"/>
|
||||||
@@ -43,4 +43,4 @@
|
|||||||
<rect x="120" y="29" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="120" y="29" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="104" y="37" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="104" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||||
<rect x="124" y="37" width="8" height="8" fill="#CCBEFF"/>
|
<rect x="124" y="37" width="8" height="8" fill="#CCBEFF"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -12,6 +12,11 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if os.Getuid() == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "Error: dankinstall must not be run as root")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
fileLogger, err := log.NewFileLogger()
|
fileLogger, err := log.NewFileLogger()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
fmt.Printf("Warning: Failed to create log file: %v\n", err)
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func runBrightnessList(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
fmt.Printf("%-*s %-12s %-*s %s\n", idPad, "Device", "Class", namePad, "Name", "Brightness")
|
||||||
|
|
||||||
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
sepLen := idPad + 2 + 12 + 2 + namePad + 2 + 15
|
||||||
for i := 0; i < sepLen; i++ {
|
for range sepLen {
|
||||||
fmt.Print("─")
|
fmt.Print("─")
|
||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -211,45 +211,13 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
exponential, _ := cmd.Flags().GetBool("exponential")
|
exponential, _ := cmd.Flags().GetBool("exponential")
|
||||||
exponent, _ := cmd.Flags().GetFloat64("exponent")
|
exponent, _ := cmd.Flags().GetFloat64("exponent")
|
||||||
|
|
||||||
// For backlight/leds devices, try logind backend first (requires D-Bus connection)
|
|
||||||
parts := strings.SplitN(deviceID, ":", 2)
|
parts := strings.SplitN(deviceID, ":", 2)
|
||||||
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
|
if len(parts) == 2 && (parts[0] == "backlight" || parts[0] == "leds") {
|
||||||
subsystem := parts[0]
|
if ok := tryLogindBrightness(parts[0], parts[1], deviceID, percent, exponential, exponent); ok {
|
||||||
name := parts[1]
|
return
|
||||||
|
|
||||||
// Initialize backends needed for logind approach
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("NewSysfsBackend failed: %v", err)
|
|
||||||
} else {
|
|
||||||
logind, err := brightness.NewLogindBackend()
|
|
||||||
if err != nil {
|
|
||||||
log.Debugf("NewLogindBackend failed: %v", err)
|
|
||||||
} else {
|
|
||||||
defer logind.Close()
|
|
||||||
|
|
||||||
// Get device info to convert percent to value
|
|
||||||
dev, err := sysfs.GetDevice(deviceID)
|
|
||||||
if err == nil {
|
|
||||||
// Calculate hardware value using the same logic as Manager.setViaSysfsWithLogind
|
|
||||||
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
|
|
||||||
|
|
||||||
// Call logind with hardware value
|
|
||||||
if err := logind.SetBrightness(subsystem, name, uint32(value)); err == nil {
|
|
||||||
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
|
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
log.Debugf("logind.SetBrightness failed: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to direct sysfs (requires write permissions)
|
|
||||||
sysfs, err := brightness.NewSysfsBackend()
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
|
if err := sysfs.SetBrightnessWithExponent(deviceID, percent, exponential, exponent); err == nil {
|
||||||
@@ -280,6 +248,37 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
log.Fatalf("Failed to set brightness for device: %s", deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func tryLogindBrightness(subsystem, name, deviceID string, percent int, exponential bool, exponent float64) bool {
|
||||||
|
sysfs, err := brightness.NewSysfsBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("NewSysfsBackend failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
logind, err := brightness.NewLogindBackend()
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("NewLogindBackend failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer logind.Close()
|
||||||
|
|
||||||
|
dev, err := sysfs.GetDevice(deviceID)
|
||||||
|
if err != nil {
|
||||||
|
log.Debugf("sysfs.GetDeviceByID failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
value := sysfs.PercentToValueWithExponent(percent, dev, exponential, exponent)
|
||||||
|
if err := logind.SetBrightness(subsystem, name, uint32(value)); err != nil {
|
||||||
|
log.Debugf("logind.SetBrightness failed: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("set %s to %d%% (%d) via logind", deviceID, percent, value)
|
||||||
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func getBrightnessDevices(includeDDC bool) []string {
|
func getBrightnessDevices(includeDDC bool) []string {
|
||||||
allDevices := getAllBrightnessDevices(includeDDC)
|
allDevices := getAllBrightnessDevices(includeDDC)
|
||||||
|
|
||||||
|
|||||||
300
core/cmd/dms/commands_chroma.go
Normal file
300
core/cmd/dms/commands_chroma.go
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/v2"
|
||||||
|
"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/yuin/goldmark"
|
||||||
|
highlighting "github.com/yuin/goldmark-highlighting/v2"
|
||||||
|
"github.com/yuin/goldmark/extension"
|
||||||
|
"github.com/yuin/goldmark/parser"
|
||||||
|
ghtml "github.com/yuin/goldmark/renderer/html"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
chromaLanguage string
|
||||||
|
chromaStyle string
|
||||||
|
chromaInline bool
|
||||||
|
chromaMarkdown bool
|
||||||
|
chromaLineNumbers bool
|
||||||
|
|
||||||
|
// Caching layer for performance
|
||||||
|
lexerCache = make(map[string]chroma.Lexer)
|
||||||
|
styleCache = make(map[string]*chroma.Style)
|
||||||
|
formatterCache = make(map[string]*html.Formatter)
|
||||||
|
cacheMutex sync.RWMutex
|
||||||
|
maxFileSize = int64(5 * 1024 * 1024) // 5MB default
|
||||||
|
)
|
||||||
|
|
||||||
|
var chromaCmd = &cobra.Command{
|
||||||
|
Use: "chroma [file]",
|
||||||
|
Short: "Syntax highlight source code",
|
||||||
|
Long: `Generate syntax-highlighted HTML from source code.
|
||||||
|
|
||||||
|
Reads from file or stdin, outputs HTML with syntax highlighting.
|
||||||
|
Language is auto-detected from filename or can be specified with --language.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms chroma main.go
|
||||||
|
dms chroma --language python script.py
|
||||||
|
echo "def foo(): pass" | dms chroma -l python
|
||||||
|
cat code.rs | dms chroma -l rust --style dracula
|
||||||
|
dms chroma --markdown README.md
|
||||||
|
dms chroma --markdown --style github-dark notes.md
|
||||||
|
dms chroma list-languages
|
||||||
|
dms chroma list-styles`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runChroma,
|
||||||
|
}
|
||||||
|
|
||||||
|
var chromaListLanguagesCmd = &cobra.Command{
|
||||||
|
Use: "list-languages",
|
||||||
|
Short: "List all supported languages",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for _, name := range lexers.Names(true) {
|
||||||
|
fmt.Println(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var chromaListStylesCmd = &cobra.Command{
|
||||||
|
Use: "list-styles",
|
||||||
|
Short: "List all available color styles",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
for _, name := range styles.Names() {
|
||||||
|
fmt.Println(name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chromaCmd.Flags().StringVarP(&chromaLanguage, "language", "l", "", "Language for highlighting (auto-detect if not specified)")
|
||||||
|
chromaCmd.Flags().StringVarP(&chromaStyle, "style", "s", "monokai", "Color style (monokai, dracula, github, etc.)")
|
||||||
|
chromaCmd.Flags().BoolVar(&chromaInline, "inline", false, "Output inline styles instead of CSS classes")
|
||||||
|
chromaCmd.Flags().BoolVar(&chromaLineNumbers, "line-numbers", false, "Show line numbers in output")
|
||||||
|
chromaCmd.Flags().BoolVarP(&chromaMarkdown, "markdown", "m", false, "Render markdown with syntax-highlighted code blocks")
|
||||||
|
chromaCmd.Flags().Int64Var(&maxFileSize, "max-size", 5*1024*1024, "Maximum file size to process without warning (bytes)")
|
||||||
|
|
||||||
|
chromaCmd.AddCommand(chromaListLanguagesCmd)
|
||||||
|
chromaCmd.AddCommand(chromaListStylesCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedLexer(key string, fallbackFunc func() chroma.Lexer) chroma.Lexer {
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if lexer, ok := lexerCache[key]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
lexer := fallbackFunc()
|
||||||
|
if lexer != nil {
|
||||||
|
cacheMutex.Lock()
|
||||||
|
lexerCache[key] = lexer
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
}
|
||||||
|
return lexer
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedStyle(name string) *chroma.Style {
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if style, ok := styleCache[name]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
style := styles.Get(name)
|
||||||
|
if style == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: Style '%s' not found, using fallback\n", name)
|
||||||
|
style = styles.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheMutex.Lock()
|
||||||
|
styleCache[name] = style
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedFormatter(inline bool, lineNumbers bool) *html.Formatter {
|
||||||
|
key := fmt.Sprintf("inline=%t,lineNumbers=%t", inline, lineNumbers)
|
||||||
|
|
||||||
|
cacheMutex.RLock()
|
||||||
|
if formatter, ok := formatterCache[key]; ok {
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
|
var opts []html.Option
|
||||||
|
if inline {
|
||||||
|
opts = append(opts, html.WithClasses(false))
|
||||||
|
} else {
|
||||||
|
opts = append(opts, html.WithClasses(true))
|
||||||
|
}
|
||||||
|
opts = append(opts, html.TabWidth(4))
|
||||||
|
|
||||||
|
if lineNumbers {
|
||||||
|
opts = append(opts, html.WithLineNumbers(true))
|
||||||
|
opts = append(opts, html.LineNumbersInTable(false))
|
||||||
|
opts = append(opts, html.WithLinkableLineNumbers(false, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := html.New(opts...)
|
||||||
|
|
||||||
|
cacheMutex.Lock()
|
||||||
|
formatterCache[key] = formatter
|
||||||
|
cacheMutex.Unlock()
|
||||||
|
return formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func runChroma(cmd *cobra.Command, args []string) {
|
||||||
|
var source string
|
||||||
|
var filename string
|
||||||
|
|
||||||
|
// Read from file or stdin
|
||||||
|
if len(args) > 0 {
|
||||||
|
filename = args[0]
|
||||||
|
|
||||||
|
// Check file size before reading
|
||||||
|
fileInfo, err := os.Stat(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading file info: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileInfo.Size() > maxFileSize {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: File size (%d bytes) exceeds recommended limit (%d bytes)\n",
|
||||||
|
fileInfo.Size(), maxFileSize)
|
||||||
|
fmt.Fprintf(os.Stderr, "Processing may be slow. Consider using smaller files.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading file: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
source = string(content)
|
||||||
|
} else {
|
||||||
|
stat, _ := os.Stdin.Stat()
|
||||||
|
if (stat.Mode() & os.ModeCharDevice) != 0 {
|
||||||
|
_ = cmd.Help()
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := io.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error reading stdin: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
source = string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle empty input
|
||||||
|
if strings.TrimSpace(source) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle Markdown rendering
|
||||||
|
if chromaMarkdown {
|
||||||
|
md := goldmark.New(
|
||||||
|
goldmark.WithExtensions(
|
||||||
|
extension.GFM,
|
||||||
|
highlighting.NewHighlighting(
|
||||||
|
highlighting.WithStyle(chromaStyle),
|
||||||
|
highlighting.WithFormatOptions(
|
||||||
|
html.WithClasses(!chromaInline),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
goldmark.WithParserOptions(
|
||||||
|
parser.WithAutoHeadingID(),
|
||||||
|
),
|
||||||
|
goldmark.WithRendererOptions(
|
||||||
|
ghtml.WithHardWraps(),
|
||||||
|
ghtml.WithXHTML(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := md.Convert([]byte(source), &buf); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Markdown rendering error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Print(buf.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect or use specified lexer
|
||||||
|
var lexer chroma.Lexer
|
||||||
|
if chromaLanguage != "" {
|
||||||
|
lexer = getCachedLexer(chromaLanguage, func() chroma.Lexer {
|
||||||
|
l := lexers.Get(chromaLanguage)
|
||||||
|
if l == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Unknown language: %s\n", chromaLanguage)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
})
|
||||||
|
} else if filename != "" {
|
||||||
|
lexer = getCachedLexer("file:"+filename, func() chroma.Lexer {
|
||||||
|
return lexers.Match(filename)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try content analysis if no lexer found (limit to first 1KB for performance)
|
||||||
|
if lexer == nil {
|
||||||
|
analyzeContent := source
|
||||||
|
if len(source) > 1024 {
|
||||||
|
analyzeContent = source[:1024]
|
||||||
|
}
|
||||||
|
lexer = lexers.Analyse(analyzeContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to plaintext
|
||||||
|
if lexer == nil {
|
||||||
|
lexer = lexers.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
lexer = chroma.Coalesce(lexer)
|
||||||
|
|
||||||
|
// Get cached style
|
||||||
|
style := getCachedStyle(chromaStyle)
|
||||||
|
|
||||||
|
// Get cached formatter
|
||||||
|
formatter := getCachedFormatter(chromaInline, chromaLineNumbers)
|
||||||
|
|
||||||
|
// Tokenize
|
||||||
|
iterator, err := lexer.Tokenise(nil, source)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Tokenization error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format and output
|
||||||
|
if chromaLineNumbers {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := formatter.Format(&buf, style, iterator); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Add spacing between line numbers
|
||||||
|
output := buf.String()
|
||||||
|
output = strings.ReplaceAll(output, "</span><span>", "</span>\u00A0\u00A0<span>")
|
||||||
|
fmt.Print(output)
|
||||||
|
} else {
|
||||||
|
if err := formatter.Format(os.Stdout, style, iterator); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Formatting error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1004
core/cmd/dms/commands_clipboard.go
Normal file
1004
core/cmd/dms/commands_clipboard.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/colorpicker"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -121,13 +121,7 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyToClipboard(text string) {
|
func copyToClipboard(text string) {
|
||||||
var cmd *exec.Cmd
|
if err := clipboard.CopyText(text); err != nil {
|
||||||
if _, err := exec.LookPath("wl-copy"); err == nil {
|
fmt.Fprintln(os.Stderr, "clipboard copy failed:", err)
|
||||||
cmd = exec.Command("wl-copy", text)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(os.Stderr, "wl-copy not found, cannot copy to clipboard")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
Long: "Send IPC commands to running DMS shell (qs -c dms ipc <args>)",
|
|
||||||
PreRunE: findConfig,
|
PreRunE: findConfig,
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
@@ -77,6 +76,13 @@ var ipcCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
ipcCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
|
_ = findConfig(cmd, args)
|
||||||
|
printIPCHelp()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
var debugSrvCmd = &cobra.Command{
|
var debugSrvCmd = &cobra.Command{
|
||||||
Use: "debug-srv",
|
Use: "debug-srv",
|
||||||
Short: "Start the debug server",
|
Short: "Start the debug server",
|
||||||
@@ -152,8 +158,25 @@ var pluginsUninstallCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pluginsUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update <plugin-id>",
|
||||||
|
Short: "Update a plugin by ID",
|
||||||
|
Long: "Update an installed DMS plugin using its ID (e.g., 'myPlugin'). Plugin names are also supported.",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) != 0 {
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return getInstalledPluginIDs(), cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := updatePluginCLI(args[0]); err != nil {
|
||||||
|
log.Fatalf("Error updating plugin: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
func runVersion(cmd *cobra.Command, args []string) {
|
func runVersion(cmd *cobra.Command, args []string) {
|
||||||
printASCII()
|
|
||||||
fmt.Printf("%s\n", formatVersion(Version))
|
fmt.Printf("%s\n", formatVersion(Version))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +225,7 @@ func getBaseVersion() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
return "0.6.2"
|
return "1.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
func startDebugServer() error {
|
func startDebugServer() error {
|
||||||
@@ -408,49 +431,70 @@ func uninstallPluginCLI(idOrName string) error {
|
|||||||
return fmt.Errorf("failed to create registry: %w", err)
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginList, err := registry.List()
|
pluginList, _ := registry.List()
|
||||||
if err != nil {
|
plugin := plugins.FindByIDOrName(idOrName, pluginList)
|
||||||
return fmt.Errorf("failed to list plugins: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, try to find by ID (preferred method)
|
if plugin != nil {
|
||||||
var plugin *plugins.Plugin
|
installed, err := manager.IsInstalled(*plugin)
|
||||||
for _, p := range pluginList {
|
if err != nil {
|
||||||
if p.ID == idOrName {
|
return fmt.Errorf("failed to check install status: %w", err)
|
||||||
plugin = &p
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
if !installed {
|
||||||
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
// Fallback to name for backward compatibility
|
|
||||||
if plugin == nil {
|
|
||||||
for _, p := range pluginList {
|
|
||||||
if p.Name == idOrName {
|
|
||||||
plugin = &p
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
if err := manager.Uninstall(*plugin); err != nil {
|
||||||
|
return fmt.Errorf("failed to uninstall plugin: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if plugin == nil {
|
fmt.Printf("Uninstalling plugin: %s\n", idOrName)
|
||||||
return fmt.Errorf("plugin not found: %s", idOrName)
|
if err := manager.UninstallByIDOrName(idOrName); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
fmt.Printf("Plugin uninstalled successfully: %s\n", idOrName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
installed, err := manager.IsInstalled(*plugin)
|
func updatePluginCLI(idOrName string) error {
|
||||||
|
manager, err := plugins.NewManager()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check install status: %w", err)
|
return fmt.Errorf("failed to create manager: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !installed {
|
registry, err := plugins.NewRegistry()
|
||||||
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Uninstalling plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
pluginList, _ := registry.List()
|
||||||
if err := manager.Uninstall(*plugin); err != nil {
|
plugin := plugins.FindByIDOrName(idOrName, pluginList)
|
||||||
return fmt.Errorf("failed to uninstall plugin: %w", err)
|
|
||||||
|
if plugin != nil {
|
||||||
|
installed, err := manager.IsInstalled(*plugin)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to check install status: %w", err)
|
||||||
|
}
|
||||||
|
if !installed {
|
||||||
|
return fmt.Errorf("plugin not installed: %s", plugin.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Updating plugin: %s (ID: %s)\n", plugin.Name, plugin.ID)
|
||||||
|
if err := manager.Update(*plugin); err != nil {
|
||||||
|
return fmt.Errorf("failed to update plugin: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", plugin.Name)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Plugin uninstalled successfully: %s\n", plugin.Name)
|
fmt.Printf("Updating plugin: %s\n", idOrName)
|
||||||
|
if err := manager.UpdateByIDOrName(idOrName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Plugin updated successfully: %s\n", idOrName)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +517,14 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
colorCmd,
|
colorCmd,
|
||||||
screenshotCmd,
|
screenshotCmd,
|
||||||
notifyActionCmd,
|
notifyActionCmd,
|
||||||
|
notifyCmd,
|
||||||
|
genericNotifyActionCmd,
|
||||||
matugenCmd,
|
matugenCmd,
|
||||||
|
clipboardCmd,
|
||||||
|
chromaCmd,
|
||||||
|
doctorCmd,
|
||||||
|
configCmd,
|
||||||
|
dlCmd,
|
||||||
|
randrCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
318
core/cmd/dms/commands_config.go
Normal file
318
core/cmd/dms/commands_config.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configCmd = &cobra.Command{
|
||||||
|
Use: "config",
|
||||||
|
Short: "Configuration utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolveIncludeCmd = &cobra.Command{
|
||||||
|
Use: "resolve-include <compositor> <filename>",
|
||||||
|
Short: "Check if a file is included in compositor config",
|
||||||
|
Long: "Recursively check if a file is included/sourced in compositor configuration. Returns JSON with exists and included status.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
switch len(args) {
|
||||||
|
case 0:
|
||||||
|
return []string{"hyprland", "niri", "mangowc"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
case 1:
|
||||||
|
return []string{"cursor.kdl", "cursor.conf", "outputs.kdl", "outputs.conf", "binds.kdl", "binds.conf"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runResolveInclude,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configCmd.AddCommand(resolveIncludeCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncludeResult struct {
|
||||||
|
Exists bool `json:"exists"`
|
||||||
|
Included bool `json:"included"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runResolveInclude(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
filename := args[1]
|
||||||
|
|
||||||
|
var result IncludeResult
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case "hyprland":
|
||||||
|
result, err = checkHyprlandInclude(filename)
|
||||||
|
case "niri":
|
||||||
|
result, err = checkNiriInclude(filename)
|
||||||
|
case "mangowc", "dwl", "mango":
|
||||||
|
result, err = checkMangoWCInclude(filename)
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error checking include: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkHyprlandInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "hyprland.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = hyprlandFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hyprlandFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if hyprlandFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkNiriInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.kdl")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = niriFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func niriFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
content := string(data)
|
||||||
|
|
||||||
|
for _, line := range strings.Split(content, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "//") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "include") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
startQuote := strings.Index(trimmed, "\"")
|
||||||
|
if startQuote == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
endQuote := strings.LastIndex(trimmed, "\"")
|
||||||
|
if endQuote <= startQuote {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
includePath := trimmed[startQuote+1 : endQuote]
|
||||||
|
if matchesTarget(includePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := includePath
|
||||||
|
if !filepath.IsAbs(includePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, includePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if niriFindInclude(fullPath, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkMangoWCInclude(filename string) (IncludeResult, error) {
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/mango")
|
||||||
|
if err != nil {
|
||||||
|
return IncludeResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(configDir, "dms", filename)
|
||||||
|
result := IncludeResult{}
|
||||||
|
|
||||||
|
if _, err := os.Stat(targetPath); err == nil {
|
||||||
|
result.Exists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
mainConfig := filepath.Join(configDir, "config.conf")
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
mainConfig = filepath.Join(configDir, "mango.conf")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(mainConfig); os.IsNotExist(err) {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
processed := make(map[string]bool)
|
||||||
|
result.Included = mangowcFindInclude(mainConfig, "dms/"+filename, processed)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mangowcFindInclude(filePath, target string, processed map[string]bool) bool {
|
||||||
|
absPath, err := filepath.Abs(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if processed[absPath] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
processed[absPath] = true
|
||||||
|
|
||||||
|
data, err := os.ReadFile(absPath)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
baseDir := filepath.Dir(absPath)
|
||||||
|
lines := strings.Split(string(data), "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "#") || trimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(trimmed, "source") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
|
if len(parts) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sourcePath := strings.TrimSpace(parts[1])
|
||||||
|
if matchesTarget(sourcePath, target) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPath := sourcePath
|
||||||
|
if !filepath.IsAbs(sourcePath) {
|
||||||
|
fullPath = filepath.Join(baseDir, sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
expanded, err := utils.ExpandPath(fullPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if mangowcFindInclude(expanded, target, processed) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func matchesTarget(path, target string) bool {
|
||||||
|
path = strings.TrimPrefix(path, "./")
|
||||||
|
target = strings.TrimPrefix(target, "./")
|
||||||
|
return path == target || strings.HasSuffix(path, "/"+target)
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ func init() {
|
|||||||
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
dank16Cmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
dank16Cmd.Flags().Bool("kitty", false, "Output in Kitty terminal format")
|
||||||
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
dank16Cmd.Flags().Bool("foot", false, "Output in Foot terminal format")
|
||||||
|
dank16Cmd.Flags().Bool("neovim", false, "Output in Neovim plugin format")
|
||||||
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
dank16Cmd.Flags().Bool("alacritty", false, "Output in Alacritty terminal format")
|
||||||
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
dank16Cmd.Flags().Bool("ghostty", false, "Output in Ghostty terminal format")
|
||||||
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
dank16Cmd.Flags().Bool("wezterm", false, "Output in Wezterm terminal format")
|
||||||
@@ -40,6 +41,7 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
isJson, _ := cmd.Flags().GetBool("json")
|
isJson, _ := cmd.Flags().GetBool("json")
|
||||||
isKitty, _ := cmd.Flags().GetBool("kitty")
|
isKitty, _ := cmd.Flags().GetBool("kitty")
|
||||||
isFoot, _ := cmd.Flags().GetBool("foot")
|
isFoot, _ := cmd.Flags().GetBool("foot")
|
||||||
|
isNeovim, _ := cmd.Flags().GetBool("neovim")
|
||||||
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
isAlacritty, _ := cmd.Flags().GetBool("alacritty")
|
||||||
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
isGhostty, _ := cmd.Flags().GetBool("ghostty")
|
||||||
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
isWezterm, _ := cmd.Flags().GetBool("wezterm")
|
||||||
@@ -116,6 +118,8 @@ func runDank16(cmd *cobra.Command, args []string) {
|
|||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
} else if isWezterm {
|
} else if isWezterm {
|
||||||
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
fmt.Print(dank16.GenerateWeztermTheme(colors))
|
||||||
|
} else if isNeovim {
|
||||||
|
fmt.Print(dank16.GenerateNeovimTheme(colors))
|
||||||
} else {
|
} else {
|
||||||
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
fmt.Print(dank16.GenerateGhosttyTheme(colors))
|
||||||
}
|
}
|
||||||
|
|||||||
1103
core/cmd/dms/commands_doctor.go
Normal file
1103
core/cmd/dms/commands_doctor.go
Normal file
File diff suppressed because it is too large
Load Diff
99
core/cmd/dms/commands_download.go
Normal file
99
core/cmd/dms/commands_download.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var dlOutput string
|
||||||
|
var dlUserAgent string
|
||||||
|
var dlTimeout int
|
||||||
|
var dlIPv4Only bool
|
||||||
|
|
||||||
|
var dlCmd = &cobra.Command{
|
||||||
|
Use: "dl <url>",
|
||||||
|
Short: "Download a URL to stdout or file",
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runDownload(args[0]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
|
||||||
|
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
|
||||||
|
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
|
||||||
|
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDownload(url string) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case dlUserAgent != "":
|
||||||
|
req.Header.Set("User-Agent", dlUserAgent)
|
||||||
|
default:
|
||||||
|
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client := &http.Client{Transport: transport}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if dir := filepath.Dir(dlOutput); dir != "." {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("mkdir failed: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := os.Create(dlOutput)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create failed: %w", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||||
|
os.Remove(dlOutput)
|
||||||
|
return fmt.Errorf("write failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(dlOutput)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -15,6 +15,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/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -121,10 +122,10 @@ func updateArchLinux() error {
|
|||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
if commandExists("yay") {
|
if utils.CommandExists("yay") {
|
||||||
helper = "yay"
|
helper = "yay"
|
||||||
updateCmd = exec.Command("yay", "-S", packageName)
|
updateCmd = exec.Command("yay", "-S", packageName)
|
||||||
} else if commandExists("paru") {
|
} else if utils.CommandExists("paru") {
|
||||||
helper = "paru"
|
helper = "paru"
|
||||||
updateCmd = exec.Command("paru", "-S", packageName)
|
updateCmd = exec.Command("paru", "-S", packageName)
|
||||||
} else {
|
} else {
|
||||||
@@ -376,7 +377,7 @@ func updateDMSBinary() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
version := ""
|
version := ""
|
||||||
for _, line := range strings.Split(string(output), "\n") {
|
for line := range strings.SplitSeq(string(output), "\n") {
|
||||||
if strings.Contains(line, "\"tag_name\"") {
|
if strings.Contains(line, "\"tag_name\"") {
|
||||||
parts := strings.Split(line, "\"")
|
parts := strings.Split(line, "\"")
|
||||||
if len(parts) >= 4 {
|
if len(parts) >= 4 {
|
||||||
@@ -442,7 +443,7 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
decompressedPath := filepath.Join(tempDir, "dms")
|
decompressedPath := filepath.Join(tempDir, "dms")
|
||||||
|
|
||||||
if err := os.Chmod(decompressedPath, 0755); err != nil {
|
if err := os.Chmod(decompressedPath, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to make binary executable: %w", err)
|
return fmt.Errorf("failed to make binary executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"golang.org/x/text/cases"
|
"golang.org/x/text/cases"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
@@ -76,6 +78,28 @@ func installGreeter() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debian/openSUSE
|
||||||
|
greeter.TryInstallGreeterPackage(logFunc, "")
|
||||||
|
if isPackageOnlyGreeterDistro() && !greeter.IsGreeterPackaged() {
|
||||||
|
return fmt.Errorf("dms-greeter must be installed from distro packages on this distribution. %s", packageInstallHint())
|
||||||
|
}
|
||||||
|
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||||
|
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already fully configured, prompt the user
|
||||||
|
if isGreeterEnabled() {
|
||||||
|
fmt.Print("\nGreeter is already installed and configured. Re-run to re-sync settings and permissions? [Y/n]: ")
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(strings.ToLower(response))
|
||||||
|
if response == "n" || response == "no" {
|
||||||
|
fmt.Println("Run 'dms greeter sync' to re-sync theme and settings at any time.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nDetecting DMS installation...")
|
fmt.Println("\nDetecting DMS installation...")
|
||||||
dmsPath, err := greeter.DetectDMSPath()
|
dmsPath, err := greeter.DetectDMSPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -113,20 +137,36 @@ func installGreeter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguring greetd...")
|
fmt.Println("\nConfiguring greetd...")
|
||||||
if err := greeter.ConfigureGreetd(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
// Use empty path when packaged (greeter finds /usr/share/quickshell/dms-greeter); else use user's DMS path
|
||||||
|
greeterPathForConfig := ""
|
||||||
|
if !greeter.IsGreeterPackaged() {
|
||||||
|
greeterPathForConfig = dmsPath
|
||||||
|
}
|
||||||
|
if err := greeter.ConfigureGreetd(greeterPathForConfig, selectedCompositor, logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureGraphicalTarget(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := handleConflictingDisplayManagers(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureGreetdEnabled(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\n=== Installation Complete ===")
|
fmt.Println("\n=== Installation Complete ===")
|
||||||
fmt.Println("\nTo test the greeter, run:")
|
fmt.Println("\nTo start the greeter now, run:")
|
||||||
fmt.Println(" sudo systemctl start greetd")
|
fmt.Println(" sudo systemctl start greetd")
|
||||||
fmt.Println("\nTo enable on boot, run:")
|
fmt.Println("\nOr reboot to see the greeter at next boot.")
|
||||||
fmt.Println(" sudo systemctl enable --now greetd")
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -146,12 +186,30 @@ func syncGreeter() error {
|
|||||||
}
|
}
|
||||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||||
|
|
||||||
|
if !isGreeterEnabled() {
|
||||||
|
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
|
||||||
|
fmt.Print("Would you like to enable it now? (Y/n): ")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
|
||||||
|
if response != "n" && response != "no" {
|
||||||
|
if err := enableGreeter(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("greeter must be enabled before syncing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cacheDir := "/var/cache/dms-greeter"
|
cacheDir := "/var/cache/dms-greeter"
|
||||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||||
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
greeterGroupExists := checkGroupExists("greeter")
|
greeterGroup := greeter.DetectGreeterGroup()
|
||||||
|
greeterGroupExists := utils.HasGroup(greeterGroup)
|
||||||
if greeterGroupExists {
|
if greeterGroupExists {
|
||||||
currentUser, err := user.Current()
|
currentUser, err := user.Current()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -164,36 +222,59 @@ func syncGreeter() error {
|
|||||||
return fmt.Errorf("failed to check groups: %w", err)
|
return fmt.Errorf("failed to check groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||||
if !inGreeterGroup {
|
if !inGreeterGroup {
|
||||||
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
|
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
||||||
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
|
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
||||||
|
|
||||||
var response string
|
var response string
|
||||||
fmt.Scanln(&response)
|
fmt.Scanln(&response)
|
||||||
response = strings.ToLower(strings.TrimSpace(response))
|
response = strings.ToLower(strings.TrimSpace(response))
|
||||||
|
|
||||||
if response == "y" || response == "yes" {
|
if response != "n" && response != "no" {
|
||||||
fmt.Println("\nAdding user to greeter group...")
|
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
|
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
||||||
addUserCmd.Stdout = os.Stdout
|
addUserCmd.Stdout = os.Stdout
|
||||||
addUserCmd.Stderr = os.Stderr
|
addUserCmd.Stderr = os.Stderr
|
||||||
if err := addUserCmd.Run(); err != nil {
|
if err := addUserCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to add user to greeter group: %w", err)
|
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||||
}
|
}
|
||||||
fmt.Println("✓ User added to greeter group")
|
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||||
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compositor := detectConfiguredCompositor()
|
||||||
|
if compositor == "" {
|
||||||
|
compositors := greeter.DetectCompositors()
|
||||||
|
switch len(compositors) {
|
||||||
|
case 0:
|
||||||
|
return fmt.Errorf("no supported compositors found")
|
||||||
|
case 1:
|
||||||
|
compositor = compositors[0]
|
||||||
|
fmt.Printf("✓ Using compositor: %s\n", compositor)
|
||||||
|
default:
|
||||||
|
var err error
|
||||||
|
compositor, err = promptCompositorChoice(compositors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("✓ Selected compositor: %s\n", compositor)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nSetting up permissions and ACLs...")
|
fmt.Println("\nSetting up permissions and ACLs...")
|
||||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nSynchronizing DMS configurations...")
|
fmt.Println("\nSynchronizing DMS configurations...")
|
||||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,21 +285,6 @@ func syncGreeter() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkGroupExists(groupName string) bool {
|
|
||||||
data, err := os.ReadFile("/etc/group")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
lines := strings.Split(string(data), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.HasPrefix(line, groupName+":") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func disableDisplayManager(dmName string) (bool, error) {
|
func disableDisplayManager(dmName string) (bool, error) {
|
||||||
state, err := getSystemdServiceState(dmName)
|
state, err := getSystemdServiceState(dmName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -300,20 +366,29 @@ func ensureGreetdEnabled() error {
|
|||||||
fmt.Println(" ✓ Unmasked greetd")
|
fmt.Println(" ✓ Unmasked greetd")
|
||||||
}
|
}
|
||||||
|
|
||||||
switch state.EnabledState {
|
if state.EnabledState == "enabled" || state.EnabledState == "enabled-runtime" {
|
||||||
case "disabled", "masked", "masked-runtime":
|
fmt.Println(" Reasserting greetd as active display manager...")
|
||||||
|
} else {
|
||||||
fmt.Println(" Enabling greetd service...")
|
fmt.Println(" Enabling greetd service...")
|
||||||
enableCmd := exec.Command("sudo", "systemctl", "enable", "greetd")
|
}
|
||||||
enableCmd.Stdout = os.Stdout
|
|
||||||
enableCmd.Stderr = os.Stderr
|
enableCmd := exec.Command("sudo", "systemctl", "enable", "--force", "greetd")
|
||||||
if err := enableCmd.Run(); err != nil {
|
enableCmd.Stdout = os.Stdout
|
||||||
return fmt.Errorf("failed to enable greetd: %w", err)
|
enableCmd.Stderr = os.Stderr
|
||||||
|
if err := enableCmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("failed to enable greetd: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledState, _, verifyErr := checkSystemdServiceEnabled("greetd")
|
||||||
|
if verifyErr != nil {
|
||||||
|
fmt.Printf(" ⚠ Warning: Could not verify greetd enabled state: %v\n", verifyErr)
|
||||||
|
} else {
|
||||||
|
switch enabledState {
|
||||||
|
case "enabled", "enabled-runtime", "static", "indirect", "alias":
|
||||||
|
fmt.Printf(" ✓ greetd enabled (state: %s)\n", enabledState)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("greetd is still in state '%s' after enable operation", enabledState)
|
||||||
}
|
}
|
||||||
fmt.Println(" ✓ Enabled greetd service")
|
|
||||||
case "enabled", "enabled-runtime":
|
|
||||||
fmt.Println(" ✓ greetd is already enabled")
|
|
||||||
default:
|
|
||||||
fmt.Printf(" ℹ greetd is in state '%s' (should work, no action needed)\n", state.EnabledState)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -350,7 +425,7 @@ func ensureGraphicalTarget() error {
|
|||||||
func handleConflictingDisplayManagers() error {
|
func handleConflictingDisplayManagers() error {
|
||||||
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
||||||
|
|
||||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
|
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||||
|
|
||||||
disabledAny := false
|
disabledAny := false
|
||||||
var errors []string
|
var errors []string
|
||||||
@@ -419,6 +494,10 @@ func enableGreeter() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configContent := string(data)
|
configContent := string(data)
|
||||||
|
if greeter.IsGreeterPackaged() && greeter.HasLegacyLocalGreeterWrapper() {
|
||||||
|
return fmt.Errorf("legacy manual wrapper detected at /usr/local/bin/dms-greeter; remove it before using packaged dms-greeter: sudo rm -f /usr/local/bin/dms-greeter")
|
||||||
|
}
|
||||||
|
|
||||||
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
configAlreadyCorrect := strings.Contains(configContent, "dms-greeter")
|
||||||
|
|
||||||
if configAlreadyCorrect {
|
if configAlreadyCorrect {
|
||||||
@@ -448,7 +527,7 @@ func enableGreeter() error {
|
|||||||
fmt.Println("Detecting installed compositors...")
|
fmt.Println("Detecting installed compositors...")
|
||||||
compositors := greeter.DetectCompositors()
|
compositors := greeter.DetectCompositors()
|
||||||
|
|
||||||
if commandExists("sway") {
|
if utils.CommandExists("sway") {
|
||||||
compositors = append(compositors, "sway")
|
compositors = append(compositors, "sway")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +599,7 @@ func enableGreeter() error {
|
|||||||
newConfig := strings.Join(finalLines, "\n")
|
newConfig := strings.Join(finalLines, "\n")
|
||||||
|
|
||||||
tmpFile := "/tmp/greetd-config.toml"
|
tmpFile := "/tmp/greetd-config.toml"
|
||||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write temp config: %w", err)
|
return fmt.Errorf("failed to write temp config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,6 +630,81 @@ func enableGreeter() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isGreeterEnabled() bool {
|
||||||
|
data, err := os.ReadFile("/etc/greetd/config.toml")
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(string(data), "dms-greeter")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectConfiguredCompositor() string {
|
||||||
|
data, err := os.ReadFile("/etc/greetd/config.toml")
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.Contains(trimmed, "--command niri"):
|
||||||
|
return "niri"
|
||||||
|
case strings.Contains(trimmed, "--command hyprland"):
|
||||||
|
return "hyprland"
|
||||||
|
case strings.Contains(trimmed, "--command sway"):
|
||||||
|
return "sway"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func packageInstallHint() string {
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return "Install package: dms-greeter"
|
||||||
|
}
|
||||||
|
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||||
|
if !exists {
|
||||||
|
return "Install package: dms-greeter"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch config.Family {
|
||||||
|
case distros.FamilyDebian:
|
||||||
|
return "Install with 'sudo apt install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#debian)"
|
||||||
|
case distros.FamilySUSE:
|
||||||
|
return "Install with 'sudo zypper install dms-greeter' (requires DankLinux OBS repo — see https://danklinux.com/docs/dankgreeter/installation#opensuse)"
|
||||||
|
case distros.FamilyUbuntu:
|
||||||
|
return "Install with 'sudo apt install dms-greeter' (requires ppa:avengemedia/danklinux: sudo add-apt-repository ppa:avengemedia/danklinux)"
|
||||||
|
case distros.FamilyFedora:
|
||||||
|
return "Install with 'sudo dnf install dms-greeter' (requires COPR: sudo dnf copr enable avengemedia/danklinux)"
|
||||||
|
case distros.FamilyArch:
|
||||||
|
return "Install from AUR with 'paru -S greetd-dms-greeter-git' or 'yay -S greetd-dms-greeter-git'"
|
||||||
|
default:
|
||||||
|
return "Run 'dms greeter install' to install greeter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPackageOnlyGreeterDistro() bool {
|
||||||
|
osInfo, err := distros.GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
config, exists := distros.Registry[osInfo.Distribution.ID]
|
||||||
|
if !exists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return config.Family == distros.FamilyDebian ||
|
||||||
|
config.Family == distros.FamilySUSE ||
|
||||||
|
config.Family == distros.FamilyUbuntu ||
|
||||||
|
config.Family == distros.FamilyFedora ||
|
||||||
|
config.Family == distros.FamilyArch
|
||||||
|
}
|
||||||
|
|
||||||
func promptCompositorChoice(compositors []string) (string, error) {
|
func promptCompositorChoice(compositors []string) (string, error) {
|
||||||
fmt.Println("\nMultiple compositors detected:")
|
fmt.Println("\nMultiple compositors detected:")
|
||||||
for i, comp := range compositors {
|
for i, comp := range compositors {
|
||||||
@@ -591,8 +745,8 @@ func checkGreeterStatus() error {
|
|||||||
if data, err := os.ReadFile(configPath); err == nil {
|
if data, err := os.ReadFile(configPath); err == nil {
|
||||||
configContent := string(data)
|
configContent := string(data)
|
||||||
if strings.Contains(configContent, "dms-greeter") {
|
if strings.Contains(configContent, "dms-greeter") {
|
||||||
lines := strings.Split(configContent, "\n")
|
lines := strings.SplitSeq(configContent, "\n")
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
trimmed := strings.TrimSpace(line)
|
trimmed := strings.TrimSpace(line)
|
||||||
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
if strings.HasPrefix(trimmed, "command =") || strings.HasPrefix(trimmed, "command=") {
|
||||||
parts := strings.SplitN(trimmed, "=", 2)
|
parts := strings.SplitN(trimmed, "=", 2)
|
||||||
@@ -619,7 +773,7 @@ func checkGreeterStatus() error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✗ Greeter config not found")
|
fmt.Println(" ✗ Greeter config not found")
|
||||||
fmt.Println(" Run 'dms greeter install' to install greeter")
|
fmt.Printf(" %s\n", packageInstallHint())
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("\nGroup Membership:")
|
fmt.Println("\nGroup Membership:")
|
||||||
@@ -629,12 +783,13 @@ func checkGreeterStatus() error {
|
|||||||
return fmt.Errorf("failed to check groups: %w", err)
|
return fmt.Errorf("failed to check groups: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
greeterGroup := greeter.DetectGreeterGroup()
|
||||||
|
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||||
if inGreeterGroup {
|
if inGreeterGroup {
|
||||||
fmt.Println(" ✓ User is in greeter group")
|
fmt.Printf(" ✓ User is in %s group\n", greeterGroup)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(" ✗ User is NOT in greeter group")
|
fmt.Printf(" ✗ User is NOT in %s group\n", greeterGroup)
|
||||||
fmt.Println(" Run 'dms greeter install' to add user to greeter group")
|
fmt.Println(" Run 'dms greeter sync' to set up group membership and permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := "/var/cache/dms-greeter"
|
cacheDir := "/var/cache/dms-greeter"
|
||||||
@@ -643,7 +798,7 @@ func checkGreeterStatus() error {
|
|||||||
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
fmt.Printf(" ✓ %s exists\n", cacheDir)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
fmt.Printf(" ✗ %s not found\n", cacheDir)
|
||||||
fmt.Println(" Run 'dms greeter install' to create cache directory")
|
fmt.Printf(" %s\n", packageInstallHint())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds/providers"
|
||||||
@@ -57,12 +58,15 @@ var keybindsRemoveCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
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")
|
||||||
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
keybindsSetCmd.Flags().Bool("allow-when-locked", false, "Allow when screen is locked")
|
||||||
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
keybindsSetCmd.Flags().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||||
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
keybindsSetCmd.Flags().Bool("no-repeat", false, "Disable key repeat")
|
||||||
|
keybindsSetCmd.Flags().Bool("no-inhibiting", false, "Keep bind active when shortcuts are inhibited (allow-inhibiting=false)")
|
||||||
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
keybindsSetCmd.Flags().String("replace-key", "", "Original key to replace (removes old key)")
|
||||||
|
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
|
||||||
|
|
||||||
keybindsCmd.AddCommand(keybindsListCmd)
|
keybindsCmd.AddCommand(keybindsListCmd)
|
||||||
keybindsCmd.AddCommand(keybindsShowCmd)
|
keybindsCmd.AddCommand(keybindsShowCmd)
|
||||||
@@ -79,19 +83,35 @@ func init() {
|
|||||||
func initializeProviders() {
|
func initializeProviders() {
|
||||||
registry := keybinds.GetDefaultRegistry()
|
registry := keybinds.GetDefaultRegistry()
|
||||||
|
|
||||||
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
|
hyprlandProvider := providers.NewHyprlandProvider("")
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
mangowcProvider := providers.NewMangoWCProvider("$HOME/.config/mango")
|
mangowcProvider := providers.NewMangoWCProvider("")
|
||||||
if err := registry.Register(mangowcProvider); err != nil {
|
if err := registry.Register(mangowcProvider); err != nil {
|
||||||
log.Warnf("Failed to register MangoWC provider: %v", err)
|
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
configDir, _ := os.UserConfigDir()
|
||||||
if err := registry.Register(swayProvider); err != nil {
|
|
||||||
log.Warnf("Failed to register Sway provider: %v", err)
|
if configDir != "" {
|
||||||
|
scrollProvider := providers.NewSwayProvider(filepath.Join(configDir, "scroll"))
|
||||||
|
if err := registry.Register(scrollProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Scroll provider: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
miracleProvider := providers.NewMiracleProvider("")
|
||||||
|
if err := registry.Register(miracleProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Miracle WM provider: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configDir != "" {
|
||||||
|
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
|
||||||
|
if err := registry.Register(swayProvider); err != nil {
|
||||||
|
log.Warnf("Failed to register Sway provider: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
niriProvider := providers.NewNiriProvider("")
|
niriProvider := providers.NewNiriProvider("")
|
||||||
@@ -105,12 +125,21 @@ func initializeProviders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runKeybindsList(_ *cobra.Command, _ []string) {
|
func runKeybindsList(cmd *cobra.Command, _ []string) {
|
||||||
providerList := keybinds.GetDefaultRegistry().List()
|
providerList := keybinds.GetDefaultRegistry().List()
|
||||||
|
asJSON, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if asJSON {
|
||||||
|
output, _ := json.Marshal(providerList)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if len(providerList) == 0 {
|
if len(providerList) == 0 {
|
||||||
fmt.Fprintln(os.Stdout, "No providers available")
|
fmt.Fprintln(os.Stdout, "No providers available")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stdout, "Available providers:")
|
fmt.Fprintln(os.Stdout, "Available providers:")
|
||||||
for _, name := range providerList {
|
for _, name := range providerList {
|
||||||
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
fmt.Fprintf(os.Stdout, " - %s\n", name)
|
||||||
@@ -125,6 +154,10 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
|
|||||||
return providers.NewMangoWCProvider(path)
|
return providers.NewMangoWCProvider(path)
|
||||||
case "sway":
|
case "sway":
|
||||||
return providers.NewSwayProvider(path)
|
return providers.NewSwayProvider(path)
|
||||||
|
case "scroll":
|
||||||
|
return providers.NewSwayProvider(path)
|
||||||
|
case "miracle":
|
||||||
|
return providers.NewMiracleProvider(path)
|
||||||
case "niri":
|
case "niri":
|
||||||
return providers.NewNiriProvider(path)
|
return providers.NewNiriProvider(path)
|
||||||
default:
|
default:
|
||||||
@@ -194,6 +227,12 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
|
|||||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||||
options["repeat"] = false
|
options["repeat"] = false
|
||||||
}
|
}
|
||||||
|
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||||
|
options["allow-inhibiting"] = false
|
||||||
|
}
|
||||||
|
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||||
|
options["flags"] = v
|
||||||
|
}
|
||||||
|
|
||||||
desc, _ := cmd.Flags().GetString("desc")
|
desc, _ := cmd.Flags().GetString("desc")
|
||||||
if err := writable.SetBind(key, action, desc, options); err != nil {
|
if err := writable.SetBind(key, action, desc, options); err != nil {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/matugen"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,9 +32,16 @@ var matugenQueueCmd = &cobra.Command{
|
|||||||
Run: runMatugenQueue,
|
Run: runMatugenQueue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var matugenCheckCmd = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check which template apps are detected",
|
||||||
|
Run: runMatugenCheck,
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
matugenCmd.AddCommand(matugenGenerateCmd)
|
matugenCmd.AddCommand(matugenGenerateCmd)
|
||||||
matugenCmd.AddCommand(matugenQueueCmd)
|
matugenCmd.AddCommand(matugenQueueCmd)
|
||||||
|
matugenCmd.AddCommand(matugenCheckCmd)
|
||||||
|
|
||||||
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")
|
||||||
@@ -49,6 +56,7 @@ func init() {
|
|||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
@@ -68,6 +76,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
stockColors, _ := cmd.Flags().GetString("stock-colors")
|
||||||
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
syncModeWithPortal, _ := cmd.Flags().GetBool("sync-mode-with-portal")
|
||||||
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
terminalsAlwaysDark, _ := cmd.Flags().GetBool("terminals-always-dark")
|
||||||
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -75,19 +84,24 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
ConfigDir: configDir,
|
ConfigDir: configDir,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Value: value,
|
Value: value,
|
||||||
Mode: mode,
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
TerminalsAlwaysDark: terminalsAlwaysDark,
|
TerminalsAlwaysDark: terminalsAlwaysDark,
|
||||||
|
SkipTemplates: skipTemplates,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||||
opts := buildMatugenOptions(cmd)
|
opts := buildMatugenOptions(cmd)
|
||||||
if err := matugen.Run(opts); err != nil {
|
err := matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,33 +111,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
wait, _ := cmd.Flags().GetBool("wait")
|
wait, _ := cmd.Flags().GetBool("wait")
|
||||||
timeout, _ := cmd.Flags().GetDuration("timeout")
|
timeout, _ := cmd.Flags().GetDuration("timeout")
|
||||||
|
|
||||||
socketPath := os.Getenv("DMS_SOCKET")
|
request := models.Request{
|
||||||
if socketPath == "" {
|
ID: 1,
|
||||||
var err error
|
Method: "matugen.queue",
|
||||||
socketPath, err = server.FindSocket()
|
Params: map[string]any{
|
||||||
if err != nil {
|
|
||||||
log.Info("No socket available, running synchronously")
|
|
||||||
if err := matugen.Run(opts); err != nil {
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.Dial("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Info("Socket connection failed, running synchronously")
|
|
||||||
if err := matugen.Run(opts); err != nil {
|
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
request := map[string]any{
|
|
||||||
"id": 1,
|
|
||||||
"method": "matugen.queue",
|
|
||||||
"params": map[string]any{
|
|
||||||
"stateDir": opts.StateDir,
|
"stateDir": opts.StateDir,
|
||||||
"shellDir": opts.ShellDir,
|
"shellDir": opts.ShellDir,
|
||||||
"configDir": opts.ConfigDir,
|
"configDir": opts.ConfigDir,
|
||||||
@@ -136,15 +127,23 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"stockColors": opts.StockColors,
|
"stockColors": opts.StockColors,
|
||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
|
"skipTemplates": opts.SkipTemplates,
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewEncoder(conn).Encode(request); err != nil {
|
|
||||||
log.Fatalf("Failed to send request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !wait {
|
if !wait {
|
||||||
|
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||||
|
log.Info("Server unavailable, running synchronously")
|
||||||
|
err := matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Println("Theme generation queued")
|
fmt.Println("Theme generation queued")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -154,17 +153,22 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
resultCh := make(chan error, 1)
|
resultCh := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
var response struct {
|
resp, ok := tryServerRequest(request)
|
||||||
ID int `json:"id"`
|
if !ok {
|
||||||
Result any `json:"result"`
|
log.Info("Server unavailable, running synchronously")
|
||||||
Error string `json:"error"`
|
err := matugen.Run(opts)
|
||||||
}
|
switch {
|
||||||
if err := json.NewDecoder(conn).Decode(&response); err != nil {
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
resultCh <- fmt.Errorf("failed to read response: %w", err)
|
resultCh <- matugen.ErrNoChanges
|
||||||
|
case err != nil:
|
||||||
|
resultCh <- err
|
||||||
|
default:
|
||||||
|
resultCh <- nil
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if response.Error != "" {
|
if resp.Error != "" {
|
||||||
resultCh <- fmt.Errorf("server error: %s", response.Error)
|
resultCh <- fmt.Errorf("server error: %s", resp.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
resultCh <- nil
|
resultCh <- nil
|
||||||
@@ -172,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-resultCh:
|
case err := <-resultCh:
|
||||||
if err != nil {
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Theme generation completed")
|
fmt.Println("Theme generation completed")
|
||||||
@@ -180,3 +187,12 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Timeout waiting for theme generation")
|
log.Fatalf("Timeout waiting for theme generation")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runMatugenCheck(cmd *cobra.Command, args []string) {
|
||||||
|
checks := matugen.CheckTemplates(nil)
|
||||||
|
data, err := json.Marshal(checks)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to marshal check results: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
}
|
||||||
|
|||||||
68
core/cmd/dms/commands_notify.go
Normal file
68
core/cmd/dms/commands_notify.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/notify"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
notifyAppName string
|
||||||
|
notifyIcon string
|
||||||
|
notifyFile string
|
||||||
|
notifyTimeout int
|
||||||
|
)
|
||||||
|
|
||||||
|
var notifyCmd = &cobra.Command{
|
||||||
|
Use: "notify <summary> [body]",
|
||||||
|
Short: "Send a desktop notification",
|
||||||
|
Long: `Send a desktop notification with optional actions.
|
||||||
|
|
||||||
|
If --file is provided, the notification will have "Open" and "Open Folder" actions.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dms notify "Hello" "World"
|
||||||
|
dms notify "File received" "photo.jpg" --file ~/Downloads/photo.jpg --icon smartphone
|
||||||
|
dms notify "Download complete" --file ~/Downloads/file.zip --app "My App"`,
|
||||||
|
Args: cobra.MinimumNArgs(1),
|
||||||
|
Run: runNotify,
|
||||||
|
}
|
||||||
|
|
||||||
|
var genericNotifyActionCmd = &cobra.Command{
|
||||||
|
Use: "notify-action-generic",
|
||||||
|
Hidden: true,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
notify.RunActionListener(args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
notifyCmd.Flags().StringVar(¬ifyAppName, "app", "DMS", "Application name")
|
||||||
|
notifyCmd.Flags().StringVar(¬ifyIcon, "icon", "", "Icon name or path")
|
||||||
|
notifyCmd.Flags().StringVar(¬ifyFile, "file", "", "File path (enables Open/Open Folder actions)")
|
||||||
|
notifyCmd.Flags().IntVar(¬ifyTimeout, "timeout", 5000, "Timeout in milliseconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runNotify(cmd *cobra.Command, args []string) {
|
||||||
|
summary := args[0]
|
||||||
|
body := ""
|
||||||
|
if len(args) > 1 {
|
||||||
|
body = args[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
n := notify.Notification{
|
||||||
|
AppName: notifyAppName,
|
||||||
|
Icon: notifyIcon,
|
||||||
|
Summary: summary,
|
||||||
|
Body: body,
|
||||||
|
FilePath: notifyFile,
|
||||||
|
Timeout: int32(notifyTimeout),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := notify.Send(n); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime"
|
"mime"
|
||||||
"net"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -93,32 +90,6 @@ func mimeTypeToCategories(mimeType string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runOpen(target string) {
|
func runOpen(target string) {
|
||||||
socketPath, err := server.FindSocket()
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("DMS socket not found: %v", err)
|
|
||||||
fmt.Println("DMS is not running. Please start DMS first.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.Dial("unix", socketPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("DMS socket connection failed: %v", err)
|
|
||||||
fmt.Println("DMS is not running. Please start DMS first.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
buf := make([]byte, 1)
|
|
||||||
for {
|
|
||||||
_, err := conn.Read(buf)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if buf[0] == '\n' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse file:// URIs to extract the actual file path
|
// Parse file:// URIs to extract the actual file path
|
||||||
actualTarget := target
|
actualTarget := target
|
||||||
detectedMimeType := openMimeType
|
detectedMimeType := openMimeType
|
||||||
@@ -160,6 +131,12 @@ func runOpen(target string) {
|
|||||||
detectedRequestType = "url"
|
detectedRequestType = "url"
|
||||||
}
|
}
|
||||||
log.Infof("Detected HTTP(S) URL")
|
log.Infof("Detected HTTP(S) URL")
|
||||||
|
} else if strings.HasPrefix(target, "dms://") {
|
||||||
|
// Handle DMS internal URLs (theme/plugin install, etc.)
|
||||||
|
if detectedRequestType == "" {
|
||||||
|
detectedRequestType = "url"
|
||||||
|
}
|
||||||
|
log.Infof("Detected DMS internal URL")
|
||||||
} else if _, err := os.Stat(target); err == nil {
|
} else if _, err := os.Stat(target); err == nil {
|
||||||
// Handle local file paths directly (not file:// URIs)
|
// Handle local file paths directly (not file:// URIs)
|
||||||
// Convert to absolute path
|
// Convert to absolute path
|
||||||
@@ -206,7 +183,7 @@ func runOpen(target string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
method := "apppicker.open"
|
method := "apppicker.open"
|
||||||
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://")) {
|
if detectedMimeType == "" && len(detectedCategories) == 0 && (strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "dms://")) {
|
||||||
method = "browser.open"
|
method = "browser.open"
|
||||||
params["url"] = target
|
params["url"] = target
|
||||||
}
|
}
|
||||||
@@ -219,8 +196,9 @@ func runOpen(target string) {
|
|||||||
|
|
||||||
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
log.Infof("Sending request - Method: %s, Params: %+v", method, params)
|
||||||
|
|
||||||
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
if err := sendServerRequestFireAndForget(req); err != nil {
|
||||||
log.Fatalf("Failed to send request: %v", err)
|
fmt.Println("DMS is not running. Please start DMS first.")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Request sent successfully")
|
log.Infof("Request sent successfully")
|
||||||
|
|||||||
58
core/cmd/dms/commands_randr.go
Normal file
58
core/cmd/dms/commands_randr.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var randrCmd = &cobra.Command{
|
||||||
|
Use: "randr",
|
||||||
|
Short: "Query output display information",
|
||||||
|
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
|
||||||
|
Run: runRandr,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
randrCmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrJSON struct {
|
||||||
|
Outputs []randrOutput `json:"outputs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRandr(cmd *cobra.Command, args []string) {
|
||||||
|
outputs, err := queryRandr()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonFlag, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if jsonFlag {
|
||||||
|
data, err := json.Marshal(randrJSON{Outputs: outputs})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to marshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, out := range outputs {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
status := "enabled"
|
||||||
|
if !out.Enabled {
|
||||||
|
status = "disabled"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s (%s)\n", out.Name, status)
|
||||||
|
fmt.Printf(" Scale: %.4g\n", out.Scale)
|
||||||
|
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
|
||||||
|
if out.Refresh > 0 {
|
||||||
|
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dms"
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,11 +18,9 @@ var rootCmd = &cobra.Command{
|
|||||||
Use: "dms",
|
Use: "dms",
|
||||||
Short: "dms CLI",
|
Short: "dms CLI",
|
||||||
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
Long: "dms is the DankMaterialShell management CLI and backend server.",
|
||||||
Run: runInteractiveMode,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add the -c flag
|
|
||||||
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
rootCmd.PersistentFlags().StringVarP(&customConfigPath, "config", "c", "", "Specify a custom path to the DMS config directory")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,7 +34,7 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
if statErr == nil && !info.IsDir() {
|
if statErr == nil && !info.IsDir() {
|
||||||
configPath = customConfigPath
|
configPath = customConfigPath
|
||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil // <-- Guard statement
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if statErr != nil {
|
if statErr != nil {
|
||||||
@@ -50,15 +46,18 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
|
|
||||||
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
configStateFile := filepath.Join(getRuntimeDir(), "danklinux.path")
|
||||||
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
if data, readErr := os.ReadFile(configStateFile); readErr == nil {
|
||||||
statePath := strings.TrimSpace(string(data))
|
if len(getAllDMSPIDs()) == 0 {
|
||||||
shellPath := filepath.Join(statePath, "shell.qml")
|
os.Remove(configStateFile)
|
||||||
|
|
||||||
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 // <-- Guard statement
|
|
||||||
} else {
|
} 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)
|
os.Remove(configStateFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,18 +72,3 @@ func findConfig(cmd *cobra.Command, args []string) error {
|
|||||||
log.Debug("Using config from: %s", configPath)
|
log.Debug("Using config from: %s", configPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func runInteractiveMode(cmd *cobra.Command, args []string) {
|
|
||||||
detector, _ := dms.NewDetector()
|
|
||||||
|
|
||||||
if !detector.IsDMSInstalled() {
|
|
||||||
log.Error("DankMaterialShell (DMS) is not detected as installed on this system.")
|
|
||||||
log.Info("Please install DMS using dankinstall before using this management interface.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
model := dms.NewModel(Version)
|
|
||||||
p := tea.NewProgram(model, tea.WithAltScreen())
|
|
||||||
if _, err := p.Run(); err != nil {
|
|
||||||
log.Fatalf("Error running program: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,25 +4,25 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"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
|
||||||
ssIncludeCursor bool
|
ssCursor string
|
||||||
ssFormat string
|
ssFormat string
|
||||||
ssQuality int
|
ssQuality int
|
||||||
ssOutputDir string
|
ssOutputDir string
|
||||||
ssFilename string
|
ssFilename string
|
||||||
ssNoClipboard bool
|
ssNoClipboard bool
|
||||||
ssNoFile bool
|
ssNoFile bool
|
||||||
ssNoNotify bool
|
ssNoNotify bool
|
||||||
ssStdout bool
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var screenshotCmd = &cobra.Command{
|
var screenshotCmd = &cobra.Command{
|
||||||
@@ -52,7 +52,7 @@ Examples:
|
|||||||
dms screenshot last # Last region (pre-selected)
|
dms screenshot last # Last region (pre-selected)
|
||||||
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 --cursor # 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`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
|
|||||||
|
|
||||||
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().BoolVar(&ssIncludeCursor, "cursor", false, "Include cursor in screenshot")
|
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)")
|
||||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||||
@@ -136,7 +136,9 @@ 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.IncludeCursor = ssIncludeCursor
|
if strings.EqualFold(ssCursor, "on") {
|
||||||
|
config.Cursor = screenshot.CursorOn
|
||||||
|
}
|
||||||
config.Clipboard = !ssNoClipboard
|
config.Clipboard = !ssNoClipboard
|
||||||
config.SaveFile = !ssNoFile
|
config.SaveFile = !ssNoFile
|
||||||
config.Notify = !ssNoNotify
|
config.Notify = !ssNoNotify
|
||||||
@@ -257,9 +259,7 @@ func copyImageToClipboard(buf *screenshot.ShmBuffer, format screenshot.Format, q
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("wl-copy", "--type", mimeType)
|
return clipboard.Copy(data.Bytes(), mimeType)
|
||||||
cmd.Stdin = &data
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) error {
|
||||||
@@ -295,7 +295,14 @@ 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)
|
||||||
swapRB := pixelFormat == uint32(screenshot.FormatARGB8888) || pixelFormat == uint32(screenshot.FormatXRGB8888) || pixelFormat == 0
|
|
||||||
|
var swapRB bool
|
||||||
|
switch pixelFormat {
|
||||||
|
case uint32(screenshot.FormatABGR8888), uint32(screenshot.FormatXBGR8888):
|
||||||
|
swapRB = false
|
||||||
|
default:
|
||||||
|
swapRB = true
|
||||||
|
}
|
||||||
|
|
||||||
for y := 0; y < dstH; y++ {
|
for y := 0; y < dstH; y++ {
|
||||||
srcY := int(float64(y) / scale)
|
srcY := int(float64(y) / scale)
|
||||||
@@ -309,16 +316,17 @@ func bufferToRGBThumbnail(buf *screenshot.ShmBuffer, maxSize int, pixelFormat ui
|
|||||||
}
|
}
|
||||||
si := srcY*buf.Stride + srcX*4
|
si := srcY*buf.Stride + srcX*4
|
||||||
di := (y*dstW + x) * 3
|
di := (y*dstW + x) * 3
|
||||||
if si+2 < len(data) {
|
if si+3 >= len(data) {
|
||||||
if swapRB {
|
continue
|
||||||
rgb[di+0] = data[si+2]
|
}
|
||||||
rgb[di+1] = data[si+1]
|
if swapRB {
|
||||||
rgb[di+2] = data[si+0]
|
rgb[di+0] = data[si+2]
|
||||||
} else {
|
rgb[di+1] = data[si+1]
|
||||||
rgb[di+0] = data[si+0]
|
rgb[di+2] = data[si+0]
|
||||||
rgb[di+1] = data[si+1]
|
} else {
|
||||||
rgb[di+2] = data[si+2]
|
rgb[di+0] = data[si+0]
|
||||||
}
|
rgb[di+1] = data[si+1]
|
||||||
|
rgb[di+2] = data[si+2]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,7 +378,37 @@ func runScreenshotList(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, o := range outputs {
|
for _, o := range outputs {
|
||||||
fmt.Printf("%s: %dx%d+%d+%d (scale: %d)\n",
|
scaleStr := fmt.Sprintf("%.2f", o.FractionalScale)
|
||||||
o.Name, o.Width, o.Height, o.X, o.Y, o.Scale)
|
if o.FractionalScale == float64(int(o.FractionalScale)) {
|
||||||
|
scaleStr = fmt.Sprintf("%d", int(o.FractionalScale))
|
||||||
|
}
|
||||||
|
|
||||||
|
transformStr := transformName(o.Transform)
|
||||||
|
|
||||||
|
fmt.Printf("%s: %dx%d+%d+%d scale=%s transform=%s\n",
|
||||||
|
o.Name, o.Width, o.Height, o.X, o.Y, scaleStr, transformStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func transformName(t int32) string {
|
||||||
|
switch t {
|
||||||
|
case 0:
|
||||||
|
return "normal"
|
||||||
|
case 1:
|
||||||
|
return "90"
|
||||||
|
case 2:
|
||||||
|
return "180"
|
||||||
|
case 3:
|
||||||
|
return "270"
|
||||||
|
case 4:
|
||||||
|
return "flipped"
|
||||||
|
case 5:
|
||||||
|
return "flipped-90"
|
||||||
|
case 6:
|
||||||
|
return "flipped-180"
|
||||||
|
case 7:
|
||||||
|
return "flipped-270"
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%d", t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ 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/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,11 +26,249 @@ var setupCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var setupBindsCmd = &cobra.Command{
|
||||||
|
Use: "binds",
|
||||||
|
Short: "Deploy default keybinds config",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("binds"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupLayoutCmd = &cobra.Command{
|
||||||
|
Use: "layout",
|
||||||
|
Short: "Deploy default layout config",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("layout"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupColorsCmd = &cobra.Command{
|
||||||
|
Use: "colors",
|
||||||
|
Short: "Deploy default colors config",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("colors"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupAlttabCmd = &cobra.Command{
|
||||||
|
Use: "alttab",
|
||||||
|
Short: "Deploy default alt-tab config (niri only)",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("alttab"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupOutputsCmd = &cobra.Command{
|
||||||
|
Use: "outputs",
|
||||||
|
Short: "Deploy default outputs config",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("outputs"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupCursorCmd = &cobra.Command{
|
||||||
|
Use: "cursor",
|
||||||
|
Short: "Deploy default cursor config",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("cursor"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var setupWindowrulesCmd = &cobra.Command{
|
||||||
|
Use: "windowrules",
|
||||||
|
Short: "Deploy default window rules config",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
if err := runSetupDmsConfig("windowrules"); err != nil {
|
||||||
|
log.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type dmsConfigSpec struct {
|
||||||
|
niriFile string
|
||||||
|
hyprFile string
|
||||||
|
niriContent func(terminal string) string
|
||||||
|
hyprContent func(terminal string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
var dmsConfigSpecs = map[string]dmsConfigSpec{
|
||||||
|
"binds": {
|
||||||
|
niriFile: "binds.kdl",
|
||||||
|
hyprFile: "binds.conf",
|
||||||
|
niriContent: func(t string) string {
|
||||||
|
return strings.ReplaceAll(config.NiriBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
|
},
|
||||||
|
hyprContent: func(t string) string {
|
||||||
|
return strings.ReplaceAll(config.HyprBindsConfig, "{{TERMINAL_COMMAND}}", t)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
niriFile: "layout.kdl",
|
||||||
|
hyprFile: "layout.conf",
|
||||||
|
niriContent: func(_ string) string { return config.NiriLayoutConfig },
|
||||||
|
hyprContent: func(_ string) string { return config.HyprLayoutConfig },
|
||||||
|
},
|
||||||
|
"colors": {
|
||||||
|
niriFile: "colors.kdl",
|
||||||
|
hyprFile: "colors.conf",
|
||||||
|
niriContent: func(_ string) string { return config.NiriColorsConfig },
|
||||||
|
hyprContent: func(_ string) string { return config.HyprColorsConfig },
|
||||||
|
},
|
||||||
|
"alttab": {
|
||||||
|
niriFile: "alttab.kdl",
|
||||||
|
niriContent: func(_ string) string { return config.NiriAlttabConfig },
|
||||||
|
},
|
||||||
|
"outputs": {
|
||||||
|
niriFile: "outputs.kdl",
|
||||||
|
hyprFile: "outputs.conf",
|
||||||
|
niriContent: func(_ string) string { return "" },
|
||||||
|
hyprContent: func(_ string) string { return "" },
|
||||||
|
},
|
||||||
|
"cursor": {
|
||||||
|
niriFile: "cursor.kdl",
|
||||||
|
hyprFile: "cursor.conf",
|
||||||
|
niriContent: func(_ string) string { return "" },
|
||||||
|
hyprContent: func(_ string) string { return "" },
|
||||||
|
},
|
||||||
|
"windowrules": {
|
||||||
|
niriFile: "windowrules.kdl",
|
||||||
|
hyprFile: "windowrules.conf",
|
||||||
|
niriContent: func(_ string) string { return "" },
|
||||||
|
hyprContent: func(_ string) string { return "" },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectTerminal() (string, error) {
|
||||||
|
terminals := []string{"ghostty", "foot", "kitty", "alacritty"}
|
||||||
|
var found []string
|
||||||
|
for _, t := range terminals {
|
||||||
|
if utils.CommandExists(t) {
|
||||||
|
found = append(found, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch len(found) {
|
||||||
|
case 0:
|
||||||
|
return "ghostty", nil
|
||||||
|
case 1:
|
||||||
|
return found[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Multiple terminals detected:")
|
||||||
|
for i, t := range found {
|
||||||
|
fmt.Printf("%d) %s\n", i+1, t)
|
||||||
|
}
|
||||||
|
fmt.Printf("\nChoice (1-%d): ", len(found))
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
|
choice := 0
|
||||||
|
fmt.Sscanf(response, "%d", &choice)
|
||||||
|
if choice < 1 || choice > len(found) {
|
||||||
|
return "", fmt.Errorf("invalid choice")
|
||||||
|
}
|
||||||
|
return found[choice-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectCompositorForSetup() (string, error) {
|
||||||
|
compositors := greeter.DetectCompositors()
|
||||||
|
|
||||||
|
switch len(compositors) {
|
||||||
|
case 0:
|
||||||
|
return "", fmt.Errorf("no supported compositors found (niri or Hyprland required)")
|
||||||
|
case 1:
|
||||||
|
return strings.ToLower(compositors[0]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
selected, err := greeter.PromptCompositorChoice(compositors)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return strings.ToLower(selected), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSetupDmsConfig(name string) error {
|
||||||
|
spec, ok := dmsConfigSpecs[name]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown config: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
compositor, err := detectCompositorForSetup()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var filename string
|
||||||
|
var contentFn func(string) string
|
||||||
|
switch compositor {
|
||||||
|
case "niri":
|
||||||
|
filename = spec.niriFile
|
||||||
|
contentFn = spec.niriContent
|
||||||
|
case "hyprland":
|
||||||
|
filename = spec.hyprFile
|
||||||
|
contentFn = spec.hyprContent
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
if filename == "" {
|
||||||
|
return fmt.Errorf("%s is not supported for %s", name, compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dmsDir string
|
||||||
|
switch compositor {
|
||||||
|
case "niri":
|
||||||
|
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "niri", "dms")
|
||||||
|
case "hyprland":
|
||||||
|
dmsDir = filepath.Join(os.Getenv("HOME"), ".config", "hypr", "dms")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(dmsDir, filename)
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
return fmt.Errorf("%s already exists and is not empty: %s", name, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal := "ghostty"
|
||||||
|
if contentFn != nil && name == "binds" {
|
||||||
|
terminal, err = detectTerminal()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content := contentFn(terminal)
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Deployed %s to %s\n", name, path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func runSetup() error {
|
func runSetup() error {
|
||||||
fmt.Println("=== DMS Configuration Setup ===")
|
fmt.Println("=== DMS Configuration Setup ===")
|
||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
|
useSystemd := promptSystemd()
|
||||||
|
|
||||||
if !wmSelected && !terminalSelected {
|
if !wmSelected && !terminalSelected {
|
||||||
fmt.Println("No configurations selected. Exiting.")
|
fmt.Println("No configurations selected. Exiting.")
|
||||||
@@ -67,14 +307,14 @@ func runSetup() error {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if wmSelected && terminalSelected {
|
if wmSelected && terminalSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, terminal)
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, terminal, useSystemd)
|
||||||
} else if wmSelected {
|
} else if wmSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, wm, deps.TerminalGhostty)
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, wm, deps.TerminalGhostty, useSystemd)
|
||||||
if len(results) > 1 {
|
if len(results) > 1 {
|
||||||
results = results[:1]
|
results = results[:1]
|
||||||
}
|
}
|
||||||
} else if terminalSelected {
|
} else if terminalSelected {
|
||||||
results, err = deployer.DeployConfigurationsWithTerminal(ctx, deps.WindowManagerNiri, terminal)
|
results, err = deployer.DeployConfigurationsWithSystemd(ctx, deps.WindowManagerNiri, terminal, useSystemd)
|
||||||
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
if len(results) > 0 && results[0].ConfigType == "Niri" {
|
||||||
results = results[1:]
|
results = results[1:]
|
||||||
}
|
}
|
||||||
@@ -144,6 +384,19 @@ func promptTerminal() (deps.Terminal, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func promptSystemd() bool {
|
||||||
|
fmt.Println("\nUse systemd for session management?")
|
||||||
|
fmt.Println("1) Yes (recommended for most distros)")
|
||||||
|
fmt.Println("2) No (standalone, no systemd integration)")
|
||||||
|
|
||||||
|
var response string
|
||||||
|
fmt.Print("\nChoice (1-2): ")
|
||||||
|
fmt.Scanln(&response)
|
||||||
|
response = strings.TrimSpace(response)
|
||||||
|
|
||||||
|
return response != "2"
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
homeDir := os.Getenv("HOME")
|
||||||
willBackup := false
|
willBackup := false
|
||||||
|
|||||||
338
core/cmd/dms/commands_windowrules.go
Normal file
338
core/cmd/dms/commands_windowrules.go
Normal file
@@ -0,0 +1,338 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var windowrulesCmd = &cobra.Command{
|
||||||
|
Use: "windowrules",
|
||||||
|
Short: "Manage window rules",
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesListCmd = &cobra.Command{
|
||||||
|
Use: "list [compositor]",
|
||||||
|
Short: "List all window rules",
|
||||||
|
Long: "List all window rules from compositor config file. Returns JSON with rules and DMS status.",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesAddCmd = &cobra.Command{
|
||||||
|
Use: "add <compositor> '<json>'",
|
||||||
|
Short: "Add a window rule to DMS file",
|
||||||
|
Long: "Add a new window rule to the DMS-managed rules file.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesUpdateCmd = &cobra.Command{
|
||||||
|
Use: "update <compositor> <id> '<json>'",
|
||||||
|
Short: "Update a window rule in DMS file",
|
||||||
|
Long: "Update an existing window rule in the DMS-managed rules file.",
|
||||||
|
Args: cobra.ExactArgs(3),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesRemoveCmd = &cobra.Command{
|
||||||
|
Use: "remove <compositor> <id>",
|
||||||
|
Short: "Remove a window rule from DMS file",
|
||||||
|
Long: "Remove a window rule from the DMS-managed rules file.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesRemove,
|
||||||
|
}
|
||||||
|
|
||||||
|
var windowrulesReorderCmd = &cobra.Command{
|
||||||
|
Use: "reorder <compositor> '<json-array-of-ids>'",
|
||||||
|
Short: "Reorder window rules in DMS file",
|
||||||
|
Long: "Reorder window rules by providing a JSON array of rule IDs in the desired order.",
|
||||||
|
Args: cobra.ExactArgs(2),
|
||||||
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
}
|
||||||
|
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||||
|
},
|
||||||
|
Run: runWindowrulesReorder,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
configCmd.AddCommand(windowrulesCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesListCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesAddCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesUpdateCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesRemoveCmd)
|
||||||
|
windowrulesCmd.AddCommand(windowrulesReorderCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowRulesListResult struct {
|
||||||
|
Rules []windowrules.WindowRule `json:"rules"`
|
||||||
|
DMSStatus *windowrules.DMSRulesStatus `json:"dmsStatus,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WindowRuleWriteResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCompositor(args []string) string {
|
||||||
|
if len(args) > 0 {
|
||||||
|
return strings.ToLower(args[0])
|
||||||
|
}
|
||||||
|
if os.Getenv("NIRI_SOCKET") != "" {
|
||||||
|
return "niri"
|
||||||
|
}
|
||||||
|
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||||
|
// return "hyprland"
|
||||||
|
// }
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleError(errMsg string) {
|
||||||
|
result := WindowRuleWriteResult{Success: false, Error: errMsg}
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeRuleSuccess(id, path string) {
|
||||||
|
result := WindowRuleWriteResult{Success: true, ID: id, Path: path}
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := getCompositor(args)
|
||||||
|
if compositor == "" {
|
||||||
|
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
||||||
|
}
|
||||||
|
|
||||||
|
var result WindowRulesListResult
|
||||||
|
|
||||||
|
switch compositor {
|
||||||
|
case "niri":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to expand niri config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse niri window rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allRules := providers.ConvertNiriRulesToWindowRules(parseResult.Rules)
|
||||||
|
|
||||||
|
provider := providers.NewNiriWritableProvider(configDir)
|
||||||
|
dmsRulesPath := provider.GetOverridePath()
|
||||||
|
dmsRules, _ := provider.LoadDMSRules()
|
||||||
|
|
||||||
|
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||||
|
for i, dr := range dmsRules {
|
||||||
|
dmsRuleMap[i] = dr
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsIdx := 0
|
||||||
|
for i, r := range allRules {
|
||||||
|
if r.Source == dmsRulesPath {
|
||||||
|
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||||
|
allRules[i].ID = dmr.ID
|
||||||
|
allRules[i].Name = dmr.Name
|
||||||
|
}
|
||||||
|
dmsIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rules = allRules
|
||||||
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
|
case "hyprland":
|
||||||
|
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to parse hyprland window rules: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
allRules := providers.ConvertHyprlandRulesToWindowRules(parseResult.Rules)
|
||||||
|
|
||||||
|
provider := providers.NewHyprlandWritableProvider(configDir)
|
||||||
|
dmsRulesPath := provider.GetOverridePath()
|
||||||
|
dmsRules, _ := provider.LoadDMSRules()
|
||||||
|
|
||||||
|
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||||
|
for i, dr := range dmsRules {
|
||||||
|
dmsRuleMap[i] = dr
|
||||||
|
}
|
||||||
|
|
||||||
|
dmsIdx := 0
|
||||||
|
for i, r := range allRules {
|
||||||
|
if r.Source == dmsRulesPath {
|
||||||
|
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||||
|
allRules[i].ID = dmr.ID
|
||||||
|
allRules[i].Name = dmr.Name
|
||||||
|
}
|
||||||
|
dmsIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Rules = allRules
|
||||||
|
result.DMSStatus = parseResult.DMSStatus
|
||||||
|
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown compositor: %s", compositor)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, _ := json.Marshal(result)
|
||||||
|
fmt.Fprintln(os.Stdout, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesAdd(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
ruleJSON := args[1]
|
||||||
|
|
||||||
|
var rule windowrules.WindowRule
|
||||||
|
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if rule.ID == "" {
|
||||||
|
rule.ID = generateRuleID()
|
||||||
|
}
|
||||||
|
rule.Enabled = true
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.SetRule(rule); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesUpdate(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
ruleID := args[1]
|
||||||
|
ruleJSON := args[2]
|
||||||
|
|
||||||
|
var rule windowrules.WindowRule
|
||||||
|
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
rule.ID = ruleID
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.SetRule(rule); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesRemove(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
ruleID := args[1]
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.RemoveRule(ruleID); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess(ruleID, provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWindowrulesReorder(cmd *cobra.Command, args []string) {
|
||||||
|
compositor := strings.ToLower(args[0])
|
||||||
|
idsJSON := args[1]
|
||||||
|
|
||||||
|
var ids []string
|
||||||
|
if err := json.Unmarshal([]byte(idsJSON), &ids); err != nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Invalid JSON array: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := getWindowRulesProvider(compositor)
|
||||||
|
if provider == nil {
|
||||||
|
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := provider.ReorderRules(ids); err != nil {
|
||||||
|
writeRuleError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
writeRuleSuccess("", provider.GetOverridePath())
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
||||||
|
switch compositor {
|
||||||
|
case "niri":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return providers.NewNiriWritableProvider(configDir)
|
||||||
|
case "hyprland":
|
||||||
|
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return providers.NewHyprlandWritableProvider(configDir)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRuleID() string {
|
||||||
|
return fmt.Sprintf("wr_%d", time.Now().UnixNano())
|
||||||
|
}
|
||||||
@@ -87,20 +87,14 @@ func newDPMSClient() (*dpmsClient, error) {
|
|||||||
switch e.Interface {
|
switch e.Interface {
|
||||||
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
case wlr_output_power.ZwlrOutputPowerManagerV1InterfaceName:
|
||||||
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
powerMgr := wlr_output_power.NewZwlrOutputPowerManagerV1(c.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 1)
|
||||||
if version > 1 {
|
|
||||||
version = 1
|
|
||||||
}
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
if err := registry.Bind(e.Name, e.Interface, version, powerMgr); err == nil {
|
||||||
c.powerMgr = powerMgr
|
c.powerMgr = powerMgr
|
||||||
}
|
}
|
||||||
|
|
||||||
case "wl_output":
|
case "wl_output":
|
||||||
output := wlclient.NewOutput(c.ctx)
|
output := wlclient.NewOutput(c.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 4)
|
||||||
if version > 4 {
|
|
||||||
version = 4
|
|
||||||
}
|
|
||||||
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
if err := registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
outputID := fmt.Sprintf("output-%d", output.ID())
|
outputID := fmt.Sprintf("output-%d", output.ID())
|
||||||
state := &outputState{
|
state := &outputState{
|
||||||
|
|||||||
@@ -19,11 +19,14 @@ func init() {
|
|||||||
// Add subcommands to greeter
|
// Add subcommands to greeter
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to setup
|
||||||
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to update
|
// Add subcommands to update
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|||||||
@@ -18,10 +18,13 @@ func init() {
|
|||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
// Add subcommands to greeter
|
||||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||||
|
|
||||||
|
// Add subcommands to setup
|
||||||
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
// Add subcommands to plugins
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
// Add common commands to root
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|||||||
172
core/cmd/dms/randr_client.go
Normal file
172
core/cmd/dms/randr_client.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type randrOutput struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Scale float64 `json:"scale"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Refresh int32 `json:"refresh"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrHead struct {
|
||||||
|
name string
|
||||||
|
enabled bool
|
||||||
|
scale float64
|
||||||
|
currentModeID uint32
|
||||||
|
modeIDs []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrMode struct {
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
refresh int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrClient struct {
|
||||||
|
display *wlclient.Display
|
||||||
|
ctx *wlclient.Context
|
||||||
|
manager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
|
heads map[uint32]*randrHead
|
||||||
|
modes map[uint32]*randrMode
|
||||||
|
done bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryRandr() ([]randrOutput, error) {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &randrClient{
|
||||||
|
display: display,
|
||||||
|
ctx: display.Context(),
|
||||||
|
heads: make(map[uint32]*randrHead),
|
||||||
|
modes: make(map[uint32]*randrMode),
|
||||||
|
}
|
||||||
|
defer c.ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||||
|
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
|
||||||
|
version := min(e.Version, 4)
|
||||||
|
|
||||||
|
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||||
|
c.handleHead(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||||
|
c.done = true
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||||
|
c.manager = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// First roundtrip: discover globals and bind manager
|
||||||
|
syncCallback, err := display.Sync()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sync display: %w", err)
|
||||||
|
}
|
||||||
|
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||||
|
if c.manager == nil {
|
||||||
|
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
|
||||||
|
c.done = true
|
||||||
|
}
|
||||||
|
// Otherwise wait for manager's DoneHandler
|
||||||
|
})
|
||||||
|
|
||||||
|
for !c.done {
|
||||||
|
if err := c.ctx.Dispatch(); err != nil {
|
||||||
|
return nil, fmt.Errorf("dispatch error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.err != nil {
|
||||||
|
return nil, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.buildOutputs(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||||
|
handle := e.Head
|
||||||
|
headID := handle.ID()
|
||||||
|
|
||||||
|
head := &randrHead{
|
||||||
|
modeIDs: make([]uint32, 0),
|
||||||
|
}
|
||||||
|
c.heads[headID] = head
|
||||||
|
|
||||||
|
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||||
|
head.name = e.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
|
||||||
|
head.enabled = e.Enabled != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||||
|
head.scale = e.Scale
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
|
||||||
|
head.currentModeID = e.Mode.ID()
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
|
||||||
|
modeHandle := e.Mode
|
||||||
|
modeID := modeHandle.ID()
|
||||||
|
|
||||||
|
head.modeIDs = append(head.modeIDs, modeID)
|
||||||
|
|
||||||
|
mode := &randrMode{}
|
||||||
|
c.modes[modeID] = mode
|
||||||
|
|
||||||
|
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
|
||||||
|
mode.width = e.Width
|
||||||
|
mode.height = e.Height
|
||||||
|
})
|
||||||
|
|
||||||
|
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
|
||||||
|
mode.refresh = e.Refresh
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *randrClient) buildOutputs() []randrOutput {
|
||||||
|
outputs := make([]randrOutput, 0, len(c.heads))
|
||||||
|
|
||||||
|
for _, head := range c.heads {
|
||||||
|
out := randrOutput{
|
||||||
|
Name: head.name,
|
||||||
|
Scale: head.scale,
|
||||||
|
Enabled: head.enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode, ok := c.modes[head.currentModeID]; ok {
|
||||||
|
out.Width = mode.width
|
||||||
|
out.Height = mode.height
|
||||||
|
out.Refresh = mode.refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs = append(outputs, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
}
|
||||||
114
core/cmd/dms/server_client.go
Normal file
114
core/cmd/dms/server_client.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendServerRequest(req models.Request) (*models.Response[any], error) {
|
||||||
|
socketPath := getServerSocketPath()
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
scanner.Scan() // discard initial capabilities message
|
||||||
|
|
||||||
|
reqData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Write(reqData); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write newline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !scanner.Scan() {
|
||||||
|
return nil, fmt.Errorf("failed to read response")
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp models.Response[any]
|
||||||
|
if err := json.Unmarshal(scanner.Bytes(), &resp); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendServerRequestFireAndForget sends a request without waiting for a response.
|
||||||
|
// Useful for commands that trigger UI or async operations.
|
||||||
|
func sendServerRequestFireAndForget(req models.Request) error {
|
||||||
|
socketPath := getServerSocketPath()
|
||||||
|
|
||||||
|
conn, err := net.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to server (is it running?): %w", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
scanner.Scan() // discard initial capabilities message
|
||||||
|
|
||||||
|
reqData, err := json.Marshal(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to marshal request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Write(reqData); err != nil {
|
||||||
|
return fmt.Errorf("failed to write request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := conn.Write([]byte("\n")); err != nil {
|
||||||
|
return fmt.Errorf("failed to write newline: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// tryServerRequest attempts to send a request but returns false if server unavailable.
|
||||||
|
// Does not log errors - caller can decide what to do on failure.
|
||||||
|
func tryServerRequest(req models.Request) (*models.Response[any], bool) {
|
||||||
|
resp, err := sendServerRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return resp, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerSocketPath() string {
|
||||||
|
runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
|
||||||
|
if runtimeDir == "" {
|
||||||
|
runtimeDir = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(runtimeDir)
|
||||||
|
if err != nil {
|
||||||
|
return filepath.Join(runtimeDir, "danklinux.sock")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
if name == "danklinux.sock" {
|
||||||
|
return filepath.Join(runtimeDir, name)
|
||||||
|
}
|
||||||
|
if len(name) > 10 && name[:10] == "danklinux-" && filepath.Ext(name) == ".sock" {
|
||||||
|
return filepath.Join(runtimeDir, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.GetSocketPath()
|
||||||
|
}
|
||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,6 +20,25 @@ import (
|
|||||||
|
|
||||||
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
|
var isSessionManaged bool
|
||||||
|
|
||||||
func execDetachedRestart(targetPID int) {
|
func execDetachedRestart(targetPID int) {
|
||||||
@@ -104,7 +125,6 @@ func getAllDMSPIDs() []int {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the child process is still alive
|
|
||||||
proc, err := os.FindProcess(childPID)
|
proc, err := os.FindProcess(childPID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.Remove(pidFile)
|
os.Remove(pidFile)
|
||||||
@@ -112,18 +132,15 @@ func getAllDMSPIDs() []int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||||
// Process is dead, remove stale PID file
|
|
||||||
os.Remove(pidFile)
|
os.Remove(pidFile)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pids = append(pids, childPID)
|
pids = append(pids, childPID)
|
||||||
|
|
||||||
// Also get the parent PID from the filename
|
|
||||||
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
parentPIDStr := strings.TrimPrefix(entry.Name(), "danklinux-")
|
||||||
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
parentPIDStr = strings.TrimSuffix(parentPIDStr, ".pid")
|
||||||
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
if parentPID, err := strconv.Atoi(parentPIDStr); err == nil {
|
||||||
// Check if parent is still alive
|
|
||||||
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
if parentProc, err := os.FindProcess(parentPID); err == nil {
|
||||||
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
if err := parentProc.Signal(syscall.Signal(0)); err == nil {
|
||||||
pids = append(pids, parentPID)
|
pids = append(pids, parentPID)
|
||||||
@@ -159,6 +176,7 @@ func runShellInteractive(session bool) {
|
|||||||
errChan <- fmt.Errorf("server panic: %v", r)
|
errChan <- fmt.Errorf("server panic: %v", r)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
server.CLIVersion = Version
|
||||||
if err := server.Start(false); err != nil {
|
if err := server.Start(false); err != nil {
|
||||||
errChan <- fmt.Errorf("server error: %w", err)
|
errChan <- fmt.Errorf("server error: %w", err)
|
||||||
}
|
}
|
||||||
@@ -168,8 +186,10 @@ func runShellInteractive(session bool) {
|
|||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
if os.Getenv("QT_LOGGING_RULES") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
@@ -183,6 +203,16 @@ func runShellInteractive(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
@@ -217,15 +247,28 @@ func runShellInteractive(session bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
// Handle SIGUSR1 restart for non-session managed processes
|
if sig == syscall.SIGUSR1 {
|
||||||
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
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...")
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
execDetachedRestart(os.Getpid())
|
execDetachedRestart(os.Getpid())
|
||||||
// Exit immediately to avoid race conditions with detached restart
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other signals: clean shutdown
|
// 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)
|
||||||
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
log.Infof("\nReceived signal %v, shutting down...", sig)
|
log.Infof("\nReceived signal %v, shutting down...", sig)
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
@@ -239,7 +282,7 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
os.Exit(1)
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +325,6 @@ func restartShell() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func killShell() {
|
func killShell() {
|
||||||
// Get all tracked DMS PIDs from PID files
|
|
||||||
pids := getAllDMSPIDs()
|
pids := getAllDMSPIDs()
|
||||||
|
|
||||||
if len(pids) == 0 {
|
if len(pids) == 0 {
|
||||||
@@ -293,14 +335,12 @@ func killShell() {
|
|||||||
currentPid := os.Getpid()
|
currentPid := os.Getpid()
|
||||||
uniquePids := make(map[int]bool)
|
uniquePids := make(map[int]bool)
|
||||||
|
|
||||||
// Deduplicate and filter out current process
|
|
||||||
for _, pid := range pids {
|
for _, pid := range pids {
|
||||||
if pid != currentPid {
|
if pid != currentPid {
|
||||||
uniquePids[pid] = true
|
uniquePids[pid] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kill all tracked processes
|
|
||||||
for pid := range uniquePids {
|
for pid := range uniquePids {
|
||||||
proc, err := os.FindProcess(pid)
|
proc, err := os.FindProcess(pid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -308,7 +348,6 @@ func killShell() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if process is still alive before killing
|
|
||||||
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
if err := proc.Signal(syscall.Signal(0)); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -320,7 +359,6 @@ func killShell() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up any remaining PID files
|
|
||||||
dir := getRuntimeDir()
|
dir := getRuntimeDir()
|
||||||
entries, err := os.ReadDir(dir)
|
entries, err := os.ReadDir(dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -337,14 +375,7 @@ func killShell() {
|
|||||||
|
|
||||||
func runShellDaemon(session bool) {
|
func runShellDaemon(session bool) {
|
||||||
isSessionManaged = session
|
isSessionManaged = session
|
||||||
// Check if this is the daemon child process by looking for the hidden flag
|
isDaemonChild := slices.Contains(os.Args, "--daemon-child")
|
||||||
isDaemonChild := false
|
|
||||||
for _, arg := range os.Args {
|
|
||||||
if arg == "--daemon-child" {
|
|
||||||
isDaemonChild = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDaemonChild {
|
if !isDaemonChild {
|
||||||
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
fmt.Fprintf(os.Stderr, "dms %s\n", Version)
|
||||||
@@ -395,8 +426,10 @@ func runShellDaemon(session bool) {
|
|||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
cmd := exec.CommandContext(ctx, "qs", "-p", configPath)
|
||||||
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
cmd.Env = append(os.Environ(), "DMS_SOCKET="+socketPath)
|
||||||
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
if os.Getenv("QT_LOGGING_RULES") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
if qtRules := log.GetQtLoggingRules(); qtRules != "" {
|
||||||
|
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
@@ -410,6 +443,16 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error opening /dev/null: %v", err)
|
log.Fatalf("Error opening /dev/null: %v", err)
|
||||||
@@ -450,15 +493,28 @@ func runShellDaemon(session bool) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case sig := <-sigChan:
|
case sig := <-sigChan:
|
||||||
// Handle SIGUSR1 restart for non-session managed processes
|
if sig == syscall.SIGUSR1 {
|
||||||
if sig == syscall.SIGUSR1 && !isSessionManaged {
|
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...")
|
log.Infof("Received SIGUSR1, spawning detached restart process...")
|
||||||
execDetachedRestart(os.Getpid())
|
execDetachedRestart(os.Getpid())
|
||||||
// Exit immediately to avoid race conditions with detached restart
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// All other signals: clean shutdown
|
// 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)
|
||||||
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
|
case <-time.After(500 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
cancel()
|
cancel()
|
||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
@@ -470,17 +526,25 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Process.Signal(syscall.SIGTERM)
|
cmd.Process.Signal(syscall.SIGTERM)
|
||||||
}
|
}
|
||||||
os.Remove(socketPath)
|
os.Remove(socketPath)
|
||||||
os.Exit(1)
|
os.Exit(getProcessExitCode(cmd.ProcessState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var qsHasAnyDisplay = sync.OnceValue(func() bool {
|
||||||
|
out, err := exec.Command("qs", "ipc", "--help").Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(string(out), "--any-display")
|
||||||
|
})
|
||||||
|
|
||||||
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
||||||
targets := make(ipcTargets)
|
targets := make(ipcTargets)
|
||||||
var currentTarget string
|
var currentTarget string
|
||||||
for _, line := range strings.Split(output, "\n") {
|
for line := range strings.SplitSeq(output, "\n") {
|
||||||
if strings.HasPrefix(line, "target ") {
|
if after, ok := strings.CutPrefix(line, "target "); ok {
|
||||||
currentTarget = strings.TrimSpace(strings.TrimPrefix(line, "target "))
|
currentTarget = strings.TrimSpace(after)
|
||||||
targets[currentTarget] = make(map[string][]string)
|
targets[currentTarget] = make(map[string][]string)
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
if strings.HasPrefix(line, " function") && currentTarget != "" {
|
||||||
@@ -505,7 +569,11 @@ func parseTargetsFromIPCShowOutput(output string) ipcTargets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getShellIPCCompletions(args []string, _ string) []string {
|
func getShellIPCCompletions(args []string, _ string) []string {
|
||||||
cmdArgs := []string{"-p", configPath, "ipc", "show"}
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
var targets ipcTargets
|
var targets ipcTargets
|
||||||
|
|
||||||
@@ -550,16 +618,20 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
log.Error("IPC command requires arguments")
|
printIPCHelp()
|
||||||
log.Info("Usage: dms ipc <command> [args...]")
|
return
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if args[0] != "call" {
|
if args[0] != "call" {
|
||||||
args = append([]string{"call"}, args...)
|
args = append([]string{"call"}, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := append([]string{"-p", configPath, "ipc"}, args...)
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@@ -569,3 +641,45 @@ func runShellIPCCommand(args []string) {
|
|||||||
log.Fatalf("Error running IPC command: %v", err)
|
log.Fatalf("Error running IPC command: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printIPCHelp() {
|
||||||
|
fmt.Println("Usage: dms ipc <target> <function> [args...]")
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
cmdArgs := []string{"ipc"}
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath, "show")
|
||||||
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
|
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Could not retrieve available IPC targets (is DMS running?)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
targets := parseTargetsFromIPCShowOutput(string(output))
|
||||||
|
if len(targets) == 0 {
|
||||||
|
fmt.Println("No IPC targets available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Targets:")
|
||||||
|
|
||||||
|
targetNames := make([]string, 0, len(targets))
|
||||||
|
for name := range targets {
|
||||||
|
targetNames = append(targetNames, name)
|
||||||
|
}
|
||||||
|
slices.Sort(targetNames)
|
||||||
|
|
||||||
|
for _, targetName := range targetNames {
|
||||||
|
funcs := targets[targetName]
|
||||||
|
funcNames := make([]string, 0, len(funcs))
|
||||||
|
for fn := range funcs {
|
||||||
|
funcNames = append(funcNames, fn)
|
||||||
|
}
|
||||||
|
slices.Sort(funcNames)
|
||||||
|
fmt.Printf(" %-16s %s\n", targetName, strings.Join(funcNames, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ func getThemedASCII() string {
|
|||||||
logo := `
|
logo := `
|
||||||
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
██████╗ █████╗ ███╗ ██╗██╗ ██╗
|
||||||
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝
|
||||||
██║ ██║███████║██╔██╗ ██║█████╔╝
|
██║ ██║███████║██╔██╗ ██║█████╔╝
|
||||||
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
██║ ██║██╔══██║██║╚██╗██║██╔═██╗
|
||||||
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
██████╔╝██║ ██║██║ ╚████║██║ ██╗
|
||||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝`
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,10 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// findCommandPath returns the absolute path to a command in PATH
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
func findCommandPath(cmd string) (string, error) {
|
||||||
path, err := exec.LookPath(cmd)
|
path, err := exec.LookPath(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -42,13 +37,7 @@ func checkSystemdServiceEnabled(serviceName string) (string, bool, error) {
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
knownStates := []string{"disabled", "masked", "masked-runtime", "not-found", "enabled", "enabled-runtime", "static", "indirect", "alias"}
|
||||||
isKnownState := false
|
isKnownState := slices.Contains(knownStates, stateStr)
|
||||||
for _, known := range knownStates {
|
|
||||||
if stateStr == known {
|
|
||||||
isKnownState = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isKnownState {
|
if !isKnownState {
|
||||||
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
return stateStr, false, fmt.Errorf("systemctl is-enabled failed: %w (output: %s)", err, stateStr)
|
||||||
|
|||||||
51
core/go.mod
51
core/go.mod
@@ -1,56 +1,61 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.24.6
|
go 1.25.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
|
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 v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/godbus/dbus/v5 v5.2.0
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||||
github.com/pilebones/go-udev v0.9.1
|
github.com/pilebones/go-udev v0.9.1
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||||
github.com/spf13/cobra v1.10.1
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39
|
github.com/yuin/goldmark v1.7.16
|
||||||
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
|
go.etcd.io/bbolt v1.4.3
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||||
|
golang.org/x/image v0.36.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
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.3.0 // indirect
|
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.6.0 // indirect
|
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
github.com/cloudflare/circl v1.6.3 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // 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-20251126203821-7f9c95185ee0 // indirect
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
github.com/go-logfmt/logfmt v0.6.1 // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.4.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.3.0 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||||
github.com/sergi/go-diff v1.4.0 // indirect
|
github.com/sergi/go-diff v1.4.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.3 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
golang.org/x/crypto v0.45.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.50.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
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.3.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.2 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 // 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.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // 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-20251128074608-48f817f57805
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0
|
github.com/lucasb-eyer/go-colorful v1.3.0
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
@@ -64,7 +69,11 @@ require (
|
|||||||
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 v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sys v0.38.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.31.0
|
golang.org/x/text v0.34.0
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// v0.0.1 tag is missing a LICENSE file; master has it.
|
||||||
|
// See: https://github.com/mattn/go-localereader/issues/2
|
||||||
|
replace github.com/mattn/go-localereader v0.0.1 => github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75
|
||||||
|
|||||||
116
core/go.sum
116
core/go.sum
@@ -4,6 +4,14 @@ github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBi
|
|||||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0 h1:kPstgsQtY8CmDOOFZd81ytM9Gi3f6ImzPCKF7nNhQ2U=
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3npbIyyaQWenGpZ/uXg=
|
||||||
|
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/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
|
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/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=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
@@ -12,40 +20,40 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z
|
|||||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
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.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
github.com/charmbracelet/colorprofile v0.4.2/go.mod h1:0rTi81QpwDElInthtrQ6Ni7cG0sDtwAd4C4le060fT8=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
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/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 v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
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=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0 h1:k32vueaksef9WIKCNcoqRNyKbyvkvkysNYnAWz2fN4s=
|
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||||
github.com/clipperhouse/displaywidth v0.6.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
|
||||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
|
||||||
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/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
|
||||||
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||||
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
|
github.com/dlclark/regexp2 v1.11.5/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=
|
||||||
@@ -56,28 +64,30 @@ 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-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-20251126203821-7f9c95185ee0 h1:eY5aB2GXiVdgTueBcqsBt53WuJTRZAuCdIS/86Pcq5c=
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3 h1:UU7oARtwQ5g85aFiCSwIUA6PBmAshYj0sytl/5CCBgs=
|
||||||
github.com/go-git/go-billy/v6 v6.0.0-20251126203821-7f9c95185ee0/go.mod h1:0NjwVNrwtVFZBReAp5OoGklGJIgJFEbVyHneAr4lc8k=
|
github.com/go-git/go-billy/v6 v6.0.0-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1 h1:OH8i1ojV9bWfr0ZfasfpgtUXQHQyVS8HXik/V1C099w=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
|
||||||
github.com/go-git/go-git-fixtures/v5 v5.1.1/go.mod h1:Altk43lx3b1ks+dVoAG2300o5WWUnktvfY3VI6bcaXU=
|
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805 h1:jxQ3BzYeErNRvlI/4+0mpwqMzvB4g97U+ksfgvrUEbY=
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f h1:TBkCJv9YwPOuXq1OG0r01bcxRrvs15Hp/DtZuPt4H6s=
|
||||||
github.com/go-git/go-git/v6 v6.0.0-20251128074608-48f817f57805/go.mod h1:dIwT3uWK1ooHInyVnK2JS5VfQ3peVGYaw2QPqX7uFvs=
|
github.com/go-git/go-git/v6 v6.0.0-20260216160506-e6a3f881772f/go.mod h1:B88nWzfnhTlIikoJ4d84Nc9noKS5mJoA7SgDdkt0aPU=
|
||||||
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/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.0 h1:3WexO+U+yg9T70v9FdHr9kCxYlazaAXUhx2VMkbfax8=
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.0/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||||
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/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvEZpPN+yS6swJA0nzqgZvDzgl/OPyefU=
|
||||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83/go.mod h1:iHAf8OIncO2gcQ8XOjS7CMJ2aPbX2Bs0wl5pZyanEqk=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||||
github.com/kevinburke/ssh_config v1.4.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/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -91,8 +101,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
|||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||||
github.com/mattn/go-localereader v0.0.1/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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
@@ -112,14 +122,14 @@ github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUc
|
|||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a h1:8ZZwZWIQKC0YVMyaCkbrdeI8faTjD1QBrRAAWc1TjMI=
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20250930225324-bf4099d4614a/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/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.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
|
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||||
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
|
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
@@ -127,29 +137,43 @@ 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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39 h1:DHNhtq3sNNzrvduZZIiFyXWOL9IWaDPHqTnLJp+rCBY=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
golang.org/x/exp v0.0.0-20251125195548-87e1e737ad39/go.mod h1:46edojNIoXTNOhySWIWdix628clX9ODXwPsQuG6hsK0=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
|
||||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
|
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||||
|
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||||
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
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=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/bin/sh
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -9,8 +9,8 @@ NC='\033[0m' # No Color
|
|||||||
|
|
||||||
# Check for root privileges
|
# Check for root privileges
|
||||||
if [ "$(id -u)" == "0" ]; then
|
if [ "$(id -u)" == "0" ]; then
|
||||||
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
printf "%bError: This script must not be run as root%b\n" "$RED" "$NC"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if running on Linux
|
# Check if running on Linux
|
||||||
@@ -22,17 +22,17 @@ fi
|
|||||||
# Detect architecture
|
# Detect architecture
|
||||||
ARCH=$(uname -m)
|
ARCH=$(uname -m)
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
x86_64)
|
x86_64)
|
||||||
ARCH="amd64"
|
ARCH="amd64"
|
||||||
;;
|
;;
|
||||||
aarch64)
|
aarch64)
|
||||||
ARCH="arm64"
|
ARCH="arm64"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
printf "%bError: Unsupported architecture: %s%b\n" "$RED" "$ARCH" "$NC"
|
||||||
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
printf "This installer only supports x86_64 (amd64) and aarch64 (arm64) architectures\n"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Get the latest release version
|
# Get the latest release version
|
||||||
@@ -55,7 +55,7 @@ curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LAT
|
|||||||
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
curl -L "https://github.com/AvengeMedia/DankMaterialShell/releases/download/$LATEST_VERSION/dankinstall-$ARCH.gz.sha256" -o "expected.sha256"
|
||||||
|
|
||||||
# Get the expected checksum
|
# Get the expected checksum
|
||||||
EXPECTED_CHECKSUM=$(cat expected.sha256 | awk '{print $1}')
|
EXPECTED_CHECKSUM=$(awk '{print $1}' expected.sha256)
|
||||||
|
|
||||||
# Calculate actual checksum
|
# Calculate actual checksum
|
||||||
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
printf "%bVerifying checksum...%b\n" "$GREEN" "$NC"
|
||||||
@@ -67,7 +67,7 @@ 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
|
cd - >/dev/null
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -82,5 +82,5 @@ printf "%bRunning installer...%b\n" "$GREEN" "$NC"
|
|||||||
./installer
|
./installer
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
cd - > /dev/null
|
cd - >/dev/null
|
||||||
rm -rf "$TEMP_DIR"
|
rm -rf "$TEMP_DIR"
|
||||||
|
|||||||
576
core/internal/clipboard/clipboard.go
Normal file
576
core/internal/clipboard/clipboard.go
Normal file
@@ -0,0 +1,576 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Copy(data []byte, mimeType string) error {
|
||||||
|
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
|
if foreground {
|
||||||
|
return copyServeWithWriter(func(writer io.Writer) error {
|
||||||
|
total := 0
|
||||||
|
for total < len(data) {
|
||||||
|
n, err := writer.Write(data[total:])
|
||||||
|
total += n
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total != len(data) {
|
||||||
|
return io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
return CopyReader(bytes.NewReader(data), mimeType, foreground, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
|
if !foreground {
|
||||||
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyServeReader(data, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
|
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||||
|
if pasteOnce {
|
||||||
|
args = append(args, "--paste-once")
|
||||||
|
}
|
||||||
|
args = append(args, "--type", mimeType)
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
if stdinSource, ok := data.(*os.File); ok {
|
||||||
|
cmd.Stdin = stdinSource
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
|
}
|
||||||
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyServeReader(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
|
cachedData, err := createClipboardCacheFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create clipboard cache file: %w", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cachedData.Name())
|
||||||
|
|
||||||
|
if _, err := io.Copy(cachedData, data); err != nil {
|
||||||
|
return fmt.Errorf("cache clipboard data: %w", err)
|
||||||
|
}
|
||||||
|
if err := cachedData.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close temp cache file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return copyServeWithWriter(func(writer io.Writer) error {
|
||||||
|
cachedFile, err := os.Open(cachedData.Name())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open temp cache file: %w", err)
|
||||||
|
}
|
||||||
|
defer cachedFile.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(writer, cachedFile); err != nil {
|
||||||
|
return fmt.Errorf("write clipboard data: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
|
preferredDirs := []string{}
|
||||||
|
|
||||||
|
if cacheDir, err := os.UserCacheDir(); err == nil {
|
||||||
|
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
|
||||||
|
}
|
||||||
|
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
|
||||||
|
|
||||||
|
for _, dir := range preferredDirs {
|
||||||
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
|
||||||
|
if err == nil {
|
||||||
|
return cachedData, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyServeWithWriter(writeTo func(io.Writer) error, mimeType string, pasteOnce bool) error {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer display.Destroy()
|
||||||
|
|
||||||
|
ctx := 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(ctx)
|
||||||
|
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(ctx)
|
||||||
|
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()
|
||||||
|
|
||||||
|
source, err := dataControlMgr.CreateDataSource()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create data source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := source.Offer(mimeType); err != nil {
|
||||||
|
return fmt.Errorf("offer mime type: %w", err)
|
||||||
|
}
|
||||||
|
if mimeType == "text/plain;charset=utf-8" || mimeType == "text/plain" {
|
||||||
|
if err := source.Offer("text/plain"); err != nil {
|
||||||
|
return fmt.Errorf("offer text/plain: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("text/plain;charset=utf-8"); err != nil {
|
||||||
|
return fmt.Errorf("offer text/plain;charset=utf-8: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("UTF8_STRING"); err != nil {
|
||||||
|
return fmt.Errorf("offer UTF8_STRING: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("STRING"); err != nil {
|
||||||
|
return fmt.Errorf("offer STRING: %w", err)
|
||||||
|
}
|
||||||
|
if err := source.Offer("TEXT"); err != nil {
|
||||||
|
return fmt.Errorf("offer TEXT: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled := make(chan struct{})
|
||||||
|
pasted := make(chan struct{}, 1)
|
||||||
|
sendErr := make(chan error, 1)
|
||||||
|
|
||||||
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
|
defer syscall.Close(e.Fd)
|
||||||
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
|
defer file.Close()
|
||||||
|
if err := writeTo(file); err != nil {
|
||||||
|
select {
|
||||||
|
case sendErr <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case pasted <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||||
|
close(cancelled)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := device.SetSelection(source); err != nil {
|
||||||
|
return fmt.Errorf("set selection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil
|
||||||
|
case err := <-sendErr:
|
||||||
|
return err
|
||||||
|
case <-pasted:
|
||||||
|
if pasteOnce {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyText(text string) error {
|
||||||
|
return Copy([]byte(text), "text/plain;charset=utf-8")
|
||||||
|
}
|
||||||
|
|
||||||
|
func Paste() ([]byte, string, error) {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer display.Destroy()
|
||||||
|
|
||||||
|
ctx := display.Context()
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", 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(ctx)
|
||||||
|
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(ctx)
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||||
|
bindErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if bindErr != nil {
|
||||||
|
return nil, "", fmt.Errorf("registry bind: %w", bindErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if dataControlMgr == nil {
|
||||||
|
return nil, "", fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
|
}
|
||||||
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
|
if seat == nil {
|
||||||
|
return nil, "", fmt.Errorf("no seat available")
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := dataControlMgr.GetDataDevice(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
|
||||||
|
})
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
if !gotSelection || selectionOffer == nil {
|
||||||
|
return nil, "", fmt.Errorf("no clipboard data")
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeTypes := offerMimeTypes[selectionOffer]
|
||||||
|
selectedMime := selectPreferredMimeType(mimeTypes)
|
||||||
|
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()
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return len(mime) > 6 && mime[:6] == "image/"
|
||||||
|
}
|
||||||
|
|
||||||
|
type Offer struct {
|
||||||
|
MimeType string
|
||||||
|
Data []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||||
|
if !foreground {
|
||||||
|
return copyMultiFork(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyMultiServe(offers, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||||
|
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||||
|
if pasteOnce {
|
||||||
|
args = append(args, "--paste-once")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
cmd.Stdout = nil
|
||||||
|
cmd.Stderr = nil
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offer := range offers {
|
||||||
|
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||||
|
if _, err := stdin.Write(offer.Data); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
return fmt.Errorf("write offer data: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
|
}
|
||||||
|
defer display.Destroy()
|
||||||
|
|
||||||
|
ctx := 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(ctx)
|
||||||
|
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(ctx)
|
||||||
|
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()
|
||||||
|
|
||||||
|
source, err := dataControlMgr.CreateDataSource()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create data source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
offerMap := make(map[string][]byte)
|
||||||
|
for _, offer := range offers {
|
||||||
|
if err := source.Offer(offer.MimeType); err != nil {
|
||||||
|
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||||
|
}
|
||||||
|
offerMap[offer.MimeType] = offer.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelled := make(chan struct{})
|
||||||
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
|
defer syscall.Close(e.Fd)
|
||||||
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
|
file.Write(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case pasted <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||||
|
close(cancelled)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := device.SetSelection(source); err != nil {
|
||||||
|
return fmt.Errorf("set selection: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
display.Roundtrip()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancelled:
|
||||||
|
return nil
|
||||||
|
case <-pasted:
|
||||||
|
if pasteOnce {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := ctx.Dispatch(); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
248
core/internal/clipboard/store.go
Normal file
248
core/internal/clipboard/store.go
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
_ "image/gif"
|
||||||
|
_ "image/jpeg"
|
||||||
|
_ "image/png"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "golang.org/x/image/bmp"
|
||||||
|
_ "golang.org/x/image/tiff"
|
||||||
|
"hash/fnv"
|
||||||
|
|
||||||
|
bolt "go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StoreConfig struct {
|
||||||
|
MaxHistory int
|
||||||
|
MaxEntrySize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultStoreConfig() StoreConfig {
|
||||||
|
return StoreConfig{
|
||||||
|
MaxHistory: 100,
|
||||||
|
MaxEntrySize: 5 * 1024 * 1024,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
ID uint64
|
||||||
|
Data []byte
|
||||||
|
MimeType string
|
||||||
|
Preview string
|
||||||
|
Size int
|
||||||
|
Timestamp time.Time
|
||||||
|
IsImage bool
|
||||||
|
Hash uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func Store(data []byte, mimeType string) error {
|
||||||
|
return StoreWithConfig(data, mimeType, DefaultStoreConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if int64(len(data)) > cfg.MaxEntrySize {
|
||||||
|
return fmt.Errorf("data too large: %d > %d", len(data), cfg.MaxEntrySize)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbPath, err := GetDBPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get db path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open db: %w", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
entry := Entry{
|
||||||
|
Data: data,
|
||||||
|
MimeType: mimeType,
|
||||||
|
Size: len(data),
|
||||||
|
Timestamp: time.Now(),
|
||||||
|
IsImage: IsImageMimeType(mimeType),
|
||||||
|
Hash: computeHash(data),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case entry.IsImage:
|
||||||
|
entry.Preview = imagePreview(data, mimeType)
|
||||||
|
default:
|
||||||
|
entry.Preview = textPreview(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Update(func(tx *bolt.Tx) error {
|
||||||
|
b, err := tx.CreateBucketIfNotExists([]byte("clipboard"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := deduplicateInTx(b, entry.Hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := b.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
entry.ID = id
|
||||||
|
|
||||||
|
encoded, err := encodeEntry(entry)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Put(itob(id), encoded); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return trimLengthInTx(b, cfg.MaxHistory)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDBPath() (string, error) {
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
cacheDir = filepath.Join(homeDir, ".cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
newDir := filepath.Join(cacheDir, "DankMaterialShell", "clipboard")
|
||||||
|
newPath := filepath.Join(newDir, "db")
|
||||||
|
|
||||||
|
if _, err := os.Stat(newPath); err == nil {
|
||||||
|
return newPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
oldDir := filepath.Join(cacheDir, "dms-clipboard")
|
||||||
|
oldPath := filepath.Join(oldDir, "db")
|
||||||
|
|
||||||
|
if _, err := os.Stat(oldPath); err == nil {
|
||||||
|
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := os.Rename(oldPath, newPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
os.Remove(oldDir)
|
||||||
|
return newPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return newPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||||
|
c := b.Cursor()
|
||||||
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
|
if extractHash(v) != hash {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimLengthInTx(b *bolt.Bucket, maxHistory int) error {
|
||||||
|
c := b.Cursor()
|
||||||
|
var count int
|
||||||
|
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||||
|
if count < maxHistory {
|
||||||
|
count++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := b.Delete(k); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeEntry(e Entry) ([]byte, error) {
|
||||||
|
buf := new(bytes.Buffer)
|
||||||
|
|
||||||
|
binary.Write(buf, binary.BigEndian, e.ID)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(len(e.Data)))
|
||||||
|
buf.Write(e.Data)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(len(e.MimeType)))
|
||||||
|
buf.WriteString(e.MimeType)
|
||||||
|
binary.Write(buf, binary.BigEndian, uint32(len(e.Preview)))
|
||||||
|
buf.WriteString(e.Preview)
|
||||||
|
binary.Write(buf, binary.BigEndian, int32(e.Size))
|
||||||
|
binary.Write(buf, binary.BigEndian, e.Timestamp.Unix())
|
||||||
|
if e.IsImage {
|
||||||
|
buf.WriteByte(1)
|
||||||
|
} else {
|
||||||
|
buf.WriteByte(0)
|
||||||
|
}
|
||||||
|
binary.Write(buf, binary.BigEndian, e.Hash)
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func itob(v uint64) []byte {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
binary.BigEndian.PutUint64(b, v)
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func computeHash(data []byte) uint64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write(data)
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractHash(data []byte) uint64 {
|
||||||
|
if len(data) < 8 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint64(data[len(data)-8:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func textPreview(data []byte) string {
|
||||||
|
text := string(data)
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
text = strings.Join(strings.Fields(text), " ")
|
||||||
|
|
||||||
|
if len(text) > 100 {
|
||||||
|
return text[:100] + "…"
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePreview(data []byte, format string) string {
|
||||||
|
config, imgFmt, err := image.DecodeConfig(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("[[ image %s %s ]]", sizeStr(len(data)), format)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sizeStr(size int) string {
|
||||||
|
units := []string{"B", "KiB", "MiB"}
|
||||||
|
var i int
|
||||||
|
fsize := float64(size)
|
||||||
|
for fsize >= 1024 && i < len(units)-1 {
|
||||||
|
fsize /= 1024
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.0f %s", fsize, units[i])
|
||||||
|
}
|
||||||
303
core/internal/clipboard/watch.go
Normal file
303
core/internal/clipboard/watch.go
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
package clipboard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClipboardChange struct {
|
||||||
|
Data []byte
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
mimesCopy := make([]string, len(mimes))
|
||||||
|
copy(mimesCopy, mimes)
|
||||||
|
|
||||||
|
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, mimesCopy)
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
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 isTimeoutError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ type Output struct {
|
|||||||
height int32
|
height int32
|
||||||
scale int32
|
scale int32
|
||||||
fractionalScale float64
|
fractionalScale float64
|
||||||
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
type LayerSurface struct {
|
type LayerSurface struct {
|
||||||
@@ -220,10 +221,7 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
|
|
||||||
case client.OutputInterfaceName:
|
case client.OutputInterfaceName:
|
||||||
output := client.NewOutput(p.ctx)
|
output := client.NewOutput(p.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 4)
|
||||||
if version > 4 {
|
|
||||||
version = 4
|
|
||||||
}
|
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, output); err == nil {
|
||||||
p.outputsMu.Lock()
|
p.outputsMu.Lock()
|
||||||
p.outputs[e.Name] = &Output{
|
p.outputs[e.Name] = &Output{
|
||||||
@@ -238,20 +236,14 @@ func (p *Picker) handleGlobal(e client.RegistryGlobalEvent) {
|
|||||||
|
|
||||||
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
case wlr_layer_shell.ZwlrLayerShellV1InterfaceName:
|
||||||
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
layerShell := wlr_layer_shell.NewZwlrLayerShellV1(p.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 4)
|
||||||
if version > 4 {
|
|
||||||
version = 4
|
|
||||||
}
|
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, layerShell); err == nil {
|
||||||
p.layerShell = layerShell
|
p.layerShell = layerShell
|
||||||
}
|
}
|
||||||
|
|
||||||
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
case wlr_screencopy.ZwlrScreencopyManagerV1InterfaceName:
|
||||||
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
screencopy := wlr_screencopy.NewZwlrScreencopyManagerV1(p.ctx)
|
||||||
version := e.Version
|
version := min(e.Version, 3)
|
||||||
if version > 3 {
|
|
||||||
version = 3
|
|
||||||
}
|
|
||||||
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
if err := p.registry.Bind(e.Name, e.Interface, version, screencopy); err == nil {
|
||||||
p.screencopy = screencopy
|
p.screencopy = screencopy
|
||||||
}
|
}
|
||||||
@@ -276,6 +268,7 @@ func (p *Picker) setupOutputHandlers(name uint32, output *client.Output) {
|
|||||||
if o, ok := p.outputs[name]; ok {
|
if o, ok := p.outputs[name]; ok {
|
||||||
o.x = e.X
|
o.x = e.X
|
||||||
o.y = e.Y
|
o.y = e.Y
|
||||||
|
o.transform = int32(e.Transform)
|
||||||
}
|
}
|
||||||
p.outputsMu.Unlock()
|
p.outputsMu.Unlock()
|
||||||
})
|
})
|
||||||
@@ -485,8 +478,19 @@ func (p *Picker) captureForSurface(ls *LayerSurface) {
|
|||||||
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
frame.SetReadyHandler(func(e wlr_screencopy.ZwlrScreencopyFrameV1ReadyEvent) {
|
||||||
ls.state.OnScreencopyReady()
|
ls.state.OnScreencopyReady()
|
||||||
|
|
||||||
logicalW, _ := ls.state.LogicalSize()
|
|
||||||
screenBuf := ls.state.ScreenBuffer()
|
screenBuf := ls.state.ScreenBuffer()
|
||||||
|
if screenBuf != nil && ls.output.transform != TransformNormal {
|
||||||
|
invTransform := InverseTransform(ls.output.transform)
|
||||||
|
transformed, err := screenBuf.ApplyTransform(invTransform)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("apply transform failed", "err", err)
|
||||||
|
} else if transformed != screenBuf {
|
||||||
|
ls.state.ReplaceScreenBuffer(transformed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logicalW, _ := ls.state.LogicalSize()
|
||||||
|
screenBuf = ls.state.ScreenBuffer()
|
||||||
if logicalW > 0 && screenBuf != nil {
|
if logicalW > 0 && screenBuf != nil {
|
||||||
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
|
ls.output.fractionalScale = float64(screenBuf.Width) / float64(logicalW)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,25 @@ import "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/shm"
|
|||||||
|
|
||||||
type ShmBuffer = shm.Buffer
|
type ShmBuffer = shm.Buffer
|
||||||
|
|
||||||
|
const (
|
||||||
|
TransformNormal = shm.TransformNormal
|
||||||
|
Transform90 = shm.Transform90
|
||||||
|
Transform180 = shm.Transform180
|
||||||
|
Transform270 = shm.Transform270
|
||||||
|
TransformFlipped = shm.TransformFlipped
|
||||||
|
TransformFlipped90 = shm.TransformFlipped90
|
||||||
|
TransformFlipped180 = shm.TransformFlipped180
|
||||||
|
TransformFlipped270 = shm.TransformFlipped270
|
||||||
|
)
|
||||||
|
|
||||||
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
func CreateShmBuffer(width, height, stride int) (*ShmBuffer, error) {
|
||||||
return shm.CreateBuffer(width, height, stride)
|
return shm.CreateBuffer(width, height, stride)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InverseTransform(transform int32) int32 {
|
||||||
|
return shm.InverseTransform(transform)
|
||||||
|
}
|
||||||
|
|
||||||
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
|
func GetPixelColor(buf *ShmBuffer, x, y int) Color {
|
||||||
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
|
return GetPixelColorWithFormat(buf, x, y, FormatARGB8888)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package colorpicker
|
package colorpicker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -15,6 +16,8 @@ const (
|
|||||||
FormatXRGB8888 = shm.FormatXRGB8888
|
FormatXRGB8888 = shm.FormatXRGB8888
|
||||||
FormatABGR8888 = shm.FormatABGR8888
|
FormatABGR8888 = shm.FormatABGR8888
|
||||||
FormatXBGR8888 = shm.FormatXBGR8888
|
FormatXBGR8888 = shm.FormatXBGR8888
|
||||||
|
FormatRGB888 = shm.FormatRGB888
|
||||||
|
FormatBGR888 = shm.FormatBGR888
|
||||||
)
|
)
|
||||||
|
|
||||||
type SurfaceState struct {
|
type SurfaceState struct {
|
||||||
@@ -79,6 +82,11 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
|
|||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
bpp := format.BytesPerPixel()
|
||||||
|
if stride < width*bpp {
|
||||||
|
return fmt.Errorf("invalid stride %d for width %d (bpp=%d)", stride, width, bpp)
|
||||||
|
}
|
||||||
|
|
||||||
if s.screenBuf != nil {
|
if s.screenBuf != nil {
|
||||||
s.screenBuf.Close()
|
s.screenBuf.Close()
|
||||||
s.screenBuf = nil
|
s.screenBuf = nil
|
||||||
@@ -90,6 +98,7 @@ func (s *SurfaceState) OnScreencopyBuffer(format PixelFormat, width, height, str
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.screenBuf = buf
|
s.screenBuf = buf
|
||||||
|
s.screenBuf.Format = format
|
||||||
s.screenFormat = format
|
s.screenFormat = format
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -106,6 +115,20 @@ func (s *SurfaceState) ScreenFormat() PixelFormat {
|
|||||||
return s.screenFormat
|
return s.screenFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SurfaceState) ReplaceScreenBuffer(newBuf *ShmBuffer) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
if s.screenBuf != nil {
|
||||||
|
s.screenBuf.Close()
|
||||||
|
}
|
||||||
|
s.screenBuf = newBuf
|
||||||
|
s.screenFormat = newBuf.Format
|
||||||
|
|
||||||
|
s.recomputeScale()
|
||||||
|
s.ensureRenderBuffers()
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
|
func (s *SurfaceState) OnScreencopyFlags(flags uint32) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
s.yInverted = (flags & 1) != 0
|
s.yInverted = (flags & 1) != 0
|
||||||
@@ -120,6 +143,15 @@ func (s *SurfaceState) OnScreencopyReady() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.screenFormat.Is24Bit() {
|
||||||
|
converted, newFormat, err := s.screenBuf.ConvertTo32Bit(s.screenFormat)
|
||||||
|
if err == nil && converted != s.screenBuf {
|
||||||
|
s.screenBuf.Close()
|
||||||
|
s.screenBuf = converted
|
||||||
|
s.screenFormat = newFormat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.recomputeScale()
|
s.recomputeScale()
|
||||||
s.ensureRenderBuffers()
|
s.ensureRenderBuffers()
|
||||||
s.readyForDisplay = true
|
s.readyForDisplay = true
|
||||||
@@ -279,10 +311,10 @@ func (s *SurfaceState) Redraw() *ShmBuffer {
|
|||||||
drawMagnifierWithInversion(
|
drawMagnifierWithInversion(
|
||||||
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
dst.Data(), dst.Stride, dst.Width, dst.Height,
|
||||||
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
s.screenBuf.Data(), s.screenBuf.Stride, s.screenBuf.Width, s.screenBuf.Height,
|
||||||
px, py, picked, s.yInverted,
|
px, py, picked, s.yInverted, s.screenFormat,
|
||||||
)
|
)
|
||||||
|
|
||||||
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase)
|
drawColorPreview(dst.Data(), dst.Stride, dst.Width, dst.Height, px, py, picked, s.displayFormat, s.lowercase, s.screenFormat)
|
||||||
|
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
@@ -390,6 +422,7 @@ func drawMagnifierWithInversion(
|
|||||||
cx, cy int,
|
cx, cy int,
|
||||||
borderColor Color,
|
borderColor Color,
|
||||||
yInverted bool,
|
yInverted bool,
|
||||||
|
format PixelFormat,
|
||||||
) {
|
) {
|
||||||
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
|
if dstW <= 0 || dstH <= 0 || srcW <= 0 || srcH <= 0 {
|
||||||
return
|
return
|
||||||
@@ -407,6 +440,14 @@ func drawMagnifierWithInversion(
|
|||||||
innerRadius := float64(outerRadius - borderThickness)
|
innerRadius := float64(outerRadius - borderThickness)
|
||||||
outerRadiusF := float64(outerRadius)
|
outerRadiusF := float64(outerRadius)
|
||||||
|
|
||||||
|
var rOff, bOff int
|
||||||
|
switch format {
|
||||||
|
case FormatABGR8888, FormatXBGR8888:
|
||||||
|
rOff, bOff = 0, 2
|
||||||
|
default:
|
||||||
|
rOff, bOff = 2, 0
|
||||||
|
}
|
||||||
|
|
||||||
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
|
for dy := -outerRadius - 2; dy <= outerRadius+2; dy++ {
|
||||||
y := cy + dy
|
y := cy + dy
|
||||||
if y < 0 || y >= dstH {
|
if y < 0 || y >= dstH {
|
||||||
@@ -431,9 +472,9 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bgColor := Color{
|
bgColor := Color{
|
||||||
B: dst[dstOff+0],
|
R: dst[dstOff+rOff],
|
||||||
G: dst[dstOff+1],
|
G: dst[dstOff+1],
|
||||||
R: dst[dstOff+2],
|
B: dst[dstOff+bOff],
|
||||||
A: dst[dstOff+3],
|
A: dst[dstOff+3],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,7 +503,7 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
magColor := Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
magColor := Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
|
||||||
finalColor = blendColors(magColor, borderColor, alpha)
|
finalColor = blendColors(magColor, borderColor, alpha)
|
||||||
} else {
|
} else {
|
||||||
finalColor = borderColor
|
finalColor = borderColor
|
||||||
@@ -483,24 +524,25 @@ func drawMagnifierWithInversion(
|
|||||||
}
|
}
|
||||||
srcOff := sy*srcStride + sx*4
|
srcOff := sy*srcStride + sx*4
|
||||||
if srcOff+4 <= len(src) {
|
if srcOff+4 <= len(src) {
|
||||||
finalColor = Color{B: src[srcOff+0], G: src[srcOff+1], R: src[srcOff+2], A: 255}
|
finalColor = Color{R: src[srcOff+rOff], G: src[srcOff+1], B: src[srcOff+bOff], A: 255}
|
||||||
} else {
|
} else {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dst[dstOff+0] = finalColor.B
|
dst[dstOff+rOff] = finalColor.R
|
||||||
dst[dstOff+1] = finalColor.G
|
dst[dstOff+1] = finalColor.G
|
||||||
dst[dstOff+2] = finalColor.R
|
dst[dstOff+bOff] = finalColor.B
|
||||||
dst[dstOff+3] = 255
|
dst[dstOff+3] = 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius)
|
drawMagnifierCrosshair(dst, dstStride, dstW, dstH, cx, cy, int(innerRadius), crossThickness, crossInnerRadius, format)
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
@@ -998,7 +1040,7 @@ var fontGlyphs = map[rune][fontH]uint8{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool) {
|
func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Color, format OutputFormat, lowercase bool, pixelFormat PixelFormat) {
|
||||||
text := formatColorForPreview(c, format, lowercase)
|
text := formatColorForPreview(c, format, lowercase)
|
||||||
if len(text) == 0 {
|
if len(text) == 0 {
|
||||||
return
|
return
|
||||||
@@ -1033,9 +1075,8 @@ func drawColorPreview(data []byte, stride, width, height int, cx, cy int, c Colo
|
|||||||
y = height - boxH
|
y = height - boxH
|
||||||
}
|
}
|
||||||
|
|
||||||
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c)
|
drawFilledRect(data, stride, width, height, x, y, boxW, boxH, c, pixelFormat)
|
||||||
|
|
||||||
// Use contrasting text color based on luminance
|
|
||||||
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
|
||||||
if lum > 128 {
|
if lum > 128 {
|
||||||
@@ -1043,7 +1084,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)
|
drawText(data, stride, width, height, x+paddingX, y+paddingY, text, fg, pixelFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
|
func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string {
|
||||||
@@ -1064,7 +1105,7 @@ func formatColorForPreview(c Color, format OutputFormat, lowercase bool) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color) {
|
func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Color, format PixelFormat) {
|
||||||
if w <= 0 || h <= 0 {
|
if w <= 0 || h <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1073,6 +1114,14 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
|
|||||||
x = clamp(x, 0, width)
|
x = clamp(x, 0, width)
|
||||||
y = clamp(y, 0, height)
|
y = clamp(y, 0, height)
|
||||||
|
|
||||||
|
var rOff, bOff int
|
||||||
|
switch format {
|
||||||
|
case FormatABGR8888, FormatXBGR8888:
|
||||||
|
rOff, bOff = 0, 2
|
||||||
|
default:
|
||||||
|
rOff, bOff = 2, 0
|
||||||
|
}
|
||||||
|
|
||||||
for yy := y; yy < yEnd; yy++ {
|
for yy := y; yy < yEnd; yy++ {
|
||||||
rowOff := yy * stride
|
rowOff := yy * stride
|
||||||
for xx := x; xx < xEnd; xx++ {
|
for xx := x; xx < xEnd; xx++ {
|
||||||
@@ -1080,27 +1129,35 @@ func drawFilledRect(data []byte, stride, width, height, x, y, w, h int, col Colo
|
|||||||
if off+4 > len(data) {
|
if off+4 > len(data) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data[off+0] = col.B
|
data[off+rOff] = col.R
|
||||||
data[off+1] = col.G
|
data[off+1] = col.G
|
||||||
data[off+2] = col.R
|
data[off+bOff] = col.B
|
||||||
data[off+3] = 255
|
data[off+3] = 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawText(data []byte, stride, width, height, x, y int, text string, col Color) {
|
func drawText(data []byte, stride, width, height, x, y int, text string, col Color, format PixelFormat) {
|
||||||
for i, r := range text {
|
for i, r := range text {
|
||||||
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col)
|
drawGlyph(data, stride, width, height, x+i*(fontW+2), y, r, col, format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color) {
|
func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color, format PixelFormat) {
|
||||||
g, ok := fontGlyphs[r]
|
g, ok := fontGlyphs[r]
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for row := 0; row < fontH; row++ {
|
var rOff, bOff int
|
||||||
|
switch format {
|
||||||
|
case FormatABGR8888, FormatXBGR8888:
|
||||||
|
rOff, bOff = 0, 2
|
||||||
|
default:
|
||||||
|
rOff, bOff = 2, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for row := range fontH {
|
||||||
yy := y + row
|
yy := y + row
|
||||||
if yy < 0 || yy >= height {
|
if yy < 0 || yy >= height {
|
||||||
continue
|
continue
|
||||||
@@ -1108,7 +1165,7 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
|
|||||||
rowPattern := g[row]
|
rowPattern := g[row]
|
||||||
dstRowOff := yy * stride
|
dstRowOff := yy * stride
|
||||||
|
|
||||||
for colIdx := 0; colIdx < fontW; colIdx++ {
|
for colIdx := range fontW {
|
||||||
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
|
if (rowPattern & (1 << (fontW - 1 - colIdx))) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1123,9 +1180,9 @@ func drawGlyph(data []byte, stride, width, height, x, y int, r rune, col Color)
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
data[off+0] = col.B
|
data[off+rOff] = col.R
|
||||||
data[off+1] = col.G
|
data[off+1] = col.G
|
||||||
data[off+2] = col.R
|
data[off+bOff] = col.B
|
||||||
data[off+3] = 255
|
data[off+3] = 255
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
314
core/internal/colorpicker/state_test.go
Normal file
314
core/internal/colorpicker/state_test.go
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
package colorpicker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentPointerMotion(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 50
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := range goroutines {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := range iterations {
|
||||||
|
s.OnPointerMotion(float64(id*10+j), float64(id*10+j))
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentScaleAccess(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 30
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := range goroutines / 2 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
s.SetScale(int32(id%3 + 1))
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range goroutines / 2 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
scale := s.Scale()
|
||||||
|
assert.GreaterOrEqual(t, scale, int32(1))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentLogicalSize(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 20
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for i := range goroutines / 2 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(id int) {
|
||||||
|
defer wg.Done()
|
||||||
|
for j := range iterations {
|
||||||
|
_ = s.OnLayerConfigure(1920+id, 1080+j)
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
for range goroutines / 2 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
w, h := s.LogicalSize()
|
||||||
|
_ = w
|
||||||
|
_ = h
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentIsDone(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 30
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for range goroutines / 3 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
s.OnPointerButton(0x110, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for range goroutines / 3 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
s.OnKey(1, 1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
for range goroutines / 3 {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
picked, cancelled := s.IsDone()
|
||||||
|
_ = picked
|
||||||
|
_ = cancelled
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentIsReady(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 20
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for range goroutines {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
_ = s.IsReady()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ConcurrentSwapBuffers(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
const goroutines = 20
|
||||||
|
const iterations = 100
|
||||||
|
|
||||||
|
for range goroutines {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for range iterations {
|
||||||
|
s.SwapBuffers()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ZeroScale(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
s.SetScale(0)
|
||||||
|
assert.Equal(t, int32(1), s.Scale())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_NegativeScale(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
s.SetScale(-5)
|
||||||
|
assert.Equal(t, int32(1), s.Scale())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ZeroDimensionConfigure(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
|
||||||
|
err := s.OnLayerConfigure(0, 100)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.OnLayerConfigure(100, 0)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
err = s.OnLayerConfigure(-1, 100)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
w, h := s.LogicalSize()
|
||||||
|
assert.Equal(t, 0, w)
|
||||||
|
assert.Equal(t, 0, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_PickColorNilBuffer(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
color, ok := s.PickColor()
|
||||||
|
assert.False(t, ok)
|
||||||
|
assert.Equal(t, Color{}, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_RedrawNilBuffer(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.Redraw()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_RedrawScreenOnlyNilBuffer(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.RedrawScreenOnly()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_FrontRenderBufferNil(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.FrontRenderBuffer()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_ScreenBufferNil(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
buf := s.ScreenBuffer()
|
||||||
|
assert.Nil(t, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSurfaceState_DestroyMultipleTimes(t *testing.T) {
|
||||||
|
s := NewSurfaceState(FormatHex, false)
|
||||||
|
s.Destroy()
|
||||||
|
s.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClamp(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v, lo, hi, expected int
|
||||||
|
}{
|
||||||
|
{5, 0, 10, 5},
|
||||||
|
{-5, 0, 10, 0},
|
||||||
|
{15, 0, 10, 10},
|
||||||
|
{0, 0, 10, 0},
|
||||||
|
{10, 0, 10, 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := clamp(tt.v, tt.lo, tt.hi)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClampF(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v, lo, hi, expected float64
|
||||||
|
}{
|
||||||
|
{5.0, 0.0, 10.0, 5.0},
|
||||||
|
{-5.0, 0.0, 10.0, 0.0},
|
||||||
|
{15.0, 0.0, 10.0, 10.0},
|
||||||
|
{0.0, 0.0, 10.0, 0.0},
|
||||||
|
{10.0, 0.0, 10.0, 10.0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := clampF(tt.v, tt.lo, tt.hi)
|
||||||
|
assert.InDelta(t, tt.expected, result, 0.001)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAbs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v, expected int
|
||||||
|
}{
|
||||||
|
{5, 5},
|
||||||
|
{-5, 5},
|
||||||
|
{0, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := abs(tt.v)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlendColors(t *testing.T) {
|
||||||
|
bg := Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
fg := Color{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
|
||||||
|
result := blendColors(bg, fg, 0.0)
|
||||||
|
assert.Equal(t, bg.R, result.R)
|
||||||
|
assert.Equal(t, bg.G, result.G)
|
||||||
|
assert.Equal(t, bg.B, result.B)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, 1.0)
|
||||||
|
assert.Equal(t, fg.R, result.R)
|
||||||
|
assert.Equal(t, fg.G, result.G)
|
||||||
|
assert.Equal(t, fg.B, result.B)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, 0.5)
|
||||||
|
assert.InDelta(t, 127, int(result.R), 1)
|
||||||
|
assert.InDelta(t, 127, int(result.G), 1)
|
||||||
|
assert.InDelta(t, 127, int(result.B), 1)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, -1.0)
|
||||||
|
assert.Equal(t, bg.R, result.R)
|
||||||
|
|
||||||
|
result = blendColors(bg, fg, 2.0)
|
||||||
|
assert.Equal(t, fg.R, result.R)
|
||||||
|
}
|
||||||
@@ -46,11 +46,20 @@ func (cd *ConfigDeployer) DeployConfigurationsWithTerminal(ctx context.Context,
|
|||||||
return cd.DeployConfigurationsSelective(ctx, wm, terminal, nil, nil)
|
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) {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
var results []DeploymentResult
|
var results []DeploymentResult
|
||||||
|
|
||||||
shouldReplaceConfig := func(configType string) bool {
|
shouldReplaceConfig := func(configType string) bool {
|
||||||
@@ -64,7 +73,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
switch wm {
|
switch wm {
|
||||||
case deps.WindowManagerNiri:
|
case deps.WindowManagerNiri:
|
||||||
if shouldReplaceConfig("Niri") {
|
if shouldReplaceConfig("Niri") {
|
||||||
result, err := cd.deployNiriConfig(terminal)
|
result, err := cd.deployNiriConfig(terminal, useSystemd)
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
|
return results, fmt.Errorf("failed to deploy Niri config: %w", err)
|
||||||
@@ -72,7 +81,7 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
}
|
}
|
||||||
case deps.WindowManagerHyprland:
|
case deps.WindowManagerHyprland:
|
||||||
if shouldReplaceConfig("Hyprland") {
|
if shouldReplaceConfig("Hyprland") {
|
||||||
result, err := cd.deployHyprlandConfig(terminal)
|
result, err := cd.deployHyprlandConfig(terminal, useSystemd)
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
return results, fmt.Errorf("failed to deploy Hyprland config: %w", err)
|
||||||
@@ -110,20 +119,20 @@ func (cd *ConfigDeployer) DeployConfigurationsSelectiveWithReinstalls(ctx contex
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Niri",
|
ConfigType: "Niri",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "niri", "config.kdl"),
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
dmsDir := filepath.Join(configDir, "dms")
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -141,19 +150,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(result.BackupPath, existingData, 0o644); 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
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
polkitPath, err := cd.detectPolkitAgent()
|
|
||||||
if err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
|
||||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1"
|
|
||||||
}
|
|
||||||
|
|
||||||
var terminalCommand string
|
var terminalCommand string
|
||||||
switch terminal {
|
switch terminal {
|
||||||
case deps.TerminalGhostty:
|
case deps.TerminalGhostty:
|
||||||
@@ -166,11 +169,14 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
terminalCommand = "ghostty"
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(NiriConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
newConfig := strings.ReplaceAll(NiriConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
|
||||||
|
if !useSystemd {
|
||||||
|
newConfig = cd.transformNiriConfigForNonSystemd(newConfig, terminalCommand)
|
||||||
|
}
|
||||||
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig)
|
mergedConfig, err := cd.mergeNiriOutputSections(newConfig, existingConfig, dmsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
cd.log(fmt.Sprintf("Warning: Failed to merge output sections: %v", err))
|
||||||
} else {
|
} else {
|
||||||
@@ -179,7 +185,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal) (DeploymentRe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
@@ -203,11 +209,19 @@ 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)},
|
||||||
|
{"outputs.kdl", ""},
|
||||||
|
{"cursor.kdl", ""},
|
||||||
|
{"windowrules.kdl", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
path := filepath.Join(dmsDir, cfg.name)
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
@@ -225,7 +239,7 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -241,14 +255,14 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(GhosttyConfig), 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -259,10 +273,16 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
colorResult := DeploymentResult{
|
colorResult := DeploymentResult{
|
||||||
ConfigType: "Ghostty Colors",
|
ConfigType: "Ghostty Colors",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config-dankcolors"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "themes", "dankcolors"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
themesDir := filepath.Dir(colorResult.Path)
|
||||||
|
if err := os.MkdirAll(themesDir, 0o755); err != nil {
|
||||||
|
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
||||||
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0o644); err != nil {
|
||||||
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
colorResult.Error = fmt.Errorf("failed to write color config: %w", err)
|
||||||
return results, colorResult.Error
|
return results, colorResult.Error
|
||||||
}
|
}
|
||||||
@@ -283,7 +303,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -299,14 +319,14 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(KittyConfig), 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -320,7 +340,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-theme.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0644); err != nil {
|
if err := os.WriteFile(themeResult.Path, []byte(KittyThemeConfig), 0o644); err != nil {
|
||||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
return results, themeResult.Error
|
return results, themeResult.Error
|
||||||
}
|
}
|
||||||
@@ -334,7 +354,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "kitty", "dank-tabs.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0644); err != nil {
|
if err := os.WriteFile(tabsResult.Path, []byte(KittyTabsConfig), 0o644); err != nil {
|
||||||
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
tabsResult.Error = fmt.Errorf("failed to write tabs config: %w", err)
|
||||||
return results, tabsResult.Error
|
return results, tabsResult.Error
|
||||||
}
|
}
|
||||||
@@ -355,7 +375,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(mainResult.Path)
|
configDir := filepath.Dir(mainResult.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -371,14 +391,14 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
mainResult.BackupPath = mainResult.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(mainResult.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(mainResult.BackupPath, existingData, 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create backup: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", mainResult.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0644); err != nil {
|
if err := os.WriteFile(mainResult.Path, []byte(AlacrittyConfig), 0o644); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
mainResult.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
@@ -392,7 +412,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "alacritty", "dank-theme.toml"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0644); err != nil {
|
if err := os.WriteFile(themeResult.Path, []byte(AlacrittyThemeConfig), 0o644); err != nil {
|
||||||
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
themeResult.Error = fmt.Errorf("failed to write theme config: %w", err)
|
||||||
return results, themeResult.Error
|
return results, themeResult.Error
|
||||||
}
|
}
|
||||||
@@ -404,59 +424,31 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
|||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectPolkitAgent tries to find the polkit authentication agent on the system
|
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||||
// Prioritizes mate-polkit paths since that's what we install
|
|
||||||
func (cd *ConfigDeployer) detectPolkitAgent() (string, error) {
|
|
||||||
// Prioritize mate-polkit paths first
|
|
||||||
matePaths := []string{
|
|
||||||
"/usr/libexec/polkit-mate-authentication-agent-1", // Fedora path
|
|
||||||
"/usr/lib/mate-polkit/polkit-mate-authentication-agent-1",
|
|
||||||
"/usr/libexec/mate-polkit/polkit-mate-authentication-agent-1",
|
|
||||||
"/usr/lib/polkit-mate/polkit-mate-authentication-agent-1",
|
|
||||||
"/usr/lib/x86_64-linux-gnu/mate-polkit/polkit-mate-authentication-agent-1",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range matePaths {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
cd.log(fmt.Sprintf("Found mate-polkit agent at: %s", path))
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to other polkit agents if mate-polkit is not found
|
|
||||||
fallbackPaths := []string{
|
|
||||||
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
|
|
||||||
"/usr/libexec/polkit-gnome-authentication-agent-1",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, path := range fallbackPaths {
|
|
||||||
if _, err := os.Stat(path); err == nil {
|
|
||||||
cd.log(fmt.Sprintf("Found fallback polkit agent at: %s", path))
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("no polkit agent found in common locations")
|
|
||||||
}
|
|
||||||
|
|
||||||
// mergeNiriOutputSections extracts output sections from existing config and merges them into the new config
|
|
||||||
func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig string) (string, error) {
|
|
||||||
// Regular expression to match output sections (including commented ones)
|
|
||||||
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
outputRegex := regexp.MustCompile(`(?m)^(/-)?\s*output\s+"[^"]+"\s*\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
|
|
||||||
// Find all output sections in the existing config
|
|
||||||
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
existingOutputs := outputRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
if len(existingOutputs) == 0 {
|
if len(existingOutputs) == 0 {
|
||||||
// No output sections to merge
|
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the example output section from the new config
|
outputsPath := filepath.Join(dmsDir, "outputs.kdl")
|
||||||
|
if _, err := os.Stat(outputsPath); err != nil {
|
||||||
|
var outputsContent strings.Builder
|
||||||
|
for _, output := range existingOutputs {
|
||||||
|
outputsContent.WriteString(output)
|
||||||
|
outputsContent.WriteString("\n\n")
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
|
||||||
|
} else {
|
||||||
|
cd.log("Migrated output sections to dms/outputs.kdl")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
exampleOutputRegex := regexp.MustCompile(`(?m)^/-output "eDP-2" \{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}`)
|
||||||
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
mergedConfig := exampleOutputRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
// Find where to insert the output sections (after the input section)
|
|
||||||
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
inputEndRegex := regexp.MustCompile(`(?m)^}$`)
|
||||||
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
inputMatches := inputEndRegex.FindAllStringIndex(newConfig, -1)
|
||||||
|
|
||||||
@@ -464,7 +456,6 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
|
|||||||
return "", fmt.Errorf("could not find insertion point for output sections")
|
return "", fmt.Errorf("could not find insertion point for output sections")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert after the first closing brace (end of input section)
|
|
||||||
insertPos := inputMatches[0][1]
|
insertPos := inputMatches[0][1]
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
@@ -482,18 +473,24 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
// deployHyprlandConfig handles Hyprland configuration deployment with backup and merging
|
||||||
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (DeploymentResult, error) {
|
func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystemd bool) (DeploymentResult, error) {
|
||||||
result := DeploymentResult{
|
result := DeploymentResult{
|
||||||
ConfigType: "Hyprland",
|
ConfigType: "Hyprland",
|
||||||
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
Path: filepath.Join(os.Getenv("HOME"), ".config", "hypr", "hyprland.conf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(result.Path)
|
configDir := filepath.Dir(result.Path)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
result.Error = fmt.Errorf("failed to create config directory: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dmsDir := filepath.Join(configDir, "dms")
|
||||||
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
var existingConfig string
|
var existingConfig string
|
||||||
if _, err := os.Stat(result.Path); err == nil {
|
if _, err := os.Stat(result.Path); err == nil {
|
||||||
cd.log("Found existing Hyprland configuration")
|
cd.log("Found existing Hyprland configuration")
|
||||||
@@ -507,21 +504,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
|
|||||||
|
|
||||||
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
timestamp := time.Now().Format("2006-01-02_15-04-05")
|
||||||
result.BackupPath = result.Path + ".backup." + timestamp
|
result.BackupPath = result.Path + ".backup." + timestamp
|
||||||
if err := os.WriteFile(result.BackupPath, existingData, 0644); err != nil {
|
if err := os.WriteFile(result.BackupPath, existingData, 0o644); 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
|
||||||
}
|
}
|
||||||
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect polkit agent path
|
|
||||||
polkitPath, err := cd.detectPolkitAgent()
|
|
||||||
if err != nil {
|
|
||||||
cd.log(fmt.Sprintf("Warning: Could not detect polkit agent: %v", err))
|
|
||||||
polkitPath = "/usr/lib/mate-polkit/polkit-mate-authentication-agent-1" // fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine terminal command based on choice
|
|
||||||
var terminalCommand string
|
var terminalCommand string
|
||||||
switch terminal {
|
switch terminal {
|
||||||
case deps.TerminalGhostty:
|
case deps.TerminalGhostty:
|
||||||
@@ -531,15 +520,17 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
|
|||||||
case deps.TerminalAlacritty:
|
case deps.TerminalAlacritty:
|
||||||
terminalCommand = "alacritty"
|
terminalCommand = "alacritty"
|
||||||
default:
|
default:
|
||||||
terminalCommand = "ghostty" // fallback to ghostty
|
terminalCommand = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
newConfig := strings.ReplaceAll(HyprlandConfig, "{{POLKIT_AGENT_PATH}}", polkitPath)
|
newConfig := strings.ReplaceAll(HyprlandConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||||
newConfig = strings.ReplaceAll(newConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
|
||||||
|
if !useSystemd {
|
||||||
|
newConfig = cd.transformHyprlandConfigForNonSystemd(newConfig, terminalCommand)
|
||||||
|
}
|
||||||
|
|
||||||
// If there was an existing config, merge the monitor sections
|
|
||||||
if existingConfig != "" {
|
if existingConfig != "" {
|
||||||
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig)
|
mergedConfig, err := cd.mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
cd.log(fmt.Sprintf("Warning: Failed to merge monitor sections: %v", err))
|
||||||
} else {
|
} else {
|
||||||
@@ -548,36 +539,75 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal) (Deployme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(result.Path, []byte(newConfig), 0644); err != nil {
|
if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil {
|
||||||
result.Error = fmt.Errorf("failed to write config: %w", err)
|
result.Error = fmt.Errorf("failed to write config: %w", err)
|
||||||
return result, result.Error
|
return result, result.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := cd.deployHyprlandDmsConfigs(dmsDir, terminalCommand); err != nil {
|
||||||
|
result.Error = fmt.Errorf("failed to deploy dms configs: %w", err)
|
||||||
|
return result, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
result.Deployed = true
|
result.Deployed = true
|
||||||
cd.log("Successfully deployed Hyprland configuration")
|
cd.log("Successfully deployed Hyprland configuration")
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeHyprlandMonitorSections extracts monitor sections from existing config and merges them into the new config
|
func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalCommand string) error {
|
||||||
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig string) (string, error) {
|
configs := []struct {
|
||||||
// Regular expression to match monitor lines (including commented ones)
|
name string
|
||||||
// Matches: monitor = NAME, RESOLUTION, POSITION, SCALE, etc.
|
content string
|
||||||
// Also matches commented versions: # monitor = ...
|
}{
|
||||||
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
{"colors.conf", HyprColorsConfig},
|
||||||
|
{"layout.conf", HyprLayoutConfig},
|
||||||
|
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
|
{"outputs.conf", ""},
|
||||||
|
{"cursor.conf", ""},
|
||||||
|
{"windowrules.conf", ""},
|
||||||
|
}
|
||||||
|
|
||||||
// Find all monitor lines in the existing config
|
for _, cfg := range configs {
|
||||||
|
path := filepath.Join(dmsDir, cfg.name)
|
||||||
|
// Skip if file already exists and is not empty to preserve user modifications
|
||||||
|
if info, err := os.Stat(path); err == nil && info.Size() > 0 {
|
||||||
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write %s: %w", cfg.name, err)
|
||||||
|
}
|
||||||
|
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig, dmsDir string) (string, error) {
|
||||||
|
monitorRegex := regexp.MustCompile(`(?m)^#?\s*monitor\s*=.*$`)
|
||||||
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
existingMonitors := monitorRegex.FindAllString(existingConfig, -1)
|
||||||
|
|
||||||
if len(existingMonitors) == 0 {
|
if len(existingMonitors) == 0 {
|
||||||
// No monitor sections to merge
|
|
||||||
return newConfig, nil
|
return newConfig, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the example monitor line from the new config
|
outputsPath := filepath.Join(dmsDir, "outputs.conf")
|
||||||
|
if _, err := os.Stat(outputsPath); err != nil {
|
||||||
|
var outputsContent strings.Builder
|
||||||
|
for _, monitor := range existingMonitors {
|
||||||
|
outputsContent.WriteString(monitor)
|
||||||
|
outputsContent.WriteString("\n")
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0o644); err != nil {
|
||||||
|
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
||||||
|
} else {
|
||||||
|
cd.log("Migrated monitor sections to dms/outputs.conf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
exampleMonitorRegex := regexp.MustCompile(`(?m)^# monitor = eDP-2.*$`)
|
||||||
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
mergedConfig := exampleMonitorRegex.ReplaceAllString(newConfig, "")
|
||||||
|
|
||||||
// Find where to insert the monitor sections (after the MONITOR CONFIG header)
|
|
||||||
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
monitorHeaderRegex := regexp.MustCompile(`(?m)^# MONITOR CONFIG\n# ==================$`)
|
||||||
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
headerMatch := monitorHeaderRegex.FindStringIndex(mergedConfig)
|
||||||
|
|
||||||
@@ -585,8 +615,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
return "", fmt.Errorf("could not find MONITOR CONFIG section")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert after the header
|
insertPos := headerMatch[1] + 1
|
||||||
insertPos := headerMatch[1] + 1 // +1 for the newline
|
|
||||||
|
|
||||||
var builder strings.Builder
|
var builder strings.Builder
|
||||||
builder.WriteString(mergedConfig[:insertPos])
|
builder.WriteString(mergedConfig[:insertPos])
|
||||||
@@ -601,3 +630,70 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
|
|
||||||
return builder.String(), nil
|
return builder.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
|
lines := strings.Split(config, "\n")
|
||||||
|
var result []string
|
||||||
|
startupSectionFound := false
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "exec-once = dbus-update-activation-environment") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
||||||
|
startupSectionFound = true
|
||||||
|
result = append(result, "exec-once = dms run")
|
||||||
|
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
|
||||||
|
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
||||||
|
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
||||||
|
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
||||||
|
result = append(result, fmt.Sprintf("env = TERMINAL,%s", terminalCommand))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !startupSectionFound {
|
||||||
|
for i, line := range result {
|
||||||
|
if strings.Contains(line, "STARTUP APPS") {
|
||||||
|
insertLines := []string{
|
||||||
|
"exec-once = dms run",
|
||||||
|
"env = QT_QPA_PLATFORM,wayland;xcb",
|
||||||
|
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
||||||
|
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
||||||
|
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
||||||
|
fmt.Sprintf("env = TERMINAL,%s", terminalCommand),
|
||||||
|
}
|
||||||
|
result = append(result[:i+2], append(insertLines, result[i+2:]...)...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(result, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
|
envVars := fmt.Sprintf(`environment {
|
||||||
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
|
QT_QPA_PLATFORM "wayland;xcb"
|
||||||
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
|
QT_QPA_PLATFORMTHEME "gtk3"
|
||||||
|
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||||
|
TERMINAL "%s"
|
||||||
|
}`, terminalCommand)
|
||||||
|
|
||||||
|
config = regexp.MustCompile(`environment \{[^}]*\}`).ReplaceAllString(config, envVars)
|
||||||
|
|
||||||
|
spawnDms := `spawn-at-startup "dms" "run"`
|
||||||
|
if !strings.Contains(config, spawnDms) {
|
||||||
|
// Insert spawn-at-startup for dms after the environment block
|
||||||
|
envBlockEnd := regexp.MustCompile(`environment \{[^}]*\}`)
|
||||||
|
if loc := envBlockEnd.FindStringIndex(config); loc != nil {
|
||||||
|
config = config[:loc[1]] + "\n" + spawnDms + config[loc[1]:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package config
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -11,23 +10,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDetectPolkitAgent(t *testing.T) {
|
|
||||||
cd := &ConfigDeployer{}
|
|
||||||
|
|
||||||
// This test depends on the system having a polkit agent installed
|
|
||||||
// We'll just test that the function doesn't crash and returns some path or error
|
|
||||||
path, err := cd.detectPolkitAgent()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// If no polkit agent is found, that's okay for testing
|
|
||||||
assert.Contains(t, err.Error(), "no polkit agent found")
|
|
||||||
} else {
|
|
||||||
// If found, it should be a valid path
|
|
||||||
assert.NotEmpty(t, path)
|
|
||||||
assert.True(t, strings.Contains(path, "polkit"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeNiriOutputSections(t *testing.T) {
|
func TestMergeNiriOutputSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
@@ -179,7 +161,8 @@ layout {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig)
|
tmpDir := t.TempDir()
|
||||||
|
result, err := cd.mergeNiriOutputSections(tt.newConfig, tt.existingConfig, tmpDir)
|
||||||
|
|
||||||
if tt.wantError {
|
if tt.wantError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -237,9 +220,9 @@ func TestConfigDeploymentFlow(t *testing.T) {
|
|||||||
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
t.Run("deploy ghostty config with existing file", func(t *testing.T) {
|
||||||
existingContent := "# Old config\nfont-size = 14\n"
|
existingContent := "# Old config\nfont-size = 14\n"
|
||||||
ghosttyPath := getGhosttyPath()
|
ghosttyPath := getGhosttyPath()
|
||||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results, err := cd.deployGhosttyConfig()
|
results, err := cd.deployGhosttyConfig()
|
||||||
@@ -272,17 +255,6 @@ func getGhosttyPath() string {
|
|||||||
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
return filepath.Join(os.Getenv("HOME"), ".config", "ghostty", "config")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPolkitPathInjection(t *testing.T) {
|
|
||||||
|
|
||||||
testConfig := `spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
|
||||||
other content`
|
|
||||||
|
|
||||||
result := strings.Replace(testConfig, "{{POLKIT_AGENT_PATH}}", "/test/polkit/path", 1)
|
|
||||||
|
|
||||||
assert.Contains(t, result, `spawn-at-startup "/test/polkit/path"`)
|
|
||||||
assert.NotContains(t, result, "{{POLKIT_AGENT_PATH}}")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
func TestMergeHyprlandMonitorSections(t *testing.T) {
|
||||||
cd := &ConfigDeployer{}
|
cd := &ConfigDeployer{}
|
||||||
|
|
||||||
@@ -391,7 +363,8 @@ input {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig)
|
tmpDir := t.TempDir()
|
||||||
|
result, err := cd.mergeHyprlandMonitorSections(tt.newConfig, tt.existingConfig, tmpDir)
|
||||||
|
|
||||||
if tt.wantError {
|
if tt.wantError {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -424,7 +397,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
cd := NewConfigDeployer(logChan)
|
cd := NewConfigDeployer(logChan)
|
||||||
|
|
||||||
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
t.Run("deploy hyprland config to empty directory", func(t *testing.T) {
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty)
|
result, err := cd.deployHyprlandConfig(deps.TerminalGhostty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||||
@@ -435,7 +408,7 @@ func TestHyprlandConfigDeployment(t *testing.T) {
|
|||||||
content, err := os.ReadFile(result.Path)
|
content, err := os.ReadFile(result.Path)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
assert.Contains(t, string(content), "# MONITOR CONFIG")
|
||||||
assert.Contains(t, string(content), "bind = $mod, T, exec, $TERMINAL")
|
assert.Contains(t, string(content), "source = ./dms/binds.conf")
|
||||||
assert.Contains(t, string(content), "exec-once = ")
|
assert.Contains(t, string(content), "exec-once = ")
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -449,12 +422,12 @@ general {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
hyprPath := filepath.Join(tempDir, ".config", "hypr", "hyprland.conf")
|
||||||
err := os.MkdirAll(filepath.Dir(hyprPath), 0755)
|
err := os.MkdirAll(filepath.Dir(hyprPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
result, err := cd.deployHyprlandConfig(deps.TerminalKitty)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, "Hyprland", result.ConfigType)
|
assert.Equal(t, "Hyprland", result.ConfigType)
|
||||||
@@ -471,7 +444,7 @@ general {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
assert.Contains(t, string(newContent), "monitor = DP-1, 1920x1080@144")
|
||||||
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
assert.Contains(t, string(newContent), "monitor = HDMI-A-1, 3840x2160@60")
|
||||||
assert.Contains(t, string(newContent), "bind = $mod, T, exec, $TERMINAL")
|
assert.Contains(t, string(newContent), "source = ./dms/binds.conf")
|
||||||
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
assert.NotContains(t, string(newContent), "monitor = eDP-2")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -479,7 +452,6 @@ general {
|
|||||||
func TestNiriConfigStructure(t *testing.T) {
|
func TestNiriConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, NiriConfig, "input {")
|
assert.Contains(t, NiriConfig, "input {")
|
||||||
assert.Contains(t, NiriConfig, "layout {")
|
assert.Contains(t, NiriConfig, "layout {")
|
||||||
assert.Contains(t, NiriConfig, "{{POLKIT_AGENT_PATH}}")
|
|
||||||
|
|
||||||
assert.Contains(t, NiriBindsConfig, "binds {")
|
assert.Contains(t, NiriBindsConfig, "binds {")
|
||||||
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
assert.Contains(t, NiriBindsConfig, `spawn "{{TERMINAL_COMMAND}}"`)
|
||||||
@@ -489,18 +461,13 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
|||||||
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
assert.Contains(t, HyprlandConfig, "# MONITOR CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
assert.Contains(t, HyprlandConfig, "# STARTUP APPS")
|
||||||
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
assert.Contains(t, HyprlandConfig, "# INPUT CONFIG")
|
||||||
assert.Contains(t, HyprlandConfig, "# KEYBINDINGS")
|
assert.Contains(t, HyprlandConfig, "source = ./dms/binds.conf")
|
||||||
assert.Contains(t, HyprlandConfig, "{{POLKIT_AGENT_PATH}}")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, T, exec, $TERMINAL")
|
|
||||||
assert.Contains(t, HyprlandConfig, "bind = $mod, space, exec, dms ipc call spotlight toggle")
|
|
||||||
assert.Contains(t, HyprlandConfig, "windowrule {")
|
|
||||||
assert.Contains(t, HyprlandConfig, "match:class = ^(com\\.mitchellh\\.ghostty)$")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyConfigStructure(t *testing.T) {
|
func TestGhosttyConfigStructure(t *testing.T) {
|
||||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||||
assert.Contains(t, GhosttyConfig, "config-file = ./config-dankcolors")
|
assert.Contains(t, GhosttyConfig, "theme = dankcolors")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGhosttyColorConfigStructure(t *testing.T) {
|
func TestGhosttyColorConfigStructure(t *testing.T) {
|
||||||
@@ -633,9 +600,9 @@ func TestAlacrittyConfigDeployment(t *testing.T) {
|
|||||||
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
t.Run("deploy alacritty config with existing file", func(t *testing.T) {
|
||||||
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||||
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
alacrittyPath := filepath.Join(tempDir, ".config", "alacritty", "alacritty.toml")
|
||||||
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0755)
|
err := os.MkdirAll(filepath.Dir(alacrittyPath), 0o755)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0o644)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results, err := cd.deployAlacrittyConfig()
|
results, err := cd.deployAlacrittyConfig()
|
||||||
|
|||||||
@@ -7,18 +7,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// LocateDMSConfig searches for DMS installation following XDG Base Directory specification
|
|
||||||
func LocateDMSConfig() (string, error) {
|
func LocateDMSConfig() (string, error) {
|
||||||
var primaryPaths []string
|
var primaryPaths []string
|
||||||
|
|
||||||
configHome := os.Getenv("XDG_CONFIG_HOME")
|
configHome, err := os.UserConfigDir()
|
||||||
if configHome == "" {
|
if err == nil && configHome != "" {
|
||||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
|
||||||
configHome = filepath.Join(homeDir, ".config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if configHome != "" {
|
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(configHome, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,7 +21,7 @@ func LocateDMSConfig() (string, error) {
|
|||||||
dataDirs = "/usr/local/share:/usr/share"
|
dataDirs = "/usr/local/share:/usr/share"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range strings.Split(dataDirs, ":") {
|
for dir := range strings.SplitSeq(dataDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
@@ -40,7 +33,7 @@ func LocateDMSConfig() (string, error) {
|
|||||||
configDirs = "/etc/xdg"
|
configDirs = "/etc/xdg"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range strings.Split(configDirs, ":") {
|
for dir := range strings.SplitSeq(configDirs, ":") {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
primaryPaths = append(primaryPaths, filepath.Join(dir, "quickshell", "dms"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,4 +48,4 @@ keybind = shift+enter=text:\n
|
|||||||
gtk-single-instance = true
|
gtk-single-instance = true
|
||||||
|
|
||||||
# Dank color generation
|
# Dank color generation
|
||||||
config-file = ./config-dankcolors
|
theme = dankcolors
|
||||||
|
|||||||
162
core/internal/config/embedded/hypr-binds.conf
Normal file
162
core/internal/config/embedded/hypr-binds.conf
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
# === Application Launchers ===
|
||||||
|
bind = SUPER, T, exec, {{TERMINAL_COMMAND}}
|
||||||
|
bind = SUPER, space, exec, dms ipc call spotlight toggle
|
||||||
|
bind = SUPER, V, exec, dms ipc call clipboard toggle
|
||||||
|
bind = SUPER, M, exec, dms ipc call processlist focusOrToggle
|
||||||
|
bind = SUPER, comma, exec, dms ipc call settings focusOrToggle
|
||||||
|
bind = SUPER, N, exec, dms ipc call notifications toggle
|
||||||
|
bind = SUPER SHIFT, N, exec, dms ipc call notepad toggle
|
||||||
|
bind = SUPER, Y, exec, dms ipc call dankdash wallpaper
|
||||||
|
bind = SUPER, TAB, exec, dms ipc call hypr toggleOverview
|
||||||
|
bind = SUPER, X, exec, dms ipc call powermenu toggle
|
||||||
|
|
||||||
|
# === Cheat sheet
|
||||||
|
bind = SUPER SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
||||||
|
|
||||||
|
# === Security ===
|
||||||
|
bind = SUPER ALT, L, exec, dms ipc call lock lock
|
||||||
|
bind = SUPER SHIFT, E, exit
|
||||||
|
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
||||||
|
|
||||||
|
# === Audio Controls ===
|
||||||
|
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
||||||
|
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
||||||
|
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
||||||
|
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
||||||
|
bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
||||||
|
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
||||||
|
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
||||||
|
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
||||||
|
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
|
||||||
|
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
|
||||||
|
|
||||||
|
# === Brightness Controls ===
|
||||||
|
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||||
|
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
||||||
|
|
||||||
|
# === Window Management ===
|
||||||
|
bind = SUPER, Q, killactive
|
||||||
|
bind = SUPER, F, fullscreen, 1
|
||||||
|
bind = SUPER SHIFT, F, fullscreen, 0
|
||||||
|
bind = SUPER SHIFT, T, togglefloating
|
||||||
|
bind = SUPER, W, togglegroup
|
||||||
|
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
||||||
|
|
||||||
|
# === Focus Navigation ===
|
||||||
|
bind = SUPER, left, movefocus, l
|
||||||
|
bind = SUPER, down, movefocus, d
|
||||||
|
bind = SUPER, up, movefocus, u
|
||||||
|
bind = SUPER, right, movefocus, r
|
||||||
|
bind = SUPER, H, movefocus, l
|
||||||
|
bind = SUPER, J, movefocus, d
|
||||||
|
bind = SUPER, K, movefocus, u
|
||||||
|
bind = SUPER, L, movefocus, r
|
||||||
|
|
||||||
|
# === Window Movement ===
|
||||||
|
bind = SUPER SHIFT, left, movewindow, l
|
||||||
|
bind = SUPER SHIFT, down, movewindow, d
|
||||||
|
bind = SUPER SHIFT, up, movewindow, u
|
||||||
|
bind = SUPER SHIFT, right, movewindow, r
|
||||||
|
bind = SUPER SHIFT, H, movewindow, l
|
||||||
|
bind = SUPER SHIFT, J, movewindow, d
|
||||||
|
bind = SUPER SHIFT, K, movewindow, u
|
||||||
|
bind = SUPER SHIFT, L, movewindow, r
|
||||||
|
|
||||||
|
# === Column Navigation ===
|
||||||
|
bind = SUPER, Home, focuswindow, first
|
||||||
|
bind = SUPER, End, focuswindow, last
|
||||||
|
|
||||||
|
# === Monitor Navigation ===
|
||||||
|
bind = SUPER CTRL, left, focusmonitor, l
|
||||||
|
bind = SUPER CTRL, right, focusmonitor, r
|
||||||
|
bind = SUPER CTRL, H, focusmonitor, l
|
||||||
|
bind = SUPER CTRL, J, focusmonitor, d
|
||||||
|
bind = SUPER CTRL, K, focusmonitor, u
|
||||||
|
bind = SUPER CTRL, L, focusmonitor, r
|
||||||
|
|
||||||
|
# === Move to Monitor ===
|
||||||
|
bind = SUPER SHIFT CTRL, left, movewindow, mon:l
|
||||||
|
bind = SUPER SHIFT CTRL, down, movewindow, mon:d
|
||||||
|
bind = SUPER SHIFT CTRL, up, movewindow, mon:u
|
||||||
|
bind = SUPER SHIFT CTRL, right, movewindow, mon:r
|
||||||
|
bind = SUPER SHIFT CTRL, H, movewindow, mon:l
|
||||||
|
bind = SUPER SHIFT CTRL, J, movewindow, mon:d
|
||||||
|
bind = SUPER SHIFT CTRL, K, movewindow, mon:u
|
||||||
|
bind = SUPER SHIFT CTRL, L, movewindow, mon:r
|
||||||
|
|
||||||
|
# === Workspace Navigation ===
|
||||||
|
bind = SUPER, Page_Down, workspace, e+1
|
||||||
|
bind = SUPER, Page_Up, workspace, e-1
|
||||||
|
bind = SUPER, U, workspace, e+1
|
||||||
|
bind = SUPER, I, workspace, e-1
|
||||||
|
bind = SUPER CTRL, down, movetoworkspace, e+1
|
||||||
|
bind = SUPER CTRL, up, movetoworkspace, e-1
|
||||||
|
bind = SUPER CTRL, U, movetoworkspace, e+1
|
||||||
|
bind = SUPER CTRL, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Workspace Management ===
|
||||||
|
bind = CTRL SHIFT, R, exec, dms ipc call workspace-rename open
|
||||||
|
|
||||||
|
# === Move Workspaces ===
|
||||||
|
bind = SUPER SHIFT, Page_Down, movetoworkspace, e+1
|
||||||
|
bind = SUPER SHIFT, Page_Up, movetoworkspace, e-1
|
||||||
|
bind = SUPER SHIFT, U, movetoworkspace, e+1
|
||||||
|
bind = SUPER SHIFT, I, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Mouse Wheel Navigation ===
|
||||||
|
bind = SUPER, mouse_down, workspace, e+1
|
||||||
|
bind = SUPER, mouse_up, workspace, e-1
|
||||||
|
bind = SUPER CTRL, mouse_down, movetoworkspace, e+1
|
||||||
|
bind = SUPER CTRL, mouse_up, movetoworkspace, e-1
|
||||||
|
|
||||||
|
# === Numbered Workspaces ===
|
||||||
|
bind = SUPER, 1, workspace, 1
|
||||||
|
bind = SUPER, 2, workspace, 2
|
||||||
|
bind = SUPER, 3, workspace, 3
|
||||||
|
bind = SUPER, 4, workspace, 4
|
||||||
|
bind = SUPER, 5, workspace, 5
|
||||||
|
bind = SUPER, 6, workspace, 6
|
||||||
|
bind = SUPER, 7, workspace, 7
|
||||||
|
bind = SUPER, 8, workspace, 8
|
||||||
|
bind = SUPER, 9, workspace, 9
|
||||||
|
|
||||||
|
# === Move to Numbered Workspaces ===
|
||||||
|
bind = SUPER SHIFT, 1, movetoworkspace, 1
|
||||||
|
bind = SUPER SHIFT, 2, movetoworkspace, 2
|
||||||
|
bind = SUPER SHIFT, 3, movetoworkspace, 3
|
||||||
|
bind = SUPER SHIFT, 4, movetoworkspace, 4
|
||||||
|
bind = SUPER SHIFT, 5, movetoworkspace, 5
|
||||||
|
bind = SUPER SHIFT, 6, movetoworkspace, 6
|
||||||
|
bind = SUPER SHIFT, 7, movetoworkspace, 7
|
||||||
|
bind = SUPER SHIFT, 8, movetoworkspace, 8
|
||||||
|
bind = SUPER SHIFT, 9, movetoworkspace, 9
|
||||||
|
|
||||||
|
# === Column Management ===
|
||||||
|
bind = SUPER, bracketleft, layoutmsg, preselect l
|
||||||
|
bind = SUPER, bracketright, layoutmsg, preselect r
|
||||||
|
|
||||||
|
# === Sizing & Layout ===
|
||||||
|
bind = SUPER, R, layoutmsg, togglesplit
|
||||||
|
bind = SUPER CTRL, F, resizeactive, exact 100%
|
||||||
|
|
||||||
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||||
|
bindmd = SUPER, mouse:273, Resize window, resizewindow
|
||||||
|
|
||||||
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
|
bindd = SUPER, code:20, Expand window left, resizeactive, -100 0
|
||||||
|
bindd = SUPER, code:21, Shrink window left, resizeactive, 100 0
|
||||||
|
|
||||||
|
# === Manual Sizing ===
|
||||||
|
binde = SUPER, minus, resizeactive, -10% 0
|
||||||
|
binde = SUPER, equal, resizeactive, 10% 0
|
||||||
|
binde = SUPER SHIFT, minus, resizeactive, 0 -10%
|
||||||
|
binde = SUPER SHIFT, equal, resizeactive, 0 10%
|
||||||
|
|
||||||
|
# === Screenshots ===
|
||||||
|
bind = , Print, exec, dms screenshot
|
||||||
|
bind = CTRL, Print, exec, dms screenshot full
|
||||||
|
bind = ALT, Print, exec, dms screenshot window
|
||||||
|
|
||||||
|
# === System Controls ===
|
||||||
|
bind = SUPER SHIFT, P, dpms, toggle
|
||||||
25
core/internal/config/embedded/hypr-colors.conf
Normal file
25
core/internal/config/embedded/hypr-colors.conf
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# ! Auto-generated file. Do not edit directly.
|
||||||
|
# Remove source = ./dms/colors.conf from your config to override.
|
||||||
|
|
||||||
|
$primary = rgb(d0bcff)
|
||||||
|
$outline = rgb(948f99)
|
||||||
|
$error = rgb(f2b8b5)
|
||||||
|
|
||||||
|
general {
|
||||||
|
col.active_border = $primary
|
||||||
|
col.inactive_border = $outline
|
||||||
|
}
|
||||||
|
|
||||||
|
group {
|
||||||
|
col.border_active = $primary
|
||||||
|
col.border_inactive = $outline
|
||||||
|
col.border_locked_active = $error
|
||||||
|
col.border_locked_inactive = $outline
|
||||||
|
|
||||||
|
groupbar {
|
||||||
|
col.active = $primary
|
||||||
|
col.inactive = $outline
|
||||||
|
col.locked_active = $error
|
||||||
|
col.locked_inactive = $outline
|
||||||
|
}
|
||||||
|
}
|
||||||
11
core/internal/config/embedded/hypr-layout.conf
Normal file
11
core/internal/config/embedded/hypr-layout.conf
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# Auto-generated by DMS - do not edit manually
|
||||||
|
|
||||||
|
general {
|
||||||
|
gaps_in = 4
|
||||||
|
gaps_out = 4
|
||||||
|
border_size = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
decoration {
|
||||||
|
rounding = 12
|
||||||
|
}
|
||||||
@@ -10,8 +10,8 @@ monitor = , preferred,auto,auto
|
|||||||
# ==================
|
# ==================
|
||||||
# STARTUP APPS
|
# STARTUP APPS
|
||||||
# ==================
|
# ==================
|
||||||
exec-once = bash -c "wl-paste --watch cliphist store &"
|
exec-once = dbus-update-activation-environment --systemd --all
|
||||||
exec-once = {{POLKIT_AGENT_PATH}}
|
exec-once = systemctl --user start hyprland-session.target
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# INPUT CONFIG
|
# INPUT CONFIG
|
||||||
@@ -27,10 +27,7 @@ input {
|
|||||||
general {
|
general {
|
||||||
gaps_in = 5
|
gaps_in = 5
|
||||||
gaps_out = 5
|
gaps_out = 5
|
||||||
border_size = 0 # off in niri
|
border_size = 2
|
||||||
|
|
||||||
col.active_border = rgba(707070ff)
|
|
||||||
col.inactive_border = rgba(d0d0d0ff)
|
|
||||||
|
|
||||||
layout = dwindle
|
layout = dwindle
|
||||||
}
|
}
|
||||||
@@ -42,7 +39,7 @@ decoration {
|
|||||||
rounding = 12
|
rounding = 12
|
||||||
|
|
||||||
active_opacity = 1.0
|
active_opacity = 1.0
|
||||||
inactive_opacity = 0.9
|
inactive_opacity = 1.0
|
||||||
|
|
||||||
shadow {
|
shadow {
|
||||||
enabled = true
|
enabled = true
|
||||||
@@ -84,292 +81,40 @@ master {
|
|||||||
misc {
|
misc {
|
||||||
disable_hyprland_logo = true
|
disable_hyprland_logo = true
|
||||||
disable_splash_rendering = true
|
disable_splash_rendering = true
|
||||||
vrr = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
# WINDOW RULES
|
# WINDOW RULES
|
||||||
# ==================
|
# ==================
|
||||||
windowrule {
|
windowrule = tile on, match:class ^(org\.wezfurlong\.wezterm)$
|
||||||
name = windowrule-1
|
|
||||||
tile = on
|
|
||||||
match:class = ^(org\.wezfurlong\.wezterm)$
|
|
||||||
border_size = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
windowrule = rounding 12, match:class ^(org\.gnome\.)
|
||||||
|
|
||||||
windowrule {
|
windowrule = tile on, match:class ^(gnome-control-center)$
|
||||||
name = windowrule-2
|
windowrule = tile on, match:class ^(pavucontrol)$
|
||||||
rounding = 12
|
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||||
match:class = ^(org\.gnome\.)
|
|
||||||
border_size = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
windowrule = float on, match:class ^(gnome-calculator)$
|
||||||
|
windowrule = float on, match:class ^(galculator)$
|
||||||
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
|
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||||
|
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
||||||
|
|
||||||
|
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||||
|
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||||
|
|
||||||
windowrule {
|
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||||
name = windowrule-3
|
windowrule = float on, match:class ^(zoom)$
|
||||||
tile = on
|
|
||||||
match:class = ^(gnome-control-center)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
# DMS windows floating by default
|
||||||
name = windowrule-4
|
# ! Hyprland doesn't size these windows correctly so disabling by default here
|
||||||
tile = on
|
# windowrule = float on, match:class ^(org.quickshell)$
|
||||||
match:class = ^(pavucontrol)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
name = windowrule-5
|
layerrule = no_anim on, match:namespace ^dms:.*
|
||||||
tile = on
|
|
||||||
match:class = ^(nm-connection-editor)$
|
|
||||||
}
|
|
||||||
|
|
||||||
|
source = ./dms/colors.conf
|
||||||
windowrule {
|
source = ./dms/outputs.conf
|
||||||
name = windowrule-6
|
source = ./dms/layout.conf
|
||||||
float = on
|
source = ./dms/cursor.conf
|
||||||
match:class = ^(gnome-calculator)$
|
source = ./dms/binds.conf
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-7
|
|
||||||
float = on
|
|
||||||
match:class = ^(galculator)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-8
|
|
||||||
float = on
|
|
||||||
match:class = ^(blueman-manager)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-9
|
|
||||||
float = on
|
|
||||||
match:class = ^(org\.gnome\.Nautilus)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-10
|
|
||||||
float = on
|
|
||||||
match:class = ^(steam)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-11
|
|
||||||
float = on
|
|
||||||
match:class = ^(xdg-desktop-portal)$
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-12
|
|
||||||
border_size = 0
|
|
||||||
match:class = ^(Alacritty)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-13
|
|
||||||
border_size = 0
|
|
||||||
match:class = ^(zen)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-14
|
|
||||||
border_size = 0
|
|
||||||
match:class = ^(com\.mitchellh\.ghostty)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-15
|
|
||||||
border_size = 0
|
|
||||||
match:class = ^(kitty)$
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-16
|
|
||||||
float = on
|
|
||||||
match:class = ^(firefox)$
|
|
||||||
match:title = ^(Picture-in-Picture)$
|
|
||||||
}
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-17
|
|
||||||
float = on
|
|
||||||
match:class = ^(zoom)$
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
windowrule {
|
|
||||||
name = windowrule-18
|
|
||||||
opacity = 0.9 0.9
|
|
||||||
match:float = 0
|
|
||||||
match:focus = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
layerrule {
|
|
||||||
name = layerrule-1
|
|
||||||
no_anim = on
|
|
||||||
match:namespace = ^(quickshell)$
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================
|
|
||||||
# KEYBINDINGS
|
|
||||||
# ==================
|
|
||||||
$mod = SUPER
|
|
||||||
|
|
||||||
# === Application Launchers ===
|
|
||||||
bind = $mod, T, exec, $TERMINAL
|
|
||||||
bind = $mod, space, exec, dms ipc call spotlight toggle
|
|
||||||
bind = $mod, V, exec, dms ipc call clipboard toggle
|
|
||||||
bind = $mod, M, exec, dms ipc call processlist focusOrToggle
|
|
||||||
bind = $mod, comma, exec, dms ipc call settings focusOrToggle
|
|
||||||
bind = $mod, N, exec, dms ipc call notifications toggle
|
|
||||||
bind = $mod SHIFT, N, exec, dms ipc call notepad toggle
|
|
||||||
bind = $mod, Y, exec, dms ipc call dankdash wallpaper
|
|
||||||
bind = $mod, TAB, exec, dms ipc call hypr toggleOverview
|
|
||||||
|
|
||||||
# === Cheat sheet
|
|
||||||
bind = $mod SHIFT, Slash, exec, dms ipc call keybinds toggle hyprland
|
|
||||||
|
|
||||||
# === Security ===
|
|
||||||
bind = $mod ALT, L, exec, dms ipc call lock lock
|
|
||||||
bind = $mod SHIFT, E, exit
|
|
||||||
bind = CTRL ALT, Delete, exec, dms ipc call processlist focusOrToggle
|
|
||||||
|
|
||||||
# === Audio Controls ===
|
|
||||||
bindel = , XF86AudioRaiseVolume, exec, dms ipc call audio increment 3
|
|
||||||
bindel = , XF86AudioLowerVolume, exec, dms ipc call audio decrement 3
|
|
||||||
bindl = , XF86AudioMute, exec, dms ipc call audio mute
|
|
||||||
bindl = , XF86AudioMicMute, exec, dms ipc call audio micmute
|
|
||||||
|
|
||||||
# === Brightness Controls ===
|
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
|
||||||
bindel = , XF86MonBrightnessDown, exec, dms ipc call brightness decrement 5 ""
|
|
||||||
|
|
||||||
# === Window Management ===
|
|
||||||
bind = $mod, Q, killactive
|
|
||||||
bind = $mod, F, fullscreen, 1
|
|
||||||
bind = $mod SHIFT, F, fullscreen, 0
|
|
||||||
bind = $mod SHIFT, T, togglefloating
|
|
||||||
bind = $mod, W, togglegroup
|
|
||||||
|
|
||||||
# === Focus Navigation ===
|
|
||||||
bind = $mod, left, movefocus, l
|
|
||||||
bind = $mod, down, movefocus, d
|
|
||||||
bind = $mod, up, movefocus, u
|
|
||||||
bind = $mod, right, movefocus, r
|
|
||||||
bind = $mod, H, movefocus, l
|
|
||||||
bind = $mod, J, movefocus, d
|
|
||||||
bind = $mod, K, movefocus, u
|
|
||||||
bind = $mod, L, movefocus, r
|
|
||||||
|
|
||||||
# === Window Movement ===
|
|
||||||
bind = $mod SHIFT, left, movewindow, l
|
|
||||||
bind = $mod SHIFT, down, movewindow, d
|
|
||||||
bind = $mod SHIFT, up, movewindow, u
|
|
||||||
bind = $mod SHIFT, right, movewindow, r
|
|
||||||
bind = $mod SHIFT, H, movewindow, l
|
|
||||||
bind = $mod SHIFT, J, movewindow, d
|
|
||||||
bind = $mod SHIFT, K, movewindow, u
|
|
||||||
bind = $mod SHIFT, L, movewindow, r
|
|
||||||
|
|
||||||
# === Column Navigation ===
|
|
||||||
bind = $mod, Home, focuswindow, first
|
|
||||||
bind = $mod, End, focuswindow, last
|
|
||||||
|
|
||||||
# === Monitor Navigation ===
|
|
||||||
bind = $mod CTRL, left, focusmonitor, l
|
|
||||||
bind = $mod CTRL, right, focusmonitor, r
|
|
||||||
bind = $mod CTRL, H, focusmonitor, l
|
|
||||||
bind = $mod CTRL, J, focusmonitor, d
|
|
||||||
bind = $mod CTRL, K, focusmonitor, u
|
|
||||||
bind = $mod CTRL, L, focusmonitor, r
|
|
||||||
|
|
||||||
# === Move to Monitor ===
|
|
||||||
bind = $mod SHIFT CTRL, left, movewindow, mon:l
|
|
||||||
bind = $mod SHIFT CTRL, down, movewindow, mon:d
|
|
||||||
bind = $mod SHIFT CTRL, up, movewindow, mon:u
|
|
||||||
bind = $mod SHIFT CTRL, right, movewindow, mon:r
|
|
||||||
bind = $mod SHIFT CTRL, H, movewindow, mon:l
|
|
||||||
bind = $mod SHIFT CTRL, J, movewindow, mon:d
|
|
||||||
bind = $mod SHIFT CTRL, K, movewindow, mon:u
|
|
||||||
bind = $mod SHIFT CTRL, L, movewindow, mon:r
|
|
||||||
|
|
||||||
# === Workspace Navigation ===
|
|
||||||
bind = $mod, Page_Down, workspace, e+1
|
|
||||||
bind = $mod, Page_Up, workspace, e-1
|
|
||||||
bind = $mod, U, workspace, e+1
|
|
||||||
bind = $mod, I, workspace, e-1
|
|
||||||
bind = $mod CTRL, down, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, up, movetoworkspace, e-1
|
|
||||||
bind = $mod CTRL, U, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Move Workspaces ===
|
|
||||||
bind = $mod SHIFT, Page_Down, movetoworkspace, e+1
|
|
||||||
bind = $mod SHIFT, Page_Up, movetoworkspace, e-1
|
|
||||||
bind = $mod SHIFT, U, movetoworkspace, e+1
|
|
||||||
bind = $mod SHIFT, I, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Mouse Wheel Navigation ===
|
|
||||||
bind = $mod, mouse_down, workspace, e+1
|
|
||||||
bind = $mod, mouse_up, workspace, e-1
|
|
||||||
bind = $mod CTRL, mouse_down, movetoworkspace, e+1
|
|
||||||
bind = $mod CTRL, mouse_up, movetoworkspace, e-1
|
|
||||||
|
|
||||||
# === Numbered Workspaces ===
|
|
||||||
bind = $mod, 1, workspace, 1
|
|
||||||
bind = $mod, 2, workspace, 2
|
|
||||||
bind = $mod, 3, workspace, 3
|
|
||||||
bind = $mod, 4, workspace, 4
|
|
||||||
bind = $mod, 5, workspace, 5
|
|
||||||
bind = $mod, 6, workspace, 6
|
|
||||||
bind = $mod, 7, workspace, 7
|
|
||||||
bind = $mod, 8, workspace, 8
|
|
||||||
bind = $mod, 9, workspace, 9
|
|
||||||
|
|
||||||
# === Move to Numbered Workspaces ===
|
|
||||||
bind = $mod SHIFT, 1, movetoworkspace, 1
|
|
||||||
bind = $mod SHIFT, 2, movetoworkspace, 2
|
|
||||||
bind = $mod SHIFT, 3, movetoworkspace, 3
|
|
||||||
bind = $mod SHIFT, 4, movetoworkspace, 4
|
|
||||||
bind = $mod SHIFT, 5, movetoworkspace, 5
|
|
||||||
bind = $mod SHIFT, 6, movetoworkspace, 6
|
|
||||||
bind = $mod SHIFT, 7, movetoworkspace, 7
|
|
||||||
bind = $mod SHIFT, 8, movetoworkspace, 8
|
|
||||||
bind = $mod SHIFT, 9, movetoworkspace, 9
|
|
||||||
|
|
||||||
# === Column Management ===
|
|
||||||
bind = $mod, bracketleft, layoutmsg, preselect l
|
|
||||||
bind = $mod, bracketright, layoutmsg, preselect r
|
|
||||||
|
|
||||||
# === Sizing & Layout ===
|
|
||||||
bind = $mod, R, layoutmsg, togglesplit
|
|
||||||
bind = $mod CTRL, F, resizeactive, exact 100%
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindmd = $mod, mouse:272, Move window, movewindow
|
|
||||||
bindmd = $mod, mouse:273, Resize window, resizewindow
|
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
|
||||||
bindd = $mod, code:20, Expand window left, resizeactive, -100 0
|
|
||||||
bindd = $mod, code:21, Shrink window left, resizeactive, 100 0
|
|
||||||
|
|
||||||
# === Manual Sizing ===
|
|
||||||
binde = $mod, minus, resizeactive, -10% 0
|
|
||||||
binde = $mod, equal, resizeactive, 10% 0
|
|
||||||
binde = $mod SHIFT, minus, resizeactive, 0 -10%
|
|
||||||
binde = $mod SHIFT, equal, resizeactive, 0 10%
|
|
||||||
|
|
||||||
# === Screenshots ===
|
|
||||||
bind = , Print, exec, dms screenshot
|
|
||||||
bind = CTRL, Print, exec, dms screenshot full
|
|
||||||
bind = ALT, Print, exec, dms screenshot window
|
|
||||||
|
|
||||||
# === System Controls ===
|
|
||||||
bind = $mod SHIFT, P, dpms, off
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
highlight {
|
highlight {
|
||||||
corner-radius 12
|
corner-radius 12
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ binds {
|
|||||||
Mod+M hotkey-overlay-title="Task Manager" {
|
Mod+M hotkey-overlay-title="Task Manager" {
|
||||||
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
spawn "dms" "ipc" "call" "processlist" "focusOrToggle";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Super+X hotkey-overlay-title="Power Menu: Toggle" { spawn "dms" "ipc" "call" "powermenu" "toggle"; }
|
||||||
Mod+Comma hotkey-overlay-title="Settings" {
|
Mod+Comma hotkey-overlay-title="Settings" {
|
||||||
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
spawn "dms" "ipc" "call" "settings" "focusOrToggle";
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,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 {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
|
}
|
||||||
|
XF86AudioPlay allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "playPause";
|
||||||
|
}
|
||||||
|
XF86AudioPrev allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "previous";
|
||||||
|
}
|
||||||
|
XF86AudioNext allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "next";
|
||||||
|
}
|
||||||
|
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
||||||
|
}
|
||||||
|
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
||||||
|
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
||||||
|
}
|
||||||
|
|
||||||
// === Brightness Controls ===
|
// === Brightness Controls ===
|
||||||
XF86MonBrightnessUp allow-when-locked=true {
|
XF86MonBrightnessUp allow-when-locked=true {
|
||||||
@@ -62,6 +82,7 @@ binds {
|
|||||||
Mod+Shift+T { toggle-window-floating; }
|
Mod+Shift+T { toggle-window-floating; }
|
||||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||||
Mod+W { toggle-column-tabbed-display; }
|
Mod+W { toggle-column-tabbed-display; }
|
||||||
|
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
|
||||||
|
|
||||||
// === Focus Navigation ===
|
// === Focus Navigation ===
|
||||||
Mod+Left { focus-column-left; }
|
Mod+Left { focus-column-left; }
|
||||||
@@ -119,6 +140,11 @@ binds {
|
|||||||
Mod+Ctrl+U { move-column-to-workspace-down; }
|
Mod+Ctrl+U { move-column-to-workspace-down; }
|
||||||
Mod+Ctrl+I { move-column-to-workspace-up; }
|
Mod+Ctrl+I { move-column-to-workspace-up; }
|
||||||
|
|
||||||
|
// === Workspace Management ===
|
||||||
|
Ctrl+Shift+R hotkey-overlay-title="Rename Workspace" {
|
||||||
|
spawn "dms" "ipc" "call" "workspace-rename" "open";
|
||||||
|
}
|
||||||
|
|
||||||
// === Move Workspaces ===
|
// === Move Workspaces ===
|
||||||
Mod+Shift+Page_Down { move-workspace-down; }
|
Mod+Shift+Page_Down { move-workspace-down; }
|
||||||
Mod+Shift+Page_Up { move-workspace-up; }
|
Mod+Shift+Page_Up { move-workspace-up; }
|
||||||
@@ -192,4 +218,4 @@ binds {
|
|||||||
// === System Controls ===
|
// === System Controls ===
|
||||||
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
|
||||||
Mod+Shift+P { power-off-monitors; }
|
Mod+Shift+P { power-off-monitors; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
|
// ! Auto-generated file. Do not edit directly.
|
||||||
|
// Remove `include "dms/colors.kdl"` from your config to override.
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
background-color "transparent"
|
background-color "transparent"
|
||||||
|
|
||||||
focus-ring {
|
focus-ring {
|
||||||
active-color "#9dcbfb"
|
active-color "#d0bcff"
|
||||||
inactive-color "#8c9199"
|
inactive-color "#948f99"
|
||||||
urgent-color "#ffb4ab"
|
urgent-color "#f2b8b5"
|
||||||
}
|
}
|
||||||
|
|
||||||
border {
|
border {
|
||||||
active-color "#9dcbfb"
|
active-color "#d0bcff"
|
||||||
inactive-color "#8c9199"
|
inactive-color "#948f99"
|
||||||
urgent-color "#ffb4ab"
|
urgent-color "#f2b8b5"
|
||||||
}
|
}
|
||||||
|
|
||||||
shadow {
|
shadow {
|
||||||
@@ -18,19 +21,19 @@ layout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tab-indicator {
|
tab-indicator {
|
||||||
active-color "#9dcbfb"
|
active-color "#d0bcff"
|
||||||
inactive-color "#8c9199"
|
inactive-color "#948f99"
|
||||||
urgent-color "#ffb4ab"
|
urgent-color "#f2b8b5"
|
||||||
}
|
}
|
||||||
|
|
||||||
insert-hint {
|
insert-hint {
|
||||||
color "#9dcbfb80"
|
color "#d0bcff80"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recent-windows {
|
recent-windows {
|
||||||
highlight {
|
highlight {
|
||||||
active-color "#124a73"
|
active-color "#4f378b"
|
||||||
urgent-color "#ffb4ab"
|
urgent-color "#f2b8b5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
core/internal/config/embedded/niri-greeter.kdl
Normal file
17
core/internal/config/embedded/niri-greeter.kdl
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DMS_RUN_GREETER "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
gestures {
|
||||||
|
hot-corners {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
background-color "#000000"
|
||||||
|
}
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
// ! DO NOT EDIT !
|
||||||
|
// ! AUTO-GENERATED BY DMS !
|
||||||
|
// ! CHANGES WILL BE OVERWRITTEN !
|
||||||
|
// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE !
|
||||||
|
|
||||||
layout {
|
layout {
|
||||||
gaps 4
|
gaps 4
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,64 @@ gestures {
|
|||||||
input {
|
input {
|
||||||
keyboard {
|
keyboard {
|
||||||
xkb {
|
xkb {
|
||||||
|
// You can set rules, model, layout, variant and options.
|
||||||
|
// For more information, see xkeyboard-config(7).
|
||||||
|
|
||||||
|
// For example:
|
||||||
|
// layout "us,ru"
|
||||||
|
// options "grp:win_space_toggle,compose:ralt,ctrl:nocaps"
|
||||||
|
|
||||||
|
// If this section is empty, niri will fetch xkb settings
|
||||||
|
// from org.freedesktop.locale1. You can control these using
|
||||||
|
// localectl set-x11-keymap.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable numlock on startup, omitting this setting disables it.
|
||||||
numlock
|
numlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Next sections include libinput settings.
|
||||||
|
// Omitting settings disables them, or leaves them at their default values.
|
||||||
|
// All commented-out settings here are examples, not defaults.
|
||||||
touchpad {
|
touchpad {
|
||||||
|
// off
|
||||||
|
tap
|
||||||
|
// dwt
|
||||||
|
// dwtp
|
||||||
|
// drag false
|
||||||
|
// drag-lock
|
||||||
|
natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "two-finger"
|
||||||
|
// disabled-on-external-mouse
|
||||||
}
|
}
|
||||||
|
|
||||||
mouse {
|
mouse {
|
||||||
|
// off
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "no-scroll"
|
||||||
}
|
}
|
||||||
|
|
||||||
trackpoint {
|
trackpoint {
|
||||||
|
// off
|
||||||
|
// natural-scroll
|
||||||
|
// accel-speed 0.2
|
||||||
|
// accel-profile "flat"
|
||||||
|
// scroll-method "on-button-down"
|
||||||
|
// scroll-button 273
|
||||||
|
// scroll-button-lock
|
||||||
|
// middle-emulation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uncomment this to make the mouse warp to the center of newly focused windows.
|
||||||
|
// warp-mouse-to-focus
|
||||||
|
|
||||||
|
// Focus windows and outputs automatically when moving the mouse into them.
|
||||||
|
// Setting max-scroll-amount="0%" makes it work only on windows already fully on screen.
|
||||||
|
// focus-follows-mouse max-scroll-amount="0%"
|
||||||
}
|
}
|
||||||
// You can configure outputs by their name, which you can find
|
// You can configure outputs by their name, which you can find
|
||||||
// by running `niri msg outputs` while inside a niri instance.
|
// by running `niri msg outputs` while inside a niri instance.
|
||||||
@@ -44,7 +93,6 @@ input {
|
|||||||
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
// https://github.com/YaLTeR/niri/wiki/Configuration:-Layout
|
||||||
layout {
|
layout {
|
||||||
// Set gaps around windows in logical pixels.
|
// Set gaps around windows in logical pixels.
|
||||||
gaps 5
|
|
||||||
background-color "transparent"
|
background-color "transparent"
|
||||||
// When to center a column when changing focus, options are:
|
// When to center a column when changing focus, options are:
|
||||||
// - "never", default behavior, focusing an off-screen column will keep at the left
|
// - "never", default behavior, focusing an off-screen column will keep at the left
|
||||||
@@ -87,11 +135,6 @@ layout {
|
|||||||
inactive-color "#d0d0d0" // Light gray
|
inactive-color "#d0d0d0" // Light gray
|
||||||
urgent-color "#cc4444" // Softer red
|
urgent-color "#cc4444" // Softer red
|
||||||
}
|
}
|
||||||
focus-ring {
|
|
||||||
width 2
|
|
||||||
active-color "#808080" // Medium gray
|
|
||||||
inactive-color "#505050" // Dark gray
|
|
||||||
}
|
|
||||||
shadow {
|
shadow {
|
||||||
softness 30
|
softness 30
|
||||||
spread 5
|
spread 5
|
||||||
@@ -115,8 +158,6 @@ overview {
|
|||||||
// which may be more convenient to use.
|
// which may be more convenient to use.
|
||||||
// See the binds section below for more spawn examples.
|
// See the binds section below for more spawn examples.
|
||||||
// This line starts waybar, a commonly used bar for Wayland compositors.
|
// This line starts waybar, a commonly used bar for Wayland compositors.
|
||||||
spawn-at-startup "bash" "-c" "wl-paste --watch cliphist store &"
|
|
||||||
spawn-at-startup "{{POLKIT_AGENT_PATH}}"
|
|
||||||
environment {
|
environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
}
|
}
|
||||||
@@ -187,10 +228,14 @@ window-rule {
|
|||||||
match app-id=r#"^galculator$"#
|
match app-id=r#"^galculator$"#
|
||||||
match app-id=r#"^blueman-manager$"#
|
match app-id=r#"^blueman-manager$"#
|
||||||
match app-id=r#"^org\.gnome\.Nautilus$"#
|
match app-id=r#"^org\.gnome\.Nautilus$"#
|
||||||
match app-id=r#"^steam$"#
|
|
||||||
match app-id=r#"^xdg-desktop-portal$"#
|
match app-id=r#"^xdg-desktop-portal$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
|
window-rule {
|
||||||
|
match app-id=r#"^steam$"# title=r#"^notificationtoasts_\d+_desktop$"#
|
||||||
|
default-floating-position x=10 y=10 relative-to="bottom-right"
|
||||||
|
open-focused false
|
||||||
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||||
match app-id="Alacritty"
|
match app-id="Alacritty"
|
||||||
@@ -199,10 +244,6 @@ window-rule {
|
|||||||
match app-id="kitty"
|
match app-id="kitty"
|
||||||
draw-border-with-background false
|
draw-border-with-background false
|
||||||
}
|
}
|
||||||
window-rule {
|
|
||||||
match is-active=false
|
|
||||||
opacity 0.9
|
|
||||||
}
|
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
match app-id=r#"firefox$"# title="^Picture-in-Picture$"
|
||||||
match app-id="zoom"
|
match app-id="zoom"
|
||||||
@@ -232,3 +273,5 @@ include "dms/colors.kdl"
|
|||||||
include "dms/layout.kdl"
|
include "dms/layout.kdl"
|
||||||
include "dms/alttab.kdl"
|
include "dms/alttab.kdl"
|
||||||
include "dms/binds.kdl"
|
include "dms/binds.kdl"
|
||||||
|
include "dms/outputs.kdl"
|
||||||
|
include "dms/cursor.kdl"
|
||||||
|
|||||||
@@ -4,3 +4,12 @@ import _ "embed"
|
|||||||
|
|
||||||
//go:embed embedded/hyprland.conf
|
//go:embed embedded/hyprland.conf
|
||||||
var HyprlandConfig string
|
var HyprlandConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-colors.conf
|
||||||
|
var HyprColorsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-layout.conf
|
||||||
|
var HyprLayoutConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/hypr-binds.conf
|
||||||
|
var HyprBindsConfig string
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ var NiriAlttabConfig string
|
|||||||
|
|
||||||
//go:embed embedded/niri-binds.kdl
|
//go:embed embedded/niri-binds.kdl
|
||||||
var NiriBindsConfig string
|
var NiriBindsConfig string
|
||||||
|
|
||||||
|
//go:embed embedded/niri-greeter.kdl
|
||||||
|
var NiriGreeterConfig string
|
||||||
|
|||||||
@@ -113,13 +113,14 @@ func RGBToHSV(rgb RGB) HSV {
|
|||||||
delta := max - min
|
delta := max - min
|
||||||
|
|
||||||
var h float64
|
var h float64
|
||||||
if delta == 0 {
|
switch {
|
||||||
|
case delta == 0:
|
||||||
h = 0
|
h = 0
|
||||||
} else if max == rgb.R {
|
case max == rgb.R:
|
||||||
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
|
h = math.Mod((rgb.G-rgb.B)/delta, 6.0) / 6.0
|
||||||
} else if max == rgb.G {
|
case max == rgb.G:
|
||||||
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
|
h = ((rgb.B-rgb.R)/delta + 2.0) / 6.0
|
||||||
} else {
|
default:
|
||||||
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
|
h = ((rgb.R-rgb.G)/delta + 4.0) / 6.0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,31 +199,6 @@ func labToHex(L, a, b float64) string {
|
|||||||
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
return fmt.Sprintf("#%02x%02x%02x", r, g, b2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust brightness while keeping the same hue
|
|
||||||
func retoneToL(hex string, Ltarget float64) string {
|
|
||||||
rgb := HexToRGB(hex)
|
|
||||||
col := colorful.Color{R: rgb.R, G: rgb.G, B: rgb.B}
|
|
||||||
L, a, b := col.Lab()
|
|
||||||
L100 := L * 100.0
|
|
||||||
|
|
||||||
scale := 1.0
|
|
||||||
if L100 != 0 {
|
|
||||||
scale = Ltarget / L100
|
|
||||||
}
|
|
||||||
|
|
||||||
a2, b2 := a*scale, b*scale
|
|
||||||
|
|
||||||
// Don't let it get too saturated
|
|
||||||
maxChroma := 0.4
|
|
||||||
if math.Hypot(a2, b2) > maxChroma {
|
|
||||||
k := maxChroma / math.Hypot(a2, b2)
|
|
||||||
a2 *= k
|
|
||||||
b2 *= k
|
|
||||||
}
|
|
||||||
|
|
||||||
return labToHex(Ltarget, a2, b2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
func DeltaPhiStar(hexFg, hexBg string, negativePolarity bool) float64 {
|
||||||
Lf := getLstar(hexFg)
|
Lf := getLstar(hexFg)
|
||||||
Lb := getLstar(hexBg)
|
Lb := getLstar(hexBg)
|
||||||
@@ -344,7 +320,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
}
|
}
|
||||||
|
|
||||||
step := 0.5
|
step := 0.5
|
||||||
for i := 0; i < 120; i++ {
|
for range 120 {
|
||||||
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
Lf = math.Max(0, math.Min(100, Lf+dir*step))
|
||||||
cand := labToHex(Lf, af, bf)
|
cand := labToHex(Lf, af, bf)
|
||||||
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
@@ -355,6 +331,59 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
return hexColor
|
return hexColor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bidirectional contrast - tries both lighter and darker, picks closest to original
|
||||||
|
func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLightMode bool) string {
|
||||||
|
current := DeltaPhiStarContrast(hexColor, hexBg, isLightMode)
|
||||||
|
if current >= minLc {
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
|
fg := HexToRGB(hexColor)
|
||||||
|
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||||
|
origL, af, bf := cf.Lab()
|
||||||
|
|
||||||
|
var darkerResult, lighterResult string
|
||||||
|
darkerL, lighterL := origL, origL
|
||||||
|
darkerFound, lighterFound := false, false
|
||||||
|
|
||||||
|
step := 0.5
|
||||||
|
for i := range 120 {
|
||||||
|
if !darkerFound {
|
||||||
|
darkerL = math.Max(0, origL-float64(i)*step)
|
||||||
|
cand := labToHex(darkerL, af, bf)
|
||||||
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
|
darkerResult = cand
|
||||||
|
darkerFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !lighterFound {
|
||||||
|
lighterL = math.Min(100, origL+float64(i)*step)
|
||||||
|
cand := labToHex(lighterL, af, bf)
|
||||||
|
if DeltaPhiStarContrast(cand, hexBg, isLightMode) >= minLc {
|
||||||
|
lighterResult = cand
|
||||||
|
lighterFound = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if darkerFound && lighterFound {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if darkerFound && lighterFound {
|
||||||
|
if math.Abs(darkerL-origL) <= math.Abs(lighterL-origL) {
|
||||||
|
return darkerResult
|
||||||
|
}
|
||||||
|
return lighterResult
|
||||||
|
}
|
||||||
|
if darkerFound {
|
||||||
|
return darkerResult
|
||||||
|
}
|
||||||
|
if lighterFound {
|
||||||
|
return lighterResult
|
||||||
|
}
|
||||||
|
return hexColor
|
||||||
|
}
|
||||||
|
|
||||||
type PaletteOptions struct {
|
type PaletteOptions struct {
|
||||||
IsLight bool
|
IsLight bool
|
||||||
Background string
|
Background string
|
||||||
@@ -368,6 +397,29 @@ func ensureContrastAuto(hexColor, hexBg string, target float64, opts PaletteOpti
|
|||||||
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureContrastBidirectional(hexColor, hexBg string, target float64, opts PaletteOptions) string {
|
||||||
|
if opts.UseDPS {
|
||||||
|
return EnsureContrastDPSBidirectional(hexColor, hexBg, target, opts.IsLight)
|
||||||
|
}
|
||||||
|
return EnsureContrast(hexColor, hexBg, target, opts.IsLight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func blendHue(base, target, factor float64) float64 {
|
||||||
|
diff := target - base
|
||||||
|
if diff > 0.5 {
|
||||||
|
diff -= 1.0
|
||||||
|
} else if diff < -0.5 {
|
||||||
|
diff += 1.0
|
||||||
|
}
|
||||||
|
result := base + diff*factor
|
||||||
|
if result < 0 {
|
||||||
|
result += 1.0
|
||||||
|
} else if result >= 1.0 {
|
||||||
|
result -= 1.0
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func DeriveContainer(primary string, isLight bool) string {
|
func DeriveContainer(primary string, isLight bool) string {
|
||||||
rgb := HexToRGB(primary)
|
rgb := HexToRGB(primary)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
@@ -388,6 +440,9 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
rgb := HexToRGB(baseColor)
|
rgb := HexToRGB(baseColor)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
|
|
||||||
|
pr := HexToRGB(primaryColor)
|
||||||
|
ph := RGBToHSV(pr)
|
||||||
|
|
||||||
var palette Palette
|
var palette Palette
|
||||||
|
|
||||||
var normalTextTarget, secondaryTarget float64
|
var normalTextTarget, secondaryTarget float64
|
||||||
@@ -409,115 +464,136 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
}
|
}
|
||||||
palette.Color0 = NewColorInfo(bgColor)
|
palette.Color0 = NewColorInfo(bgColor)
|
||||||
|
|
||||||
hueShift := (hsv.H - 0.6) * 0.12
|
baseSat := math.Max(ph.S, 0.5)
|
||||||
satBoost := 1.15
|
baseVal := math.Max(ph.V, 0.5)
|
||||||
|
|
||||||
redH := math.Mod(0.0+hueShift+1.0, 1.0)
|
redH := blendHue(0.0, ph.H, 0.12)
|
||||||
var redColor string
|
greenH := blendHue(0.33, ph.H, 0.10)
|
||||||
if opts.IsLight {
|
yellowH := blendHue(0.14, ph.H, 0.04)
|
||||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.80*satBoost, 1.0), V: 0.55}))
|
|
||||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
redColor = RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.65*satBoost, 1.0), V: 0.80}))
|
|
||||||
palette.Color1 = NewColorInfo(ensureContrastAuto(redColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
greenH := math.Mod(0.33+hueShift+1.0, 1.0)
|
accentTarget := secondaryTarget * 0.7
|
||||||
var greenColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.9, 0.80)*satBoost, 1.0), V: 0.45}))
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
greenColor = RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.42*satBoost, 1.0), V: 0.84}))
|
|
||||||
palette.Color2 = NewColorInfo(ensureContrastAuto(greenColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
yellowH := math.Mod(0.15+hueShift+1.0, 1.0)
|
|
||||||
var yellowColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.75*satBoost, 1.0), V: 0.50}))
|
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
yellowColor = RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.38*satBoost, 1.0), V: 0.86}))
|
|
||||||
palette.Color3 = NewColorInfo(ensureContrastAuto(yellowColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
var blueColor string
|
|
||||||
if opts.IsLight {
|
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.9, 0.7), V: hsv.V * 1.1}))
|
|
||||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
blueColor = RGBToHex(HSVToRGB(HSV{H: hsv.H, S: math.Max(hsv.S*0.8, 0.6), V: math.Min(hsv.V*1.6, 1.0)}))
|
|
||||||
palette.Color4 = NewColorInfo(ensureContrastAuto(blueColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
magH := hsv.H - 0.03
|
|
||||||
if magH < 0 {
|
|
||||||
magH += 1.0
|
|
||||||
}
|
|
||||||
var magColor string
|
|
||||||
hr := HexToRGB(primaryColor)
|
|
||||||
hh := RGBToHSV(hr)
|
|
||||||
if opts.IsLight {
|
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Max(hh.S*0.9, 0.7), V: hh.V * 0.85}))
|
|
||||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
|
||||||
} else {
|
|
||||||
magColor = RGBToHex(HSVToRGB(HSV{H: hh.H, S: hh.S * 0.8, V: hh.V * 0.75}))
|
|
||||||
palette.Color5 = NewColorInfo(ensureContrastAuto(magColor, bgColor, normalTextTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
cyanH := hsv.H + 0.08
|
|
||||||
if cyanH > 1.0 {
|
|
||||||
cyanH -= 1.0
|
|
||||||
}
|
|
||||||
palette.Color6 = NewColorInfo(ensureContrastAuto(primaryColor, bgColor, normalTextTarget, opts))
|
|
||||||
|
|
||||||
if opts.IsLight {
|
if opts.IsLight {
|
||||||
palette.Color7 = NewColorInfo("#1a1a1a")
|
redS := math.Min(baseSat*1.2, 1.0)
|
||||||
palette.Color8 = NewColorInfo("#2e2e2e")
|
redV := baseVal * 0.95
|
||||||
} else {
|
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||||
palette.Color7 = NewColorInfo("#abb2bf")
|
|
||||||
palette.Color8 = NewColorInfo("#5c6370")
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.IsLight {
|
greenS := math.Min(baseSat*1.3, 1.0)
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.70*satBoost, 1.0), V: 0.65}))
|
greenV := baseVal * 0.75
|
||||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(math.Max(hsv.S*0.85, 0.75)*satBoost, 1.0), V: 0.55}))
|
|
||||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
|
||||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.68*satBoost, 1.0), V: 0.60}))
|
|
||||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
|
||||||
hr := HexToRGB(primaryColor)
|
|
||||||
hh := RGBToHSV(hr)
|
|
||||||
brightBlue := RGBToHex(HSVToRGB(HSV{H: hh.H, S: math.Min(hh.S*1.1, 1.0), V: math.Min(hh.V*1.2, 1.0)}))
|
|
||||||
palette.Color12 = NewColorInfo(ensureContrastAuto(brightBlue, bgColor, secondaryTarget, opts))
|
|
||||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.9, 0.75), V: math.Min(hsv.V*1.25, 1.0)}))
|
|
||||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
|
||||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: cyanH, S: math.Max(hsv.S*0.75, 0.65), V: math.Min(hsv.V*1.25, 1.0)}))
|
|
||||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
|
||||||
} else {
|
|
||||||
brightRed := RGBToHex(HSVToRGB(HSV{H: redH, S: math.Min(0.50*satBoost, 1.0), V: 0.88}))
|
|
||||||
palette.Color9 = NewColorInfo(ensureContrastAuto(brightRed, bgColor, secondaryTarget, opts))
|
|
||||||
brightGreen := RGBToHex(HSVToRGB(HSV{H: greenH, S: math.Min(0.35*satBoost, 1.0), V: 0.88}))
|
|
||||||
palette.Color10 = NewColorInfo(ensureContrastAuto(brightGreen, bgColor, secondaryTarget, opts))
|
|
||||||
brightYellow := RGBToHex(HSVToRGB(HSV{H: yellowH, S: math.Min(0.30*satBoost, 1.0), V: 0.91}))
|
|
||||||
palette.Color11 = NewColorInfo(ensureContrastAuto(brightYellow, bgColor, secondaryTarget, opts))
|
|
||||||
brightBlue := retoneToL(primaryColor, 85.0)
|
|
||||||
palette.Color12 = NewColorInfo(brightBlue)
|
|
||||||
brightMag := RGBToHex(HSVToRGB(HSV{H: magH, S: math.Max(hsv.S*0.7, 0.6), V: math.Min(hsv.V*1.3, 0.9)}))
|
|
||||||
palette.Color13 = NewColorInfo(ensureContrastAuto(brightMag, bgColor, secondaryTarget, opts))
|
|
||||||
brightCyanH := hsv.H + 0.02
|
|
||||||
if brightCyanH > 1.0 {
|
|
||||||
brightCyanH -= 1.0
|
|
||||||
}
|
|
||||||
brightCyan := RGBToHex(HSVToRGB(HSV{H: brightCyanH, S: math.Max(hsv.S*0.6, 0.5), V: math.Min(hsv.V*1.2, 0.85)}))
|
|
||||||
palette.Color14 = NewColorInfo(ensureContrastAuto(brightCyan, bgColor, secondaryTarget, opts))
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.IsLight {
|
yellowS := math.Min(baseSat*1.5, 1.0)
|
||||||
palette.Color15 = NewColorInfo("#1a1a1a")
|
yellowV := math.Min(baseVal*1.2, 1.0)
|
||||||
|
palette.Color3 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
blueS := math.Min(ph.S*1.05, 1.0)
|
||||||
|
blueV := math.Min(ph.V*1.05, 1.0)
|
||||||
|
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
// Color5 matches primary_container exactly (light container in light mode)
|
||||||
|
container5 := DeriveContainer(primaryColor, true)
|
||||||
|
palette.Color5 = NewColorInfo(container5)
|
||||||
|
|
||||||
|
palette.Color6 = NewColorInfo(primaryColor)
|
||||||
|
|
||||||
|
gray7S := baseSat * 0.08
|
||||||
|
gray7V := baseVal * 0.28
|
||||||
|
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
gray8S := baseSat * 0.05
|
||||||
|
gray8V := baseVal * 0.85
|
||||||
|
dimTarget := secondaryTarget * 0.5
|
||||||
|
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
|
||||||
|
|
||||||
|
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||||
|
brightRedV := math.Min(baseVal*1.2, 1.0)
|
||||||
|
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightGreenS := math.Min(baseSat*1.1, 1.0)
|
||||||
|
brightGreenV := math.Min(baseVal*1.1, 1.0)
|
||||||
|
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightYellowS := math.Min(baseSat*1.4, 1.0)
|
||||||
|
brightYellowV := math.Min(baseVal*1.3, 1.0)
|
||||||
|
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightBlueS := math.Min(ph.S*1.1, 1.0)
|
||||||
|
brightBlueV := math.Min(ph.V*1.15, 1.0)
|
||||||
|
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
lightContainer := DeriveContainer(primaryColor, true)
|
||||||
|
palette.Color13 = NewColorInfo(lightContainer)
|
||||||
|
|
||||||
|
brightCyanS := ph.S * 0.5
|
||||||
|
brightCyanV := math.Min(ph.V*1.3, 1.0)
|
||||||
|
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightCyanS, V: brightCyanV})))
|
||||||
|
|
||||||
|
white15S := baseSat * 0.04
|
||||||
|
white15V := math.Min(baseVal*1.5, 1.0)
|
||||||
|
palette.Color15 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})))
|
||||||
} else {
|
} else {
|
||||||
palette.Color15 = NewColorInfo("#ffffff")
|
redS := math.Min(baseSat*1.1, 1.0)
|
||||||
|
redV := math.Min(baseVal*1.15, 1.0)
|
||||||
|
palette.Color1 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: redH, S: redS, V: redV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
greenS := math.Min(baseSat*1.0, 1.0)
|
||||||
|
greenV := math.Min(baseVal*1.0, 1.0)
|
||||||
|
palette.Color2 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: greenH, S: greenS, V: greenV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
yellowS := math.Min(baseSat*1.1, 1.0)
|
||||||
|
yellowV := math.Min(baseVal*1.25, 1.0)
|
||||||
|
palette.Color3 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: yellowH, S: yellowS, V: yellowV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
// Slightly more saturated variant of primary
|
||||||
|
blueS := math.Min(ph.S*1.2, 1.0)
|
||||||
|
blueV := ph.V * 0.95
|
||||||
|
palette.Color4 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: ph.H, S: blueS, V: blueV})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
// Color5 matches primary_container exactly (dark container in dark mode)
|
||||||
|
darkContainer := DeriveContainer(primaryColor, false)
|
||||||
|
palette.Color5 = NewColorInfo(darkContainer)
|
||||||
|
|
||||||
|
palette.Color6 = NewColorInfo(primaryColor)
|
||||||
|
|
||||||
|
gray7S := baseSat * 0.12
|
||||||
|
gray7V := math.Min(baseVal*1.05, 1.0)
|
||||||
|
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
|
gray8S := baseSat * 0.15
|
||||||
|
gray8V := baseVal * 0.65
|
||||||
|
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||||
|
|
||||||
|
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||||
|
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||||
|
palette.Color9 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: redH, S: brightRedS, V: brightRedV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightGreenS := math.Min(baseSat*0.7, 1.0)
|
||||||
|
brightGreenV := math.Min(baseVal*1.2, 1.0)
|
||||||
|
palette.Color10 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: greenH, S: brightGreenS, V: brightGreenV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
brightYellowS := math.Min(baseSat*0.7, 1.0)
|
||||||
|
brightYellowV := math.Min(baseVal*1.5, 1.0)
|
||||||
|
palette.Color11 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: yellowH, S: brightYellowS, V: brightYellowV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
// Create a gradient of primary variants: Color12 -> Color13 -> Color14 -> Color15 (near white)
|
||||||
|
// Color12: Start of the lighter gradient - slightly desaturated
|
||||||
|
brightBlueS := ph.S * 0.85
|
||||||
|
brightBlueV := math.Min(ph.V*1.1, 1.0)
|
||||||
|
palette.Color12 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: ph.H, S: brightBlueS, V: brightBlueV})), bgColor, accentTarget, opts))
|
||||||
|
|
||||||
|
// Medium-high saturation pastel primary
|
||||||
|
color13S := ph.S * 0.7
|
||||||
|
color13V := math.Min(ph.V*1.3, 1.0)
|
||||||
|
palette.Color13 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color13S, V: color13V})))
|
||||||
|
|
||||||
|
// Lower saturation, lighter variant
|
||||||
|
color14S := ph.S * 0.45
|
||||||
|
color14V := math.Min(ph.V*1.4, 1.0)
|
||||||
|
palette.Color14 = NewColorInfo(RGBToHex(HSVToRGB(HSV{H: ph.H, S: color14S, V: color14V})))
|
||||||
|
|
||||||
|
white15S := baseSat * 0.05
|
||||||
|
white15V := math.Min(baseVal*1.45, 1.0)
|
||||||
|
palette.Color15 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: white15S, V: white15V})), bgColor, normalTextTarget, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
return palette
|
return palette
|
||||||
|
|||||||
@@ -366,10 +366,19 @@ func TestGeneratePalette(t *testing.T) {
|
|||||||
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
t.Errorf("Light mode background = %s, expected #f8f8f8", result.Color0.Hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tt.opts.IsLight && result.Color15.Hex != "#1a1a1a" {
|
// Color15 is now derived from primary, so just verify it's a valid color
|
||||||
t.Errorf("Light mode foreground = %s, expected #1a1a1a", result.Color15.Hex)
|
// and has appropriate luminance for the mode (now theme-tinted, not pure white/black)
|
||||||
} else if !tt.opts.IsLight && result.Color15.Hex != "#ffffff" {
|
color15Lum := Luminance(result.Color15.Hex)
|
||||||
t.Errorf("Dark mode foreground = %s, expected #ffffff", result.Color15.Hex)
|
if tt.opts.IsLight {
|
||||||
|
// Light mode: Color15 should still be relatively light
|
||||||
|
if color15Lum < 0.5 {
|
||||||
|
t.Errorf("Light mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Dark mode: Color15 should be light (but may have theme tint, so lower threshold)
|
||||||
|
if color15Lum < 0.5 {
|
||||||
|
t.Errorf("Dark mode Color15 = %s (lum %.2f) is too dark", result.Color15.Hex, color15Lum)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -579,6 +588,10 @@ func TestGeneratePaletteWithDPS(t *testing.T) {
|
|||||||
|
|
||||||
bgColor := result.Color0.Hex
|
bgColor := result.Color0.Hex
|
||||||
for i := 1; i < 8; i++ {
|
for i := 1; i < 8; i++ {
|
||||||
|
// Skip Color5 (container) and Color6 (exact primary) - intentionally not contrast-adjusted
|
||||||
|
if i == 5 || i == 6 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
lc := DeltaPhiStarContrast(colors[i].Hex, bgColor, tt.opts.IsLight)
|
||||||
minLc := 30.0
|
minLc := 30.0
|
||||||
if lc < minLc && lc > 0 {
|
if lc < minLc && lc > 0 {
|
||||||
@@ -658,7 +671,7 @@ func TestContrastAlgorithmComparison(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
differentCount := 0
|
differentCount := 0
|
||||||
for i := 0; i < 16; i++ {
|
for i := range 16 {
|
||||||
if wcagColors[i].Hex != dpsColors[i].Hex {
|
if wcagColors[i].Hex != dpsColors[i].Hex {
|
||||||
differentCount++
|
differentCount++
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,3 +112,24 @@ func GenerateWeztermTheme(p Palette) string {
|
|||||||
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
p.Color12.Hex, p.Color13.Hex, p.Color14.Hex, p.Color15.Hex)
|
||||||
return result.String()
|
return result.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GenerateNeovimTheme(p Palette) string {
|
||||||
|
var result strings.Builder
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_0 = \"%s\"\n", p.Color0.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_1 = \"%s\"\n", p.Color1.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_2 = \"%s\"\n", p.Color2.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_3 = \"%s\"\n", p.Color3.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_4 = \"%s\"\n", p.Color4.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_5 = \"%s\"\n", p.Color5.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_6 = \"%s\"\n", p.Color6.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_7 = \"%s\"\n", p.Color7.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_8 = \"%s\"\n", p.Color8.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_9 = \"%s\"\n", p.Color9.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_10 = \"%s\"\n", p.Color10.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_11 = \"%s\"\n", p.Color11.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_12 = \"%s\"\n", p.Color12.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_13 = \"%s\"\n", p.Color13.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_14 = \"%s\"\n", p.Color14.Hex)
|
||||||
|
fmt.Fprintf(&result, "vim.g.terminal_color_15 = \"%s\"\n", p.Color15.Hex)
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -25,6 +26,9 @@ func init() {
|
|||||||
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
@@ -40,6 +44,9 @@ func init() {
|
|||||||
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ArchDistribution struct {
|
type ArchDistribution struct {
|
||||||
@@ -90,6 +97,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectGit())
|
dependencies = append(dependencies, a.detectGit())
|
||||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, a.detectQuickshell())
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, a.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, a.detectXDGPortal())
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
dependencies = append(dependencies, a.detectAccountsService())
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
@@ -103,40 +111,22 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
dependencies = append(dependencies, a.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base detections (common across distros)
|
|
||||||
dependencies = append(dependencies, a.detectMatugen())
|
dependencies = append(dependencies, a.detectMatugen())
|
||||||
dependencies = append(dependencies, a.detectDgop())
|
dependencies = append(dependencies, a.detectDgop())
|
||||||
dependencies = append(dependencies, a.detectClipboardTools()...)
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
func (a *ArchDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return a.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", a.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if a.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||||
if a.packageInstalled("accountsservice") {
|
}
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
Name: "accountsservice",
|
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -154,13 +144,12 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
||||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
}
|
}
|
||||||
@@ -182,13 +171,11 @@ func (a *ArchDistribution) getQuickshellMapping(variant deps.PackageVariant) Pac
|
|||||||
if forceQuickshellGit || variant == deps.VariantGit {
|
if forceQuickshellGit || variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "quickshell", Repository: RepoTypeSystem}
|
// ! TODO - for now we're only forcing quickshell-git on ARCH, as other distros use DL repos which pin a newer quickshell
|
||||||
|
return PackageMapping{Name: "quickshell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
func (a *ArchDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeAUR}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeSystem}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -362,7 +349,11 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
a.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := a.EnableDMSService(ctx); err != nil {
|
if err := a.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
a.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := a.EnableDMSService(ctx, wm); err != nil {
|
||||||
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
a.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,29 +440,10 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
|
|||||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
hasNiri := false
|
hasNiri := false
|
||||||
hasQuickshell := false
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "niri-git" {
|
if pkg == "niri-git" {
|
||||||
hasNiri = true
|
hasNiri = true
|
||||||
}
|
}
|
||||||
if pkg == "quickshell" || pkg == "quickshell-git" {
|
|
||||||
hasQuickshell = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If quickshell is in the list, always reinstall google-breakpad first
|
|
||||||
if hasQuickshell {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.63,
|
|
||||||
Step: "Reinstalling google-breakpad for quickshell...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
|
||||||
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
@@ -536,12 +508,9 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
for _, dep := range dmsDepencies {
|
if slices.Contains(dmsDepencies, pkg) {
|
||||||
if pkg == dep {
|
deps = append(deps, pkg)
|
||||||
deps = append(deps, pkg)
|
isDep = true
|
||||||
isDep = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !isDep {
|
if !isDep {
|
||||||
others = append(others, pkg)
|
others = append(others, pkg)
|
||||||
@@ -567,7 +536,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
a.log(fmt.Sprintf("Warning: failed to clean existing cache for %s: %v", pkg, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -628,10 +597,16 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
// since we manually manage those dependencies
|
if pkg == "dms-shell-bin" {
|
||||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
progressChan <- InstallProgressMsg{
|
||||||
// Pre-install dependencies from .SRCINFO
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
@@ -640,19 +615,19 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Installing package dependencies and makedepends",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies and makedepends explicitly
|
// Install dependencies from .SRCINFO
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
depFilter := ""
|
||||||
|
if pkg == "dms-shell-git" {
|
||||||
|
depFilter = ` | sed -E 's/[[:space:]]*(quickshell|dgop)[[:space:]]*/ /g' | tr -s ' '`
|
||||||
|
}
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
||||||
fmt.Sprintf(`
|
fmt.Sprintf(`
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' %s | sed 's/[[:space:]]*$//')
|
||||||
if [[ "%s" == *"quickshell"* ]]; then
|
|
||||||
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
|
||||||
fi
|
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
||||||
fi
|
fi
|
||||||
`, srcinfoPath, pkg, sudoPassword))
|
`, srcinfoPath, depFilter, sudoPassword))
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
||||||
@@ -669,14 +644,6 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -689,7 +656,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
buildCmd.Dir = packageDir
|
buildCmd.Dir = packageDir
|
||||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
||||||
|
|
||||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
|
|||||||
@@ -76,47 +76,55 @@ func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *
|
|||||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common dependency detection methods
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists("git") {
|
if b.commandExists(name) {
|
||||||
status = deps.StatusInstalled
|
status = deps.StatusInstalled
|
||||||
}
|
}
|
||||||
|
|
||||||
return deps.Dependency{
|
return deps.Dependency{
|
||||||
Name: "git",
|
Name: name,
|
||||||
Status: status,
|
Status: status,
|
||||||
Description: "Version control system",
|
Description: description,
|
||||||
Required: true,
|
Required: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectPackage(name, description string, installed bool) deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if installed {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Description: description,
|
||||||
|
Required: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if installed {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Description: description,
|
||||||
|
Required: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||||
|
return b.detectCommand("git", "Version control system")
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
func (b *BaseDistribution) detectMatugen() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return b.detectCommand("matugen", "Material Design color generation tool")
|
||||||
if b.commandExists("matugen") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "matugen",
|
|
||||||
Status: status,
|
|
||||||
Description: "Material Design color generation tool",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
func (b *BaseDistribution) detectDgop() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return b.detectCommand("dgop", "Desktop portal management tool")
|
||||||
if b.commandExists("dgop") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "dgop",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop portal management tool",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
func (b *BaseDistribution) detectDMS() deps.Dependency {
|
||||||
@@ -190,37 +198,6 @@ func (b *BaseDistribution) detectSpecificTerminal(terminal deps.Terminal) deps.D
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectClipboardTools() []deps.Dependency {
|
|
||||||
var dependencies []deps.Dependency
|
|
||||||
|
|
||||||
cliphist := deps.StatusMissing
|
|
||||||
if b.commandExists("cliphist") {
|
|
||||||
cliphist = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
wlClipboard := deps.StatusMissing
|
|
||||||
if b.commandExists("wl-copy") && b.commandExists("wl-paste") {
|
|
||||||
wlClipboard = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies = append(dependencies,
|
|
||||||
deps.Dependency{
|
|
||||||
Name: "cliphist",
|
|
||||||
Status: cliphist,
|
|
||||||
Description: "Wayland clipboard manager",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
deps.Dependency{
|
|
||||||
Name: "wl-clipboard",
|
|
||||||
Status: wlClipboard,
|
|
||||||
Description: "Wayland clipboard utilities",
|
|
||||||
Required: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return dependencies
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
func (b *BaseDistribution) detectHyprlandTools() []deps.Dependency {
|
||||||
var dependencies []deps.Dependency
|
var dependencies []deps.Dependency
|
||||||
|
|
||||||
@@ -570,7 +547,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
envDir := filepath.Join(homeDir, ".config", "environment.d")
|
||||||
if err := os.MkdirAll(envDir, 0755); err != nil {
|
if err := os.MkdirAll(envDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
return fmt.Errorf("failed to create environment.d directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,15 +563,12 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
|||||||
terminalCmd = "ghostty"
|
terminalCmd = "ghostty"
|
||||||
}
|
}
|
||||||
|
|
||||||
content := fmt.Sprintf(`QT_QPA_PLATFORM=wayland
|
content := fmt.Sprintf(`ELECTRON_OZONE_PLATFORM_HINT=auto
|
||||||
ELECTRON_OZONE_PLATFORM_HINT=auto
|
|
||||||
QT_QPA_PLATFORMTHEME=gtk3
|
|
||||||
QT_QPA_PLATFORMTHEME_QT6=gtk3
|
|
||||||
TERMINAL=%s
|
TERMINAL=%s
|
||||||
`, terminalCmd)
|
`, terminalCmd)
|
||||||
|
|
||||||
envFile := filepath.Join(envDir, "90-dms.conf")
|
envFile := filepath.Join(envDir, "90-dms.conf")
|
||||||
if err := os.WriteFile(envFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(envFile, []byte(content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write environment config: %w", err)
|
return fmt.Errorf("failed to write environment config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,12 +576,53 @@ TERMINAL=%s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) EnableDMSService(ctx context.Context) error {
|
func (b *BaseDistribution) EnableDMSService(ctx context.Context, wm deps.WindowManager) error {
|
||||||
cmd := exec.CommandContext(ctx, "systemctl", "--user", "enable", "--now", "dms")
|
switch wm {
|
||||||
if err := cmd.Run(); err != nil {
|
case deps.WindowManagerNiri:
|
||||||
return fmt.Errorf("failed to enable dms service: %w", err)
|
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "niri.service", "dms").Run(); err != nil {
|
||||||
|
b.log("Warning: failed to add dms as a want for niri.service")
|
||||||
|
}
|
||||||
|
case deps.WindowManagerHyprland:
|
||||||
|
if err := exec.CommandContext(ctx, "systemctl", "--user", "add-wants", "hyprland-session.target", "dms").Run(); err != nil {
|
||||||
|
b.log("Warning: failed to add dms as a want for hyprland-session.target")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.log("Enabled dms systemd user service")
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) WriteWindowManagerConfig(wm deps.WindowManager) error {
|
||||||
|
if wm == deps.WindowManagerHyprland {
|
||||||
|
if err := b.WriteHyprlandSessionTarget(); err != nil {
|
||||||
|
return fmt.Errorf("failed to write hyprland session target: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get home directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
||||||
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
targetPath := filepath.Join(targetDir, "hyprland-session.target")
|
||||||
|
content := `[Unit]
|
||||||
|
Description=Hyprland Session Target
|
||||||
|
Requires=graphical-session.target
|
||||||
|
After=graphical-session.target
|
||||||
|
`
|
||||||
|
|
||||||
|
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.log(fmt.Sprintf("Wrote hyprland-session.target to %s", targetPath))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
||||||
@@ -36,13 +37,13 @@ func TestBaseDistribution_detectDMS_NotInstalled(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
||||||
if !commandExists("git") {
|
if !utils.CommandExists("git") {
|
||||||
t.Skip("git not available")
|
t.Skip("git not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -54,7 +55,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
exec.Command("git", "-C", dmsPath, "checkout", "-b", "master").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0644)
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
|
|
||||||
@@ -80,13 +81,13 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
||||||
if !commandExists("git") {
|
if !utils.CommandExists("git") {
|
||||||
t.Skip("git not available")
|
t.Skip("git not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -98,7 +99,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
exec.Command("git", "-C", dmsPath, "remote", "add", "origin", "https://github.com/AvengeMedia/DankMaterialShell.git").Run()
|
||||||
|
|
||||||
testFile := filepath.Join(dmsPath, "test.txt")
|
testFile := filepath.Join(dmsPath, "test.txt")
|
||||||
os.WriteFile(testFile, []byte("test"), 0644)
|
os.WriteFile(testFile, []byte("test"), 0o644)
|
||||||
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
exec.Command("git", "-C", dmsPath, "add", ".").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
exec.Command("git", "-C", dmsPath, "commit", "-m", "initial").Run()
|
||||||
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
exec.Command("git", "-C", dmsPath, "tag", "v0.0.1").Run()
|
||||||
@@ -124,7 +125,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
|||||||
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
func TestBaseDistribution_detectDMS_DirectoryWithoutGit(t *testing.T) {
|
||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||||
os.MkdirAll(dmsPath, 0755)
|
os.MkdirAll(dmsPath, 0o755)
|
||||||
|
|
||||||
originalHome := os.Getenv("HOME")
|
originalHome := os.Getenv("HOME")
|
||||||
defer os.Setenv("HOME", originalHome)
|
defer os.Setenv("HOME", originalHome)
|
||||||
@@ -164,11 +165,6 @@ func TestBaseDistribution_NewBaseDistribution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func commandExists(cmd string) bool {
|
|
||||||
_, err := exec.LookPath(cmd)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBaseDistribution_versionCompare(t *testing.T) {
|
func TestBaseDistribution_versionCompare(t *testing.T) {
|
||||||
logChan := make(chan string, 10)
|
logChan := make(chan string, 10)
|
||||||
defer close(logChan)
|
defer close(logChan)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -60,6 +61,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, d.detectGit())
|
dependencies = append(dependencies, d.detectGit())
|
||||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, d.detectQuickshell())
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, d.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, d.detectXDGPortal())
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
dependencies = append(dependencies, d.detectAccountsService())
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
@@ -69,51 +71,24 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, d.detectMatugen())
|
dependencies = append(dependencies, d.detectMatugen())
|
||||||
dependencies = append(dependencies, d.detectDgop())
|
dependencies = append(dependencies, d.detectDgop())
|
||||||
dependencies = append(dependencies, d.detectClipboardTools()...)
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
func (d *DebianDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return d.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", d.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if d.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (d *DebianDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return d.detectCommand("xwayland-satellite", "Xwayland support")
|
||||||
if d.commandExists("xwayland-satellite") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xwayland-satellite",
|
|
||||||
Status: status,
|
|
||||||
Description: "Xwayland support",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||||
if d.packageInstalled("accountsservice") {
|
}
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
Name: "accountsservice",
|
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -132,16 +107,15 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// DMS packages from OBS with variant support
|
// DMS packages from OBS with variant support
|
||||||
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,7 +182,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -225,7 +199,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -338,7 +312,11 @@ func (d *DebianDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.EnableDMSService(ctx); err != nil {
|
if err := d.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
d.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := d.EnableDMSService(ctx, wm); err != nil {
|
||||||
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,6 +391,8 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
debianVersion := "Debian_13"
|
debianVersion := "Debian_13"
|
||||||
if osInfo.VersionID == "testing" {
|
if osInfo.VersionID == "testing" {
|
||||||
debianVersion = "Debian_Testing"
|
debianVersion = "Debian_Testing"
|
||||||
|
} else if osInfo.VersionCodename == "sid" || osInfo.VersionID == "sid" || strings.Contains(strings.ToLower(osInfo.PrettyName), "sid") || strings.Contains(strings.ToLower(osInfo.PrettyName), "unstable") {
|
||||||
|
debianVersion = "Debian_Unstable"
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, pkg := range obsPkgs {
|
for _, pkg := range obsPkgs {
|
||||||
@@ -449,14 +429,14 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("curl & gpg to add key for %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
keyCmd := fmt.Sprintf("curl -fsSL %s/Release.key | gpg --dearmor -o %s", baseURL, keyringPath)
|
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s] %s/ /", keyringPath, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -467,7 +447,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("echo '%s' | tee %s", repoLine, listFile))
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
@@ -502,7 +482,7 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -575,7 +555,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
if err := d.installRust(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Rust: %w", err)
|
return fmt.Errorf("failed to install Rust: %w", err)
|
||||||
}
|
}
|
||||||
case "cliphist", "dgop":
|
case "dgop":
|
||||||
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
if err := d.installGo(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install Go: %w", err)
|
return fmt.Errorf("failed to install Go: %w", err)
|
||||||
}
|
}
|
||||||
@@ -612,7 +592,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -651,7 +631,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ func init() {
|
|||||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
@@ -75,6 +78,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectGit())
|
dependencies = append(dependencies, f.detectGit())
|
||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, f.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
dependencies = append(dependencies, f.detectAccountsService())
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
@@ -88,26 +92,14 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
dependencies = append(dependencies, f.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base detections (common across distros)
|
|
||||||
dependencies = append(dependencies, f.detectMatugen())
|
dependencies = append(dependencies, f.detectMatugen())
|
||||||
dependencies = append(dependencies, f.detectDgop())
|
dependencies = append(dependencies, f.detectDgop())
|
||||||
dependencies = append(dependencies, f.detectClipboardTools()...)
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
func (f *FedoraDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return f.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", f.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if f.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
func (f *FedoraDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -127,14 +119,13 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
|
||||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
}
|
}
|
||||||
@@ -166,18 +157,15 @@ func (f *FedoraDistribution) getDmsMapping(variant deps.PackageVariant) PackageM
|
|||||||
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
return PackageMapping{Name: "dms", Repository: RepoTypeCOPR, RepoURL: "avengemedia/dms"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "sdegler/hyprland"}
|
||||||
return PackageMapping{Name: "hyprland-git", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "hyprland", Repository: RepoTypeCOPR, RepoURL: "solopasha/hyprland"}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
func (f *FedoraDistribution) getNiriMapping(variant deps.PackageVariant) PackageMapping {
|
||||||
if variant == deps.VariantGit {
|
if variant == deps.VariantGit {
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri-git"}
|
||||||
}
|
}
|
||||||
return PackageMapping{Name: "niri", Repository: RepoTypeSystem}
|
return PackageMapping{Name: "niri", Repository: RepoTypeCOPR, RepoURL: "yalter/niri"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (f *FedoraDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -208,6 +196,10 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getPrerequisites() []string {
|
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"dnf-plugins-core",
|
"dnf-plugins-core",
|
||||||
@@ -362,7 +354,11 @@ func (f *FedoraDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
f.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := f.EnableDMSService(ctx); err != nil {
|
if err := f.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
f.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.EnableDMSService(ctx, wm); err != nil {
|
||||||
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
f.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, g.detectWindowManager(wm))
|
dependencies = append(dependencies, g.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, g.detectQuickshell())
|
dependencies = append(dependencies, g.detectQuickshell())
|
||||||
dependencies = append(dependencies, g.detectXDGPortal())
|
dependencies = append(dependencies, g.detectXDGPortal())
|
||||||
dependencies = append(dependencies, g.detectPolkitAgent())
|
|
||||||
dependencies = append(dependencies, g.detectAccountsService())
|
dependencies = append(dependencies, g.detectAccountsService())
|
||||||
|
|
||||||
if wm == deps.WindowManagerHyprland {
|
if wm == deps.WindowManagerHyprland {
|
||||||
@@ -108,65 +107,20 @@ func (g *GentooDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
|
|
||||||
dependencies = append(dependencies, g.detectMatugen())
|
dependencies = append(dependencies, g.detectMatugen())
|
||||||
dependencies = append(dependencies, g.detectDgop())
|
dependencies = append(dependencies, g.detectDgop())
|
||||||
dependencies = append(dependencies, g.detectClipboardTools()...)
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
func (g *GentooDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return g.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", g.packageInstalled("sys-apps/xdg-desktop-portal-gtk"))
|
||||||
if g.packageInstalled("sys-apps/xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *GentooDistribution) detectPolkitAgent() deps.Dependency {
|
|
||||||
status := deps.StatusMissing
|
|
||||||
if g.packageInstalled("mate-extra/mate-polkit") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "mate-polkit",
|
|
||||||
Status: status,
|
|
||||||
Description: "PolicyKit authentication agent",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (g *GentooDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return g.detectPackage("xwayland-satellite", "Xwayland support", g.packageInstalled("gui-apps/xwayland-satellite"))
|
||||||
if g.packageInstalled("gui-apps/xwayland-satellite") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xwayland-satellite",
|
|
||||||
Status: status,
|
|
||||||
Description: "Xwayland support",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
func (g *GentooDistribution) detectAccountsService() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return g.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", g.packageInstalled("sys-apps/accountsservice"))
|
||||||
if g.packageInstalled("sys-apps/accountsservice") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "accountsservice",
|
|
||||||
Status: status,
|
|
||||||
Description: "D-Bus interface for user account query and manipulation",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
func (g *GentooDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -185,9 +139,7 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
"git": {Name: "dev-vcs/git", Repository: RepoTypeSystem},
|
||||||
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"kitty": {Name: "x11-terms/kitty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
"alacritty": {Name: "x11-terms/alacritty", Repository: RepoTypeSystem, UseFlags: "X wayland"},
|
||||||
"wl-clipboard": {Name: "gui-apps/wl-clipboard", Repository: RepoTypeSystem},
|
|
||||||
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
"xdg-desktop-portal-gtk": {Name: "sys-apps/xdg-desktop-portal-gtk", Repository: RepoTypeSystem, UseFlags: "wayland X"},
|
||||||
"mate-polkit": {Name: "mate-extra/mate-polkit", Repository: RepoTypeSystem},
|
|
||||||
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "sys-apps/accountsservice", Repository: RepoTypeSystem},
|
||||||
|
|
||||||
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
"qtbase": {Name: "dev-qt/qtbase", Repository: RepoTypeSystem, UseFlags: "wayland opengl vulkan widgets"},
|
||||||
@@ -197,7 +149,6 @@ func (g *GentooDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
|
|
||||||
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": g.getQuickshellMapping(variants["quickshell"]),
|
||||||
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
"matugen": {Name: "x11-misc/matugen", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
||||||
"cliphist": {Name: "app-misc/cliphist", Repository: RepoTypeGURU, AcceptKeywords: archKeyword},
|
|
||||||
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": g.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
"dgop": {Name: "dgop", Repository: RepoTypeManual, BuildFunc: "installDgop"},
|
||||||
}
|
}
|
||||||
@@ -223,12 +174,8 @@ func (g *GentooDistribution) getDmsMapping(_ deps.PackageVariant) PackageMapping
|
|||||||
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
return PackageMapping{Name: "dms", Repository: RepoTypeManual, BuildFunc: "installDankMaterialShell"}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getHyprlandMapping(variant deps.PackageVariant) PackageMapping {
|
func (g *GentooDistribution) getHyprlandMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
archKeyword := g.getArchKeyword()
|
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: g.getArchKeyword()}
|
||||||
if variant == deps.VariantGit {
|
|
||||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeGURU, UseFlags: "X", AcceptKeywords: archKeyword}
|
|
||||||
}
|
|
||||||
return PackageMapping{Name: "gui-wm/hyprland", Repository: RepoTypeSystem, UseFlags: "X", AcceptKeywords: archKeyword}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
func (g *GentooDistribution) getNiriMapping(_ deps.PackageVariant) PackageMapping {
|
||||||
@@ -456,7 +403,11 @@ func (g *GentooDistribution) InstallPackages(ctx context.Context, dependencies [
|
|||||||
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
g.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := g.EnableDMSService(ctx); err != nil {
|
if err := g.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
g.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := g.EnableDMSService(ctx, wm); err != nil {
|
||||||
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
g.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const (
|
|||||||
PhaseAURPackages
|
PhaseAURPackages
|
||||||
PhaseCursorTheme
|
PhaseCursorTheme
|
||||||
PhaseConfiguration
|
PhaseConfiguration
|
||||||
|
PhaseGreeterSetup
|
||||||
PhaseComplete
|
PhaseComplete
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ type ManualPackageInstaller struct {
|
|||||||
|
|
||||||
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
// parseLatestTagFromGitOutput parses git ls-remote output and returns the latest tag
|
||||||
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
func (m *ManualPackageInstaller) parseLatestTagFromGitOutput(output string) string {
|
||||||
lines := strings.Split(output, "\n")
|
lines := strings.SplitSeq(output, "\n")
|
||||||
for _, line := range lines {
|
for line := range lines {
|
||||||
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
if strings.Contains(line, "refs/tags/") && !strings.Contains(line, "^{}") {
|
||||||
parts := strings.Split(line, "refs/tags/")
|
parts := strings.Split(line, "refs/tags/")
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
@@ -74,10 +74,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installHyprland(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install hyprland: %w", err)
|
return fmt.Errorf("failed to install hyprland: %w", err)
|
||||||
}
|
}
|
||||||
case "hyprpicker":
|
|
||||||
if err := m.installHyprpicker(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
|
||||||
}
|
|
||||||
case "ghostty":
|
case "ghostty":
|
||||||
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installGhostty(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install ghostty: %w", err)
|
return fmt.Errorf("failed to install ghostty: %w", err)
|
||||||
@@ -86,10 +82,6 @@ func (m *ManualPackageInstaller) InstallManualPackages(ctx context.Context, pack
|
|||||||
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installMatugen(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install matugen: %w", err)
|
return fmt.Errorf("failed to install matugen: %w", err)
|
||||||
}
|
}
|
||||||
case "cliphist":
|
|
||||||
if err := m.installCliphist(ctx, sudoPassword, progressChan); err != nil {
|
|
||||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
|
||||||
}
|
|
||||||
case "xwayland-satellite":
|
case "xwayland-satellite":
|
||||||
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
if err := m.installXwaylandSatellite(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
return fmt.Errorf("failed to install xwayland-satellite: %w", err)
|
||||||
@@ -111,12 +103,12 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
tmpDir := filepath.Join(cacheDir, "dgop-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -168,10 +160,10 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
buildDir := filepath.Join(homeDir, ".cache", "dankinstall", "niri-build")
|
||||||
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
tmpDir := filepath.Join(homeDir, ".cache", "dankinstall", "tmp")
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -245,12 +237,12 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -281,7 +273,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildDir := tmpDir + "/build"
|
buildDir := tmpDir + "/build"
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,12 +343,12 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
tmpDir := filepath.Join(cacheDir, "hyprland-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -405,184 +397,6 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installHyprpicker(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
m.log("Installing hyprpicker from source...")
|
|
||||||
|
|
||||||
homeDir := os.Getenv("HOME")
|
|
||||||
if homeDir == "" {
|
|
||||||
return fmt.Errorf("HOME environment variable not set")
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install hyprutils first
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.05,
|
|
||||||
Step: "Building hyprutils dependency...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "git clone https://github.com/hyprwm/hyprutils.git",
|
|
||||||
}
|
|
||||||
|
|
||||||
hyprutilsDir := filepath.Join(cacheDir, "hyprutils-build")
|
|
||||||
if err := os.MkdirAll(hyprutilsDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create hyprutils directory: %w", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(hyprutilsDir)
|
|
||||||
|
|
||||||
cloneUtilsCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprutils.git", hyprutilsDir)
|
|
||||||
if err := cloneUtilsCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to clone hyprutils: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configureUtilsCmd := exec.CommandContext(ctx, "cmake",
|
|
||||||
"--no-warn-unused-cli",
|
|
||||||
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
|
||||||
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
|
||||||
"-DBUILD_TESTING=off",
|
|
||||||
"-S", ".",
|
|
||||||
"-B", "./build")
|
|
||||||
configureUtilsCmd.Dir = hyprutilsDir
|
|
||||||
configureUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
|
||||||
if err := m.runWithProgressStep(configureUtilsCmd, progressChan, PhaseSystemPackages, 0.05, 0.1, "Configuring hyprutils..."); err != nil {
|
|
||||||
return fmt.Errorf("failed to configure hyprutils: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buildUtilsCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "all")
|
|
||||||
buildUtilsCmd.Dir = hyprutilsDir
|
|
||||||
buildUtilsCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
|
||||||
if err := m.runWithProgressStep(buildUtilsCmd, progressChan, PhaseSystemPackages, 0.1, 0.2, "Building hyprutils..."); err != nil {
|
|
||||||
return fmt.Errorf("failed to build hyprutils: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
installUtilsCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
|
||||||
installUtilsCmd.Dir = hyprutilsDir
|
|
||||||
if err := installUtilsCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to install hyprutils: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install hyprwayland-scanner
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.2,
|
|
||||||
Step: "Building hyprwayland-scanner dependency...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "git clone https://github.com/hyprwm/hyprwayland-scanner.git",
|
|
||||||
}
|
|
||||||
|
|
||||||
scannerDir := filepath.Join(cacheDir, "hyprwayland-scanner-build")
|
|
||||||
if err := os.MkdirAll(scannerDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create scanner directory: %w", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(scannerDir)
|
|
||||||
|
|
||||||
cloneScannerCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprwayland-scanner.git", scannerDir)
|
|
||||||
if err := cloneScannerCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to clone hyprwayland-scanner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
configureScannerCmd := exec.CommandContext(ctx, "cmake",
|
|
||||||
"-DCMAKE_INSTALL_PREFIX=/usr",
|
|
||||||
"-B", "build")
|
|
||||||
configureScannerCmd.Dir = scannerDir
|
|
||||||
configureScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
|
||||||
if err := m.runWithProgressStep(configureScannerCmd, progressChan, PhaseSystemPackages, 0.2, 0.25, "Configuring hyprwayland-scanner..."); err != nil {
|
|
||||||
return fmt.Errorf("failed to configure hyprwayland-scanner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
buildScannerCmd := exec.CommandContext(ctx, "cmake", "--build", "build", "-j")
|
|
||||||
buildScannerCmd.Dir = scannerDir
|
|
||||||
buildScannerCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
|
||||||
if err := m.runWithProgressStep(buildScannerCmd, progressChan, PhaseSystemPackages, 0.25, 0.35, "Building hyprwayland-scanner..."); err != nil {
|
|
||||||
return fmt.Errorf("failed to build hyprwayland-scanner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
installScannerCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
|
||||||
installScannerCmd.Dir = scannerDir
|
|
||||||
if err := installScannerCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to install hyprwayland-scanner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now build hyprpicker
|
|
||||||
tmpDir := filepath.Join(cacheDir, "hyprpicker-build")
|
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
|
||||||
}
|
|
||||||
defer os.RemoveAll(tmpDir)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.35,
|
|
||||||
Step: "Cloning hyprpicker repository...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "git clone https://github.com/hyprwm/hyprpicker.git",
|
|
||||||
}
|
|
||||||
|
|
||||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "https://github.com/hyprwm/hyprpicker.git", tmpDir)
|
|
||||||
if err := cloneCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to clone hyprpicker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.45,
|
|
||||||
Step: "Configuring hyprpicker build...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "cmake -B build -S . -DCMAKE_BUILD_TYPE=Release",
|
|
||||||
}
|
|
||||||
|
|
||||||
configureCmd := exec.CommandContext(ctx, "cmake",
|
|
||||||
"--no-warn-unused-cli",
|
|
||||||
"-DCMAKE_BUILD_TYPE:STRING=Release",
|
|
||||||
"-DCMAKE_INSTALL_PREFIX:PATH=/usr",
|
|
||||||
"-S", ".",
|
|
||||||
"-B", "./build")
|
|
||||||
configureCmd.Dir = tmpDir
|
|
||||||
configureCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
|
||||||
|
|
||||||
output, err := configureCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
m.log(fmt.Sprintf("cmake configure failed. Output:\n%s", string(output)))
|
|
||||||
return fmt.Errorf("failed to configure hyprpicker: %w\nCMake output:\n%s", err, string(output))
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.55,
|
|
||||||
Step: "Building hyprpicker...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "cmake --build build --target hyprpicker",
|
|
||||||
}
|
|
||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "cmake", "--build", "./build", "--config", "Release", "--target", "hyprpicker")
|
|
||||||
buildCmd.Dir = tmpDir
|
|
||||||
buildCmd.Env = append(os.Environ(), "TMPDIR="+cacheDir)
|
|
||||||
if err := m.runWithProgressStep(buildCmd, progressChan, PhaseSystemPackages, 0.55, 0.8, "Building hyprpicker..."); err != nil {
|
|
||||||
return fmt.Errorf("failed to build hyprpicker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.8,
|
|
||||||
Step: "Installing hyprpicker...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: "sudo cmake --install build",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install ./build")
|
|
||||||
installCmd.Dir = tmpDir
|
|
||||||
if err := installCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to install hyprpicker: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.log("hyprpicker installed successfully from source")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing Ghostty from source...")
|
m.log("Installing Ghostty from source...")
|
||||||
|
|
||||||
@@ -592,12 +406,12 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
tmpDir := filepath.Join(cacheDir, "ghostty-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -714,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
}
|
}
|
||||||
|
|
||||||
configDir := filepath.Dir(dmsPath)
|
configDir := filepath.Dir(dmsPath)
|
||||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
if err := os.MkdirAll(configDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,52 +617,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installCliphist(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
|
||||||
m.log("Installing cliphist from source...")
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.1,
|
|
||||||
Step: "Installing cliphist via go install...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "go install go.senan.xyz/cliphist@latest",
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd := exec.CommandContext(ctx, "go", "install", "go.senan.xyz/cliphist@latest")
|
|
||||||
if err := m.runWithProgressStep(installCmd, progressChan, PhaseSystemPackages, 0.1, 0.7, "Building cliphist..."); err != nil {
|
|
||||||
return fmt.Errorf("failed to install cliphist: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
homeDir := os.Getenv("HOME")
|
|
||||||
sourcePath := filepath.Join(homeDir, "go", "bin", "cliphist")
|
|
||||||
targetPath := "/usr/local/bin/cliphist"
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.7,
|
|
||||||
Step: "Installing cliphist binary to system...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
|
||||||
}
|
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy cliphist to /usr/local/bin: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make it executable
|
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make cliphist executable: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.log("cliphist installed successfully from source")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
m.log("Installing xwayland-satellite from source...")
|
m.log("Installing xwayland-satellite from source...")
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ func init() {
|
|||||||
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("opensuse-tumbleweed", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewOpenSUSEDistribution(config, logChan)
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("opensuse-leap", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
|
})
|
||||||
|
Register("opensuse-slowroll", "#73BA25", FamilySUSE, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewOpenSUSEDistribution(config, logChan)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenSUSEDistribution struct {
|
type OpenSUSEDistribution struct {
|
||||||
@@ -65,6 +71,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectGit())
|
dependencies = append(dependencies, o.detectGit())
|
||||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, o.detectQuickshell())
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, o.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, o.detectXDGPortal())
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
dependencies = append(dependencies, o.detectAccountsService())
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
@@ -78,26 +85,14 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
dependencies = append(dependencies, o.detectXwaylandSatellite())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Base detections (common across distros)
|
|
||||||
dependencies = append(dependencies, o.detectMatugen())
|
dependencies = append(dependencies, o.detectMatugen())
|
||||||
dependencies = append(dependencies, o.detectDgop())
|
dependencies = append(dependencies, o.detectDgop())
|
||||||
dependencies = append(dependencies, o.detectClipboardTools()...)
|
|
||||||
|
|
||||||
return dependencies, nil
|
return dependencies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
func (o *OpenSUSEDistribution) detectXDGPortal() deps.Dependency {
|
||||||
status := deps.StatusMissing
|
return o.detectPackage("xdg-desktop-portal-gtk", "Desktop integration portal for GTK", o.packageInstalled("xdg-desktop-portal-gtk"))
|
||||||
if o.packageInstalled("xdg-desktop-portal-gtk") {
|
|
||||||
status = deps.StatusInstalled
|
|
||||||
}
|
|
||||||
|
|
||||||
return deps.Dependency{
|
|
||||||
Name: "xdg-desktop-portal-gtk",
|
|
||||||
Status: status,
|
|
||||||
Description: "Desktop integration portal for GTK",
|
|
||||||
Required: true,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
||||||
@@ -106,6 +101,10 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -114,17 +113,16 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
packages := map[string]PackageMapping{
|
packages := map[string]PackageMapping{
|
||||||
// Standard zypper packages
|
// Standard zypper packages
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
|
||||||
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
"kitty": {Name: "kitty", Repository: RepoTypeSystem},
|
||||||
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
"alacritty": {Name: "alacritty", Repository: RepoTypeSystem},
|
||||||
"wl-clipboard": {Name: "wl-clipboard", Repository: RepoTypeSystem},
|
|
||||||
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
"xdg-desktop-portal-gtk": {Name: "xdg-desktop-portal-gtk", Repository: RepoTypeSystem},
|
||||||
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
"accountsservice": {Name: "accountsservice", Repository: RepoTypeSystem},
|
||||||
"cliphist": {Name: "cliphist", Repository: RepoTypeSystem},
|
|
||||||
|
|
||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
}
|
}
|
||||||
@@ -377,7 +375,11 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
o.log(fmt.Sprintf("Warning: failed to write environment config: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := o.EnableDMSService(ctx); err != nil {
|
if err := o.WriteWindowManagerConfig(wm); err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to write window manager config: %v", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := o.EnableDMSService(ctx, wm); err != nil {
|
||||||
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
o.log(fmt.Sprintf("Warning: failed to enable dms service: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +446,19 @@ func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []
|
|||||||
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
|
osInfo, err := GetOSInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get OS info: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
obsDistroVersion := "openSUSE_Tumbleweed"
|
||||||
|
switch osInfo.Distribution.ID {
|
||||||
|
case "opensuse-leap":
|
||||||
|
obsDistroVersion = fmt.Sprintf("openSUSE_Leap_%s", osInfo.VersionID)
|
||||||
|
case "opensuse-slowroll":
|
||||||
|
obsDistroVersion = "openSUSE_Slowroll"
|
||||||
|
}
|
||||||
|
|
||||||
for _, pkg := range obsPkgs {
|
for _, pkg := range obsPkgs {
|
||||||
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
if pkg.RepoURL != "" && !enabledRepos[pkg.RepoURL] {
|
||||||
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
o.log(fmt.Sprintf("Enabling OBS repository: %s", pkg.RepoURL))
|
||||||
@@ -451,8 +466,8 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
// RepoURL format: "home:AvengeMedia:danklinux"
|
// RepoURL format: "home:AvengeMedia:danklinux"
|
||||||
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
repoPath := strings.ReplaceAll(pkg.RepoURL, ":", ":/")
|
||||||
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
repoName := strings.ReplaceAll(pkg.RepoURL, ":", "-")
|
||||||
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/openSUSE_Tumbleweed/%s.repo",
|
repoURL := fmt.Sprintf("https://download.opensuse.org/repositories/%s/%s/%s.repo",
|
||||||
repoPath, pkg.RepoURL)
|
repoPath, obsDistroVersion, pkg.RepoURL)
|
||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
checkCmd := exec.CommandContext(ctx, "zypper", "repos", repoName)
|
||||||
if checkCmd.Run() == nil {
|
if checkCmd.Run() == nil {
|
||||||
@@ -472,7 +487,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to enable OBS repo %s: %w", pkg.RepoURL, err)
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledRepos[pkg.RepoURL] = true
|
enabledRepos[pkg.RepoURL] = true
|
||||||
@@ -531,12 +546,12 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
cacheDir := filepath.Join(homeDir, ".cache", "dankinstall")
|
||||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
if err := os.MkdirAll(cacheDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
tmpDir := filepath.Join(cacheDir, "quickshell-build")
|
||||||
if err := os.MkdirAll(tmpDir, 0755); err != nil {
|
if err := os.MkdirAll(tmpDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp directory: %w", err)
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
@@ -567,7 +582,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildDir := tmpDir + "/build"
|
buildDir := tmpDir + "/build"
|
||||||
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create build directory: %w", err)
|
return fmt.Errorf("failed to create build directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,12 @@ type DistroInfo struct {
|
|||||||
|
|
||||||
// OSInfo contains complete OS information
|
// OSInfo contains complete OS information
|
||||||
type OSInfo struct {
|
type OSInfo struct {
|
||||||
Distribution DistroInfo
|
Distribution DistroInfo
|
||||||
Version string
|
Version string
|
||||||
VersionID string
|
VersionID string
|
||||||
PrettyName string
|
VersionCodename string
|
||||||
Architecture string
|
PrettyName string
|
||||||
|
Architecture string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOSInfo detects the current OS and returns information about it
|
// GetOSInfo detects the current OS and returns information about it
|
||||||
@@ -72,6 +73,8 @@ func GetOSInfo() (*OSInfo, error) {
|
|||||||
info.VersionID = value
|
info.VersionID = value
|
||||||
case "VERSION":
|
case "VERSION":
|
||||||
info.Version = value
|
info.Version = value
|
||||||
|
case "VERSION_CODENAME":
|
||||||
|
info.VersionCodename = value
|
||||||
case "PRETTY_NAME":
|
case "PRETTY_NAME":
|
||||||
info.PrettyName = value
|
info.PrettyName = value
|
||||||
}
|
}
|
||||||
@@ -100,6 +103,10 @@ func IsUnsupportedDistro(distroID, versionID string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if distroID == "debian" {
|
if distroID == "debian" {
|
||||||
|
// unstable/sid support
|
||||||
|
if versionID == "sid" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if versionID == "" {
|
if versionID == "" {
|
||||||
// debian testing/sid have no version ID
|
// debian testing/sid have no version ID
|
||||||
return false
|
return false
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user