mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 23:32:50 -04:00
Compare commits
571 Commits
03cfa55e0b
..
frame
| Author | SHA1 | Date | |
|---|---|---|---|
| 1217b25de5 | |||
| e913630f90 | |||
| 220bb2708b | |||
| e57ab3e1f3 | |||
| 952ab9b753 | |||
| 28f9aabcd9 | |||
| 3d9bd73336 | |||
| 3497d5f523 | |||
| 8ef1d95e65 | |||
| e9aeb9ac60 | |||
| fb02f7294d | |||
| f15d49d80a | |||
| c471cff456 | |||
| f83bb10e0c | |||
| 74ad58b1e1 | |||
| 577863b969 | |||
| 03d2a3fd39 | |||
| 802b23ed60 | |||
| 2b9f3a9eef | |||
| 62c60900eb | |||
| b381e1e54c | |||
| e7ee26ce74 | |||
| 521a3fa6e8 | |||
| 5ee93a67fe | |||
| 5d0a03c822 | |||
| 293c2a0035 | |||
| 9a5fa50541 | |||
| d5ceea8a56 | |||
| faa5e7e02d | |||
| 516c478f3d | |||
| 906c6a2501 | |||
| 86d8fe4fa4 | |||
| 9b44bc3259 | |||
| 59b6d2237b | |||
| 7e559cc0bb | |||
| fd1facfce8 | |||
| 8f26193cc3 | |||
| 43b2e5315d | |||
| 5cad89e9cc | |||
| 3804d2f00b | |||
| 4d649468d5 | |||
| c5f145be36 | |||
| 76dff870a7 | |||
| 6c8d3fc007 | |||
| e7ffa23016 | |||
| 4266c064a9 | |||
| 5f631b36cd | |||
| be8326f497 | |||
| 07dbba6c53 | |||
| a53b9afb44 | |||
| a0c7ffd6b9 | |||
| 7ca1d2325a | |||
| 8d0f256f74 | |||
| 1a9449da1b | |||
| 1caf8942b7 | |||
| 9efbcbcd20 | |||
| 3d07b8c9c1 | |||
| dae74a40c0 | |||
| 959190dcbc | |||
| 1e48976ae5 | |||
| 0a8c111e12 | |||
| 19c786c0be | |||
| 7f8b260560 | |||
| 368536f698 | |||
| b227221df6 | |||
| 8e047f45f5 | |||
| fbe8cbb23f | |||
| 28315a165f | |||
| 1b32829dac | |||
| 1fce29324f | |||
| 1fab90178a | |||
| eb04ab7dca | |||
| e9fa2c78ee | |||
| 59dae954cd | |||
| 5c4ce86da4 | |||
| 0cf2c40377 | |||
| 679a59ad76 | |||
| db3209afbe | |||
| f0be36062e | |||
| 9578d6daf9 | |||
| cc6766135d | |||
| 28c9bb0925 | |||
| 7826d827dd | |||
| 7f392acc54 | |||
| 190fd662ad | |||
| e18587c471 | |||
| ddb079b62d | |||
| e7c8d208e2 | |||
| 0e2162cf29 | |||
| 4cf9b0adc7 | |||
| 1661d32641 | |||
| aa59187403 | |||
| bb08e1233a | |||
| 5343e97ab2 | |||
| edc544df7a | |||
| a880edd9fb | |||
| 7e1d808d70 | |||
| ce93f22669 | |||
| a58037b968 | |||
| ccf0b60935 | |||
| aad7011b1c | |||
| 3bde7ef4d3 | |||
| 04555dbfa7 | |||
| 3b494aa591 | |||
| 365387c3cd | |||
| bb74a0ca4d | |||
| 9cf2ef84b7 | |||
| 46aaf5ff77 | |||
| c544bda5df | |||
| e86227f05f | |||
| 53da60e4ca | |||
| 727d9c6c22 | |||
| 908e1f600e | |||
| 270d800df2 | |||
| d445d182ea | |||
| 476256c9e7 | |||
| 06ea7373f7 | |||
| e78ba77def | |||
| 7113afe9e2 | |||
| 1a2b6524e6 | |||
| 95c4aa9e4c | |||
| 9f2518c9e1 | |||
| 76c50a654a | |||
| ded2c38551 | |||
| 772094eacd | |||
| bddc2f6295 | |||
| 25dce2961b | |||
| 653cfbe6e0 | |||
| c539311083 | |||
| 60118c5d5b | |||
| c6b9b36566 | |||
| fd5b1b7c00 | |||
| ebc77b62c8 | |||
| 2ce888581f | |||
| 0e901b6404 | |||
| 688b9076e7 | |||
| c6ec7579b6 | |||
| 9417edac8d | |||
| 6185cc79d7 | |||
| 4ecdba94c2 | |||
| a11640d840 | |||
| 177a4c4095 | |||
| 63df19ab78 | |||
| 54e0eb5979 | |||
| 185284d422 | |||
| ce240405d9 | |||
| 58b700ed0d | |||
| d436fa4920 | |||
| d58486193e | |||
| e9404eb9b6 | |||
| 0fef4d515e | |||
| 86f9cf4376 | |||
| acf63c57e8 | |||
| baa956c3a1 | |||
| bb2081a936 | |||
| c984b0b9ae | |||
| 754bf8fa3c | |||
| 7840294517 | |||
| caaee88654 | |||
| e872ddc1e7 | |||
| 1eca9b4c2c | |||
| fe5bd42e25 | |||
| 32d16d0673 | |||
| 27c26d35ab | |||
| e04c919d78 | |||
| 246b6c44b0 | |||
| 847ddf7d38 | |||
| 16e8199f9e | |||
| 7d1519f546 | |||
| 1bf66ee482 | |||
| 39a43f4de5 | |||
| 971a511edb | |||
| 0f8e0bc2b4 | |||
| 537c44e354 | |||
| db53a9a719 | |||
| f4a10de790 | |||
| 8c9fe84d02 | |||
| f0fcc77bdb | |||
| cf4c4b7d69 | |||
| 7bb8499353 | |||
| ee1a2bc7de | |||
| 20d383d4ab | |||
| 9cb0d8baf2 | |||
| 362ded3bc9 | |||
| 654f2ec7ad | |||
| 3600e034b8 | |||
| d7c501e175 | |||
| b9e9da579f | |||
| 7bea6b4a62 | |||
| ab211266a6 | |||
| 4da22a4345 | |||
| fbc1ff62c7 | |||
| 1fe72e1a66 | |||
| f82d7610e3 | |||
| bd6ad53875 | |||
| 5d09acca4c | |||
| b4e7c4a4cd | |||
| a6269084c0 | |||
| 8271d8423d | |||
| c76e29c457 | |||
| 4750a7553b | |||
| 60786921a9 | |||
| 751bbcc127 | |||
| 58e8dd5456 | |||
| 1586c25847 | |||
| cded5a7948 | |||
| 6238e065f2 | |||
| 72fbbfdd0d | |||
| 2796c1cd4d | |||
| 54c9886627 | |||
| 05713cb389 | |||
| 8bb3ee5f18 | |||
| bc0b4825f1 | |||
| ef7f17abf4 | |||
| 876cd21f0b | |||
| 5c92d49873 | |||
| da47b573be | |||
| 2f04be8778 | |||
| 69178ddfd8 | |||
| a310f6fff0 | |||
| 7474abe286 | |||
| df2ba3a3c6 | |||
| e536456236 | |||
| 8d77122da3 | |||
| fb66effa51 | |||
| 5052e71c31 | |||
| bfc78d16ca | |||
| c425e3562b | |||
| 1f26092aa9 | |||
| 2849bb96f4 | |||
| 7b749f2a4c | |||
| 8803c94ce0 | |||
| f5235c943b | |||
| 59fec889b5 | |||
| f42f04a807 | |||
| 51f6f37925 | |||
| 9651a4ca98 | |||
| 2b7fd36322 | |||
| b8014fd4df | |||
| 07460f6e1f | |||
| f7bf3b2afb | |||
| 056f298cdf | |||
| e83da53162 | |||
| 9f38a47a02 | |||
| 236a4d4a6d | |||
| 0909471510 | |||
| 05eaf59c89 | |||
| 7749613801 | |||
| e3dbaedbb4 | |||
| 9f17ced6de | |||
| de54ef871d | |||
| b0da45d6d0 | |||
| 9b2a46fa92 | |||
| 12099d2db6 | |||
| 84fa75936a | |||
| d78d8121a1 | |||
| a9a3a52872 | |||
| 912e3bdfce | |||
| ee1b25d9e8 | |||
| 20ef5e2c18 | |||
| 6ee419bc52 | |||
| 85b00d3c76 | |||
| bc4ad31d48 | |||
| 71aad8ee32 | |||
| 8bb8231559 | |||
| 3cf9caae89 | |||
| f983c67135 | |||
| f2aef5b93f | |||
| 46d4288969 | |||
| 65516e872f | |||
| 171329246c | |||
| b2bee699e0 | |||
| 95c66b4d67 | |||
| babc8feb2b | |||
| 2f445c546a | |||
| a0283b3e3e | |||
| 61bd156fb0 | |||
| 8ad0cf8e5f | |||
| ecd6d70da6 | |||
| 359617d927 | |||
| 38c286329a | |||
| 401b4095cc | |||
| 06ab1a8ef0 | |||
| 726fb8b015 | |||
| b3b5c7a59f | |||
| d18f934978 | |||
| e67f1f79bc | |||
| e931829411 | |||
| db8ebd606c | |||
| 072a358a94 | |||
| 6ceb1b150c | |||
| a4e03e1877 | |||
| 02b3e4277b | |||
| 37daf801e6 | |||
| 68d9f7eeb2 | |||
| 526e2420ca | |||
| a9cc58fc28 | |||
| 77889ce1c6 | |||
| 549073119e | |||
| 5c5af5795f | |||
| 68e10934e4 | |||
| c67bb1444a | |||
| 07389a152e | |||
| e562e21555 | |||
| 86dfe7dd3f | |||
| ac0a8f3449 | |||
| 8e4a63db67 | |||
| c02c63806f | |||
| 42e5d7f6e9 | |||
| d8cf1af422 | |||
| 9723661c80 | |||
| 81cba7ad97 | |||
| c23f58de40 | |||
| 2cf67ca7da | |||
| 392bd850ea | |||
| 3b2ad9d1bd | |||
| 27b7474180 | |||
| 63948d728e | |||
| d219d3b873 | |||
| 93ab290bc1 | |||
| 7335c5d79a | |||
| 242ead722a | |||
| 8a6d9696a8 | |||
| 896b7ea242 | |||
| 0c7f4c7828 | |||
| 3d35af2a87 | |||
| fed3c36f84 | |||
| 414d81aa40 | |||
| d548803769 | |||
| 1180258394 | |||
| 48a566a24b | |||
| 3bc5d1df81 | |||
| c7222e2e86 | |||
| 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 |
@@ -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)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,32 +7,32 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## DankMaterialShell Bug Report
|
## DankMaterialShell Bug Report
|
||||||
Limit your report to one issue per submission unless closely related
|
Limit your report to one issue per submission unless similarly related
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor
|
label: Compositor
|
||||||
options:
|
options:
|
||||||
- label: Niri
|
- Niri
|
||||||
- label: Hyprland
|
- Hyprland
|
||||||
- label: MangoWC (dwl)
|
- MangoWC (dwl)
|
||||||
- label: Sway
|
- Sway
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: distribution
|
id: distribution
|
||||||
attributes:
|
attributes:
|
||||||
label: Distribution
|
label: Distribution
|
||||||
options:
|
options:
|
||||||
- label: Arch Linux
|
- Arch Linux
|
||||||
- label: CachyOS
|
- CachyOS
|
||||||
- label: Fedora
|
- Fedora
|
||||||
- label: NixOS
|
- NixOS
|
||||||
- label: Debian
|
- Debian
|
||||||
- label: Ubuntu
|
- Ubuntu
|
||||||
- label: Gentoo
|
- Gentoo
|
||||||
- label: OpenSUSE
|
- OpenSUSE
|
||||||
- label: Other (specify below)
|
- Other (specify below)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@@ -42,12 +42,45 @@ body:
|
|||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
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
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -v
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -v` command
|
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 -v` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -69,7 +102,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps_to_reproduce
|
id: steps_to_reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce & Installation Method
|
label: Steps to Reproduce
|
||||||
description: Please provide detailed steps to reproduce the issue
|
description: Please provide detailed steps to reproduce the issue
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. ...
|
1. ...
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Feature Request
|
name: Feature Request
|
||||||
description: Suggest a new feature or improvement for DMS
|
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:
|
labels:
|
||||||
- enhancement
|
- enhancement
|
||||||
body:
|
body:
|
||||||
@@ -23,18 +23,25 @@ body:
|
|||||||
placeholder: Why is this feature important?
|
placeholder: Why is this feature important?
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor(s)
|
label: Compositor(s)
|
||||||
description: Is this feature specific to one or more compositors?
|
description: Is this feature specific to one or more compositors?
|
||||||
options:
|
options:
|
||||||
- label: All compositors
|
- All compositors
|
||||||
- label: Niri
|
- Niri
|
||||||
- label: Hyprland
|
- Hyprland
|
||||||
- label: MangoWC (dwl)
|
- MangoWC (dwl)
|
||||||
- label: Sway
|
- Sway
|
||||||
- label: Other (specify below)
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: compositor_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., Wayfire, Mutter, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -7,32 +7,87 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## DankMaterialShell Support Request
|
## DankMaterialShell Support Request
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor
|
label: Compositor
|
||||||
options:
|
options:
|
||||||
- label: Niri
|
- Niri
|
||||||
- label: Hyprland
|
- Hyprland
|
||||||
- label: MangoWC (dwl)
|
- MangoWC (dwl)
|
||||||
- label: Sway
|
- Sway
|
||||||
- label: Other (specify below)
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: compositor_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., Wayfire, Mutter, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: dropdown
|
||||||
id: distribution
|
id: distribution
|
||||||
attributes:
|
attributes:
|
||||||
label: Distribution
|
label: Distribution
|
||||||
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
options:
|
||||||
placeholder: Your Linux distribution
|
- 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:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -v
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -v` command
|
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 -v` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
@@ -1,383 +0,0 @@
|
|||||||
name: Update OBS Packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package:
|
|
||||||
description: "Package to update (dms, dms-git, or all)"
|
|
||||||
required: false
|
|
||||||
default: "all"
|
|
||||||
force_upload:
|
|
||||||
description: "Force upload without version check"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "false"
|
|
||||||
- "true"
|
|
||||||
rebuild_release:
|
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
schedule:
|
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-updates:
|
|
||||||
name: Check for updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
|
||||||
packages: ${{ steps.check.outputs.packages }}
|
|
||||||
version: ${{ steps.check.outputs.version }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by tag: $VERSION (always update)"
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
|
||||||
echo "Checking if dms-git source has changed..."
|
|
||||||
|
|
||||||
# Get current commit hash (8 chars to match spec format)
|
|
||||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
|
||||||
|
|
||||||
# Check OBS for last uploaded commit
|
|
||||||
OBS_BASE="$HOME/.cache/osc-checkouts"
|
|
||||||
mkdir -p "$OBS_BASE"
|
|
||||||
OBS_PROJECT="home:AvengeMedia:dms-git"
|
|
||||||
|
|
||||||
if [[ -d "$OBS_BASE/$OBS_PROJECT/dms-git" ]]; then
|
|
||||||
cd "$OBS_BASE/$OBS_PROJECT/dms-git"
|
|
||||||
osc up -q 2>/dev/null || true
|
|
||||||
|
|
||||||
# Extract commit hash from spec Version line & format like; 0.6.2+git2264.a679be68
|
|
||||||
if [[ -f "dms-git.spec" ]]; then
|
|
||||||
OBS_COMMIT=$(grep "^Version:" "dms-git.spec" | grep -oP '\.[a-f0-9]{8}' | tr -d '.' || echo "")
|
|
||||||
|
|
||||||
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
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No spec file in OBS, proceeding with update"
|
|
||||||
fi
|
|
||||||
|
|
||||||
cd "${{ github.workspace }}"
|
|
||||||
else
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 First upload to OBS, update needed"
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
|
||||||
PKG="${{ github.event.inputs.package }}"
|
|
||||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: all packages"
|
|
||||||
else
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: $PKG"
|
|
||||||
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
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
update-obs:
|
|
||||||
name: Upload to OBS
|
|
||||||
needs: check-updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: |
|
|
||||||
github.event.inputs.force_upload == 'true' ||
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Generate GitHub App Token
|
|
||||||
id: generate_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.generate_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Check if last commit was automated
|
|
||||||
id: check-loop
|
|
||||||
run: |
|
|
||||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
|
||||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
|
||||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "✅ Last commit was not automated, proceeding"
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Determine packages to update
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: packages
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by tag: $VERSION"
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by schedule: updating git package"
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Manual trigger: ${{ github.event.inputs.package }}"
|
|
||||||
else
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Update dms-git spec version
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
|
||||||
run: |
|
|
||||||
# Get commit info for dms-git versioning
|
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 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")
|
|
||||||
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
|
||||||
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
|
|
||||||
|
|
||||||
# Add changelog entry
|
|
||||||
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)"
|
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY" distro/opensuse/dms-git.spec
|
|
||||||
|
|
||||||
- name: Update Debian dms-git changelog version
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && (contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all')
|
|
||||||
run: |
|
|
||||||
# Get commit info for dms-git versioning
|
|
||||||
COMMIT_HASH=$(git rev-parse --short=8 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")
|
|
||||||
|
|
||||||
# Debian version format: 0.6.2+git2256.9162e314
|
|
||||||
NEW_VERSION="${BASE_VERSION}+git${COMMIT_COUNT}.${COMMIT_HASH}"
|
|
||||||
echo "📦 Updating Debian dms-git changelog to version: $NEW_VERSION"
|
|
||||||
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
|
|
||||||
CHANGELOG_FILE="distro/debian/dms-git/debian/changelog"
|
|
||||||
|
|
||||||
# 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.check-loop.outputs.skip != 'true' && steps.packages.outputs.version != ''
|
|
||||||
run: |
|
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
|
||||||
VERSION_NO_V="${VERSION#v}"
|
|
||||||
echo "Updating packaging to version $VERSION_NO_V"
|
|
||||||
|
|
||||||
# Update openSUSE dms spec (stable only)
|
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
# Update openSUSE spec changelog
|
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
|
||||||
CHANGELOG_ENTRY="* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1\\n- Update to stable $VERSION release\\n- Bug fixes and improvements"
|
|
||||||
sed -i "/%changelog/a\\$CHANGELOG_ENTRY\\n" distro/opensuse/dms.spec
|
|
||||||
|
|
||||||
# Update Debian _service files (both tar_scm and download_url formats)
|
|
||||||
for service in distro/debian/*/_service; do
|
|
||||||
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"
|
|
||||||
|
|
||||||
# Update download_url paths (for dms 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
|
|
||||||
done
|
|
||||||
|
|
||||||
# Update Debian changelog for dms stable
|
|
||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
|
||||||
CHANGELOG_DATE=$(date -R)
|
|
||||||
TEMP_CHANGELOG=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$TEMP_CHANGELOG" << EOF
|
|
||||||
dms ($VERSION_NO_V) stable; urgency=medium
|
|
||||||
|
|
||||||
* Update to $VERSION stable release
|
|
||||||
* Bug fixes and improvements
|
|
||||||
|
|
||||||
-- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
cat "distro/debian/dms/debian/changelog" >> "$TEMP_CHANGELOG"
|
|
||||||
mv "$TEMP_CHANGELOG" "distro/debian/dms/debian/changelog"
|
|
||||||
|
|
||||||
echo "✓ Updated Debian changelog to $VERSION_NO_V"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.24"
|
|
||||||
|
|
||||||
- name: Install OSC
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
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: Upload to OBS
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
env:
|
|
||||||
FORCE_UPLOAD: ${{ github.event.inputs.force_upload }}
|
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
|
||||||
run: |
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
MESSAGE="Automated update from GitHub Actions"
|
|
||||||
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
bash distro/scripts/obs-upload.sh dms "$MESSAGE"
|
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
|
||||||
else
|
|
||||||
bash distro/scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Get changed packages
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: changed-packages
|
|
||||||
run: |
|
|
||||||
# Check if there are any changes to commit
|
|
||||||
if git diff --exit-code distro/debian/ distro/opensuse/ >/dev/null 2>&1; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog or spec changes to commit"
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
# Get list of changed packages for commit message
|
|
||||||
CHANGED_DEB=$(git diff --name-only distro/debian/ 2>/dev/null | grep 'debian/changelog' | xargs dirname 2>/dev/null | xargs dirname 2>/dev/null | xargs basename 2>/dev/null | tr '\n' ', ' | sed 's/, $//' || echo "")
|
|
||||||
CHANGED_SUSE=$(git diff --name-only distro/opensuse/ 2>/dev/null | grep '\.spec$' | sed 's|distro/opensuse/||' | sed 's/\.spec$//' | tr '\n' ', ' | sed 's/, $//' || echo "")
|
|
||||||
|
|
||||||
PKGS=$(echo "$CHANGED_DEB,$CHANGED_SUSE" | tr ',' '\n' | grep -v '^$' | sort -u | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "packages=$PKGS" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Changed packages: $PKGS"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit packaging changes
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "dms-ci[bot]"
|
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
|
||||||
git add distro/debian/*/debian/changelog distro/opensuse/*.spec
|
|
||||||
git commit -m "ci: Auto-update OBS packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
|
||||||
git pull --rebase origin master
|
|
||||||
git push
|
|
||||||
|
|
||||||
- name: Summary
|
|
||||||
run: |
|
|
||||||
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
|
|
||||||
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
|
|
||||||
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
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
name: Update PPA Packages
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
package:
|
|
||||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
|
||||||
required: false
|
|
||||||
default: "dms-git"
|
|
||||||
force_upload:
|
|
||||||
description: "Force upload without version check"
|
|
||||||
required: false
|
|
||||||
default: "false"
|
|
||||||
type: choice
|
|
||||||
options:
|
|
||||||
- "false"
|
|
||||||
- "true"
|
|
||||||
rebuild_release:
|
|
||||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
schedule:
|
|
||||||
- cron: "0 */3 * * *" # Every 3 hours for dms-git builds
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-updates:
|
|
||||||
name: Check for updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
outputs:
|
|
||||||
has_updates: ${{ steps.check.outputs.has_updates }}
|
|
||||||
packages: ${{ steps.check.outputs.packages }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Check for updates
|
|
||||||
id: check
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
|
||||||
echo "Checking if dms-git source has changed..."
|
|
||||||
|
|
||||||
# Get current commit hash (8 chars to match changelog format)
|
|
||||||
CURRENT_COMMIT=$(git rev-parse --short=8 HEAD)
|
|
||||||
|
|
||||||
# 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
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog file found, proceeding with upload"
|
|
||||||
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
|
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
upload-ppa:
|
|
||||||
name: Upload to PPA
|
|
||||||
needs: check-updates
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
if: |
|
|
||||||
github.event.inputs.force_upload == 'true' ||
|
|
||||||
github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.check-updates.outputs.has_updates == 'true'
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Generate GitHub App Token
|
|
||||||
id: generate_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.generate_token.outputs.token }}
|
|
||||||
|
|
||||||
- name: Check if last commit was automated
|
|
||||||
id: check-loop
|
|
||||||
run: |
|
|
||||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | head -1)
|
|
||||||
if [[ "$LAST_COMMIT_MSG" == "ci: Auto-update PPA packages"* ]] || [[ "$LAST_COMMIT_MSG" == "ci: Auto-update OBS packages"* ]]; then
|
|
||||||
echo "⏭️ Last commit was automated ($LAST_COMMIT_MSG), skipping to prevent infinite loop"
|
|
||||||
echo "skip=true" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "✅ Last commit was not automated, proceeding"
|
|
||||||
echo "skip=false" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: "1.24"
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Install build dependencies
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
debhelper \
|
|
||||||
devscripts \
|
|
||||||
dput \
|
|
||||||
lftp \
|
|
||||||
build-essential \
|
|
||||||
fakeroot \
|
|
||||||
dpkg-dev
|
|
||||||
|
|
||||||
- name: Configure GPG
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
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: Determine packages to upload
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: packages
|
|
||||||
run: |
|
|
||||||
if [[ "${{ github.event.inputs.force_upload }}" == "true" ]]; then
|
|
||||||
PKG="${{ github.event.inputs.package }}"
|
|
||||||
if [[ -z "$PKG" || "$PKG" == "all" ]]; then
|
|
||||||
echo "packages=all" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: all packages"
|
|
||||||
else
|
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "🚀 Force upload: $PKG"
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
echo "Triggered by schedule: uploading git package"
|
|
||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
|
||||||
# Manual package selection should respect change detection
|
|
||||||
SELECTED_PKG="${{ github.event.inputs.package }}"
|
|
||||||
UPDATED_PKG="${{ needs.check-updates.outputs.packages }}"
|
|
||||||
|
|
||||||
# Check if manually selected package is in the updated list
|
|
||||||
if [[ "$UPDATED_PKG" == *"$SELECTED_PKG"* ]] || [[ "$SELECTED_PKG" == "all" ]]; then
|
|
||||||
echo "packages=$SELECTED_PKG" >> $GITHUB_OUTPUT
|
|
||||||
echo "📦 Manual selection (has updates): $SELECTED_PKG"
|
|
||||||
else
|
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
|
||||||
echo "⚠️ Manual selection '$SELECTED_PKG' has no updates - skipping (use force_upload to override)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Upload to PPA
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
run: |
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
REBUILD_RELEASE="${{ github.event.inputs.rebuild_release }}"
|
|
||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
|
||||||
echo "No packages selected for upload. Skipping."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build command arguments
|
|
||||||
BUILD_ARGS=()
|
|
||||||
if [[ -n "$REBUILD_RELEASE" ]]; then
|
|
||||||
BUILD_ARGS+=("$REBUILD_RELEASE")
|
|
||||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms to PPA..."
|
|
||||||
if [ -n "$REBUILD_RELEASE" ]; then
|
|
||||||
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
|
|
||||||
fi
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh dms dms questing "${BUILD_ARGS[@]}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-git to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh dms-git dms-git questing "${BUILD_ARGS[@]}"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading dms-greeter to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh dms-greeter danklinux questing "${BUILD_ARGS[@]}"
|
|
||||||
else
|
|
||||||
# Map package to PPA name
|
|
||||||
case "$PACKAGES" in
|
|
||||||
dms)
|
|
||||||
PPA_NAME="dms"
|
|
||||||
;;
|
|
||||||
dms-git)
|
|
||||||
PPA_NAME="dms-git"
|
|
||||||
;;
|
|
||||||
dms-greeter)
|
|
||||||
PPA_NAME="danklinux"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
PPA_NAME="$PACKAGES"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
echo "Uploading $PACKAGES to PPA..."
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
bash distro/scripts/ppa-upload.sh "$PACKAGES" "$PPA_NAME" questing "${BUILD_ARGS[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Get changed packages
|
|
||||||
if: steps.check-loop.outputs.skip != 'true'
|
|
||||||
id: changed-packages
|
|
||||||
run: |
|
|
||||||
# Check if there are any changelog changes to commit
|
|
||||||
if git diff --exit-code distro/ubuntu/ >/dev/null 2>&1; then
|
|
||||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 No changelog changes to commit"
|
|
||||||
else
|
|
||||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
|
||||||
# Get list of changed packages for commit message (deduplicate)
|
|
||||||
CHANGED=$(git diff --name-only distro/ubuntu/ | grep 'debian/changelog' | sed 's|/debian/changelog||' | xargs -I{} basename {} | sort -u | tr '\n' ',' | sed 's/,$//')
|
|
||||||
echo "packages=$CHANGED" >> $GITHUB_OUTPUT
|
|
||||||
echo "📋 Changed packages: $CHANGED"
|
|
||||||
echo "📋 Debug - Changed files:"
|
|
||||||
git diff --name-only distro/ubuntu/ | grep 'debian/changelog' || echo "No changelog files found"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Commit changelog changes
|
|
||||||
if: steps.check-loop.outputs.skip != 'true' && steps.changed-packages.outputs.has_changes == 'true'
|
|
||||||
run: |
|
|
||||||
git config user.name "dms-ci[bot]"
|
|
||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
|
||||||
git add distro/ubuntu/*/debian/changelog
|
|
||||||
git commit -m "ci: Auto-update PPA packages [${{ steps.changed-packages.outputs.packages }}]" -m "🤖 Automated by GitHub Actions"
|
|
||||||
git pull --rebase origin master
|
|
||||||
git push
|
|
||||||
|
|
||||||
- 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
|
|
||||||
echo "- **Status**: Skipped (no changes detected)" >> $GITHUB_STEP_SUMMARY
|
|
||||||
fi
|
|
||||||
|
|
||||||
PACKAGES="${{ steps.packages.outputs.packages }}"
|
|
||||||
if [[ "$PACKAGES" == "all" ]]; then
|
|
||||||
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "- **PPA danklinux**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [[ "$PACKAGES" == "dms" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [[ "$PACKAGES" == "dms-git" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
elif [[ "$PACKAGES" == "dms-greeter" ]]; then
|
|
||||||
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/danklinux/+packages" >> $GITHUB_STEP_SUMMARY
|
|
||||||
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
|
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
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@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Install flatpak
|
- name: Install flatpak
|
||||||
run: sudo apt update && sudo apt install -y flatpak
|
run: sudo apt update && sudo apt install -y flatpak
|
||||||
@@ -20,5 +20,10 @@ jobs:
|
|||||||
- name: Add a flatpak that mutagen could support
|
- name: Add a flatpak that mutagen could support
|
||||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: core/go.mod
|
||||||
|
|
||||||
- name: run pre-commit hooks
|
- name: run pre-commit hooks
|
||||||
uses: j178/prek-action@v1
|
uses: j178/prek-action@v1
|
||||||
|
|||||||
@@ -32,13 +32,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: ./core/go.mod
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts (${{ matrix.arch }})
|
- name: Upload artifacts (${{ matrix.arch }})
|
||||||
if: matrix.arch == 'arm64'
|
if: matrix.arch == 'arm64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -120,7 +120,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload artifacts with completions
|
- name: Upload artifacts with completions
|
||||||
if: matrix.arch == 'amd64'
|
if: matrix.arch == 'amd64'
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: core-assets-${{ matrix.arch }}
|
name: core-assets-${{ matrix.arch }}
|
||||||
path: |
|
path: |
|
||||||
@@ -147,7 +147,7 @@ jobs:
|
|||||||
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# - name: Checkout
|
# - name: Checkout
|
||||||
# uses: actions/checkout@v4
|
# uses: actions/checkout@v6
|
||||||
# with:
|
# with:
|
||||||
# token: ${{ steps.app_token.outputs.token }}
|
# token: ${{ steps.app_token.outputs.token }}
|
||||||
# fetch-depth: 0
|
# fetch-depth: 0
|
||||||
@@ -181,7 +181,7 @@ jobs:
|
|||||||
TAG: ${{ inputs.tag }}
|
TAG: ${{ inputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.tag }}
|
ref: ${{ inputs.tag }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -191,8 +191,13 @@ jobs:
|
|||||||
git fetch origin --force tag ${TAG}
|
git fetch origin --force tag ${TAG}
|
||||||
git checkout ${TAG}
|
git checkout ${TAG}
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Download core artifacts
|
- name: Download core artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v5
|
||||||
with:
|
with:
|
||||||
pattern: core-assets-*
|
pattern: core-assets-*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
@@ -229,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
|
||||||
@@ -387,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:
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: version
|
id: version
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
rpm -qpi "$SRPM"
|
rpm -qpi "$SRPM"
|
||||||
|
|
||||||
- name: Upload SRPM artifact
|
- name: Upload SRPM artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v5
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.package }}-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 }}
|
||||||
|
|||||||
+176
-93
@@ -9,6 +9,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
|
- dms-greeter
|
||||||
- dms-git
|
- dms-git
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
@@ -17,7 +18,7 @@ on:
|
|||||||
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:
|
||||||
@@ -31,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -72,12 +73,27 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Helper function to check dms-greeter stable tag
|
||||||
|
check_dms_greeter_stable() {
|
||||||
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
|
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
||||||
|
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
||||||
|
|
||||||
|
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
||||||
|
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Tag selected or pushed - always update stable package
|
# Run from tag with no package specified - update both stable packages
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
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
|
||||||
@@ -103,15 +119,18 @@ jobs:
|
|||||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||||
|
|
||||||
elif [[ "$PKG" == "all" ]]; then
|
elif [[ "$PKG" == "all" ]]; then
|
||||||
# Check each package and build list of those needing updates
|
# Check each stable package and build list of those needing updates
|
||||||
PACKAGES_TO_UPDATE=()
|
PACKAGES_TO_UPDATE=()
|
||||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
|
||||||
if check_dms_stable; then
|
if check_dms_stable; then
|
||||||
PACKAGES_TO_UPDATE+=("dms")
|
PACKAGES_TO_UPDATE+=("dms")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if check_dms_greeter_stable; then
|
||||||
|
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||||
|
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -120,7 +139,7 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "packages=" >> $GITHUB_OUTPUT
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
echo "✓ All packages up to date"
|
echo "✓ Both packages up to date"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
elif [[ "$PKG" == "dms-git" ]]; then
|
elif [[ "$PKG" == "dms-git" ]]; then
|
||||||
@@ -144,6 +163,18 @@ jobs:
|
|||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||||
|
if check_dms_greeter_stable; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
# Unknown package - proceed anyway
|
# Unknown package - proceed anyway
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
@@ -164,22 +195,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Wait before OBS upload
|
||||||
|
run: sleep 3
|
||||||
|
|
||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
# Use check-updates outputs when available
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
# Tag selected or pushed - use the tag from GITHUB_REF
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
|
||||||
# Check if check-updates already determined a version (from auto-detection)
|
|
||||||
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
|
||||||
# Use version from check-updates job
|
# Use version from check-updates job
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
@@ -191,40 +218,16 @@ jobs:
|
|||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
# Manual workflow dispatch
|
||||||
|
|
||||||
# Determine version for dms stable
|
# Determine version for dms stable and dms-greeter using the API
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
echo "Using latest release from API: $LATEST_TAG"
|
||||||
else
|
else
|
||||||
# Auto-detect latest release for dms
|
echo "ERROR: Could not fetch latest release from API"
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
exit 1
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
echo "Auto-detected latest release: $LATEST_TAG"
|
|
||||||
else
|
|
||||||
echo "ERROR: Could not auto-detect latest release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
|
||||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
|
||||||
else
|
|
||||||
# Auto-detect latest release for "all"
|
|
||||||
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 "Auto-detected latest release: $LATEST_TAG"
|
|
||||||
else
|
|
||||||
echo "ERROR: Could not auto-detect latest release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -244,7 +247,7 @@ jobs:
|
|||||||
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')
|
||||||
run: |
|
run: |
|
||||||
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)
|
||||||
@@ -265,7 +268,7 @@ jobs:
|
|||||||
} > distro/opensuse/dms-git.spec
|
} > 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')
|
||||||
run: |
|
run: |
|
||||||
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)
|
||||||
@@ -283,59 +286,70 @@ jobs:
|
|||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
- name: Update dms stable version
|
- name: Update stable version (dms + dms-greeter)
|
||||||
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}"
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||||
|
|
||||||
# Update spec file
|
# Update dms spec and changelog when dms is in the upload list
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
if [[ "$PACKAGES" == *"dms"* ]]; then
|
||||||
|
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"
|
||||||
|
|
||||||
# Verify the update
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||||
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
{
|
||||||
|
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
|
||||||
|
|
||||||
# Single changelog entry (full history on OBS website)
|
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
CHANGELOG_DATE=$(date -R)
|
||||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
{
|
||||||
{
|
echo "dms (${VERSION_NO_V}db1) stable; urgency=medium"
|
||||||
echo "$LOCAL_SPEC_HEAD"
|
echo ""
|
||||||
echo "%changelog"
|
echo " * Update to $VERSION stable release"
|
||||||
echo "* $DATE_STR AvengeMedia <maintainer@avengemedia.com> - ${VERSION_NO_V}-1"
|
echo ""
|
||||||
echo "- Update to stable $VERSION release"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > distro/opensuse/dms.spec
|
} > "distro/debian/dms/debian/changelog"
|
||||||
|
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Update Debian _service files (both tar_scm and download_url formats)
|
# 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)
|
# Update tar_scm revision parameter (for dms-git)
|
||||||
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
|
||||||
|
# Update download_url paths (for dms, dms-greeter stable)
|
||||||
# Update download_url paths (for dms stable)
|
|
||||||
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Update Debian changelog for dms stable (single entry, history on OBS website)
|
|
||||||
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 Debian changelog to ${VERSION_NO_V}db1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version-file: ./core/go.mod
|
||||||
|
|
||||||
- name: Install OSC
|
- name: Install OSC
|
||||||
run: |
|
run: |
|
||||||
@@ -354,6 +368,7 @@ jobs:
|
|||||||
chmod 600 ~/.config/osc/oscrc
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
- name: Upload to OBS
|
- name: Upload to OBS
|
||||||
|
id: upload
|
||||||
env:
|
env:
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||||
@@ -362,6 +377,8 @@ jobs:
|
|||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
echo "✓ No packages need uploading. All up to date!"
|
echo "✓ No packages need uploading. All up to date!"
|
||||||
|
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -371,7 +388,10 @@ jobs:
|
|||||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
UPLOADED_PACKAGES=()
|
||||||
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
|
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
||||||
# Loop through each package and upload
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
echo ""
|
echo ""
|
||||||
@@ -382,13 +402,37 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
LOG_FILE=$(mktemp)
|
||||||
|
set +e
|
||||||
if [[ "$PKG" == "dms-git" ]]; then
|
if [[ "$PKG" == "dms-git" ]]; then
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
|
||||||
else
|
else
|
||||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
||||||
fi
|
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
|
done
|
||||||
|
|
||||||
|
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||||
|
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@@ -402,20 +446,59 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
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
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
for PKG in $PACKAGES; do
|
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
|
case "$PKG" in
|
||||||
dms)
|
dms)
|
||||||
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
dms-git)
|
dms-git)
|
||||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
package:
|
package:
|
||||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
description: "Package to upload"
|
||||||
required: false
|
required: true
|
||||||
default: "dms-git"
|
type: choice
|
||||||
|
options:
|
||||||
|
- dms
|
||||||
|
- dms-greeter
|
||||||
|
- dms-git
|
||||||
|
- all
|
||||||
|
default: "dms"
|
||||||
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:
|
||||||
@@ -25,7 +31,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -139,7 +145,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
# Fallback
|
# Fallback
|
||||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -151,14 +157,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
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@v6
|
||||||
with:
|
with:
|
||||||
go-version: "1.24"
|
go-version-file: ./core/go.mod
|
||||||
cache: false
|
cache: false
|
||||||
|
|
||||||
- name: Install build dependencies
|
- name: Install build dependencies
|
||||||
@@ -209,7 +215,7 @@ jobs:
|
|||||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
# PACKAGES can be space-separated list (e.g., "dms-git dms dms-greeter" from "all" check)
|
||||||
# Loop through each package and upload
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
# Map package to PPA name
|
# Map package to PPA name
|
||||||
@@ -236,7 +242,11 @@ jobs:
|
|||||||
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 "$PKG" "$PPA_NAME" questing ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}
|
# ppa-upload.sh uploads to questing + resolute when series is omitted
|
||||||
|
if ! bash distro/scripts/ppa-upload.sh "$PKG" "$PPA_NAME" ${REBUILD_RELEASE:+"$REBUILD_RELEASE"}; then
|
||||||
|
echo "::error::Upload failed for $PKG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
token: ${{ steps.app_token.outputs.token }}
|
token: ${{ steps.app_token.outputs.token }}
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
|
||||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
git add flake.nix
|
git add flake.nix
|
||||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
git pull --rebase origin master
|
git pull --rebase origin ${{ github.ref_name }}
|
||||||
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:${{ github.ref_name }}
|
||||||
else
|
else
|
||||||
echo "No changes to flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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.*
|
||||||
|
|||||||
+13
-3
@@ -5,8 +5,18 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
- repo: local
|
||||||
rev: v0.10.0.1
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
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
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
This file is more of a quick reference so I know what to account for before next releases.
|
This file is more of a quick reference so I know what to account for before next releases.
|
||||||
|
|
||||||
|
# 1.5.0
|
||||||
|
- Overhauled shadows
|
||||||
|
- App ID changed to com.danklinux.dms - breaking for window rules
|
||||||
|
- Greeter stuff
|
||||||
|
- Terminal mux
|
||||||
|
- Locale overrides
|
||||||
|
- new neovim theming
|
||||||
|
|
||||||
# 1.4.0
|
# 1.4.0
|
||||||
|
|
||||||
- Overhauled system monitor, graphs, styling
|
- Overhauled system monitor, graphs, styling
|
||||||
@@ -8,6 +16,7 @@ This file is more of a quick reference so I know what to account for before next
|
|||||||
- launcher actions, customize env, args, name, icon
|
- launcher actions, customize env, args, name, icon
|
||||||
- launcher v2 - omega stuff, GIF search, supa powerful
|
- launcher v2 - omega stuff, GIF search, supa powerful
|
||||||
- dock on bar
|
- dock on bar
|
||||||
|
- window rule manager, with IPC - #TODO verify RTL layout (niri only)
|
||||||
|
|
||||||
# 1.2.0
|
# 1.2.0
|
||||||
|
|
||||||
|
|||||||
+38
-3
@@ -22,7 +22,7 @@ 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
|
||||||
|
|
||||||
@@ -37,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -53,7 +86,9 @@ touch .qmlls.ini
|
|||||||
|
|
||||||
4. Restart dms to generate the `.qmlls.ini` file
|
4. Restart dms to generate the `.qmlls.ini` file
|
||||||
|
|
||||||
5. Make your changes, test, and open a pull request.
|
5. Run `make lint-qml` from the repo root to lint QML entrypoints (requires the `.qmlls.ini` generated above). The script needs the **Qt 6** `qmllint`; it checks `qmllint6`, Fedora's `qmllint-qt6`, `/usr/lib/qt6/bin/qmllint`, then `qmllint` in `PATH`. If your Qt 6 binary lives elsewhere, set `QMLLINT=/path/to/qmllint`.
|
||||||
|
|
||||||
|
6. Make your changes, test, and open a pull request.
|
||||||
|
|
||||||
### I18n/Localization
|
### I18n/Localization
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ SHELL_INSTALL_DIR=$(DATA_DIR)/quickshell/dms
|
|||||||
ASSETS_DIR=assets
|
ASSETS_DIR=assets
|
||||||
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
APPLICATIONS_DIR=$(DATA_DIR)/applications
|
||||||
|
|
||||||
.PHONY: all build clean install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
.PHONY: all build clean lint-qml install install-bin install-shell install-completions install-systemd install-icon install-desktop uninstall uninstall-bin uninstall-shell uninstall-completions uninstall-systemd uninstall-icon uninstall-desktop help
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
@@ -32,6 +32,9 @@ clean:
|
|||||||
@$(MAKE) -C $(CORE_DIR) clean
|
@$(MAKE) -C $(CORE_DIR) clean
|
||||||
@echo "Clean complete"
|
@echo "Clean complete"
|
||||||
|
|
||||||
|
lint-qml:
|
||||||
|
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||||
|
|
||||||
# Installation targets
|
# Installation targets
|
||||||
install-bin:
|
install-bin:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@@ -58,10 +61,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:
|
||||||
@@ -76,7 +79,7 @@ install-desktop:
|
|||||||
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
@update-desktop-database -q $(APPLICATIONS_DIR) 2>/dev/null || true
|
||||||
@echo "Desktop entry installed"
|
@echo "Desktop entry installed"
|
||||||
|
|
||||||
install: build install-bin install-shell install-completions install-systemd install-icon install-desktop
|
install: install-bin install-shell install-completions install-systemd install-icon install-desktop
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Installation complete!"
|
@echo "Installation complete!"
|
||||||
@echo ""
|
@echo ""
|
||||||
@@ -130,6 +133,7 @@ help:
|
|||||||
@echo " all (default) - Build the DMS binary"
|
@echo " all (default) - Build the DMS binary"
|
||||||
@echo " build - Same as 'all'"
|
@echo " build - Same as 'all'"
|
||||||
@echo " clean - Clean build artifacts"
|
@echo " clean - Clean build artifacts"
|
||||||
|
@echo " lint-qml - Run qmllint on shell entrypoints using the Quickshell tooling VFS"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Install:"
|
@echo "Install:"
|
||||||
@echo " install - Build and install everything (requires sudo)"
|
@echo " install - Build and install everything (requires sudo)"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
|||||||
|
|
||||||
</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/), [Scroll](https://github.com/dawsers/scroll), 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), [labwc](https://labwc.github.io/), and [Scroll](https://github.com/dawsers/scroll) 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)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ packages:
|
|||||||
outpkg: mocks_brightness
|
outpkg: mocks_brightness
|
||||||
interfaces:
|
interfaces:
|
||||||
DBusConn:
|
DBusConn:
|
||||||
|
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
|
||||||
|
config:
|
||||||
|
dir: "internal/mocks/geolocation"
|
||||||
|
outpkg: mocks_geolocation
|
||||||
|
interfaces:
|
||||||
|
Client:
|
||||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/network"
|
dir: "internal/mocks/network"
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
|
||||||
rev: v2.6.2
|
|
||||||
hooks:
|
|
||||||
- id: golangci-lint-fmt
|
|
||||||
require_serial: true
|
|
||||||
- id: golangci-lint-full
|
|
||||||
- id: golangci-lint-config-verify
|
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
- id: golangci-lint-fmt
|
||||||
|
name: golangci-lint-fmt
|
||||||
|
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 fmt
|
||||||
|
language: system
|
||||||
|
require_serial: true
|
||||||
|
types: [go]
|
||||||
|
pass_filenames: false
|
||||||
|
- id: golangci-lint-full
|
||||||
|
name: golangci-lint-full
|
||||||
|
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run --fix
|
||||||
|
language: system
|
||||||
|
require_serial: true
|
||||||
|
types: [go]
|
||||||
|
pass_filenames: false
|
||||||
|
- id: golangci-lint-config-verify
|
||||||
|
name: golangci-lint-config-verify
|
||||||
|
entry: go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 config verify
|
||||||
|
language: system
|
||||||
|
files: \.golangci\.(?:yml|yaml|toml|json)
|
||||||
|
pass_filenames: false
|
||||||
- id: go-test
|
- id: go-test
|
||||||
name: go test
|
name: go test
|
||||||
entry: go test ./...
|
entry: go test ./...
|
||||||
|
|||||||
+3
-3
@@ -63,19 +63,19 @@ endif
|
|||||||
|
|
||||||
build-all: build dankinstall
|
build-all: build dankinstall
|
||||||
|
|
||||||
install: build
|
install:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-all: build-all
|
install-all:
|
||||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME)
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|
||||||
install-dankinstall: dankinstall
|
install-dankinstall:
|
||||||
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
@echo "Installing $(BINARY_NAME_INSTALL) to $(INSTALL_DIR)..."
|
||||||
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
@install -D -m 755 $(BUILD_DIR)/$(BINARY_NAME_INSTALL) $(INSTALL_DIR)/$(BINARY_NAME_INSTALL)
|
||||||
@echo "Installation complete"
|
@echo "Installation complete"
|
||||||
|
|||||||
+1
-1
@@ -96,7 +96,7 @@ 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:**
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter uninstall",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var authCmd = &cobra.Command{
|
||||||
|
Use: "auth",
|
||||||
|
Short: "Manage DMS authentication sync",
|
||||||
|
Long: "Manage shared PAM/authentication setup for DMS greeter and lock screen",
|
||||||
|
}
|
||||||
|
|
||||||
|
var authSyncCmd = &cobra.Command{
|
||||||
|
Use: "sync",
|
||||||
|
Short: "Sync DMS authentication configuration",
|
||||||
|
Long: "Apply shared PAM/authentication changes for the lock screen and greeter based on current DMS settings",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
yes, _ := cmd.Flags().GetBool("yes")
|
||||||
|
term, _ := cmd.Flags().GetBool("terminal")
|
||||||
|
if term {
|
||||||
|
if err := syncAuthInTerminal(yes); err != nil {
|
||||||
|
log.Fatalf("Error launching auth sync in terminal: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := syncAuth(yes); err != nil {
|
||||||
|
log.Fatalf("Error syncing authentication: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
authSyncCmd.Flags().BoolP("yes", "y", false, "Non-interactive mode: skip prompts")
|
||||||
|
authSyncCmd.Flags().BoolP("terminal", "t", false, "Run auth sync in a new terminal (for entering sudo password)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncAuth(nonInteractive bool) error {
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Println("=== DMS Authentication Sync ===")
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
logFunc := func(msg string) {
|
||||||
|
fmt.Println(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sharedpam.SyncAuthConfig(logFunc, "", sharedpam.SyncAuthOptions{}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nonInteractive {
|
||||||
|
fmt.Println("\n=== Authentication Sync Complete ===")
|
||||||
|
fmt.Println("\nAuthentication changes have been applied.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncAuthInTerminal(nonInteractive bool) error {
|
||||||
|
syncFlags := make([]string, 0, 1)
|
||||||
|
if nonInteractive {
|
||||||
|
syncFlags = append(syncFlags, "--yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
shellSyncCmd := "dms auth sync"
|
||||||
|
if len(syncFlags) > 0 {
|
||||||
|
shellSyncCmd += " " + strings.Join(syncFlags, " ")
|
||||||
|
}
|
||||||
|
shellCmd := shellSyncCmd + `; echo; echo "Authentication sync finished. Closing in 3 seconds..."; sleep 3`
|
||||||
|
return runCommandInTerminal(shellCmd)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var blurCmd = &cobra.Command{
|
||||||
|
Use: "blur",
|
||||||
|
Short: "Background blur utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var blurCheckCmd = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: runBlurCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
blurCmd.AddCommand(blurCheckCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||||
|
supported, err := blur.ProbeSupport()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch supported {
|
||||||
|
case true:
|
||||||
|
fmt.Println("supported")
|
||||||
|
default:
|
||||||
|
fmt.Println("unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,17 +12,21 @@ import (
|
|||||||
_ "image/jpeg"
|
_ "image/jpeg"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -48,6 +52,7 @@ var (
|
|||||||
clipCopyForeground bool
|
clipCopyForeground bool
|
||||||
clipCopyPasteOnce bool
|
clipCopyPasteOnce bool
|
||||||
clipCopyType string
|
clipCopyType string
|
||||||
|
clipCopyDownload bool
|
||||||
clipJSONOutput bool
|
clipJSONOutput bool
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,6 +112,7 @@ var clipClearCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var clipWatchStore bool
|
var clipWatchStore bool
|
||||||
|
var clipWatchMimes bool
|
||||||
|
|
||||||
var clipSearchCmd = &cobra.Command{
|
var clipSearchCmd = &cobra.Command{
|
||||||
Use: "search [query]",
|
Use: "search [query]",
|
||||||
@@ -184,11 +190,12 @@ func init() {
|
|||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
|
clipCopyCmd.Flags().BoolVarP(&clipCopyForeground, "foreground", "f", false, "Stay in foreground instead of forking")
|
||||||
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
clipCopyCmd.Flags().BoolVarP(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
||||||
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
clipCopyCmd.Flags().StringVarP(&clipCopyType, "type", "t", "text/plain;charset=utf-8", "MIME type")
|
||||||
|
clipCopyCmd.Flags().BoolVarP(&clipCopyDownload, "download", "d", false, "Download URL as image and copy as file")
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipWatchCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipHistoryCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
clipGetCmd.Flags().BoolVar(&clipJSONOutput, "json", false, "Output as JSON")
|
||||||
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "c", false, "Copy entry to clipboard")
|
clipGetCmd.Flags().BoolVarP(&clipGetCopy, "copy", "C", false, "Copy entry to clipboard")
|
||||||
|
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
clipSearchCmd.Flags().IntVarP(&clipSearchLimit, "limit", "l", 50, "Max results")
|
||||||
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
clipSearchCmd.Flags().IntVarP(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
||||||
@@ -205,6 +212,7 @@ func init() {
|
|||||||
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking")
|
clipConfigSetCmd.Flags().BoolVar(&clipConfigEnabled, "enable", false, "Enable clipboard tracking")
|
||||||
|
|
||||||
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
clipWatchCmd.Flags().BoolVarP(&clipWatchStore, "store", "s", false, "Store clipboard changes to history (no server required)")
|
||||||
|
clipWatchCmd.Flags().BoolVarP(&clipWatchMimes, "mimes", "m", false, "Show all offered MIME types")
|
||||||
|
|
||||||
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
||||||
|
|
||||||
@@ -214,15 +222,49 @@ func init() {
|
|||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
|
copyFromStdin := false
|
||||||
|
|
||||||
if len(args) > 0 {
|
switch {
|
||||||
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
} else {
|
case clipCopyDownload || clipCopyType == "__multi__":
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("read stdin: %v", err)
|
log.Fatalf("read stdin: %v", err)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
copyFromStdin = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if clipCopyDownload {
|
||||||
|
filePath, err := downloadToTempFile(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("download: %v", err)
|
||||||
|
}
|
||||||
|
if err := copyFileToClipboard(filePath); err != nil {
|
||||||
|
log.Fatalf("copy file: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Downloaded and copied: %s\n", filePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if clipCopyType == "__multi__" {
|
||||||
|
offers, err := parseMultiOffers(data)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("parse multi offers: %v", err)
|
||||||
|
}
|
||||||
|
if err := clipboard.CopyMulti(offers, true, clipCopyPasteOnce); err != nil {
|
||||||
|
log.Fatalf("copy multi: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if copyFromStdin {
|
||||||
|
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
|
log.Fatalf("copy: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
@@ -230,6 +272,40 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseMultiOffers(data []byte) ([]clipboard.Offer, error) {
|
||||||
|
var offers []clipboard.Offer
|
||||||
|
pos := 0
|
||||||
|
|
||||||
|
for pos < len(data) {
|
||||||
|
mimeEnd := bytes.IndexByte(data[pos:], 0)
|
||||||
|
if mimeEnd == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
mimeType := string(data[pos : pos+mimeEnd])
|
||||||
|
pos += mimeEnd + 1
|
||||||
|
|
||||||
|
lenEnd := bytes.IndexByte(data[pos:], 0)
|
||||||
|
if lenEnd == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dataLen, err := strconv.Atoi(string(data[pos : pos+lenEnd]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse length: %w", err)
|
||||||
|
}
|
||||||
|
pos += lenEnd + 1
|
||||||
|
|
||||||
|
if pos+dataLen > len(data) {
|
||||||
|
return nil, fmt.Errorf("data truncated")
|
||||||
|
}
|
||||||
|
offerData := data[pos : pos+dataLen]
|
||||||
|
pos += dataLen
|
||||||
|
|
||||||
|
offers = append(offers, clipboard.Offer{MimeType: mimeType, Data: offerData})
|
||||||
|
}
|
||||||
|
|
||||||
|
return offers, nil
|
||||||
|
}
|
||||||
|
|
||||||
func runClipPaste(cmd *cobra.Command, args []string) {
|
func runClipPaste(cmd *cobra.Command, args []string) {
|
||||||
data, _, err := clipboard.Paste()
|
data, _, err := clipboard.Paste()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -264,6 +340,30 @@ func runClipWatch(cmd *cobra.Command, args []string) {
|
|||||||
}); err != nil && err != context.Canceled {
|
}); err != nil && err != context.Canceled {
|
||||||
log.Fatalf("Watch error: %v", err)
|
log.Fatalf("Watch error: %v", err)
|
||||||
}
|
}
|
||||||
|
case clipWatchMimes:
|
||||||
|
if err := clipboard.WatchAll(ctx, func(data []byte, mimeType string, allMimes []string) {
|
||||||
|
if clipJSONOutput {
|
||||||
|
out := map[string]any{
|
||||||
|
"data": string(data),
|
||||||
|
"mimeType": mimeType,
|
||||||
|
"mimeTypes": allMimes,
|
||||||
|
"timestamp": time.Now().Format(time.RFC3339),
|
||||||
|
"size": len(data),
|
||||||
|
}
|
||||||
|
j, _ := json.Marshal(out)
|
||||||
|
fmt.Println(string(j))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("=== Clipboard Change ===\n")
|
||||||
|
fmt.Printf("Selected: %s\n", mimeType)
|
||||||
|
fmt.Printf("All MIME types:\n")
|
||||||
|
for _, m := range allMimes {
|
||||||
|
fmt.Printf(" - %s\n", m)
|
||||||
|
}
|
||||||
|
fmt.Printf("Size: %d bytes\n\n", len(data))
|
||||||
|
}); err != nil && err != context.Canceled {
|
||||||
|
log.Fatalf("Watch error: %v", err)
|
||||||
|
}
|
||||||
case clipJSONOutput:
|
case clipJSONOutput:
|
||||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
||||||
out := map[string]any{
|
out := map[string]any{
|
||||||
@@ -386,16 +486,13 @@ func runClipGet(cmd *cobra.Command, args []string) {
|
|||||||
req := models.Request{
|
req := models.Request{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Method: "clipboard.copyEntry",
|
Method: "clipboard.copyEntry",
|
||||||
Params: map[string]any{
|
Params: map[string]any{"id": id},
|
||||||
"id": id,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := sendServerRequest(req)
|
resp, err := sendServerRequest(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
log.Fatalf("Error: %s", resp.Error)
|
log.Fatalf("Error: %s", resp.Error)
|
||||||
}
|
}
|
||||||
@@ -672,7 +769,7 @@ func runClipExport(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(args[0], out, 0644); err != nil {
|
if err := os.WriteFile(args[0], out, 0o644); err != nil {
|
||||||
log.Fatalf("Failed to write file: %v", err)
|
log.Fatalf("Failed to write file: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Printf("Exported to %s\n", args[0])
|
fmt.Printf("Exported to %s\n", args[0])
|
||||||
@@ -727,7 +824,7 @@ func runClipMigrate(cmd *cobra.Command, args []string) {
|
|||||||
log.Fatalf("Cliphist db not found: %s", dbPath)
|
log.Fatalf("Cliphist db not found: %s", dbPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{
|
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{
|
||||||
ReadOnly: true,
|
ReadOnly: true,
|
||||||
Timeout: 1 * time.Second,
|
Timeout: 1 * time.Second,
|
||||||
})
|
})
|
||||||
@@ -795,3 +892,113 @@ func detectMimeType(data []byte) string {
|
|||||||
func btoi(v []byte) uint64 {
|
func btoi(v []byte) uint64 {
|
||||||
return binary.BigEndian.Uint64(v)
|
return binary.BigEndian.Uint64(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadToTempFile(rawURL string) (string, error) {
|
||||||
|
if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") {
|
||||||
|
return "", fmt.Errorf("invalid URL: %s", rawURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("parse URL: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := filepath.Ext(parsedURL.Path)
|
||||||
|
if ext == "" {
|
||||||
|
ext = ".png"
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var contentType string
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
for attempt := 0; attempt < 3; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
time.Sleep(time.Duration(attempt) * 500 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", rawURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("create request: %w", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
req.Header.Set("Accept", "image/*,video/*,*/*")
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("download (attempt %d): %w", attempt+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
resp.Body.Close()
|
||||||
|
lastErr = fmt.Errorf("download failed (attempt %d): status %d", attempt+1, resp.StatusCode)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err = io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("read response (attempt %d): %w", attempt+1, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
contentType = resp.Header.Get("Content-Type")
|
||||||
|
if idx := strings.Index(contentType, ";"); idx != -1 {
|
||||||
|
contentType = strings.TrimSpace(contentType[:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
lastErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastErr != nil {
|
||||||
|
return "", lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) == 0 {
|
||||||
|
return "", fmt.Errorf("downloaded empty file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(contentType, "image/") && !strings.HasPrefix(contentType, "video/") {
|
||||||
|
if _, _, err := image.DecodeConfig(bytes.NewReader(data)); err != nil {
|
||||||
|
return "", fmt.Errorf("not a valid media file (content-type: %s)", contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheDir, err := os.UserCacheDir()
|
||||||
|
if err != nil {
|
||||||
|
cacheDir = "/tmp"
|
||||||
|
}
|
||||||
|
clipDir := filepath.Join(cacheDir, "dms", "clipboard")
|
||||||
|
if err := os.MkdirAll(clipDir, 0o755); err != nil {
|
||||||
|
return "", fmt.Errorf("create cache dir: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
|
||||||
|
if err := os.WriteFile(filePath, data, 0o644); err != nil {
|
||||||
|
return "", fmt.Errorf("write file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return filePath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFileToClipboard(filePath string) error {
|
||||||
|
req := models.Request{
|
||||||
|
ID: 1,
|
||||||
|
Method: "clipboard.copyFile",
|
||||||
|
Params: map[string]any{"filePath": filePath},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := sendServerRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("server request: %w", err)
|
||||||
|
}
|
||||||
|
if resp.Error != "" {
|
||||||
|
return fmt.Errorf("server error: %s", resp.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ Output format flags (mutually exclusive, default: --hex):
|
|||||||
--cmyk - CMYK values (C% M% Y% K%)
|
--cmyk - CMYK values (C% M% Y% K%)
|
||||||
--json - JSON with all formats
|
--json - JSON with all formats
|
||||||
|
|
||||||
|
Optional:
|
||||||
|
--raw - Removes ANSI escape codes and background colors. Use this when piping to other commands
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
dms color pick # Pick color, output as hex
|
dms color pick # Pick color, output as hex
|
||||||
dms color pick --rgb # Output as RGB
|
dms color pick --rgb # Output as RGB
|
||||||
@@ -53,6 +56,7 @@ func init() {
|
|||||||
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
colorPickCmd.Flags().Bool("hsv", false, "Output as HSV (H S% V%)")
|
||||||
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
colorPickCmd.Flags().Bool("cmyk", false, "Output as CMYK (C% M% Y% K%)")
|
||||||
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
colorPickCmd.Flags().Bool("json", false, "Output all formats as JSON")
|
||||||
|
colorPickCmd.Flags().Bool("raw", false, "Removes ANSI escape codes and background colors. Use this when piping to other commands")
|
||||||
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
colorPickCmd.Flags().StringVarP(&colorOutputFmt, "output-format", "o", "", "Custom output format template")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
colorPickCmd.Flags().BoolVarP(&colorAutocopy, "autocopy", "a", false, "Copy result to clipboard")
|
||||||
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
colorPickCmd.Flags().BoolVarP(&colorLowercase, "lowercase", "l", false, "Output hex in lowercase")
|
||||||
@@ -113,7 +117,15 @@ func runColorPick(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
if jsonOutput {
|
if jsonOutput {
|
||||||
fmt.Println(output)
|
fmt.Println(output)
|
||||||
} else if color.IsDark() {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if raw, _ := cmd.Flags().GetBool("raw"); raw {
|
||||||
|
fmt.Printf("%s\n", output)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if color.IsDark() {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[97m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
fmt.Printf("\033[48;2;%d;%d;%dm\033[30m %s \033[0m\n", color.R, color.G, color.B, output)
|
||||||
|
|||||||
@@ -64,9 +64,8 @@ var killCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
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)
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -524,5 +523,8 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
chromaCmd,
|
chromaCmd,
|
||||||
doctorCmd,
|
doctorCmd,
|
||||||
configCmd,
|
configCmd,
|
||||||
|
dlCmd,
|
||||||
|
randrCmd,
|
||||||
|
blurCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||||
@@ -101,11 +102,13 @@ var doctorCmd = &cobra.Command{
|
|||||||
var (
|
var (
|
||||||
doctorVerbose bool
|
doctorVerbose bool
|
||||||
doctorJSON bool
|
doctorJSON bool
|
||||||
|
doctorCopy bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
|
doctorCmd.Flags().BoolVarP(&doctorVerbose, "verbose", "v", false, "Show detailed output including paths and versions")
|
||||||
doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format")
|
doctorCmd.Flags().BoolVarP(&doctorJSON, "json", "j", false, "Output results in JSON format")
|
||||||
|
doctorCmd.Flags().BoolVarP(&doctorCopy, "copy", "C", false, "Copy results to clipboard in GitHub-friendly format")
|
||||||
}
|
}
|
||||||
|
|
||||||
type category int
|
type category int
|
||||||
@@ -192,7 +195,7 @@ func (r checkResult) toJSON() checkResultJSON {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runDoctor(cmd *cobra.Command, args []string) {
|
func runDoctor(cmd *cobra.Command, args []string) {
|
||||||
if !doctorJSON {
|
if !doctorJSON && !doctorCopy {
|
||||||
printDoctorHeader()
|
printDoctorHeader()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,9 +213,17 @@ func runDoctor(cmd *cobra.Command, args []string) {
|
|||||||
checkEnvironmentVars(),
|
checkEnvironmentVars(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if doctorJSON {
|
switch {
|
||||||
|
case doctorCopy:
|
||||||
|
text := formatResultsPlain(results)
|
||||||
|
if err := clipboard.CopyOpts([]byte(text), "text/plain;charset=utf-8", false, false); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Failed to copy to clipboard: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Doctor report copied to clipboard")
|
||||||
|
case doctorJSON:
|
||||||
printResultsJSON(results)
|
printResultsJSON(results)
|
||||||
} else {
|
default:
|
||||||
printResults(results)
|
printResults(results)
|
||||||
printSummary(results, qsMissingFeatures)
|
printSummary(results, qsMissingFeatures)
|
||||||
}
|
}
|
||||||
@@ -591,7 +602,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(testScript, []byte(qmlContent), 0644); err != nil {
|
if err := os.WriteFile(testScript, []byte(qmlContent), 0o644); err != nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,6 +649,120 @@ func checkI2CAvailability() checkResult {
|
|||||||
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
return checkResult{catOptionalFeatures, "I2C/DDC", statusOK, fmt.Sprintf("%d monitor(s) detected", len(devices)), "External monitor brightness control", doctorDocsURL + "#optional-features"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkImageFormatPlugins() []checkResult {
|
||||||
|
url := doctorDocsURL + "#optional-features"
|
||||||
|
|
||||||
|
pluginDirs := findQtPluginDirs()
|
||||||
|
if len(pluginDirs) == 0 {
|
||||||
|
return []checkResult{
|
||||||
|
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
||||||
|
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pluginCheck struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
plugins []struct{ file, format string }
|
||||||
|
}
|
||||||
|
|
||||||
|
checks := []pluginCheck{
|
||||||
|
{
|
||||||
|
name: "qt6-imageformats",
|
||||||
|
desc: "WebP, TIFF, GIF, JP2 support",
|
||||||
|
plugins: []struct{ file, format string }{
|
||||||
|
{"libqwebp.so", "WebP"},
|
||||||
|
{"libqtiff.so", "TIFF"},
|
||||||
|
{"libqgif.so", "GIF"},
|
||||||
|
{"libqjp2.so", "JP2"},
|
||||||
|
{"libqicns.so", "ICNS"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "kimageformats",
|
||||||
|
desc: "AVIF, HEIF, JXL support",
|
||||||
|
plugins: []struct{ file, format string }{
|
||||||
|
{"kimg_avif.so", "AVIF"},
|
||||||
|
{"kimg_heif.so", "HEIF"},
|
||||||
|
{"kimg_jxl.so", "JXL"},
|
||||||
|
{"kimg_exr.so", "EXR"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var results []checkResult
|
||||||
|
for _, c := range checks {
|
||||||
|
var found []string
|
||||||
|
var foundDirs []string
|
||||||
|
for _, pluginDir := range pluginDirs {
|
||||||
|
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||||
|
for _, p := range c.plugins {
|
||||||
|
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||||
|
if !slices.Contains(found, p.format) {
|
||||||
|
found = append(found, p.format)
|
||||||
|
}
|
||||||
|
if !slices.Contains(foundDirs, imageFormatsDir) {
|
||||||
|
foundDirs = append(foundDirs, imageFormatsDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var result checkResult
|
||||||
|
switch {
|
||||||
|
case len(found) == 0:
|
||||||
|
result = checkResult{catOptionalFeatures, c.name, statusWarn, "Not installed", c.desc, url}
|
||||||
|
default:
|
||||||
|
details := ""
|
||||||
|
if doctorVerbose {
|
||||||
|
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
|
||||||
|
}
|
||||||
|
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||||
|
}
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
func findQtPluginDirs() []string {
|
||||||
|
var dirs []string
|
||||||
|
|
||||||
|
addDir := func(dir string) {
|
||||||
|
if dir != "" {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||||
|
dirs = append(dirs, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
||||||
|
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||||
|
for dir := range strings.SplitSeq(envPath, ":") {
|
||||||
|
addDir(dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try qtpaths
|
||||||
|
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||||
|
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||||
|
addDir(strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: common distro paths
|
||||||
|
for _, dir := range []string{
|
||||||
|
"/usr/lib/qt6/plugins",
|
||||||
|
"/usr/lib64/qt6/plugins",
|
||||||
|
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
||||||
|
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
||||||
|
} {
|
||||||
|
addDir(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dirs
|
||||||
|
}
|
||||||
|
|
||||||
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||||
switch stackResult.Backend {
|
switch stackResult.Backend {
|
||||||
case network.BackendNetworkManager:
|
case network.BackendNetworkManager:
|
||||||
@@ -678,7 +803,21 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
||||||
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "logind", logindStatus, logindMsg, "Session management", optionalFeaturesURL})
|
||||||
|
|
||||||
|
cupsPkHelperBus := "org.opensuse.CupsPkHelper.Mechanism"
|
||||||
|
var cupsPkStatus status
|
||||||
|
var cupsPkMsg string
|
||||||
|
switch {
|
||||||
|
case utils.IsDBusServiceAvailable(cupsPkHelperBus):
|
||||||
|
cupsPkStatus, cupsPkMsg = statusOK, "Running"
|
||||||
|
case utils.IsDBusServiceActivatable(cupsPkHelperBus):
|
||||||
|
cupsPkStatus, cupsPkMsg = statusOK, "Available"
|
||||||
|
default:
|
||||||
|
cupsPkStatus, cupsPkMsg = statusWarn, "Not available (install cups-pk-helper)"
|
||||||
|
}
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "cups-pk-helper", cupsPkStatus, cupsPkMsg, "Printer management", optionalFeaturesURL})
|
||||||
|
|
||||||
results = append(results, checkI2CAvailability())
|
results = append(results, checkI2CAvailability())
|
||||||
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||||
@@ -752,7 +891,7 @@ func checkConfigurationFiles() []checkResult {
|
|||||||
|
|
||||||
status := statusOK
|
status := statusOK
|
||||||
message := "Present"
|
message := "Present"
|
||||||
if info.Mode().Perm()&0200 == 0 {
|
if info.Mode().Perm()&0o200 == 0 {
|
||||||
status = statusWarn
|
status = statusWarn
|
||||||
message += " (read-only)"
|
message += " (read-only)"
|
||||||
}
|
}
|
||||||
@@ -929,3 +1068,36 @@ func printSummary(results []checkResult, qsMissingFeatures bool) {
|
|||||||
}
|
}
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func formatResultsPlain(results []checkResult) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString("## DMS Doctor Report\n\n")
|
||||||
|
|
||||||
|
currentCategory := category(-1)
|
||||||
|
for _, r := range results {
|
||||||
|
if r.category != currentCategory {
|
||||||
|
if currentCategory != -1 {
|
||||||
|
sb.WriteString("\n")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&sb, "**%s**\n", r.category.String())
|
||||||
|
currentCategory = r.category
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&sb, "- [%s] %s: %s\n", r.status, r.name, r.message)
|
||||||
|
|
||||||
|
if doctorVerbose && r.details != "" {
|
||||||
|
fmt.Fprintf(&sb, " - %s\n", r.details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ds DoctorStatus
|
||||||
|
for _, r := range results {
|
||||||
|
ds.Add(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.WriteString("\n---\n")
|
||||||
|
fmt.Fprintf(&sb, "**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||||
|
ds.ErrorCount(), ds.WarningCount(), ds.OKCount())
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+1291
-162
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
sharedpam "github.com/AvengeMedia/DankMaterialShell/core/internal/pam"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSyncGreeterConfigsAndAuthDelegatesSharedAuth(t *testing.T) {
|
||||||
|
origGreeterConfigSyncFn := greeterConfigSyncFn
|
||||||
|
origSharedAuthSyncFn := sharedAuthSyncFn
|
||||||
|
t.Cleanup(func() {
|
||||||
|
greeterConfigSyncFn = origGreeterConfigSyncFn
|
||||||
|
sharedAuthSyncFn = origSharedAuthSyncFn
|
||||||
|
})
|
||||||
|
|
||||||
|
var calls []string
|
||||||
|
greeterConfigSyncFn = func(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||||
|
if dmsPath != "/tmp/dms" {
|
||||||
|
t.Fatalf("unexpected dmsPath %q", dmsPath)
|
||||||
|
}
|
||||||
|
if compositor != "niri" {
|
||||||
|
t.Fatalf("unexpected compositor %q", compositor)
|
||||||
|
}
|
||||||
|
if sudoPassword != "" {
|
||||||
|
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
||||||
|
}
|
||||||
|
calls = append(calls, "configs")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var gotOptions sharedpam.SyncAuthOptions
|
||||||
|
sharedAuthSyncFn = func(logFunc func(string), sudoPassword string, options sharedpam.SyncAuthOptions) error {
|
||||||
|
if sudoPassword != "" {
|
||||||
|
t.Fatalf("expected empty sudoPassword, got %q", sudoPassword)
|
||||||
|
}
|
||||||
|
gotOptions = options
|
||||||
|
calls = append(calls, "auth")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{
|
||||||
|
ForceGreeterAuth: true,
|
||||||
|
}, func() {
|
||||||
|
calls = append(calls, "before-auth")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("syncGreeterConfigsAndAuth returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wantCalls := []string{"configs", "before-auth", "auth"}
|
||||||
|
if !reflect.DeepEqual(calls, wantCalls) {
|
||||||
|
t.Fatalf("call order = %v, want %v", calls, wantCalls)
|
||||||
|
}
|
||||||
|
if !gotOptions.ForceGreeterAuth {
|
||||||
|
t.Fatalf("expected ForceGreeterAuth to be true, got %+v", gotOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSyncGreeterConfigsAndAuthStopsOnConfigError(t *testing.T) {
|
||||||
|
origGreeterConfigSyncFn := greeterConfigSyncFn
|
||||||
|
origSharedAuthSyncFn := sharedAuthSyncFn
|
||||||
|
t.Cleanup(func() {
|
||||||
|
greeterConfigSyncFn = origGreeterConfigSyncFn
|
||||||
|
sharedAuthSyncFn = origSharedAuthSyncFn
|
||||||
|
})
|
||||||
|
|
||||||
|
greeterConfigSyncFn = func(string, string, func(string), string) error {
|
||||||
|
return errors.New("config sync failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
authCalled := false
|
||||||
|
sharedAuthSyncFn = func(func(string), string, sharedpam.SyncAuthOptions) error {
|
||||||
|
authCalled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := syncGreeterConfigsAndAuth("/tmp/dms", "niri", func(string) {}, sharedpam.SyncAuthOptions{}, nil)
|
||||||
|
if err == nil || err.Error() != "config sync failed" {
|
||||||
|
t.Fatalf("expected config sync error, got %v", err)
|
||||||
|
}
|
||||||
|
if authCalled {
|
||||||
|
t.Fatal("expected auth sync not to run after config sync failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -63,6 +64,7 @@ func init() {
|
|||||||
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)")
|
keybindsSetCmd.Flags().String("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
|
||||||
|
|
||||||
@@ -81,24 +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)
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
|
configDir, _ := os.UserConfigDir()
|
||||||
if err := registry.Register(scrollProvider); err != nil {
|
|
||||||
log.Warnf("Failed to register Scroll 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
swayProvider := providers.NewSwayProvider("$HOME/.config/sway")
|
miracleProvider := providers.NewMiracleProvider("")
|
||||||
if err := registry.Register(swayProvider); err != nil {
|
if err := registry.Register(miracleProvider); err != nil {
|
||||||
log.Warnf("Failed to register Sway provider: %v", err)
|
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("")
|
||||||
@@ -143,6 +156,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
|
|||||||
return providers.NewSwayProvider(path)
|
return providers.NewSwayProvider(path)
|
||||||
case "scroll":
|
case "scroll":
|
||||||
return providers.NewSwayProvider(path)
|
return providers.NewSwayProvider(path)
|
||||||
|
case "miracle":
|
||||||
|
return providers.NewMiracleProvider(path)
|
||||||
case "niri":
|
case "niri":
|
||||||
return providers.NewNiriProvider(path)
|
return providers.NewNiriProvider(path)
|
||||||
default:
|
default:
|
||||||
@@ -212,6 +227,9 @@ 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 != "" {
|
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||||
options["flags"] = v
|
options["flags"] = v
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -55,10 +57,11 @@ func init() {
|
|||||||
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
cmd.Flags().Bool("sync-mode-with-portal", false, "Sync color scheme with GNOME portal")
|
||||||
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
cmd.Flags().Bool("terminals-always-dark", false, "Force terminal themes to dark variant")
|
||||||
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
cmd.Flags().String("skip-templates", "", "Comma-separated list of templates to skip")
|
||||||
|
cmd.Flags().Float64("contrast", 0, "Contrast value from -1 to 1 (0 = standard)")
|
||||||
}
|
}
|
||||||
|
|
||||||
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
matugenQueueCmd.Flags().Bool("wait", true, "Wait for completion")
|
||||||
matugenQueueCmd.Flags().Duration("timeout", 30*time.Second, "Timeout for waiting")
|
matugenQueueCmd.Flags().Duration("timeout", 90*time.Second, "Timeout for waiting")
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||||
@@ -75,6 +78,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
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")
|
skipTemplates, _ := cmd.Flags().GetString("skip-templates")
|
||||||
|
contrast, _ := cmd.Flags().GetFloat64("contrast")
|
||||||
|
|
||||||
return matugen.Options{
|
return matugen.Options{
|
||||||
StateDir: stateDir,
|
StateDir: stateDir,
|
||||||
@@ -85,6 +89,7 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
Mode: matugen.ColorMode(mode),
|
Mode: matugen.ColorMode(mode),
|
||||||
IconTheme: iconTheme,
|
IconTheme: iconTheme,
|
||||||
MatugenType: matugenType,
|
MatugenType: matugenType,
|
||||||
|
Contrast: contrast,
|
||||||
RunUserTemplates: runUserTemplates,
|
RunUserTemplates: runUserTemplates,
|
||||||
StockColors: stockColors,
|
StockColors: stockColors,
|
||||||
SyncModeWithPortal: syncModeWithPortal,
|
SyncModeWithPortal: syncModeWithPortal,
|
||||||
@@ -95,7 +100,11 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,6 +131,7 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
"syncModeWithPortal": opts.SyncModeWithPortal,
|
"syncModeWithPortal": opts.SyncModeWithPortal,
|
||||||
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
"terminalsAlwaysDark": opts.TerminalsAlwaysDark,
|
||||||
"skipTemplates": opts.SkipTemplates,
|
"skipTemplates": opts.SkipTemplates,
|
||||||
|
"contrast": opts.Contrast,
|
||||||
"wait": wait,
|
"wait": wait,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -129,7 +139,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
if !wait {
|
if !wait {
|
||||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||||
log.Info("Server unavailable, running synchronously")
|
log.Info("Server unavailable, running synchronously")
|
||||||
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)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -146,11 +160,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
resp, ok := tryServerRequest(request)
|
resp, ok := tryServerRequest(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Info("Server unavailable, running synchronously")
|
log.Info("Server unavailable, running synchronously")
|
||||||
if err := matugen.Run(opts); err != nil {
|
err := matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
resultCh <- matugen.ErrNoChanges
|
||||||
|
case err != nil:
|
||||||
resultCh <- err
|
resultCh <- err
|
||||||
return
|
default:
|
||||||
|
resultCh <- nil
|
||||||
}
|
}
|
||||||
resultCh <- nil
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
@@ -162,7 +180,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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,16 +13,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
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
|
ssNoConfirm bool
|
||||||
|
ssReset bool
|
||||||
|
ssStdout bool
|
||||||
)
|
)
|
||||||
|
|
||||||
var screenshotCmd = &cobra.Command{
|
var screenshotCmd = &cobra.Command{
|
||||||
@@ -50,9 +52,11 @@ Examples:
|
|||||||
dms screenshot output -o DP-1 # Specific output
|
dms screenshot output -o DP-1 # Specific output
|
||||||
dms screenshot window # Focused window (Hyprland)
|
dms screenshot window # Focused window (Hyprland)
|
||||||
dms screenshot last # Last region (pre-selected)
|
dms screenshot last # Last region (pre-selected)
|
||||||
|
dms screenshot --reset # Reset last region pre-selection
|
||||||
dms screenshot --no-clipboard # Save file only
|
dms screenshot --no-clipboard # Save file only
|
||||||
dms screenshot --no-file # Clipboard only
|
dms screenshot --no-file # Clipboard only
|
||||||
dms screenshot --cursor # Include cursor
|
dms screenshot --no-confirm # Region capture on mouse release
|
||||||
|
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 +115,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")
|
||||||
@@ -119,6 +123,8 @@ func init() {
|
|||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoClipboard, "no-clipboard", false, "Don't copy to clipboard")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoFile, "no-file", false, "Don't save to file")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoNotify, "no-notify", false, "Don't show notification")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssNoConfirm, "no-confirm", false, "Region mode: capture on mouse release without Enter/Space confirmation")
|
||||||
|
screenshotCmd.PersistentFlags().BoolVar(&ssReset, "reset", false, "Reset saved last-region preselection before capturing")
|
||||||
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
screenshotCmd.PersistentFlags().BoolVar(&ssStdout, "stdout", false, "Output image to stdout (for piping to swappy, etc.)")
|
||||||
|
|
||||||
screenshotCmd.AddCommand(ssRegionCmd)
|
screenshotCmd.AddCommand(ssRegionCmd)
|
||||||
@@ -136,10 +142,14 @@ 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
|
||||||
|
config.NoConfirm = ssNoConfirm
|
||||||
|
config.Reset = ssReset
|
||||||
config.Stdout = ssStdout
|
config.Stdout = ssStdout
|
||||||
|
|
||||||
if ssOutputDir != "" {
|
if ssOutputDir != "" {
|
||||||
|
|||||||
@@ -9,14 +9,17 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
var setupCmd = &cobra.Command{
|
var setupCmd = &cobra.Command{
|
||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: requireMutableSystemCommand,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
@@ -24,6 +27,243 @@ 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 ===")
|
||||||
|
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||||
|
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
immutablePolicyOnce sync.Once
|
||||||
|
immutablePolicy immutableCommandPolicy
|
||||||
|
immutablePolicyErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/cli-policy.default.json
|
||||||
|
var defaultCLIPolicyJSON []byte
|
||||||
|
|
||||||
|
type immutableCommandPolicy struct {
|
||||||
|
ImmutableSystem bool
|
||||||
|
ImmutableReason string
|
||||||
|
BlockedCommands []string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliPolicyFile struct {
|
||||||
|
PolicyVersion int `json:"policy_version"`
|
||||||
|
ImmutableSystem *bool `json:"immutable_system"`
|
||||||
|
BlockedCommands *[]string `json:"blocked_commands"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCommandSpec(raw string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||||
|
return strings.Join(strings.Fields(normalized), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBlockedCommands(raw []string) []string {
|
||||||
|
normalized := make([]string, 0, len(raw))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, cmd := range raw {
|
||||||
|
spec := normalizeCommandSpec(cmd)
|
||||||
|
if spec == "" || seen[spec] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[spec] = true
|
||||||
|
normalized = append(normalized, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||||
|
normalizedPath := normalizeCommandSpec(commandPath)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range blocked {
|
||||||
|
spec := normalizeCommandSpec(entry)
|
||||||
|
if spec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(data, &policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||||
|
policyFile, err := loadPolicyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policyFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyFile.ImmutableSystem != nil {
|
||||||
|
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands != nil {
|
||||||
|
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
}
|
||||||
|
if policyFile.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*policyFile.Message)
|
||||||
|
if msg != "" {
|
||||||
|
base.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOSReleaseMap(path string) map[string]string {
|
||||||
|
values := make(map[string]string)
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||||
|
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||||
|
values[key] = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnyToken(text string, tokens ...string) bool {
|
||||||
|
if text == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.Contains(text, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectImmutableSystem() (bool, string) {
|
||||||
|
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||||
|
return true, "/run/ostree-booted is present"
|
||||||
|
}
|
||||||
|
|
||||||
|
osRelease := readOSReleaseMap("/etc/os-release")
|
||||||
|
if len(osRelease) == 0 {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id := osRelease["ID"]
|
||||||
|
idLike := osRelease["ID_LIKE"]
|
||||||
|
variantID := osRelease["VARIANT_ID"]
|
||||||
|
name := osRelease["NAME"]
|
||||||
|
prettyName := osRelease["PRETTY_NAME"]
|
||||||
|
|
||||||
|
immutableIDs := map[string]bool{
|
||||||
|
"bluefin": true,
|
||||||
|
"bazzite": true,
|
||||||
|
"silverblue": true,
|
||||||
|
"kinoite": true,
|
||||||
|
"sericea": true,
|
||||||
|
"onyx": true,
|
||||||
|
"aurora": true,
|
||||||
|
"fedora-iot": true,
|
||||||
|
"fedora-coreos": true,
|
||||||
|
}
|
||||||
|
if immutableIDs[id] {
|
||||||
|
return true, "os-release ID=" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||||
|
if hasAnyToken(variantID, markers...) {
|
||||||
|
return true, "os-release VARIANT_ID=" + variantID
|
||||||
|
}
|
||||||
|
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||||
|
return true, "os-release ID_LIKE=" + idLike
|
||||||
|
}
|
||||||
|
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||||
|
return true, "os-release identifies an atomic/ostree variant"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||||
|
immutablePolicyOnce.Do(func() {
|
||||||
|
detectedImmutable, reason := detectImmutableSystem()
|
||||||
|
immutablePolicy = immutableCommandPolicy{
|
||||||
|
ImmutableSystem: detectedImmutable,
|
||||||
|
ImmutableReason: reason,
|
||||||
|
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
||||||
|
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPolicy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||||
|
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if defaultPolicy.BlockedCommands != nil {
|
||||||
|
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||||
|
}
|
||||||
|
if defaultPolicy.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||||
|
if msg != "" {
|
||||||
|
immutablePolicy.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if immutablePolicyErr != nil {
|
||||||
|
return nil, immutablePolicyErr
|
||||||
|
}
|
||||||
|
return &immutablePolicy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||||
|
policy, err := getImmutablePolicy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !policy.ImmutableSystem {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||||
|
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := ""
|
||||||
|
if policy.ImmutableReason != "" {
|
||||||
|
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
|
}
|
||||||
@@ -16,18 +16,14 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
authCmd.AddCommand(authSyncCmd)
|
||||||
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
// Add subcommands to update
|
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
|
rootCmd.AddCommand(authCmd)
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
|
|||||||
@@ -11,26 +11,22 @@ import (
|
|||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add flags
|
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
authCmd.AddCommand(authSyncCmd)
|
||||||
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
rootCmd.AddCommand(authCmd)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Block root
|
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+59
-5
@@ -192,6 +192,9 @@ func runShellInteractive(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
||||||
|
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
}
|
}
|
||||||
@@ -210,7 +213,7 @@ func runShellInteractive(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||||
}
|
}
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
@@ -432,6 +435,9 @@ func runShellDaemon(session bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ! TODO - remove when QS 0.3 is up and we can use the pragma
|
||||||
|
cmd.Env = append(cmd.Env, "QS_APP_ID=com.danklinux.dms")
|
||||||
|
|
||||||
if isSessionManaged && hasSystemdRun() {
|
if isSessionManaged && hasSystemdRun() {
|
||||||
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
|
||||||
}
|
}
|
||||||
@@ -450,7 +456,7 @@ func runShellDaemon(session bool) {
|
|||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||||
}
|
}
|
||||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
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)
|
||||||
@@ -616,6 +622,43 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFirstDMSPID() (int, bool) {
|
||||||
|
dir := getRuntimeDir()
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if proc.Signal(syscall.Signal(0)) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -627,10 +670,21 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
if qsHasAnyDisplay() {
|
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
switch pid, ok := getFirstDMSPID(); {
|
||||||
|
case ok:
|
||||||
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
|
default:
|
||||||
|
if err := findConfig(nil, nil); err != nil {
|
||||||
|
log.Fatalf("Error finding config: %v", err)
|
||||||
|
}
|
||||||
|
// ! TODO - remove check when QS 0.3 is released
|
||||||
|
if qsHasAnyDisplay() {
|
||||||
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
|
}
|
||||||
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
|
||||||
cmdArgs = append(cmdArgs, args...)
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
@@ -7,14 +7,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
|
||||||
path, err := exec.LookPath(cmd)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
|
||||||
}
|
|
||||||
return path, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", packageName)
|
cmd := exec.Command("pacman", "-Q", packageName)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
|
|||||||
+32
-21
@@ -1,11 +1,13 @@
|
|||||||
module github.com/AvengeMedia/DankMaterialShell/core
|
module github.com/AvengeMedia/DankMaterialShell/core
|
||||||
|
|
||||||
go 1.24.6
|
go 1.26.0
|
||||||
|
|
||||||
|
toolchain go1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||||
github.com/alecthomas/chroma/v2 v2.17.2
|
github.com/alecthomas/chroma/v2 v2.23.1
|
||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/charmbracelet/log v0.4.2
|
github.com/charmbracelet/log v0.4.2
|
||||||
@@ -13,50 +15,55 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.2.2
|
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-20251203232544-981d4ecc17c3
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.5
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0
|
||||||
github.com/yuin/goldmark v1.7.16
|
github.com/yuin/goldmark v1.7.16
|
||||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||||
go.etcd.io/bbolt v1.4.3
|
go.etcd.io/bbolt v1.4.3
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||||
golang.org/x/image v0.35.0
|
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.7.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.2 // 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/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
|
github.com/fogleman/gg v1.3.0 // indirect
|
||||||
github.com/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-20260114122816-19306b749ecc // 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/freetype v0.0.0-20170609003504-e2365dfdc4a0 // 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/pkg/errors v0.9.1 // 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.47.0 // indirect
|
github.com/yeqown/reedsolomon v1.0.0 // indirect
|
||||||
golang.org/x/net v0.49.0 // indirect
|
golang.org/x/crypto v0.48.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.4.1 // 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.4 // 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-20260114124804-a8db3a6585a6
|
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
|
||||||
@@ -70,7 +77,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.40.0
|
golang.org/x/sys v0.41.0
|
||||||
golang.org/x/text v0.33.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
|
||||||
|
|||||||
+56
-46
@@ -7,11 +7,11 @@ github.com/Wifx/gonetworkmanager/v2 v2.2.0/go.mod h1:fMDb//SHsKWxyDUAwXvCqurV3np
|
|||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
|
||||||
github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
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.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||||
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=
|
||||||
@@ -20,32 +20,30 @@ 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.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
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.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
|
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||||
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
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.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
|
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||||
github.com/clipperhouse/displaywidth v0.7.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.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
|
|
||||||
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
|
||||||
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=
|
||||||
@@ -60,23 +58,27 @@ 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=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||||
|
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
github.com/go-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-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
|
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-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
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.2-0.20251229094738-4b14af179146 h1:xYfxAopYyL44ot6dMBIb1Z1njFM0ZBQ99HdIB99KxLs=
|
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.2-0.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
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-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
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-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
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.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||||
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||||
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=
|
||||||
@@ -88,8 +90,8 @@ github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83 h1:B+A58zGFuDrvE
|
|||||||
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=
|
||||||
@@ -103,8 +105,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=
|
||||||
@@ -117,6 +119,8 @@ github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3
|
|||||||
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
|
||||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
@@ -124,8 +128,8 @@ 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-20251203232544-981d4ecc17c3 h1:msKaIZrrNpvofLPDzNBW3152PJBsnPZsoNNosOCS+C0=
|
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
|
||||||
github.com/sblinch/kdl-go v0.0.0-20251203232544-981d4ecc17c3/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=
|
||||||
@@ -144,6 +148,12 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
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=
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.5 h1:HCOe2bSjkhZyYoyyNaXNzh4DJZll6inVJQQw+8228Zk=
|
||||||
|
github.com/yeqown/go-qrcode/v2 v2.2.5/go.mod h1:uHpt9CM0V1HeXLz+Wg5MN50/sI/fQhfkZlOM+cOTHxw=
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0 h1:chdyhEfRtUPgQtuPeaWVGQ/TQx4rE1PqeoW3U+53t34=
|
||||||
|
github.com/yeqown/go-qrcode/writer/standard v1.3.0/go.mod h1:O4MbzsotGCvy8upYPCR91j81dr5XLT7heuljcNXW+oQ=
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0 h1:x1h/Ej/uJnNu8jaX7GLHBWmZKCAWjEJTetkqaabr4B0=
|
||||||
|
github.com/yeqown/reedsolomon v1.0.0/go.mod h1:P76zpcn2TCuL0ul1Fso373qHRc69LKwAw/Iy6g1WiiM=
|
||||||
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
@@ -152,24 +162,24 @@ github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.m
|
|||||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
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 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
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=
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package blur
|
||||||
|
|
||||||
|
import (
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||||
|
|
||||||
|
func ProbeSupport() (bool, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer display.Context().Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case extBackgroundEffectInterface:
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package clipboard
|
package clipboard
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
@@ -12,17 +14,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyOpts(data, mimeType, false, false)
|
return CopyReader(bytes.NewReader(data), mimeType, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
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 {
|
if !foreground {
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServe(data, mimeType, pasteOnce)
|
return copyServeReader(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
args = append(args, "--paste-once")
|
args = append(args, "--paste-once")
|
||||||
@@ -30,30 +52,102 @@ func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
args = append(args, "--type", mimeType)
|
args = append(args, "--type", mimeType)
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
cmd.Env = append(os.Environ(), "DMS_CLIP_FORKED=1")
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
stdout, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
switch src := data.(type) {
|
||||||
return fmt.Errorf("start: %w", err)
|
case *os.File:
|
||||||
|
cmd.Stdin = src
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := stdin.Write(data); err != nil {
|
var buf [1]byte
|
||||||
stdin.Close()
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
}
|
}
|
||||||
stdin.Close()
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
func signalReady() {
|
||||||
|
if os.Getenv("DMS_CLIP_FORKED") == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte{1})
|
||||||
|
}
|
||||||
|
|
||||||
|
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("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -139,12 +233,18 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
|
|
||||||
cancelled := make(chan struct{})
|
cancelled := make(chan struct{})
|
||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
sendErr := make(chan error, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
defer syscall.Close(e.Fd)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
file.Write(data)
|
if err := writeTo(file); err != nil {
|
||||||
|
select {
|
||||||
|
case sendErr <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -160,11 +260,14 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-cancelled:
|
case <-cancelled:
|
||||||
return nil
|
return nil
|
||||||
|
case err := <-sendErr:
|
||||||
|
return err
|
||||||
case <-pasted:
|
case <-pasted:
|
||||||
if pasteOnce {
|
if pasteOnce {
|
||||||
return nil
|
return nil
|
||||||
@@ -330,3 +433,163 @@ func selectPreferredMimeType(mimes []string) string {
|
|||||||
func IsImageMimeType(mime string) bool {
|
func IsImageMimeType(mime string) bool {
|
||||||
return len(mime) > 6 && mime[:6] == "image/"
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
|||||||
return fmt.Errorf("get db path: %w", err)
|
return fmt.Errorf("get db path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
db, err := bolt.Open(dbPath, 0644, &bolt.Options{Timeout: 1 * time.Second})
|
db, err := bolt.Open(dbPath, 0o644, &bolt.Options{Timeout: 1 * time.Second})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open db: %w", err)
|
return fmt.Errorf("open db: %w", err)
|
||||||
}
|
}
|
||||||
@@ -132,7 +132,7 @@ func GetDBPath() (string, error) {
|
|||||||
oldPath := filepath.Join(oldDir, "db")
|
oldPath := filepath.Join(oldDir, "db")
|
||||||
|
|
||||||
if _, err := os.Stat(oldPath); err == nil {
|
if _, err := os.Stat(oldPath); err == nil {
|
||||||
if err := os.MkdirAll(newDir, 0700); err != nil {
|
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
if err := os.Rename(oldPath, newPath); err != nil {
|
if err := os.Rename(oldPath, newPath); err != nil {
|
||||||
@@ -142,7 +142,7 @@ func GetDBPath() (string, error) {
|
|||||||
return newPath, nil
|
return newPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(newDir, 0700); err != nil {
|
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return newPath, nil
|
return newPath, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package clipboard
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,8 +13,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ClipboardChange struct {
|
type ClipboardChange struct {
|
||||||
Data []byte
|
Data []byte
|
||||||
MimeType string
|
MimeType string
|
||||||
|
MimeTypes []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
func Watch(ctx context.Context, callback func(data []byte, mimeType string)) error {
|
||||||
@@ -130,13 +132,154 @@ func Watch(ctx context.Context, callback func(data []byte, mimeType string)) err
|
|||||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||||
return fmt.Errorf("set read deadline: %w", err)
|
return fmt.Errorf("set read deadline: %w", err)
|
||||||
}
|
}
|
||||||
if err := wlCtx.Dispatch(); err != nil && err != os.ErrDeadlineExceeded {
|
if err := wlCtx.Dispatch(); err != nil {
|
||||||
|
if isTimeoutError(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return fmt.Errorf("dispatch: %w", err)
|
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) {
|
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
||||||
ch := make(chan ClipboardChange, 16)
|
ch := make(chan ClipboardChange, 16)
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
|
|||||||
@@ -126,13 +126,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -150,7 +150,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -185,7 +185,7 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -211,6 +211,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.kdl", ""},
|
{"outputs.kdl", ""},
|
||||||
{"cursor.kdl", ""},
|
{"cursor.kdl", ""},
|
||||||
|
{"windowrules.kdl", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
@@ -220,7 +221,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
|||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
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))
|
||||||
@@ -238,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
|
||||||
}
|
}
|
||||||
@@ -254,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
|
||||||
}
|
}
|
||||||
@@ -276,12 +277,12 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
themesDir := filepath.Dir(colorResult.Path)
|
themesDir := filepath.Dir(colorResult.Path)
|
||||||
if err := os.MkdirAll(themesDir, 0755); err != nil {
|
if err := os.MkdirAll(themesDir, 0o755); err != nil {
|
||||||
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
mainResult.Error = fmt.Errorf("failed to create themes directory: %w", err)
|
||||||
return []DeploymentResult{mainResult}, mainResult.Error
|
return []DeploymentResult{mainResult}, mainResult.Error
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(colorResult.Path, []byte(GhosttyColorConfig), 0644); err != nil {
|
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
|
||||||
}
|
}
|
||||||
@@ -302,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
|
||||||
}
|
}
|
||||||
@@ -318,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
|
||||||
}
|
}
|
||||||
@@ -339,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
|
||||||
}
|
}
|
||||||
@@ -353,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
|
||||||
}
|
}
|
||||||
@@ -374,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
|
||||||
}
|
}
|
||||||
@@ -390,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
|
||||||
}
|
}
|
||||||
@@ -411,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
|
||||||
}
|
}
|
||||||
@@ -438,7 +439,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
|||||||
outputsContent.WriteString(output)
|
outputsContent.WriteString(output)
|
||||||
outputsContent.WriteString("\n\n")
|
outputsContent.WriteString("\n\n")
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
|
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))
|
cd.log(fmt.Sprintf("Warning: Failed to migrate outputs to %s: %v", outputsPath, err))
|
||||||
} else {
|
} else {
|
||||||
cd.log("Migrated output sections to dms/outputs.kdl")
|
cd.log("Migrated output sections to dms/outputs.kdl")
|
||||||
@@ -479,13 +480,13 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -503,7 +504,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -538,7 +539,7 @@ func (cd *ConfigDeployer) deployHyprlandConfig(terminal deps.Terminal, useSystem
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
@@ -563,6 +564,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||||
{"outputs.conf", ""},
|
{"outputs.conf", ""},
|
||||||
{"cursor.conf", ""},
|
{"cursor.conf", ""},
|
||||||
|
{"windowrules.conf", ""},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
@@ -572,7 +574,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
|||||||
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(path, []byte(cfg.content), 0644); err != nil {
|
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))
|
||||||
@@ -596,7 +598,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
|||||||
outputsContent.WriteString(monitor)
|
outputsContent.WriteString(monitor)
|
||||||
outputsContent.WriteString("\n")
|
outputsContent.WriteString("\n")
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(outputsPath, []byte(outputsContent.String()), 0644); err != nil {
|
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))
|
cd.log(fmt.Sprintf("Warning: Failed to migrate monitors to %s: %v", outputsPath, err))
|
||||||
} else {
|
} else {
|
||||||
cd.log("Migrated monitor sections to dms/outputs.conf")
|
cd.log("Migrated monitor sections to dms/outputs.conf")
|
||||||
@@ -642,7 +644,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
|
|||||||
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
if strings.HasPrefix(trimmed, "exec-once = systemctl --user start") {
|
||||||
startupSectionFound = true
|
startupSectionFound = true
|
||||||
result = append(result, "exec-once = dms run")
|
result = append(result, "exec-once = dms run")
|
||||||
result = append(result, "env = QT_QPA_PLATFORM,wayland")
|
result = append(result, "env = QT_QPA_PLATFORM,wayland;xcb")
|
||||||
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
result = append(result, "env = ELECTRON_OZONE_PLATFORM_HINT,auto")
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
result = append(result, "env = QT_QPA_PLATFORMTHEME,gtk3")
|
||||||
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
result = append(result, "env = QT_QPA_PLATFORMTHEME_QT6,gtk3")
|
||||||
@@ -657,7 +659,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
|
|||||||
if strings.Contains(line, "STARTUP APPS") {
|
if strings.Contains(line, "STARTUP APPS") {
|
||||||
insertLines := []string{
|
insertLines := []string{
|
||||||
"exec-once = dms run",
|
"exec-once = dms run",
|
||||||
"env = QT_QPA_PLATFORM,wayland",
|
"env = QT_QPA_PLATFORM,wayland;xcb",
|
||||||
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
||||||
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
"env = QT_QPA_PLATFORMTHEME,gtk3",
|
||||||
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
"env = QT_QPA_PLATFORMTHEME_QT6,gtk3",
|
||||||
@@ -675,7 +677,7 @@ func (cd *ConfigDeployer) transformHyprlandConfigForNonSystemd(config, terminalC
|
|||||||
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
func (cd *ConfigDeployer) transformNiriConfigForNonSystemd(config, terminalCommand string) string {
|
||||||
envVars := fmt.Sprintf(`environment {
|
envVars := fmt.Sprintf(`environment {
|
||||||
XDG_CURRENT_DESKTOP "niri"
|
XDG_CURRENT_DESKTOP "niri"
|
||||||
QT_QPA_PLATFORM "wayland"
|
QT_QPA_PLATFORM "wayland;xcb"
|
||||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||||
QT_QPA_PLATFORMTHEME "gtk3"
|
QT_QPA_PLATFORMTHEME "gtk3"
|
||||||
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
QT_QPA_PLATFORMTHEME_QT6 "gtk3"
|
||||||
|
|||||||
@@ -220,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()
|
||||||
@@ -422,9 +422,9 @@ 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, true)
|
result, err := cd.deployHyprlandConfig(deps.TerminalKitty, true)
|
||||||
@@ -600,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()
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ bindl = , XF86AudioPause, exec, dms ipc call mpris playPause
|
|||||||
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
bindl = , XF86AudioPlay, exec, dms ipc call mpris playPause
|
||||||
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
bindl = , XF86AudioPrev, exec, dms ipc call mpris previous
|
||||||
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
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 ===
|
# === Brightness Controls ===
|
||||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||||
@@ -38,6 +40,7 @@ bind = SUPER, F, fullscreen, 1
|
|||||||
bind = SUPER SHIFT, F, fullscreen, 0
|
bind = SUPER SHIFT, F, fullscreen, 0
|
||||||
bind = SUPER SHIFT, T, togglefloating
|
bind = SUPER SHIFT, T, togglefloating
|
||||||
bind = SUPER, W, togglegroup
|
bind = SUPER, W, togglegroup
|
||||||
|
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
||||||
|
|
||||||
# === Focus Navigation ===
|
# === Focus Navigation ===
|
||||||
bind = SUPER, left, movefocus, l
|
bind = SUPER, left, movefocus, l
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ master {
|
|||||||
misc {
|
misc {
|
||||||
disable_hyprland_logo = true
|
disable_hyprland_logo = true
|
||||||
disable_splash_rendering = true
|
disable_splash_rendering = true
|
||||||
vrr = 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ==================
|
# ==================
|
||||||
@@ -99,9 +98,11 @@ windowrule = float on, match:class ^(gnome-calculator)$
|
|||||||
windowrule = float on, match:class ^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||||
windowrule = float on, match:class ^(steam)$
|
|
||||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
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 = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||||
windowrule = float on, match:class ^(zoom)$
|
windowrule = float on, match:class ^(zoom)$
|
||||||
|
|
||||||
@@ -110,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
|
|||||||
# windowrule = float on, match:class ^(org.quickshell)$
|
# windowrule = float on, match:class ^(org.quickshell)$
|
||||||
|
|
||||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||||
|
layerrule = no_anim on, match:namespace ^dms:.*
|
||||||
|
|
||||||
source = ./dms/colors.conf
|
source = ./dms/colors.conf
|
||||||
source = ./dms/outputs.conf
|
source = ./dms/outputs.conf
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ binds {
|
|||||||
XF86AudioNext allow-when-locked=true {
|
XF86AudioNext allow-when-locked=true {
|
||||||
spawn "dms" "ipc" "call" "mpris" "next";
|
spawn "dms" "ipc" "call" "mpris" "next";
|
||||||
}
|
}
|
||||||
|
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
||||||
|
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 {
|
||||||
@@ -76,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; }
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
hotkey-overlay {
|
||||||
|
skip-at-startup
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DMS_RUN_GREETER "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
gestures {
|
||||||
|
hot-corners {
|
||||||
|
off
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout {
|
||||||
|
background-color "#000000"
|
||||||
|
}
|
||||||
@@ -228,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"
|
||||||
@@ -248,6 +252,7 @@ window-rule {
|
|||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
|
match app-id=r#"com.danklinux.dms$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+118
-62
@@ -26,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)
|
||||||
})
|
})
|
||||||
@@ -94,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())
|
||||||
|
|
||||||
@@ -121,12 +125,52 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", pkg)
|
cmd := exec.Command("pacman", "-Q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
||||||
|
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
||||||
|
data, err := os.ReadFile(srcinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
var pkg string
|
||||||
|
var target *[]string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "makedepends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "makedepends = ")
|
||||||
|
target = &makedeps
|
||||||
|
case strings.HasPrefix(line, "depends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "depends = ")
|
||||||
|
target = &deps
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
||||||
|
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
||||||
|
pkg = pkg[:idx]
|
||||||
|
}
|
||||||
|
pkg = strings.TrimSpace(pkg)
|
||||||
|
if pkg != "" {
|
||||||
|
*target = append(*target, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps, makedeps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
||||||
|
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -136,6 +180,7 @@ 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},
|
||||||
@@ -431,29 +476,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
|
||||||
@@ -534,6 +560,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
|
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
||||||
|
if visited[pkg] {
|
||||||
|
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
visited[pkg] = true
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -607,48 +643,8 @@ 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" {
|
|
||||||
// Pre-install dependencies from .SRCINFO
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
|
||||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Installing package dependencies and makedepends",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install dependencies and makedepends explicitly
|
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
|
||||||
|
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
|
||||||
fmt.Sprintf(`
|
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | 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
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, pkg, sudoPassword))
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
|
||||||
fmt.Sprintf(`
|
|
||||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
|
||||||
if [ ! -z "$makedeps" ]; then
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, sudoPassword))
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
@@ -656,6 +652,66 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Classifying dependencies as system or AUR",
|
||||||
|
}
|
||||||
|
|
||||||
|
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
var systemPkgs []string
|
||||||
|
var aurPkgs []string
|
||||||
|
|
||||||
|
for _, dep := range append(runtimeDeps, makeDeps...) {
|
||||||
|
if seen[dep] || a.packageInstalled(dep) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[dep] = true
|
||||||
|
if a.isInSystemRepo(dep) {
|
||||||
|
systemPkgs = append(systemPkgs, dep)
|
||||||
|
} else {
|
||||||
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.32*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
||||||
|
}
|
||||||
|
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aurDep := range aurPkgs {
|
||||||
|
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
||||||
|
}
|
||||||
|
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
||||||
|
startProgress+0.35*(endProgress-startProgress),
|
||||||
|
startProgress+0.39*(endProgress-startProgress),
|
||||||
|
visited,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -668,7 +724,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)
|
||||||
|
|||||||
@@ -102,6 +102,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||||
return b.detectCommand("git", "Version control system")
|
return b.detectCommand("git", "Version control system")
|
||||||
}
|
}
|
||||||
@@ -534,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -555,7 +568,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -594,7 +607,7 @@ func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
targetDir := filepath.Join(homeDir, ".config", "systemd", "user")
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
if err := os.MkdirAll(targetDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
return fmt.Errorf("failed to create systemd user directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +618,7 @@ Requires=graphical-session.target
|
|||||||
After=graphical-session.target
|
After=graphical-session.target
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(targetPath, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(targetPath, []byte(content), 0o644); err != nil {
|
||||||
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
return fmt.Errorf("failed to write hyprland-session.target: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ func TestBaseDistribution_detectDMS_Installed(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)
|
||||||
@@ -55,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()
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(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)
|
||||||
@@ -99,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()
|
||||||
@@ -125,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)
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ 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"
|
||||||
@@ -61,6 +60,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())
|
||||||
|
|
||||||
@@ -86,10 +86,32 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(pkg)
|
||||||
err := cmd.Run()
|
}
|
||||||
return err == nil
|
|
||||||
|
func debianPackageInstalledPrecisely(pkg string) bool {
|
||||||
|
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)) == "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
func debianRepoArchitecture(arch string) string {
|
||||||
|
switch arch {
|
||||||
|
case "amd64", "x86_64":
|
||||||
|
return "amd64"
|
||||||
|
case "arm64", "aarch64":
|
||||||
|
return "arm64"
|
||||||
|
default:
|
||||||
|
return arch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -108,6 +130,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
// 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"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
@@ -188,12 +211,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: "Installing development dependencies...",
|
Step: "Installing development dependencies...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
||||||
"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")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg 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)
|
||||||
}
|
}
|
||||||
@@ -373,6 +396,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-install-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
@@ -430,7 +461,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s, arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -476,20 +507,46 @@ 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{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
groupIndex := 0
|
||||||
Phase: PhaseSystemPackages,
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
Progress: 0.40,
|
if len(groupPackages) == 0 {
|
||||||
Step: "Installing system packages...",
|
return nil
|
||||||
IsComplete: false,
|
}
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
groupIndex++
|
||||||
|
startProgress := 0.40
|
||||||
|
endProgress := 0.60
|
||||||
|
if totalGroups > 1 {
|
||||||
|
if groupIndex == 1 {
|
||||||
|
endProgress = 0.50
|
||||||
|
} else {
|
||||||
|
startProgress = 0.50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := d.aptInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseSystemPackages,
|
||||||
|
Progress: startProgress,
|
||||||
|
Step: "Installing system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
for _, group := range groups {
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
@@ -120,6 +124,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
|
|
||||||
// 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"},
|
||||||
"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"},
|
||||||
@@ -191,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",
|
||||||
@@ -475,28 +484,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.40,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -506,26 +494,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
for _, pkg := range packages {
|
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
if minimal {
|
||||||
break
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := f.dnfInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: groupStart,
|
||||||
|
Step: step,
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.70,
|
|
||||||
Step: "Installing COPR packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const (
|
|||||||
PhaseAURPackages
|
PhaseAURPackages
|
||||||
PhaseCursorTheme
|
PhaseCursorTheme
|
||||||
PhaseConfiguration
|
PhaseConfiguration
|
||||||
|
PhaseGreeterSetup
|
||||||
PhaseComplete
|
PhaseComplete
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
type minimalInstallGroup struct {
|
||||||
|
packages []string
|
||||||
|
minimal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldPreferMinimalInstall(pkg string) bool {
|
||||||
|
switch pkg {
|
||||||
|
case "niri", "niri-git":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if shouldPreferMinimalInstall(pkg) {
|
||||||
|
minimal = append(minimal, pkg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
normal = append(normal, pkg)
|
||||||
|
}
|
||||||
|
return normal, minimal
|
||||||
|
}
|
||||||
|
|
||||||
|
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
||||||
|
normal, minimal := splitMinimalInstallPackages(packages)
|
||||||
|
groups := make([]minimalInstallGroup, 0, 2)
|
||||||
|
if len(minimal) > 0 {
|
||||||
|
groups = append(groups, minimalInstallGroup{
|
||||||
|
packages: minimal,
|
||||||
|
minimal: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(normal) > 0 {
|
||||||
|
groups = append(groups, minimalInstallGroup{
|
||||||
|
packages: normal,
|
||||||
|
minimal: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
@@ -29,6 +30,8 @@ type OpenSUSEDistribution struct {
|
|||||||
config DistroConfig
|
config DistroConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
||||||
|
|
||||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
base := NewBaseDistribution(logChan)
|
base := NewBaseDistribution(logChan)
|
||||||
return &OpenSUSEDistribution{
|
return &OpenSUSEDistribution{
|
||||||
@@ -71,6 +74,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())
|
||||||
|
|
||||||
@@ -100,6 +104,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))
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
// 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"},
|
"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"},
|
||||||
@@ -193,35 +202,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{}
|
||||||
"make",
|
|
||||||
"unzip",
|
|
||||||
"gcc",
|
|
||||||
"gcc-c++",
|
|
||||||
"cmake",
|
|
||||||
"ninja",
|
|
||||||
"pkgconf-pkg-config",
|
|
||||||
"git",
|
|
||||||
"qt6-base-devel",
|
|
||||||
"qt6-declarative-devel",
|
|
||||||
"qt6-declarative-private-devel",
|
|
||||||
"qt6-shadertools",
|
|
||||||
"qt6-shadertools-devel",
|
|
||||||
"qt6-wayland-devel",
|
|
||||||
"qt6-waylandclient-private-devel",
|
|
||||||
"spirv-tools-devel",
|
|
||||||
"cli11-devel",
|
|
||||||
"wayland-protocols-devel",
|
|
||||||
"libgbm-devel",
|
|
||||||
"libdrm-devel",
|
|
||||||
"pipewire-devel",
|
|
||||||
"jemalloc-devel",
|
|
||||||
"wayland-utils",
|
|
||||||
"Mesa-libGLESv3-devel",
|
|
||||||
"pam-devel",
|
|
||||||
"glib2-devel",
|
|
||||||
"polkit-devel",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -291,6 +272,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting prerequisite check...",
|
LogOutput: "Starting prerequisite check...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
@@ -321,7 +306,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +321,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,9 +411,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
||||||
|
|
||||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
||||||
|
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{openSUSENiriWaylandServerPackage}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||||
|
for _, pkg := range extraPkgs {
|
||||||
|
if slices.Contains(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
||||||
|
systemPkgs = append(systemPkgs, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
names := make([]string, len(packages))
|
names := make([]string, len(packages))
|
||||||
for i, pkg := range packages {
|
for i, pkg := range packages {
|
||||||
@@ -508,27 +516,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func isOpenSUSEInstallMediaURI(uri string) bool {
|
||||||
|
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
||||||
|
|
||||||
|
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "hd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "iso:/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseZypperInstallMediaAliases(output string) []string {
|
||||||
|
var aliases []string
|
||||||
|
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || !strings.Contains(line, "|") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(line, "|")
|
||||||
|
if len(parts) < 7 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := parts[1]
|
||||||
|
enabled := strings.ToLower(parts[3])
|
||||||
|
uri := parts[len(parts)-1]
|
||||||
|
|
||||||
|
if alias == "" || strings.EqualFold(alias, "alias") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if enabled != "" && enabled != "yes" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isOpenSUSEInstallMediaURI(uri) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases = append(aliases, alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
||||||
|
output, err := listCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
||||||
|
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases := parseZypperInstallMediaAliases(string(output))
|
||||||
|
if len(aliases) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.055,
|
||||||
|
Step: "Disabling install media repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
||||||
|
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alias := range aliases {
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", escapeSingleQuotes(alias)))
|
||||||
|
repoOutput, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
|
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
||||||
|
}
|
||||||
|
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
||||||
|
args := []string{"zypper", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
groupIndex := 0
|
||||||
Phase: PhaseSystemPackages,
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
Progress: 0.40,
|
if len(groupPackages) == 0 {
|
||||||
Step: "Installing system packages...",
|
return nil
|
||||||
IsComplete: false,
|
}
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := o.zypperInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: groupStart,
|
||||||
|
Step: step,
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
for _, group := range groups {
|
||||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -540,12 +667,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)
|
||||||
@@ -576,7 +703,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectGit())
|
dependencies = append(dependencies, u.detectGit())
|
||||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, u.detectQuickshell())
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, u.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, u.detectXDGPortal())
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
dependencies = append(dependencies, u.detectAccountsService())
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
@@ -94,10 +95,12 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(pkg)
|
||||||
err := cmd.Run()
|
|
||||||
return err == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -116,6 +119,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
// DMS packages from PPAs
|
// DMS packages from PPAs
|
||||||
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
@@ -448,21 +452,7 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
args := []string{"apt-get", "install", "-y"}
|
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.40,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -471,21 +461,59 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
|
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{"apt-get", "install", "-y"}
|
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
args = append(args, packages...)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-install-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
Phase: PhaseAURPackages,
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
Progress: 0.70,
|
totalGroups := len(groups)
|
||||||
Step: "Installing PPA packages...",
|
|
||||||
IsComplete: false,
|
groupIndex := 0
|
||||||
NeedsSudo: true,
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := u.aptInstallArgs(groupPackages, minimal)
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: phase,
|
||||||
|
Progress: groupStart,
|
||||||
|
Step: step,
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
for _, group := range groups {
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package geolocation
|
||||||
|
|
||||||
|
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
|
||||||
|
func NewClient() Client {
|
||||||
|
geoclueClient, err := newGeoClueClient()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("GeoClue2 unavailable: %v", err)
|
||||||
|
return newSeededIpClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
loc, _ := geoclueClient.GetLocation()
|
||||||
|
if loc.Latitude != 0 || loc.Longitude != 0 {
|
||||||
|
log.Info("Using GeoClue2 location")
|
||||||
|
return geoclueClient
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("GeoClue2 has no fix yet, seeding with IP location")
|
||||||
|
ipLoc, err := fetchIPLocation()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("IP location seed failed: %v", err)
|
||||||
|
return geoclueClient
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Seeded GeoClue2 with IP location")
|
||||||
|
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
|
||||||
|
return geoclueClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSeededIpClient() *IpClient {
|
||||||
|
client := newIpClient()
|
||||||
|
ipLoc, err := fetchIPLocation()
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("IP location also failed: %v", err)
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Using IP location")
|
||||||
|
client.currLocation.Latitude = ipLoc.Latitude
|
||||||
|
client.currLocation.Longitude = ipLoc.Longitude
|
||||||
|
return client
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
package geolocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dbusGeoClueService = "org.freedesktop.GeoClue2"
|
||||||
|
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
|
||||||
|
dbusGeoClueInterface = dbusGeoClueService
|
||||||
|
|
||||||
|
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
|
||||||
|
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
|
||||||
|
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
|
||||||
|
|
||||||
|
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
|
||||||
|
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
|
||||||
|
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
|
||||||
|
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
|
||||||
|
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
|
||||||
|
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
|
||||||
|
|
||||||
|
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
|
||||||
|
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
|
||||||
|
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeoClueClient struct {
|
||||||
|
currLocation *Location
|
||||||
|
locationMutex sync.RWMutex
|
||||||
|
|
||||||
|
dbusConn *dbus.Conn
|
||||||
|
clientPath dbus.ObjectPath
|
||||||
|
signals chan *dbus.Signal
|
||||||
|
|
||||||
|
stopChan chan struct{}
|
||||||
|
sigWG sync.WaitGroup
|
||||||
|
|
||||||
|
subscribers syncmap.Map[string, chan Location]
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGeoClueClient() (*GeoClueClient, error) {
|
||||||
|
dbusConn, err := dbus.ConnectSystemBus()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &GeoClueClient{
|
||||||
|
dbusConn: dbusConn,
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
signals: make(chan *dbus.Signal, 256),
|
||||||
|
|
||||||
|
currLocation: &Location{
|
||||||
|
Latitude: 0.0,
|
||||||
|
Longitude: 0.0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.setupClient(); err != nil {
|
||||||
|
dbusConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.startSignalPump(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) Close() {
|
||||||
|
close(c.stopChan)
|
||||||
|
|
||||||
|
c.sigWG.Wait()
|
||||||
|
|
||||||
|
if c.signals != nil {
|
||||||
|
c.dbusConn.RemoveSignal(c.signals)
|
||||||
|
close(c.signals)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||||
|
close(ch)
|
||||||
|
c.subscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if c.dbusConn != nil {
|
||||||
|
c.dbusConn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) Subscribe(id string) chan Location {
|
||||||
|
ch := make(chan Location, 64)
|
||||||
|
c.subscribers.Store(id, ch)
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) Unsubscribe(id string) {
|
||||||
|
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
|
||||||
|
close(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) setupClient() error {
|
||||||
|
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
|
||||||
|
|
||||||
|
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
|
||||||
|
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||||
|
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
|
||||||
|
return fmt.Errorf("failed to set desktop ID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
|
||||||
|
return fmt.Errorf("failed to set time threshold: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) startSignalPump() error {
|
||||||
|
c.dbusConn.Signal(c.signals)
|
||||||
|
|
||||||
|
if err := c.dbusConn.AddMatchSignal(
|
||||||
|
dbus.WithMatchObjectPath(c.clientPath),
|
||||||
|
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
||||||
|
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sigWG.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer c.sigWG.Done()
|
||||||
|
|
||||||
|
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||||
|
clientObj.Call(dbusGeoClueClientTimeStart, 0)
|
||||||
|
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.stopChan:
|
||||||
|
return
|
||||||
|
case sig, ok := <-c.signals:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sig == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handleSignal(sig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
|
||||||
|
switch sig.Name {
|
||||||
|
case dbusGeoClueClientLocationUpdated:
|
||||||
|
if len(sig.Body) != 2 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.handleLocationUpdated(newLocationPath); err != nil {
|
||||||
|
log.Warn("GeoClue: Failed to handle location update: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
|
||||||
|
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
|
||||||
|
|
||||||
|
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.locationMutex.Lock()
|
||||||
|
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
|
||||||
|
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
|
||||||
|
c.locationMutex.Unlock()
|
||||||
|
|
||||||
|
c.notifySubscribers()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) notifySubscribers() {
|
||||||
|
currentLocation, err := c.GetLocation()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||||
|
select {
|
||||||
|
case ch <- currentLocation:
|
||||||
|
default:
|
||||||
|
log.Warn("GeoClue: subscriber channel full, dropping update")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) SeedLocation(loc Location) {
|
||||||
|
c.locationMutex.Lock()
|
||||||
|
defer c.locationMutex.Unlock()
|
||||||
|
c.currLocation.Latitude = loc.Latitude
|
||||||
|
c.currLocation.Longitude = loc.Longitude
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *GeoClueClient) GetLocation() (Location, error) {
|
||||||
|
c.locationMutex.RLock()
|
||||||
|
defer c.locationMutex.RUnlock()
|
||||||
|
if c.currLocation == nil {
|
||||||
|
return Location{
|
||||||
|
Latitude: 0.0,
|
||||||
|
Longitude: 0.0,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
stateCopy := *c.currLocation
|
||||||
|
return stateCopy, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package geolocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IpClient struct {
|
||||||
|
currLocation *Location
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipLocationResult struct {
|
||||||
|
Location
|
||||||
|
City string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipAPIResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
City string `json:"city"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIpClient() *IpClient {
|
||||||
|
return &IpClient{
|
||||||
|
currLocation: &Location{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IpClient) Subscribe(id string) chan Location {
|
||||||
|
ch := make(chan Location, 1)
|
||||||
|
if location, err := c.GetLocation(); err == nil {
|
||||||
|
ch <- location
|
||||||
|
}
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *IpClient) Unsubscribe(id string) {}
|
||||||
|
|
||||||
|
func (c *IpClient) Close() {}
|
||||||
|
|
||||||
|
func (c *IpClient) GetLocation() (Location, error) {
|
||||||
|
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
|
||||||
|
return *c.currLocation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := fetchIPLocation()
|
||||||
|
if err != nil {
|
||||||
|
return Location{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.currLocation.Latitude = result.Latitude
|
||||||
|
c.currLocation.Longitude = result.Longitude
|
||||||
|
return *c.currLocation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchIPLocation() (ipLocationResult, error) {
|
||||||
|
client := &http.Client{Timeout: 10 * time.Second}
|
||||||
|
|
||||||
|
resp, err := client.Get("http://ip-api.com/json/")
|
||||||
|
if err != nil {
|
||||||
|
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data ipAPIResponse
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
|
||||||
|
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipLocationResult{
|
||||||
|
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
|
||||||
|
City: data.City,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package geolocation
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
GetLocation() (Location, error)
|
||||||
|
|
||||||
|
Subscribe(id string) chan Location
|
||||||
|
Unsubscribe(id string)
|
||||||
|
|
||||||
|
Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# AppArmor profile for dms-greeter
|
||||||
|
#
|
||||||
|
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
||||||
|
# Manual edits will be overwritten on next sync.
|
||||||
|
#
|
||||||
|
# Mode: complain (denials are logged, nothing is blocked)
|
||||||
|
# To switch to enforce after validating with `aa-logprof`:
|
||||||
|
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
||||||
|
#
|
||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
#include <abstractions/bash>
|
||||||
|
|
||||||
|
# The launcher script itself
|
||||||
|
/usr/bin/dms-greeter r,
|
||||||
|
|
||||||
|
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
||||||
|
/var/cache/dms-greeter/ rw,
|
||||||
|
/var/cache/dms-greeter/** rwlk,
|
||||||
|
|
||||||
|
# DMS config — packaged path
|
||||||
|
/usr/share/quickshell/dms-greeter/ r,
|
||||||
|
/usr/share/quickshell/dms-greeter/** r,
|
||||||
|
/usr/share/quickshell/ r,
|
||||||
|
/usr/share/quickshell/** r,
|
||||||
|
|
||||||
|
# DMS config — system and user overrides
|
||||||
|
/etc/dms/ r,
|
||||||
|
/etc/dms/** r,
|
||||||
|
/usr/share/dms/ r,
|
||||||
|
/usr/share/dms/** r,
|
||||||
|
/home/*/.config/quickshell/ r,
|
||||||
|
/home/*/.config/quickshell/** r,
|
||||||
|
/root/.config/quickshell/ r,
|
||||||
|
/root/.config/quickshell/** r,
|
||||||
|
|
||||||
|
# greetd / PAM — read-only for session setup
|
||||||
|
/etc/greetd/ r,
|
||||||
|
/etc/greetd/** r,
|
||||||
|
/etc/pam.d/ r,
|
||||||
|
/etc/pam.d/** r,
|
||||||
|
/usr/lib/pam.d/ r,
|
||||||
|
/usr/lib/pam.d/** r,
|
||||||
|
|
||||||
|
# Compositor binaries — run unconfined so each compositor uses its own profile
|
||||||
|
/usr/bin/niri Ux,
|
||||||
|
/usr/bin/hyprland Ux,
|
||||||
|
/usr/bin/Hyprland Ux,
|
||||||
|
/usr/bin/sway Ux,
|
||||||
|
/usr/bin/labwc Ux,
|
||||||
|
/usr/bin/scroll Ux,
|
||||||
|
/usr/bin/miracle-wm Ux,
|
||||||
|
/usr/bin/mango Ux,
|
||||||
|
|
||||||
|
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
||||||
|
/usr/bin/qs Ux,
|
||||||
|
/usr/bin/quickshell Ux,
|
||||||
|
|
||||||
|
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
||||||
|
/run/user/[0-9]*/ rw,
|
||||||
|
/run/user/[0-9]*/** rw,
|
||||||
|
|
||||||
|
# DRM / GPU devices (required for Wayland compositor startup)
|
||||||
|
/dev/dri/ r,
|
||||||
|
/dev/dri/* rw,
|
||||||
|
/dev/udmabuf rw,
|
||||||
|
|
||||||
|
# Input devices
|
||||||
|
/dev/input/ r,
|
||||||
|
/dev/input/* r,
|
||||||
|
|
||||||
|
# Systemd journal / logging
|
||||||
|
/run/systemd/journal/socket rw,
|
||||||
|
/dev/log rw,
|
||||||
|
|
||||||
|
# Shell helper binaries invoked by the launcher script
|
||||||
|
/usr/bin/env ix,
|
||||||
|
/usr/bin/mkdir ix,
|
||||||
|
/usr/bin/cat ix,
|
||||||
|
/usr/bin/grep ix,
|
||||||
|
/usr/bin/dirname ix,
|
||||||
|
/usr/bin/basename ix,
|
||||||
|
/usr/bin/command ix,
|
||||||
|
/bin/env ix,
|
||||||
|
/bin/mkdir ix,
|
||||||
|
|
||||||
|
# Signal management (compositor lifecycle)
|
||||||
|
signal (send, receive) set=("term", "int", "hup", "kill"),
|
||||||
|
}
|
||||||
+1692
-105
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,98 @@
|
|||||||
|
package greeter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTestFile(t *testing.T, path string, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
settingsJSON string
|
||||||
|
sessionJSON string
|
||||||
|
wantSourcePath string
|
||||||
|
wantResolvedWallpaper string
|
||||||
|
wantDynamicOverrideUsed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": "Pictures/blue.jpg",
|
||||||
|
"matugenScheme": "scheme-tonal-spot",
|
||||||
|
"iconTheme": "Papirus"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":true}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
||||||
|
wantDynamicOverrideUsed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic theme without override uses desktop colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": ""
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "purple",
|
||||||
|
"greeterWallpaperPath": "/tmp/blue.jpg"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "/tmp/blue.jpg",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
writeTestFile(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||||
|
writeTestFile(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||||
|
|
||||||
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
||||||
|
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
||||||
|
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
||||||
|
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
||||||
|
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
||||||
|
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -79,16 +79,16 @@ func TestFindJSONFiles(t *testing.T) {
|
|||||||
txtFile := filepath.Join(tmpDir, "readme.txt")
|
txtFile := filepath.Join(tmpDir, "readme.txt")
|
||||||
subdir := filepath.Join(tmpDir, "subdir")
|
subdir := filepath.Join(tmpDir, "subdir")
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file1: %v", err)
|
t.Fatalf("Failed to create file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file2: %v", err)
|
t.Fatalf("Failed to create file2: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(txtFile, []byte("text"), 0644); err != nil {
|
if err := os.WriteFile(txtFile, []byte("text"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create txt file: %v", err)
|
t.Fatalf("Failed to create txt file: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(subdir, 0755); err != nil {
|
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,10 +143,10 @@ func TestFindJSONFilesMultiplePaths(t *testing.T) {
|
|||||||
file1 := filepath.Join(tmpDir1, "app1.json")
|
file1 := filepath.Join(tmpDir1, "app1.json")
|
||||||
file2 := filepath.Join(tmpDir2, "app2.json")
|
file2 := filepath.Join(tmpDir2, "app2.json")
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file1: %v", err)
|
t.Fatalf("Failed to create file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create file2: %v", err)
|
t.Fatalf("Failed to create file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@ func TestAutoDiscoverProviders(t *testing.T) {
|
|||||||
}`
|
}`
|
||||||
|
|
||||||
file := filepath.Join(tmpDir, "testapp.json")
|
file := filepath.Join(tmpDir, "testapp.json")
|
||||||
if err := os.WriteFile(file, []byte(jsonContent), 0644); err != nil {
|
if err := os.WriteFile(file, []byte(jsonContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ func TestAutoDiscoverProvidersNoFactory(t *testing.T) {
|
|||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
file := filepath.Join(tmpDir, "test.json")
|
file := filepath.Join(tmpDir, "test.json")
|
||||||
if err := os.WriteFile(file, []byte("{}"), 0644); err != nil {
|
if err := os.WriteFile(file, []byte("{}"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to create test file: %v", err)
|
t.Fatalf("Failed to create test file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,7 +216,7 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
|
|||||||
|
|
||||||
overridePath := h.GetOverridePath()
|
overridePath := h.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +398,7 @@ func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
|||||||
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
||||||
overridePath := h.GetOverridePath()
|
overridePath := h.GetOverridePath()
|
||||||
content := h.generateBindsContent(binds)
|
content := h.generateBindsContent(binds)
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
|
func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string {
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ bind = SUPER, right, movefocus, r
|
|||||||
bind = SUPER, T, exec, kitty # Terminal
|
bind = SUPER, T, exec, kitty # Terminal
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +245,7 @@ bind = SUPER, B, exec, app2
|
|||||||
#/# = SUPER, C, exec, app3 # Custom comment
|
#/# = SUPER, C, exec, app3 # Custom comment
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,10 +278,10 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
|||||||
content1 := "bind = SUPER, Q, killactive\n"
|
content1 := "bind = SUPER, Q, killactive\n"
|
||||||
content2 := "bind = SUPER, T, exec, kitty\n"
|
content2 := "bind = SUPER, T, exec, kitty\n"
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte(content1), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file1: %v", err)
|
t.Fatalf("Failed to write file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "test.conf")
|
configFile := filepath.Join(tmpSubdir, "test.conf")
|
||||||
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ bind = SUPER, Q, killactive
|
|||||||
bind = SUPER, T, exec, kitty
|
bind = SUPER, T, exec, kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ bind = SUPER, T, exec, kitty # Terminal
|
|||||||
bind = SUPER, 1, workspace, 1
|
bind = SUPER, 1, workspace, 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ func TestFormatKey(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ func TestDescriptionFallback(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := os.WriteFile(configFile, []byte(tt.content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
|
|||||||
|
|
||||||
overridePath := m.GetOverridePath()
|
overridePath := m.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,7 +360,7 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
|||||||
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
||||||
overridePath := m.GetOverridePath()
|
overridePath := m.GetOverridePath()
|
||||||
content := m.generateBindsContent(binds)
|
content := m.generateBindsContent(binds)
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
||||||
|
|||||||
@@ -238,7 +238,7 @@ bind=Ctrl,1,view,1,0
|
|||||||
bind=Ctrl,2,view,2,0
|
bind=Ctrl,2,view,2,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,10 +276,10 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
|||||||
content1 := "bind=ALT,q,killclient,\n"
|
content1 := "bind=ALT,q,killclient,\n"
|
||||||
content2 := "bind=Alt,t,spawn,kitty\n"
|
content2 := "bind=Alt,t,spawn,kitty\n"
|
||||||
|
|
||||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
if err := os.WriteFile(file1, []byte(content1), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file1: %v", err)
|
t.Fatalf("Failed to write file1: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
if err := os.WriteFile(file2, []byte(content2), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write file2: %v", err)
|
t.Fatalf("Failed to write file2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,7 +300,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
|
|||||||
|
|
||||||
content := "bind=ALT,q,killclient,\n"
|
content := "bind=ALT,q,killclient,\n"
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write config: %v", err)
|
t.Fatalf("Failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,13 +347,13 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "config.conf")
|
configFile := filepath.Join(tmpSubdir, "config.conf")
|
||||||
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +384,7 @@ bind=ALT,q,killclient,
|
|||||||
bind=Alt,t,spawn,kitty
|
bind=Alt,t,spawn,kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -458,7 +458,7 @@ bind=Ctrl,2,view,2,0
|
|||||||
bind=Ctrl,3,view,3,0
|
bind=Ctrl,3,view,3,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ bind=SUPER,n,switch_layout
|
|||||||
bind=ALT+SHIFT,X,incgaps,1
|
bind=ALT+SHIFT,X,incgaps,1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,7 +285,7 @@ bind=ALT,Left,focusdir,left
|
|||||||
bind=Ctrl,1,view,1,0
|
bind=Ctrl,1,view,1,0
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiracleProvider struct {
|
||||||
|
configPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMiracleProvider(configPath string) *MiracleProvider {
|
||||||
|
if configPath == "" {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err == nil {
|
||||||
|
configPath = filepath.Join(configDir, "miracle-wm")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &MiracleProvider{configPath: configPath}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) Name() string {
|
||||||
|
return "miracle"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||||
|
config, err := ParseMiracleConfig(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings := MiracleConfigToBindings(config)
|
||||||
|
categorizedBinds := make(map[string][]keybinds.Keybind)
|
||||||
|
|
||||||
|
for _, kb := range bindings {
|
||||||
|
category := m.categorizeAction(kb.Action)
|
||||||
|
bind := keybinds.Keybind{
|
||||||
|
Key: m.formatKey(kb),
|
||||||
|
Description: kb.Comment,
|
||||||
|
Action: kb.Action,
|
||||||
|
}
|
||||||
|
categorizedBinds[category] = append(categorizedBinds[category], bind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &keybinds.CheatSheet{
|
||||||
|
Title: "Miracle WM Keybinds",
|
||||||
|
Provider: m.Name(),
|
||||||
|
Binds: categorizedBinds,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) GetOverridePath() string {
|
||||||
|
expanded, err := utils.ExpandPath(m.configPath)
|
||||||
|
if err != nil {
|
||||||
|
return filepath.Join(m.configPath, "config.yaml")
|
||||||
|
}
|
||||||
|
return filepath.Join(expanded, "config.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string {
|
||||||
|
parts := make([]string, 0, len(kb.Mods)+1)
|
||||||
|
parts = append(parts, kb.Mods...)
|
||||||
|
parts = append(parts, kb.Key)
|
||||||
|
return strings.Join(parts, "+")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MiracleProvider) categorizeAction(action string) string {
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"):
|
||||||
|
return "Workspace"
|
||||||
|
case strings.Contains(action, "select_") || strings.Contains(action, "move_"):
|
||||||
|
return "Window"
|
||||||
|
case action == "toggle_resize" || strings.HasPrefix(action, "resize_"):
|
||||||
|
return "Window"
|
||||||
|
case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace":
|
||||||
|
return "Window"
|
||||||
|
case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal":
|
||||||
|
return "Layout"
|
||||||
|
case action == "quit_compositor":
|
||||||
|
return "System"
|
||||||
|
case action == "terminal":
|
||||||
|
return "Execute"
|
||||||
|
case strings.HasPrefix(action, "magnifier_"):
|
||||||
|
return "Accessibility"
|
||||||
|
case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"):
|
||||||
|
return "Execute"
|
||||||
|
default:
|
||||||
|
return "Execute"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
package providers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MiracleConfig struct {
|
||||||
|
Terminal string `yaml:"terminal"`
|
||||||
|
ActionKey string `yaml:"action_key"`
|
||||||
|
DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"`
|
||||||
|
CustomActions []MiracleCustomAction `yaml:"custom_actions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiracleActionOverride struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Action string `yaml:"action"`
|
||||||
|
Modifiers []string `yaml:"modifiers"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiracleCustomAction struct {
|
||||||
|
Command string `yaml:"command"`
|
||||||
|
Action string `yaml:"action"`
|
||||||
|
Modifiers []string `yaml:"modifiers"`
|
||||||
|
Key string `yaml:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MiracleKeyBinding struct {
|
||||||
|
Mods []string
|
||||||
|
Key string
|
||||||
|
Action string
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
var miracleDefaultBinds = []MiracleKeyBinding{
|
||||||
|
{Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"},
|
||||||
|
{Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"},
|
||||||
|
{Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"},
|
||||||
|
{Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"},
|
||||||
|
{Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"},
|
||||||
|
{Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"},
|
||||||
|
{Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"},
|
||||||
|
{Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"},
|
||||||
|
{Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"},
|
||||||
|
{Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"},
|
||||||
|
{Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"},
|
||||||
|
{Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"},
|
||||||
|
{Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"},
|
||||||
|
{Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"},
|
||||||
|
{Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"},
|
||||||
|
{Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"},
|
||||||
|
{Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"},
|
||||||
|
{Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"},
|
||||||
|
{Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseMiracleConfig(configPath string) (*MiracleConfig, error) {
|
||||||
|
expanded, err := utils.ExpandPath(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := os.Stat(expanded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var configFile string
|
||||||
|
if info.IsDir() {
|
||||||
|
configFile = filepath.Join(expanded, "config.yaml")
|
||||||
|
} else {
|
||||||
|
configFile = expanded
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var config MiracleConfig
|
||||||
|
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.ActionKey == "" {
|
||||||
|
config.ActionKey = "meta"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveMiracleModifier(mod, actionKey string) string {
|
||||||
|
switch mod {
|
||||||
|
case "primary":
|
||||||
|
return resolveActionKey(actionKey)
|
||||||
|
case "alt", "alt_left", "alt_right":
|
||||||
|
return "Alt"
|
||||||
|
case "shift", "shift_left", "shift_right":
|
||||||
|
return "Shift"
|
||||||
|
case "ctrl", "ctrl_left", "ctrl_right":
|
||||||
|
return "Ctrl"
|
||||||
|
case "meta", "meta_left", "meta_right":
|
||||||
|
return "Super"
|
||||||
|
default:
|
||||||
|
return mod
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveActionKey(actionKey string) string {
|
||||||
|
switch actionKey {
|
||||||
|
case "meta":
|
||||||
|
return "Super"
|
||||||
|
case "alt":
|
||||||
|
return "Alt"
|
||||||
|
case "ctrl":
|
||||||
|
return "Ctrl"
|
||||||
|
default:
|
||||||
|
return "Super"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func miracleKeyCodeToName(keyCode string) string {
|
||||||
|
name := strings.TrimPrefix(keyCode, "KEY_")
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
|
||||||
|
switch name {
|
||||||
|
case "enter":
|
||||||
|
return "Return"
|
||||||
|
case "space":
|
||||||
|
return "Space"
|
||||||
|
case "up":
|
||||||
|
return "Up"
|
||||||
|
case "down":
|
||||||
|
return "Down"
|
||||||
|
case "left":
|
||||||
|
return "Left"
|
||||||
|
case "right":
|
||||||
|
return "Right"
|
||||||
|
case "tab":
|
||||||
|
return "Tab"
|
||||||
|
case "escape", "esc":
|
||||||
|
return "Escape"
|
||||||
|
case "delete":
|
||||||
|
return "Delete"
|
||||||
|
case "backspace":
|
||||||
|
return "BackSpace"
|
||||||
|
case "home":
|
||||||
|
return "Home"
|
||||||
|
case "end":
|
||||||
|
return "End"
|
||||||
|
case "pageup":
|
||||||
|
return "Page_Up"
|
||||||
|
case "pagedown":
|
||||||
|
return "Page_Down"
|
||||||
|
case "print":
|
||||||
|
return "Print"
|
||||||
|
case "pause":
|
||||||
|
return "Pause"
|
||||||
|
case "volumeup":
|
||||||
|
return "XF86AudioRaiseVolume"
|
||||||
|
case "volumedown":
|
||||||
|
return "XF86AudioLowerVolume"
|
||||||
|
case "mute":
|
||||||
|
return "XF86AudioMute"
|
||||||
|
case "micmute":
|
||||||
|
return "XF86AudioMicMute"
|
||||||
|
case "brightnessup":
|
||||||
|
return "XF86MonBrightnessUp"
|
||||||
|
case "brightnessdown":
|
||||||
|
return "XF86MonBrightnessDown"
|
||||||
|
case "kbdillumup":
|
||||||
|
return "XF86KbdBrightnessUp"
|
||||||
|
case "kbdillumdown":
|
||||||
|
return "XF86KbdBrightnessDown"
|
||||||
|
case "comma":
|
||||||
|
return "comma"
|
||||||
|
case "minus":
|
||||||
|
return "minus"
|
||||||
|
case "equal":
|
||||||
|
return "equal"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(name) == 1 {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding {
|
||||||
|
overridden := make(map[string]bool)
|
||||||
|
var bindings []MiracleKeyBinding
|
||||||
|
|
||||||
|
for _, override := range config.DefaultActionOverrides {
|
||||||
|
mods := make([]string, 0, len(override.Modifiers))
|
||||||
|
for _, mod := range override.Modifiers {
|
||||||
|
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings = append(bindings, MiracleKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: miracleKeyCodeToName(override.Key),
|
||||||
|
Action: override.Name,
|
||||||
|
Comment: miracleActionDescription(override.Name),
|
||||||
|
})
|
||||||
|
overridden[override.Name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, def := range miracleDefaultBinds {
|
||||||
|
if overridden[def.Action] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bindings = append(bindings, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, custom := range config.CustomActions {
|
||||||
|
mods := make([]string, 0, len(custom.Modifiers))
|
||||||
|
for _, mod := range custom.Modifiers {
|
||||||
|
mods = append(mods, resolveMiracleModifier(mod, config.ActionKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
bindings = append(bindings, MiracleKeyBinding{
|
||||||
|
Mods: mods,
|
||||||
|
Key: miracleKeyCodeToName(custom.Key),
|
||||||
|
Action: custom.Command,
|
||||||
|
Comment: custom.Command,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return bindings
|
||||||
|
}
|
||||||
|
|
||||||
|
func miracleActionDescription(action string) string {
|
||||||
|
switch action {
|
||||||
|
case "terminal":
|
||||||
|
return "Open terminal"
|
||||||
|
case "request_vertical":
|
||||||
|
return "Layout windows vertically"
|
||||||
|
case "request_horizontal":
|
||||||
|
return "Layout windows horizontally"
|
||||||
|
case "select_up":
|
||||||
|
return "Select window above"
|
||||||
|
case "select_down":
|
||||||
|
return "Select window below"
|
||||||
|
case "select_left":
|
||||||
|
return "Select window left"
|
||||||
|
case "select_right":
|
||||||
|
return "Select window right"
|
||||||
|
case "move_up":
|
||||||
|
return "Move window up"
|
||||||
|
case "move_down":
|
||||||
|
return "Move window down"
|
||||||
|
case "move_left":
|
||||||
|
return "Move window left"
|
||||||
|
case "move_right":
|
||||||
|
return "Move window right"
|
||||||
|
case "toggle_resize":
|
||||||
|
return "Toggle resize mode"
|
||||||
|
case "fullscreen":
|
||||||
|
return "Toggle fullscreen"
|
||||||
|
case "quit_active_window":
|
||||||
|
return "Close window"
|
||||||
|
case "quit_compositor":
|
||||||
|
return "Exit compositor"
|
||||||
|
case "toggle_floating":
|
||||||
|
return "Toggle floating"
|
||||||
|
case "toggle_pinned_to_workspace":
|
||||||
|
return "Toggle pinned to workspace"
|
||||||
|
case "toggle_tabbing":
|
||||||
|
return "Toggle tabbing layout"
|
||||||
|
case "toggle_stacking":
|
||||||
|
return "Toggle stacking layout"
|
||||||
|
case "magnifier_on":
|
||||||
|
return "Enable magnifier"
|
||||||
|
case "magnifier_off":
|
||||||
|
return "Disable magnifier"
|
||||||
|
case "magnifier_increase_size":
|
||||||
|
return "Increase magnifier area"
|
||||||
|
case "magnifier_decrease_size":
|
||||||
|
return "Decrease magnifier area"
|
||||||
|
case "magnifier_increase_scale":
|
||||||
|
return "Increase magnifier scale"
|
||||||
|
case "magnifier_decrease_scale":
|
||||||
|
return "Decrease magnifier scale"
|
||||||
|
}
|
||||||
|
|
||||||
|
if num, ok := strings.CutPrefix(action, "select_workspace_"); ok {
|
||||||
|
return "Workspace " + num
|
||||||
|
}
|
||||||
|
if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok {
|
||||||
|
return "Move to workspace " + num
|
||||||
|
}
|
||||||
|
|
||||||
|
return action
|
||||||
|
}
|
||||||
@@ -118,6 +118,9 @@ func (n *NiriProvider) categorizeByAction(action string) string {
|
|||||||
return "Overview"
|
return "Overview"
|
||||||
case action == "quit" ||
|
case action == "quit" ||
|
||||||
action == "power-off-monitors" ||
|
action == "power-off-monitors" ||
|
||||||
|
action == "power-on-monitors" ||
|
||||||
|
action == "suspend" ||
|
||||||
|
action == "do-screen-transition" ||
|
||||||
action == "toggle-keyboard-shortcuts-inhibit" ||
|
action == "toggle-keyboard-shortcuts-inhibit" ||
|
||||||
strings.Contains(action, "dpms"):
|
strings.Contains(action, "dpms"):
|
||||||
return "System"
|
return "System"
|
||||||
@@ -151,13 +154,16 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
|||||||
}
|
}
|
||||||
|
|
||||||
bind := keybinds.Keybind{
|
bind := keybinds.Keybind{
|
||||||
Key: keyStr,
|
Key: keyStr,
|
||||||
Description: kb.Description,
|
Description: kb.Description,
|
||||||
Action: rawAction,
|
Action: rawAction,
|
||||||
Subcategory: subcategory,
|
Subcategory: subcategory,
|
||||||
Source: source,
|
Source: source,
|
||||||
HideOnOverlay: kb.HideOnOverlay,
|
HideOnOverlay: kb.HideOnOverlay,
|
||||||
CooldownMs: kb.CooldownMs,
|
CooldownMs: kb.CooldownMs,
|
||||||
|
AllowWhenLocked: kb.AllowWhenLocked,
|
||||||
|
AllowInhibiting: kb.AllowInhibiting,
|
||||||
|
Repeat: kb.Repeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == "dms" && conflicts != nil {
|
if source == "dms" && conflicts != nil {
|
||||||
@@ -235,7 +241,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
|
|||||||
|
|
||||||
overridePath := n.GetOverridePath()
|
overridePath := n.GetOverridePath()
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(overridePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create dms directory: %w", err)
|
return fmt.Errorf("failed to create dms directory: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,20 +341,18 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
|||||||
val := arg.ValueString()
|
val := arg.ValueString()
|
||||||
if val == "" {
|
if val == "" {
|
||||||
parts = append(parts, `""`)
|
parts = append(parts, `""`)
|
||||||
|
} else if strings.ContainsAny(val, " \t") {
|
||||||
|
parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`)
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, val)
|
parts = append(parts, val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if actionNode.Properties != nil {
|
if actionNode.Properties != nil {
|
||||||
if val, ok := actionNode.Properties.Get("focus"); ok {
|
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
|
||||||
parts = append(parts, "focus="+val.String())
|
if val, ok := actionNode.Properties.Get(propName); ok {
|
||||||
}
|
parts = append(parts, propName+"="+val.String())
|
||||||
if val, ok := actionNode.Properties.Get("show-pointer"); ok {
|
}
|
||||||
parts = append(parts, "show-pointer="+val.String())
|
|
||||||
}
|
|
||||||
if val, ok := actionNode.Properties.Get("write-to-disk"); ok {
|
|
||||||
parts = append(parts, "write-to-disk="+val.String())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -372,6 +376,9 @@ func (n *NiriProvider) extractOptions(node *document.Node) map[string]any {
|
|||||||
if val, ok := node.Properties.Get("allow-when-locked"); ok {
|
if val, ok := node.Properties.Get("allow-when-locked"); ok {
|
||||||
opts["allow-when-locked"] = val.String() == "true"
|
opts["allow-when-locked"] = val.String() == "true"
|
||||||
}
|
}
|
||||||
|
if val, ok := node.Properties.Get("allow-inhibiting"); ok {
|
||||||
|
opts["allow-inhibiting"] = val.String() == "true"
|
||||||
|
}
|
||||||
return opts
|
return opts
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,6 +412,9 @@ func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node {
|
|||||||
if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
|
if v, ok := bind.Options["allow-when-locked"]; ok && v == true {
|
||||||
node.AddProperty("allow-when-locked", true, "")
|
node.AddProperty("allow-when-locked", true, "")
|
||||||
}
|
}
|
||||||
|
if v, ok := bind.Options["allow-inhibiting"]; ok && v == false {
|
||||||
|
node.AddProperty("allow-inhibiting", false, "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bind.Description != "" {
|
if bind.Description != "" {
|
||||||
@@ -485,7 +495,7 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NiriProvider) getBindSortPriority(action string) int {
|
func (n *NiriProvider) getBindSortPriority(action string) int {
|
||||||
|
|||||||
@@ -12,14 +12,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type NiriKeyBinding struct {
|
type NiriKeyBinding struct {
|
||||||
Mods []string
|
Mods []string
|
||||||
Key string
|
Key string
|
||||||
Action string
|
Action string
|
||||||
Args []string
|
Args []string
|
||||||
Description string
|
Description string
|
||||||
HideOnOverlay bool
|
HideOnOverlay bool
|
||||||
CooldownMs int
|
CooldownMs int
|
||||||
Source string
|
AllowWhenLocked bool
|
||||||
|
AllowInhibiting *bool
|
||||||
|
Repeat *bool
|
||||||
|
Source string
|
||||||
}
|
}
|
||||||
|
|
||||||
type NiriSection struct {
|
type NiriSection struct {
|
||||||
@@ -269,8 +272,10 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
|
|||||||
args = append(args, arg.ValueString())
|
args = append(args, arg.ValueString())
|
||||||
}
|
}
|
||||||
if actionNode.Properties != nil {
|
if actionNode.Properties != nil {
|
||||||
if val, ok := actionNode.Properties.Get("focus"); ok {
|
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
|
||||||
args = append(args, "focus="+val.String())
|
if val, ok := actionNode.Properties.Get(propName); ok {
|
||||||
|
args = append(args, propName+"="+val.String())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,6 +283,9 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
|
|||||||
var description string
|
var description string
|
||||||
var hideOnOverlay bool
|
var hideOnOverlay bool
|
||||||
var cooldownMs int
|
var cooldownMs int
|
||||||
|
var allowWhenLocked bool
|
||||||
|
var allowInhibiting *bool
|
||||||
|
var repeat *bool
|
||||||
if node.Properties != nil {
|
if node.Properties != nil {
|
||||||
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
||||||
switch val.ValueString() {
|
switch val.ValueString() {
|
||||||
@@ -290,17 +298,31 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
|
|||||||
if val, ok := node.Properties.Get("cooldown-ms"); ok {
|
if val, ok := node.Properties.Get("cooldown-ms"); ok {
|
||||||
cooldownMs, _ = strconv.Atoi(val.String())
|
cooldownMs, _ = strconv.Atoi(val.String())
|
||||||
}
|
}
|
||||||
|
if val, ok := node.Properties.Get("allow-when-locked"); ok {
|
||||||
|
allowWhenLocked = val.String() == "true"
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("allow-inhibiting"); ok {
|
||||||
|
v := val.String() == "true"
|
||||||
|
allowInhibiting = &v
|
||||||
|
}
|
||||||
|
if val, ok := node.Properties.Get("repeat"); ok {
|
||||||
|
v := val.String() == "true"
|
||||||
|
repeat = &v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &NiriKeyBinding{
|
return &NiriKeyBinding{
|
||||||
Mods: mods,
|
Mods: mods,
|
||||||
Key: key,
|
Key: key,
|
||||||
Action: action,
|
Action: action,
|
||||||
Args: args,
|
Args: args,
|
||||||
Description: description,
|
Description: description,
|
||||||
HideOnOverlay: hideOnOverlay,
|
HideOnOverlay: hideOnOverlay,
|
||||||
CooldownMs: cooldownMs,
|
CooldownMs: cooldownMs,
|
||||||
Source: p.currentSource,
|
AllowWhenLocked: allowWhenLocked,
|
||||||
|
AllowInhibiting: allowInhibiting,
|
||||||
|
Repeat: repeat,
|
||||||
|
Source: p.currentSource,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func TestNiriParseBasicBinds(t *testing.T) {
|
|||||||
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +148,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
|||||||
func TestNiriParseInclude(t *testing.T) {
|
func TestNiriParseInclude(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "dms")
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,10 +165,10 @@ include "dms/binds.kdl"
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ include "dms/binds.kdl"
|
|||||||
func TestNiriParseIncludeOverride(t *testing.T) {
|
func TestNiriParseIncludeOverride(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "dms")
|
subDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,10 +202,10 @@ include "dms/binds.kdl"
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,10 +246,10 @@ include "other.kdl"
|
|||||||
include "config.kdl"
|
include "config.kdl"
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(otherConfig, []byte(otherContent), 0644); err != nil {
|
if err := os.WriteFile(otherConfig, []byte(otherContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write other config: %v", err)
|
t.Fatalf("Failed to write other config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +272,7 @@ func TestNiriParseMissingInclude(t *testing.T) {
|
|||||||
}
|
}
|
||||||
include "nonexistent/file.kdl"
|
include "nonexistent/file.kdl"
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ input {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
Mod+T hotkey-overlay-title="Third" { spawn "third"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
|||||||
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
subDir := filepath.Join(tmpDir, "custom")
|
subDir := filepath.Join(tmpDir, "custom")
|
||||||
if err := os.MkdirAll(subDir, 0755); err != nil {
|
if err := os.MkdirAll(subDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create subdir: %v", err)
|
t.Fatalf("Failed to create subdir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,10 +409,10 @@ binds {
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(mainConfig, []byte(mainContent), 0644); err != nil {
|
if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write main config: %v", err)
|
t.Fatalf("Failed to write main config: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(includeConfig, []byte(includeContent), 0644); err != nil {
|
if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write include config: %v", err)
|
t.Fatalf("Failed to write include config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,7 +471,7 @@ func TestNiriParseMultipleArgs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,7 +508,7 @@ func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
|
|||||||
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,7 +550,7 @@ func TestNiriParseQuotedStringArgs(t *testing.T) {
|
|||||||
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +586,7 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
|||||||
Alt+Tab { next-window scope="output"; }
|
Alt+Tab { next-window scope="output"; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
|
|||||||
Mod+Shift+E { quit; }
|
Mod+Shift+E { quit; }
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +312,7 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write temp file: %v", err)
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,12 +351,12 @@ func TestNiriEmptyArgsPreservation(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
dmsDir := filepath.Join(tmpDir, "dms")
|
dmsDir := filepath.Join(tmpDir, "dms")
|
||||||
if err := os.MkdirAll(dmsDir, 0755); err != nil {
|
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||||
t.Fatalf("Failed to create dms directory: %v", err)
|
t.Fatalf("Failed to create dms directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
bindsFile := filepath.Join(dmsDir, "binds.kdl")
|
||||||
if err := os.WriteFile(bindsFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(bindsFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write binds file: %v", err)
|
t.Fatalf("Failed to write binds file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,7 +428,7 @@ recent-windows {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +621,7 @@ func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
|||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
configFile := filepath.Join(tmpDir, "config.kdl")
|
configFile := filepath.Join(tmpDir, "config.kdl")
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write temp file: %v", err)
|
t.Fatalf("Failed to write temp file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
@@ -18,14 +19,21 @@ func NewSwayProvider(configPath string) *SwayProvider {
|
|||||||
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
|
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
|
||||||
|
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
|
configDir, err := os.UserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
configDir = ""
|
||||||
|
}
|
||||||
if scrollEnvSet {
|
if scrollEnvSet {
|
||||||
configPath = "$HOME/.config/scroll"
|
if configDir != "" {
|
||||||
|
configPath = filepath.Join(configDir, "scroll")
|
||||||
|
}
|
||||||
isScroll = true
|
isScroll = true
|
||||||
} else {
|
} else {
|
||||||
configPath = "$HOME/.config/sway"
|
if configDir != "" {
|
||||||
|
configPath = filepath.Join(configDir, "sway")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Determine isScroll based on the provided config path
|
|
||||||
isScroll = strings.Contains(configPath, "scroll")
|
isScroll = strings.Contains(configPath, "scroll")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,16 +44,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SwayProvider) Name() string {
|
func (s *SwayProvider) Name() string {
|
||||||
if s != nil && s.isScroll {
|
|
||||||
return "scroll"
|
|
||||||
}
|
|
||||||
if s == nil {
|
if s == nil {
|
||||||
_, ok := os.LookupEnv("SCROLLSOCK")
|
if os.Getenv("SCROLLSOCK") != "" {
|
||||||
if ok {
|
|
||||||
return "scroll"
|
return "scroll"
|
||||||
}
|
}
|
||||||
|
return "sway"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.isScroll {
|
||||||
|
return "scroll"
|
||||||
|
}
|
||||||
return "sway"
|
return "sway"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ bindsym $mod+t exec $term
|
|||||||
bindsym $mod+d exec $menu
|
bindsym $mod+d exec $menu
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ bindsym $mod+Right focus right
|
|||||||
bindsym $mod+t exec kitty # Terminal
|
bindsym $mod+t exec kitty # Terminal
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,13 +328,13 @@ func TestSwayReadContentWithTildeExpansion(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
||||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
if err := os.MkdirAll(tmpSubdir, 0o755); err != nil {
|
||||||
t.Skip("Cannot create test directory in home")
|
t.Skip("Cannot create test directory in home")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpSubdir)
|
defer os.RemoveAll(tmpSubdir)
|
||||||
|
|
||||||
configFile := filepath.Join(tmpSubdir, "config")
|
configFile := filepath.Join(tmpSubdir, "config")
|
||||||
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ bindsym Mod4+q kill
|
|||||||
bindsym Mod4+t exec kitty
|
bindsym Mod4+t exec kitty
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +404,7 @@ bindsym $mod+2 workspace number 2
|
|||||||
bindsym $mod+Shift+1 move container to workspace number 1
|
bindsym $mod+Shift+1 move container to workspace number 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ func TestSwayProviderName(t *testing.T) {
|
|||||||
|
|
||||||
func TestSwayProviderDefaultPath(t *testing.T) {
|
func TestSwayProviderDefaultPath(t *testing.T) {
|
||||||
provider := NewSwayProvider("")
|
provider := NewSwayProvider("")
|
||||||
if provider.configPath != "$HOME/.config/sway" {
|
configDir, err := os.UserConfigDir()
|
||||||
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
|
if err != nil {
|
||||||
|
t.Skip("UserConfigDir not available")
|
||||||
|
}
|
||||||
|
expected := filepath.Join(configDir, "sway")
|
||||||
|
if provider.configPath != expected {
|
||||||
|
t.Errorf("configPath = %q, want %q", provider.configPath, expected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +195,7 @@ bindsym $mod+s layout stacking
|
|||||||
bindsym $mod+w layout tabbed
|
bindsym $mod+w layout tabbed
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +258,7 @@ bindsym $mod+f fullscreen toggle
|
|||||||
bindsym $mod+1 workspace number 1
|
bindsym $mod+1 workspace number 1
|
||||||
`
|
`
|
||||||
|
|
||||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil {
|
||||||
t.Fatalf("Failed to write test config: %v", err)
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
package keybinds
|
package keybinds
|
||||||
|
|
||||||
type Keybind struct {
|
type Keybind struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Description string `json:"desc"`
|
Description string `json:"desc"`
|
||||||
Action string `json:"action,omitempty"`
|
Action string `json:"action,omitempty"`
|
||||||
Subcategory string `json:"subcat,omitempty"`
|
Subcategory string `json:"subcat,omitempty"`
|
||||||
Source string `json:"source,omitempty"`
|
Source string `json:"source,omitempty"`
|
||||||
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
||||||
CooldownMs int `json:"cooldownMs,omitempty"`
|
CooldownMs int `json:"cooldownMs,omitempty"`
|
||||||
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
|
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
|
||||||
Conflict *Keybind `json:"conflict,omitempty"`
|
AllowWhenLocked bool `json:"allowWhenLocked,omitempty"`
|
||||||
|
AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled
|
||||||
|
Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled
|
||||||
|
Conflict *Keybind `json:"conflict,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DMSBindsStatus struct {
|
type DMSBindsStatus struct {
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package matugen
|
package matugen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -10,12 +13,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||||
"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/utils"
|
||||||
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNoChanges = errors.New("no color changes")
|
||||||
|
|
||||||
type ColorMode string
|
type ColorMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -30,6 +37,7 @@ const (
|
|||||||
TemplateKindTerminal
|
TemplateKindTerminal
|
||||||
TemplateKindGTK
|
TemplateKindGTK
|
||||||
TemplateKindVSCode
|
TemplateKindVSCode
|
||||||
|
TemplateKindEmacs
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplateDef struct {
|
type TemplateDef struct {
|
||||||
@@ -50,7 +58,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
||||||
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
||||||
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
||||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
||||||
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
||||||
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
||||||
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
||||||
@@ -62,7 +70,8 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
|
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
@@ -75,8 +84,10 @@ func (c *ColorMode) GTKTheme() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
matugenVersionOnce sync.Once
|
matugenVersionMu sync.Mutex
|
||||||
|
matugenVersionOK bool
|
||||||
matugenSupportsCOE bool
|
matugenSupportsCOE bool
|
||||||
|
matugenIsV4 bool
|
||||||
)
|
)
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@@ -88,7 +99,9 @@ type Options struct {
|
|||||||
Mode ColorMode
|
Mode ColorMode
|
||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
|
Contrast float64
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -148,14 +161,20 @@ func Run(opts Options) error {
|
|||||||
opts.AppChecker = utils.DefaultAppChecker{}
|
opts.AppChecker = utils.DefaultAppChecker{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(opts.StateDir, 0755); err != nil {
|
if err := os.MkdirAll(opts.StateDir, 0o755); err != nil {
|
||||||
return fmt.Errorf("failed to create state dir: %w", err)
|
return fmt.Errorf("failed to create state dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||||
|
|
||||||
if err := buildOnce(&opts); err != nil {
|
changed, buildErr := buildOnce(&opts)
|
||||||
return err
|
if buildErr != nil {
|
||||||
|
return buildErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
log.Info("No color changes detected, skipping refresh")
|
||||||
|
return ErrNoChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.SyncModeWithPortal {
|
if opts.SyncModeWithPortal {
|
||||||
@@ -166,25 +185,27 @@ func Run(opts Options) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildOnce(opts *Options) error {
|
func buildOnce(opts *Options) (bool, error) {
|
||||||
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp config: %w", err)
|
return false, fmt.Errorf("failed to create temp config: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(cfgFile.Name())
|
defer os.Remove(cfgFile.Name())
|
||||||
defer cfgFile.Close()
|
defer cfgFile.Close()
|
||||||
|
|
||||||
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
return false, fmt.Errorf("failed to create temp dir: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||||
return fmt.Errorf("failed to build config: %w", err)
|
return false, fmt.Errorf("failed to build config: %w", err)
|
||||||
}
|
}
|
||||||
cfgFile.Close()
|
cfgFile.Close()
|
||||||
|
|
||||||
|
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||||
|
|
||||||
var primaryDark, primaryLight, surface string
|
var primaryDark, primaryLight, surface string
|
||||||
var dank16JSON string
|
var dank16JSON string
|
||||||
var importArgs []string
|
var importArgs []string
|
||||||
@@ -196,7 +217,7 @@ func buildOnce(opts *Options) error {
|
|||||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||||
|
|
||||||
if primaryDark == "" {
|
if primaryDark == "" {
|
||||||
return fmt.Errorf("failed to extract primary dark from stock colors")
|
return false, fmt.Errorf("failed to extract primary dark from stock colors")
|
||||||
}
|
}
|
||||||
if primaryLight == "" {
|
if primaryLight == "" {
|
||||||
primaryLight = primaryDark
|
primaryLight = primaryDark
|
||||||
@@ -208,16 +229,17 @@ func buildOnce(opts *Options) error {
|
|||||||
|
|
||||||
log.Info("Running matugen color hex with stock color overrides")
|
log.Info("Running matugen color hex with stock color overrides")
|
||||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
|
args = appendContrastArg(args, opts.Contrast)
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||||
|
|
||||||
matJSON, err := runMatugenDryRun(opts)
|
matJSON, err := runMatugenDryRun(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("matugen dry-run failed: %w", err)
|
return false, fmt.Errorf("matugen dry-run failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||||
@@ -225,7 +247,7 @@ func buildOnce(opts *Options) error {
|
|||||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||||
|
|
||||||
if primaryDark == "" {
|
if primaryDark == "" {
|
||||||
return fmt.Errorf("failed to extract primary color")
|
return false, fmt.Errorf("failed to extract primary color")
|
||||||
}
|
}
|
||||||
if primaryLight == "" {
|
if primaryLight == "" {
|
||||||
primaryLight = primaryDark
|
primaryLight = primaryDark
|
||||||
@@ -244,16 +266,47 @@ func buildOnce(opts *Options) error {
|
|||||||
args = []string{opts.Kind, opts.Value}
|
args = []string{opts.Kind, opts.Value}
|
||||||
}
|
}
|
||||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
|
args = appendContrastArg(args, opts.Contrast)
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshGTK(opts.ConfigDir, opts.Mode)
|
newColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||||
signalTerminals()
|
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
if opts.ColorsOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
|
switch opts.Mode {
|
||||||
|
case ColorModeLight:
|
||||||
|
syncAccentColor(primaryLight)
|
||||||
|
default:
|
||||||
|
syncAccentColor(primaryDark)
|
||||||
|
}
|
||||||
|
refreshGTK(opts.Mode)
|
||||||
|
refreshGTK4()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !opts.ShouldSkipTemplate("qt6ct") && appExists(opts.AppChecker, []string{"qt6ct"}, nil) {
|
||||||
|
refreshQt6ct()
|
||||||
|
}
|
||||||
|
|
||||||
|
signalTerminals(opts)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendContrastArg(args []string, contrast float64) []string {
|
||||||
|
if contrast == 0 {
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
return append(args, "--contrast", strconv.FormatFloat(contrast, 'f', -1, 64))
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
@@ -293,6 +346,10 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -316,6 +373,10 @@ output_path = '%s'
|
|||||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "windsurf", filepath.Join(homeDir, ".windsurf/extensions"), opts.ShellDir)
|
||||||
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
||||||
|
case TemplateKindEmacs:
|
||||||
|
if utils.EmacsConfigDir() != "" {
|
||||||
|
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||||
}
|
}
|
||||||
@@ -414,7 +475,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
|||||||
|
|
||||||
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
||||||
tmpPath := filepath.Join(tmpDir, templateName)
|
tmpPath := filepath.Join(tmpDir, templateName)
|
||||||
if err := os.WriteFile(tmpPath, []byte(modified), 0644); err != nil {
|
if err := os.WriteFile(tmpPath, []byte(modified), 0o644); err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +534,9 @@ func substituteVars(content, shellDir string) string {
|
|||||||
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
||||||
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
||||||
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
||||||
|
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
|
||||||
|
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
|
||||||
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,67 +557,161 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
|
|||||||
return content[startIdx : startIdx+endIdx]
|
return content[startIdx : startIdx+endIdx]
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMatugenVersion() {
|
type matugenFlags struct {
|
||||||
matugenVersionOnce.Do(func() {
|
supportsCOE bool
|
||||||
cmd := exec.Command("matugen", "--version")
|
isV4 bool
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
versionStr := strings.TrimSpace(string(output))
|
|
||||||
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
|
||||||
|
|
||||||
parts := strings.Split(versionStr, ".")
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
major, err := strconv.Atoi(parts[0])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
minor, err := strconv.Atoi(parts[1])
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
|
||||||
if matugenSupportsCOE {
|
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatugen(args []string) error {
|
func detectMatugenVersion() (matugenFlags, error) {
|
||||||
checkMatugenVersion()
|
matugenVersionMu.Lock()
|
||||||
|
defer matugenVersionMu.Unlock()
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenVersionOK {
|
||||||
args = append([]string{"--continue-on-error"}, args...)
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return detectMatugenVersionLocked()
|
||||||
|
}
|
||||||
|
|
||||||
|
func redetectMatugenVersion(old matugenFlags) (matugenFlags, bool) {
|
||||||
|
matugenVersionMu.Lock()
|
||||||
|
defer matugenVersionMu.Unlock()
|
||||||
|
|
||||||
|
matugenVersionOK = false
|
||||||
|
flags, err := detectMatugenVersionLocked()
|
||||||
|
if err != nil {
|
||||||
|
return old, false
|
||||||
|
}
|
||||||
|
changed := flags.supportsCOE != old.supportsCOE || flags.isV4 != old.isV4
|
||||||
|
return flags, changed
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectMatugenVersionLocked() (matugenFlags, error) {
|
||||||
|
cmd := exec.Command("matugen", "--version")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return matugenFlags{}, fmt.Errorf("failed to get matugen version: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
versionStr := strings.TrimSpace(string(output))
|
||||||
|
versionStr = strings.TrimPrefix(versionStr, "matugen ")
|
||||||
|
|
||||||
|
parts := strings.Split(versionStr, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return matugenFlags{}, fmt.Errorf("unexpected matugen version format: %q", versionStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return matugenFlags{}, fmt.Errorf("failed to parse matugen major version %q: %w", parts[0], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return matugenFlags{}, fmt.Errorf("failed to parse matugen minor version %q: %w", parts[1], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matugenSupportsCOE = major > 3 || (major == 3 && minor >= 1)
|
||||||
|
matugenIsV4 = major >= 4
|
||||||
|
matugenVersionOK = true
|
||||||
|
|
||||||
|
if matugenSupportsCOE {
|
||||||
|
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||||
|
}
|
||||||
|
if matugenIsV4 {
|
||||||
|
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||||
|
}
|
||||||
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMatugenArgs(baseArgs []string, flags matugenFlags) []string {
|
||||||
|
args := make([]string, 0, len(baseArgs)+4)
|
||||||
|
if flags.supportsCOE {
|
||||||
|
args = append(args, "--continue-on-error")
|
||||||
|
}
|
||||||
|
args = append(args, baseArgs...)
|
||||||
|
if flags.isV4 {
|
||||||
|
args = append(args, "--source-color-index", "0")
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMatugen(baseArgs []string) error {
|
||||||
|
flags, err := detectMatugenVersion()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
args := buildMatugenArgs(baseArgs, flags)
|
||||||
cmd := exec.Command("matugen", args...)
|
cmd := exec.Command("matugen", args...)
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
runErr := cmd.Run()
|
||||||
|
if runErr == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen failed (v4=%v): %v", flags.isV4, runErr)
|
||||||
|
|
||||||
|
newFlags, changed := redetectMatugenVersion(flags)
|
||||||
|
if !changed {
|
||||||
|
return runErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen version changed (v4: %v -> %v), retrying", flags.isV4, newFlags.isV4)
|
||||||
|
args = buildMatugenArgs(baseArgs, newFlags)
|
||||||
|
retryCmd := exec.Command("matugen", args...)
|
||||||
|
retryCmd.Stdout = os.Stdout
|
||||||
|
retryCmd.Stderr = os.Stderr
|
||||||
|
return retryCmd.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMatugenDryRun(opts *Options) (string, error) {
|
func runMatugenDryRun(opts *Options) (string, error) {
|
||||||
var args []string
|
flags, err := detectMatugenVersion()
|
||||||
switch opts.Kind {
|
|
||||||
case "hex":
|
|
||||||
args = []string{"color", "hex", opts.Value}
|
|
||||||
default:
|
|
||||||
args = []string{opts.Kind, opts.Value}
|
|
||||||
}
|
|
||||||
args = append(args, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
|
||||||
|
|
||||||
cmd := exec.Command("matugen", args...)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output, dryErr := execDryRun(opts, flags)
|
||||||
|
if dryErr == nil {
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen dry-run failed (v4=%v): %v", flags.isV4, dryErr)
|
||||||
|
|
||||||
|
newFlags, changed := redetectMatugenVersion(flags)
|
||||||
|
if !changed {
|
||||||
|
return "", dryErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warnf("Matugen version changed (v4: %v -> %v), retrying dry-run", flags.isV4, newFlags.isV4)
|
||||||
|
return execDryRun(opts, newFlags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func execDryRun(opts *Options, flags matugenFlags) (string, error) {
|
||||||
|
var baseArgs []string
|
||||||
|
switch opts.Kind {
|
||||||
|
case "hex":
|
||||||
|
baseArgs = []string{"color", "hex", opts.Value}
|
||||||
|
default:
|
||||||
|
baseArgs = []string{opts.Kind, opts.Value}
|
||||||
|
}
|
||||||
|
baseArgs = append(baseArgs, "-m", "dark", "-t", opts.MatugenType, "--json", "hex", "--dry-run")
|
||||||
|
baseArgs = appendContrastArg(baseArgs, opts.Contrast)
|
||||||
|
if flags.isV4 {
|
||||||
|
baseArgs = append(baseArgs, "--source-color-index", "0", "--old-json-output")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("matugen", baseArgs...)
|
||||||
|
var stderr strings.Builder
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
if stderr.Len() > 0 {
|
||||||
|
return "", fmt.Errorf("matugen %v failed (v4=%v): %s", baseArgs, flags.isV4, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("matugen %v failed (v4=%v): %w", baseArgs, flags.isV4, err)
|
||||||
|
}
|
||||||
return strings.ReplaceAll(string(output), "\n", ""), nil
|
return strings.ReplaceAll(string(output), "\n", ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -617,40 +775,73 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
|
|||||||
return dank16.GenerateVariantJSON(variantColors)
|
return dank16.GenerateVariantJSON(variantColors)
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshGTK(configDir string, mode ColorMode) {
|
func isDMSGTKActive(configDir string) bool {
|
||||||
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||||
|
|
||||||
info, err := os.Lstat(gtkCSS)
|
info, err := os.Lstat(gtkCSS)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldRun := false
|
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
target, err := os.Readlink(gtkCSS)
|
target, err := os.Readlink(gtkCSS)
|
||||||
if err == nil && strings.Contains(target, "dank-colors.css") {
|
return err == nil && strings.Contains(target, "dank-colors.css")
|
||||||
shouldRun = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
data, err := os.ReadFile(gtkCSS)
|
|
||||||
if err == nil && strings.Contains(string(data), "dank-colors.css") {
|
|
||||||
shouldRun = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !shouldRun {
|
data, err := os.ReadFile(gtkCSS)
|
||||||
return
|
return err == nil && strings.Contains(string(data), "dank-colors.css")
|
||||||
}
|
|
||||||
|
|
||||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
|
||||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalTerminals() {
|
func refreshGTK(mode ColorMode) {
|
||||||
signalByName("kitty", syscall.SIGUSR1)
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
|
||||||
signalByName("ghostty", syscall.SIGUSR2)
|
log.Warnf("Failed to reset gtk-theme: %v", err)
|
||||||
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
}
|
||||||
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()); err != nil {
|
||||||
|
log.Warnf("Failed to set gtk-theme: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshGTK4() {
|
||||||
|
output, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
current := strings.Trim(output, "'")
|
||||||
|
|
||||||
|
var toggle string
|
||||||
|
if current == "prefer-dark" {
|
||||||
|
toggle = "default"
|
||||||
|
} else {
|
||||||
|
toggle = "prefer-dark"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", toggle); err != nil {
|
||||||
|
log.Warnf("Failed to toggle color-scheme for GTK4 refresh: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", current); err != nil {
|
||||||
|
log.Warnf("Failed to restore color-scheme for GTK4 refresh: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func refreshQt6ct() {
|
||||||
|
confPath := filepath.Join(utils.XDGConfigHome(), "qt6ct", "qt6ct.conf")
|
||||||
|
now := time.Now()
|
||||||
|
if err := os.Chtimes(confPath, now, now); err != nil {
|
||||||
|
log.Warnf("Failed to touch qt6ct.conf: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalTerminals(opts *Options) {
|
||||||
|
if !opts.ShouldSkipTemplate("kitty") && appExists(opts.AppChecker, []string{"kitty"}, nil) {
|
||||||
|
signalByName("kitty", syscall.SIGUSR1)
|
||||||
|
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
||||||
|
}
|
||||||
|
if !opts.ShouldSkipTemplate("ghostty") && appExists(opts.AppChecker, []string{"ghostty"}, nil) {
|
||||||
|
signalByName("ghostty", syscall.SIGUSR2)
|
||||||
|
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func signalByName(name string, sig syscall.Signal) {
|
func signalByName(name string, sig syscall.Signal) {
|
||||||
@@ -679,8 +870,59 @@ func syncColorScheme(mode ColorMode) {
|
|||||||
scheme = "default"
|
scheme = "default"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", scheme); err != nil {
|
||||||
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
log.Warnf("Failed to sync color-scheme: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var adwaitaAccents = []struct {
|
||||||
|
name string
|
||||||
|
colors []colorful.Color
|
||||||
|
}{
|
||||||
|
{"blue", hexColors("#3f8ae5", "#438de6", "#a4caee")},
|
||||||
|
{"green", hexColors("#26a269", "#39ac76", "#81d5ad")},
|
||||||
|
{"orange", hexColors("#f17738", "#ff7800", "#ffc994")},
|
||||||
|
{"pink", hexColors("#e4358a", "#e64392", "#f9b3d5")},
|
||||||
|
{"purple", hexColors("#954ab5", "#9c46b9", "#d099d6")},
|
||||||
|
{"red", hexColors("#e84053", "#e01b24", "#f2a1a5")},
|
||||||
|
{"slate", hexColors("#557b9f", "#6a8daf", "#b4c6d6")},
|
||||||
|
{"teal", hexColors("#129eb0", "#2190a4", "#7bdff4")},
|
||||||
|
{"yellow", hexColors("#cbac10", "#d4b411", "#f5c211")},
|
||||||
|
}
|
||||||
|
|
||||||
|
func hexColors(hexes ...string) []colorful.Color {
|
||||||
|
out := make([]colorful.Color, len(hexes))
|
||||||
|
for i, h := range hexes {
|
||||||
|
out[i], _ = colorful.Hex(h)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func closestAdwaitaAccent(primaryHex string) string {
|
||||||
|
c, err := colorful.Hex(primaryHex)
|
||||||
|
if err != nil {
|
||||||
|
return "blue"
|
||||||
|
}
|
||||||
|
|
||||||
|
best := "blue"
|
||||||
|
bestDist := math.MaxFloat64
|
||||||
|
for _, a := range adwaitaAccents {
|
||||||
|
for _, ref := range a.colors {
|
||||||
|
d := c.DistanceCIEDE2000(ref)
|
||||||
|
if d < bestDist {
|
||||||
|
bestDist = d
|
||||||
|
best = a.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncAccentColor(primaryHex string) {
|
||||||
|
accent := closestAdwaitaAccent(primaryHex)
|
||||||
|
log.Infof("Setting GNOME accent color: %s", accent)
|
||||||
|
if err := utils.GsettingsSet("org.gnome.desktop.interface", "accent-color", accent); err != nil {
|
||||||
|
log.Warnf("Failed to set accent-color: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,6 +947,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
|||||||
detected = true
|
detected = true
|
||||||
case tmpl.Kind == TemplateKindVSCode:
|
case tmpl.Kind == TemplateKindVSCode:
|
||||||
detected = checkVSCodeExtension(homeDir)
|
detected = checkVSCodeExtension(homeDir)
|
||||||
|
case tmpl.Kind == TemplateKindEmacs:
|
||||||
|
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
|
||||||
default:
|
default:
|
||||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -15,13 +16,13 @@ func TestAppendConfigBinaryExists(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,13 +59,13 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,13 +100,13 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "zen config content"
|
testConfig := "zen config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,13 +140,13 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +181,13 @@ func TestAppendConfigBothExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "zen config content"
|
testConfig := "zen config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,13 +221,13 @@ func TestAppendConfigNeitherExists(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "test config content"
|
testConfig := "test config content"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,13 +262,13 @@ func TestAppendConfigNoChecks(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
testConfig := "always include"
|
testConfig := "always include"
|
||||||
configPath := filepath.Join(configsDir, "test.toml")
|
configPath := filepath.Join(configsDir, "test.toml")
|
||||||
if err := os.WriteFile(configPath, []byte(testConfig), 0644); err != nil {
|
if err := os.WriteFile(configPath, []byte(testConfig), 0o644); err != nil {
|
||||||
t.Fatalf("failed to write config: %v", err)
|
t.Fatalf("failed to write config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +299,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
|
|||||||
|
|
||||||
shellDir := filepath.Join(tempDir, "shell")
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
if err := os.MkdirAll(configsDir, 0755); err != nil {
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
t.Fatalf("failed to create configs dir: %v", err)
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write base config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: filepath.Join(tempDir, "config"),
|
||||||
|
StateDir: filepath.Join(tempDir, "state"),
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(output)
|
||||||
|
assert.Contains(t, content, "[templates.dank]")
|
||||||
|
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||||
|
assert.NotContains(t, content, "[templates.gtk]")
|
||||||
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package matugen
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -93,10 +94,13 @@ func (q *Queue) runWorker() {
|
|||||||
err := Run(job.Options)
|
err := Run(job.Options)
|
||||||
|
|
||||||
var result Result
|
var result Result
|
||||||
if err != nil {
|
switch {
|
||||||
result = Result{Success: false, Error: err}
|
case err == nil:
|
||||||
} else {
|
|
||||||
result = Result{Success: true}
|
result = Result{Success: true}
|
||||||
|
case errors.Is(err, ErrNoChanges):
|
||||||
|
result = Result{Success: true}
|
||||||
|
default:
|
||||||
|
result = Result{Success: false, Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
q.finishJob(result)
|
q.finishJob(result)
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||||
|
|
||||||
|
package mocks_geolocation
|
||||||
|
|
||||||
|
import (
|
||||||
|
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
|
mock "github.com/stretchr/testify/mock"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockClient is an autogenerated mock type for the Client type
|
||||||
|
type MockClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockClient_Expecter struct {
|
||||||
|
mock *mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_m *MockClient) EXPECT() *MockClient_Expecter {
|
||||||
|
return &MockClient_Expecter{mock: &_m.Mock}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close provides a mock function with no fields
|
||||||
|
func (_m *MockClient) Close() {
|
||||||
|
_m.Called()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||||
|
type MockClient_Close_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close is a helper method to define mock.On call
|
||||||
|
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
|
||||||
|
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
|
||||||
|
_c.Call.Return()
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
|
||||||
|
_c.Run(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation provides a mock function with no fields
|
||||||
|
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
|
||||||
|
ret := _m.Called()
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetLocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 geolocation.Location
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
|
||||||
|
return rf()
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
|
||||||
|
r0 = rf()
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(geolocation.Location)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func() error); ok {
|
||||||
|
r1 = rf()
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
|
||||||
|
type MockClient_GetLocation_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLocation is a helper method to define mock.On call
|
||||||
|
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
|
||||||
|
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run()
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe provides a mock function with given fields: id
|
||||||
|
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
|
||||||
|
ret := _m.Called(id)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for Subscribe")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 chan geolocation.Location
|
||||||
|
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
|
||||||
|
r0 = rf(id)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(chan geolocation.Location)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
|
||||||
|
type MockClient_Subscribe_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe is a helper method to define mock.On call
|
||||||
|
// - id string
|
||||||
|
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
|
||||||
|
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||||
|
_c.Call.Return(_a0)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe provides a mock function with given fields: id
|
||||||
|
func (_m *MockClient) Unsubscribe(id string) {
|
||||||
|
_m.Called(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
|
||||||
|
type MockClient_Unsubscribe_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe is a helper method to define mock.On call
|
||||||
|
// - id string
|
||||||
|
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
|
||||||
|
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
|
||||||
|
_c.Call.Return()
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
|
||||||
|
_c.Run(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||||
|
// The first argument is typically a *testing.T value.
|
||||||
|
func NewMockClient(t interface {
|
||||||
|
mock.TestingT
|
||||||
|
Cleanup(func())
|
||||||
|
}) *MockClient {
|
||||||
|
mock := &MockClient{}
|
||||||
|
mock.Mock.Test(t)
|
||||||
|
|
||||||
|
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||||
|
|
||||||
|
return mock
|
||||||
|
}
|
||||||
@@ -1062,6 +1062,62 @@ func (_c *MockBackend_GetWiFiNetworkDetails_Call) RunAndReturn(run func(string)
|
|||||||
return _c
|
return _c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetWiFiQRCodeContent provides a mock function with given fields: ssid
|
||||||
|
func (_m *MockBackend) GetWiFiQRCodeContent(ssid string) (string, error) {
|
||||||
|
ret := _m.Called(ssid)
|
||||||
|
|
||||||
|
if len(ret) == 0 {
|
||||||
|
panic("no return value specified for GetWiFiQRCodeContent")
|
||||||
|
}
|
||||||
|
|
||||||
|
var r0 string
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func(string) (string, error)); ok {
|
||||||
|
return rf(ssid)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func(string) string); ok {
|
||||||
|
r0 = rf(ssid)
|
||||||
|
} else {
|
||||||
|
r0 = ret.Get(0).(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||||
|
r1 = rf(ssid)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockBackend_GetWiFiQRCodeContent_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiQRCodeContent'
|
||||||
|
type MockBackend_GetWiFiQRCodeContent_Call struct {
|
||||||
|
*mock.Call
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWiFiQRCodeContent is a helper method to define mock.On call
|
||||||
|
// - ssid string
|
||||||
|
func (_e *MockBackend_Expecter) GetWiFiQRCodeContent(ssid interface{}) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||||
|
return &MockBackend_GetWiFiQRCodeContent_Call{Call: _e.mock.On("GetWiFiQRCodeContent", ssid)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Run(run func(ssid string)) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||||
|
_c.Call.Run(func(args mock.Arguments) {
|
||||||
|
run(args[0].(string))
|
||||||
|
})
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockBackend_GetWiFiQRCodeContent_Call) Return(_a0 string, _a1 error) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||||
|
_c.Call.Return(_a0, _a1)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (_c *MockBackend_GetWiFiQRCodeContent_Call) RunAndReturn(run func(string) (string, error)) *MockBackend_GetWiFiQRCodeContent_Call {
|
||||||
|
_c.Call.Return(run)
|
||||||
|
return _c
|
||||||
|
}
|
||||||
|
|
||||||
// GetWiredConnections provides a mock function with no fields
|
// GetWiredConnections provides a mock function with no fields
|
||||||
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
|
func (_m *MockBackend) GetWiredConnections() ([]network.WiredConnection, error) {
|
||||||
ret := _m.Called()
|
ret := _m.Called()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -15,6 +16,9 @@ const (
|
|||||||
notifyDest = "org.freedesktop.Notifications"
|
notifyDest = "org.freedesktop.Notifications"
|
||||||
notifyPath = "/org/freedesktop/Notifications"
|
notifyPath = "/org/freedesktop/Notifications"
|
||||||
notifyInterface = "org.freedesktop.Notifications"
|
notifyInterface = "org.freedesktop.Notifications"
|
||||||
|
|
||||||
|
maxSummaryLen = 29
|
||||||
|
maxBodyLen = 80
|
||||||
)
|
)
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
@@ -39,6 +43,13 @@ func Send(n Notification) error {
|
|||||||
n.Timeout = 5000
|
n.Timeout = 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(n.Summary) > maxSummaryLen {
|
||||||
|
n.Summary = n.Summary[:maxSummaryLen-3] + "..."
|
||||||
|
}
|
||||||
|
if len(n.Body) > maxBodyLen {
|
||||||
|
n.Body = n.Body[:maxBodyLen-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
var actions []string
|
var actions []string
|
||||||
if n.FilePath != "" {
|
if n.FilePath != "" {
|
||||||
actions = []string{
|
actions = []string{
|
||||||
@@ -49,7 +60,11 @@ func Send(n Notification) error {
|
|||||||
|
|
||||||
hints := map[string]dbus.Variant{}
|
hints := map[string]dbus.Variant{}
|
||||||
if n.FilePath != "" {
|
if n.FilePath != "" {
|
||||||
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
imgPath := n.FilePath
|
||||||
|
if !strings.HasPrefix(imgPath, "file://") {
|
||||||
|
imgPath = "file://" + imgPath
|
||||||
|
}
|
||||||
|
hints["image_path"] = dbus.MakeVariant(imgPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := conn.Object(notifyDest, notifyPath)
|
obj := conn.Object(notifyDest, notifyPath)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user