mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-05 12:02:06 -04:00
Compare commits
303 Commits
6d4c31492c
...
hotfix-1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03a8e1e0d5 | ||
|
|
4d4d3c20a1 | ||
|
|
cef16d6bc9 | ||
|
|
aafaad1791 | ||
|
|
7906fdc2b0 | ||
|
|
397650ca52 | ||
|
|
826207006a | ||
|
|
58c2fcd31c | ||
|
|
b2a2b425ec | ||
|
|
942c9c9609 | ||
|
|
46d6e1cff3 | ||
|
|
a4137c57c1 | ||
|
|
1ad8b627f1 | ||
|
|
58a02ce290 | ||
|
|
8e1ad1a2be | ||
|
|
68cd7ab32c | ||
|
|
f649ce9a8e | ||
|
|
c4df242f07 | ||
|
|
26846c8d55 | ||
|
|
31b44a667c | ||
|
|
4f3b73ee21 | ||
|
|
4cfae91f02 | ||
|
|
8d947a6e95 | ||
|
|
1e84d4252c | ||
|
|
76072e1d4c | ||
|
|
6408dce4a9 | ||
|
|
0b2e1cca38 | ||
|
|
c1bfd8c0b7 | ||
|
|
90ffa5833b | ||
|
|
169c669286 | ||
|
|
f8350deafc | ||
|
|
0286a1b80b | ||
|
|
7c3e6c1f02 | ||
|
|
d2d72db3c9 | ||
|
|
f81f861408 | ||
|
|
af494543f5 | ||
|
|
db4de55338 | ||
|
|
37ecbbbbde | ||
|
|
d6a6d2a438 | ||
|
|
bf1c6eec74 | ||
|
|
0ddae80584 | ||
|
|
5c96c03bfa | ||
|
|
dfe36e47d8 | ||
|
|
63e1b75e57 | ||
|
|
29efdd8598 | ||
|
|
34d03cf11b | ||
|
|
c339389d44 | ||
|
|
af5f6eb656 | ||
|
|
a6d28e2553 | ||
|
|
6213267908 | ||
|
|
d084114149 | ||
|
|
f6d99eca0d | ||
|
|
722eb3289e | ||
|
|
b7f2bdcb2d | ||
|
|
11c20db6e6 | ||
|
|
8a4e3f8bb1 | ||
|
|
bc8fe97c13 | ||
|
|
47262155aa | ||
|
|
dd4c41a6b2 | ||
|
|
92a25fdb6a | ||
|
|
d6650be008 | ||
|
|
2646e7b19a | ||
|
|
4133f11d82 | ||
|
|
22ed740394 | ||
|
|
063299a434 | ||
|
|
44d836c975 | ||
|
|
da437e77fb | ||
|
|
34a6bbfb32 | ||
|
|
9ed53bac9e | ||
|
|
3a6752c3d2 | ||
|
|
ef19568dd7 | ||
|
|
f280cd9d3b | ||
|
|
cf4ce3c476 | ||
|
|
ae59e53c4c | ||
|
|
7e0d661f63 | ||
|
|
0b33d3f905 | ||
|
|
d62bdda56b | ||
|
|
5841b38cd9 | ||
|
|
83e2b5a7a6 | ||
|
|
2f863f64ee | ||
|
|
1a8b397cfd | ||
|
|
196c421b75 | ||
|
|
8399d64c2d | ||
|
|
c530eab303 | ||
|
|
45b6362dd3 | ||
|
|
50b77dcfc3 | ||
|
|
be8f3adf01 | ||
|
|
75a8c171ea | ||
|
|
466ff59573 | ||
|
|
053bb91927 | ||
|
|
2c9b22c016 | ||
|
|
a9ee91586e | ||
|
|
81bce74612 | ||
|
|
f2a6d2c7da | ||
|
|
0a9a34912e | ||
|
|
abff670814 | ||
|
|
0d49acaaa8 | ||
|
|
ebe1785411 | ||
|
|
f9f0192b22 | ||
|
|
e5cdbf4cf5 | ||
|
|
13ef1efa7b | ||
|
|
fbd9301a2d | ||
|
|
24e3024b57 | ||
|
|
52d5af11ba | ||
|
|
44a45b00cf | ||
|
|
2b78fe5b9f | ||
|
|
14f92669c6 | ||
|
|
124106de87 | ||
|
|
bb8e0d384f | ||
|
|
59d37847ec | ||
|
|
acdc531dca | ||
|
|
ce75dac81b | ||
|
|
b8d40761ff | ||
|
|
3a7430f6da | ||
|
|
242660c51d | ||
|
|
8a6c1e45ce | ||
|
|
b8e5f9f3b1 | ||
|
|
d60e70f9cc | ||
|
|
cdb70fadb3 | ||
|
|
7867deef60 | ||
|
|
a77c1adb32 | ||
|
|
da14d75a3b | ||
|
|
7c66a34931 | ||
|
|
425715e0f0 | ||
|
|
a3baf8ce31 | ||
|
|
605e03b065 | ||
|
|
0e9b21d359 | ||
|
|
ba5bf0cabc | ||
|
|
96b9d7aab3 | ||
|
|
750e4c4527 | ||
|
|
7417e26444 | ||
|
|
00e1099912 | ||
|
|
bd46d29ff0 | ||
|
|
1a9d7684b9 | ||
|
|
0133c19276 | ||
|
|
46bb3b613b | ||
|
|
5839a5de30 | ||
|
|
535d0bb0f0 | ||
|
|
4d316007af | ||
|
|
3c2d60d8e1 | ||
|
|
9c4f4cbd0d | ||
|
|
a337585b00 | ||
|
|
1cdec5d687 | ||
|
|
081b15e24c | ||
|
|
b04cb7b3cc | ||
|
|
e2c3ff00fb | ||
|
|
c783ff3dcf | ||
|
|
2c360dc3e8 | ||
|
|
5342647bfb | ||
|
|
46a2f6f0d8 | ||
|
|
f8af8fc171 | ||
|
|
3d0ee9d72b | ||
|
|
5a0bb260b4 | ||
|
|
9a7f1f5f2f | ||
|
|
d88b04fc30 | ||
|
|
6fe4cc98b9 | ||
|
|
b9bcfd8d2c | ||
|
|
e3bd31bb52 | ||
|
|
0922e3e459 | ||
|
|
a168b12bb2 | ||
|
|
8c01deba86 | ||
|
|
b645487e79 | ||
|
|
91569affd7 | ||
|
|
1ed44ee6f3 | ||
|
|
fce120fa31 | ||
|
|
a02b0c0c3c | ||
|
|
c86999f389 | ||
|
|
2b546967d2 | ||
|
|
591d2ba4d4 | ||
|
|
37cc4ab197 | ||
|
|
d775974a90 | ||
|
|
cc62aa4a9e | ||
|
|
5b8b7b04be | ||
|
|
b4a8853591 | ||
|
|
4220dfe2a5 | ||
|
|
4557426c28 | ||
|
|
8ee7fe8e66 | ||
|
|
c4a41f994a | ||
|
|
fa639424f5 | ||
|
|
e618a8390c | ||
|
|
393e9ed2e4 | ||
|
|
e1ea441215 | ||
|
|
654661fd66 | ||
|
|
c5a21f8da0 | ||
|
|
ca5b168117 | ||
|
|
aa88eb42ee | ||
|
|
ac84cadd77 | ||
|
|
81d5235b9f | ||
|
|
8944762c76 | ||
|
|
3d05c34673 | ||
|
|
c2ee41c844 | ||
|
|
6b537f30a5 | ||
|
|
a3ae95df09 | ||
|
|
4349d68f87 | ||
|
|
7d5c20125a | ||
|
|
2583dbd3f2 | ||
|
|
a103b93583 | ||
|
|
fff018eafb | ||
|
|
60b824e7a4 | ||
|
|
e27e904157 | ||
|
|
fe15667986 | ||
|
|
bd9029e533 | ||
|
|
fa71d563ea | ||
|
|
143918bc5e | ||
|
|
961680af8c | ||
|
|
6e3b3ce888 | ||
|
|
44292c3b55 | ||
|
|
c024c1b8e4 | ||
|
|
13adfdec11 | ||
|
|
e9ec28aab7 | ||
|
|
0af4d1d6e3 | ||
|
|
3ef0e63533 | ||
|
|
f4dad69ccd | ||
|
|
b811316d0c | ||
|
|
f59aeb2782 | ||
|
|
24ce41935e | ||
|
|
3c4749ead0 | ||
|
|
22ab5b9660 | ||
|
|
22f16f1da3 | ||
|
|
a97409dfd7 | ||
|
|
eaa6a664c8 | ||
|
|
d934b3b3b4 | ||
|
|
546cbfb3ca | ||
|
|
39b70a53a0 | ||
|
|
795f84adce | ||
|
|
3d80a9dd0f | ||
|
|
9669e9bc87 | ||
|
|
5f2a5a5d7d | ||
|
|
ecfd721fc0 | ||
|
|
07242a00b3 | ||
|
|
4602442feb | ||
|
|
a90717b20c | ||
|
|
02edce2999 | ||
|
|
f2d9066f90 | ||
|
|
f6f7b1ed72 | ||
|
|
803bc1cb7f | ||
|
|
67d3aa9da3 | ||
|
|
9fbff5e833 | ||
|
|
c371140a97 | ||
|
|
c755a3719d | ||
|
|
4f153f3026 | ||
|
|
f2b1dbd256 | ||
|
|
be0ca993ff | ||
|
|
ed87e1b00b | ||
|
|
ac509933d7 | ||
|
|
f49f98ff85 | ||
|
|
10923346d7 | ||
|
|
f27bffc387 | ||
|
|
36b43f93a3 | ||
|
|
2deeab9d08 | ||
|
|
f00854879c | ||
|
|
75fd62865b | ||
|
|
757054e140 | ||
|
|
eda59b348c | ||
|
|
d19e81ffac | ||
|
|
60c6872aec | ||
|
|
a9cb2fe912 | ||
|
|
a168a8160c | ||
|
|
78662f9613 | ||
|
|
d9d7bb8dcc | ||
|
|
3136f48b30 | ||
|
|
0c46711b01 | ||
|
|
68159b5c41 | ||
|
|
6557d66f94 | ||
|
|
9553cb06d3 | ||
|
|
122fb16dfb | ||
|
|
511502220f | ||
|
|
8bfe7439c0 | ||
|
|
8499033221 | ||
|
|
705d5b04dd | ||
|
|
17eaa761f8 | ||
|
|
1cdbd01748 | ||
|
|
08cc076a4c | ||
|
|
2a02d5594c | ||
|
|
2263338878 | ||
|
|
26bc5425d3 | ||
|
|
38b4d1dc95 | ||
|
|
3aaca7ff39 | ||
|
|
83d9808536 | ||
|
|
ad458dfece | ||
|
|
8f6fe7ed2b | ||
|
|
419a692593 | ||
|
|
03fdf795e0 | ||
|
|
832807a217 | ||
|
|
f7df3b2a68 | ||
|
|
0d03e73595 | ||
|
|
c5ae1a77d3 | ||
|
|
5f16624000 | ||
|
|
80025804ab | ||
|
|
028d3b4e61 | ||
|
|
9cce5ccfe6 | ||
|
|
a260b8060e | ||
|
|
f945307232 | ||
|
|
8f44d52cb2 | ||
|
|
3413cb7b89 | ||
|
|
4e3b24ffbb | ||
|
|
03cfa55e0b | ||
|
|
a887e60f40 | ||
|
|
816819bf9f | ||
|
|
78f3bb3812 | ||
|
|
01d7ed5dd8 | ||
|
|
50311db280 | ||
|
|
01b1a276c5 |
57
.agents/settings.local.json
Normal file
57
.agents/settings.local.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(cat:*)",
|
||||
"Bash(git -C /home/purian23/dms diff --stat .github/workflows/)",
|
||||
"Bash(git -C /home/purian23/projects/danklinux diff --stat .github/workflows/)",
|
||||
"Bash(git -C /home/purian23/dms diff .github/workflows/)",
|
||||
"Bash(git -C /home/purian23/dms diff .github/workflows/run-ppa.yml)",
|
||||
"Bash(osc cat:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git show-ref:*)",
|
||||
"Bash(git tag:*)",
|
||||
"Bash(bash -c 'ALL_PATHS=$(grep -A 5 \"\"<service name=\\\"\"download_url\\\"\">\"\" distro/debian/dms/_service | grep \"\"<param name=\\\"\"path\\\"\">\"\" | sed \"\"s/.*<param name=\\\"\"path\\\"\">\\(.*\\)<\\/param>.*/\\1/\"\"); SOURCE_PATH=\"\"\"\"; for path in $ALL_PATHS; do if echo \"\"$path\"\" | grep -qE \"\"(source|archive|\\.tar\\.(gz|xz|bz2))\"\" && ! echo \"\"$path\"\" | grep -qE \"\"(distropkg|binary)\"\"; then SOURCE_PATH=\"\"$path\"\"; break; fi; done; echo \"\"Selected path: $SOURCE_PATH\"\"')",
|
||||
"Bash(curl:*)",
|
||||
"Bash(tar:*)",
|
||||
"Bash(git -C /home/purian23/dms log:*)",
|
||||
"Bash(osc status:*)",
|
||||
"Bash(osc commit:*)",
|
||||
"Bash(osc up:*)",
|
||||
"Bash(osc results:*)",
|
||||
"Bash(osc api:*)",
|
||||
"Bash(systemctl:*)",
|
||||
"Bash(dms version:*)",
|
||||
"Bash(git describe:*)",
|
||||
"Bash(qmlsc:*)",
|
||||
"Bash(qmllint-qt6:*)",
|
||||
"Bash(make fmt:*)",
|
||||
"Bash(make test:*)",
|
||||
"Bash(dms chroma list-styles:*)",
|
||||
"Bash(python3:*)",
|
||||
"Bash(time dms chroma:*)",
|
||||
"Bash(dms chroma:*)",
|
||||
"Bash(make build:*)",
|
||||
"Bash(pgrep:*)",
|
||||
"Bash(go build:*)",
|
||||
"Bash(/tmp/dms-test chroma:*)",
|
||||
"Bash(1)",
|
||||
"Bash(go install:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(journalctl:*)",
|
||||
"Bash(qdbus:*)",
|
||||
"Bash(TZ='Asia/Tokyo' date:*)",
|
||||
"Bash(dms --help:*)",
|
||||
"Bash(dms run:*)",
|
||||
"Bash(dms status:*)",
|
||||
"Bash(dms kill:*)",
|
||||
"Bash(tee:*)",
|
||||
"Bash(qmlscene:*)",
|
||||
"Bash(quickshell --version:*)",
|
||||
"WebFetch(domain:forum.qt.io)",
|
||||
"Bash(gh api:*)",
|
||||
"WebFetch(domain:github.com)",
|
||||
"WebFetch(domain:raw.githubusercontent.com)"
|
||||
]
|
||||
}
|
||||
}
|
||||
71
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
71
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -8,31 +8,31 @@ body:
|
||||
value: |
|
||||
## DankMaterialShell Bug Report
|
||||
Limit your report to one issue per submission unless closely related
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor
|
||||
options:
|
||||
- label: Niri
|
||||
- label: Hyprland
|
||||
- label: MangoWC (dwl)
|
||||
- label: Sway
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: Distribution
|
||||
options:
|
||||
- label: Arch Linux
|
||||
- label: CachyOS
|
||||
- label: Fedora
|
||||
- label: NixOS
|
||||
- label: Debian
|
||||
- label: Ubuntu
|
||||
- label: Gentoo
|
||||
- label: OpenSUSE
|
||||
- label: Other (specify below)
|
||||
- Arch Linux
|
||||
- CachyOS
|
||||
- Fedora
|
||||
- NixOS
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- Gentoo
|
||||
- OpenSUSE
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -42,12 +42,45 @@ body:
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: installation_method
|
||||
attributes:
|
||||
label: Select your Installation Method
|
||||
options:
|
||||
- DankInstaller
|
||||
- Distro Packaging
|
||||
- Source
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method_different
|
||||
attributes:
|
||||
label: Was your original Installation method different?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: original_installation_method_specify
|
||||
attributes:
|
||||
label: If no, specify
|
||||
placeholder: e.g., Distro Packaging, then Source
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -69,7 +102,7 @@ body:
|
||||
- type: textarea
|
||||
id: steps_to_reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce & Installation Method
|
||||
label: Steps to Reproduce
|
||||
description: Please provide detailed steps to reproduce the issue
|
||||
placeholder: |
|
||||
1. ...
|
||||
|
||||
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
23
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,5 +1,5 @@
|
||||
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:
|
||||
- enhancement
|
||||
body:
|
||||
@@ -23,18 +23,25 @@ body:
|
||||
placeholder: Why is this feature important?
|
||||
validations:
|
||||
required: false
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor(s)
|
||||
description: Is this feature specific to one or more compositors?
|
||||
options:
|
||||
- label: All compositors
|
||||
- label: Niri
|
||||
- label: Hyprland
|
||||
- label: MangoWC (dwl)
|
||||
- label: Sway
|
||||
- label: Other (specify below)
|
||||
- All compositors
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: compositor_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., Wayfire, Mutter, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
79
.github/ISSUE_TEMPLATE/support_request.yml
vendored
79
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -7,32 +7,87 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Support Request
|
||||
- type: checkboxes
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
label: Compositor
|
||||
options:
|
||||
- label: Niri
|
||||
- label: Hyprland
|
||||
- label: MangoWC (dwl)
|
||||
- label: Sway
|
||||
- label: Other (specify below)
|
||||
- Niri
|
||||
- Hyprland
|
||||
- MangoWC (dwl)
|
||||
- Sway
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: compositor_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., Wayfire, Mutter, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
- type: dropdown
|
||||
id: distribution
|
||||
attributes:
|
||||
label: Distribution
|
||||
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
||||
placeholder: Your Linux distribution
|
||||
options:
|
||||
- Arch Linux
|
||||
- CachyOS
|
||||
- Fedora
|
||||
- NixOS
|
||||
- Debian
|
||||
- Ubuntu
|
||||
- Gentoo
|
||||
- OpenSUSE
|
||||
- Other (specify below)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: distribution_other
|
||||
attributes:
|
||||
label: If Other, please specify
|
||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: installation_method
|
||||
attributes:
|
||||
label: Select your Installation Method
|
||||
options:
|
||||
- DankInstaller
|
||||
- Distro Packaging
|
||||
- Source
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method_different
|
||||
attributes:
|
||||
label: Was your original Installation method different?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
default: 0
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
id: original_installation_method_specify
|
||||
attributes:
|
||||
label: If no, specify
|
||||
placeholder: e.g., Distro Packaging, then Source
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -v
|
||||
description: Output of `dms doctor -v` command
|
||||
placeholder: Paste the output of `dms doctor -v` here
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
|
||||
383
.github/workflows/backup/run-obs.yml.bak
vendored
383
.github/workflows/backup/run-obs.yml.bak
vendored
@@ -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
|
||||
298
.github/workflows/backup/run-ppa.yml.bak
vendored
298
.github/workflows/backup/run-ppa.yml.bak
vendored
@@ -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
|
||||
5
.github/workflows/prek.yml
vendored
5
.github/workflows/prek.yml
vendored
@@ -20,5 +20,10 @@ jobs:
|
||||
- name: Add a flatpak that mutagen could support
|
||||
run: sudo flatpak install -y org.freedesktop.Platform/x86_64/24.08 app.zen_browser.zen
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: core/go.mod
|
||||
|
||||
- name: run pre-commit hooks
|
||||
uses: j178/prek-action@v1
|
||||
|
||||
19
.github/workflows/release.yml
vendored
19
.github/workflows/release.yml
vendored
@@ -191,6 +191,11 @@ jobs:
|
||||
git fetch origin --force tag ${TAG}
|
||||
git checkout ${TAG}
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Download core artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
@@ -229,6 +234,7 @@ jobs:
|
||||
- **`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-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
|
||||
|
||||
### Checksums
|
||||
@@ -387,6 +393,19 @@ jobs:
|
||||
rm -rf _temp_full
|
||||
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
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
|
||||
4
.github/workflows/run-obs.yml
vendored
4
.github/workflows/run-obs.yml
vendored
@@ -17,7 +17,7 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
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:
|
||||
check-updates:
|
||||
@@ -335,7 +335,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version-file: ./core/go.mod
|
||||
|
||||
- name: Install OSC
|
||||
run: |
|
||||
|
||||
4
.github/workflows/run-ppa.yml
vendored
4
.github/workflows/run-ppa.yml
vendored
@@ -12,7 +12,7 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
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:
|
||||
check-updates:
|
||||
@@ -158,7 +158,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.24"
|
||||
go-version-file: ./core/go.mod
|
||||
cache: false
|
||||
|
||||
- name: Install build dependencies
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -56,6 +56,8 @@ UNUSED
|
||||
|
||||
CLAUDE-activeContext.md
|
||||
CLAUDE-temp.md
|
||||
AGENTS-activeContext.md
|
||||
AGENTS-temp.md
|
||||
|
||||
# Auto-generated theme files
|
||||
*.generated.*
|
||||
|
||||
@@ -5,8 +5,18 @@ repos:
|
||||
- id: trailing-whitespace
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: v0.10.0.1
|
||||
- repo: local
|
||||
hooks:
|
||||
- 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
|
||||
|
||||
@@ -8,6 +8,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 v2 - omega stuff, GIF search, supa powerful
|
||||
- dock on bar
|
||||
- window rule manager, with IPC - #TODO verify RTL layout (niri only)
|
||||
|
||||
# 1.2.0
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ nix develop
|
||||
|
||||
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
|
||||
- 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/)
|
||||
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
|
||||
{
|
||||
"[qml]": {
|
||||
"editor.defaultFormatter": "qt-project.qmlls",
|
||||
"editor.formatOnSave": 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -58,10 +58,10 @@ install-completions:
|
||||
install-systemd:
|
||||
@echo "Installing systemd user service..."
|
||||
@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
|
||||
@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"
|
||||
|
||||
install-icon:
|
||||
|
||||
@@ -19,7 +19,7 @@ Built with [Quickshell](https://quickshell.org/) and [Go](https://go.dev/)
|
||||
|
||||
</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
|
||||
|
||||
@@ -105,7 +105,7 @@ Extend functionality with the [plugin registry](https://plugins.danklinux.com).
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.6.2
|
||||
rev: v2.9.0
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
require_serial: true
|
||||
|
||||
@@ -96,7 +96,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
||||
|
||||
## Building
|
||||
|
||||
Requires Go 1.24+
|
||||
Requires Go 1.25+
|
||||
|
||||
**Development build:**
|
||||
|
||||
|
||||
@@ -12,17 +12,21 @@ import (
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -48,6 +52,7 @@ var (
|
||||
clipCopyForeground bool
|
||||
clipCopyPasteOnce bool
|
||||
clipCopyType string
|
||||
clipCopyDownload bool
|
||||
clipJSONOutput bool
|
||||
)
|
||||
|
||||
@@ -107,6 +112,7 @@ var clipClearCmd = &cobra.Command{
|
||||
}
|
||||
|
||||
var clipWatchStore bool
|
||||
var clipWatchMimes bool
|
||||
|
||||
var clipSearchCmd = &cobra.Command{
|
||||
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(&clipCopyPasteOnce, "paste-once", "o", false, "Exit after first paste")
|
||||
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")
|
||||
clipHistoryCmd.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(&clipSearchOffset, "offset", "o", 0, "Result offset")
|
||||
@@ -205,6 +212,7 @@ func init() {
|
||||
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(&clipWatchMimes, "mimes", "m", false, "Show all offered MIME types")
|
||||
|
||||
clipMigrateCmd.Flags().BoolVar(&clipMigrateDelete, "delete", false, "Delete cliphist db after successful migration")
|
||||
|
||||
@@ -215,9 +223,10 @@ func init() {
|
||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
var data []byte
|
||||
|
||||
if len(args) > 0 {
|
||||
switch {
|
||||
case len(args) > 0:
|
||||
data = []byte(args[0])
|
||||
} else {
|
||||
default:
|
||||
var err error
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
if err != nil {
|
||||
@@ -225,11 +234,68 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
||||
}
|
||||
}
|
||||
|
||||
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 err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||
log.Fatalf("copy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
data, _, err := clipboard.Paste()
|
||||
if err != nil {
|
||||
@@ -264,6 +330,30 @@ func runClipWatch(cmd *cobra.Command, args []string) {
|
||||
}); err != nil && err != context.Canceled {
|
||||
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:
|
||||
if err := clipboard.Watch(ctx, func(data []byte, mimeType string) {
|
||||
out := map[string]any{
|
||||
@@ -386,16 +476,13 @@ func runClipGet(cmd *cobra.Command, args []string) {
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "clipboard.copyEntry",
|
||||
Params: map[string]any{
|
||||
"id": id,
|
||||
},
|
||||
Params: map[string]any{"id": id},
|
||||
}
|
||||
|
||||
resp, err := sendServerRequest(req)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to copy clipboard entry: %v", err)
|
||||
}
|
||||
|
||||
if resp.Error != "" {
|
||||
log.Fatalf("Error: %s", resp.Error)
|
||||
}
|
||||
@@ -672,7 +759,7 @@ func runClipExport(cmd *cobra.Command, args []string) {
|
||||
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)
|
||||
}
|
||||
fmt.Printf("Exported to %s\n", args[0])
|
||||
@@ -727,7 +814,7 @@ func runClipMigrate(cmd *cobra.Command, args []string) {
|
||||
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,
|
||||
Timeout: 1 * time.Second,
|
||||
})
|
||||
@@ -795,3 +882,113 @@ func detectMimeType(data []byte) string {
|
||||
func btoi(v []byte) uint64 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -524,5 +524,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
chromaCmd,
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
dlCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/brightness"
|
||||
@@ -101,11 +102,13 @@ var doctorCmd = &cobra.Command{
|
||||
var (
|
||||
doctorVerbose bool
|
||||
doctorJSON bool
|
||||
doctorCopy bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
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(&doctorCopy, "copy", "C", false, "Copy results to clipboard in GitHub-friendly format")
|
||||
}
|
||||
|
||||
type category int
|
||||
@@ -192,7 +195,7 @@ func (r checkResult) toJSON() checkResultJSON {
|
||||
}
|
||||
|
||||
func runDoctor(cmd *cobra.Command, args []string) {
|
||||
if !doctorJSON {
|
||||
if !doctorJSON && !doctorCopy {
|
||||
printDoctorHeader()
|
||||
}
|
||||
|
||||
@@ -210,9 +213,17 @@ func runDoctor(cmd *cobra.Command, args []string) {
|
||||
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)
|
||||
} else {
|
||||
default:
|
||||
printResults(results)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -638,6 +649,109 @@ func checkI2CAvailability() checkResult {
|
||||
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"
|
||||
|
||||
pluginDir := findQtPluginDir()
|
||||
if pluginDir == "" {
|
||||
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},
|
||||
}
|
||||
}
|
||||
|
||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||
|
||||
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
|
||||
for _, p := range c.plugins {
|
||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||
found = append(found, p.format)
|
||||
}
|
||||
}
|
||||
|
||||
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, ", "), imageFormatsDir)
|
||||
}
|
||||
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func findQtPluginDir() string {
|
||||
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
|
||||
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||
for dir := range strings.SplitSeq(envPath, ":") {
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try qtpaths
|
||||
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||
if dir := strings.TrimSpace(string(output)); dir != "" {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||
switch stackResult.Backend {
|
||||
case network.BackendNetworkManager:
|
||||
@@ -678,7 +792,21 @@ func checkOptionalDependencies() []checkResult {
|
||||
logindStatus, logindMsg := getOptionalDBusStatus("org.freedesktop.login1")
|
||||
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, checkImageFormatPlugins()...)
|
||||
|
||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
||||
@@ -752,7 +880,7 @@ func checkConfigurationFiles() []checkResult {
|
||||
|
||||
status := statusOK
|
||||
message := "Present"
|
||||
if info.Mode().Perm()&0200 == 0 {
|
||||
if info.Mode().Perm()&0o200 == 0 {
|
||||
status = statusWarn
|
||||
message += " (read-only)"
|
||||
}
|
||||
@@ -929,3 +1057,36 @@ func printSummary(results []checkResult, qsMissingFeatures bool) {
|
||||
}
|
||||
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")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("**%s**\n", r.category.String()))
|
||||
currentCategory = r.category
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("- [%s] %s: %s\n", r.status, r.name, r.message))
|
||||
|
||||
if doctorVerbose && r.details != "" {
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", r.details))
|
||||
}
|
||||
}
|
||||
|
||||
var ds DoctorStatus
|
||||
for _, r := range results {
|
||||
ds.Add(r)
|
||||
}
|
||||
|
||||
sb.WriteString("\n---\n")
|
||||
sb.WriteString(fmt.Sprintf("**Summary:** %d error(s), %d warning(s), %d ok\n",
|
||||
ds.ErrorCount(), ds.WarningCount(), ds.OKCount()))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
99
core/cmd/dms/commands_download.go
Normal file
99
core/cmd/dms/commands_download.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var dlOutput string
|
||||
var dlUserAgent string
|
||||
var dlTimeout int
|
||||
var dlIPv4Only bool
|
||||
|
||||
var dlCmd = &cobra.Command{
|
||||
Use: "dl <url>",
|
||||
Short: "Download a URL to stdout or file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := runDownload(args[0]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
dlCmd.Flags().StringVarP(&dlOutput, "output", "o", "", "Output file path (default: stdout)")
|
||||
dlCmd.Flags().StringVar(&dlUserAgent, "user-agent", "", "Custom User-Agent header")
|
||||
dlCmd.Flags().IntVar(&dlTimeout, "timeout", 10, "Request timeout in seconds")
|
||||
dlCmd.Flags().BoolVarP(&dlIPv4Only, "ipv4", "4", false, "Force IPv4 only")
|
||||
}
|
||||
|
||||
func runDownload(url string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(dlTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case dlUserAgent != "":
|
||||
req.Header.Set("User-Agent", dlUserAgent)
|
||||
default:
|
||||
req.Header.Set("User-Agent", "DankMaterialShell/1.0 (Linux)")
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
transport := &http.Transport{DialContext: dialer.DialContext}
|
||||
if dlIPv4Only {
|
||||
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp4", addr)
|
||||
}
|
||||
}
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("download failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if dlOutput == "" {
|
||||
_, err = io.Copy(os.Stdout, resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
if dir := filepath.Dir(dlOutput); dir != "." {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return fmt.Errorf("mkdir failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
f, err := os.Create(dlOutput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create failed: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
os.Remove(dlOutput)
|
||||
return fmt.Errorf("write failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(dlOutput)
|
||||
return nil
|
||||
}
|
||||
@@ -119,7 +119,7 @@ func installGreeter() error {
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, selectedCompositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -147,12 +147,30 @@ func syncGreeter() error {
|
||||
}
|
||||
fmt.Printf("✓ Found DMS at: %s\n", dmsPath)
|
||||
|
||||
if !isGreeterEnabled() {
|
||||
fmt.Println("\n⚠ DMS greeter is not enabled in greetd config.")
|
||||
fmt.Print("Would you like to enable it now? (Y/n): ")
|
||||
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response != "n" && response != "no" {
|
||||
if err := enableGreeter(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("greeter must be enabled before syncing")
|
||||
}
|
||||
}
|
||||
|
||||
cacheDir := "/var/cache/dms-greeter"
|
||||
if _, err := os.Stat(cacheDir); os.IsNotExist(err) {
|
||||
return fmt.Errorf("greeter cache directory not found at %s\nPlease install the greeter first", cacheDir)
|
||||
}
|
||||
|
||||
greeterGroupExists := checkGroupExists("greeter")
|
||||
greeterGroup := greeter.DetectGreeterGroup()
|
||||
greeterGroupExists := utils.HasGroup(greeterGroup)
|
||||
if greeterGroupExists {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
@@ -165,36 +183,59 @@ func syncGreeter() error {
|
||||
return fmt.Errorf("failed to check groups: %w", err)
|
||||
}
|
||||
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), "greeter")
|
||||
inGreeterGroup := strings.Contains(string(groupsOutput), greeterGroup)
|
||||
if !inGreeterGroup {
|
||||
fmt.Println("\n⚠ Warning: You are not in the greeter group.")
|
||||
fmt.Print("Would you like to add your user to the greeter group? (y/N): ")
|
||||
fmt.Printf("\n⚠ Warning: You are not in the %s group.\n", greeterGroup)
|
||||
fmt.Printf("Would you like to add your user to the %s group? (Y/n): ", greeterGroup)
|
||||
|
||||
var response string
|
||||
fmt.Scanln(&response)
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
|
||||
if response == "y" || response == "yes" {
|
||||
fmt.Println("\nAdding user to greeter group...")
|
||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", "greeter", currentUser.Username)
|
||||
if response != "n" && response != "no" {
|
||||
fmt.Printf("\nAdding user to %s group...\n", greeterGroup)
|
||||
addUserCmd := exec.Command("sudo", "usermod", "-aG", greeterGroup, currentUser.Username)
|
||||
addUserCmd.Stdout = os.Stdout
|
||||
addUserCmd.Stderr = os.Stderr
|
||||
if err := addUserCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to add user to greeter group: %w", err)
|
||||
return fmt.Errorf("failed to add user to %s group: %w", greeterGroup, err)
|
||||
}
|
||||
fmt.Println("✓ User added to greeter group")
|
||||
fmt.Printf("✓ User added to %s group\n", greeterGroup)
|
||||
fmt.Println("⚠ You will need to log out and back in for the group change to take effect")
|
||||
} else {
|
||||
return fmt.Errorf("aborted: user must be in the greeter group before syncing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compositor := detectConfiguredCompositor()
|
||||
if compositor == "" {
|
||||
compositors := greeter.DetectCompositors()
|
||||
switch len(compositors) {
|
||||
case 0:
|
||||
return fmt.Errorf("no supported compositors found")
|
||||
case 1:
|
||||
compositor = compositors[0]
|
||||
fmt.Printf("✓ Using compositor: %s\n", compositor)
|
||||
default:
|
||||
var err error
|
||||
compositor, err = promptCompositorChoice(compositors)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("✓ Selected compositor: %s\n", compositor)
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("✓ Detected compositor from config: %s\n", compositor)
|
||||
}
|
||||
|
||||
fmt.Println("\nSetting up permissions and ACLs...")
|
||||
if err := greeter.SetupDMSGroup(logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("\nSynchronizing DMS configurations...")
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, logFunc, ""); err != nil {
|
||||
if err := greeter.SyncDMSConfigs(dmsPath, compositor, logFunc, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -205,21 +246,6 @@ func syncGreeter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkGroupExists(groupName string) bool {
|
||||
data, err := os.ReadFile("/etc/group")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
lines := strings.SplitSeq(string(data), "\n")
|
||||
for line := range lines {
|
||||
if strings.HasPrefix(line, groupName+":") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func disableDisplayManager(dmName string) (bool, error) {
|
||||
state, err := getSystemdServiceState(dmName)
|
||||
if err != nil {
|
||||
@@ -351,7 +377,7 @@ func ensureGraphicalTarget() error {
|
||||
func handleConflictingDisplayManagers() error {
|
||||
fmt.Println("\n=== Checking for Conflicting Display Managers ===")
|
||||
|
||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm"}
|
||||
conflictingDMs := []string{"gdm", "gdm3", "lightdm", "sddm", "lxdm", "xdm", "cosmic-greeter"}
|
||||
|
||||
disabledAny := false
|
||||
var errors []string
|
||||
@@ -552,6 +578,39 @@ func enableGreeter() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isGreeterEnabled() bool {
|
||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(data), "dms-greeter")
|
||||
}
|
||||
|
||||
func detectConfiguredCompositor() string {
|
||||
data, err := os.ReadFile("/etc/greetd/config.toml")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "command") || !strings.Contains(trimmed, "dms-greeter") {
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(trimmed, "--command niri"):
|
||||
return "niri"
|
||||
case strings.Contains(trimmed, "--command hyprland"):
|
||||
return "hyprland"
|
||||
case strings.Contains(trimmed, "--command sway"):
|
||||
return "sway"
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func promptCompositorChoice(compositors []string) (string, error) {
|
||||
fmt.Println("\nMultiple compositors detected:")
|
||||
for i, comp := range compositors {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
"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().Int("cooldown-ms", 0, "Cooldown in milliseconds")
|
||||
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("flags", "", "Hyprland bind flags (e.g., 'e' for repeat, 'l' for locked, 'r' for release)")
|
||||
|
||||
@@ -81,24 +83,35 @@ func init() {
|
||||
func initializeProviders() {
|
||||
registry := keybinds.GetDefaultRegistry()
|
||||
|
||||
hyprlandProvider := providers.NewHyprlandProvider("$HOME/.config/hypr")
|
||||
hyprlandProvider := providers.NewHyprlandProvider("")
|
||||
if err := registry.Register(hyprlandProvider); err != nil {
|
||||
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 {
|
||||
log.Warnf("Failed to register MangoWC provider: %v", err)
|
||||
}
|
||||
|
||||
scrollProvider := providers.NewSwayProvider("$HOME/.config/scroll")
|
||||
if err := registry.Register(scrollProvider); err != nil {
|
||||
log.Warnf("Failed to register Scroll provider: %v", err)
|
||||
configDir, _ := os.UserConfigDir()
|
||||
|
||||
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")
|
||||
if err := registry.Register(swayProvider); err != nil {
|
||||
log.Warnf("Failed to register Sway provider: %v", err)
|
||||
miracleProvider := providers.NewMiracleProvider("")
|
||||
if err := registry.Register(miracleProvider); err != nil {
|
||||
log.Warnf("Failed to register Miracle WM provider: %v", err)
|
||||
}
|
||||
|
||||
if configDir != "" {
|
||||
swayProvider := providers.NewSwayProvider(filepath.Join(configDir, "sway"))
|
||||
if err := registry.Register(swayProvider); err != nil {
|
||||
log.Warnf("Failed to register Sway provider: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
niriProvider := providers.NewNiriProvider("")
|
||||
@@ -143,6 +156,8 @@ func makeProviderWithPath(name, path string) keybinds.Provider {
|
||||
return providers.NewSwayProvider(path)
|
||||
case "scroll":
|
||||
return providers.NewSwayProvider(path)
|
||||
case "miracle":
|
||||
return providers.NewMiracleProvider(path)
|
||||
case "niri":
|
||||
return providers.NewNiriProvider(path)
|
||||
default:
|
||||
@@ -212,6 +227,9 @@ func runKeybindsSet(cmd *cobra.Command, args []string) {
|
||||
if v, _ := cmd.Flags().GetBool("no-repeat"); v {
|
||||
options["repeat"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetBool("no-inhibiting"); v {
|
||||
options["allow-inhibiting"] = false
|
||||
}
|
||||
if v, _ := cmd.Flags().GetString("flags"); v != "" {
|
||||
options["flags"] = v
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ssOutputName string
|
||||
ssIncludeCursor bool
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
ssOutputDir string
|
||||
ssFilename string
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssStdout bool
|
||||
ssOutputName string
|
||||
ssCursor string
|
||||
ssFormat string
|
||||
ssQuality int
|
||||
ssOutputDir string
|
||||
ssFilename string
|
||||
ssNoClipboard bool
|
||||
ssNoFile bool
|
||||
ssNoNotify bool
|
||||
ssStdout bool
|
||||
)
|
||||
|
||||
var screenshotCmd = &cobra.Command{
|
||||
@@ -52,7 +52,7 @@ Examples:
|
||||
dms screenshot last # Last region (pre-selected)
|
||||
dms screenshot --no-clipboard # Save file only
|
||||
dms screenshot --no-file # Clipboard only
|
||||
dms screenshot --cursor # Include cursor
|
||||
dms screenshot --cursor=on # Include cursor
|
||||
dms screenshot -f jpg -q 85 # JPEG with quality 85`,
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ var notifyActionCmd = &cobra.Command{
|
||||
|
||||
func init() {
|
||||
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().IntVarP(&ssQuality, "quality", "q", 90, "JPEG quality (1-100)")
|
||||
screenshotCmd.PersistentFlags().StringVarP(&ssOutputDir, "dir", "d", "", "Output directory")
|
||||
@@ -136,7 +136,9 @@ func getScreenshotConfig(mode screenshot.Mode) screenshot.Config {
|
||||
config := screenshot.DefaultConfig()
|
||||
config.Mode = mode
|
||||
config.OutputName = ssOutputName
|
||||
config.IncludeCursor = ssIncludeCursor
|
||||
if strings.EqualFold(ssCursor, "on") {
|
||||
config.Cursor = screenshot.CursorOn
|
||||
}
|
||||
config.Clipboard = !ssNoClipboard
|
||||
config.SaveFile = !ssNoFile
|
||||
config.Notify = !ssNoNotify
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"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/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -24,6 +26,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 {
|
||||
fmt.Println("=== DMS Configuration Setup ===")
|
||||
|
||||
|
||||
338
core/cmd/dms/commands_windowrules.go
Normal file
338
core/cmd/dms/commands_windowrules.go
Normal file
@@ -0,0 +1,338 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/windowrules/providers"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var windowrulesCmd = &cobra.Command{
|
||||
Use: "windowrules",
|
||||
Short: "Manage window rules",
|
||||
}
|
||||
|
||||
var windowrulesListCmd = &cobra.Command{
|
||||
Use: "list [compositor]",
|
||||
Short: "List all window rules",
|
||||
Long: "List all window rules from compositor config file. Returns JSON with rules and DMS status.",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesList,
|
||||
}
|
||||
|
||||
var windowrulesAddCmd = &cobra.Command{
|
||||
Use: "add <compositor> '<json>'",
|
||||
Short: "Add a window rule to DMS file",
|
||||
Long: "Add a new window rule to the DMS-managed rules file.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
// ! disabled hyprland return []string{"hyprland", "niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesAdd,
|
||||
}
|
||||
|
||||
var windowrulesUpdateCmd = &cobra.Command{
|
||||
Use: "update <compositor> <id> '<json>'",
|
||||
Short: "Update a window rule in DMS file",
|
||||
Long: "Update an existing window rule in the DMS-managed rules file.",
|
||||
Args: cobra.ExactArgs(3),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesUpdate,
|
||||
}
|
||||
|
||||
var windowrulesRemoveCmd = &cobra.Command{
|
||||
Use: "remove <compositor> <id>",
|
||||
Short: "Remove a window rule from DMS file",
|
||||
Long: "Remove a window rule from the DMS-managed rules file.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesRemove,
|
||||
}
|
||||
|
||||
var windowrulesReorderCmd = &cobra.Command{
|
||||
Use: "reorder <compositor> '<json-array-of-ids>'",
|
||||
Short: "Reorder window rules in DMS file",
|
||||
Long: "Reorder window rules by providing a JSON array of rule IDs in the desired order.",
|
||||
Args: cobra.ExactArgs(2),
|
||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
if len(args) == 0 {
|
||||
return []string{"niri"}, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
return nil, cobra.ShellCompDirectiveNoFileComp
|
||||
},
|
||||
Run: runWindowrulesReorder,
|
||||
}
|
||||
|
||||
func init() {
|
||||
configCmd.AddCommand(windowrulesCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesListCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesAddCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesUpdateCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesRemoveCmd)
|
||||
windowrulesCmd.AddCommand(windowrulesReorderCmd)
|
||||
}
|
||||
|
||||
type WindowRulesListResult struct {
|
||||
Rules []windowrules.WindowRule `json:"rules"`
|
||||
DMSStatus *windowrules.DMSRulesStatus `json:"dmsStatus,omitempty"`
|
||||
}
|
||||
|
||||
type WindowRuleWriteResult struct {
|
||||
Success bool `json:"success"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func getCompositor(args []string) string {
|
||||
if len(args) > 0 {
|
||||
return strings.ToLower(args[0])
|
||||
}
|
||||
if os.Getenv("NIRI_SOCKET") != "" {
|
||||
return "niri"
|
||||
}
|
||||
// if os.Getenv("HYPRLAND_INSTANCE_SIGNATURE") != "" {
|
||||
// return "hyprland"
|
||||
// }
|
||||
return ""
|
||||
}
|
||||
|
||||
func writeRuleError(errMsg string) {
|
||||
result := WindowRuleWriteResult{Success: false, Error: errMsg}
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func writeRuleSuccess(id, path string) {
|
||||
result := WindowRuleWriteResult{Success: true, ID: id, Path: path}
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func runWindowrulesList(cmd *cobra.Command, args []string) {
|
||||
compositor := getCompositor(args)
|
||||
if compositor == "" {
|
||||
log.Fatalf("Could not detect compositor. Please specify: hyprland or niri")
|
||||
}
|
||||
|
||||
var result WindowRulesListResult
|
||||
|
||||
switch compositor {
|
||||
case "niri":
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to expand niri config path: %v", err)
|
||||
}
|
||||
|
||||
parseResult, err := providers.ParseNiriWindowRules(configDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse niri window rules: %v", err)
|
||||
}
|
||||
|
||||
allRules := providers.ConvertNiriRulesToWindowRules(parseResult.Rules)
|
||||
|
||||
provider := providers.NewNiriWritableProvider(configDir)
|
||||
dmsRulesPath := provider.GetOverridePath()
|
||||
dmsRules, _ := provider.LoadDMSRules()
|
||||
|
||||
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||
for i, dr := range dmsRules {
|
||||
dmsRuleMap[i] = dr
|
||||
}
|
||||
|
||||
dmsIdx := 0
|
||||
for i, r := range allRules {
|
||||
if r.Source == dmsRulesPath {
|
||||
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||
allRules[i].ID = dmr.ID
|
||||
allRules[i].Name = dmr.Name
|
||||
}
|
||||
dmsIdx++
|
||||
}
|
||||
}
|
||||
|
||||
result.Rules = allRules
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
case "hyprland":
|
||||
log.Fatalf("Hyprland support is currently disabled.") // ! disabled hyprland
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to expand hyprland config path: %v", err)
|
||||
}
|
||||
|
||||
parseResult, err := providers.ParseHyprlandWindowRules(configDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse hyprland window rules: %v", err)
|
||||
}
|
||||
|
||||
allRules := providers.ConvertHyprlandRulesToWindowRules(parseResult.Rules)
|
||||
|
||||
provider := providers.NewHyprlandWritableProvider(configDir)
|
||||
dmsRulesPath := provider.GetOverridePath()
|
||||
dmsRules, _ := provider.LoadDMSRules()
|
||||
|
||||
dmsRuleMap := make(map[int]windowrules.WindowRule)
|
||||
for i, dr := range dmsRules {
|
||||
dmsRuleMap[i] = dr
|
||||
}
|
||||
|
||||
dmsIdx := 0
|
||||
for i, r := range allRules {
|
||||
if r.Source == dmsRulesPath {
|
||||
if dmr, ok := dmsRuleMap[dmsIdx]; ok {
|
||||
allRules[i].ID = dmr.ID
|
||||
allRules[i].Name = dmr.Name
|
||||
}
|
||||
dmsIdx++
|
||||
}
|
||||
}
|
||||
|
||||
result.Rules = allRules
|
||||
result.DMSStatus = parseResult.DMSStatus
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown compositor: %s", compositor)
|
||||
}
|
||||
|
||||
output, _ := json.Marshal(result)
|
||||
fmt.Fprintln(os.Stdout, string(output))
|
||||
}
|
||||
|
||||
func runWindowrulesAdd(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
ruleJSON := args[1]
|
||||
|
||||
var rule windowrules.WindowRule
|
||||
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||
}
|
||||
|
||||
if rule.ID == "" {
|
||||
rule.ID = generateRuleID()
|
||||
}
|
||||
rule.Enabled = true
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.SetRule(rule); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func runWindowrulesUpdate(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
ruleID := args[1]
|
||||
ruleJSON := args[2]
|
||||
|
||||
var rule windowrules.WindowRule
|
||||
if err := json.Unmarshal([]byte(ruleJSON), &rule); err != nil {
|
||||
writeRuleError(fmt.Sprintf("Invalid JSON: %v", err))
|
||||
}
|
||||
|
||||
rule.ID = ruleID
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.SetRule(rule); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess(rule.ID, provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func runWindowrulesRemove(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
ruleID := args[1]
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.RemoveRule(ruleID); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess(ruleID, provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func runWindowrulesReorder(cmd *cobra.Command, args []string) {
|
||||
compositor := strings.ToLower(args[0])
|
||||
idsJSON := args[1]
|
||||
|
||||
var ids []string
|
||||
if err := json.Unmarshal([]byte(idsJSON), &ids); err != nil {
|
||||
writeRuleError(fmt.Sprintf("Invalid JSON array: %v", err))
|
||||
}
|
||||
|
||||
provider := getWindowRulesProvider(compositor)
|
||||
if provider == nil {
|
||||
writeRuleError(fmt.Sprintf("Unknown compositor: %s", compositor))
|
||||
}
|
||||
|
||||
if err := provider.ReorderRules(ids); err != nil {
|
||||
writeRuleError(err.Error())
|
||||
}
|
||||
|
||||
writeRuleSuccess("", provider.GetOverridePath())
|
||||
}
|
||||
|
||||
func getWindowRulesProvider(compositor string) windowrules.WritableProvider {
|
||||
switch compositor {
|
||||
case "niri":
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/niri")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return providers.NewNiriWritableProvider(configDir)
|
||||
case "hyprland":
|
||||
configDir, err := utils.ExpandPath("$HOME/.config/hypr")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return providers.NewHyprlandWritableProvider(configDir)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateRuleID() string {
|
||||
return fmt.Sprintf("wr_%d", time.Now().UnixNano())
|
||||
}
|
||||
@@ -19,6 +19,9 @@ func init() {
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
// Add subcommands to update
|
||||
updateCmd.AddCommand(updateCheckCmd)
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ func init() {
|
||||
// Add subcommands to greeter
|
||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
||||
|
||||
// Add subcommands to setup
|
||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||
|
||||
// Add subcommands to plugins
|
||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ func runShellInteractive(session bool) {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||
}
|
||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||
}
|
||||
|
||||
cmd.Stdin = os.Stdin
|
||||
@@ -450,7 +450,7 @@ func runShellDaemon(session bool) {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORMTHEME_QT6=gtk3")
|
||||
}
|
||||
if os.Getenv("QT_QPA_PLATFORM") == "" {
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland")
|
||||
cmd.Env = append(cmd.Env, "QT_QPA_PLATFORM=wayland;xcb")
|
||||
}
|
||||
|
||||
devNull, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
|
||||
|
||||
45
core/go.mod
45
core/go.mod
@@ -1,11 +1,11 @@
|
||||
module github.com/AvengeMedia/DankMaterialShell/core
|
||||
|
||||
go 1.24.6
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/Wifx/gonetworkmanager/v2 v2.2.0
|
||||
github.com/alecthomas/chroma/v2 v2.17.2
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.1
|
||||
github.com/charmbracelet/bubbles v1.0.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
@@ -13,50 +13,49 @@ require (
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/holoplot/go-evdev v0.0.0-20250804134636-ab1d56a1fe83
|
||||
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/stretchr/testify v1.11.1
|
||||
github.com/yuin/goldmark v1.7.16
|
||||
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96
|
||||
golang.org/x/image v0.35.0
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a
|
||||
golang.org/x/image v0.36.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.7.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/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/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/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/stretchr/objx v0.5.3 // indirect
|
||||
golang.org/x/crypto v0.47.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 (
|
||||
github.com/atotto/clipboard v0.1.4 // 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/x/ansi v0.11.4 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // 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/lucasb-eyer/go-colorful v1.3.0
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
@@ -70,7 +69,11 @@ require (
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.33.0
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
golang.org/x/sys v0.41.0
|
||||
golang.org/x/text v0.34.0
|
||||
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
|
||||
|
||||
90
core/go.sum
90
core/go.sum
@@ -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/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.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI=
|
||||
github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
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/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/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||
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/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||
github.com/charmbracelet/colorprofile v0.4.2 h1:BdSNuMjRbotnxHSfxy+PCSa4xAmz7szw70ktAtWRYrY=
|
||||
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/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||
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/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
||||
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.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
|
||||
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
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.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
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/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
|
||||
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
|
||||
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/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
|
||||
@@ -66,12 +64,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
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/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-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
|
||||
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.20251229094738-4b14af179146/go.mod h1:QE/75B8tBSLNGyUUbA9tw3EGHoFtYOtypa2h8YJxsWI=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6 h1:Yo1MlE8LpvD0pr7mZ04b6hKZKQcPvLrQFgyY1jNMEyU=
|
||||
github.com/go-git/go-git/v6 v6.0.0-20260114124804-a8db3a6585a6/go.mod h1:enMzPHv+9hL4B7tH7OJGQKNzCkMzXovUoaiXfsLF7Xs=
|
||||
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-20260209124918-37866f83c2d3/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
|
||||
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.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
|
||||
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-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/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
@@ -88,8 +86,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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
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.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
|
||||
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
@@ -103,8 +101,8 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/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-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=
|
||||
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
@@ -124,8 +122,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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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-20251203232544-981d4ecc17c3/go.mod h1:b3oNGuAKOQzhsCKmuLc/urEOPzgHj6fB8vl8bwTBh28=
|
||||
github.com/sblinch/kdl-go v0.0.0-20260121213736-8b7053306ca6 h1:JsjzqC6ymELkN4XlTjZPSahSAem21GySugLbKz6uF5E=
|
||||
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/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -152,24 +150,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/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
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.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
|
||||
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
|
||||
golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
|
||||
golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
|
||||
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
|
||||
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
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 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -330,3 +330,163 @@ func selectPreferredMimeType(mimes []string) string {
|
||||
func IsImageMimeType(mime string) bool {
|
||||
return len(mime) > 6 && mime[:6] == "image/"
|
||||
}
|
||||
|
||||
type Offer struct {
|
||||
MimeType string
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func CopyMulti(offers []Offer, foreground, pasteOnce bool) error {
|
||||
if !foreground {
|
||||
return copyMultiFork(offers, pasteOnce)
|
||||
}
|
||||
return copyMultiServe(offers, pasteOnce)
|
||||
}
|
||||
|
||||
func copyMultiFork(offers []Offer, pasteOnce bool) error {
|
||||
args := []string{os.Args[0], "cl", "copy", "--foreground", "--type", "__multi__"}
|
||||
if pasteOnce {
|
||||
args = append(args, "--paste-once")
|
||||
}
|
||||
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Stdin = nil
|
||||
cmd.Stdout = nil
|
||||
cmd.Stderr = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
|
||||
for _, offer := range offers {
|
||||
fmt.Fprintf(stdin, "%s\x00%d\x00", offer.MimeType, len(offer.Data))
|
||||
if _, err := stdin.Write(offer.Data); err != nil {
|
||||
stdin.Close()
|
||||
return fmt.Errorf("write offer data: %w", err)
|
||||
}
|
||||
}
|
||||
stdin.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
ctx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(ctx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
source, err := dataControlMgr.CreateDataSource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create data source: %w", err)
|
||||
}
|
||||
|
||||
offerMap := make(map[string][]byte)
|
||||
for _, offer := range offers {
|
||||
if err := source.Offer(offer.MimeType); err != nil {
|
||||
return fmt.Errorf("offer %s: %w", offer.MimeType, err)
|
||||
}
|
||||
offerMap[offer.MimeType] = offer.Data
|
||||
}
|
||||
|
||||
cancelled := make(chan struct{})
|
||||
pasted := make(chan struct{}, 1)
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
defer syscall.Close(e.Fd)
|
||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||
defer file.Close()
|
||||
|
||||
if data, ok := offerMap[e.MimeType]; ok {
|
||||
file.Write(data)
|
||||
}
|
||||
|
||||
select {
|
||||
case pasted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||
close(cancelled)
|
||||
})
|
||||
|
||||
if err := device.SetSelection(source); err != nil {
|
||||
return fmt.Errorf("set selection: %w", err)
|
||||
}
|
||||
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-cancelled:
|
||||
return nil
|
||||
case <-pasted:
|
||||
if pasteOnce {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
if err := ctx.Dispatch(); err != nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func StoreWithConfig(data []byte, mimeType string, cfg StoreConfig) error {
|
||||
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 {
|
||||
return fmt.Errorf("open db: %w", err)
|
||||
}
|
||||
@@ -132,7 +132,7 @@ func GetDBPath() (string, error) {
|
||||
oldPath := filepath.Join(oldDir, "db")
|
||||
|
||||
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
|
||||
}
|
||||
if err := os.Rename(oldPath, newPath); err != nil {
|
||||
@@ -142,7 +142,7 @@ func GetDBPath() (string, error) {
|
||||
return newPath, nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(newDir, 0700); err != nil {
|
||||
if err := os.MkdirAll(newDir, 0o700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return newPath, nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package clipboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -12,8 +13,9 @@ import (
|
||||
)
|
||||
|
||||
type ClipboardChange struct {
|
||||
Data []byte
|
||||
MimeType string
|
||||
Data []byte
|
||||
MimeType string
|
||||
MimeTypes []string
|
||||
}
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func WatchAll(ctx context.Context, callback func(data []byte, mimeType string, allMimeTypes []string)) error {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("wayland connect: %w", err)
|
||||
}
|
||||
defer display.Destroy()
|
||||
|
||||
wlCtx := display.Context()
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return fmt.Errorf("get registry: %w", err)
|
||||
}
|
||||
defer registry.Destroy()
|
||||
|
||||
var dataControlMgr *ext_data_control.ExtDataControlManagerV1
|
||||
var seat *wlclient.Seat
|
||||
var bindErr error
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
switch e.Interface {
|
||||
case "ext_data_control_manager_v1":
|
||||
dataControlMgr = ext_data_control.NewExtDataControlManagerV1(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, dataControlMgr); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
case "wl_seat":
|
||||
if seat != nil {
|
||||
return
|
||||
}
|
||||
seat = wlclient.NewSeat(wlCtx)
|
||||
if err := registry.Bind(e.Name, e.Interface, e.Version, seat); err != nil {
|
||||
bindErr = err
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
if bindErr != nil {
|
||||
return fmt.Errorf("registry bind: %w", bindErr)
|
||||
}
|
||||
if dataControlMgr == nil {
|
||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||
}
|
||||
defer dataControlMgr.Destroy()
|
||||
if seat == nil {
|
||||
return fmt.Errorf("no seat available")
|
||||
}
|
||||
|
||||
device, err := dataControlMgr.GetDataDevice(seat)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get data device: %w", err)
|
||||
}
|
||||
defer device.Destroy()
|
||||
|
||||
offerMimeTypes := make(map[*ext_data_control.ExtDataControlOfferV1][]string)
|
||||
|
||||
device.SetDataOfferHandler(func(e ext_data_control.ExtDataControlDeviceV1DataOfferEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
offerMimeTypes[e.Id] = nil
|
||||
e.Id.SetOfferHandler(func(me ext_data_control.ExtDataControlOfferV1OfferEvent) {
|
||||
offerMimeTypes[e.Id] = append(offerMimeTypes[e.Id], me.MimeType)
|
||||
})
|
||||
})
|
||||
|
||||
device.SetSelectionHandler(func(e ext_data_control.ExtDataControlDeviceV1SelectionEvent) {
|
||||
if e.Id == nil {
|
||||
return
|
||||
}
|
||||
|
||||
mimes := offerMimeTypes[e.Id]
|
||||
selectedMime := selectPreferredMimeType(mimes)
|
||||
if selectedMime == "" {
|
||||
return
|
||||
}
|
||||
|
||||
mimesCopy := make([]string, len(mimes))
|
||||
copy(mimesCopy, mimes)
|
||||
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := e.Id.Receive(selectedMime, int(w.Fd())); err != nil {
|
||||
w.Close()
|
||||
r.Close()
|
||||
return
|
||||
}
|
||||
w.Close()
|
||||
|
||||
go func() {
|
||||
defer r.Close()
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil || len(data) == 0 {
|
||||
return
|
||||
}
|
||||
callback(data, selectedMime, mimesCopy)
|
||||
}()
|
||||
})
|
||||
|
||||
display.Roundtrip()
|
||||
display.Roundtrip()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
if err := wlCtx.SetReadDeadline(time.Now().Add(100 * time.Millisecond)); err != nil {
|
||||
return fmt.Errorf("set read deadline: %w", err)
|
||||
}
|
||||
if err := wlCtx.Dispatch(); err != nil {
|
||||
if isTimeoutError(err) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("dispatch: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTimeoutError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
return true
|
||||
}
|
||||
if netErr, ok := err.(interface{ Timeout() bool }); ok && netErr.Timeout() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func WatchChan(ctx context.Context) (<-chan ClipboardChange, <-chan error) {
|
||||
ch := make(chan ClipboardChange, 16)
|
||||
errCh := make(chan error, 1)
|
||||
|
||||
@@ -126,13 +126,13 @@ func (cd *ConfigDeployer) deployNiriConfig(terminal deps.Terminal, useSystemd bo
|
||||
}
|
||||
|
||||
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)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
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)
|
||||
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)
|
||||
return result, result.Error
|
||||
}
|
||||
@@ -211,6 +211,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e
|
||||
{"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||
{"outputs.kdl", ""},
|
||||
{"cursor.kdl", ""},
|
||||
{"windowrules.kdl", ""},
|
||||
}
|
||||
|
||||
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))
|
||||
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)
|
||||
}
|
||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||
@@ -238,7 +239,7 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
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)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
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)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
@@ -276,12 +277,12 @@ func (cd *ConfigDeployer) deployGhosttyConfig() ([]DeploymentResult, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
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)
|
||||
return results, colorResult.Error
|
||||
}
|
||||
@@ -302,7 +303,7 @@ func (cd *ConfigDeployer) deployKittyConfig() ([]DeploymentResult, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
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)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
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)
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
return results, tabsResult.Error
|
||||
}
|
||||
@@ -374,7 +375,7 @@ func (cd *ConfigDeployer) deployAlacrittyConfig() ([]DeploymentResult, error) {
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
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)
|
||||
return []DeploymentResult{mainResult}, mainResult.Error
|
||||
}
|
||||
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)
|
||||
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"),
|
||||
}
|
||||
|
||||
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)
|
||||
return results, themeResult.Error
|
||||
}
|
||||
@@ -438,7 +439,7 @@ func (cd *ConfigDeployer) mergeNiriOutputSections(newConfig, existingConfig, dms
|
||||
outputsContent.WriteString(output)
|
||||
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))
|
||||
} else {
|
||||
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)
|
||||
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)
|
||||
return result, result.Error
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
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)
|
||||
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)
|
||||
return result, result.Error
|
||||
}
|
||||
@@ -563,6 +564,7 @@ func (cd *ConfigDeployer) deployHyprlandDmsConfigs(dmsDir string, terminalComman
|
||||
{"binds.conf", strings.ReplaceAll(HyprBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)},
|
||||
{"outputs.conf", ""},
|
||||
{"cursor.conf", ""},
|
||||
{"windowrules.conf", ""},
|
||||
}
|
||||
|
||||
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))
|
||||
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)
|
||||
}
|
||||
cd.log(fmt.Sprintf("Deployed %s", cfg.name))
|
||||
@@ -596,7 +598,7 @@ func (cd *ConfigDeployer) mergeHyprlandMonitorSections(newConfig, existingConfig
|
||||
outputsContent.WriteString(monitor)
|
||||
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))
|
||||
} else {
|
||||
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") {
|
||||
startupSectionFound = true
|
||||
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 = QT_QPA_PLATFORMTHEME,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") {
|
||||
insertLines := []string{
|
||||
"exec-once = dms run",
|
||||
"env = QT_QPA_PLATFORM,wayland",
|
||||
"env = QT_QPA_PLATFORM,wayland;xcb",
|
||||
"env = ELECTRON_OZONE_PLATFORM_HINT,auto",
|
||||
"env = QT_QPA_PLATFORMTHEME,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 {
|
||||
envVars := fmt.Sprintf(`environment {
|
||||
XDG_CURRENT_DESKTOP "niri"
|
||||
QT_QPA_PLATFORM "wayland"
|
||||
QT_QPA_PLATFORM "wayland;xcb"
|
||||
ELECTRON_OZONE_PLATFORM_HINT "auto"
|
||||
QT_QPA_PLATFORMTHEME "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) {
|
||||
existingContent := "# Old config\nfont-size = 14\n"
|
||||
ghosttyPath := getGhosttyPath()
|
||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0755)
|
||||
err := os.MkdirAll(filepath.Dir(ghosttyPath), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0644)
|
||||
err = os.WriteFile(ghosttyPath, []byte(existingContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
results, err := cd.deployGhosttyConfig()
|
||||
@@ -422,9 +422,9 @@ general {
|
||||
}
|
||||
`
|
||||
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)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0644)
|
||||
err = os.WriteFile(hyprPath, []byte(existingContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
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) {
|
||||
existingContent := "# Old alacritty config\n[window]\nopacity = 0.9\n"
|
||||
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)
|
||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0644)
|
||||
err = os.WriteFile(alacrittyPath, []byte(existingContent), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
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 = , XF86AudioPrev, exec, dms ipc call mpris previous
|
||||
bindl = , XF86AudioNext, exec, dms ipc call mpris next
|
||||
bindel = CTRL, XF86AudioRaiseVolume, exec, dms ipc call mpris increment 3
|
||||
bindel = CTRL, XF86AudioLowerVolume, exec, dms ipc call mpris decrement 3
|
||||
|
||||
# === Brightness Controls ===
|
||||
bindel = , XF86MonBrightnessUp, exec, dms ipc call brightness increment 5 ""
|
||||
@@ -38,6 +40,7 @@ bind = SUPER, F, fullscreen, 1
|
||||
bind = SUPER SHIFT, F, fullscreen, 0
|
||||
bind = SUPER SHIFT, T, togglefloating
|
||||
bind = SUPER, W, togglegroup
|
||||
bind = SUPER SHIFT, W, exec, dms ipc call window-rules toggle
|
||||
|
||||
# === Focus Navigation ===
|
||||
bind = SUPER, left, movefocus, l
|
||||
|
||||
@@ -81,7 +81,6 @@ master {
|
||||
misc {
|
||||
disable_hyprland_logo = 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 ^(blueman-manager)$
|
||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||
windowrule = float on, match:class ^(steam)$
|
||||
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 ^(zoom)$
|
||||
|
||||
@@ -110,6 +111,7 @@ windowrule = float on, match:class ^(zoom)$
|
||||
# windowrule = float on, match:class ^(org.quickshell)$
|
||||
|
||||
layerrule = no_anim on, match:namespace ^(quickshell)$
|
||||
layerrule = no_anim on, match:namespace ^dms:.*
|
||||
|
||||
source = ./dms/colors.conf
|
||||
source = ./dms/outputs.conf
|
||||
|
||||
@@ -60,6 +60,12 @@ binds {
|
||||
XF86AudioNext allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "next";
|
||||
}
|
||||
Ctrl+XF86AudioRaiseVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "increment" "3";
|
||||
}
|
||||
Ctrl+XF86AudioLowerVolume allow-when-locked=true {
|
||||
spawn "dms" "ipc" "call" "mpris" "decrement" "3";
|
||||
}
|
||||
|
||||
// === Brightness Controls ===
|
||||
XF86MonBrightnessUp allow-when-locked=true {
|
||||
@@ -76,6 +82,7 @@ binds {
|
||||
Mod+Shift+T { toggle-window-floating; }
|
||||
Mod+Shift+V { switch-focus-between-floating-and-tiling; }
|
||||
Mod+W { toggle-column-tabbed-display; }
|
||||
Mod+Shift+W hotkey-overlay-title="Create window rule" { spawn "dms" "ipc" "call" "window-rules" "toggle"; }
|
||||
|
||||
// === Focus Navigation ===
|
||||
Mod+Left { focus-column-left; }
|
||||
|
||||
17
core/internal/config/embedded/niri-greeter.kdl
Normal file
17
core/internal/config/embedded/niri-greeter.kdl
Normal file
@@ -0,0 +1,17 @@
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
environment {
|
||||
DMS_RUN_GREETER "1"
|
||||
}
|
||||
|
||||
gestures {
|
||||
hot-corners {
|
||||
off
|
||||
}
|
||||
}
|
||||
|
||||
layout {
|
||||
background-color "#000000"
|
||||
}
|
||||
@@ -228,10 +228,14 @@ window-rule {
|
||||
match app-id=r#"^galculator$"#
|
||||
match app-id=r#"^blueman-manager$"#
|
||||
match app-id=r#"^org\.gnome\.Nautilus$"#
|
||||
match app-id=r#"^steam$"#
|
||||
match app-id=r#"^xdg-desktop-portal$"#
|
||||
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 {
|
||||
match app-id=r#"^org\.wezfurlong\.wezterm$"#
|
||||
match app-id="Alacritty"
|
||||
|
||||
@@ -16,3 +16,6 @@ var NiriAlttabConfig string
|
||||
|
||||
//go:embed embedded/niri-binds.kdl
|
||||
var NiriBindsConfig string
|
||||
|
||||
//go:embed embedded/niri-greeter.kdl
|
||||
var NiriGreeterConfig string
|
||||
|
||||
@@ -26,6 +26,9 @@ func init() {
|
||||
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
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 {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
@@ -41,6 +44,9 @@ func init() {
|
||||
Register("artix", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
Register("XeroLinux", "#888fe2", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||
return NewArchDistribution(config, logChan)
|
||||
})
|
||||
}
|
||||
|
||||
type ArchDistribution struct {
|
||||
|
||||
@@ -534,7 +534,7 @@ func (b *BaseDistribution) WriteEnvironmentConfig(terminal deps.Terminal) error
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -555,7 +555,7 @@ TERMINAL=%s
|
||||
`, terminalCmd)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@ func (b *BaseDistribution) WriteHyprlandSessionTarget() error {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -605,7 +605,7 @@ Requires=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)
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestBaseDistribution_detectDMS_Installed(t *testing.T) {
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
os.MkdirAll(dmsPath, 0o755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
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()
|
||||
|
||||
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, "commit", "-m", "initial").Run()
|
||||
|
||||
@@ -87,7 +87,7 @@ func TestBaseDistribution_detectDMS_NeedsUpdate(t *testing.T) {
|
||||
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
os.MkdirAll(dmsPath, 0o755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
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()
|
||||
|
||||
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, "commit", "-m", "initial").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) {
|
||||
tempDir := t.TempDir()
|
||||
dmsPath := filepath.Join(tempDir, ".config", "quickshell", "dms")
|
||||
os.MkdirAll(dmsPath, 0755)
|
||||
os.MkdirAll(dmsPath, 0o755)
|
||||
|
||||
originalHome := os.Getenv("HOME")
|
||||
defer os.Setenv("HOME", originalHome)
|
||||
|
||||
@@ -430,7 +430,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
||||
}
|
||||
|
||||
// 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, runtime.GOARCH, baseURL)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
|
||||
@@ -540,12 +540,12 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
@@ -576,7 +576,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,13 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/sblinch/kdl-go"
|
||||
"github.com/sblinch/kdl-go/document"
|
||||
)
|
||||
|
||||
// DetectDMSPath checks for DMS installation following XDG Base Directory specification
|
||||
@@ -19,6 +22,21 @@ func DetectDMSPath() (string, error) {
|
||||
return config.LocateDMSConfig()
|
||||
}
|
||||
|
||||
func DetectGreeterGroup() string {
|
||||
data, err := os.ReadFile("/etc/group")
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "⚠ Warning: could not read /etc/group, defaulting to greeter")
|
||||
return "greeter"
|
||||
}
|
||||
|
||||
if group, found := utils.FindGroupData(string(data), "greeter", "greetd", "_greeter"); found {
|
||||
return group
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "⚠ Warning: no greeter group found in /etc/group, defaulting to greeter")
|
||||
return "greeter"
|
||||
}
|
||||
|
||||
// DetectCompositors checks which compositors are installed
|
||||
func DetectCompositors() []string {
|
||||
var compositors []string
|
||||
@@ -191,14 +209,17 @@ func CopyGreeterFiles(dmsPath, compositor string, logFunc func(string), sudoPass
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chown", "greeter:greeter", cacheDir); err != nil {
|
||||
group := DetectGreeterGroup()
|
||||
owner := fmt.Sprintf("%s:%s", group, group)
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chown", owner, cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory owner: %w", err)
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "750", cacheDir); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "755", cacheDir); err != nil {
|
||||
return fmt.Errorf("failed to set cache directory permissions: %w", err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: greeter:greeter, permissions: 750)", cacheDir))
|
||||
logFunc(fmt.Sprintf("✓ Created cache directory %s (owner: %s, permissions: 755)", cacheDir, owner))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -231,20 +252,22 @@ func SetupParentDirectoryACLs(logFunc func(string), sudoPassword string) error {
|
||||
{filepath.Join(homeDir, ".local", "share"), ".local/share directory"},
|
||||
}
|
||||
|
||||
owner := DetectGreeterGroup()
|
||||
|
||||
logFunc("\nSetting up parent directory ACLs for greeter user access...")
|
||||
|
||||
for _, dir := range parentDirs {
|
||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir.path, 0o755); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Set ACL to allow greeter user read+execute permission (for session discovery)
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", "u:greeter:rx", dir.path); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "setfacl", "-m", fmt.Sprintf("u:%s:rx", owner), dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set ACL on %s: %v", dir.desc, err))
|
||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:greeter:x %s", dir.path))
|
||||
logFunc(fmt.Sprintf(" You may need to run manually: setfacl -m u:%s:x %s", owner, dir.path))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -268,17 +291,19 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
return fmt.Errorf("failed to determine current user")
|
||||
}
|
||||
|
||||
group := DetectGreeterGroup()
|
||||
|
||||
// Check if user is already in greeter group
|
||||
groupsCmd := exec.Command("groups", currentUser)
|
||||
groupsOutput, err := groupsCmd.Output()
|
||||
if err == nil && strings.Contains(string(groupsOutput), "greeter") {
|
||||
logFunc(fmt.Sprintf("✓ %s is already in greeter group", currentUser))
|
||||
if err == nil && strings.Contains(string(groupsOutput), group) {
|
||||
logFunc(fmt.Sprintf("✓ %s is already in %s group", currentUser, group))
|
||||
} else {
|
||||
// Add current user to greeter group for file access permissions
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", "greeter", currentUser); err != nil {
|
||||
return fmt.Errorf("failed to add %s to greeter group: %w", currentUser, err)
|
||||
if err := runSudoCmd(sudoPassword, "usermod", "-aG", group, currentUser); err != nil {
|
||||
return fmt.Errorf("failed to add %s to %s group: %w", currentUser, group, err)
|
||||
}
|
||||
logFunc(fmt.Sprintf("✓ Added %s to greeter group (logout/login required for changes to take effect)", currentUser))
|
||||
logFunc(fmt.Sprintf("✓ Added %s to %s group (logout/login required for changes to take effect)", currentUser, group))
|
||||
}
|
||||
|
||||
configDirs := []struct {
|
||||
@@ -295,13 +320,13 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
|
||||
for _, dir := range configDirs {
|
||||
if _, err := os.Stat(dir.path); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(dir.path, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir.path, 0o755); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", dir.path, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", "greeter", dir.path); err != nil {
|
||||
if err := runSudoCmd(sudoPassword, "chgrp", "-R", group, dir.path); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to set group for %s: %v", dir.desc, err))
|
||||
continue
|
||||
}
|
||||
@@ -322,7 +347,7 @@ func SetupDMSGroup(logFunc func(string), sudoPassword string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) error {
|
||||
func SyncDMSConfigs(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||
@@ -355,14 +380,14 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
|
||||
for _, link := range symlinks {
|
||||
sourceDir := filepath.Dir(link.source)
|
||||
if _, err := os.Stat(sourceDir); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create directory %s: %v", sourceDir, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(link.source); os.IsNotExist(err) {
|
||||
if err := os.WriteFile(link.source, []byte("{}"), 0644); err != nil {
|
||||
if err := os.WriteFile(link.source, []byte("{}"), 0o644); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Could not create %s: %v", link.source, err))
|
||||
continue
|
||||
}
|
||||
@@ -378,9 +403,351 @@ func SyncDMSConfigs(dmsPath string, logFunc func(string), sudoPassword string) e
|
||||
logFunc(fmt.Sprintf("✓ Synced %s", link.desc))
|
||||
}
|
||||
|
||||
if strings.ToLower(compositor) != "niri" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := syncNiriGreeterConfig(logFunc, sudoPassword); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to sync niri greeter config: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type niriGreeterSync struct {
|
||||
processed map[string]bool
|
||||
nodes []*document.Node
|
||||
inputCount int
|
||||
outputCount int
|
||||
cursorCount int
|
||||
debugCount int
|
||||
cursorNode *document.Node
|
||||
}
|
||||
|
||||
func syncNiriGreeterConfig(logFunc func(string), sudoPassword string) error {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve user config directory: %w", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(configDir, "niri", "config.kdl")
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
logFunc("ℹ Niri config not found; skipping greeter niri sync")
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to stat niri config: %w", err)
|
||||
}
|
||||
|
||||
extractor := &niriGreeterSync{
|
||||
processed: make(map[string]bool),
|
||||
}
|
||||
|
||||
if err := extractor.processFile(configPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(extractor.nodes) == 0 {
|
||||
logFunc("ℹ No niri input/output sections found; skipping greeter niri sync")
|
||||
return nil
|
||||
}
|
||||
|
||||
content := extractor.render()
|
||||
if strings.TrimSpace(content) == "" {
|
||||
logFunc("ℹ No niri input/output content to sync; skipping greeter niri sync")
|
||||
return nil
|
||||
}
|
||||
|
||||
greeterDir := "/etc/greetd/niri"
|
||||
greeterGroup := DetectGreeterGroup()
|
||||
if err := runSudoCmd(sudoPassword, "mkdir", "-p", greeterDir); err != nil {
|
||||
return fmt.Errorf("failed to create greetd niri directory: %w", err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chown", fmt.Sprintf("root:%s", greeterGroup), greeterDir); err != nil {
|
||||
return fmt.Errorf("failed to set greetd niri directory ownership: %w", err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "chmod", "755", greeterDir); err != nil {
|
||||
return fmt.Errorf("failed to set greetd niri directory permissions: %w", err)
|
||||
}
|
||||
|
||||
dmsTemp, err := os.CreateTemp("", "dms-greeter-niri-dms-*.kdl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(dmsTemp.Name())
|
||||
|
||||
if _, err := dmsTemp.WriteString(content); err != nil {
|
||||
_ = dmsTemp.Close()
|
||||
return fmt.Errorf("failed to write temp niri config: %w", err)
|
||||
}
|
||||
if err := dmsTemp.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp niri config: %w", err)
|
||||
}
|
||||
|
||||
dmsPath := filepath.Join(greeterDir, "dms.kdl")
|
||||
if err := backupFileIfExists(sudoPassword, dmsPath, ".backup"); err != nil {
|
||||
return fmt.Errorf("failed to backup %s: %w", dmsPath, err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", dmsTemp.Name(), dmsPath); err != nil {
|
||||
return fmt.Errorf("failed to install greetd niri dms config: %w", err)
|
||||
}
|
||||
|
||||
mainContent := fmt.Sprintf("%s\ninclude \"%s\"\n", config.NiriGreeterConfig, dmsPath)
|
||||
mainTemp, err := os.CreateTemp("", "dms-greeter-niri-main-*.kdl")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer os.Remove(mainTemp.Name())
|
||||
|
||||
if _, err := mainTemp.WriteString(mainContent); err != nil {
|
||||
_ = mainTemp.Close()
|
||||
return fmt.Errorf("failed to write temp niri main config: %w", err)
|
||||
}
|
||||
if err := mainTemp.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp niri main config: %w", err)
|
||||
}
|
||||
|
||||
mainPath := filepath.Join(greeterDir, "config.kdl")
|
||||
if err := backupFileIfExists(sudoPassword, mainPath, ".backup"); err != nil {
|
||||
return fmt.Errorf("failed to backup %s: %w", mainPath, err)
|
||||
}
|
||||
if err := runSudoCmd(sudoPassword, "install", "-o", "root", "-g", greeterGroup, "-m", "0644", mainTemp.Name(), mainPath); err != nil {
|
||||
return fmt.Errorf("failed to install greetd niri main config: %w", err)
|
||||
}
|
||||
|
||||
if err := ensureGreetdNiriConfig(logFunc, sudoPassword, mainPath); err != nil {
|
||||
logFunc(fmt.Sprintf("⚠ Warning: Failed to update greetd config for niri: %v", err))
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Synced niri greeter config (%d input, %d output, %d cursor, %d debug) to %s", extractor.inputCount, extractor.outputCount, extractor.cursorCount, extractor.debugCount, dmsPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureGreetdNiriConfig(logFunc func(string), sudoPassword string, niriConfigPath string) error {
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
data, err := os.ReadFile(configPath)
|
||||
if os.IsNotExist(err) {
|
||||
logFunc("ℹ greetd config not found; skipping niri config wiring")
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read greetd config: %w", err)
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
updated := false
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "command") {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
command := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||
if !strings.Contains(command, "dms-greeter") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(command, "--command niri") {
|
||||
continue
|
||||
}
|
||||
// Strip existing -C or --config and their arguments
|
||||
command = stripConfigFlag(command)
|
||||
|
||||
newCommand := fmt.Sprintf("%s -C %s", command, niriConfigPath)
|
||||
idx := strings.Index(line, "command")
|
||||
leading := ""
|
||||
if idx > 0 {
|
||||
leading = line[:idx]
|
||||
}
|
||||
lines[i] = fmt.Sprintf("%scommand = \"%s\"", leading, newCommand)
|
||||
updated = true
|
||||
break
|
||||
}
|
||||
|
||||
if !updated {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := backupFileIfExists(sudoPassword, configPath, ".backup"); err != nil {
|
||||
return fmt.Errorf("failed to backup greetd config: %w", err)
|
||||
}
|
||||
|
||||
tmpFile, err := os.CreateTemp("", "greetd-config-*.toml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp greetd config: %w", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
|
||||
if _, err := tmpFile.WriteString(strings.Join(lines, "\n")); err != nil {
|
||||
_ = tmpFile.Close()
|
||||
return fmt.Errorf("failed to write temp greetd config: %w", err)
|
||||
}
|
||||
if err := tmpFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp greetd config: %w", err)
|
||||
}
|
||||
|
||||
if err := runSudoCmd(sudoPassword, "mv", tmpFile.Name(), configPath); err != nil {
|
||||
return fmt.Errorf("failed to update greetd config: %w", err)
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Updated greetd config to use niri config %s", niriConfigPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupFileIfExists(sudoPassword string, path string, suffix string) error {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backupPath := fmt.Sprintf("%s%s-%s", path, suffix, time.Now().Format("20060102-150405"))
|
||||
return runSudoCmd(sudoPassword, "cp", "-p", path, backupPath)
|
||||
}
|
||||
|
||||
func (s *niriGreeterSync) processFile(filePath string) error {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to resolve path %s: %w", filePath, err)
|
||||
}
|
||||
|
||||
if s.processed[absPath] {
|
||||
return nil
|
||||
}
|
||||
s.processed[absPath] = true
|
||||
|
||||
data, err := os.ReadFile(absPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||
}
|
||||
|
||||
baseDir := filepath.Dir(absPath)
|
||||
for _, node := range doc.Nodes {
|
||||
name := node.Name.String()
|
||||
switch name {
|
||||
case "include":
|
||||
if err := s.handleInclude(node, baseDir); err != nil {
|
||||
return err
|
||||
}
|
||||
case "input":
|
||||
s.nodes = append(s.nodes, node)
|
||||
s.inputCount++
|
||||
case "output":
|
||||
s.nodes = append(s.nodes, node)
|
||||
s.outputCount++
|
||||
case "cursor":
|
||||
if s.cursorNode == nil {
|
||||
s.cursorNode = node
|
||||
s.cursorNode.Children = dedupeCursorChildren(s.cursorNode.Children)
|
||||
s.nodes = append(s.nodes, node)
|
||||
s.cursorCount++
|
||||
} else if len(node.Children) > 0 {
|
||||
s.cursorNode.Children = mergeCursorChildren(s.cursorNode.Children, node.Children)
|
||||
}
|
||||
case "debug":
|
||||
s.nodes = append(s.nodes, node)
|
||||
s.debugCount++
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeCursorChildren(existing []*document.Node, incoming []*document.Node) []*document.Node {
|
||||
if len(incoming) == 0 {
|
||||
return existing
|
||||
}
|
||||
|
||||
indexByName := make(map[string]int, len(existing))
|
||||
for i, child := range existing {
|
||||
indexByName[child.Name.String()] = i
|
||||
}
|
||||
|
||||
for _, child := range incoming {
|
||||
name := child.Name.String()
|
||||
if idx, ok := indexByName[name]; ok {
|
||||
existing[idx] = child
|
||||
continue
|
||||
}
|
||||
indexByName[name] = len(existing)
|
||||
existing = append(existing, child)
|
||||
}
|
||||
|
||||
return existing
|
||||
}
|
||||
|
||||
func dedupeCursorChildren(children []*document.Node) []*document.Node {
|
||||
if len(children) == 0 {
|
||||
return children
|
||||
}
|
||||
|
||||
var result []*document.Node
|
||||
indexByName := make(map[string]int, len(children))
|
||||
for _, child := range children {
|
||||
name := child.Name.String()
|
||||
if idx, ok := indexByName[name]; ok {
|
||||
result[idx] = child
|
||||
continue
|
||||
}
|
||||
indexByName[name] = len(result)
|
||||
result = append(result, child)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *niriGreeterSync) handleInclude(node *document.Node, baseDir string) error {
|
||||
if len(node.Arguments) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
includePath := strings.Trim(node.Arguments[0].String(), "\"")
|
||||
if includePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fullPath := includePath
|
||||
if !filepath.IsAbs(includePath) {
|
||||
fullPath = filepath.Join(baseDir, includePath)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to stat include %s: %w", fullPath, err)
|
||||
}
|
||||
|
||||
return s.processFile(fullPath)
|
||||
}
|
||||
|
||||
func (s *niriGreeterSync) render() string {
|
||||
if len(s.nodes) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var builder strings.Builder
|
||||
for _, node := range s.nodes {
|
||||
_, _ = node.WriteToOptions(&builder, document.NodeWriteOptions{
|
||||
LeadingTrailingSpace: true,
|
||||
NameAndType: true,
|
||||
Depth: 0,
|
||||
Indent: []byte(" "),
|
||||
IgnoreFlags: false,
|
||||
})
|
||||
builder.WriteString("\n")
|
||||
}
|
||||
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassword string) error {
|
||||
configPath := "/etc/greetd/config.toml"
|
||||
|
||||
@@ -392,17 +759,19 @@ func ConfigureGreetd(dmsPath, compositor string, logFunc func(string), sudoPassw
|
||||
logFunc(fmt.Sprintf("✓ Backed up existing config to %s", backupPath))
|
||||
}
|
||||
|
||||
greeterUser := DetectGreeterGroup()
|
||||
|
||||
var configContent string
|
||||
if data, err := os.ReadFile(configPath); err == nil {
|
||||
configContent = string(data)
|
||||
} else {
|
||||
configContent = `[terminal]
|
||||
configContent = fmt.Sprintf(`[terminal]
|
||||
vt = 1
|
||||
|
||||
[default_session]
|
||||
|
||||
user = "greeter"
|
||||
`
|
||||
user = "%s"
|
||||
`, greeterUser)
|
||||
}
|
||||
|
||||
lines := strings.Split(configContent, "\n")
|
||||
@@ -411,7 +780,7 @@ user = "greeter"
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "command =") && !strings.HasPrefix(trimmed, "command=") {
|
||||
if strings.HasPrefix(trimmed, "user =") || strings.HasPrefix(trimmed, "user=") {
|
||||
newLines = append(newLines, `user = "greeter"`)
|
||||
newLines = append(newLines, fmt.Sprintf(`user = "%s"`, greeterUser))
|
||||
} else {
|
||||
newLines = append(newLines, line)
|
||||
}
|
||||
@@ -455,7 +824,7 @@ user = "greeter"
|
||||
newConfig := strings.Join(finalLines, "\n")
|
||||
|
||||
tmpFile := "/tmp/greetd-config.toml"
|
||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0644); err != nil {
|
||||
if err := os.WriteFile(tmpFile, []byte(newConfig), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write temp config: %w", err)
|
||||
}
|
||||
|
||||
@@ -463,10 +832,41 @@ user = "greeter"
|
||||
return fmt.Errorf("failed to move config to /etc/greetd: %w", err)
|
||||
}
|
||||
|
||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: greeter, command: %s --command %s -p %s)", wrapperCmd, compositorLower, dmsPath))
|
||||
logFunc(fmt.Sprintf("✓ Updated greetd configuration (user: %s, command: %s --command %s -p %s)", greeterUser, wrapperCmd, compositorLower, dmsPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
func stripConfigFlag(command string) string {
|
||||
for _, flag := range []string{" -C ", " --config "} {
|
||||
idx := strings.Index(command, flag)
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
before := command[:idx]
|
||||
after := command[idx+len(flag):]
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(after, `"`):
|
||||
if end := strings.Index(after[1:], `"`); end != -1 {
|
||||
after = after[end+2:]
|
||||
} else {
|
||||
after = ""
|
||||
}
|
||||
default:
|
||||
if space := strings.Index(after, " "); space != -1 {
|
||||
after = after[space:]
|
||||
} else {
|
||||
after = ""
|
||||
}
|
||||
}
|
||||
|
||||
command = strings.TrimSpace(before + after)
|
||||
}
|
||||
|
||||
return command
|
||||
}
|
||||
|
||||
func runSudoCmd(sudoPassword string, command string, args ...string) error {
|
||||
var cmd *exec.Cmd
|
||||
|
||||
|
||||
@@ -79,16 +79,16 @@ func TestFindJSONFiles(t *testing.T) {
|
||||
txtFile := filepath.Join(tmpDir, "readme.txt")
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if err := os.MkdirAll(subdir, 0755); err != nil {
|
||||
if err := os.MkdirAll(subdir, 0o755); err != nil {
|
||||
t.Fatalf("Failed to create subdir: %v", err)
|
||||
}
|
||||
|
||||
@@ -143,10 +143,10 @@ func TestFindJSONFilesMultiplePaths(t *testing.T) {
|
||||
file1 := filepath.Join(tmpDir1, "app1.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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestAutoDiscoverProviders(t *testing.T) {
|
||||
}`
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func TestAutoDiscoverProvidersNoFactory(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ func (h *HyprlandProvider) SetBind(key, action, description string, options map[
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -398,7 +398,7 @@ func (h *HyprlandProvider) getBindSortPriority(action string) int {
|
||||
func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error {
|
||||
overridePath := h.GetOverridePath()
|
||||
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 {
|
||||
|
||||
@@ -187,7 +187,7 @@ bind = SUPER, right, movefocus, r
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -245,7 +245,7 @@ bind = SUPER, B, exec, app2
|
||||
#/# = 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)
|
||||
}
|
||||
|
||||
@@ -278,10 +278,10 @@ func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
||||
content1 := "bind = SUPER, Q, killactive\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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -328,13 +328,13 @@ func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -381,7 +381,7 @@ bind = SUPER, Q, killactive
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ bind = SUPER, T, exec, kitty # Terminal
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestFormatKey(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ func TestDescriptionFallback(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -360,7 +360,7 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
||||
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
||||
overridePath := m.GetOverridePath()
|
||||
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 {
|
||||
|
||||
@@ -502,17 +502,17 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
p.dmsProcessed = true
|
||||
}
|
||||
|
||||
fullPath := sourcePath
|
||||
if !filepath.IsAbs(sourcePath) {
|
||||
fullPath = filepath.Join(baseDir, sourcePath)
|
||||
}
|
||||
|
||||
expanded, err := utils.ExpandPath(fullPath)
|
||||
expanded, err := utils.ExpandPath(sourcePath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
includedBinds, err := p.parseFileWithSource(expanded)
|
||||
fullPath := expanded
|
||||
if !filepath.IsAbs(expanded) {
|
||||
fullPath = filepath.Join(baseDir, expanded)
|
||||
}
|
||||
|
||||
includedBinds, err := p.parseFileWithSource(fullPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -521,33 +521,10 @@ func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKe
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding {
|
||||
data, err := os.ReadFile(dmsBindsPath)
|
||||
keybinds, err := p.parseFileWithSource(dmsBindsPath)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevSource := p.currentSource
|
||||
p.currentSource = dmsBindsPath
|
||||
|
||||
var keybinds []MangoWCKeyBinding
|
||||
lines := strings.Split(string(data), "\n")
|
||||
|
||||
for lineNum, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
kb := p.getKeybindAtLineContent(line, lineNum)
|
||||
if kb == nil {
|
||||
continue
|
||||
}
|
||||
kb.Source = dmsBindsPath
|
||||
p.addBind(kb)
|
||||
keybinds = append(keybinds, *kb)
|
||||
}
|
||||
|
||||
p.currentSource = prevSource
|
||||
p.dmsProcessed = true
|
||||
return keybinds
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ bind=Ctrl,1,view,1,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)
|
||||
}
|
||||
|
||||
@@ -276,10 +276,10 @@ func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
||||
content1 := "bind=ALT,q,killclient,\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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ func TestMangoWCReadContentSingleFile(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -347,13 +347,13 @@ func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -384,7 +384,7 @@ bind=ALT,q,killclient,
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -458,7 +458,7 @@ bind=Ctrl,2,view,2,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ bind=SUPER,n,switch_layout
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -285,7 +285,7 @@ bind=ALT,Left,focusdir,left
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
95
core/internal/keybinds/providers/miracle.go
Normal file
95
core/internal/keybinds/providers/miracle.go
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
320
core/internal/keybinds/providers/miracle_parser.go
Normal file
320
core/internal/keybinds/providers/miracle_parser.go
Normal file
@@ -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"
|
||||
case action == "quit" ||
|
||||
action == "power-off-monitors" ||
|
||||
action == "power-on-monitors" ||
|
||||
action == "suspend" ||
|
||||
action == "do-screen-transition" ||
|
||||
action == "toggle-keyboard-shortcuts-inhibit" ||
|
||||
strings.Contains(action, "dpms"):
|
||||
return "System"
|
||||
@@ -151,13 +154,16 @@ func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, co
|
||||
}
|
||||
|
||||
bind := keybinds.Keybind{
|
||||
Key: keyStr,
|
||||
Description: kb.Description,
|
||||
Action: rawAction,
|
||||
Subcategory: subcategory,
|
||||
Source: source,
|
||||
HideOnOverlay: kb.HideOnOverlay,
|
||||
CooldownMs: kb.CooldownMs,
|
||||
Key: keyStr,
|
||||
Description: kb.Description,
|
||||
Action: rawAction,
|
||||
Subcategory: subcategory,
|
||||
Source: source,
|
||||
HideOnOverlay: kb.HideOnOverlay,
|
||||
CooldownMs: kb.CooldownMs,
|
||||
AllowWhenLocked: kb.AllowWhenLocked,
|
||||
AllowInhibiting: kb.AllowInhibiting,
|
||||
Repeat: kb.Repeat,
|
||||
}
|
||||
|
||||
if source == "dms" && conflicts != nil {
|
||||
@@ -235,7 +241,7 @@ func (n *NiriProvider) SetBind(key, action, description string, options map[stri
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -335,20 +341,18 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
||||
val := arg.ValueString()
|
||||
if val == "" {
|
||||
parts = append(parts, `""`)
|
||||
} else if strings.ContainsAny(val, " \t") {
|
||||
parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`)
|
||||
} else {
|
||||
parts = append(parts, val)
|
||||
}
|
||||
}
|
||||
|
||||
if actionNode.Properties != nil {
|
||||
if val, ok := actionNode.Properties.Get("focus"); ok {
|
||||
parts = append(parts, "focus="+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())
|
||||
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
|
||||
if val, ok := actionNode.Properties.Get(propName); ok {
|
||||
parts = append(parts, propName+"="+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 {
|
||||
opts["allow-when-locked"] = val.String() == "true"
|
||||
}
|
||||
if val, ok := node.Properties.Get("allow-inhibiting"); ok {
|
||||
opts["allow-inhibiting"] = val.String() == "true"
|
||||
}
|
||||
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 {
|
||||
node.AddProperty("allow-when-locked", true, "")
|
||||
}
|
||||
if v, ok := bind.Options["allow-inhibiting"]; ok && v == false {
|
||||
node.AddProperty("allow-inhibiting", false, "")
|
||||
}
|
||||
}
|
||||
|
||||
if bind.Description != "" {
|
||||
@@ -485,7 +495,7 @@ func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(overridePath, []byte(content), 0644)
|
||||
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||
}
|
||||
|
||||
func (n *NiriProvider) getBindSortPriority(action string) int {
|
||||
|
||||
@@ -12,14 +12,17 @@ import (
|
||||
)
|
||||
|
||||
type NiriKeyBinding struct {
|
||||
Mods []string
|
||||
Key string
|
||||
Action string
|
||||
Args []string
|
||||
Description string
|
||||
HideOnOverlay bool
|
||||
CooldownMs int
|
||||
Source string
|
||||
Mods []string
|
||||
Key string
|
||||
Action string
|
||||
Args []string
|
||||
Description string
|
||||
HideOnOverlay bool
|
||||
CooldownMs int
|
||||
AllowWhenLocked bool
|
||||
AllowInhibiting *bool
|
||||
Repeat *bool
|
||||
Source string
|
||||
}
|
||||
|
||||
type NiriSection struct {
|
||||
@@ -269,8 +272,10 @@ func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBin
|
||||
args = append(args, arg.ValueString())
|
||||
}
|
||||
if actionNode.Properties != nil {
|
||||
if val, ok := actionNode.Properties.Get("focus"); ok {
|
||||
args = append(args, "focus="+val.String())
|
||||
for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} {
|
||||
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 hideOnOverlay bool
|
||||
var cooldownMs int
|
||||
var allowWhenLocked bool
|
||||
var allowInhibiting *bool
|
||||
var repeat *bool
|
||||
if node.Properties != nil {
|
||||
if val, ok := node.Properties.Get("hotkey-overlay-title"); ok {
|
||||
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 {
|
||||
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{
|
||||
Mods: mods,
|
||||
Key: key,
|
||||
Action: action,
|
||||
Args: args,
|
||||
Description: description,
|
||||
HideOnOverlay: hideOnOverlay,
|
||||
CooldownMs: cooldownMs,
|
||||
Source: p.currentSource,
|
||||
Mods: mods,
|
||||
Key: key,
|
||||
Action: action,
|
||||
Args: args,
|
||||
Description: description,
|
||||
HideOnOverlay: hideOnOverlay,
|
||||
CooldownMs: cooldownMs,
|
||||
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"; }
|
||||
}
|
||||
`
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestNiriParseRecentWindows(t *testing.T) {
|
||||
func TestNiriParseInclude(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -185,7 +185,7 @@ include "dms/binds.kdl"
|
||||
func TestNiriParseIncludeOverride(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -246,10 +246,10 @@ include "other.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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ func TestNiriParseMissingInclude(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -386,7 +386,7 @@ func TestNiriBindOverrideBehavior(t *testing.T) {
|
||||
func TestNiriBindOverrideWithIncludes(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -508,7 +508,7 @@ func TestNiriParseNumericWorkspaceBinds(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -550,7 +550,7 @@ func TestNiriParseQuotedStringArgs(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -586,7 +586,7 @@ func TestNiriParseActionWithProperties(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestNiriProviderGetCheatSheet(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ func TestNiriGenerateBindsContentRoundTrip(t *testing.T) {
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -351,12 +351,12 @@ func TestNiriEmptyArgsPreservation(t *testing.T) {
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -621,7 +621,7 @@ func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) {
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package providers
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
@@ -18,14 +19,21 @@ func NewSwayProvider(configPath string) *SwayProvider {
|
||||
_, scrollEnvSet := os.LookupEnv("SCROLLSOCK")
|
||||
|
||||
if configPath == "" {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
configDir = ""
|
||||
}
|
||||
if scrollEnvSet {
|
||||
configPath = "$HOME/.config/scroll"
|
||||
if configDir != "" {
|
||||
configPath = filepath.Join(configDir, "scroll")
|
||||
}
|
||||
isScroll = true
|
||||
} else {
|
||||
configPath = "$HOME/.config/sway"
|
||||
if configDir != "" {
|
||||
configPath = filepath.Join(configDir, "sway")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Determine isScroll based on the provided config path
|
||||
isScroll = strings.Contains(configPath, "scroll")
|
||||
}
|
||||
|
||||
@@ -36,16 +44,16 @@ func NewSwayProvider(configPath string) *SwayProvider {
|
||||
}
|
||||
|
||||
func (s *SwayProvider) Name() string {
|
||||
if s != nil && s.isScroll {
|
||||
return "scroll"
|
||||
}
|
||||
if s == nil {
|
||||
_, ok := os.LookupEnv("SCROLLSOCK")
|
||||
if ok {
|
||||
if os.Getenv("SCROLLSOCK") != "" {
|
||||
return "scroll"
|
||||
}
|
||||
return "sway"
|
||||
}
|
||||
|
||||
if s.isScroll {
|
||||
return "scroll"
|
||||
}
|
||||
return "sway"
|
||||
}
|
||||
|
||||
|
||||
@@ -200,7 +200,7 @@ bindsym $mod+t exec $term
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ bindsym $mod+Right focus right
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -328,13 +328,13 @@ func TestSwayReadContentWithTildeExpansion(t *testing.T) {
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ bindsym Mod4+q kill
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ bindsym $mod+2 workspace number 2
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,13 @@ func TestSwayProviderName(t *testing.T) {
|
||||
|
||||
func TestSwayProviderDefaultPath(t *testing.T) {
|
||||
provider := NewSwayProvider("")
|
||||
if provider.configPath != "$HOME/.config/sway" {
|
||||
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
|
||||
configDir, err := os.UserConfigDir()
|
||||
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
|
||||
`
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -253,7 +258,7 @@ bindsym $mod+f fullscreen toggle
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package keybinds
|
||||
|
||||
type Keybind struct {
|
||||
Key string `json:"key"`
|
||||
Description string `json:"desc"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Subcategory string `json:"subcat,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
||||
CooldownMs int `json:"cooldownMs,omitempty"`
|
||||
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
|
||||
Conflict *Keybind `json:"conflict,omitempty"`
|
||||
Key string `json:"key"`
|
||||
Description string `json:"desc"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Subcategory string `json:"subcat,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
HideOnOverlay bool `json:"hideOnOverlay,omitempty"`
|
||||
CooldownMs int `json:"cooldownMs,omitempty"`
|
||||
Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press
|
||||
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 {
|
||||
|
||||
@@ -3,6 +3,7 @@ package matugen
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -10,10 +11,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/dank16"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
type ColorMode string
|
||||
@@ -30,6 +33,7 @@ const (
|
||||
TemplateKindTerminal
|
||||
TemplateKindGTK
|
||||
TemplateKindVSCode
|
||||
TemplateKindEmacs
|
||||
)
|
||||
|
||||
type TemplateDef struct {
|
||||
@@ -62,7 +66,7 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "dgop", Commands: []string{"dgop"}, ConfigFile: "dgop.toml"},
|
||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml"},
|
||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||
}
|
||||
|
||||
func (c *ColorMode) GTKTheme() string {
|
||||
@@ -75,8 +79,10 @@ func (c *ColorMode) GTKTheme() string {
|
||||
}
|
||||
|
||||
var (
|
||||
matugenVersionOnce sync.Once
|
||||
matugenVersionMu sync.Mutex
|
||||
matugenVersionOK bool
|
||||
matugenSupportsCOE bool
|
||||
matugenIsV4 bool
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
@@ -148,7 +154,7 @@ func Run(opts Options) error {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -250,8 +256,22 @@ func buildOnce(opts *Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
refreshGTK(opts.ConfigDir, opts.Mode)
|
||||
signalTerminals()
|
||||
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 nil
|
||||
}
|
||||
@@ -316,6 +336,10 @@ output_path = '%s'
|
||||
appendVSCodeConfig(cfgFile, "cursor", filepath.Join(homeDir, ".cursor/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)
|
||||
case TemplateKindEmacs:
|
||||
if utils.EmacsConfigDir() != "" {
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
}
|
||||
default:
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
}
|
||||
@@ -414,7 +438,7 @@ func appendTerminalConfig(opts *Options, cfgFile *os.File, tmpDir string, checkC
|
||||
|
||||
modified := strings.ReplaceAll(string(origData), ".default.", ".dark.")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -473,6 +497,9 @@ func substituteVars(content, shellDir string) string {
|
||||
result = strings.ReplaceAll(result, "'CONFIG_DIR/", "'"+utils.XDGConfigHome()+"/")
|
||||
result = strings.ReplaceAll(result, "'DATA_DIR/", "'"+utils.XDGDataHome()+"/")
|
||||
result = strings.ReplaceAll(result, "'CACHE_DIR/", "'"+utils.XDGCacheHome()+"/")
|
||||
if emacsDir := utils.EmacsConfigDir(); emacsDir != "" {
|
||||
result = strings.ReplaceAll(result, "'EMACS_DIR/", "'"+emacsDir+"/")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -493,67 +520,160 @@ func extractTOMLSection(content, startMarker, endMarker string) string {
|
||||
return content[startIdx : startIdx+endIdx]
|
||||
}
|
||||
|
||||
func checkMatugenVersion() {
|
||||
matugenVersionOnce.Do(func() {
|
||||
cmd := exec.Command("matugen", "--version")
|
||||
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)
|
||||
}
|
||||
})
|
||||
type matugenFlags struct {
|
||||
supportsCOE bool
|
||||
isV4 bool
|
||||
}
|
||||
|
||||
func runMatugen(args []string) error {
|
||||
checkMatugenVersion()
|
||||
func detectMatugenVersion() (matugenFlags, error) {
|
||||
matugenVersionMu.Lock()
|
||||
defer matugenVersionMu.Unlock()
|
||||
|
||||
if matugenSupportsCOE {
|
||||
args = append([]string{"--continue-on-error"}, args...)
|
||||
if matugenVersionOK {
|
||||
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.Infof("Matugen %s supports --continue-on-error", versionStr)
|
||||
}
|
||||
if matugenIsV4 {
|
||||
log.Infof("Matugen %s: using v4 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.Stdout = os.Stdout
|
||||
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) {
|
||||
var args []string
|
||||
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()
|
||||
flags, err := detectMatugenVersion()
|
||||
if err != nil {
|
||||
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")
|
||||
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
|
||||
}
|
||||
|
||||
@@ -617,40 +737,73 @@ func generateDank16Variants(primaryDark, primaryLight, surface string, mode Colo
|
||||
return dank16.GenerateVariantJSON(variantColors)
|
||||
}
|
||||
|
||||
func refreshGTK(configDir string, mode ColorMode) {
|
||||
func isDMSGTKActive(configDir string) bool {
|
||||
gtkCSS := filepath.Join(configDir, "gtk-3.0", "gtk.css")
|
||||
|
||||
info, err := os.Lstat(gtkCSS)
|
||||
if err != nil {
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
shouldRun := false
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
target, err := os.Readlink(gtkCSS)
|
||||
if 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
|
||||
}
|
||||
return err == nil && strings.Contains(target, "dank-colors.css")
|
||||
}
|
||||
|
||||
if !shouldRun {
|
||||
return
|
||||
}
|
||||
|
||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", "").Run()
|
||||
exec.Command("gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", mode.GTKTheme()).Run()
|
||||
data, err := os.ReadFile(gtkCSS)
|
||||
return err == nil && strings.Contains(string(data), "dank-colors.css")
|
||||
}
|
||||
|
||||
func signalTerminals() {
|
||||
signalByName("kitty", syscall.SIGUSR1)
|
||||
signalByName("ghostty", syscall.SIGUSR2)
|
||||
signalByName(".kitty-wrapped", syscall.SIGUSR1)
|
||||
signalByName(".ghostty-wrappe", syscall.SIGUSR2)
|
||||
func refreshGTK(mode ColorMode) {
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "gtk-theme", ""); err != nil {
|
||||
log.Warnf("Failed to reset gtk-theme: %v", err)
|
||||
}
|
||||
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) {
|
||||
@@ -679,8 +832,59 @@ func syncColorScheme(mode ColorMode) {
|
||||
scheme = "default"
|
||||
}
|
||||
|
||||
if err := exec.Command("gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme).Run(); err != nil {
|
||||
exec.Command("dconf", "write", "/org/gnome/desktop/interface/color-scheme", "'"+scheme+"'").Run()
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "color-scheme", scheme); err != nil {
|
||||
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 +909,8 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
||||
detected = true
|
||||
case tmpl.Kind == TemplateKindVSCode:
|
||||
detected = checkVSCodeExtension(homeDir)
|
||||
case tmpl.Kind == TemplateKindEmacs:
|
||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
|
||||
default:
|
||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks)
|
||||
}
|
||||
|
||||
@@ -15,13 +15,13 @@ func TestAppendConfigBinaryExists(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "test config content"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -58,13 +58,13 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "test config content"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -99,13 +99,13 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "zen config content"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -139,13 +139,13 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "test config content"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -180,13 +180,13 @@ func TestAppendConfigBothExist(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "zen config content"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -220,13 +220,13 @@ func TestAppendConfigNeitherExists(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "test config content"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -261,13 +261,13 @@ func TestAppendConfigNoChecks(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
testConfig := "always include"
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ const (
|
||||
notifyDest = "org.freedesktop.Notifications"
|
||||
notifyPath = "/org/freedesktop/Notifications"
|
||||
notifyInterface = "org.freedesktop.Notifications"
|
||||
|
||||
maxSummaryLen = 29
|
||||
maxBodyLen = 80
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
@@ -39,6 +42,13 @@ func Send(n Notification) error {
|
||||
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
|
||||
if n.FilePath != "" {
|
||||
actions = []string{
|
||||
|
||||
@@ -116,12 +116,12 @@ func (m *Manager) Install(plugin Plugin) error {
|
||||
return fmt.Errorf("plugin already installed: %s", plugin.Name)
|
||||
}
|
||||
|
||||
if err := m.fs.MkdirAll(m.pluginsDir, 0755); err != nil {
|
||||
if err := m.fs.MkdirAll(m.pluginsDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create plugins directory: %w", err)
|
||||
}
|
||||
|
||||
reposDir := filepath.Join(m.pluginsDir, ".repos")
|
||||
if err := m.fs.MkdirAll(reposDir, 0755); err != nil {
|
||||
if err := m.fs.MkdirAll(reposDir, 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create repos directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ func (m *Manager) Install(plugin Plugin) error {
|
||||
|
||||
metaPath := pluginPath + ".meta"
|
||||
metaContent := fmt.Sprintf("repo=%s\npath=%s\nrepodir=%s", plugin.Repo, plugin.Path, repoName)
|
||||
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0644); err != nil {
|
||||
if err := afero.WriteFile(m.fs, metaPath, []byte(metaContent), 0o644); err != nil {
|
||||
return fmt.Errorf("failed to write metadata: %w", err)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -66,7 +66,7 @@ func TestIsInstalled(t *testing.T) {
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
err := fs.MkdirAll(pluginPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
installed, err := manager.IsInstalled(plugin)
|
||||
@@ -100,7 +100,7 @@ func TestInstall(t *testing.T) {
|
||||
cloneCalled = true
|
||||
assert.Equal(t, filepath.Join(pluginsDir, plugin.ID), path)
|
||||
assert.Equal(t, plugin.Repo, url)
|
||||
return fs.MkdirAll(path, 0755)
|
||||
return fs.MkdirAll(path, 0o755)
|
||||
},
|
||||
}
|
||||
manager.gitClient = mockGit
|
||||
@@ -118,7 +118,7 @@ func TestInstall(t *testing.T) {
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
err := fs.MkdirAll(pluginPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager.Install(plugin)
|
||||
@@ -137,7 +137,7 @@ func TestManagerUpdate(t *testing.T) {
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
err := fs.MkdirAll(pluginPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
pullCalled := false
|
||||
@@ -171,7 +171,7 @@ func TestUninstall(t *testing.T) {
|
||||
|
||||
plugin := Plugin{ID: "test-plugin", Name: "TestPlugin"}
|
||||
pluginPath := filepath.Join(pluginsDir, plugin.ID)
|
||||
err := fs.MkdirAll(pluginPath, 0755)
|
||||
err := fs.MkdirAll(pluginPath, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manager.Uninstall(plugin)
|
||||
@@ -195,14 +195,14 @@ func TestListInstalled(t *testing.T) {
|
||||
t.Run("lists installed plugins", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0755)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin2"), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin2", "plugin.json"), []byte(`{"id":"Plugin2"}`), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
installed, err := manager.ListInstalled()
|
||||
@@ -223,15 +223,15 @@ func TestListInstalled(t *testing.T) {
|
||||
t.Run("ignores files and .repos directory", func(t *testing.T) {
|
||||
manager, fs, pluginsDir := setupTestManager(t)
|
||||
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0755)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, "Plugin1"), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "Plugin1", "plugin.json"), []byte(`{"id":"Plugin1"}`), 0o644)
|
||||
require.NoError(t, err)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0755)
|
||||
err = fs.MkdirAll(filepath.Join(pluginsDir, ".repos"), 0o755)
|
||||
require.NoError(t, err)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("test"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
installed, err := manager.ListInstalled()
|
||||
|
||||
@@ -27,6 +27,7 @@ type Plugin struct {
|
||||
Distro []string `json:"distro"`
|
||||
Screenshot string `json:"screenshot,omitempty"`
|
||||
RequiresDMS string `json:"requires_dms,omitempty"`
|
||||
Featured bool `json:"featured,omitempty"`
|
||||
}
|
||||
|
||||
type GitClient interface {
|
||||
@@ -147,7 +148,7 @@ func (r *Registry) Update() error {
|
||||
}
|
||||
|
||||
if !exists {
|
||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -162,7 +163,7 @@ func (r *Registry) Update() error {
|
||||
return fmt.Errorf("failed to remove corrupted registry: %w", err)
|
||||
}
|
||||
|
||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0755); err != nil {
|
||||
if err := r.fs.MkdirAll(filepath.Dir(r.cacheDir), 0o755); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -63,13 +63,13 @@ func setupTestRegistry(t *testing.T) (*Registry, afero.Fs, string) {
|
||||
|
||||
func createTestPlugin(t *testing.T, fs afero.Fs, dir string, filename string, plugin Plugin) {
|
||||
pluginsDir := filepath.Join(dir, "plugins")
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
data, err := json.Marshal(plugin)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, filename), data, 0o644)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -118,10 +118,10 @@ func TestLoadPlugins(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "README.md"), []byte("# Test"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := Plugin{
|
||||
@@ -146,7 +146,7 @@ func TestLoadPlugins(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0755)
|
||||
err := fs.MkdirAll(filepath.Join(pluginsDir, "subdir"), 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := Plugin{
|
||||
@@ -170,10 +170,10 @@ func TestLoadPlugins(t *testing.T) {
|
||||
registry, fs, tmpDir := setupTestRegistry(t)
|
||||
|
||||
pluginsDir := filepath.Join(tmpDir, "plugins")
|
||||
err := fs.MkdirAll(pluginsDir, 0755)
|
||||
err := fs.MkdirAll(pluginsDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0644)
|
||||
err = afero.WriteFile(fs, filepath.Join(pluginsDir, "invalid.json"), []byte("{invalid json}"), 0o644)
|
||||
require.NoError(t, err)
|
||||
|
||||
plugin := Plugin{
|
||||
@@ -303,7 +303,7 @@ func TestUpdate(t *testing.T) {
|
||||
Distro: []string{"any"},
|
||||
}
|
||||
|
||||
err := fs.MkdirAll(tmpDir, 0755)
|
||||
err := fs.MkdirAll(tmpDir, 0o755)
|
||||
require.NoError(t, err)
|
||||
|
||||
pullCalled := false
|
||||
|
||||
@@ -67,6 +67,9 @@ func FilterByCapability(capability string, plugins []Plugin) []Plugin {
|
||||
|
||||
func SortByFirstParty(plugins []Plugin) []Plugin {
|
||||
sort.SliceStable(plugins, func(i, j int) bool {
|
||||
if plugins[i].Featured != plugins[j].Featured {
|
||||
return plugins[i].Featured
|
||||
}
|
||||
isFirstPartyI := strings.HasPrefix(plugins[i].Repo, "https://github.com/AvengeMedia")
|
||||
isFirstPartyJ := strings.HasPrefix(plugins[j].Repo, "https://github.com/AvengeMedia")
|
||||
if isFirstPartyI != isFirstPartyJ {
|
||||
|
||||
@@ -258,7 +258,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy != nil {
|
||||
if proxy != nil && !proxy.IsZombie() {
|
||||
e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1)
|
||||
} else {
|
||||
groupHandle := &ExtWorkspaceGroupHandleV1{}
|
||||
@@ -278,7 +278,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
||||
l := 0
|
||||
objectID := client.Uint32(data[l : l+4])
|
||||
proxy := i.Context().GetProxy(objectID)
|
||||
if proxy != nil {
|
||||
if proxy != nil && !proxy.IsZombie() {
|
||||
e.Workspace = proxy.(*ExtWorkspaceHandleV1)
|
||||
} else {
|
||||
wsHandle := &ExtWorkspaceHandleV1{}
|
||||
|
||||
@@ -21,6 +21,7 @@ const (
|
||||
CompositorNiri
|
||||
CompositorDWL
|
||||
CompositorScroll
|
||||
CompositorMiracle
|
||||
)
|
||||
|
||||
var detectedCompositor Compositor = -1
|
||||
@@ -34,6 +35,7 @@ func DetectCompositor() Compositor {
|
||||
niriSocket := os.Getenv("NIRI_SOCKET")
|
||||
swaySocket := os.Getenv("SWAYSOCK")
|
||||
scrollSocket := os.Getenv("SCROLLSOCK")
|
||||
miracleSocket := os.Getenv("MIRACLESOCK")
|
||||
|
||||
switch {
|
||||
case niriSocket != "":
|
||||
@@ -46,7 +48,11 @@ func DetectCompositor() Compositor {
|
||||
detectedCompositor = CompositorScroll
|
||||
return detectedCompositor
|
||||
}
|
||||
|
||||
case miracleSocket != "":
|
||||
if _, err := os.Stat(miracleSocket); err == nil {
|
||||
detectedCompositor = CompositorMiracle
|
||||
return detectedCompositor
|
||||
}
|
||||
case swaySocket != "":
|
||||
if _, err := os.Stat(swaySocket); err == nil {
|
||||
detectedCompositor = CompositorSway
|
||||
@@ -260,6 +266,25 @@ func getScrollFocusedMonitor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func getMiracleFocusedMonitor() string {
|
||||
output, err := exec.Command("miraclemsg", "-t", "get_workspaces").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var workspaces []swayWorkspace
|
||||
if err := json.Unmarshal(output, &workspaces); err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, ws := range workspaces {
|
||||
if ws.Focused {
|
||||
return ws.Output
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type niriWorkspace struct {
|
||||
Output string `json:"output"`
|
||||
IsFocused bool `json:"is_focused"`
|
||||
@@ -407,6 +432,8 @@ func GetFocusedMonitor() string {
|
||||
return getSwayFocusedMonitor()
|
||||
case CompositorScroll:
|
||||
return getScrollFocusedMonitor()
|
||||
case CompositorMiracle:
|
||||
return getMiracleFocusedMonitor()
|
||||
case CompositorNiri:
|
||||
return getNiriFocusedMonitor()
|
||||
case CompositorDWL:
|
||||
|
||||
@@ -107,7 +107,7 @@ func GetOutputDir() string {
|
||||
|
||||
if xdgPics := getXDGPicturesDir(); xdgPics != "" {
|
||||
screenshotDir := filepath.Join(xdgPics, "Screenshots")
|
||||
if err := os.MkdirAll(screenshotDir, 0755); err == nil {
|
||||
if err := os.MkdirAll(screenshotDir, 0o755); err == nil {
|
||||
return screenshotDir
|
||||
}
|
||||
return xdgPics
|
||||
|
||||
@@ -108,7 +108,7 @@ func NewRegionSelector(s *Screenshoter) *RegionSelector {
|
||||
screenshoter: s,
|
||||
outputs: make(map[uint32]*WaylandOutput),
|
||||
preCapture: make(map[*WaylandOutput]*PreCapture),
|
||||
showCapturedCursor: true,
|
||||
showCapturedCursor: s.config.Cursor == CursorOn,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -453,10 +453,7 @@ func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted
|
||||
}
|
||||
|
||||
func (s *Screenshoter) captureWholeOutput(output *WaylandOutput) (*CaptureResult, error) {
|
||||
cursor := int32(0)
|
||||
if s.config.IncludeCursor {
|
||||
cursor = 1
|
||||
}
|
||||
cursor := int32(s.config.Cursor)
|
||||
|
||||
frame, err := s.screencopy.CaptureOutput(cursor, output.wlOutput)
|
||||
if err != nil {
|
||||
@@ -624,10 +621,7 @@ func (s *Screenshoter) captureRegionOnOutput(output *WaylandOutput, region Regio
|
||||
}
|
||||
}
|
||||
|
||||
cursor := int32(0)
|
||||
if s.config.IncludeCursor {
|
||||
cursor = 1
|
||||
}
|
||||
cursor := int32(s.config.Cursor)
|
||||
|
||||
frame, err := s.screencopy.CaptureOutputRegion(cursor, output.wlOutput, localX, localY, w, h)
|
||||
if err != nil {
|
||||
|
||||
@@ -39,7 +39,7 @@ func LoadState() (*PersistentState, error) {
|
||||
func SaveState(state *PersistentState) error {
|
||||
path := getStateFilePath()
|
||||
dir := filepath.Dir(path)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func SaveState(state *PersistentState) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, data, 0644)
|
||||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
|
||||
func GetLastRegion() Region {
|
||||
|
||||
@@ -3,11 +3,11 @@ package screenshot
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
|
||||
type ThemeColors struct {
|
||||
@@ -83,12 +83,11 @@ func getColorsFilePath() string {
|
||||
}
|
||||
|
||||
func isLightMode() bool {
|
||||
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "color-scheme").Output()
|
||||
scheme, err := utils.GsettingsGet("org.gnome.desktop.interface", "color-scheme")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
scheme := strings.TrimSpace(string(out))
|
||||
switch scheme {
|
||||
case "'prefer-light'", "'default'":
|
||||
return true
|
||||
|
||||
@@ -19,6 +19,13 @@ const (
|
||||
FormatPPM
|
||||
)
|
||||
|
||||
type CursorMode int
|
||||
|
||||
const (
|
||||
CursorOff CursorMode = iota
|
||||
CursorOn
|
||||
)
|
||||
|
||||
type Region struct {
|
||||
X int32 `json:"x"`
|
||||
Y int32 `json:"y"`
|
||||
@@ -42,29 +49,29 @@ type Output struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Mode Mode
|
||||
OutputName string
|
||||
IncludeCursor bool
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
Mode Mode
|
||||
OutputName string
|
||||
Cursor CursorMode
|
||||
Format Format
|
||||
Quality int
|
||||
OutputDir string
|
||||
Filename string
|
||||
Clipboard bool
|
||||
SaveFile bool
|
||||
Notify bool
|
||||
Stdout bool
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Mode: ModeRegion,
|
||||
IncludeCursor: false,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
Filename: "",
|
||||
Clipboard: true,
|
||||
SaveFile: true,
|
||||
Notify: true,
|
||||
Mode: ModeRegion,
|
||||
Cursor: CursorOff,
|
||||
Format: FormatPNG,
|
||||
Quality: 90,
|
||||
OutputDir: "",
|
||||
Filename: "",
|
||||
Clipboard: true,
|
||||
SaveFile: true,
|
||||
Notify: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,12 @@ func (m *Manager) Rescan() {
|
||||
}
|
||||
}
|
||||
|
||||
if m.sysfsReady && m.sysfsBackend != nil {
|
||||
if err := m.sysfsBackend.Rescan(); err != nil {
|
||||
log.Debugf("Sysfs rescan failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,10 @@ func shouldSuppressDevice(name string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *SysfsBackend) Rescan() error {
|
||||
return b.scanDevices()
|
||||
}
|
||||
|
||||
func (b *SysfsBackend) GetDevices() ([]Device, error) {
|
||||
devices := make([]Device, 0)
|
||||
|
||||
@@ -186,7 +190,7 @@ func (b *SysfsBackend) SetBrightnessWithExponent(id string, percent int, exponen
|
||||
brightnessPath := filepath.Join(devicePath, "brightness")
|
||||
|
||||
data := []byte(fmt.Sprintf("%d", value))
|
||||
if err := os.WriteFile(brightnessPath, data, 0644); err != nil {
|
||||
if err := os.WriteFile(brightnessPath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("write brightness: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ func TestManager_SetBrightness_LogindSuccess(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -86,13 +86,13 @@ func TestManager_SetBrightness_LogindFailsFallbackToSysfs(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -158,13 +158,13 @@ func TestManager_SetBrightness_NoLogind(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -215,13 +215,13 @@ func TestManager_SetBrightness_LEDWithLogind(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
||||
if err := os.MkdirAll(ledsDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(ledsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -136,26 +136,26 @@ func TestSysfsBackend_ScanDevices(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
backlightDir := filepath.Join(tmpDir, "backlight", "test_backlight")
|
||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("100\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("50\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ledsDir := filepath.Join(tmpDir, "leds", "test_led")
|
||||
if err := os.MkdirAll(ledsDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(ledsDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "max_brightness"), []byte("255\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(ledsDir, "brightness"), []byte("128\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ func setupTestManager(t *testing.T) (*Manager, string) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
backlightDir := filepath.Join(tmpDir, "backlight", "intel_backlight")
|
||||
if err := os.MkdirAll(backlightDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(backlightDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "max_brightness"), []byte("1000\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(backlightDir, "brightness"), []byte("500\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ func TestHandleEvent_ChangeAction(t *testing.T) {
|
||||
um := &UdevMonitor{stop: make(chan struct{})}
|
||||
|
||||
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
||||
if err := os.WriteFile(brightnessPath, []byte("800\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(brightnessPath, []byte("800\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ func TestHandleChange_InvalidBrightnessValue(t *testing.T) {
|
||||
um := &UdevMonitor{stop: make(chan struct{})}
|
||||
|
||||
brightnessPath := filepath.Join(tmpDir, "backlight", "intel_backlight", "brightness")
|
||||
if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0644); err != nil {
|
||||
if err := os.WriteFile(brightnessPath, []byte("not_a_number\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ func HandleRequest(conn net.Conn, req models.Request, m *Manager) {
|
||||
handleGetPinnedEntries(conn, req, m)
|
||||
case "clipboard.getPinnedCount":
|
||||
handleGetPinnedCount(conn, req, m)
|
||||
case "clipboard.copyFile":
|
||||
handleCopyFile(conn, req, m)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, "unknown method: "+req.Method)
|
||||
}
|
||||
@@ -126,11 +128,36 @@ func handleCopyEntry(conn net.Conn, req models.Request, m *Manager) {
|
||||
return
|
||||
}
|
||||
|
||||
filePath := m.EntryToFile(entry)
|
||||
if filePath != "" {
|
||||
if err := m.CopyFile(filePath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
models.Respond(conn, req.ID, map[string]any{
|
||||
"success": true,
|
||||
"filePath": filePath,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.SetClipboard(entry.Data, entry.MimeType); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if entry.Pinned {
|
||||
if err := m.CreateHistoryEntryFromPinned(entry); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if err := m.TouchEntry(uint64(id)); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied to clipboard"})
|
||||
}
|
||||
|
||||
@@ -281,3 +308,18 @@ func handleGetPinnedCount(conn net.Conn, req models.Request, m *Manager) {
|
||||
count := m.GetPinnedCount()
|
||||
models.Respond(conn, req.ID, map[string]int{"count": count})
|
||||
}
|
||||
|
||||
func handleCopyFile(conn net.Conn, req models.Request, m *Manager) {
|
||||
filePath, err := params.String(req.Params, "filePath")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.CopyFile(filePath); err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "copied"})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
_ "image/png"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -19,8 +20,10 @@ import (
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/godbus/dbus/v5"
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
_ "golang.org/x/image/webp"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
@@ -104,7 +107,7 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error)
|
||||
}
|
||||
|
||||
func openDB(path string) (*bolt.DB, error) {
|
||||
db, err := bolt.Open(path, 0644, &bolt.Options{
|
||||
db, err := bolt.Open(path, 0o644, &bolt.Options{
|
||||
Timeout: 1 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -229,8 +232,15 @@ func (m *Manager) setupDataDeviceSync() {
|
||||
return
|
||||
}
|
||||
|
||||
prevOffer := m.currentOffer
|
||||
m.currentOffer = offer
|
||||
|
||||
if prevOffer != nil && prevOffer != offer {
|
||||
m.offerMutex.Lock()
|
||||
delete(m.offerMimeTypes, prevOffer)
|
||||
m.offerMutex.Unlock()
|
||||
}
|
||||
|
||||
m.offerMutex.RLock()
|
||||
mimes := m.offerMimeTypes[offer]
|
||||
m.offerMutex.RUnlock()
|
||||
@@ -316,6 +326,13 @@ func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
||||
}
|
||||
|
||||
func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
||||
if mimeType == "text/uri-list" {
|
||||
if imgData, imgMime, ok := m.tryReadImageFromURI(data); ok {
|
||||
data = imgData
|
||||
mimeType = imgMime
|
||||
}
|
||||
}
|
||||
|
||||
entry := Entry{
|
||||
Data: data,
|
||||
MimeType: mimeType,
|
||||
@@ -327,6 +344,8 @@ func (m *Manager) storeClipboardEntry(data []byte, mimeType string) {
|
||||
switch {
|
||||
case entry.IsImage:
|
||||
entry.Preview = m.imagePreview(data, mimeType)
|
||||
case mimeType == "text/uri-list":
|
||||
entry.Preview, entry.IsImage = m.uriListPreview(data)
|
||||
default:
|
||||
entry.Preview = m.textPreview(data)
|
||||
}
|
||||
@@ -376,6 +395,10 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
||||
if extractHash(v) != hash {
|
||||
continue
|
||||
}
|
||||
entry, err := decodeEntry(v)
|
||||
if err == nil && entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if err := b.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -493,10 +516,10 @@ func computeHash(data []byte) uint64 {
|
||||
}
|
||||
|
||||
func extractHash(data []byte) uint64 {
|
||||
if len(data) < 8 {
|
||||
if len(data) < 9 {
|
||||
return 0
|
||||
}
|
||||
return binary.BigEndian.Uint64(data[len(data)-8:])
|
||||
return binary.BigEndian.Uint64(data[len(data)-9 : len(data)-1])
|
||||
}
|
||||
|
||||
func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
|
||||
@@ -507,6 +530,7 @@ func (m *Manager) hasSensitiveMimeType(mimes []string) bool {
|
||||
|
||||
func (m *Manager) selectMimeType(mimes []string) string {
|
||||
preferredTypes := []string{
|
||||
"text/uri-list",
|
||||
"text/plain;charset=utf-8",
|
||||
"text/plain",
|
||||
"UTF8_STRING",
|
||||
@@ -527,8 +551,14 @@ func (m *Manager) selectMimeType(mimes []string) string {
|
||||
}
|
||||
}
|
||||
|
||||
if len(mimes) > 0 {
|
||||
return mimes[0]
|
||||
// Skip useless MIME types when falling back
|
||||
for _, mime := range mimes {
|
||||
switch mime {
|
||||
case "application/vnd.portal.filetransfer":
|
||||
continue
|
||||
default:
|
||||
return mime
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
@@ -557,6 +587,73 @@ func (m *Manager) imagePreview(data []byte, format string) string {
|
||||
return fmt.Sprintf("[[ image %s %s %dx%d ]]", sizeStr(len(data)), imgFmt, config.Width, config.Height)
|
||||
}
|
||||
|
||||
func (m *Manager) uriListPreview(data []byte) (string, bool) {
|
||||
text := strings.TrimSpace(string(data))
|
||||
uris := strings.Split(text, "\r\n")
|
||||
if len(uris) == 0 {
|
||||
uris = strings.Split(text, "\n")
|
||||
}
|
||||
|
||||
if len(uris) > 1 {
|
||||
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
|
||||
}
|
||||
|
||||
if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
|
||||
filePath := strings.TrimPrefix(uris[0], "file://")
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil || info.IsDir() {
|
||||
return m.textPreview(data), false
|
||||
}
|
||||
|
||||
cfg := m.getConfig()
|
||||
if info.Size() <= cfg.MaxEntrySize {
|
||||
if imgData, err := os.ReadFile(filePath); err == nil {
|
||||
if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); err == nil {
|
||||
return fmt.Sprintf("[[ file %s %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
|
||||
}
|
||||
|
||||
return m.textPreview(data), false
|
||||
}
|
||||
|
||||
func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
|
||||
text := strings.TrimSpace(string(data))
|
||||
uris := strings.Split(text, "\r\n")
|
||||
if len(uris) == 0 {
|
||||
uris = strings.Split(text, "\n")
|
||||
}
|
||||
|
||||
if len(uris) != 1 || !strings.HasPrefix(uris[0], "file://") {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
filePath := strings.TrimPrefix(uris[0], "file://")
|
||||
info, err := os.Stat(filePath)
|
||||
if err != nil || info.IsDir() {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
cfg := m.getConfig()
|
||||
if info.Size() > cfg.MaxEntrySize {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
imgData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
_, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData))
|
||||
if err != nil {
|
||||
return nil, "", false
|
||||
}
|
||||
|
||||
return imgData, "image/" + imgFmt, true
|
||||
}
|
||||
|
||||
func sizeStr(size int) string {
|
||||
units := []string{"B", "KiB", "MiB"}
|
||||
var i int
|
||||
@@ -745,6 +842,84 @@ func (m *Manager) DeleteEntry(id uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) TouchEntry(id uint64) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
entry, err := m.GetEntry(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.Timestamp = time.Now()
|
||||
|
||||
if err := m.storeEntry(*entry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CreateHistoryEntryFromPinned(pinnedEntry *Entry) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
// Create a new unpinned entry with the same data
|
||||
newEntry := Entry{
|
||||
Data: pinnedEntry.Data,
|
||||
MimeType: pinnedEntry.MimeType,
|
||||
Size: pinnedEntry.Size,
|
||||
Timestamp: time.Now(),
|
||||
IsImage: pinnedEntry.IsImage,
|
||||
Preview: pinnedEntry.Preview,
|
||||
Pinned: false,
|
||||
}
|
||||
|
||||
if err := m.storeEntryWithoutDedup(newEntry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) storeEntryWithoutDedup(entry Entry) error {
|
||||
if m.db == nil {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
entry.Hash = computeHash(entry.Data)
|
||||
|
||||
return m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
|
||||
id, err := b.NextSequence()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entry.ID = id
|
||||
|
||||
encoded, err := encodeEntry(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := b.Put(itob(id), encoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.trimLengthInTx(b)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) ClearHistory() {
|
||||
if m.db == nil {
|
||||
return
|
||||
@@ -810,23 +985,23 @@ func (m *Manager) compactDB() error {
|
||||
tmpPath := m.dbPath + ".compact"
|
||||
defer os.Remove(tmpPath)
|
||||
|
||||
srcDB, err := bolt.Open(m.dbPath, 0644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
|
||||
srcDB, err := bolt.Open(m.dbPath, 0o644, &bolt.Options{ReadOnly: true, Timeout: time.Second})
|
||||
if err != nil {
|
||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
}
|
||||
|
||||
dstDB, err := bolt.Open(tmpPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
dstDB, err := bolt.Open(tmpPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
if err != nil {
|
||||
srcDB.Close()
|
||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("open destination: %w", err)
|
||||
}
|
||||
|
||||
if err := bolt.Compact(dstDB, srcDB, 0); err != nil {
|
||||
srcDB.Close()
|
||||
dstDB.Close()
|
||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("compact: %w", err)
|
||||
}
|
||||
|
||||
@@ -834,11 +1009,11 @@ func (m *Manager) compactDB() error {
|
||||
dstDB.Close()
|
||||
|
||||
if err := os.Rename(tmpPath, m.dbPath); err != nil {
|
||||
m.db, _ = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
m.db, _ = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
return fmt.Errorf("rename: %w", err)
|
||||
}
|
||||
|
||||
m.db, err = bolt.Open(m.dbPath, 0644, &bolt.Options{Timeout: time.Second})
|
||||
m.db, err = bolt.Open(m.dbPath, 0o644, &bolt.Options{Timeout: time.Second})
|
||||
if err != nil {
|
||||
return fmt.Errorf("reopen: %w", err)
|
||||
}
|
||||
@@ -885,11 +1060,21 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
||||
}
|
||||
})
|
||||
|
||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||
m.ownerLock.Lock()
|
||||
m.isOwner = false
|
||||
m.ownerLock.Unlock()
|
||||
})
|
||||
|
||||
m.currentSource = source
|
||||
m.sourceMutex.Lock()
|
||||
m.sourceMimeTypes = []string{mimeType}
|
||||
m.sourceMutex.Unlock()
|
||||
|
||||
m.ownerLock.Lock()
|
||||
m.isOwner = true
|
||||
m.ownerLock.Unlock()
|
||||
|
||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
||||
if err := device.SetSelection(source); err != nil {
|
||||
log.Errorf("Failed to set selection: %v", err)
|
||||
@@ -1291,6 +1476,8 @@ func (m *Manager) StoreData(data []byte, mimeType string) error {
|
||||
switch {
|
||||
case entry.IsImage:
|
||||
entry.Preview = m.imagePreview(data, mimeType)
|
||||
case mimeType == "text/uri-list":
|
||||
entry.Preview, entry.IsImage = m.uriListPreview(data)
|
||||
default:
|
||||
entry.Preview = m.textPreview(data)
|
||||
}
|
||||
@@ -1310,6 +1497,37 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
return fmt.Errorf("database not available")
|
||||
}
|
||||
|
||||
entryToPin, err := m.GetEntry(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var hashExists bool
|
||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
if b == nil {
|
||||
return nil
|
||||
}
|
||||
c := b.Cursor()
|
||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||
entry, err := decodeEntry(v)
|
||||
if err != nil || !entry.Pinned {
|
||||
continue
|
||||
}
|
||||
if entry.Hash == entryToPin.Hash {
|
||||
hashExists = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if hashExists {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check pinned count
|
||||
cfg := m.getConfig()
|
||||
pinnedCount := 0
|
||||
@@ -1334,7 +1552,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
||||
return fmt.Errorf("maximum pinned entries reached (%d)", cfg.MaxPinned)
|
||||
}
|
||||
|
||||
err := m.db.Update(func(tx *bolt.Tx) error {
|
||||
err = m.db.Update(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte("clipboard"))
|
||||
v := b.Get(itob(id))
|
||||
if v == nil {
|
||||
@@ -1454,3 +1672,240 @@ func (m *Manager) GetPinnedCount() int {
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func (m *Manager) CopyFile(filePath string) error {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
cfg := m.getConfig()
|
||||
if fileInfo.Size() > cfg.MaxEntrySize {
|
||||
return fmt.Errorf("file too large: %d > %d", fileInfo.Size(), cfg.MaxEntrySize)
|
||||
}
|
||||
|
||||
fileData, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read file: %w", err)
|
||||
}
|
||||
|
||||
exportedPath, err := m.ExportFileForFlatpak(filePath)
|
||||
if err != nil {
|
||||
exportedPath = filePath
|
||||
}
|
||||
fileURI := "file://" + exportedPath
|
||||
|
||||
if imgData, imgMime, ok := m.tryReadImageFromURI([]byte("file://" + filePath)); ok {
|
||||
entry := Entry{
|
||||
Data: imgData,
|
||||
MimeType: imgMime,
|
||||
Size: len(imgData),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: true,
|
||||
Preview: m.imagePreview(imgData, imgMime),
|
||||
}
|
||||
if err := m.storeEntry(entry); err != nil {
|
||||
log.Errorf("Failed to store file entry: %v", err)
|
||||
}
|
||||
} else {
|
||||
entry := Entry{
|
||||
Data: fileData,
|
||||
MimeType: "text/uri-list",
|
||||
Size: len(fileData),
|
||||
Timestamp: time.Now(),
|
||||
IsImage: false,
|
||||
Preview: fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)),
|
||||
}
|
||||
if err := m.storeEntry(entry); err != nil {
|
||||
log.Errorf("Failed to store file entry: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.updateState()
|
||||
m.notifySubscribers()
|
||||
|
||||
_, imgMime, imgErr := image.DecodeConfig(bytes.NewReader(fileData))
|
||||
|
||||
m.post(func() {
|
||||
if m.dataControlMgr == nil || m.dataDevice == nil {
|
||||
log.Error("Data control manager or device not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
dataMgr := m.dataControlMgr.(*ext_data_control.ExtDataControlManagerV1)
|
||||
source, err := dataMgr.CreateDataSource()
|
||||
if err != nil {
|
||||
log.Errorf("Failed to create data source: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
type offer struct {
|
||||
mime string
|
||||
data []byte
|
||||
}
|
||||
offers := []offer{
|
||||
{"x-special/gnome-copied-files", []byte("copy\n" + fileURI)},
|
||||
{"text/uri-list", []byte(fileURI + "\r\n")},
|
||||
{"text/plain", []byte(filePath)},
|
||||
}
|
||||
|
||||
if imgErr == nil {
|
||||
imgMimeType := "image/" + imgMime
|
||||
offers = append(offers, offer{imgMimeType, fileData})
|
||||
}
|
||||
|
||||
offerData := make(map[string][]byte)
|
||||
for _, o := range offers {
|
||||
if err := source.Offer(o.mime); err != nil {
|
||||
log.Errorf("Failed to offer %s: %v", o.mime, err)
|
||||
return
|
||||
}
|
||||
offerData[o.mime] = o.data
|
||||
}
|
||||
|
||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||
fd := e.Fd
|
||||
defer syscall.Close(fd)
|
||||
file := os.NewFile(uintptr(fd), "clipboard-pipe")
|
||||
defer file.Close()
|
||||
if data, ok := offerData[e.MimeType]; ok {
|
||||
file.Write(data)
|
||||
}
|
||||
})
|
||||
|
||||
source.SetCancelledHandler(func(e ext_data_control.ExtDataControlSourceV1CancelledEvent) {
|
||||
m.ownerLock.Lock()
|
||||
m.isOwner = false
|
||||
m.ownerLock.Unlock()
|
||||
})
|
||||
|
||||
m.currentSource = source
|
||||
|
||||
m.ownerLock.Lock()
|
||||
m.isOwner = true
|
||||
m.ownerLock.Unlock()
|
||||
|
||||
device := m.dataDevice.(*ext_data_control.ExtDataControlDeviceV1)
|
||||
if err := device.SetSelection(source); err != nil {
|
||||
log.Errorf("Failed to set selection: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) EntryToFile(entry *Entry) string {
|
||||
switch {
|
||||
case entry.MimeType == "text/uri-list":
|
||||
data := strings.TrimSpace(string(entry.Data))
|
||||
lines := strings.Split(data, "\n")
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
uri := strings.TrimSuffix(strings.TrimSpace(lines[0]), "\r")
|
||||
if path, ok := strings.CutPrefix(uri, "file://"); ok {
|
||||
return path
|
||||
}
|
||||
case entry.IsImage:
|
||||
ext := ".png"
|
||||
if suffix, ok := strings.CutPrefix(entry.MimeType, "image/"); ok {
|
||||
ext = "." + suffix
|
||||
}
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
clipDir := filepath.Join(cacheDir, "dms", "clipboard")
|
||||
if err := os.MkdirAll(clipDir, 0o755); err != nil {
|
||||
return ""
|
||||
}
|
||||
filePath := filepath.Join(clipDir, fmt.Sprintf("%d%s", time.Now().UnixNano(), ext))
|
||||
if os.WriteFile(filePath, entry.Data, 0o644) != nil {
|
||||
return ""
|
||||
}
|
||||
return filePath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *Manager) ExportFileForFlatpak(filePath string) (string, error) {
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
return "", fmt.Errorf("file not found: %w", err)
|
||||
}
|
||||
|
||||
if m.dbusConn == nil {
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("connect session bus: %w", err)
|
||||
}
|
||||
if !conn.SupportsUnixFDs() {
|
||||
conn.Close()
|
||||
return "", fmt.Errorf("D-Bus connection does not support Unix FD passing")
|
||||
}
|
||||
m.dbusConn = conn
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("open file: %w", err)
|
||||
}
|
||||
fd := int(file.Fd())
|
||||
|
||||
portal := m.dbusConn.Object("org.freedesktop.portal.Documents", "/org/freedesktop/portal/documents")
|
||||
|
||||
var docIds []string
|
||||
var extra map[string]dbus.Variant
|
||||
flags := uint32(0)
|
||||
|
||||
err = portal.Call(
|
||||
"org.freedesktop.portal.Documents.AddFull",
|
||||
0,
|
||||
[]dbus.UnixFD{dbus.UnixFD(fd)},
|
||||
flags,
|
||||
"",
|
||||
[]string{},
|
||||
).Store(&docIds, &extra)
|
||||
|
||||
file.Close()
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("AddFull: %w", err)
|
||||
}
|
||||
|
||||
if len(docIds) == 0 {
|
||||
return "", fmt.Errorf("no doc IDs returned")
|
||||
}
|
||||
|
||||
docId := docIds[0]
|
||||
|
||||
for _, app := range getInstalledFlatpaks() {
|
||||
_ = portal.Call(
|
||||
"org.freedesktop.portal.Documents.GrantPermissions",
|
||||
0,
|
||||
docId,
|
||||
app,
|
||||
[]string{"read"},
|
||||
).Err
|
||||
}
|
||||
|
||||
uid := os.Getuid()
|
||||
basename := filepath.Base(filePath)
|
||||
exportedPath := fmt.Sprintf("/run/user/%d/doc/%s/%s", uid, docId, basename)
|
||||
|
||||
return exportedPath, nil
|
||||
}
|
||||
|
||||
func getInstalledFlatpaks() []string {
|
||||
out, err := exec.Command("flatpak", "list", "--app", "--columns=application").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var apps []string
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if app := strings.TrimSpace(line); app != "" {
|
||||
apps = append(apps, app)
|
||||
}
|
||||
}
|
||||
return apps
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wlcontext"
|
||||
@@ -65,7 +66,7 @@ func SaveConfig(cfg Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,7 +75,7 @@ func SaveConfig(cfg Config) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
return os.WriteFile(path, data, 0o644)
|
||||
}
|
||||
|
||||
type SearchParams struct {
|
||||
@@ -157,6 +158,8 @@ type Manager struct {
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastState *State
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
|
||||
@@ -60,12 +60,44 @@ func (m *Manager) Call(bus, dest, path, iface, method string, args []any) (*Call
|
||||
obj := conn.Object(dest, dbus.ObjectPath(path))
|
||||
fullMethod := iface + "." + method
|
||||
|
||||
call := obj.Call(fullMethod, 0, args...)
|
||||
convertedArgs := convertArgs(args)
|
||||
call := obj.Call(fullMethod, 0, convertedArgs...)
|
||||
if call.Err != nil {
|
||||
return nil, fmt.Errorf("dbus call failed: %w", call.Err)
|
||||
}
|
||||
|
||||
return &CallResult{Values: call.Body}, nil
|
||||
return &CallResult{Values: dbusutil.NormalizeSlice(call.Body)}, nil
|
||||
}
|
||||
|
||||
func convertArgs(args []any) []any {
|
||||
result := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
result[i] = convertArg(arg)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertArg(arg any) any {
|
||||
switch v := arg.(type) {
|
||||
case float64:
|
||||
if v == float64(uint32(v)) && v >= 0 && v <= float64(^uint32(0)) {
|
||||
return uint32(v)
|
||||
}
|
||||
if v == float64(int32(v)) {
|
||||
return int32(v)
|
||||
}
|
||||
return v
|
||||
case []any:
|
||||
return convertArgs(v)
|
||||
case map[string]any:
|
||||
result := make(map[string]any)
|
||||
for k, val := range v {
|
||||
result[k] = convertArg(val)
|
||||
}
|
||||
return result
|
||||
default:
|
||||
return arg
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetProperty(bus, dest, path, iface, property string) (*PropertyResult, error) {
|
||||
@@ -244,9 +276,7 @@ func (m *Manager) UnsubscribeClient(clientID string) {
|
||||
})
|
||||
|
||||
for _, subID := range toDelete {
|
||||
if err := m.Unsubscribe(subID); err != nil {
|
||||
log.Warnf("dbus: failed to unsubscribe %s: %v", subID, err)
|
||||
}
|
||||
_ = m.Unsubscribe(subID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
@@ -107,22 +105,8 @@ func (m *Manager) GetUserIconFile(username string) (string, error) {
|
||||
}
|
||||
|
||||
func (m *Manager) SetIconTheme(iconTheme string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
check := exec.CommandContext(ctx, "gsettings", "writable", "org.gnome.desktop.interface", "icon-theme")
|
||||
if err := check.Run(); err == nil {
|
||||
cmd := exec.CommandContext(ctx, "gsettings", "set", "org.gnome.desktop.interface", "icon-theme", iconTheme)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("gsettings set failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
if err := utils.GsettingsSet("org.gnome.desktop.interface", "icon-theme", iconTheme); err != nil {
|
||||
return fmt.Errorf("failed to set icon theme: %w", err)
|
||||
}
|
||||
|
||||
checkDconf := exec.CommandContext(ctx, "dconf", "write", "/org/gnome/desktop/interface/icon-theme", fmt.Sprintf("'%s'", iconTheme))
|
||||
if err := checkDconf.Run(); err != nil {
|
||||
return fmt.Errorf("both gsettings and dconf unavailable or failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,4 +16,8 @@ const (
|
||||
dbusScreensaverPath = "/ScreenSaver"
|
||||
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
|
||||
dbusScreensaverInterface = "org.freedesktop.ScreenSaver"
|
||||
|
||||
dbusGnomeScreensaverName = "org.gnome.ScreenSaver"
|
||||
dbusGnomeScreensaverPath = "/org/gnome/ScreenSaver"
|
||||
dbusGnomeScreensaverInterface = "org.gnome.ScreenSaver"
|
||||
)
|
||||
|
||||
@@ -191,6 +191,12 @@ func (m *Manager) Close() {
|
||||
return true
|
||||
})
|
||||
|
||||
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
|
||||
close(ch)
|
||||
m.screensaverSubscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
if m.systemConn != nil {
|
||||
m.systemConn.Close()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
@@ -15,6 +16,51 @@ type screensaverHandler struct {
|
||||
manager *Manager
|
||||
}
|
||||
|
||||
func screensaverIntrospectIface(ifaceName string) introspect.Interface {
|
||||
return introspect.Interface{
|
||||
Name: ifaceName,
|
||||
Methods: []introspect.Method{
|
||||
{
|
||||
Name: "Inhibit",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "application_name", Type: "s", Direction: "in"},
|
||||
{Name: "reason_for_inhibit", Type: "s", Direction: "in"},
|
||||
{Name: "cookie", Type: "u", Direction: "out"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "UnInhibit",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "cookie", Type: "u", Direction: "in"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "GetActive",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "active", Type: "b", Direction: "out"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "SetActive",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "active", Type: "b", Direction: "in"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Lock",
|
||||
},
|
||||
},
|
||||
Signals: []introspect.Signal{
|
||||
{
|
||||
Name: "ActiveChanged",
|
||||
Args: []introspect.Arg{
|
||||
{Name: "new_value", Type: "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) initializeScreensaver() error {
|
||||
if m.sessionConn == nil {
|
||||
m.stateMutex.Lock()
|
||||
@@ -23,66 +69,71 @@ func (m *Manager) initializeScreensaver() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
reply, err := m.sessionConn.RequestName(dbusScreensaverName, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to request screensaver name: %v", err)
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
log.Warnf("Screensaver name already owned by another process")
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
handler := &screensaverHandler{manager: m}
|
||||
|
||||
if err := m.sessionConn.Export(handler, dbusScreensaverPath, dbusScreensaverInterface); err != nil {
|
||||
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath, err)
|
||||
m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler,
|
||||
dbusScreensaverName, dbusScreensaverInterface, dbusScreensaverPath, dbusScreensaverPath2)
|
||||
m.screensaverGnomeClaimed = m.claimScreensaverName(handler,
|
||||
dbusGnomeScreensaverName, dbusGnomeScreensaverInterface, dbusGnomeScreensaverPath)
|
||||
|
||||
if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed {
|
||||
log.Warn("No screensaver interface could be claimed")
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = false
|
||||
m.stateMutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.sessionConn.Export(handler, dbusScreensaverPath2, dbusScreensaverInterface); err != nil {
|
||||
log.Warnf("Failed to export screensaver on %s: %v", dbusScreensaverPath2, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
introNode := &introspect.Node{
|
||||
Name: dbusScreensaverPath,
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
{Name: dbusScreensaverInterface},
|
||||
},
|
||||
}
|
||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode), dbusScreensaverPath, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath, err)
|
||||
}
|
||||
|
||||
introNode2 := &introspect.Node{
|
||||
Name: dbusScreensaverPath2,
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
{Name: dbusScreensaverInterface},
|
||||
},
|
||||
}
|
||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
|
||||
}
|
||||
|
||||
go m.watchPeerDisconnects()
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.Screensaver.Available = true
|
||||
m.state.Screensaver.Active = false
|
||||
m.state.Screensaver.Inhibited = false
|
||||
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
log.Info("Screensaver inhibit listener initialized")
|
||||
log.Info("Screensaver listener initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) claimScreensaverName(handler *screensaverHandler, name, iface string, paths ...dbus.ObjectPath) bool {
|
||||
reply, err := m.sessionConn.RequestName(name, dbus.NameFlagDoNotQueue)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to request screensaver name %s: %v", name, err)
|
||||
return false
|
||||
}
|
||||
if reply != dbus.RequestNameReplyPrimaryOwner {
|
||||
log.Warnf("Screensaver name %s already owned by another process", name)
|
||||
return false
|
||||
}
|
||||
if err := m.exportScreensaverOnPaths(handler, iface, paths...); err != nil {
|
||||
log.Warnf("Failed to export screensaver on %s: %v", name, err)
|
||||
return false
|
||||
}
|
||||
log.Infof("Claimed %s on session bus", name)
|
||||
return true
|
||||
}
|
||||
|
||||
// exportScreensaverOnPaths exports the handler and introspection on the given
|
||||
// paths under the specified interface name.
|
||||
func (m *Manager) exportScreensaverOnPaths(handler *screensaverHandler, ifaceName string, paths ...dbus.ObjectPath) error {
|
||||
iface := screensaverIntrospectIface(ifaceName)
|
||||
for _, path := range paths {
|
||||
if err := m.sessionConn.Export(handler, path, ifaceName); err != nil {
|
||||
return fmt.Errorf("export handler on %s: %w", path, err)
|
||||
}
|
||||
node := &introspect.Node{
|
||||
Name: string(path),
|
||||
Interfaces: []introspect.Interface{
|
||||
introspect.IntrospectData,
|
||||
iface,
|
||||
},
|
||||
}
|
||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(node), path, "org.freedesktop.DBus.Introspectable"); err != nil {
|
||||
log.Warnf("Failed to export introspectable on %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -248,3 +299,51 @@ func (m *Manager) NotifyScreensaverSubscribers() {
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (h *screensaverHandler) GetActive() (bool, *dbus.Error) {
|
||||
h.manager.stateMutex.RLock()
|
||||
active := h.manager.state.Screensaver.Active
|
||||
h.manager.stateMutex.RUnlock()
|
||||
return active, nil
|
||||
}
|
||||
|
||||
func (h *screensaverHandler) SetActive(active bool) *dbus.Error {
|
||||
h.manager.SetScreenLockActive(active)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *screensaverHandler) Lock() *dbus.Error {
|
||||
h.manager.SetScreenLockActive(true)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetScreenLockActive(active bool) {
|
||||
m.stateMutex.Lock()
|
||||
changed := m.state.Screensaver.Active != active
|
||||
m.state.Screensaver.Active = active
|
||||
m.stateMutex.Unlock()
|
||||
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("Screen lock active changed: %v", active)
|
||||
defer m.NotifyScreensaverSubscribers()
|
||||
|
||||
if m.sessionConn == nil {
|
||||
return
|
||||
}
|
||||
if m.screensaverFreedesktopClaimed {
|
||||
if err := m.sessionConn.Emit(dbusScreensaverPath, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
|
||||
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath, err)
|
||||
}
|
||||
if err := m.sessionConn.Emit(dbusScreensaverPath2, dbusScreensaverInterface+".ActiveChanged", active); err != nil {
|
||||
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusScreensaverPath2, err)
|
||||
}
|
||||
}
|
||||
if m.screensaverGnomeClaimed {
|
||||
if err := m.sessionConn.Emit(dbusGnomeScreensaverPath, dbusGnomeScreensaverInterface+".ActiveChanged", active); err != nil {
|
||||
log.Warnf("Failed to emit ActiveChanged on %s: %v", dbusGnomeScreensaverPath, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
102
core/internal/server/freedesktop/screensaver_test.go
Normal file
102
core/internal/server/freedesktop/screensaver_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package freedesktop
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetScreenLockActive_ChangesState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
assert.False(t, manager.GetScreensaverState().Active)
|
||||
|
||||
manager.SetScreenLockActive(true)
|
||||
assert.True(t, manager.GetScreensaverState().Active)
|
||||
|
||||
manager.SetScreenLockActive(false)
|
||||
assert.False(t, manager.GetScreensaverState().Active)
|
||||
}
|
||||
|
||||
func TestSetScreenLockActive_NoChangeNoDuplicate(t *testing.T) {
|
||||
ch := make(chan ScreensaverState, 64)
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true, Active: false},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
manager.screensaverSubscribers.Store("test", ch)
|
||||
defer manager.screensaverSubscribers.Delete("test")
|
||||
|
||||
// Setting to same value should not notify
|
||||
manager.SetScreenLockActive(false)
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
t.Fatal("should not have received notification for no-change")
|
||||
case <-time.After(50 * time.Millisecond):
|
||||
// Expected: no notification
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetScreenLockActive_NotifiesSubscribers(t *testing.T) {
|
||||
ch := make(chan ScreensaverState, 64)
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true, Active: false},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
manager.screensaverSubscribers.Store("test", ch)
|
||||
defer manager.screensaverSubscribers.Delete("test")
|
||||
|
||||
manager.SetScreenLockActive(true)
|
||||
|
||||
select {
|
||||
case state := <-ch:
|
||||
assert.True(t, state.Active)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timeout waiting for subscriber notification")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetScreenLockActive_NilSessionConn(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
assert.NotPanics(t, func() {
|
||||
manager.SetScreenLockActive(true)
|
||||
})
|
||||
assert.True(t, manager.GetScreensaverState().Active)
|
||||
}
|
||||
|
||||
func TestGetActive_ReturnsCurrentState(t *testing.T) {
|
||||
manager := &Manager{
|
||||
state: &FreedeskState{
|
||||
Screensaver: ScreensaverState{Available: true, Active: true},
|
||||
},
|
||||
stateMutex: sync.RWMutex{},
|
||||
}
|
||||
|
||||
handler := &screensaverHandler{manager: manager}
|
||||
active, dbusErr := handler.GetActive()
|
||||
assert.Nil(t, dbusErr)
|
||||
assert.True(t, active)
|
||||
}
|
||||
|
||||
func TestScreensaverState_ActiveDefaultsFalse(t *testing.T) {
|
||||
state := ScreensaverState{}
|
||||
assert.False(t, state.Active)
|
||||
}
|
||||
@@ -39,6 +39,7 @@ type ScreensaverInhibitor struct {
|
||||
|
||||
type ScreensaverState struct {
|
||||
Available bool `json:"available"`
|
||||
Active bool `json:"active"`
|
||||
Inhibited bool `json:"inhibited"`
|
||||
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
|
||||
}
|
||||
@@ -50,14 +51,16 @@ type FreedeskState struct {
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *FreedeskState
|
||||
stateMutex sync.RWMutex
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
accountsObj dbus.BusObject
|
||||
settingsObj dbus.BusObject
|
||||
currentUID uint64
|
||||
subscribers syncmap.Map[string, chan FreedeskState]
|
||||
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
||||
screensaverCookieCounter uint32
|
||||
state *FreedeskState
|
||||
stateMutex sync.RWMutex
|
||||
systemConn *dbus.Conn
|
||||
sessionConn *dbus.Conn
|
||||
accountsObj dbus.BusObject
|
||||
settingsObj dbus.BusObject
|
||||
currentUID uint64
|
||||
subscribers syncmap.Map[string, chan FreedeskState]
|
||||
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
||||
screensaverCookieCounter uint32
|
||||
screensaverFreedesktopClaimed bool
|
||||
screensaverGnomeClaimed bool
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ type SecretAgent struct {
|
||||
backend *NetworkManagerBackend
|
||||
}
|
||||
|
||||
type nmVariantMap map[string]dbus.Variant
|
||||
type nmSettingMap map[string]nmVariantMap
|
||||
type (
|
||||
nmVariantMap map[string]dbus.Variant
|
||||
nmSettingMap map[string]nmVariantMap
|
||||
)
|
||||
|
||||
const introspectXML = `
|
||||
<node>
|
||||
@@ -122,7 +124,7 @@ func (a *SecretAgent) GetSecrets(
|
||||
|
||||
connType, displayName, vpnSvc := readConnTypeAndName(conn)
|
||||
ssid := readSSID(conn)
|
||||
fields := fieldsNeeded(settingName, hints)
|
||||
fields := fieldsNeeded(settingName, hints, conn)
|
||||
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
|
||||
|
||||
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
|
||||
@@ -218,8 +220,16 @@ func (a *SecretAgent) GetSecrets(
|
||||
out[settingName] = nmVariantMap{}
|
||||
return out, nil
|
||||
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
|
||||
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
switch settingName {
|
||||
case "802-11-wireless-security":
|
||||
fields = []string{"psk"}
|
||||
case "802-1x":
|
||||
fields = infer8021xFields(conn)
|
||||
default:
|
||||
log.Warnf("[SecretAgent] Agent-owned secrets for unhandled setting %s (flags=%d)", settingName, passwordFlags)
|
||||
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
|
||||
}
|
||||
log.Infof("[SecretAgent] Agent-owned secrets, inferred fields: %v", fields)
|
||||
} else {
|
||||
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
|
||||
out := nmSettingMap{}
|
||||
@@ -300,6 +310,63 @@ func (a *SecretAgent) GetSecrets(
|
||||
return out, nil
|
||||
}
|
||||
a.backend.cachedVPNCredsMu.Unlock()
|
||||
|
||||
a.backend.cachedGPSamlMu.Lock()
|
||||
cachedGPSaml := a.backend.cachedGPSamlCookie
|
||||
if cachedGPSaml != nil && cachedGPSaml.ConnectionUUID == connUuid {
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
|
||||
log.Infof("[SecretAgent] Using cached GlobalProtect SAML cookie for %s", connUuid)
|
||||
|
||||
return buildGPSamlSecretsResponse(settingName, cachedGPSaml.Cookie, cachedGPSaml.Host, cachedGPSaml.Fingerprint), nil
|
||||
}
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
|
||||
if len(fields) == 1 && fields[0] == "gp-saml" {
|
||||
gateway := ""
|
||||
protocol := ""
|
||||
if vpnSettings, ok := conn["vpn"]; ok {
|
||||
if dataVariant, ok := vpnSettings["data"]; ok {
|
||||
if dataMap, ok := dataVariant.Value().(map[string]string); ok {
|
||||
if gw, ok := dataMap["gateway"]; ok {
|
||||
gateway = gw
|
||||
}
|
||||
if proto, ok := dataMap["protocol"]; ok && proto != "" {
|
||||
protocol = proto
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if protocol != "gp" {
|
||||
return nil, dbus.MakeFailedError(fmt.Errorf("gp-saml auth only supported for GlobalProtect (protocol=gp), got: %s", protocol))
|
||||
}
|
||||
|
||||
log.Infof("[SecretAgent] Starting GlobalProtect SAML authentication for gateway=%s", gateway)
|
||||
|
||||
samlCtx, samlCancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer samlCancel()
|
||||
|
||||
authResult, err := a.backend.runGlobalProtectSAMLAuth(samlCtx, gateway, protocol)
|
||||
if err != nil {
|
||||
log.Warnf("[SecretAgent] GlobalProtect SAML authentication failed: %v", err)
|
||||
return nil, dbus.MakeFailedError(fmt.Errorf("GlobalProtect SAML authentication failed: %w", err))
|
||||
}
|
||||
|
||||
log.Infof("[SecretAgent] GlobalProtect SAML authentication successful, returning cookie to NetworkManager")
|
||||
|
||||
a.backend.cachedGPSamlMu.Lock()
|
||||
a.backend.cachedGPSamlCookie = &cachedGPSamlCookie{
|
||||
ConnectionUUID: connUuid,
|
||||
Cookie: authResult.Cookie,
|
||||
Host: authResult.Host,
|
||||
User: authResult.User,
|
||||
Fingerprint: authResult.Fingerprint,
|
||||
}
|
||||
a.backend.cachedGPSamlMu.Unlock()
|
||||
|
||||
return buildGPSamlSecretsResponse(settingName, authResult.Cookie, authResult.Host, authResult.Fingerprint), nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
@@ -418,8 +485,19 @@ func (a *SecretAgent) GetSecrets(
|
||||
log.Infof("[SecretAgent] Cached PKCS11 PIN for potential re-request")
|
||||
}
|
||||
case "802-1x":
|
||||
out[settingName] = sec
|
||||
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(sec))
|
||||
secretsOnly := nmVariantMap{}
|
||||
for k, v := range reply.Secrets {
|
||||
switch k {
|
||||
case "password", "private-key-password", "phase2-private-key-password", "pin":
|
||||
secretsOnly[k] = dbus.MakeVariant(v)
|
||||
}
|
||||
}
|
||||
out[settingName] = secretsOnly
|
||||
|
||||
if identity, ok := reply.Secrets["identity"]; ok && identity != "" {
|
||||
a.save8021xIdentity(path, identity)
|
||||
}
|
||||
log.Infof("[SecretAgent] Returning 802-1x enterprise secrets with %d fields", len(secretsOnly))
|
||||
default:
|
||||
out[settingName] = sec
|
||||
}
|
||||
@@ -434,63 +512,6 @@ func (a *SecretAgent) GetSecrets(
|
||||
}
|
||||
a.backend.pendingVPNSaveMu.Unlock()
|
||||
log.Infof("[SecretAgent] Queued credentials persist for after connection succeeds")
|
||||
} else if reply.Save && settingName != "vpn" {
|
||||
// Non-VPN save logic
|
||||
go func() {
|
||||
log.Infof("[SecretAgent] Persisting secrets with Update2: path=%s, setting=%s", path, settingName)
|
||||
|
||||
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
|
||||
var existingSettings map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existingSettings); err != nil {
|
||||
log.Warnf("[SecretAgent] GetSettings failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
settings := make(map[string]map[string]dbus.Variant)
|
||||
if connSection, ok := existingSettings["connection"]; ok {
|
||||
settings["connection"] = connSection
|
||||
}
|
||||
|
||||
switch settingName {
|
||||
case "802-11-wireless-security":
|
||||
wifiSec, ok := existingSettings["802-11-wireless-security"]
|
||||
if !ok {
|
||||
wifiSec = make(map[string]dbus.Variant)
|
||||
}
|
||||
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
|
||||
|
||||
if psk, ok := reply.Secrets["psk"]; ok {
|
||||
wifiSec["psk"] = dbus.MakeVariant(psk)
|
||||
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
|
||||
}
|
||||
settings["802-11-wireless-security"] = wifiSec
|
||||
|
||||
case "802-1x":
|
||||
dot1x, ok := existingSettings["802-1x"]
|
||||
if !ok {
|
||||
dot1x = make(map[string]dbus.Variant)
|
||||
}
|
||||
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
|
||||
|
||||
if password, ok := reply.Secrets["password"]; ok {
|
||||
dot1x["password"] = dbus.MakeVariant(password)
|
||||
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
|
||||
}
|
||||
settings["802-1x"] = dot1x
|
||||
}
|
||||
|
||||
// Call Update2 with correct signature:
|
||||
// Update2(IN settings, IN flags, IN args) -> OUT result
|
||||
// flags: 0x1 = to-disk
|
||||
var result map[string]dbus.Variant
|
||||
err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result)
|
||||
if err != nil {
|
||||
log.Warnf("[SecretAgent] Update2(to-disk) failed: %v", err)
|
||||
} else {
|
||||
log.Infof("[SecretAgent] Successfully persisted secrets to disk for %s", settingName)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return out, nil
|
||||
@@ -523,6 +544,35 @@ func (a *SecretAgent) Introspect() (string, *dbus.Error) {
|
||||
return introspectXML, nil
|
||||
}
|
||||
|
||||
func (a *SecretAgent) save8021xIdentity(path dbus.ObjectPath, identity string) {
|
||||
connObj := a.conn.Object("org.freedesktop.NetworkManager", path)
|
||||
var existing map[string]map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.GetSettings", 0).Store(&existing); err != nil {
|
||||
log.Warnf("[SecretAgent] Failed to get settings for identity save: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
settings := make(map[string]map[string]dbus.Variant)
|
||||
if connSection, ok := existing["connection"]; ok {
|
||||
settings["connection"] = connSection
|
||||
}
|
||||
|
||||
dot1x, ok := existing["802-1x"]
|
||||
if !ok {
|
||||
dot1x = make(map[string]dbus.Variant)
|
||||
}
|
||||
dot1x["identity"] = dbus.MakeVariant(identity)
|
||||
settings["802-1x"] = dot1x
|
||||
|
||||
var result map[string]dbus.Variant
|
||||
if err := connObj.Call("org.freedesktop.NetworkManager.Settings.Connection.Update2", 0,
|
||||
settings, uint32(0x1), map[string]dbus.Variant{}).Store(&result); err != nil {
|
||||
log.Warnf("[SecretAgent] Failed to save 802.1x identity: %v", err)
|
||||
return
|
||||
}
|
||||
log.Infof("[SecretAgent] Saved 802.1x identity to connection profile")
|
||||
}
|
||||
|
||||
func readSSID(conn map[string]nmVariantMap) string {
|
||||
if w, ok := conn["802-11-wireless"]; ok {
|
||||
if v, ok := w["ssid"]; ok {
|
||||
@@ -564,12 +614,15 @@ func readConnTypeAndName(conn map[string]nmVariantMap) (string, string, string)
|
||||
return connType, name, svc
|
||||
}
|
||||
|
||||
func fieldsNeeded(setting string, hints []string) []string {
|
||||
func fieldsNeeded(setting string, hints []string, conn map[string]nmVariantMap) []string {
|
||||
switch setting {
|
||||
case "802-11-wireless-security":
|
||||
return []string{"psk"}
|
||||
case "802-1x":
|
||||
return []string{"identity", "password"}
|
||||
if len(hints) > 0 {
|
||||
return hints
|
||||
}
|
||||
return infer8021xFields(conn)
|
||||
case "vpn":
|
||||
return hints
|
||||
default:
|
||||
@@ -577,6 +630,41 @@ func fieldsNeeded(setting string, hints []string) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func infer8021xFields(conn map[string]nmVariantMap) []string {
|
||||
dot1x, ok := conn["802-1x"]
|
||||
if !ok {
|
||||
return []string{"identity", "password"}
|
||||
}
|
||||
|
||||
var fields []string
|
||||
|
||||
if v, ok := dot1x["identity"]; ok {
|
||||
if id, ok := v.Value().(string); ok && id != "" {
|
||||
// identity already stored, don't ask again
|
||||
} else {
|
||||
fields = append(fields, "identity")
|
||||
}
|
||||
} else {
|
||||
fields = append(fields, "identity")
|
||||
}
|
||||
|
||||
var eapMethods []string
|
||||
if v, ok := dot1x["eap"]; ok {
|
||||
if methods, ok := v.Value().([]string); ok {
|
||||
eapMethods = methods
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(eapMethods) > 0 && eapMethods[0] == "tls":
|
||||
fields = append(fields, "private-key-password")
|
||||
default:
|
||||
fields = append(fields, "password")
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
|
||||
func buildFieldsInfo(setting string, fields []string, vpnService string) []FieldInfo {
|
||||
result := make([]FieldInfo, 0, len(fields))
|
||||
for _, f := range fields {
|
||||
@@ -630,12 +718,25 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
||||
|
||||
switch {
|
||||
case strings.Contains(vpnService, "openconnect"):
|
||||
protocol := dataMap["protocol"]
|
||||
authType := dataMap["authtype"]
|
||||
userCert := dataMap["usercert"]
|
||||
if authType == "cert" && strings.HasPrefix(userCert, "pkcs11:") {
|
||||
username := dataMap["username"]
|
||||
|
||||
if authType == "cert" && strings.HasPrefix(dataMap["usercert"], "pkcs11:") {
|
||||
return []string{"key_pass"}
|
||||
}
|
||||
if dataMap["username"] == "" {
|
||||
|
||||
if needsExternalBrowserAuth(protocol, authType, username, dataMap) {
|
||||
switch protocol {
|
||||
case "gp":
|
||||
log.Infof("[SecretAgent] GlobalProtect SAML auth detected")
|
||||
return []string{"gp-saml"}
|
||||
default:
|
||||
log.Infof("[SecretAgent] External browser auth detected for protocol '%s' but only GlobalProtect (gp) SAML is currently supported, falling back to credentials", protocol)
|
||||
}
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
fields = []string{"username", "password"}
|
||||
}
|
||||
case strings.Contains(vpnService, "openvpn"):
|
||||
@@ -654,8 +755,31 @@ func inferVPNFields(conn map[string]nmVariantMap, vpnService string) []string {
|
||||
return fields
|
||||
}
|
||||
|
||||
func needsExternalBrowserAuth(protocol, authType, username string, data map[string]string) bool {
|
||||
if method, ok := data["saml-auth-method"]; ok {
|
||||
if method == "REDIRECT" || method == "POST" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if authType != "" && authType != "password" && authType != "cert" {
|
||||
return true
|
||||
}
|
||||
|
||||
switch protocol {
|
||||
case "gp":
|
||||
if authType == "" && username == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func vpnFieldMeta(field, vpnService string) (label string, isSecret bool) {
|
||||
switch field {
|
||||
case "gp-saml":
|
||||
return "GlobalProtect SAML/SSO", false
|
||||
case "key_pass":
|
||||
return "PIN", true
|
||||
case "password":
|
||||
@@ -756,3 +880,18 @@ func reasonFromFlags(flags uint32) string {
|
||||
}
|
||||
return "required"
|
||||
}
|
||||
|
||||
func buildGPSamlSecretsResponse(settingName, cookie, host, fingerprint string) nmSettingMap {
|
||||
out := nmSettingMap{}
|
||||
vpnSec := nmVariantMap{}
|
||||
|
||||
secrets := map[string]string{
|
||||
"cookie": cookie,
|
||||
"gateway": host,
|
||||
"gwcert": fingerprint,
|
||||
}
|
||||
vpnSec["secrets"] = dbus.MakeVariant(secrets)
|
||||
|
||||
out[settingName] = vpnSec
|
||||
return out
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user