mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-02 02:22:06 -04:00
Compare commits
264 Commits
v1.4.1
...
eb5afcdc40
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5afcdc40 | ||
|
|
dd2a3f3efc | ||
|
|
161fa2dba5 | ||
|
|
f5f50f7add | ||
|
|
372cf2f566 | ||
|
|
b70acbc283 | ||
|
|
571c1158bf | ||
|
|
ac03a2e4b2 | ||
|
|
07460dc3b7 | ||
|
|
7412fee590 | ||
|
|
85c2954958 | ||
|
|
7c9e805cbe | ||
|
|
6926470b04 | ||
|
|
713ba1efbb | ||
|
|
1919ca7243 | ||
|
|
bcf41ed5ca | ||
|
|
5033bdc630 | ||
|
|
b8bfaf9a26 | ||
|
|
da45714c54 | ||
|
|
0c2d00b79c | ||
|
|
c10b42f599 | ||
|
|
4c617cf022 | ||
|
|
e75b95b854 | ||
|
|
6b15670918 | ||
|
|
c52b9e19a1 | ||
|
|
7a3444bd30 | ||
|
|
a733d760e4 | ||
|
|
1b33079e39 | ||
|
|
1cf0dd1031 | ||
|
|
8d49a5cbfc | ||
|
|
f5928b09d3 | ||
|
|
38373aa5f2 | ||
|
|
665680e15e | ||
|
|
210607cfbc | ||
|
|
69fca14611 | ||
|
|
10a235e686 | ||
|
|
253cc7f8a3 | ||
|
|
a63ad99684 | ||
|
|
c44c032879 | ||
|
|
dc1ce55971 | ||
|
|
9f65882a12 | ||
|
|
97bf83cce6 | ||
|
|
96bf0162d6 | ||
|
|
73b833731a | ||
|
|
84522aeaad | ||
|
|
faf1a277d2 | ||
|
|
60515736e6 | ||
|
|
1715e2eab7 | ||
|
|
4e14cf5cce | ||
|
|
a644c93b1b | ||
|
|
f9428a1009 | ||
|
|
b4b51785e5 | ||
|
|
0a97df6d49 | ||
|
|
352ba77677 | ||
|
|
d320035d97 | ||
|
|
8d262a9555 | ||
|
|
9bfa8310d2 | ||
|
|
088ed806ae | ||
|
|
07d2c94676 | ||
|
|
0bc1b7a3c2 | ||
|
|
c5987b28c0 | ||
|
|
18901c7cde | ||
|
|
519a8357a1 | ||
|
|
799773c62b | ||
|
|
247a674c79 | ||
|
|
72b598057c | ||
|
|
8180e30e8e | ||
|
|
dd9851b4f0 | ||
|
|
056576fbe7 | ||
|
|
d28a5bdf7f | ||
|
|
f62ea119f7 | ||
|
|
a1f9b98727 | ||
|
|
b9c8914d46 | ||
|
|
8697840d46 | ||
|
|
1f64bb8031 | ||
|
|
eea7d12c0b | ||
|
|
85173126f4 | ||
|
|
222187d8a6 | ||
|
|
bef3f65f63 | ||
|
|
bff83fe563 | ||
|
|
cbf00d133a | ||
|
|
347f06b758 | ||
|
|
9070903512 | ||
|
|
e9d030f6d8 | ||
|
|
fbf9e6d1b9 | ||
|
|
e803812344 | ||
|
|
9a64f2acf0 | ||
|
|
c647eafadc | ||
|
|
720ec07d13 | ||
|
|
4b4334e611 | ||
|
|
b69a96e80b | ||
|
|
1e6a73fd60 | ||
|
|
60b6280750 | ||
|
|
9e079f8a4b | ||
|
|
62c2e858ef | ||
|
|
78357d45bb | ||
|
|
3ff9564c9b | ||
|
|
b0989cecad | ||
|
|
47be6a1033 | ||
|
|
31b415b086 | ||
|
|
7156e1e299 | ||
|
|
c72c9bfb08 | ||
|
|
73c75fcc2c | ||
|
|
2ff42eba41 | ||
|
|
9f13465cd7 | ||
|
|
366a98e0cc | ||
|
|
31aeb8dc4b | ||
|
|
c4e7f3d62f | ||
|
|
a1d13f276a | ||
|
|
dbf132d633 | ||
|
|
59451890f1 | ||
|
|
e633c9e039 | ||
|
|
6c1fff2df1 | ||
|
|
3891d125d1 | ||
|
|
997011e008 | ||
|
|
2504396435 | ||
|
|
d206723b36 | ||
|
|
a0ec3d59b8 | ||
|
|
17ef08aa58 | ||
|
|
57279d1c53 | ||
|
|
8b003ac9cd | ||
|
|
0ea10b0ad2 | ||
|
|
2db4c9daa0 | ||
|
|
363964e90b | ||
|
|
a7b49eba70 | ||
|
|
4ae334f60f | ||
|
|
86c0064ff9 | ||
|
|
5a6b52f07f | ||
|
|
5aaa56853f | ||
|
|
35913c22f5 | ||
|
|
d7b560573c | ||
|
|
02a274ebe2 | ||
|
|
fc7b61c20b | ||
|
|
5880043f56 | ||
|
|
fee3b7f2a7 | ||
|
|
c0b0339fca | ||
|
|
26c1e62204 | ||
|
|
7b2d4dbe30 | ||
|
|
78c5d46c6b | ||
|
|
3fb85df504 | ||
|
|
227dd24726 | ||
|
|
ae6a656899 | ||
|
|
a4055e0f01 | ||
|
|
6d98c229ef | ||
|
|
71d93ad85e | ||
|
|
4ec21fcd3d | ||
|
|
0a2fe03fee | ||
|
|
4f4745609b | ||
|
|
a69cd515fb | ||
|
|
06c4b97a6b | ||
|
|
a6cf71a190 | ||
|
|
21750156dc | ||
|
|
f9b737f543 | ||
|
|
246b59f3b9 | ||
|
|
dcda81ea64 | ||
|
|
9909b665cd | ||
|
|
4bcd786be3 | ||
|
|
64c9222000 | ||
|
|
12acf2dd51 | ||
|
|
fea97b4aad | ||
|
|
c6d398eeac | ||
|
|
7a74be83d7 | ||
|
|
67a6427418 | ||
|
|
18b20d3225 | ||
|
|
8a76885fb6 | ||
|
|
69b1e61ab7 | ||
|
|
3f24cf37ca | ||
|
|
01218f34cb | ||
|
|
9da58d8296 | ||
|
|
af0038e634 | ||
|
|
05c312b9eb | ||
|
|
89d5c958c4 | ||
|
|
e4d86ad595 | ||
|
|
532b54a028 | ||
|
|
504d027c3f | ||
|
|
e8f95f4533 | ||
|
|
b83256c83a | ||
|
|
8e2cd21be8 | ||
|
|
c5413608da | ||
|
|
586bcad442 | ||
|
|
3b3d10f730 | ||
|
|
4834891b36 | ||
|
|
f60e65aecb | ||
|
|
01387b0123 | ||
|
|
1476658c23 | ||
|
|
7861c6e316 | ||
|
|
d2247d7b24 | ||
|
|
2ff78d4a02 | ||
|
|
785243ce5f | ||
|
|
0e1b868384 | ||
|
|
2b08e800e8 | ||
|
|
74e4f8ea1e | ||
|
|
9c58569b4c | ||
|
|
29de677e00 | ||
|
|
fae4944845 | ||
|
|
07a0ac4b7d | ||
|
|
b2d8f4d73b | ||
|
|
fe58c45233 | ||
|
|
3ea4e389eb | ||
|
|
7276f295fc | ||
|
|
93ed96a789 | ||
|
|
bea325e94c | ||
|
|
2f8f1c30ad | ||
|
|
f859a14173 | ||
|
|
153f39da48 | ||
|
|
e4accdd1c7 | ||
|
|
a2c89e0a8c | ||
|
|
e282831c2e | ||
|
|
5c5ff6195a | ||
|
|
c4bbf54679 | ||
|
|
98acafb4b8 | ||
|
|
da20681fc0 | ||
|
|
b38cb961b2 | ||
|
|
7a0bb07518 | ||
|
|
403e3e90a2 | ||
|
|
50b91f14b6 | ||
|
|
b3df47fce0 | ||
|
|
09bd65d746 | ||
|
|
020d56ab7f | ||
|
|
f3bee65da9 | ||
|
|
b14b0946e2 | ||
|
|
ca44205f1c | ||
|
|
2d39e8fd2a | ||
|
|
6d4df6e927 | ||
|
|
b8ab86e6c0 | ||
|
|
837329a6d8 | ||
|
|
8c6c2ffd23 | ||
|
|
ad3c8b6755 | ||
|
|
03a8e1e0d5 | ||
|
|
4d4d3c20a1 | ||
|
|
cef16d6bc9 | ||
|
|
aafaad1791 | ||
|
|
7906fdc2b0 | ||
|
|
397650ca52 | ||
|
|
826207006a | ||
|
|
58c2fcd31c | ||
|
|
b2a2b425ec | ||
|
|
942c9c9609 | ||
|
|
46d6e1cff3 | ||
|
|
a4137c57c1 | ||
|
|
1ad8b627f1 | ||
|
|
58a02ce290 | ||
|
|
8e1ad1a2be | ||
|
|
68cd7ab32c | ||
|
|
f649ce9a8e | ||
|
|
c4df242f07 | ||
|
|
26846c8d55 | ||
|
|
31b44a667c | ||
|
|
4f3b73ee21 | ||
|
|
4cfae91f02 | ||
|
|
8d947a6e95 | ||
|
|
1e84d4252c | ||
|
|
76072e1d4c | ||
|
|
6408dce4a9 | ||
|
|
0b2e1cca38 | ||
|
|
c1bfd8c0b7 | ||
|
|
90ffa5833b | ||
|
|
169c669286 | ||
|
|
f8350deafc | ||
|
|
0286a1b80b | ||
|
|
7c3e6c1f02 | ||
|
|
d2d72db3c9 | ||
|
|
f81f861408 | ||
|
|
af494543f5 |
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
69
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,32 +7,32 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## DankMaterialShell Bug Report
|
## DankMaterialShell Bug Report
|
||||||
Limit your report to one issue per submission unless closely related
|
Limit your report to one issue per submission unless similarly related
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor
|
label: Compositor
|
||||||
options:
|
options:
|
||||||
- label: Niri
|
- Niri
|
||||||
- label: Hyprland
|
- Hyprland
|
||||||
- label: MangoWC (dwl)
|
- MangoWC (dwl)
|
||||||
- label: Sway
|
- Sway
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: distribution
|
id: distribution
|
||||||
attributes:
|
attributes:
|
||||||
label: Distribution
|
label: Distribution
|
||||||
options:
|
options:
|
||||||
- label: Arch Linux
|
- Arch Linux
|
||||||
- label: CachyOS
|
- CachyOS
|
||||||
- label: Fedora
|
- Fedora
|
||||||
- label: NixOS
|
- NixOS
|
||||||
- label: Debian
|
- Debian
|
||||||
- label: Ubuntu
|
- Ubuntu
|
||||||
- label: Gentoo
|
- Gentoo
|
||||||
- label: OpenSUSE
|
- OpenSUSE
|
||||||
- label: Other (specify below)
|
- Other (specify below)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: input
|
- type: input
|
||||||
@@ -42,12 +42,45 @@ body:
|
|||||||
placeholder: e.g., PikaOS, Void Linux, etc.
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: installation_method
|
||||||
|
attributes:
|
||||||
|
label: Select your Installation Method
|
||||||
|
options:
|
||||||
|
- DankInstaller
|
||||||
|
- Distro Packaging
|
||||||
|
- Source
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: original_installation_method
|
||||||
|
attributes:
|
||||||
|
label: Was this your original Installation method?
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- No (specify below)
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: original_installation_method_specify
|
||||||
|
attributes:
|
||||||
|
label: If no, specify
|
||||||
|
placeholder: e.g., Distro Packaging, then Source
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -vC
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -vC` command
|
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
|
||||||
placeholder: Paste the output of `dms doctor -vC` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@@ -69,7 +102,7 @@ body:
|
|||||||
- type: textarea
|
- type: textarea
|
||||||
id: steps_to_reproduce
|
id: steps_to_reproduce
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce & Installation Method
|
label: Steps to Reproduce
|
||||||
description: Please provide detailed steps to reproduce the issue
|
description: Please provide detailed steps to reproduce the issue
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. ...
|
1. ...
|
||||||
|
|||||||
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
21
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -23,18 +23,25 @@ body:
|
|||||||
placeholder: Why is this feature important?
|
placeholder: Why is this feature important?
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor(s)
|
label: Compositor(s)
|
||||||
description: Is this feature specific to one or more compositors?
|
description: Is this feature specific to one or more compositors?
|
||||||
options:
|
options:
|
||||||
- label: All compositors
|
- All compositors
|
||||||
- label: Niri
|
- Niri
|
||||||
- label: Hyprland
|
- Hyprland
|
||||||
- label: MangoWC (dwl)
|
- MangoWC (dwl)
|
||||||
- label: Sway
|
- Sway
|
||||||
- label: Other (specify below)
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: compositor_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., Wayfire, Mutter, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
75
.github/ISSUE_TEMPLATE/support_request.yml
vendored
75
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@@ -7,32 +7,87 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
## DankMaterialShell Support Request
|
## DankMaterialShell Support Request
|
||||||
- type: checkboxes
|
- type: dropdown
|
||||||
id: compositor
|
id: compositor
|
||||||
attributes:
|
attributes:
|
||||||
label: Compositor
|
label: Compositor
|
||||||
options:
|
options:
|
||||||
- label: Niri
|
- Niri
|
||||||
- label: Hyprland
|
- Hyprland
|
||||||
- label: MangoWC (dwl)
|
- MangoWC (dwl)
|
||||||
- label: Sway
|
- Sway
|
||||||
- label: Other (specify below)
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: compositor_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., Wayfire, Mutter, etc.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: dropdown
|
||||||
id: distribution
|
id: distribution
|
||||||
attributes:
|
attributes:
|
||||||
label: Distribution
|
label: Distribution
|
||||||
description: Which Linux distribution are you using? (e.g., Arch, Fedora, Debian, etc.)
|
options:
|
||||||
placeholder: Your Linux distribution
|
- Arch Linux
|
||||||
|
- CachyOS
|
||||||
|
- Fedora
|
||||||
|
- NixOS
|
||||||
|
- Debian
|
||||||
|
- Ubuntu
|
||||||
|
- Gentoo
|
||||||
|
- OpenSUSE
|
||||||
|
- Other (specify below)
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: input
|
||||||
|
id: distribution_other
|
||||||
|
attributes:
|
||||||
|
label: If Other, please specify
|
||||||
|
placeholder: e.g., PikaOS, Void Linux, etc.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: dropdown
|
||||||
|
id: installation_method
|
||||||
|
attributes:
|
||||||
|
label: Select your Installation Method
|
||||||
|
options:
|
||||||
|
- DankInstaller
|
||||||
|
- Distro Packaging
|
||||||
|
- Source
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: original_installation_method_different
|
||||||
|
attributes:
|
||||||
|
label: Was your original Installation method different?
|
||||||
|
options:
|
||||||
|
- "Yes"
|
||||||
|
- No (specify below)
|
||||||
|
default: 0
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
- type: input
|
||||||
|
id: original_installation_method_specify
|
||||||
|
attributes:
|
||||||
|
label: If no, specify
|
||||||
|
placeholder: e.g., Distro Packaging, then Source
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: dms_doctor
|
id: dms_doctor
|
||||||
attributes:
|
attributes:
|
||||||
label: dms doctor -vC
|
label: dms doctor -vC
|
||||||
description: Output of `dms doctor -vC` command
|
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||||
placeholder: Paste the output of `dms doctor -vC` here
|
placeholder: Paste the output of `dms doctor -vC` here
|
||||||
|
value: |
|
||||||
|
<details>
|
||||||
|
<summary>Click to expand</summary>
|
||||||
|
|
||||||
|
|
||||||
|
</details>
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: textarea
|
- type: textarea
|
||||||
|
|||||||
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
|
|
||||||
215
.github/workflows/run-obs.yml
vendored
215
.github/workflows/run-obs.yml
vendored
@@ -10,6 +10,7 @@ on:
|
|||||||
options:
|
options:
|
||||||
- dms
|
- dms
|
||||||
- dms-git
|
- dms-git
|
||||||
|
- dms-greeter
|
||||||
- all
|
- all
|
||||||
default: "dms"
|
default: "dms"
|
||||||
rebuild_release:
|
rebuild_release:
|
||||||
@@ -72,12 +73,27 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Helper function to check dms-greeter stable tag
|
||||||
|
check_dms_greeter_stable() {
|
||||||
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
|
local OBS_SPEC=$(curl -s -u "$OBS_USERNAME:$OBS_PASSWORD" "https://api.opensuse.org/source/home:AvengeMedia:danklinux/dms-greeter/dms-greeter.spec" 2>/dev/null || echo "")
|
||||||
|
local OBS_VERSION=$(echo "$OBS_SPEC" | grep "^Version:" | awk '{print $2}' | xargs | sed 's/^v//')
|
||||||
|
|
||||||
|
if [[ -n "$LATEST_TAG" && "$LATEST_TAG" == "v$OBS_VERSION" ]]; then
|
||||||
|
echo "📋 dms-greeter: Tag $LATEST_TAG already exists, skipping"
|
||||||
|
return 1 # No update needed
|
||||||
|
else
|
||||||
|
echo "📋 dms-greeter: New tag ${LATEST_TAG:-unknown} (OBS has ${OBS_VERSION:-none})"
|
||||||
|
return 0 # Update needed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Main logic
|
# Main logic
|
||||||
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
REBUILD="${{ github.event.inputs.rebuild_release }}"
|
||||||
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]] && [[ -z "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Tag selected or pushed - always update stable package
|
# Run from tag with no package specified - update both stable packages
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
echo "packages=dms dms-greeter" >> $GITHUB_OUTPUT
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
VERSION="${GITHUB_REF#refs/tags/}"
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
@@ -112,6 +128,10 @@ jobs:
|
|||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
if check_dms_greeter_stable; then
|
||||||
|
PACKAGES_TO_UPDATE+=("dms-greeter")
|
||||||
|
[[ -n "$LATEST_TAG" ]] && echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
if [[ ${#PACKAGES_TO_UPDATE[@]} -gt 0 ]]; then
|
||||||
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
echo "packages=${PACKAGES_TO_UPDATE[*]}" >> $GITHUB_OUTPUT
|
||||||
@@ -144,6 +164,18 @@ jobs:
|
|||||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
elif [[ "$PKG" == "dms-greeter" ]]; then
|
||||||
|
if check_dms_greeter_stable; then
|
||||||
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||||
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
else
|
else
|
||||||
# Unknown package - proceed anyway
|
# Unknown package - proceed anyway
|
||||||
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
echo "packages=$PKG" >> $GITHUB_OUTPUT
|
||||||
@@ -171,15 +203,8 @@ jobs:
|
|||||||
- name: Determine packages to update
|
- name: Determine packages to update
|
||||||
id: packages
|
id: packages
|
||||||
run: |
|
run: |
|
||||||
# Check if GITHUB_REF points to a tag (works for both push events and workflow_dispatch with tag selected)
|
# Use check-updates outputs when available
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
||||||
# Tag selected or pushed - use the tag from GITHUB_REF
|
|
||||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
|
||||||
# Check if check-updates already determined a version (from auto-detection)
|
|
||||||
elif [[ -n "${{ needs.check-updates.outputs.version }}" ]]; then
|
|
||||||
# Use version from check-updates job
|
# Use version from check-updates job
|
||||||
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
echo "packages=${{ needs.check-updates.outputs.packages }}" >> $GITHUB_OUTPUT
|
||||||
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
echo "version=${{ needs.check-updates.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
@@ -191,42 +216,18 @@ jobs:
|
|||||||
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
|
||||||
# Manual workflow dispatch
|
# Manual workflow dispatch
|
||||||
|
|
||||||
# Determine version for dms stable
|
# Determine version for dms stable and dms-greeter using the API
|
||||||
if [[ "${{ github.event.inputs.package }}" == "dms" ]]; then
|
# GITHUB_REF is unreliable when "Use workflow from" a tag; API works from any ref
|
||||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
if [[ "${{ github.event.inputs.package }}" == "dms" ]] || [[ "${{ github.event.inputs.package }}" == "dms-greeter" ]] || [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
|
||||||
else
|
|
||||||
# Auto-detect latest release for dms
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
if [[ -n "$LATEST_TAG" ]]; then
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
||||||
echo "Auto-detected latest release: $LATEST_TAG"
|
echo "Using latest release from API: $LATEST_TAG"
|
||||||
else
|
else
|
||||||
echo "ERROR: Could not auto-detect latest release"
|
echo "ERROR: Could not fetch latest release from API"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
elif [[ "${{ github.event.inputs.package }}" == "all" ]]; then
|
|
||||||
# Use github.ref if tag selected, otherwise auto-detect latest
|
|
||||||
if [[ "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
|
||||||
VERSION="${GITHUB_REF#refs/tags/}"
|
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
|
||||||
echo "Using tag from GITHUB_REF: $VERSION"
|
|
||||||
else
|
|
||||||
# Auto-detect latest release for "all"
|
|
||||||
LATEST_TAG=$(curl -s https://api.github.com/repos/AvengeMedia/DankMaterialShell/releases/latest | grep '"tag_name"' | sed 's/.*"tag_name": "\([^"]*\)".*/\1/' || echo "")
|
|
||||||
if [[ -n "$LATEST_TAG" ]]; then
|
|
||||||
echo "version=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
||||||
echo "Auto-detected latest release: $LATEST_TAG"
|
|
||||||
else
|
|
||||||
echo "ERROR: Could not auto-detect latest release"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
# Use filtered packages from check-updates when package="all" and no rebuild/tag specified
|
||||||
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
if [[ "${{ github.event.inputs.package }}" == "all" ]] && [[ -z "${{ github.event.inputs.rebuild_release }}" ]] && [[ ! "${{ github.ref }}" =~ ^refs/tags/ ]]; then
|
||||||
@@ -283,21 +284,20 @@ jobs:
|
|||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms-git/debian/changelog"
|
} > "distro/debian/dms-git/debian/changelog"
|
||||||
|
|
||||||
- name: Update dms stable version
|
- name: Update stable version (dms + dms-greeter)
|
||||||
if: steps.packages.outputs.version != ''
|
if: steps.packages.outputs.version != ''
|
||||||
run: |
|
run: |
|
||||||
VERSION="${{ steps.packages.outputs.version }}"
|
VERSION="${{ steps.packages.outputs.version }}"
|
||||||
VERSION_NO_V="${VERSION#v}"
|
VERSION_NO_V="${VERSION#v}"
|
||||||
|
PACKAGES="${{ steps.packages.outputs.packages }}"
|
||||||
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
echo "==> Updating packaging files to version: $VERSION_NO_V"
|
||||||
|
|
||||||
# Update spec file
|
# Update dms spec and changelog when dms is in the upload list
|
||||||
|
if [[ "$PACKAGES" == *"dms"* ]]; then
|
||||||
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/dms.spec
|
||||||
|
|
||||||
# Verify the update
|
|
||||||
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
UPDATED_VERSION=$(grep -oP '^Version:\s+\K[0-9.]+' distro/opensuse/dms.spec | head -1)
|
||||||
echo "✓ Spec file now shows Version: $UPDATED_VERSION"
|
echo "✓ dms spec now shows Version: $UPDATED_VERSION"
|
||||||
|
|
||||||
# Single changelog entry (full history on OBS website)
|
|
||||||
DATE_STR=$(date "+%a %b %d %Y")
|
DATE_STR=$(date "+%a %b %d %Y")
|
||||||
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
LOCAL_SPEC_HEAD=$(sed -n '1,/%changelog/{ /%changelog/d; p }' distro/opensuse/dms.spec)
|
||||||
{
|
{
|
||||||
@@ -307,19 +307,6 @@ jobs:
|
|||||||
echo "- Update to stable $VERSION release"
|
echo "- Update to stable $VERSION release"
|
||||||
} > distro/opensuse/dms.spec
|
} > 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 (single entry, history on OBS website)
|
|
||||||
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
if [[ -f "distro/debian/dms/debian/changelog" ]]; then
|
||||||
CHANGELOG_DATE=$(date -R)
|
CHANGELOG_DATE=$(date -R)
|
||||||
{
|
{
|
||||||
@@ -329,8 +316,33 @@ jobs:
|
|||||||
echo ""
|
echo ""
|
||||||
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
} > "distro/debian/dms/debian/changelog"
|
} > "distro/debian/dms/debian/changelog"
|
||||||
echo "✓ Updated Debian changelog to ${VERSION_NO_V}db1"
|
echo "✓ Updated dms changelog to ${VERSION_NO_V}db1"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update dms-greeter changelog when dms-greeter is in the upload list
|
||||||
|
if [[ "$PACKAGES" == *"dms-greeter"* ]] && [[ -f "distro/debian/dms-greeter/debian/changelog" ]]; then
|
||||||
|
CHANGELOG_DATE=$(date -R)
|
||||||
|
{
|
||||||
|
echo "dms-greeter (${VERSION_NO_V}db1) unstable; urgency=medium"
|
||||||
|
echo ""
|
||||||
|
echo " * Update to $VERSION stable release"
|
||||||
|
echo ""
|
||||||
|
echo " -- Avenge Media <AvengeMedia.US@gmail.com> $CHANGELOG_DATE"
|
||||||
|
} > "distro/debian/dms-greeter/debian/changelog"
|
||||||
|
echo "✓ Updated dms-greeter changelog to ${VERSION_NO_V}db1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update Debian _service files for packages in upload list (download_url paths)
|
||||||
|
for service in distro/debian/*/_service; do
|
||||||
|
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, dms-greeter stable)
|
||||||
|
sed -i "s|/v[0-9.]\+/|/$VERSION/|g" "$service"
|
||||||
|
sed -i "s|/tags/v[0-9.]\+\.tar\.gz|/tags/$VERSION.tar.gz|g" "$service"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
@@ -354,6 +366,7 @@ jobs:
|
|||||||
chmod 600 ~/.config/osc/oscrc
|
chmod 600 ~/.config/osc/oscrc
|
||||||
|
|
||||||
- name: Upload to OBS
|
- name: Upload to OBS
|
||||||
|
id: upload
|
||||||
env:
|
env:
|
||||||
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
|
||||||
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
TAG_VERSION: ${{ steps.packages.outputs.version }}
|
||||||
@@ -362,6 +375,8 @@ jobs:
|
|||||||
|
|
||||||
if [[ -z "$PACKAGES" ]]; then
|
if [[ -z "$PACKAGES" ]]; then
|
||||||
echo "✓ No packages need uploading. All up to date!"
|
echo "✓ No packages need uploading. All up to date!"
|
||||||
|
echo "uploaded_packages=" >> $GITHUB_OUTPUT
|
||||||
|
echo "skipped_packages=" >> $GITHUB_OUTPUT
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -371,6 +386,9 @@ jobs:
|
|||||||
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
echo "==> Version being uploaded: ${{ steps.packages.outputs.version }}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
UPLOADED_PACKAGES=()
|
||||||
|
SKIPPED_PACKAGES=()
|
||||||
|
|
||||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||||
# Loop through each package and upload
|
# Loop through each package and upload
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
@@ -382,13 +400,37 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
LOG_FILE=$(mktemp)
|
||||||
|
set +e
|
||||||
if [[ "$PKG" == "dms-git" ]]; then
|
if [[ "$PKG" == "dms-git" ]]; then
|
||||||
bash distro/scripts/obs-upload.sh dms-git "Automated git update"
|
bash distro/scripts/obs-upload.sh dms-git "Automated git update" >"$LOG_FILE" 2>&1
|
||||||
else
|
else
|
||||||
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE"
|
bash distro/scripts/obs-upload.sh "$PKG" "$MESSAGE" >"$LOG_FILE" 2>&1
|
||||||
fi
|
fi
|
||||||
|
STATUS=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cat "$LOG_FILE"
|
||||||
|
|
||||||
|
if [[ $STATUS -ne 0 ]]; then
|
||||||
|
rm -f "$LOG_FILE"
|
||||||
|
echo "❌ Upload failed for $PKG"
|
||||||
|
exit $STATUS
|
||||||
|
fi
|
||||||
|
|
||||||
|
if grep -Eq "Exiting gracefully \(no changes needed\)|No changes needed for this package\. Exiting gracefully\." "$LOG_FILE"; then
|
||||||
|
echo "ℹ️ $PKG is already up to date. Skipped."
|
||||||
|
SKIPPED_PACKAGES+=("$PKG")
|
||||||
|
else
|
||||||
|
UPLOADED_PACKAGES+=("$PKG")
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$LOG_FILE"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
echo "uploaded_packages=${UPLOADED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||||
|
echo "skipped_packages=${SKIPPED_PACKAGES[*]}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Summary
|
- name: Summary
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
@@ -402,20 +444,59 @@ jobs:
|
|||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
echo "All packages are current. Run completed successfully." >> $GITHUB_STEP_SUMMARY
|
||||||
else
|
else
|
||||||
echo "**Packages Uploaded:**" >> $GITHUB_STEP_SUMMARY
|
UPLOADED_PACKAGES="${{ steps.upload.outputs.uploaded_packages }}"
|
||||||
|
SKIPPED_PACKAGES="${{ steps.upload.outputs.skipped_packages }}"
|
||||||
|
TOTAL_COUNT=$(wc -w <<<"$PACKAGES" | tr -d ' ')
|
||||||
|
UPLOADED_COUNT=0
|
||||||
|
SKIPPED_COUNT=0
|
||||||
|
if [[ -n "$UPLOADED_PACKAGES" ]]; then
|
||||||
|
UPLOADED_COUNT=$(wc -w <<<"$UPLOADED_PACKAGES" | tr -d ' ')
|
||||||
|
fi
|
||||||
|
if [[ -n "$SKIPPED_PACKAGES" ]]; then
|
||||||
|
SKIPPED_COUNT=$(wc -w <<<"$SKIPPED_PACKAGES" | tr -d ' ')
|
||||||
|
fi
|
||||||
|
in_list() {
|
||||||
|
local item="$1"
|
||||||
|
local list="$2"
|
||||||
|
[[ " $list " == *" $item "* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "${{ job.status }}" == "success" ]]; then
|
||||||
|
echo "**Status:** ✅ Completed successfully" >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "**Status:** ❌ Completed with errors" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Processed:** $TOTAL_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Uploaded:** $UPLOADED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Skipped (up to date):** $SKIPPED_COUNT package(s)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "**Packages:**" >> $GITHUB_STEP_SUMMARY
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
for PKG in $PACKAGES; do
|
for PKG in $PACKAGES; do
|
||||||
|
STATUS_ICON="✅"
|
||||||
|
STATUS_TEXT="uploaded"
|
||||||
|
if in_list "$PKG" "$SKIPPED_PACKAGES"; then
|
||||||
|
STATUS_ICON="ℹ️"
|
||||||
|
STATUS_TEXT="up to date (skipped)"
|
||||||
|
elif ! in_list "$PKG" "$UPLOADED_PACKAGES"; then
|
||||||
|
STATUS_ICON="❌"
|
||||||
|
STATUS_TEXT="failed"
|
||||||
|
fi
|
||||||
|
|
||||||
case "$PKG" in
|
case "$PKG" in
|
||||||
dms)
|
dms)
|
||||||
echo "- ✅ **dms** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms/dms)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
dms-git)
|
dms-git)
|
||||||
echo "- ✅ **dms-git** → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
echo "- $STATUS_ICON **dms-git** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:dms-git/dms-git)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
;;
|
||||||
|
dms-greeter)
|
||||||
|
echo "- $STATUS_ICON **dms-greeter** ($STATUS_TEXT) → [View builds](https://build.opensuse.org/package/show/home:AvengeMedia:danklinux/dms-greeter)" >> $GITHUB_STEP_SUMMARY
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "" >> $GITHUB_STEP_SUMMARY
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
if [[ -n "${{ github.event.inputs.rebuild_release }}" ]]; then
|
||||||
|
|||||||
6
.github/workflows/update-vendor-hash.yml
vendored
6
.github/workflows/update-vendor-hash.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
echo "Build succeeded, no hash update needed"
|
echo "Build succeeded, no hash update needed"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1)
|
new_hash=$(echo "$output" | grep -oP "got:\s+\K\S+" | head -n1 || true)
|
||||||
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
[ -n "$new_hash" ] || { echo "Could not extract new vendorHash"; echo "$output"; exit 1; }
|
||||||
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
current_hash=$(grep -oP 'vendorHash = "\K[^"]+' flake.nix)
|
||||||
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
[ "$current_hash" = "$new_hash" ] && { echo "vendorHash already up to date"; exit 0; }
|
||||||
@@ -59,8 +59,8 @@ jobs:
|
|||||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||||
git add flake.nix
|
git add flake.nix
|
||||||
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
git commit -m "nix: update vendorHash for go.mod changes" || exit 0
|
||||||
git pull --rebase origin master
|
git pull --rebase origin ${{ github.ref_name }}
|
||||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:master
|
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||||
else
|
else
|
||||||
echo "No changes to flake.nix"
|
echo "No changes to flake.nix"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -5,11 +5,13 @@ repos:
|
|||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
- repo: local
|
||||||
rev: v0.10.0.1
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: shellcheck
|
- id: shellcheck
|
||||||
args: [-e, SC2164, -e, SC2001, -e, SC2012, -e, SC2317]
|
name: shellcheck
|
||||||
|
entry: shellcheck -e SC2164 -e SC2001 -e SC2012 -e SC2317
|
||||||
|
language: system
|
||||||
|
types: [shell]
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-mod-tidy
|
- id: go-mod-tidy
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ nix develop
|
|||||||
|
|
||||||
This will provide:
|
This will provide:
|
||||||
|
|
||||||
- Go 1.24 toolchain (go, gopls, delve, go-tools) and GNU Make
|
- Go 1.25+ toolchain (go, gopls, delve, go-tools) and GNU Make
|
||||||
- Quickshell and required QML packages
|
- Quickshell and required QML packages
|
||||||
- Properly configured QML2_IMPORT_PATH
|
- Properly configured QML2_IMPORT_PATH
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ The on-screen preview displays the selected format. JSON output includes hex, RG
|
|||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
Requires Go 1.24+
|
Requires Go 1.25+
|
||||||
|
|
||||||
**Development build:**
|
**Development build:**
|
||||||
|
|
||||||
|
|||||||
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
10
core/cmd/dms/assets/cli-policy.default.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"policy_version": 1,
|
||||||
|
"blocked_commands": [
|
||||||
|
"greeter install",
|
||||||
|
"greeter enable",
|
||||||
|
"greeter uninstall",
|
||||||
|
"setup"
|
||||||
|
],
|
||||||
|
"message": "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes."
|
||||||
|
}
|
||||||
40
core/cmd/dms/commands_blur.go
Normal file
40
core/cmd/dms/commands_blur.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var blurCmd = &cobra.Command{
|
||||||
|
Use: "blur",
|
||||||
|
Short: "Background blur utilities",
|
||||||
|
}
|
||||||
|
|
||||||
|
var blurCheckCmd = &cobra.Command{
|
||||||
|
Use: "check",
|
||||||
|
Short: "Check if the compositor supports background blur (ext-background-effect-v1)",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: runBlurCheck,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
blurCmd.AddCommand(blurCheckCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBlurCheck(cmd *cobra.Command, args []string) {
|
||||||
|
supported, err := blur.ProbeSupport()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch supported {
|
||||||
|
case true:
|
||||||
|
fmt.Println("supported")
|
||||||
|
default:
|
||||||
|
fmt.Println("unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -236,6 +236,7 @@ func runBrightnessSet(cmd *cobra.Command, args []string) {
|
|||||||
defer ddc.Close()
|
defer ddc.Close()
|
||||||
time.Sleep(100 * time.Millisecond)
|
time.Sleep(100 * time.Millisecond)
|
||||||
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
if err := ddc.SetBrightnessWithExponent(deviceID, percent, exponential, exponent, nil); err == nil {
|
||||||
|
ddc.WaitPending()
|
||||||
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
fmt.Printf("Set %s to %d%%\n", deviceID, percent)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,16 +222,19 @@ func init() {
|
|||||||
|
|
||||||
func runClipCopy(cmd *cobra.Command, args []string) {
|
func runClipCopy(cmd *cobra.Command, args []string) {
|
||||||
var data []byte
|
var data []byte
|
||||||
|
copyFromStdin := false
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case len(args) > 0:
|
case len(args) > 0:
|
||||||
data = []byte(args[0])
|
data = []byte(args[0])
|
||||||
default:
|
case clipCopyDownload || clipCopyType == "__multi__":
|
||||||
var err error
|
var err error
|
||||||
data, err = io.ReadAll(os.Stdin)
|
data, err = io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("read stdin: %v", err)
|
log.Fatalf("read stdin: %v", err)
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
copyFromStdin = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if clipCopyDownload {
|
if clipCopyDownload {
|
||||||
@@ -257,6 +260,13 @@ func runClipCopy(cmd *cobra.Command, args []string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if copyFromStdin {
|
||||||
|
if err := clipboard.CopyReader(os.Stdin, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
|
log.Fatalf("copy: %v", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
if err := clipboard.CopyOpts(data, clipCopyType, clipCopyForeground, clipCopyPasteOnce); err != nil {
|
||||||
log.Fatalf("copy: %v", err)
|
log.Fatalf("copy: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ var killCmd = &cobra.Command{
|
|||||||
var ipcCmd = &cobra.Command{
|
var ipcCmd = &cobra.Command{
|
||||||
Use: "ipc [target] [function] [args...]",
|
Use: "ipc [target] [function] [args...]",
|
||||||
Short: "Send IPC commands to running DMS shell",
|
Short: "Send IPC commands to running DMS shell",
|
||||||
PreRunE: findConfig,
|
|
||||||
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||||
_ = findConfig(cmd, args)
|
_ = findConfig(cmd, args)
|
||||||
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
return getShellIPCCompletions(args, toComplete), cobra.ShellCompDirectiveNoFileComp
|
||||||
@@ -525,5 +524,7 @@ func getCommonCommands() []*cobra.Command {
|
|||||||
doctorCmd,
|
doctorCmd,
|
||||||
configCmd,
|
configCmd,
|
||||||
dlCmd,
|
dlCmd,
|
||||||
|
randrCmd,
|
||||||
|
blurCmd,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/blur"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
@@ -82,7 +83,7 @@ func (ds *DoctorStatus) OKCount() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
quickshellVersionRegex = regexp.MustCompile(`quickshell (\d+\.\d+\.\d+)`)
|
quickshellVersionRegex = regexp.MustCompile(`(?i)quickshell (\d+\.\d+\.\d+)`)
|
||||||
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
hyprlandVersionRegex = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`)
|
||||||
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
niriVersionRegex = regexp.MustCompile(`niri (\d+\.\d+)`)
|
||||||
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
swayVersionRegex = regexp.MustCompile(`sway version (\d+\.\d+)`)
|
||||||
@@ -90,6 +91,7 @@ var (
|
|||||||
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
wayfireVersionRegex = regexp.MustCompile(`wayfire (\d+\.\d+)`)
|
||||||
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
labwcVersionRegex = regexp.MustCompile(`labwc (\d+\.\d+\.\d+)`)
|
||||||
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
mangowcVersionRegex = regexp.MustCompile(`mango (\d+\.\d+\.\d+)`)
|
||||||
|
miracleVersionRegex = regexp.MustCompile(`miracle-wm v?(\d+\.\d+\.\d+)`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var doctorCmd = &cobra.Command{
|
var doctorCmd = &cobra.Command{
|
||||||
@@ -468,6 +470,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
{"Wayfire", "wayfire", "--version", wayfireVersionRegex, []string{"wayfire"}},
|
||||||
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
{"labwc", "labwc", "--version", labwcVersionRegex, []string{"labwc"}},
|
||||||
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
{"mangowc", "mango", "-v", mangowcVersionRegex, []string{"mango"}},
|
||||||
|
{"Miracle WM", "miracle-wm", "--version", miracleVersionRegex, []string{"miracle-wm"}},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -500,7 +503,7 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{
|
results = append(results, checkResult{
|
||||||
catCompositor, "Compositor", statusError,
|
catCompositor, "Compositor", statusError,
|
||||||
"No supported Wayland compositor found",
|
"No supported Wayland compositor found",
|
||||||
"Install Hyprland, niri, Sway, River, or Wayfire",
|
"Install Hyprland, niri, Sway, River, Wayfire, or miracle-wm",
|
||||||
doctorDocsURL + "#compositor-checks",
|
doctorDocsURL + "#compositor-checks",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -509,9 +512,24 @@ func checkWindowManagers() []checkResult {
|
|||||||
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
results = append(results, checkResult{catCompositor, "Active", statusInfo, wm, "", doctorDocsURL + "#compositor"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
results = append(results, checkCompositorBlurSupport())
|
||||||
|
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkCompositorBlurSupport() checkResult {
|
||||||
|
supported, err := blur.ProbeSupport()
|
||||||
|
if err != nil {
|
||||||
|
return checkResult{catCompositor, "Background Blur", statusInfo, "Unable to verify", err.Error(), doctorDocsURL + "#compositor-checks"}
|
||||||
|
}
|
||||||
|
|
||||||
|
if supported {
|
||||||
|
return checkResult{catCompositor, "Background Blur", statusOK, "Supported", "Compositor supports ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkResult{catCompositor, "Background Blur", statusWarn, "Unsupported", "Compositor does not support ext-background-effect-v1", doctorDocsURL + "#compositor-checks"}
|
||||||
|
}
|
||||||
|
|
||||||
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
func getVersionFromCommand(cmd, arg string, regex *regexp.Regexp) string {
|
||||||
output, err := exec.Command(cmd, arg).CombinedOutput()
|
output, err := exec.Command(cmd, arg).CombinedOutput()
|
||||||
if err != nil && len(output) == 0 {
|
if err != nil && len(output) == 0 {
|
||||||
@@ -535,6 +553,8 @@ func detectRunningWM() string {
|
|||||||
return "Hyprland"
|
return "Hyprland"
|
||||||
case os.Getenv("NIRI_SOCKET") != "":
|
case os.Getenv("NIRI_SOCKET") != "":
|
||||||
return "niri"
|
return "niri"
|
||||||
|
case os.Getenv("MIRACLESOCK") != "":
|
||||||
|
return "Miracle WM"
|
||||||
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
case os.Getenv("XDG_CURRENT_DESKTOP") != "":
|
||||||
return os.Getenv("XDG_CURRENT_DESKTOP")
|
return os.Getenv("XDG_CURRENT_DESKTOP")
|
||||||
}
|
}
|
||||||
@@ -553,6 +573,7 @@ func checkQuickshellFeatures() ([]checkResult, bool) {
|
|||||||
qmlContent := `
|
qmlContent := `
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
|
||||||
ShellRoot {
|
ShellRoot {
|
||||||
id: root
|
id: root
|
||||||
@@ -561,6 +582,7 @@ ShellRoot {
|
|||||||
property bool idleMonitorAvailable: false
|
property bool idleMonitorAvailable: false
|
||||||
property bool idleInhibitorAvailable: false
|
property bool idleInhibitorAvailable: false
|
||||||
property bool shortcutInhibitorAvailable: false
|
property bool shortcutInhibitorAvailable: false
|
||||||
|
property bool backgroundBlurAvailable: false
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 50
|
interval: 50
|
||||||
@@ -578,16 +600,18 @@ ShellRoot {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
var testItem = Qt.createQmlObject(
|
var testItem = Qt.createQmlObject(
|
||||||
'import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
'import Quickshell; import Quickshell.Wayland; import QtQuick; QtObject { ' +
|
||||||
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
'readonly property bool hasIdleMonitor: typeof IdleMonitor !== "undefined"; ' +
|
||||||
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
'readonly property bool hasIdleInhibitor: typeof IdleInhibitor !== "undefined"; ' +
|
||||||
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined" ' +
|
'readonly property bool hasShortcutInhibitor: typeof ShortcutInhibitor !== "undefined"; ' +
|
||||||
|
'readonly property bool hasBackgroundBlur: typeof BackgroundEffect !== "undefined" ' +
|
||||||
'}',
|
'}',
|
||||||
root
|
root
|
||||||
)
|
)
|
||||||
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
root.idleMonitorAvailable = testItem.hasIdleMonitor
|
||||||
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
root.idleInhibitorAvailable = testItem.hasIdleInhibitor
|
||||||
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
root.shortcutInhibitorAvailable = testItem.hasShortcutInhibitor
|
||||||
|
root.backgroundBlurAvailable = testItem.hasBackgroundBlur
|
||||||
testItem.destroy()
|
testItem.destroy()
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
@@ -596,6 +620,8 @@ ShellRoot {
|
|||||||
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
console.warn(root.idleInhibitorAvailable ? "FEATURE:IdleInhibitor:OK" : "FEATURE:IdleInhibitor:UNAVAILABLE")
|
||||||
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
console.warn(root.shortcutInhibitorAvailable ? "FEATURE:ShortcutInhibitor:OK" : "FEATURE:ShortcutInhibitor:UNAVAILABLE")
|
||||||
|
|
||||||
|
console.warn(root.backgroundBlurAvailable ? "FEATURE:BackgroundBlur:OK" : "FEATURE:BackgroundBlur:UNAVAILABLE")
|
||||||
|
|
||||||
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
Quickshell.execDetached(["kill", "-TERM", String(Quickshell.processId)])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -616,6 +642,7 @@ ShellRoot {
|
|||||||
{"IdleMonitor", "Idle detection"},
|
{"IdleMonitor", "Idle detection"},
|
||||||
{"IdleInhibitor", "Prevent idle/sleep"},
|
{"IdleInhibitor", "Prevent idle/sleep"},
|
||||||
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
{"ShortcutInhibitor", "Allow shortcut management (niri)"},
|
||||||
|
{"BackgroundBlur", "Background blur API support in Quickshell"},
|
||||||
}
|
}
|
||||||
|
|
||||||
var results []checkResult
|
var results []checkResult
|
||||||
@@ -652,16 +679,14 @@ func checkI2CAvailability() checkResult {
|
|||||||
func checkImageFormatPlugins() []checkResult {
|
func checkImageFormatPlugins() []checkResult {
|
||||||
url := doctorDocsURL + "#optional-features"
|
url := doctorDocsURL + "#optional-features"
|
||||||
|
|
||||||
pluginDir := findQtPluginDir()
|
pluginDirs := findQtPluginDirs()
|
||||||
if pluginDir == "" {
|
if len(pluginDirs) == 0 {
|
||||||
return []checkResult{
|
return []checkResult{
|
||||||
{catOptionalFeatures, "qt6-imageformats", statusInfo, "Cannot detect (plugin dir not found)", "WebP, TIFF, JP2 support", url},
|
{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},
|
{catOptionalFeatures, "kimageformats", statusInfo, "Cannot detect (plugin dir not found)", "AVIF, HEIF, JXL support", url},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
|
||||||
|
|
||||||
type pluginCheck struct {
|
type pluginCheck struct {
|
||||||
name string
|
name string
|
||||||
desc string
|
desc string
|
||||||
@@ -695,10 +720,19 @@ func checkImageFormatPlugins() []checkResult {
|
|||||||
var results []checkResult
|
var results []checkResult
|
||||||
for _, c := range checks {
|
for _, c := range checks {
|
||||||
var found []string
|
var found []string
|
||||||
|
var foundDirs []string
|
||||||
|
for _, pluginDir := range pluginDirs {
|
||||||
|
imageFormatsDir := filepath.Join(pluginDir, "imageformats")
|
||||||
for _, p := range c.plugins {
|
for _, p := range c.plugins {
|
||||||
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
if _, err := os.Stat(filepath.Join(imageFormatsDir, p.file)); err == nil {
|
||||||
|
if !slices.Contains(found, p.format) {
|
||||||
found = append(found, p.format)
|
found = append(found, p.format)
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(foundDirs, imageFormatsDir) {
|
||||||
|
foundDirs = append(foundDirs, imageFormatsDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result checkResult
|
var result checkResult
|
||||||
@@ -708,7 +742,7 @@ func checkImageFormatPlugins() []checkResult {
|
|||||||
default:
|
default:
|
||||||
details := ""
|
details := ""
|
||||||
if doctorVerbose {
|
if doctorVerbose {
|
||||||
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), imageFormatsDir)
|
details = fmt.Sprintf("Formats: %s (%s)", strings.Join(found, ", "), strings.Join(foundDirs, ":"))
|
||||||
}
|
}
|
||||||
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
result = checkResult{catOptionalFeatures, c.name, statusOK, fmt.Sprintf("Installed (%d formats)", len(found)), details, url}
|
||||||
}
|
}
|
||||||
@@ -718,22 +752,28 @@ func checkImageFormatPlugins() []checkResult {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
func findQtPluginDir() string {
|
func findQtPluginDirs() []string {
|
||||||
// Check QT_PLUGIN_PATH env var first (used by NixOS and custom setups)
|
var dirs []string
|
||||||
|
|
||||||
|
addDir := func(dir string) {
|
||||||
|
if dir != "" {
|
||||||
|
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
||||||
|
dirs = append(dirs, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check all paths in QT_PLUGIN_PATH env var (used by NixOS and custom setups)
|
||||||
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
if envPath := os.Getenv("QT_PLUGIN_PATH"); envPath != "" {
|
||||||
for dir := range strings.SplitSeq(envPath, ":") {
|
for dir := range strings.SplitSeq(envPath, ":") {
|
||||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
addDir(dir)
|
||||||
return dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try qtpaths
|
// Try qtpaths
|
||||||
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
for _, cmd := range []string{"qtpaths6", "qtpaths"} {
|
||||||
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
if output, err := exec.Command(cmd, "-query", "QT_INSTALL_PLUGINS").Output(); err == nil {
|
||||||
if dir := strings.TrimSpace(string(output)); dir != "" {
|
addDir(strings.TrimSpace(string(output)))
|
||||||
return dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -744,12 +784,10 @@ func findQtPluginDir() string {
|
|||||||
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
"/usr/lib/x86_64-linux-gnu/qt6/plugins",
|
||||||
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
"/usr/lib/aarch64-linux-gnu/qt6/plugins",
|
||||||
} {
|
} {
|
||||||
if _, err := os.Stat(filepath.Join(dir, "imageformats")); err == nil {
|
addDir(dir)
|
||||||
return dir
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ""
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
func detectNetworkBackend(stackResult *network.DetectResult) string {
|
||||||
@@ -809,10 +847,14 @@ func checkOptionalDependencies() []checkResult {
|
|||||||
results = append(results, checkImageFormatPlugins()...)
|
results = append(results, checkImageFormatPlugins()...)
|
||||||
|
|
||||||
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
terminals := []string{"ghostty", "kitty", "alacritty", "foot", "wezterm"}
|
||||||
if idx := slices.IndexFunc(terminals, utils.CommandExists); idx >= 0 {
|
terminals = slices.DeleteFunc(terminals, func(t string) bool {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, terminals[idx], "", optionalFeaturesURL})
|
return !utils.CommandExists(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(terminals) > 0 {
|
||||||
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusOK, strings.Join(terminals, ", "), "", optionalFeaturesURL})
|
||||||
} else {
|
} else {
|
||||||
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, or alacritty", optionalFeaturesURL})
|
results = append(results, checkResult{catOptionalFeatures, "Terminal", statusWarn, "None found", "Install ghostty, kitty, foot or alacritty", optionalFeaturesURL})
|
||||||
}
|
}
|
||||||
|
|
||||||
networkResult, err := network.DetectNetworkStack()
|
networkResult, err := network.DetectNetworkStack()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -15,6 +16,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@@ -109,16 +111,37 @@ func updateArchLinux() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var packageName string
|
var packageName string
|
||||||
if isArchPackageInstalled("dms-shell-bin") {
|
var isAUR bool
|
||||||
packageName = "dms-shell-bin"
|
if isArchPackageInstalled("dms-shell") {
|
||||||
|
packageName = "dms-shell"
|
||||||
} else if isArchPackageInstalled("dms-shell-git") {
|
} else if isArchPackageInstalled("dms-shell-git") {
|
||||||
packageName = "dms-shell-git"
|
packageName = "dms-shell-git"
|
||||||
|
isAUR = true
|
||||||
|
} else if isArchPackageInstalled("dms-shell-bin") {
|
||||||
|
packageName = "dms-shell-bin"
|
||||||
|
isAUR = true
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Info: Neither dms-shell-bin nor dms-shell-git package found.")
|
fmt.Println("Info: No dms-shell package found.")
|
||||||
fmt.Println("Info: Falling back to git-based update method...")
|
fmt.Println("Info: Falling back to git-based update method...")
|
||||||
return updateOtherDistros()
|
return updateOtherDistros()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isAUR {
|
||||||
|
fmt.Printf("This will update %s using pacman.\n", packageName)
|
||||||
|
if !confirmUpdate() {
|
||||||
|
return errdefs.ErrUpdateCancelled
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("\nRunning: pacman -S %s\n", packageName)
|
||||||
|
if err := privesc.Run(context.Background(), "", "pacman", "-S", "--noconfirm", packageName); err != nil {
|
||||||
|
fmt.Printf("Error: Failed to update using pacman: %v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("dms successfully updated")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var helper string
|
var helper string
|
||||||
var updateCmd *exec.Cmd
|
var updateCmd *exec.Cmd
|
||||||
|
|
||||||
@@ -454,11 +477,7 @@ func updateDMSBinary() error {
|
|||||||
|
|
||||||
fmt.Printf("Installing to %s...\n", currentPath)
|
fmt.Printf("Installing to %s...\n", currentPath)
|
||||||
|
|
||||||
replaceCmd := exec.Command("sudo", "install", "-m", "0755", decompressedPath, currentPath)
|
if err := privesc.Run(context.Background(), "", "install", "-m", "0755", decompressedPath, currentPath); err != nil {
|
||||||
replaceCmd.Stdin = os.Stdin
|
|
||||||
replaceCmd.Stdout = os.Stdout
|
|
||||||
replaceCmd.Stderr = os.Stderr
|
|
||||||
if err := replaceCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to replace binary: %w", err)
|
return fmt.Errorf("failed to replace binary: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,9 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -95,7 +97,11 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
|||||||
|
|
||||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||||
opts := buildMatugenOptions(cmd)
|
opts := buildMatugenOptions(cmd)
|
||||||
if err := matugen.Run(opts); err != nil {
|
err := matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,7 +135,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
if !wait {
|
if !wait {
|
||||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||||
log.Info("Server unavailable, running synchronously")
|
log.Info("Server unavailable, running synchronously")
|
||||||
if err := matugen.Run(opts); err != nil {
|
err := matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -146,11 +156,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
resp, ok := tryServerRequest(request)
|
resp, ok := tryServerRequest(request)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Info("Server unavailable, running synchronously")
|
log.Info("Server unavailable, running synchronously")
|
||||||
if err := matugen.Run(opts); err != nil {
|
err := matugen.Run(opts)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
resultCh <- matugen.ErrNoChanges
|
||||||
|
case err != nil:
|
||||||
resultCh <- err
|
resultCh <- err
|
||||||
return
|
default:
|
||||||
}
|
|
||||||
resultCh <- nil
|
resultCh <- nil
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if resp.Error != "" {
|
if resp.Error != "" {
|
||||||
@@ -162,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
|||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-resultCh:
|
case err := <-resultCh:
|
||||||
if err != nil {
|
switch {
|
||||||
|
case errors.Is(err, matugen.ErrNoChanges):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
log.Fatalf("Theme generation failed: %v", err)
|
log.Fatalf("Theme generation failed: %v", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Theme generation completed")
|
fmt.Println("Theme generation completed")
|
||||||
|
|||||||
58
core/cmd/dms/commands_randr.go
Normal file
58
core/cmd/dms/commands_randr.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var randrCmd = &cobra.Command{
|
||||||
|
Use: "randr",
|
||||||
|
Short: "Query output display information",
|
||||||
|
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
|
||||||
|
Run: runRandr,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
randrCmd.Flags().Bool("json", false, "Output in JSON format")
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrJSON struct {
|
||||||
|
Outputs []randrOutput `json:"outputs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRandr(cmd *cobra.Command, args []string) {
|
||||||
|
outputs, err := queryRandr()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonFlag, _ := cmd.Flags().GetBool("json")
|
||||||
|
|
||||||
|
if jsonFlag {
|
||||||
|
data, err := json.Marshal(randrJSON{Outputs: outputs})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to marshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Println(string(data))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, out := range outputs {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
status := "enabled"
|
||||||
|
if !out.Enabled {
|
||||||
|
status = "disabled"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s (%s)\n", out.Name, status)
|
||||||
|
fmt.Printf(" Scale: %.4g\n", out.Scale)
|
||||||
|
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
|
||||||
|
if out.Refresh > 0 {
|
||||||
|
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
@@ -19,6 +21,7 @@ var setupCmd = &cobra.Command{
|
|||||||
Use: "setup",
|
Use: "setup",
|
||||||
Short: "Deploy DMS configurations",
|
Short: "Deploy DMS configurations",
|
||||||
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
Long: "Deploy compositor and terminal configurations with interactive prompts",
|
||||||
|
PersistentPreRunE: preRunPrivileged,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if err := runSetup(); err != nil {
|
if err := runSetup(); err != nil {
|
||||||
log.Fatalf("Error during setup: %v", err)
|
log.Fatalf("Error during setup: %v", err)
|
||||||
@@ -266,6 +269,8 @@ func runSetupDmsConfig(name string) error {
|
|||||||
func runSetup() error {
|
func runSetup() error {
|
||||||
fmt.Println("=== DMS Configuration Setup ===")
|
fmt.Println("=== DMS Configuration Setup ===")
|
||||||
|
|
||||||
|
ensureInputGroup()
|
||||||
|
|
||||||
wm, wmSelected := promptCompositor()
|
wm, wmSelected := promptCompositor()
|
||||||
terminal, terminalSelected := promptTerminal()
|
terminal, terminalSelected := promptTerminal()
|
||||||
useSystemd := promptSystemd()
|
useSystemd := promptSystemd()
|
||||||
@@ -339,6 +344,37 @@ func runSetup() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add user to the input group for the evdev manager for inut state tracking.
|
||||||
|
// Caps Lock OSD and the Caps Lock bar indicator.
|
||||||
|
func ensureInputGroup() {
|
||||||
|
if !utils.HasGroup("input") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentUser := os.Getenv("USER")
|
||||||
|
if currentUser == "" {
|
||||||
|
currentUser = os.Getenv("LOGNAME")
|
||||||
|
}
|
||||||
|
if currentUser == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, err := execGroups(currentUser)
|
||||||
|
if err == nil && strings.Contains(out, "input") {
|
||||||
|
fmt.Printf("✓ %s is already in the input group (Caps Lock OSD enabled)\n", currentUser)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Adding user to input group for Caps Lock OSD support...")
|
||||||
|
if err := privesc.Run(context.Background(), "", "usermod", "-aG", "input", currentUser); err != nil {
|
||||||
|
fmt.Printf("⚠ Could not add %s to input group (Caps Lock OSD will be unavailable): %v\n", currentUser, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✓ Added %s to input group (logout/login required to take effect)\n", currentUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func execGroups(user string) (string, error) {
|
||||||
|
out, err := exec.Command("groups", user).Output()
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
func promptCompositor() (deps.WindowManager, bool) {
|
func promptCompositor() (deps.WindowManager, bool) {
|
||||||
fmt.Println("Select compositor:")
|
fmt.Println("Select compositor:")
|
||||||
fmt.Println("1) Niri")
|
fmt.Println("1) Niri")
|
||||||
|
|||||||
285
core/cmd/dms/immutable_policy.go
Normal file
285
core/cmd/dms/immutable_policy.go
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
cliPolicyPackagedPath = "/usr/share/dms/cli-policy.json"
|
||||||
|
cliPolicyAdminPath = "/etc/dms/cli-policy.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
immutablePolicyOnce sync.Once
|
||||||
|
immutablePolicy immutableCommandPolicy
|
||||||
|
immutablePolicyErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/cli-policy.default.json
|
||||||
|
var defaultCLIPolicyJSON []byte
|
||||||
|
|
||||||
|
type immutableCommandPolicy struct {
|
||||||
|
ImmutableSystem bool
|
||||||
|
ImmutableReason string
|
||||||
|
BlockedCommands []string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliPolicyFile struct {
|
||||||
|
PolicyVersion int `json:"policy_version"`
|
||||||
|
ImmutableSystem *bool `json:"immutable_system"`
|
||||||
|
BlockedCommands *[]string `json:"blocked_commands"`
|
||||||
|
Message *string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeCommandSpec(raw string) string {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
normalized = strings.TrimPrefix(normalized, "dms ")
|
||||||
|
return strings.Join(strings.Fields(normalized), " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeBlockedCommands(raw []string) []string {
|
||||||
|
normalized := make([]string, 0, len(raw))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
|
||||||
|
for _, cmd := range raw {
|
||||||
|
spec := normalizeCommandSpec(cmd)
|
||||||
|
if spec == "" || seen[spec] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[spec] = true
|
||||||
|
normalized = append(normalized, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBlockedByPolicy(commandPath string, blocked []string) bool {
|
||||||
|
normalizedPath := normalizeCommandSpec(commandPath)
|
||||||
|
if normalizedPath == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range blocked {
|
||||||
|
spec := normalizeCommandSpec(entry)
|
||||||
|
if spec == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if normalizedPath == spec || strings.HasPrefix(normalizedPath, spec+" ") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPolicyFile(path string) (*cliPolicyFile, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("failed to read %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var policy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(data, &policy); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &policy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergePolicyFile(base *immutableCommandPolicy, path string) error {
|
||||||
|
policyFile, err := loadPolicyFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if policyFile == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if policyFile.ImmutableSystem != nil {
|
||||||
|
base.ImmutableSystem = *policyFile.ImmutableSystem
|
||||||
|
}
|
||||||
|
if policyFile.BlockedCommands != nil {
|
||||||
|
base.BlockedCommands = normalizeBlockedCommands(*policyFile.BlockedCommands)
|
||||||
|
}
|
||||||
|
if policyFile.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*policyFile.Message)
|
||||||
|
if msg != "" {
|
||||||
|
base.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOSReleaseMap(path string) map[string]string {
|
||||||
|
values := make(map[string]string)
|
||||||
|
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(line, "=", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||||
|
value := strings.Trim(strings.TrimSpace(parts[1]), "\"")
|
||||||
|
values[key] = strings.ToLower(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAnyToken(text string, tokens ...string) bool {
|
||||||
|
if text == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, token := range tokens {
|
||||||
|
if strings.Contains(text, token) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectImmutableSystem() (bool, string) {
|
||||||
|
if _, err := os.Stat("/run/ostree-booted"); err == nil {
|
||||||
|
return true, "/run/ostree-booted is present"
|
||||||
|
}
|
||||||
|
|
||||||
|
osRelease := readOSReleaseMap("/etc/os-release")
|
||||||
|
if len(osRelease) == 0 {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
id := osRelease["ID"]
|
||||||
|
idLike := osRelease["ID_LIKE"]
|
||||||
|
variantID := osRelease["VARIANT_ID"]
|
||||||
|
name := osRelease["NAME"]
|
||||||
|
prettyName := osRelease["PRETTY_NAME"]
|
||||||
|
|
||||||
|
immutableIDs := map[string]bool{
|
||||||
|
"bluefin": true,
|
||||||
|
"bazzite": true,
|
||||||
|
"silverblue": true,
|
||||||
|
"kinoite": true,
|
||||||
|
"sericea": true,
|
||||||
|
"onyx": true,
|
||||||
|
"aurora": true,
|
||||||
|
"fedora-iot": true,
|
||||||
|
"fedora-coreos": true,
|
||||||
|
}
|
||||||
|
if immutableIDs[id] {
|
||||||
|
return true, "os-release ID=" + id
|
||||||
|
}
|
||||||
|
|
||||||
|
markers := []string{"silverblue", "kinoite", "sericea", "onyx", "bazzite", "bluefin", "aurora", "ostree", "atomic"}
|
||||||
|
if hasAnyToken(variantID, markers...) {
|
||||||
|
return true, "os-release VARIANT_ID=" + variantID
|
||||||
|
}
|
||||||
|
if hasAnyToken(idLike, "ostree", "rpm-ostree") {
|
||||||
|
return true, "os-release ID_LIKE=" + idLike
|
||||||
|
}
|
||||||
|
if hasAnyToken(name, markers...) || hasAnyToken(prettyName, markers...) {
|
||||||
|
return true, "os-release identifies an atomic/ostree variant"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImmutablePolicy() (*immutableCommandPolicy, error) {
|
||||||
|
immutablePolicyOnce.Do(func() {
|
||||||
|
detectedImmutable, reason := detectImmutableSystem()
|
||||||
|
immutablePolicy = immutableCommandPolicy{
|
||||||
|
ImmutableSystem: detectedImmutable,
|
||||||
|
ImmutableReason: reason,
|
||||||
|
BlockedCommands: []string{"greeter install", "greeter enable", "setup"},
|
||||||
|
Message: "This command is disabled on immutable/image-based systems. Use your distro-native workflow for system-level changes.",
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPolicy cliPolicyFile
|
||||||
|
if err := json.Unmarshal(defaultCLIPolicyJSON, &defaultPolicy); err != nil {
|
||||||
|
immutablePolicyErr = fmt.Errorf("failed to parse embedded default CLI policy: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if defaultPolicy.BlockedCommands != nil {
|
||||||
|
immutablePolicy.BlockedCommands = normalizeBlockedCommands(*defaultPolicy.BlockedCommands)
|
||||||
|
}
|
||||||
|
if defaultPolicy.Message != nil {
|
||||||
|
msg := strings.TrimSpace(*defaultPolicy.Message)
|
||||||
|
if msg != "" {
|
||||||
|
immutablePolicy.Message = msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyPackagedPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mergePolicyFile(&immutablePolicy, cliPolicyAdminPath); err != nil {
|
||||||
|
immutablePolicyErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if immutablePolicyErr != nil {
|
||||||
|
return nil, immutablePolicyErr
|
||||||
|
}
|
||||||
|
return &immutablePolicy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireMutableSystemCommand(cmd *cobra.Command, _ []string) error {
|
||||||
|
policy, err := getImmutablePolicy()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !policy.ImmutableSystem {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
commandPath := normalizeCommandSpec(cmd.CommandPath())
|
||||||
|
if !commandBlockedByPolicy(commandPath, policy.BlockedCommands) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
reason := ""
|
||||||
|
if policy.ImmutableReason != "" {
|
||||||
|
reason = "Detected immutable system: " + policy.ImmutableReason + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("%s%s\nCommand: dms %s\nPolicy files:\n %s\n %s", reason, policy.Message, commandPath, cliPolicyPackagedPath, cliPolicyAdminPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// preRunPrivileged combines the immutable-system check with a privesc tool
|
||||||
|
// selection prompt (shown only when multiple tools are available and the
|
||||||
|
// $DMS_PRIVESC env var isn't set).
|
||||||
|
func preRunPrivileged(cmd *cobra.Command, args []string) error {
|
||||||
|
if err := requireMutableSystemCommand(cmd, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := privesc.PromptCLI(os.Stdout, os.Stdin); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,19 +17,10 @@ func init() {
|
|||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to update
|
|
||||||
updateCmd.AddCommand(updateCheckCmd)
|
updateCmd.AddCommand(updateCheckCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd)
|
||||||
@@ -37,7 +29,9 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if os.Geteuid() == 0 {
|
clipboard.MaybeServeAndExit()
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,36 +5,30 @@ package main
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/clipboard"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Version = "dev"
|
var Version = "dev"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
// Add flags
|
|
||||||
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
runCmd.Flags().BoolP("daemon", "d", false, "Run in daemon mode")
|
||||||
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
runCmd.Flags().Bool("daemon-child", false, "Internal flag for daemon child process")
|
||||||
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
runCmd.Flags().Bool("session", false, "Session managed (like as a systemd unit)")
|
||||||
runCmd.Flags().MarkHidden("daemon-child")
|
runCmd.Flags().MarkHidden("daemon-child")
|
||||||
|
|
||||||
// Add subcommands to greeter
|
greeterCmd.AddCommand(greeterInstallCmd, greeterSyncCmd, greeterEnableCmd, greeterStatusCmd, greeterUninstallCmd)
|
||||||
greeterCmd.AddCommand(greeterSyncCmd, greeterEnableCmd, greeterStatusCmd)
|
|
||||||
|
|
||||||
// Add subcommands to setup
|
|
||||||
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
setupCmd.AddCommand(setupBindsCmd, setupLayoutCmd, setupColorsCmd, setupAlttabCmd, setupOutputsCmd, setupCursorCmd, setupWindowrulesCmd)
|
||||||
|
|
||||||
// Add subcommands to plugins
|
|
||||||
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
pluginsCmd.AddCommand(pluginsBrowseCmd, pluginsListCmd, pluginsInstallCmd, pluginsUninstallCmd, pluginsUpdateCmd)
|
||||||
|
|
||||||
// Add common commands to root
|
|
||||||
rootCmd.AddCommand(getCommonCommands()...)
|
rootCmd.AddCommand(getCommonCommands()...)
|
||||||
|
|
||||||
rootCmd.SetHelpTemplate(getHelpTemplate())
|
rootCmd.SetHelpTemplate(getHelpTemplate())
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Block root
|
clipboard.MaybeServeAndExit()
|
||||||
if os.Geteuid() == 0 {
|
|
||||||
|
if os.Geteuid() == 0 && !isReadOnlyCommand(os.Args) {
|
||||||
log.Fatal("This program should not be run as root. Exiting.")
|
log.Fatal("This program should not be run as root. Exiting.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
172
core/cmd/dms/randr_client.go
Normal file
172
core/cmd/dms/randr_client.go
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||||
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
type randrOutput struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Scale float64 `json:"scale"`
|
||||||
|
Width int32 `json:"width"`
|
||||||
|
Height int32 `json:"height"`
|
||||||
|
Refresh int32 `json:"refresh"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrHead struct {
|
||||||
|
name string
|
||||||
|
enabled bool
|
||||||
|
scale float64
|
||||||
|
currentModeID uint32
|
||||||
|
modeIDs []uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrMode struct {
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
refresh int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type randrClient struct {
|
||||||
|
display *wlclient.Display
|
||||||
|
ctx *wlclient.Context
|
||||||
|
manager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
|
heads map[uint32]*randrHead
|
||||||
|
modes map[uint32]*randrMode
|
||||||
|
done bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryRandr() ([]randrOutput, error) {
|
||||||
|
display, err := wlclient.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &randrClient{
|
||||||
|
display: display,
|
||||||
|
ctx: display.Context(),
|
||||||
|
heads: make(map[uint32]*randrHead),
|
||||||
|
modes: make(map[uint32]*randrMode),
|
||||||
|
}
|
||||||
|
defer c.ctx.Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get registry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||||
|
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||||
|
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
|
||||||
|
version := min(e.Version, 4)
|
||||||
|
|
||||||
|
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||||
|
c.handleHead(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||||
|
c.done = true
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||||
|
c.manager = mgr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// First roundtrip: discover globals and bind manager
|
||||||
|
syncCallback, err := display.Sync()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sync display: %w", err)
|
||||||
|
}
|
||||||
|
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||||
|
if c.manager == nil {
|
||||||
|
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
|
||||||
|
c.done = true
|
||||||
|
}
|
||||||
|
// Otherwise wait for manager's DoneHandler
|
||||||
|
})
|
||||||
|
|
||||||
|
for !c.done {
|
||||||
|
if err := c.ctx.Dispatch(); err != nil {
|
||||||
|
return nil, fmt.Errorf("dispatch error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.err != nil {
|
||||||
|
return nil, c.err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.buildOutputs(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||||
|
handle := e.Head
|
||||||
|
headID := handle.ID()
|
||||||
|
|
||||||
|
head := &randrHead{
|
||||||
|
modeIDs: make([]uint32, 0),
|
||||||
|
}
|
||||||
|
c.heads[headID] = head
|
||||||
|
|
||||||
|
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||||
|
head.name = e.Name
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
|
||||||
|
head.enabled = e.Enabled != 0
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||||
|
head.scale = e.Scale
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
|
||||||
|
head.currentModeID = e.Mode.ID()
|
||||||
|
})
|
||||||
|
|
||||||
|
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
|
||||||
|
modeHandle := e.Mode
|
||||||
|
modeID := modeHandle.ID()
|
||||||
|
|
||||||
|
head.modeIDs = append(head.modeIDs, modeID)
|
||||||
|
|
||||||
|
mode := &randrMode{}
|
||||||
|
c.modes[modeID] = mode
|
||||||
|
|
||||||
|
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
|
||||||
|
mode.width = e.Width
|
||||||
|
mode.height = e.Height
|
||||||
|
})
|
||||||
|
|
||||||
|
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
|
||||||
|
mode.refresh = e.Refresh
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *randrClient) buildOutputs() []randrOutput {
|
||||||
|
outputs := make([]randrOutput, 0, len(c.heads))
|
||||||
|
|
||||||
|
for _, head := range c.heads {
|
||||||
|
out := randrOutput{
|
||||||
|
Name: head.name,
|
||||||
|
Scale: head.scale,
|
||||||
|
Enabled: head.enabled,
|
||||||
|
}
|
||||||
|
|
||||||
|
if mode, ok := c.modes[head.currentModeID]; ok {
|
||||||
|
out.Width = mode.width
|
||||||
|
out.Height = mode.height
|
||||||
|
out.Refresh = mode.refresh
|
||||||
|
}
|
||||||
|
|
||||||
|
outputs = append(outputs, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputs
|
||||||
|
}
|
||||||
@@ -616,6 +616,43 @@ func getShellIPCCompletions(args []string, _ string) []string {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getFirstDMSPID() (int, bool) {
|
||||||
|
dir := getRuntimeDir()
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !strings.HasPrefix(entry.Name(), "danklinux-") || !strings.HasSuffix(entry.Name(), ".pid") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(filepath.Join(dir, entry.Name()))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pid, err := strconv.Atoi(strings.TrimSpace(string(data)))
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
proc, err := os.FindProcess(pid)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if proc.Signal(syscall.Signal(0)) != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return pid, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
func runShellIPCCommand(args []string) {
|
func runShellIPCCommand(args []string) {
|
||||||
if len(args) == 0 {
|
if len(args) == 0 {
|
||||||
printIPCHelp()
|
printIPCHelp()
|
||||||
@@ -627,10 +664,21 @@ func runShellIPCCommand(args []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmdArgs := []string{"ipc"}
|
cmdArgs := []string{"ipc"}
|
||||||
|
|
||||||
|
switch pid, ok := getFirstDMSPID(); {
|
||||||
|
case ok:
|
||||||
|
cmdArgs = append(cmdArgs, "--pid", strconv.Itoa(pid))
|
||||||
|
default:
|
||||||
|
if err := findConfig(nil, nil); err != nil {
|
||||||
|
log.Fatalf("Error finding config: %v", err)
|
||||||
|
}
|
||||||
|
// ! TODO - remove check when QS 0.3 is released
|
||||||
if qsHasAnyDisplay() {
|
if qsHasAnyDisplay() {
|
||||||
cmdArgs = append(cmdArgs, "--any-display")
|
cmdArgs = append(cmdArgs, "--any-display")
|
||||||
}
|
}
|
||||||
cmdArgs = append(cmdArgs, "-p", configPath)
|
cmdArgs = append(cmdArgs, "-p", configPath)
|
||||||
|
}
|
||||||
|
|
||||||
cmdArgs = append(cmdArgs, args...)
|
cmdArgs = append(cmdArgs, args...)
|
||||||
cmd := exec.Command("qs", cmdArgs...)
|
cmd := exec.Command("qs", cmdArgs...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
|
|||||||
@@ -7,12 +7,20 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func findCommandPath(cmd string) (string, error) {
|
// isReadOnlyCommand returns true if the CLI args indicate a command that is
|
||||||
path, err := exec.LookPath(cmd)
|
// safe to run as root (e.g. shell completion, help).
|
||||||
if err != nil {
|
func isReadOnlyCommand(args []string) bool {
|
||||||
return "", fmt.Errorf("command '%s' not found in PATH", cmd)
|
for _, arg := range args[1:] {
|
||||||
|
if strings.HasPrefix(arg, "-") {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return path, nil
|
switch arg {
|
||||||
|
case "completion", "help", "__complete":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func isArchPackageInstalled(packageName string) bool {
|
func isArchPackageInstalled(packageName string) bool {
|
||||||
|
|||||||
35
core/internal/blur/probe.go
Normal file
35
core/internal/blur/probe.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package blur
|
||||||
|
|
||||||
|
import (
|
||||||
|
wlhelpers "github.com/AvengeMedia/DankMaterialShell/core/internal/wayland/client"
|
||||||
|
client "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const extBackgroundEffectInterface = "ext_background_effect_manager_v1"
|
||||||
|
|
||||||
|
func ProbeSupport() (bool, error) {
|
||||||
|
display, err := client.Connect("")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer display.Context().Close()
|
||||||
|
|
||||||
|
registry, err := display.GetRegistry()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
found := false
|
||||||
|
registry.SetGlobalHandler(func(e client.RegistryGlobalEvent) {
|
||||||
|
switch e.Interface {
|
||||||
|
case extBackgroundEffectInterface:
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := wlhelpers.Roundtrip(display, display.Context()); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return found, nil
|
||||||
|
}
|
||||||
@@ -5,55 +5,196 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/ext_data_control"
|
||||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const envServe = "_DMS_CLIPBOARD_SERVE"
|
||||||
|
const envMime = "_DMS_CLIPBOARD_MIME"
|
||||||
|
const envPasteOnce = "_DMS_CLIPBOARD_PASTE_ONCE"
|
||||||
|
const envCacheFile = "_DMS_CLIPBOARD_CACHE"
|
||||||
|
|
||||||
|
// MaybeServeAndExit intercepts before cobra when re-exec'd as a clipboard
|
||||||
|
// child. Reads source data into memory, deletes any cache file, then serves.
|
||||||
|
func MaybeServeAndExit() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mimeType := os.Getenv(envMime)
|
||||||
|
pasteOnce := os.Getenv(envPasteOnce) == "1"
|
||||||
|
cachePath := os.Getenv(envCacheFile)
|
||||||
|
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case cachePath != "":
|
||||||
|
data, err = os.ReadFile(cachePath)
|
||||||
|
os.Remove(cachePath)
|
||||||
|
default:
|
||||||
|
data, err = io.ReadAll(os.Stdin)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: read source: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := serveClipboard(data, mimeType, pasteOnce); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "clipboard: serve: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
func Copy(data []byte, mimeType string) error {
|
func Copy(data []byte, mimeType string) error {
|
||||||
return CopyOpts(data, mimeType, false, false)
|
return copyForkCached(data, mimeType, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
func CopyOpts(data []byte, mimeType string, foreground, pasteOnce bool) error {
|
||||||
if !foreground {
|
if foreground {
|
||||||
|
return serveClipboard(data, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
return copyForkCached(data, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func CopyReader(data io.Reader, mimeType string, foreground, pasteOnce bool) error {
|
||||||
|
if foreground {
|
||||||
|
buf, err := io.ReadAll(data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("read source: %w", err)
|
||||||
|
}
|
||||||
|
return serveClipboard(buf, mimeType, pasteOnce)
|
||||||
|
}
|
||||||
return copyFork(data, mimeType, pasteOnce)
|
return copyFork(data, mimeType, pasteOnce)
|
||||||
}
|
}
|
||||||
return copyServe(data, mimeType, pasteOnce)
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyFork(data []byte, mimeType string, pasteOnce bool) error {
|
func newForkCmd(mimeType string, pasteOnce bool, extra ...string) *exec.Cmd {
|
||||||
args := []string{os.Args[0], "cl", "copy", "--foreground"}
|
cmd := exec.Command(os.Args[0])
|
||||||
if pasteOnce {
|
|
||||||
args = append(args, "--paste-once")
|
|
||||||
}
|
|
||||||
args = append(args, "--type", mimeType)
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], args[1:]...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = nil
|
|
||||||
cmd.Stderr = nil
|
cmd.Stderr = nil
|
||||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||||
|
cmd.Env = append(os.Environ(),
|
||||||
|
envServe+"=1",
|
||||||
|
envMime+"="+mimeType,
|
||||||
|
)
|
||||||
|
if pasteOnce {
|
||||||
|
cmd.Env = append(cmd.Env, envPasteOnce+"=1")
|
||||||
|
}
|
||||||
|
cmd.Env = append(cmd.Env, extra...)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitReady(cmd *exec.Cmd) error {
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return fmt.Errorf("start: %w", err)
|
||||||
|
}
|
||||||
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyForkCached(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
|
cacheFile, err := createClipboardCacheFile()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create cache file: %w", err)
|
||||||
|
}
|
||||||
|
cachePath := cacheFile.Name()
|
||||||
|
|
||||||
|
if _, err := cacheFile.Write(data); err != nil {
|
||||||
|
cacheFile.Close()
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("write cache file: %w", err)
|
||||||
|
}
|
||||||
|
if err := cacheFile.Close(); err != nil {
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return fmt.Errorf("close cache file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := newForkCmd(mimeType, pasteOnce, envCacheFile+"="+cachePath)
|
||||||
|
cmd.Stdin = nil
|
||||||
|
if err := waitReady(cmd); err != nil {
|
||||||
|
os.Remove(cachePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyFork(data io.Reader, mimeType string, pasteOnce bool) error {
|
||||||
|
cmd := newForkCmd(mimeType, pasteOnce)
|
||||||
|
|
||||||
|
switch src := data.(type) {
|
||||||
|
case *os.File:
|
||||||
|
cmd.Stdin = src
|
||||||
|
return waitReady(cmd)
|
||||||
|
|
||||||
|
default:
|
||||||
stdin, err := cmd.StdinPipe()
|
stdin, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stdin pipe: %w", err)
|
return fmt.Errorf("stdin pipe: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("stdout pipe: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("start: %w", err)
|
return fmt.Errorf("start: %w", err)
|
||||||
}
|
}
|
||||||
|
if _, err := io.Copy(stdin, data); err != nil {
|
||||||
if _, err := stdin.Write(data); err != nil {
|
|
||||||
stdin.Close()
|
stdin.Close()
|
||||||
return fmt.Errorf("write stdin: %w", err)
|
return fmt.Errorf("write stdin: %w", err)
|
||||||
}
|
}
|
||||||
stdin.Close()
|
if err := stdin.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close stdin: %w", err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
var buf [1]byte
|
||||||
|
if _, err := stdout.Read(buf[:]); err != nil {
|
||||||
|
return fmt.Errorf("waiting for clipboard ready: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func signalReady() {
|
||||||
|
if os.Getenv(envServe) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Stdout.Write([]byte{1})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createClipboardCacheFile() (*os.File, error) {
|
||||||
|
preferredDirs := []string{}
|
||||||
|
|
||||||
|
if cacheDir, err := os.UserCacheDir(); err == nil {
|
||||||
|
preferredDirs = append(preferredDirs, filepath.Join(cacheDir, "dms", "clipboard"))
|
||||||
|
}
|
||||||
|
preferredDirs = append(preferredDirs, "/var/tmp/dms/clipboard")
|
||||||
|
|
||||||
|
for _, dir := range preferredDirs {
|
||||||
|
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cachedData, err := os.CreateTemp(dir, "dms-clipboard-*")
|
||||||
|
if err == nil {
|
||||||
|
return cachedData, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return os.CreateTemp("", "dms-clipboard-*")
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveClipboard(data []byte, mimeType string, pasteOnce bool) error {
|
||||||
display, err := wlclient.Connect("")
|
display, err := wlclient.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("wayland connect: %w", err)
|
return fmt.Errorf("wayland connect: %w", err)
|
||||||
@@ -95,12 +236,10 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -141,10 +280,10 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
file.Write(data)
|
_, _ = file.Write(data)
|
||||||
select {
|
select {
|
||||||
case pasted <- struct{}{}:
|
case pasted <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
@@ -160,6 +299,7 @@ func copyServe(data []byte, mimeType string, pasteOnce bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
display.Roundtrip()
|
display.Roundtrip()
|
||||||
|
signalReady()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -418,12 +558,10 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
if bindErr != nil {
|
if bindErr != nil {
|
||||||
return fmt.Errorf("registry bind: %w", bindErr)
|
return fmt.Errorf("registry bind: %w", bindErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataControlMgr == nil {
|
if dataControlMgr == nil {
|
||||||
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
return fmt.Errorf("compositor does not support ext_data_control_manager_v1")
|
||||||
}
|
}
|
||||||
defer dataControlMgr.Destroy()
|
defer dataControlMgr.Destroy()
|
||||||
|
|
||||||
if seat == nil {
|
if seat == nil {
|
||||||
return fmt.Errorf("no seat available")
|
return fmt.Errorf("no seat available")
|
||||||
}
|
}
|
||||||
@@ -451,12 +589,12 @@ func copyMultiServe(offers []Offer, pasteOnce bool) error {
|
|||||||
pasted := make(chan struct{}, 1)
|
pasted := make(chan struct{}, 1)
|
||||||
|
|
||||||
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
source.SetSendHandler(func(e ext_data_control.ExtDataControlSourceV1SendEvent) {
|
||||||
defer syscall.Close(e.Fd)
|
_ = syscall.SetNonblock(e.Fd, false)
|
||||||
file := os.NewFile(uintptr(e.Fd), "pipe")
|
file := os.NewFile(uintptr(e.Fd), "pipe")
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
if data, ok := offerMap[e.MimeType]; ok {
|
if data, ok := offerMap[e.MimeType]; ok {
|
||||||
file.Write(data)
|
_, _ = file.Write(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ bind = SUPER, bracketright, layoutmsg, preselect r
|
|||||||
|
|
||||||
# === Sizing & Layout ===
|
# === Sizing & Layout ===
|
||||||
bind = SUPER, R, layoutmsg, togglesplit
|
bind = SUPER, R, layoutmsg, togglesplit
|
||||||
bind = SUPER CTRL, F, resizeactive, exact 100%
|
bind = SUPER CTRL, F, resizeactive, exact 100% 100%
|
||||||
|
|
||||||
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
# === Move/resize windows with mainMod + LMB/RMB and dragging ===
|
||||||
bindmd = SUPER, mouse:272, Move window, movewindow
|
bindmd = SUPER, mouse:272, Move window, movewindow
|
||||||
|
|||||||
@@ -94,13 +94,14 @@ windowrule = tile on, match:class ^(gnome-control-center)$
|
|||||||
windowrule = tile on, match:class ^(pavucontrol)$
|
windowrule = tile on, match:class ^(pavucontrol)$
|
||||||
windowrule = tile on, match:class ^(nm-connection-editor)$
|
windowrule = tile on, match:class ^(nm-connection-editor)$
|
||||||
|
|
||||||
|
windowrule = float on, match:class ^(org\.gnome\.Calculator)$
|
||||||
windowrule = float on, match:class ^(gnome-calculator)$
|
windowrule = float on, match:class ^(gnome-calculator)$
|
||||||
windowrule = float on, match:class ^(galculator)$
|
windowrule = float on, match:class ^(galculator)$
|
||||||
windowrule = float on, match:class ^(blueman-manager)$
|
windowrule = float on, match:class ^(blueman-manager)$
|
||||||
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
windowrule = float on, match:class ^(org\.gnome\.Nautilus)$
|
||||||
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
windowrule = float on, match:class ^(xdg-desktop-portal)$
|
||||||
|
|
||||||
windowrule = noinitialfocus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
windowrule = no_initial_focus on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||||
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
windowrule = pin on, match:class ^(steam)$, match:title ^(notificationtoasts)
|
||||||
|
|
||||||
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
windowrule = float on, match:class ^(firefox)$, match:title ^(Picture-in-Picture)$
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ window-rule {
|
|||||||
open-floating false
|
open-floating false
|
||||||
}
|
}
|
||||||
window-rule {
|
window-rule {
|
||||||
|
match app-id=r#"^org\.gnome\.Calculator$"#
|
||||||
match app-id=r#"^gnome-calculator$"#
|
match app-id=r#"^gnome-calculator$"#
|
||||||
match app-id=r#"^galculator$"#
|
match app-id=r#"^galculator$"#
|
||||||
match app-id=r#"^blueman-manager$"#
|
match app-id=r#"^blueman-manager$"#
|
||||||
@@ -252,6 +253,7 @@ window-rule {
|
|||||||
// Open dms windows as floating by default
|
// Open dms windows as floating by default
|
||||||
window-rule {
|
window-rule {
|
||||||
match app-id=r#"org.quickshell$"#
|
match app-id=r#"org.quickshell$"#
|
||||||
|
match app-id=r#"com.danklinux.dms$"#
|
||||||
open-floating true
|
open-floating true
|
||||||
}
|
}
|
||||||
debug {
|
debug {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -26,6 +27,9 @@ func init() {
|
|||||||
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("cachyos", "#08A283", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("catos", "#1793D1", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewArchDistribution(config, logChan)
|
||||||
|
})
|
||||||
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("endeavouros", "#7F3FBF", FamilyArch, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewArchDistribution(config, logChan)
|
return NewArchDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
@@ -94,6 +98,7 @@ func (a *ArchDistribution) DetectDependenciesWithTerminal(ctx context.Context, w
|
|||||||
dependencies = append(dependencies, a.detectGit())
|
dependencies = append(dependencies, a.detectGit())
|
||||||
dependencies = append(dependencies, a.detectWindowManager(wm))
|
dependencies = append(dependencies, a.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, a.detectQuickshell())
|
dependencies = append(dependencies, a.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, a.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, a.detectXDGPortal())
|
dependencies = append(dependencies, a.detectXDGPortal())
|
||||||
dependencies = append(dependencies, a.detectAccountsService())
|
dependencies = append(dependencies, a.detectAccountsService())
|
||||||
|
|
||||||
@@ -121,12 +126,52 @@ func (a *ArchDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
return a.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", a.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return a.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", a.packageInstalled("greetd-dms-greeter-git"))
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
func (a *ArchDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("pacman", "-Q", pkg)
|
cmd := exec.Command("pacman", "-Q", pkg)
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// parseSRCINFODeps reads a .SRCINFO file and returns runtime dep and makedep package
|
||||||
|
func parseSRCINFODeps(srcinfoPath string) (deps []string, makedeps []string, err error) {
|
||||||
|
data, err := os.ReadFile(srcinfoPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
var pkg string
|
||||||
|
var target *[]string
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(line, "makedepends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "makedepends = ")
|
||||||
|
target = &makedeps
|
||||||
|
case strings.HasPrefix(line, "depends = "):
|
||||||
|
pkg = strings.TrimPrefix(line, "depends = ")
|
||||||
|
target = &deps
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Strip version constraint (>=, <=, >, <, =) and colon-descriptions
|
||||||
|
if idx := strings.IndexAny(pkg, "><:="); idx >= 0 {
|
||||||
|
pkg = pkg[:idx]
|
||||||
|
}
|
||||||
|
pkg = strings.TrimSpace(pkg)
|
||||||
|
if pkg != "" {
|
||||||
|
*target = append(*target, pkg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return deps, makedeps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) isInSystemRepo(pkg string) bool {
|
||||||
|
return exec.Command("pacman", "-Si", pkg).Run() == nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (a *ArchDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return a.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -136,6 +181,7 @@ func (a *ArchDistribution) GetPackageMappingWithVariants(wm deps.WindowManager,
|
|||||||
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": a.getDMSMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"git": {Name: "git", Repository: RepoTypeSystem},
|
"git": {Name: "git", Repository: RepoTypeSystem},
|
||||||
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": a.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "greetd-dms-greeter-git", Repository: RepoTypeAUR},
|
||||||
"matugen": a.getMatugenMapping(variants["matugen"]),
|
"matugen": a.getMatugenMapping(variants["matugen"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
"dgop": {Name: "dgop", Repository: RepoTypeSystem},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeSystem},
|
||||||
@@ -197,11 +243,7 @@ func (a *ArchDistribution) getDMSMapping(variant deps.PackageVariant) PackageMap
|
|||||||
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
return PackageMapping{Name: "dms-shell-git", Repository: RepoTypeAUR}
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.packageInstalled("dms-shell-bin") {
|
return PackageMapping{Name: "dms-shell", Repository: RepoTypeSystem}
|
||||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
|
||||||
}
|
|
||||||
|
|
||||||
return PackageMapping{Name: "dms-shell-bin", Repository: RepoTypeAUR}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
func (a *ArchDistribution) detectXwaylandSatellite() deps.Dependency {
|
||||||
@@ -251,7 +293,7 @@ func (a *ArchDistribution) InstallPrerequisites(ctx context.Context, sudoPasswor
|
|||||||
LogOutput: "Installing base-devel development tools",
|
LogOutput: "Installing base-devel development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -S --needed --noconfirm base-devel")
|
||||||
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
if err := a.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.10); err != nil {
|
||||||
return fmt.Errorf("failed to install base-devel: %w", err)
|
return fmt.Errorf("failed to install base-devel: %w", err)
|
||||||
}
|
}
|
||||||
@@ -283,6 +325,13 @@ func (a *ArchDistribution) InstallPackages(ctx context.Context, dependencies []d
|
|||||||
|
|
||||||
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
systemPkgs, aurPkgs, manualPkgs, variantMap := a.categorizePackages(dependencies, wm, reinstallFlags, disabledFlags)
|
||||||
|
|
||||||
|
if slices.Contains(aurPkgs, "quickshell-git") && slices.Contains(systemPkgs, "dms-shell") {
|
||||||
|
if err := a.preinstallQuickshellGit(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to preinstall quickshell-git: %w", err)
|
||||||
|
}
|
||||||
|
aurPkgs = slices.DeleteFunc(aurPkgs, func(p string) bool { return p == "quickshell-git" })
|
||||||
|
}
|
||||||
|
|
||||||
// Phase 3: System Packages
|
// Phase 3: System Packages
|
||||||
if len(systemPkgs) > 0 {
|
if len(systemPkgs) > 0 {
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -400,6 +449,37 @@ func (a *ArchDistribution) categorizePackages(dependencies []deps.Dependency, wm
|
|||||||
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
return systemPkgs, aurPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) preinstallQuickshellGit(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
if a.packageInstalled("quickshell-git") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.packageInstalled("quickshell") {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.15,
|
||||||
|
Step: "Removing stable quickshell...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: "sudo pacman -Rdd --noconfirm quickshell",
|
||||||
|
LogOutput: "Removing stable quickshell so quickshell-git can be installed",
|
||||||
|
}
|
||||||
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "pacman -Rdd --noconfirm quickshell")
|
||||||
|
if err := a.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.15, 0.18); err != nil {
|
||||||
|
return fmt.Errorf("failed to remove stable quickshell: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: 0.18,
|
||||||
|
Step: "Building quickshell-git before system packages...",
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: "Installing quickshell-git ahead of dms-shell to avoid conflict",
|
||||||
|
}
|
||||||
|
return a.installSingleAURPackage(ctx, "quickshell-git", sudoPassword, progressChan, 0.18, 0.32)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -408,6 +488,9 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing system packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
args := []string{"pacman", "-S", "--needed", "--noconfirm"}
|
||||||
|
if slices.Contains(packages, "dms-shell") {
|
||||||
|
args = append(args, "--assume-installed", "dms-shell-compositor=1")
|
||||||
|
}
|
||||||
args = append(args, packages...)
|
args = append(args, packages...)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
@@ -419,7 +502,7 @@ func (a *ArchDistribution) installSystemPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return a.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,29 +514,10 @@ func (a *ArchDistribution) installAURPackages(ctx context.Context, packages []st
|
|||||||
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
a.log(fmt.Sprintf("Installing AUR packages manually: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
hasNiri := false
|
hasNiri := false
|
||||||
hasQuickshell := false
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "niri-git" {
|
if pkg == "niri-git" {
|
||||||
hasNiri = true
|
hasNiri = true
|
||||||
}
|
}
|
||||||
if pkg == "quickshell" || pkg == "quickshell-git" {
|
|
||||||
hasQuickshell = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If quickshell is in the list, always reinstall google-breakpad first
|
|
||||||
if hasQuickshell {
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseAURPackages,
|
|
||||||
Progress: 0.63,
|
|
||||||
Step: "Reinstalling google-breakpad for quickshell...",
|
|
||||||
IsComplete: false,
|
|
||||||
CommandInfo: "Reinstalling prerequisite AUR package for quickshell",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.installSingleAURPackage(ctx, "google-breakpad", sudoPassword, progressChan, 0.63, 0.65); err != nil {
|
|
||||||
return fmt.Errorf("failed to reinstall google-breakpad prerequisite for quickshell: %w", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
// If niri is in the list, install makepkg-git-lfs-proto first if not already installed
|
||||||
@@ -514,7 +578,7 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
var dmsShell []string
|
var dmsShell []string
|
||||||
|
|
||||||
for _, pkg := range packages {
|
for _, pkg := range packages {
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
dmsShell = append(dmsShell, pkg)
|
dmsShell = append(dmsShell, pkg)
|
||||||
} else {
|
} else {
|
||||||
isDep := false
|
isDep := false
|
||||||
@@ -534,6 +598,16 @@ func (a *ArchDistribution) reorderAURPackages(packages []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64) error {
|
||||||
|
return a.installSingleAURPackageInternal(ctx, pkg, sudoPassword, progressChan, startProgress, endProgress, make(map[string]bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ArchDistribution) installSingleAURPackageInternal(ctx context.Context, pkg, sudoPassword string, progressChan chan<- InstallProgressMsg, startProgress, endProgress float64, visited map[string]bool) error {
|
||||||
|
if visited[pkg] {
|
||||||
|
a.log(fmt.Sprintf("Skipping %s (already being installed, cycle detected)", pkg))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
visited[pkg] = true
|
||||||
|
|
||||||
homeDir, err := os.UserHomeDir()
|
homeDir, err := os.UserHomeDir()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get user home directory: %w", err)
|
return fmt.Errorf("failed to get user home directory: %w", err)
|
||||||
@@ -585,7 +659,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
if pkg == "dms-shell-git" {
|
||||||
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
srcinfoPath := filepath.Join(packageDir, ".SRCINFO")
|
||||||
depsToRemove := []string{
|
depsToRemove := []string{
|
||||||
"depends = quickshell",
|
"depends = quickshell",
|
||||||
@@ -607,54 +681,66 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
return fmt.Errorf("failed to remove optdepends from .SRCINFO for %s: %w", pkg, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip dependency installation for dms-shell-git and dms-shell-bin
|
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
||||||
// since we manually manage those dependencies
|
{
|
||||||
if pkg != "dms-shell-git" && pkg != "dms-shell-bin" {
|
|
||||||
// Pre-install dependencies from .SRCINFO
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.3*(endProgress-startProgress),
|
Progress: startProgress + 0.3*(endProgress-startProgress),
|
||||||
Step: fmt.Sprintf("Installing dependencies for %s...", pkg),
|
Step: fmt.Sprintf("Resolving dependencies for %s...", pkg),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
CommandInfo: "Installing package dependencies and makedepends",
|
CommandInfo: "Classifying dependencies as system or AUR",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install dependencies and makedepends explicitly
|
runtimeDeps, makeDeps, err := parseSRCINFODeps(srcinfoPath)
|
||||||
srcinfoPath = filepath.Join(packageDir, ".SRCINFO")
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse .SRCINFO for %s: %w", pkg, err)
|
||||||
depsCmd := exec.CommandContext(ctx, "bash", "-c",
|
|
||||||
fmt.Sprintf(`
|
|
||||||
deps=$(grep "depends = " "%s" | grep -v "makedepends" | sed 's/.*depends = //' | tr '\n' ' ' | sed 's/[[:space:]]*$//')
|
|
||||||
if [[ "%s" == *"quickshell"* ]]; then
|
|
||||||
deps=$(echo "$deps" | sed 's/google-breakpad//g' | sed 's/ / /g' | sed 's/^ *//g' | sed 's/ *$//g')
|
|
||||||
fi
|
|
||||||
if [ ! -z "$deps" ] && [ "$deps" != " " ]; then
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $deps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, pkg, sudoPassword))
|
|
||||||
|
|
||||||
if err := a.runWithProgress(depsCmd, progressChan, PhaseAURPackages, startProgress+0.3*(endProgress-startProgress), startProgress+0.35*(endProgress-startProgress)); err != nil {
|
|
||||||
return fmt.Errorf("FAILED to install runtime dependencies for %s: %w", pkg, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
makedepsCmd := exec.CommandContext(ctx, "bash", "-c",
|
seen := make(map[string]bool)
|
||||||
fmt.Sprintf(`
|
var systemPkgs []string
|
||||||
makedeps=$(grep -E "^[[:space:]]*makedepends = " "%s" | sed 's/^[[:space:]]*makedepends = //' | tr '\n' ' ')
|
var aurPkgs []string
|
||||||
if [ ! -z "$makedeps" ]; then
|
|
||||||
echo '%s' | sudo -S pacman -S --needed --noconfirm $makedeps
|
|
||||||
fi
|
|
||||||
`, srcinfoPath, sudoPassword))
|
|
||||||
|
|
||||||
if err := a.runWithProgress(makedepsCmd, progressChan, PhaseAURPackages, startProgress+0.35*(endProgress-startProgress), startProgress+0.4*(endProgress-startProgress)); err != nil {
|
for _, dep := range append(runtimeDeps, makeDeps...) {
|
||||||
return fmt.Errorf("FAILED to install make dependencies for %s: %w", pkg, err)
|
if seen[dep] || a.packageInstalled(dep) {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
seen[dep] = true
|
||||||
|
if a.isInSystemRepo(dep) {
|
||||||
|
systemPkgs = append(systemPkgs, dep)
|
||||||
} else {
|
} else {
|
||||||
|
aurPkgs = append(aurPkgs, dep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(systemPkgs) > 0 {
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhaseAURPackages,
|
||||||
|
Progress: startProgress + 0.32*(endProgress-startProgress),
|
||||||
|
Step: fmt.Sprintf("Installing %d system dependencies for %s...", len(systemPkgs), pkg),
|
||||||
|
IsComplete: false,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo pacman -S --needed --noconfirm %s", strings.Join(systemPkgs, " ")),
|
||||||
|
}
|
||||||
|
if err := a.installSystemPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to install system dependencies for %s: %w", pkg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, aurDep := range aurPkgs {
|
||||||
|
a.log(fmt.Sprintf("Dependency %s is AUR-only, building from source...", aurDep))
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
Progress: startProgress + 0.35*(endProgress-startProgress),
|
Progress: startProgress + 0.35*(endProgress-startProgress),
|
||||||
Step: fmt.Sprintf("Skipping dependency installation for %s (manually managed)...", pkg),
|
Step: fmt.Sprintf("Installing AUR dependency %s for %s...", aurDep, pkg),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Dependencies for %s are installed separately", pkg),
|
CommandInfo: fmt.Sprintf("Building AUR dependency: %s", aurDep),
|
||||||
|
}
|
||||||
|
if err := a.installSingleAURPackageInternal(ctx, aurDep, sudoPassword, progressChan,
|
||||||
|
startProgress+0.35*(endProgress-startProgress),
|
||||||
|
startProgress+0.39*(endProgress-startProgress),
|
||||||
|
visited,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("failed to install AUR dependency %s for %s: %w", aurDep, pkg, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +754,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
|
|
||||||
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
buildCmd := exec.CommandContext(ctx, "makepkg", "--noconfirm")
|
||||||
buildCmd.Dir = packageDir
|
buildCmd.Dir = packageDir
|
||||||
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar") // Disable compression for speed
|
buildCmd.Env = append(os.Environ(), "PKGEXT=.pkg.tar")
|
||||||
|
|
||||||
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
if err := a.runWithProgress(buildCmd, progressChan, PhaseAURPackages, startProgress+0.4*(endProgress-startProgress), startProgress+0.7*(endProgress-startProgress)); err != nil {
|
||||||
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
return fmt.Errorf("failed to build %s: %w", pkg, err)
|
||||||
@@ -683,42 +769,9 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
CommandInfo: "sudo pacman -U built-package",
|
CommandInfo: "sudo pacman -U built-package",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find .pkg.tar* files - for split packages, install the base and any installed compositor variants
|
|
||||||
var files []string
|
var files []string
|
||||||
if pkg == "dms-shell-git" || pkg == "dms-shell-bin" {
|
|
||||||
// For DMS split packages, install base package
|
|
||||||
pattern := filepath.Join(packageDir, fmt.Sprintf("%s-%s*.pkg.tar*", pkg, "*"))
|
|
||||||
matches, err := filepath.Glob(pattern)
|
|
||||||
if err == nil {
|
|
||||||
for _, match := range matches {
|
|
||||||
basename := filepath.Base(match)
|
|
||||||
// Always include base package
|
|
||||||
if !strings.Contains(basename, "hyprland") && !strings.Contains(basename, "niri") {
|
|
||||||
files = append(files, match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also update compositor-specific packages if they're installed
|
|
||||||
if strings.HasSuffix(pkg, "-git") {
|
|
||||||
if a.packageInstalled("dms-shell-hyprland-git") {
|
|
||||||
hyprlandPattern := filepath.Join(packageDir, "dms-shell-hyprland-git-*.pkg.tar*")
|
|
||||||
if hyprlandMatches, err := filepath.Glob(hyprlandPattern); err == nil && len(hyprlandMatches) > 0 {
|
|
||||||
files = append(files, hyprlandMatches[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if a.packageInstalled("dms-shell-niri-git") {
|
|
||||||
niriPattern := filepath.Join(packageDir, "dms-shell-niri-git-*.pkg.tar*")
|
|
||||||
if niriMatches, err := filepath.Glob(niriPattern); err == nil && len(niriMatches) > 0 {
|
|
||||||
files = append(files, niriMatches[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For other packages, install all built packages
|
|
||||||
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
matches, _ := filepath.Glob(filepath.Join(packageDir, "*.pkg.tar*"))
|
||||||
files = matches
|
files = matches
|
||||||
}
|
|
||||||
|
|
||||||
if len(files) == 0 {
|
if len(files) == 0 {
|
||||||
return fmt.Errorf("no package files found after building %s", pkg)
|
return fmt.Errorf("no package files found after building %s", pkg)
|
||||||
@@ -727,7 +780,7 @@ func (a *ArchDistribution) installSingleAURPackage(ctx context.Context, pkg, sud
|
|||||||
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
installArgs := []string{"pacman", "-U", "--noconfirm"}
|
||||||
installArgs = append(installArgs, files...)
|
installArgs = append(installArgs, files...)
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(installArgs, " "))
|
||||||
|
|
||||||
fileNames := make([]string, len(files))
|
fileNames := make([]string, len(files))
|
||||||
for i, f := range files {
|
for i, f := range files {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -55,27 +56,6 @@ func (b *BaseDistribution) logError(message string, err error) {
|
|||||||
b.log(errorMsg)
|
b.log(errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// escapeSingleQuotes escapes single quotes in a string for safe use in bash single-quoted strings.
|
|
||||||
// It replaces each ' with '\” which closes the quote, adds an escaped quote, and reopens the quote.
|
|
||||||
// This prevents shell injection and syntax errors when passwords contain single quotes or apostrophes.
|
|
||||||
func escapeSingleQuotes(s string) string {
|
|
||||||
return strings.ReplaceAll(s, "'", "'\\''")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeSudoCommand creates a command string that safely passes password to sudo.
|
|
||||||
// This helper escapes special characters in the password to prevent shell injection
|
|
||||||
// and syntax errors when passwords contain single quotes, apostrophes, or other special chars.
|
|
||||||
func MakeSudoCommand(sudoPassword string, command string) string {
|
|
||||||
return fmt.Sprintf("echo '%s' | sudo -S %s", escapeSingleQuotes(sudoPassword), command)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExecSudoCommand creates an exec.Cmd that runs a command with sudo using the provided password.
|
|
||||||
// The password is properly escaped to prevent shell injection and syntax errors.
|
|
||||||
func ExecSudoCommand(ctx context.Context, sudoPassword string, command string) *exec.Cmd {
|
|
||||||
cmdStr := MakeSudoCommand(sudoPassword, command)
|
|
||||||
return exec.CommandContext(ctx, "bash", "-c", cmdStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
func (b *BaseDistribution) detectCommand(name, description string) deps.Dependency {
|
||||||
status := deps.StatusMissing
|
status := deps.StatusMissing
|
||||||
if b.commandExists(name) {
|
if b.commandExists(name) {
|
||||||
@@ -102,6 +82,19 @@ func (b *BaseDistribution) detectPackage(name, description string, installed boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseDistribution) detectOptionalPackage(name, description string, installed bool) deps.Dependency {
|
||||||
|
status := deps.StatusMissing
|
||||||
|
if installed {
|
||||||
|
status = deps.StatusInstalled
|
||||||
|
}
|
||||||
|
return deps.Dependency{
|
||||||
|
Name: name,
|
||||||
|
Status: status,
|
||||||
|
Description: description,
|
||||||
|
Required: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseDistribution) detectGit() deps.Dependency {
|
func (b *BaseDistribution) detectGit() deps.Dependency {
|
||||||
return b.detectCommand("git", "Version control system")
|
return b.detectCommand("git", "Version control system")
|
||||||
}
|
}
|
||||||
@@ -697,7 +690,7 @@ func (b *BaseDistribution) installDMSBinary(ctx context.Context, sudoPassword st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Install to /usr/local/bin
|
// Install to /usr/local/bin
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
fmt.Sprintf("cp %s /usr/local/bin/dms", binaryPath))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install DMS binary: %w", err)
|
return fmt.Errorf("failed to install DMS binary: %w", err)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -61,6 +61,7 @@ func (d *DebianDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, d.detectGit())
|
dependencies = append(dependencies, d.detectGit())
|
||||||
dependencies = append(dependencies, d.detectWindowManager(wm))
|
dependencies = append(dependencies, d.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, d.detectQuickshell())
|
dependencies = append(dependencies, d.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, d.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, d.detectXDGPortal())
|
dependencies = append(dependencies, d.detectXDGPortal())
|
||||||
dependencies = append(dependencies, d.detectAccountsService())
|
dependencies = append(dependencies, d.detectAccountsService())
|
||||||
|
|
||||||
@@ -86,10 +87,41 @@ func (d *DebianDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
return d.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", d.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return d.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", d.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
func (d *DebianDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(pkg)
|
||||||
err := cmd.Run()
|
}
|
||||||
return err == nil
|
|
||||||
|
func debianPackageInstalledPrecisely(pkg string) bool {
|
||||||
|
cmd := exec.Command("dpkg-query", "-W", "-f=${db:Status-Status}", pkg)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output)) == "installed"
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(values []string, target string) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func debianRepoArchitecture(arch string) string {
|
||||||
|
switch arch {
|
||||||
|
case "amd64", "x86_64":
|
||||||
|
return "amd64"
|
||||||
|
case "arm64", "aarch64":
|
||||||
|
return "arm64"
|
||||||
|
default:
|
||||||
|
return arch
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (d *DebianDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -108,6 +140,7 @@ func (d *DebianDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
// DMS packages from OBS with variant support
|
// DMS packages from OBS with variant support
|
||||||
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": d.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": d.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
@@ -159,7 +192,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -176,7 +209,7 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential")
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -188,12 +221,12 @@ func (d *DebianDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: "Installing development dependencies...",
|
Step: "Installing development dependencies...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
CommandInfo: "sudo apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev",
|
||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
"DEBIAN_FRONTEND=noninteractive apt-get install -y curl wget git cmake ninja-build pkg-config gnupg libxcb-cursor-dev libglib2.0-dev libpolkit-agent-1-dev libjpeg-dev libpugixml-dev")
|
||||||
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := d.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
}
|
}
|
||||||
@@ -373,6 +406,14 @@ func (d *DebianDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
return names
|
return names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *DebianDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-install-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
@@ -410,7 +451,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
keyringPath := fmt.Sprintf("/etc/apt/keyrings/%s.gpg", repoName)
|
||||||
|
|
||||||
// Create keyrings directory if it doesn't exist
|
// Create keyrings directory if it doesn't exist
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword, "mkdir -p /etc/apt/keyrings")
|
||||||
if err := mkdirCmd.Run(); err != nil {
|
if err := mkdirCmd.Run(); err != nil {
|
||||||
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
d.log(fmt.Sprintf("Warning: failed to create keyrings directory: %v", err))
|
||||||
}
|
}
|
||||||
@@ -424,13 +465,13 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
}
|
}
|
||||||
|
|
||||||
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
keyCmd := fmt.Sprintf("bash -c 'rm -f %s && curl -fsSL %s/Release.key | gpg --batch --dearmor -o %s'", keyringPath, baseURL, keyringPath)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, keyCmd)
|
cmd := privesc.ExecCommand(ctx, sudoPassword, keyCmd)
|
||||||
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
if err := d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.18, 0.20); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS GPG key for %s: %w", pkg.RepoURL, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add repository
|
// Add repository
|
||||||
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, runtime.GOARCH, baseURL)
|
repoLine := fmt.Sprintf("deb [signed-by=%s arch=%s] %s/ /", keyringPath, debianRepoArchitecture(osInfo.Architecture), baseURL)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -440,7 +481,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
CommandInfo: fmt.Sprintf("echo '%s' | sudo tee %s", repoLine, listFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
addRepoCmd := ExecSudoCommand(ctx, sudoPassword,
|
addRepoCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
fmt.Sprintf("bash -c \"echo '%s' | tee %s\"", repoLine, listFile))
|
||||||
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := d.runWithProgress(addRepoCmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
return fmt.Errorf("failed to add OBS repo %s: %w", pkg.RepoURL, err)
|
||||||
@@ -460,7 +501,7 @@ func (d *DebianDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := d.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
return fmt.Errorf("failed to update package lists after adding OBS repos: %w", err)
|
||||||
}
|
}
|
||||||
@@ -476,20 +517,46 @@ func (d *DebianDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
d.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
startProgress := 0.40
|
||||||
|
endProgress := 0.60
|
||||||
|
if totalGroups > 1 {
|
||||||
|
if groupIndex == 1 {
|
||||||
|
endProgress = 0.50
|
||||||
|
} else {
|
||||||
|
startProgress = 0.50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := d.aptInstallArgs(groupPackages, minimal)
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
Progress: 0.40,
|
Progress: startProgress,
|
||||||
Step: "Installing system packages...",
|
Step: "Installing system packages...",
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, startProgress, endProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -568,7 +635,7 @@ func (d *DebianDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return d.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +653,7 @@ func (d *DebianDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y rustup")
|
||||||
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := d.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -625,7 +692,7 @@ func (d *DebianDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "DEBIAN_FRONTEND=noninteractive apt-get install -y golang-go")
|
||||||
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
return d.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.87, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("fedora", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
|
Register("evernight", "#72B8DC", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
|
return NewFedoraDistribution(config, logChan)
|
||||||
|
})
|
||||||
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
Register("nobara", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
|
||||||
return NewFedoraDistribution(config, logChan)
|
return NewFedoraDistribution(config, logChan)
|
||||||
})
|
})
|
||||||
@@ -75,6 +79,7 @@ func (f *FedoraDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, f.detectGit())
|
dependencies = append(dependencies, f.detectGit())
|
||||||
dependencies = append(dependencies, f.detectWindowManager(wm))
|
dependencies = append(dependencies, f.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, f.detectQuickshell())
|
dependencies = append(dependencies, f.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, f.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, f.detectXDGPortal())
|
dependencies = append(dependencies, f.detectXDGPortal())
|
||||||
dependencies = append(dependencies, f.detectAccountsService())
|
dependencies = append(dependencies, f.detectAccountsService())
|
||||||
|
|
||||||
@@ -120,6 +125,7 @@ func (f *FedoraDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
|
|
||||||
// COPR packages
|
// COPR packages
|
||||||
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": f.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": f.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeCOPR, RepoURL: "avengemedia/danklinux"},
|
||||||
@@ -191,6 +197,10 @@ func (f *FedoraDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return f.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", f.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) getPrerequisites() []string {
|
func (f *FedoraDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{
|
||||||
"dnf-plugins-core",
|
"dnf-plugins-core",
|
||||||
@@ -245,7 +255,7 @@ func (f *FedoraDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.logError("failed to install prerequisites", err)
|
f.logError("failed to install prerequisites", err)
|
||||||
@@ -428,7 +438,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo dnf copr enable -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
fmt.Sprintf("dnf copr enable -y %s 2>&1", pkg.RepoURL))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -452,7 +462,7 @@ func (f *FedoraDistribution) enableCOPRRepos(ctx context.Context, coprPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
CommandInfo: fmt.Sprintf("echo \"priority=1\" | sudo tee -a %s", repoFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
priorityCmd := ExecSudoCommand(ctx, sudoPassword,
|
priorityCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
fmt.Sprintf("bash -c 'echo \"priority=1\" | tee -a %s'", repoFile))
|
||||||
priorityOutput, err := priorityCmd.CombinedOutput()
|
priorityOutput, err := priorityCmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -475,28 +485,7 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"dnf", "install", "-y"}
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
|
|
||||||
for _, pkg := range packages {
|
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.40,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -506,26 +495,57 @@ func (f *FedoraDistribution) installCOPRPackages(ctx context.Context, packages [
|
|||||||
|
|
||||||
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
f.log(fmt.Sprintf("Installing COPR packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
|
return f.installDNFGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing COPR packages...", 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) dnfInstallArgs(packages []string, minimal bool) []string {
|
||||||
args := []string{"dnf", "install", "-y"}
|
args := []string{"dnf", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
for _, pkg := range packages {
|
|
||||||
if pkg == "niri" || pkg == "niri-git" {
|
|
||||||
args = append(args, "--setopt=install_weak_deps=False")
|
args = append(args, "--setopt=install_weak_deps=False")
|
||||||
break
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *FedoraDistribution) installDNFGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args = append(args, packages...)
|
args := f.dnfInstallArgs(groupPackages, minimal)
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: phase,
|
||||||
Progress: 0.70,
|
Progress: groupStart,
|
||||||
Step: "Installing COPR packages...",
|
Step: step,
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return f.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
return f.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GentooGlobalUseFlags = []string{
|
var GentooGlobalUseFlags = []string{
|
||||||
@@ -201,9 +202,9 @@ func (g *GentooDistribution) setGlobalUseFlags(ctx context.Context, sudoPassword
|
|||||||
|
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
if hasUse {
|
if hasUse {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("sed -i 's/^USE=\"\\(.*\\)\"/USE=\"\\1 %s\"/' /etc/portage/make.conf", useFlags))
|
||||||
} else {
|
} else {
|
||||||
cmd = ExecSudoCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
cmd = privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("bash -c \"echo 'USE=\\\"%s\\\"' >> /etc/portage/make.conf\"", useFlags))
|
||||||
}
|
}
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
@@ -281,7 +282,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Syncing Portage tree with emerge --sync",
|
LogOutput: "Syncing Portage tree with emerge --sync",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword, "emerge --sync --quiet")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
if syncErr != nil {
|
if syncErr != nil {
|
||||||
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
g.log(fmt.Sprintf("emerge --sync output: %s", string(syncOutput)))
|
||||||
@@ -302,7 +303,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
g.logError("failed to install prerequisites", err)
|
g.logError("failed to install prerequisites", err)
|
||||||
@@ -503,14 +504,14 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName, useFlags, sudoPassword string) error {
|
||||||
packageUseDir := "/etc/portage/package.use"
|
packageUseDir := "/etc/portage/package.use"
|
||||||
|
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
fmt.Sprintf("mkdir -p %s", packageUseDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -524,7 +525,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating USE flags for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, packageUseDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -532,7 +533,7 @@ func (g *GentooDistribution) setPackageUseFlags(ctx context.Context, packageName
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", useFlagLine, packageUseDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -557,7 +558,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enable GURU repository
|
// Enable GURU repository
|
||||||
enableCmd := ExecSudoCommand(ctx, sudoPassword,
|
enableCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
"eselect repository enable guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
output, err := enableCmd.CombinedOutput()
|
output, err := enableCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -589,7 +590,7 @@ func (g *GentooDistribution) syncGURURepo(ctx context.Context, sudoPassword stri
|
|||||||
LogOutput: "Syncing GURU repository",
|
LogOutput: "Syncing GURU repository",
|
||||||
}
|
}
|
||||||
|
|
||||||
syncCmd := ExecSudoCommand(ctx, sudoPassword,
|
syncCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
"emaint sync --repo guru 2>&1; exit_code=$?; exit $exit_code")
|
||||||
syncOutput, syncErr := syncCmd.CombinedOutput()
|
syncOutput, syncErr := syncCmd.CombinedOutput()
|
||||||
|
|
||||||
@@ -622,7 +623,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
|
|
||||||
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
acceptKeywordsDir := "/etc/portage/package.accept_keywords"
|
||||||
|
|
||||||
mkdirCmd := ExecSudoCommand(ctx, sudoPassword,
|
mkdirCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
fmt.Sprintf("mkdir -p %s", acceptKeywordsDir))
|
||||||
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
if output, err := mkdirCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
g.log(fmt.Sprintf("mkdir output: %s", string(output)))
|
||||||
@@ -636,7 +637,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
if checkExistingCmd.Run() == nil {
|
if checkExistingCmd.Run() == nil {
|
||||||
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
g.log(fmt.Sprintf("Updating accept keywords for %s from existing entry", packageName))
|
||||||
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
escapedPkg := strings.ReplaceAll(packageName, "/", "\\/")
|
||||||
replaceCmd := ExecSudoCommand(ctx, sudoPassword,
|
replaceCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
fmt.Sprintf("sed -i '/^%s /d' %s/danklinux; exit_code=$?; exit $exit_code", escapedPkg, acceptKeywordsDir))
|
||||||
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
if output, err := replaceCmd.CombinedOutput(); err != nil {
|
||||||
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
g.log(fmt.Sprintf("sed delete output: %s", string(output)))
|
||||||
@@ -644,7 +645,7 @@ func (g *GentooDistribution) setPackageAcceptKeywords(ctx context.Context, packa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
appendCmd := ExecSudoCommand(ctx, sudoPassword,
|
appendCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
fmt.Sprintf("bash -c \"echo '%s' >> %s/danklinux\"", keywordLine, acceptKeywordsDir))
|
||||||
|
|
||||||
output, err := appendCmd.CombinedOutput()
|
output, err := appendCmd.CombinedOutput()
|
||||||
@@ -695,6 +696,6 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
return g.runWithProgressTimeout(cmd, progressChan, PhaseAURPackages, 0.70, 0.85, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const (
|
|||||||
PhaseAURPackages
|
PhaseAURPackages
|
||||||
PhaseCursorTheme
|
PhaseCursorTheme
|
||||||
PhaseConfiguration
|
PhaseConfiguration
|
||||||
|
PhaseGreeterSetup
|
||||||
PhaseComplete
|
PhaseComplete
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ManualPackageInstaller provides methods for installing packages from source
|
// ManualPackageInstaller provides methods for installing packages from source
|
||||||
@@ -143,7 +144,7 @@ func (m *ManualPackageInstaller) installDgop(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
m.logError("failed to install dgop", err)
|
m.logError("failed to install dgop", err)
|
||||||
@@ -213,7 +214,7 @@ func (m *ManualPackageInstaller) installNiri(ctx context.Context, sudoPassword s
|
|||||||
CommandInfo: "dpkg -i niri.deb",
|
CommandInfo: "dpkg -i niri.deb",
|
||||||
}
|
}
|
||||||
|
|
||||||
installDebCmd := ExecSudoCommand(ctx, sudoPassword,
|
installDebCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
fmt.Sprintf("dpkg -i %s/target/debian/niri_*.deb", buildDir))
|
||||||
|
|
||||||
output, err := installDebCmd.CombinedOutput()
|
output, err := installDebCmd.CombinedOutput()
|
||||||
@@ -324,7 +325,7 @@ func (m *ManualPackageInstaller) installQuickshell(ctx context.Context, variant
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -387,7 +388,7 @@ func (m *ManualPackageInstaller) installHyprland(ctx context.Context, sudoPasswo
|
|||||||
CommandInfo: "sudo make install",
|
CommandInfo: "sudo make install",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "make install")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "make install")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Hyprland: %w", err)
|
return fmt.Errorf("failed to install Hyprland: %w", err)
|
||||||
@@ -453,7 +454,7 @@ func (m *ManualPackageInstaller) installGhostty(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
CommandInfo: "sudo cp zig-out/bin/ghostty /usr/local/bin/",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword,
|
installCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
fmt.Sprintf("cp %s/zig-out/bin/ghostty /usr/local/bin/", tmpDir))
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install Ghostty: %w", err)
|
return fmt.Errorf("failed to install Ghostty: %w", err)
|
||||||
@@ -492,16 +493,11 @@ func (m *ManualPackageInstaller) installMatugen(ctx context.Context, sudoPasswor
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy matugen to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make it executable
|
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make matugen executable: %w", err)
|
return fmt.Errorf("failed to make matugen executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,15 +642,11 @@ func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, s
|
|||||||
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
CommandInfo: fmt.Sprintf("sudo cp %s %s", sourcePath, targetPath),
|
||||||
}
|
}
|
||||||
|
|
||||||
copyCmd := exec.CommandContext(ctx, "sudo", "-S", "cp", sourcePath, targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "cp", sourcePath, targetPath); err != nil {
|
||||||
copyCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := copyCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
return fmt.Errorf("failed to copy xwayland-satellite to /usr/local/bin: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chmodCmd := exec.CommandContext(ctx, "sudo", "-S", "chmod", "+x", targetPath)
|
if err := privesc.Run(ctx, sudoPassword, "chmod", "+x", targetPath); err != nil {
|
||||||
chmodCmd.Stdin = strings.NewReader(sudoPassword + "\n")
|
|
||||||
if err := chmodCmd.Run(); err != nil {
|
|
||||||
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
return fmt.Errorf("failed to make xwayland-satellite executable: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
core/internal/distros/minimal_install.go
Normal file
44
core/internal/distros/minimal_install.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package distros
|
||||||
|
|
||||||
|
type minimalInstallGroup struct {
|
||||||
|
packages []string
|
||||||
|
minimal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldPreferMinimalInstall(pkg string) bool {
|
||||||
|
switch pkg {
|
||||||
|
case "niri", "niri-git":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitMinimalInstallPackages(packages []string) (normal []string, minimal []string) {
|
||||||
|
for _, pkg := range packages {
|
||||||
|
if shouldPreferMinimalInstall(pkg) {
|
||||||
|
minimal = append(minimal, pkg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
normal = append(normal, pkg)
|
||||||
|
}
|
||||||
|
return normal, minimal
|
||||||
|
}
|
||||||
|
|
||||||
|
func orderedMinimalInstallGroups(packages []string) []minimalInstallGroup {
|
||||||
|
normal, minimal := splitMinimalInstallPackages(packages)
|
||||||
|
groups := make([]minimalInstallGroup, 0, 2)
|
||||||
|
if len(minimal) > 0 {
|
||||||
|
groups = append(groups, minimalInstallGroup{
|
||||||
|
packages: minimal,
|
||||||
|
minimal: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(normal) > 0 {
|
||||||
|
groups = append(groups, minimalInstallGroup{
|
||||||
|
packages: normal,
|
||||||
|
minimal: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return groups
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -29,6 +30,8 @@ type OpenSUSEDistribution struct {
|
|||||||
config DistroConfig
|
config DistroConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openSUSENiriWaylandServerPackage = "libwayland-server0"
|
||||||
|
|
||||||
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
func NewOpenSUSEDistribution(config DistroConfig, logChan chan<- string) *OpenSUSEDistribution {
|
||||||
base := NewBaseDistribution(logChan)
|
base := NewBaseDistribution(logChan)
|
||||||
return &OpenSUSEDistribution{
|
return &OpenSUSEDistribution{
|
||||||
@@ -71,6 +74,7 @@ func (o *OpenSUSEDistribution) DetectDependenciesWithTerminal(ctx context.Contex
|
|||||||
dependencies = append(dependencies, o.detectGit())
|
dependencies = append(dependencies, o.detectGit())
|
||||||
dependencies = append(dependencies, o.detectWindowManager(wm))
|
dependencies = append(dependencies, o.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, o.detectQuickshell())
|
dependencies = append(dependencies, o.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, o.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, o.detectXDGPortal())
|
dependencies = append(dependencies, o.detectXDGPortal())
|
||||||
dependencies = append(dependencies, o.detectAccountsService())
|
dependencies = append(dependencies, o.detectAccountsService())
|
||||||
|
|
||||||
@@ -100,6 +104,10 @@ func (o *OpenSUSEDistribution) packageInstalled(pkg string) bool {
|
|||||||
return err == nil
|
return err == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return o.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", o.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (o *OpenSUSEDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
return o.GetPackageMappingWithVariants(wm, make(map[string]deps.PackageVariant))
|
||||||
}
|
}
|
||||||
@@ -116,6 +124,7 @@ func (o *OpenSUSEDistribution) GetPackageMappingWithVariants(wm deps.WindowManag
|
|||||||
// DMS packages from OBS
|
// DMS packages from OBS
|
||||||
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": o.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": o.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypeOBS, RepoURL: "home:AvengeMedia:danklinux"},
|
||||||
@@ -193,35 +202,7 @@ func (o *OpenSUSEDistribution) detectAccountsService() deps.Dependency {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
func (o *OpenSUSEDistribution) getPrerequisites() []string {
|
||||||
return []string{
|
return []string{}
|
||||||
"make",
|
|
||||||
"unzip",
|
|
||||||
"gcc",
|
|
||||||
"gcc-c++",
|
|
||||||
"cmake",
|
|
||||||
"ninja",
|
|
||||||
"pkgconf-pkg-config",
|
|
||||||
"git",
|
|
||||||
"qt6-base-devel",
|
|
||||||
"qt6-declarative-devel",
|
|
||||||
"qt6-declarative-private-devel",
|
|
||||||
"qt6-shadertools",
|
|
||||||
"qt6-shadertools-devel",
|
|
||||||
"qt6-wayland-devel",
|
|
||||||
"qt6-waylandclient-private-devel",
|
|
||||||
"spirv-tools-devel",
|
|
||||||
"cli11-devel",
|
|
||||||
"wayland-protocols-devel",
|
|
||||||
"libgbm-devel",
|
|
||||||
"libdrm-devel",
|
|
||||||
"pipewire-devel",
|
|
||||||
"jemalloc-devel",
|
|
||||||
"wayland-utils",
|
|
||||||
"Mesa-libGLESv3-devel",
|
|
||||||
"pam-devel",
|
|
||||||
"glib2-devel",
|
|
||||||
"polkit-devel",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -269,7 +250,7 @@ func (o *OpenSUSEDistribution) InstallPrerequisites(ctx context.Context, sudoPas
|
|||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
args := []string{"zypper", "install", "-y"}
|
||||||
args = append(args, missingPkgs...)
|
args = append(args, missingPkgs...)
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.logError("failed to install prerequisites", err)
|
o.logError("failed to install prerequisites", err)
|
||||||
@@ -291,6 +272,10 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
LogOutput: "Starting prerequisite check...",
|
LogOutput: "Starting prerequisite check...",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := o.disableInstallMediaRepos(ctx, sudoPassword, progressChan); err != nil {
|
||||||
|
return fmt.Errorf("failed to disable install media repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
if err := o.InstallPrerequisites(ctx, sudoPassword, progressChan); err != nil {
|
||||||
return fmt.Errorf("failed to install prerequisites: %w", err)
|
return fmt.Errorf("failed to install prerequisites: %w", err)
|
||||||
}
|
}
|
||||||
@@ -321,7 +306,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
LogOutput: fmt.Sprintf("Installing system packages: %s", strings.Join(systemPkgs, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan); err != nil {
|
if err := o.installZypperPackages(ctx, systemPkgs, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60); err != nil {
|
||||||
return fmt.Errorf("failed to install zypper packages: %w", err)
|
return fmt.Errorf("failed to install zypper packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -336,7 +321,7 @@ func (o *OpenSUSEDistribution) InstallPackages(ctx context.Context, dependencies
|
|||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
LogOutput: fmt.Sprintf("Installing OBS packages: %s", strings.Join(obsPkgNames, ", ")),
|
||||||
}
|
}
|
||||||
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan); err != nil {
|
if err := o.installZypperPackages(ctx, obsPkgNames, sudoPassword, progressChan, PhaseAURPackages, "Installing OBS packages...", 0.70, 0.85); err != nil {
|
||||||
return fmt.Errorf("failed to install OBS packages: %w", err)
|
return fmt.Errorf("failed to install OBS packages: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,9 +411,32 @@ func (o *OpenSUSEDistribution) categorizePackages(dependencies []deps.Dependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
systemPkgs = o.appendMissingSystemPackages(systemPkgs, openSUSENiriRuntimePackages(wm, disabledFlags))
|
||||||
|
|
||||||
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
return systemPkgs, obsPkgs, manualPkgs, variantMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func openSUSENiriRuntimePackages(wm deps.WindowManager, disabledFlags map[string]bool) []string {
|
||||||
|
if wm != deps.WindowManagerNiri || disabledFlags["niri"] {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{openSUSENiriWaylandServerPackage}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) appendMissingSystemPackages(systemPkgs []string, extraPkgs []string) []string {
|
||||||
|
for _, pkg := range extraPkgs {
|
||||||
|
if containsString(systemPkgs, pkg) || o.packageInstalled(pkg) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Adding openSUSE runtime package: %s", pkg))
|
||||||
|
systemPkgs = append(systemPkgs, pkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPkgs
|
||||||
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
func (o *OpenSUSEDistribution) extractPackageNames(packages []PackageMapping) []string {
|
||||||
names := make([]string, len(packages))
|
names := make([]string, len(packages))
|
||||||
for i, pkg := range packages {
|
for i, pkg := range packages {
|
||||||
@@ -478,7 +486,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
CommandInfo: fmt.Sprintf("sudo zypper addrepo %s", repoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
fmt.Sprintf("zypper addrepo -f %s", repoURL))
|
||||||
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
o.log(fmt.Sprintf("OBS repo %s add failed (may already exist): %v", pkg.RepoURL, err))
|
||||||
@@ -499,7 +507,7 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
CommandInfo: "sudo zypper --gpg-auto-import-keys refresh",
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshCmd := ExecSudoCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
refreshCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper --gpg-auto-import-keys refresh")
|
||||||
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := o.runWithProgress(refreshCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to refresh repositories: %w", err)
|
return fmt.Errorf("failed to refresh repositories: %w", err)
|
||||||
}
|
}
|
||||||
@@ -508,27 +516,146 @@ func (o *OpenSUSEDistribution) enableOBSRepos(ctx context.Context, obsPkgs []Pac
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func isOpenSUSEInstallMediaURI(uri string) bool {
|
||||||
|
normalizedURI := strings.ToLower(strings.TrimSpace(uri))
|
||||||
|
|
||||||
|
return strings.HasPrefix(normalizedURI, "cd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "dvd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "hd:/") ||
|
||||||
|
strings.HasPrefix(normalizedURI, "iso:/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseZypperInstallMediaAliases(output string) []string {
|
||||||
|
var aliases []string
|
||||||
|
|
||||||
|
for _, line := range strings.Split(output, "\n") {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line == "" || !strings.Contains(line, "|") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(line, "|")
|
||||||
|
if len(parts) < 7 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range parts {
|
||||||
|
parts[i] = strings.TrimSpace(parts[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
alias := parts[1]
|
||||||
|
enabled := strings.ToLower(parts[3])
|
||||||
|
uri := parts[len(parts)-1]
|
||||||
|
|
||||||
|
if alias == "" || strings.EqualFold(alias, "alias") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if enabled != "" && enabled != "yes" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isOpenSUSEInstallMediaURI(uri) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases = append(aliases, alias)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliases
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) disableInstallMediaRepos(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
|
listCmd := exec.CommandContext(ctx, "zypper", "repos", "-u")
|
||||||
|
output, err := listCmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("Warning: failed to list zypper repositories: %s", strings.TrimSpace(string(output))))
|
||||||
|
return fmt.Errorf("failed to list zypper repositories: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases := parseZypperInstallMediaAliases(string(output))
|
||||||
|
if len(aliases) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
o.log(fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")))
|
||||||
|
progressChan <- InstallProgressMsg{
|
||||||
|
Phase: PhasePrerequisites,
|
||||||
|
Progress: 0.055,
|
||||||
|
Step: "Disabling install media repositories...",
|
||||||
|
IsComplete: false,
|
||||||
|
NeedsSudo: true,
|
||||||
|
CommandInfo: fmt.Sprintf("sudo zypper modifyrepo -d %s", strings.Join(aliases, " ")),
|
||||||
|
LogOutput: fmt.Sprintf("Disabling install media repositories: %s", strings.Join(aliases, ", ")),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, alias := range aliases {
|
||||||
|
cmd := privesc.ExecCommand(ctx, sudoPassword, fmt.Sprintf("zypper modifyrepo -d '%s'", privesc.EscapeSingleQuotes(alias)))
|
||||||
|
repoOutput, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
o.log(fmt.Sprintf("Failed to disable install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
|
return fmt.Errorf("failed to disable install media repo %s: %w", alias, err)
|
||||||
|
}
|
||||||
|
o.log(fmt.Sprintf("Disabled install media repo %s: %s", alias, strings.TrimSpace(string(repoOutput))))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) zypperInstallArgs(packages []string, minimal bool) []string {
|
||||||
|
args := []string{"zypper", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OpenSUSEDistribution) installZypperPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
if len(packages) == 0 {
|
if len(packages) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
o.log(fmt.Sprintf("Installing zypper packages: %s", strings.Join(packages, ", ")))
|
||||||
|
|
||||||
args := []string{"zypper", "install", "-y"}
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
args = append(args, packages...)
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := o.zypperInstallArgs(groupPackages, minimal)
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: phase,
|
||||||
Progress: 0.40,
|
Progress: groupStart,
|
||||||
Step: "Installing system packages...",
|
Step: step,
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return o.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
return o.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant deps.PackageVariant, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -647,7 +774,7 @@ func (o *OpenSUSEDistribution) installQuickshell(ctx context.Context, variant de
|
|||||||
CommandInfo: "sudo cmake --install build",
|
CommandInfo: "sudo cmake --install build",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "cmake --install build")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "cmake --install build")
|
||||||
installCmd.Dir = tmpDir
|
installCmd.Dir = tmpDir
|
||||||
if err := installCmd.Run(); err != nil {
|
if err := installCmd.Run(); err != nil {
|
||||||
return fmt.Errorf("failed to install quickshell: %w", err)
|
return fmt.Errorf("failed to install quickshell: %w", err)
|
||||||
@@ -671,7 +798,7 @@ func (o *OpenSUSEDistribution) installRust(ctx context.Context, sudoPassword str
|
|||||||
CommandInfo: "sudo zypper install rustup",
|
CommandInfo: "sudo zypper install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "zypper install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "zypper install -y rustup")
|
||||||
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := o.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -63,6 +64,7 @@ func (u *UbuntuDistribution) DetectDependenciesWithTerminal(ctx context.Context,
|
|||||||
dependencies = append(dependencies, u.detectGit())
|
dependencies = append(dependencies, u.detectGit())
|
||||||
dependencies = append(dependencies, u.detectWindowManager(wm))
|
dependencies = append(dependencies, u.detectWindowManager(wm))
|
||||||
dependencies = append(dependencies, u.detectQuickshell())
|
dependencies = append(dependencies, u.detectQuickshell())
|
||||||
|
dependencies = append(dependencies, u.detectDMSGreeter())
|
||||||
dependencies = append(dependencies, u.detectXDGPortal())
|
dependencies = append(dependencies, u.detectXDGPortal())
|
||||||
dependencies = append(dependencies, u.detectAccountsService())
|
dependencies = append(dependencies, u.detectAccountsService())
|
||||||
|
|
||||||
@@ -94,10 +96,12 @@ func (u *UbuntuDistribution) detectAccountsService() deps.Dependency {
|
|||||||
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
return u.detectPackage("accountsservice", "D-Bus interface for user account query and manipulation", u.packageInstalled("accountsservice"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) detectDMSGreeter() deps.Dependency {
|
||||||
|
return u.detectOptionalPackage("dms-greeter", "DankMaterialShell greetd greeter", u.packageInstalled("dms-greeter"))
|
||||||
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
func (u *UbuntuDistribution) packageInstalled(pkg string) bool {
|
||||||
cmd := exec.Command("dpkg", "-l", pkg)
|
return debianPackageInstalledPrecisely(pkg)
|
||||||
err := cmd.Run()
|
|
||||||
return err == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
func (u *UbuntuDistribution) GetPackageMapping(wm deps.WindowManager) map[string]PackageMapping {
|
||||||
@@ -116,6 +120,7 @@ func (u *UbuntuDistribution) GetPackageMappingWithVariants(wm deps.WindowManager
|
|||||||
// DMS packages from PPAs
|
// DMS packages from PPAs
|
||||||
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
"dms (DankMaterialShell)": u.getDmsMapping(variants["dms (DankMaterialShell)"]),
|
||||||
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
"quickshell": u.getQuickshellMapping(variants["quickshell"]),
|
||||||
|
"dms-greeter": {Name: "dms-greeter", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"matugen": {Name: "matugen", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"dgop": {Name: "dgop", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
"ghostty": {Name: "ghostty", Repository: RepoTypePPA, RepoURL: "ppa:avengemedia/danklinux"},
|
||||||
@@ -173,7 +178,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Updating APT package lists",
|
LogOutput: "Updating APT package lists",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhasePrerequisites, 0.06, 0.07); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists: %w", err)
|
return fmt.Errorf("failed to update package lists: %w", err)
|
||||||
}
|
}
|
||||||
@@ -191,7 +196,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
checkCmd := exec.CommandContext(ctx, "dpkg", "-l", "build-essential")
|
||||||
if err := checkCmd.Run(); err != nil {
|
if err := checkCmd.Run(); err != nil {
|
||||||
// Not installed, install it
|
// Not installed, install it
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
cmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y build-essential")
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhasePrerequisites, 0.08, 0.09); err != nil {
|
||||||
return fmt.Errorf("failed to install build-essential: %w", err)
|
return fmt.Errorf("failed to install build-essential: %w", err)
|
||||||
}
|
}
|
||||||
@@ -207,7 +212,7 @@ func (u *UbuntuDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
LogOutput: "Installing additional development tools",
|
LogOutput: "Installing additional development tools",
|
||||||
}
|
}
|
||||||
|
|
||||||
devToolsCmd := ExecSudoCommand(ctx, sudoPassword,
|
devToolsCmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
"apt-get install -y curl wget git cmake ninja-build pkg-config libglib2.0-dev libpolkit-agent-1-dev")
|
||||||
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
if err := u.runWithProgress(devToolsCmd, progressChan, PhasePrerequisites, 0.10, 0.12); err != nil {
|
||||||
return fmt.Errorf("failed to install development tools: %w", err)
|
return fmt.Errorf("failed to install development tools: %w", err)
|
||||||
@@ -394,7 +399,7 @@ func (u *UbuntuDistribution) extractPackageNames(packages []PackageMapping) []st
|
|||||||
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []PackageMapping, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
enabledRepos := make(map[string]bool)
|
enabledRepos := make(map[string]bool)
|
||||||
|
|
||||||
installPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
installPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"apt-get install -y software-properties-common")
|
"apt-get install -y software-properties-common")
|
||||||
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
if err := u.runWithProgress(installPPACmd, progressChan, PhaseSystemPackages, 0.15, 0.17); err != nil {
|
||||||
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
return fmt.Errorf("failed to install software-properties-common: %w", err)
|
||||||
@@ -412,7 +417,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
CommandInfo: fmt.Sprintf("sudo add-apt-repository -y %s", pkg.RepoURL),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword,
|
cmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
fmt.Sprintf("add-apt-repository -y %s", pkg.RepoURL))
|
||||||
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
if err := u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.20, 0.22); err != nil {
|
||||||
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
u.logError(fmt.Sprintf("failed to enable PPA repo %s", pkg.RepoURL), err)
|
||||||
@@ -433,7 +438,7 @@ func (u *UbuntuDistribution) enablePPARepos(ctx context.Context, ppaPkgs []Packa
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.25, 0.27); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
return fmt.Errorf("failed to update package lists after adding PPAs: %w", err)
|
||||||
}
|
}
|
||||||
@@ -448,21 +453,7 @@ func (u *UbuntuDistribution) installAPTPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing APT packages: %s", strings.Join(packages, ", ")))
|
||||||
|
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseSystemPackages, "Installing system packages...", 0.40, 0.60)
|
||||||
args := []string{"apt-get", "install", "-y"}
|
|
||||||
args = append(args, packages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
|
||||||
Phase: PhaseSystemPackages,
|
|
||||||
Progress: 0.40,
|
|
||||||
Step: "Installing system packages...",
|
|
||||||
IsComplete: false,
|
|
||||||
NeedsSudo: true,
|
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.40, 0.60)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -471,21 +462,59 @@ func (u *UbuntuDistribution) installPPAPackages(ctx context.Context, packages []
|
|||||||
}
|
}
|
||||||
|
|
||||||
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
u.log(fmt.Sprintf("Installing PPA packages: %s", strings.Join(packages, ", ")))
|
||||||
|
return u.installAPTGroups(ctx, packages, sudoPassword, progressChan, PhaseAURPackages, "Installing PPA packages...", 0.70, 0.85)
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{"apt-get", "install", "-y"}
|
func (u *UbuntuDistribution) aptInstallArgs(packages []string, minimal bool) []string {
|
||||||
args = append(args, packages...)
|
args := []string{"DEBIAN_FRONTEND=noninteractive", "apt-get", "install", "-y"}
|
||||||
|
if minimal {
|
||||||
|
args = append(args, "--no-install-recommends")
|
||||||
|
}
|
||||||
|
return append(args, packages...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *UbuntuDistribution) installAPTGroups(ctx context.Context, packages []string, sudoPassword string, progressChan chan<- InstallProgressMsg, phase InstallPhase, step string, startProgress float64, endProgress float64) error {
|
||||||
|
groups := orderedMinimalInstallGroups(packages)
|
||||||
|
totalGroups := len(groups)
|
||||||
|
|
||||||
|
groupIndex := 0
|
||||||
|
installGroup := func(groupPackages []string, minimal bool) error {
|
||||||
|
if len(groupPackages) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
groupIndex++
|
||||||
|
groupStart := startProgress
|
||||||
|
groupEnd := endProgress
|
||||||
|
if totalGroups > 1 {
|
||||||
|
midpoint := startProgress + ((endProgress - startProgress) / 2)
|
||||||
|
if groupIndex == 1 {
|
||||||
|
groupEnd = midpoint
|
||||||
|
} else {
|
||||||
|
groupStart = midpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args := u.aptInstallArgs(groupPackages, minimal)
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: phase,
|
||||||
Progress: 0.70,
|
Progress: groupStart,
|
||||||
Step: "Installing PPA packages...",
|
Step: step,
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
NeedsSudo: true,
|
||||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseAURPackages, 0.70, 0.85)
|
return u.runWithProgress(cmd, progressChan, phase, groupStart, groupEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range groups {
|
||||||
|
if err := installGroup(group.packages, group.minimal); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manualPkgs []string, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||||
@@ -563,7 +592,7 @@ func (u *UbuntuDistribution) installBuildDependencies(ctx context.Context, manua
|
|||||||
args := []string{"apt-get", "install", "-y"}
|
args := []string{"apt-get", "install", "-y"}
|
||||||
args = append(args, depList...)
|
args = append(args, depList...)
|
||||||
|
|
||||||
cmd := ExecSudoCommand(ctx, sudoPassword, strings.Join(args, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
return u.runWithProgress(cmd, progressChan, PhaseSystemPackages, 0.80, 0.82)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -581,7 +610,7 @@ func (u *UbuntuDistribution) installRust(ctx context.Context, sudoPassword strin
|
|||||||
CommandInfo: "sudo apt-get install rustup",
|
CommandInfo: "sudo apt-get install rustup",
|
||||||
}
|
}
|
||||||
|
|
||||||
rustupInstallCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
rustupInstallCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y rustup")
|
||||||
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
if err := u.runWithProgress(rustupInstallCmd, progressChan, PhaseSystemPackages, 0.82, 0.83); err != nil {
|
||||||
return fmt.Errorf("failed to install rustup: %w", err)
|
return fmt.Errorf("failed to install rustup: %w", err)
|
||||||
}
|
}
|
||||||
@@ -621,7 +650,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
CommandInfo: "sudo add-apt-repository ppa:longsleep/golang-backports",
|
||||||
}
|
}
|
||||||
|
|
||||||
addPPACmd := ExecSudoCommand(ctx, sudoPassword,
|
addPPACmd := privesc.ExecCommand(ctx, sudoPassword,
|
||||||
"add-apt-repository -y ppa:longsleep/golang-backports")
|
"add-apt-repository -y ppa:longsleep/golang-backports")
|
||||||
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
if err := u.runWithProgress(addPPACmd, progressChan, PhaseSystemPackages, 0.87, 0.88); err != nil {
|
||||||
return fmt.Errorf("failed to add Go PPA: %w", err)
|
return fmt.Errorf("failed to add Go PPA: %w", err)
|
||||||
@@ -636,7 +665,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get update",
|
CommandInfo: "sudo apt-get update",
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get update")
|
updateCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get update")
|
||||||
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
if err := u.runWithProgress(updateCmd, progressChan, PhaseSystemPackages, 0.88, 0.89); err != nil {
|
||||||
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
return fmt.Errorf("failed to update package lists after adding Go PPA: %w", err)
|
||||||
}
|
}
|
||||||
@@ -650,7 +679,7 @@ func (u *UbuntuDistribution) installGo(ctx context.Context, sudoPassword string,
|
|||||||
CommandInfo: "sudo apt-get install golang-go",
|
CommandInfo: "sudo apt-get install golang-go",
|
||||||
}
|
}
|
||||||
|
|
||||||
installCmd := ExecSudoCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
installCmd := privesc.ExecCommand(ctx, sudoPassword, "apt-get install -y golang-go")
|
||||||
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
return u.runWithProgress(installCmd, progressChan, PhaseSystemPackages, 0.89, 0.90)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
91
core/internal/greeter/assets/apparmor/usr.bin.dms-greeter
Normal file
91
core/internal/greeter/assets/apparmor/usr.bin.dms-greeter
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# AppArmor profile for dms-greeter
|
||||||
|
#
|
||||||
|
# Managed by DMS — regenerated on every `dms greeter install` / `dms greeter sync`.
|
||||||
|
# Manual edits will be overwritten on next sync.
|
||||||
|
#
|
||||||
|
# Mode: complain (denials are logged, nothing is blocked)
|
||||||
|
# To switch to enforce after validating with `aa-logprof`:
|
||||||
|
# sudo aa-enforce /etc/apparmor.d/usr.bin.dms-greeter
|
||||||
|
#
|
||||||
|
#include <tunables/global>
|
||||||
|
|
||||||
|
profile dms-greeter /usr/bin/dms-greeter flags=(complain) {
|
||||||
|
#include <abstractions/base>
|
||||||
|
#include <abstractions/bash>
|
||||||
|
|
||||||
|
# The launcher script itself
|
||||||
|
/usr/bin/dms-greeter r,
|
||||||
|
|
||||||
|
# Cache directory — created by dms greeter sync/enable with greeter:greeter ownership
|
||||||
|
/var/cache/dms-greeter/ rw,
|
||||||
|
/var/cache/dms-greeter/** rwlk,
|
||||||
|
|
||||||
|
# DMS config — packaged path
|
||||||
|
/usr/share/quickshell/dms-greeter/ r,
|
||||||
|
/usr/share/quickshell/dms-greeter/** r,
|
||||||
|
/usr/share/quickshell/ r,
|
||||||
|
/usr/share/quickshell/** r,
|
||||||
|
|
||||||
|
# DMS config — system and user overrides
|
||||||
|
/etc/dms/ r,
|
||||||
|
/etc/dms/** r,
|
||||||
|
/usr/share/dms/ r,
|
||||||
|
/usr/share/dms/** r,
|
||||||
|
/home/*/.config/quickshell/ r,
|
||||||
|
/home/*/.config/quickshell/** r,
|
||||||
|
/root/.config/quickshell/ r,
|
||||||
|
/root/.config/quickshell/** r,
|
||||||
|
|
||||||
|
# greetd / PAM — read-only for session setup
|
||||||
|
/etc/greetd/ r,
|
||||||
|
/etc/greetd/** r,
|
||||||
|
/etc/pam.d/ r,
|
||||||
|
/etc/pam.d/** r,
|
||||||
|
/usr/lib/pam.d/ r,
|
||||||
|
/usr/lib/pam.d/** r,
|
||||||
|
|
||||||
|
# Compositor binaries — run unconfined so each compositor uses its own profile
|
||||||
|
/usr/bin/niri Ux,
|
||||||
|
/usr/bin/hyprland Ux,
|
||||||
|
/usr/bin/Hyprland Ux,
|
||||||
|
/usr/bin/sway Ux,
|
||||||
|
/usr/bin/labwc Ux,
|
||||||
|
/usr/bin/scroll Ux,
|
||||||
|
/usr/bin/miracle-wm Ux,
|
||||||
|
/usr/bin/mango Ux,
|
||||||
|
|
||||||
|
# Quickshell — run unconfined (has its own compositor profile on some distros)
|
||||||
|
/usr/bin/qs Ux,
|
||||||
|
/usr/bin/quickshell Ux,
|
||||||
|
|
||||||
|
# Wayland / XDG runtime (pipewire, wireplumber, wayland socket)
|
||||||
|
/run/user/[0-9]*/ rw,
|
||||||
|
/run/user/[0-9]*/** rw,
|
||||||
|
|
||||||
|
# DRM / GPU devices (required for Wayland compositor startup)
|
||||||
|
/dev/dri/ r,
|
||||||
|
/dev/dri/* rw,
|
||||||
|
/dev/udmabuf rw,
|
||||||
|
|
||||||
|
# Input devices
|
||||||
|
/dev/input/ r,
|
||||||
|
/dev/input/* r,
|
||||||
|
|
||||||
|
# Systemd journal / logging
|
||||||
|
/run/systemd/journal/socket rw,
|
||||||
|
/dev/log rw,
|
||||||
|
|
||||||
|
# Shell helper binaries invoked by the launcher script
|
||||||
|
/usr/bin/env ix,
|
||||||
|
/usr/bin/mkdir ix,
|
||||||
|
/usr/bin/cat ix,
|
||||||
|
/usr/bin/grep ix,
|
||||||
|
/usr/bin/dirname ix,
|
||||||
|
/usr/bin/basename ix,
|
||||||
|
/usr/bin/command ix,
|
||||||
|
/bin/env ix,
|
||||||
|
/bin/mkdir ix,
|
||||||
|
|
||||||
|
# Signal management (compositor lifecycle)
|
||||||
|
signal (send, receive) set=("term", "int", "hup", "kill"),
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
98
core/internal/greeter/installer_test.go
Normal file
98
core/internal/greeter/installer_test.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package greeter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeTestJSON(t *testing.T, path string, content string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create parent dir for %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveGreeterThemeSyncState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
settingsJSON string
|
||||||
|
sessionJSON string
|
||||||
|
wantSourcePath string
|
||||||
|
wantResolvedWallpaper string
|
||||||
|
wantDynamicOverrideUsed bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dynamic theme with greeter wallpaper override uses generated greeter colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": "Pictures/blue.jpg",
|
||||||
|
"matugenScheme": "scheme-tonal-spot",
|
||||||
|
"iconTheme": "Papirus"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":true}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "greeter-colors", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: filepath.Join("Pictures", "blue.jpg"),
|
||||||
|
wantDynamicOverrideUsed: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dynamic theme without override uses desktop colors",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "dynamic",
|
||||||
|
"greeterWallpaperPath": ""
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-dynamic theme keeps desktop colors even with override wallpaper",
|
||||||
|
settingsJSON: `{
|
||||||
|
"currentThemeName": "purple",
|
||||||
|
"greeterWallpaperPath": "/tmp/blue.jpg"
|
||||||
|
}`,
|
||||||
|
sessionJSON: `{"isLightMode":false}`,
|
||||||
|
wantSourcePath: filepath.Join(".cache", "DankMaterialShell", "dms-colors.json"),
|
||||||
|
wantResolvedWallpaper: "/tmp/blue.jpg",
|
||||||
|
wantDynamicOverrideUsed: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
homeDir := t.TempDir()
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json"), tt.settingsJSON)
|
||||||
|
writeTestJSON(t, filepath.Join(homeDir, ".local", "state", "DankMaterialShell", "session.json"), tt.sessionJSON)
|
||||||
|
|
||||||
|
state, err := resolveGreeterThemeSyncState(homeDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveGreeterThemeSyncState returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := state.effectiveColorsSource(homeDir); got != filepath.Join(homeDir, tt.wantSourcePath) {
|
||||||
|
t.Fatalf("effectiveColorsSource = %q, want %q", got, filepath.Join(homeDir, tt.wantSourcePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
wantResolvedWallpaper := tt.wantResolvedWallpaper
|
||||||
|
if wantResolvedWallpaper != "" && !filepath.IsAbs(wantResolvedWallpaper) {
|
||||||
|
wantResolvedWallpaper = filepath.Join(homeDir, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
if state.ResolvedGreeterWallpaperPath != wantResolvedWallpaper {
|
||||||
|
t.Fatalf("ResolvedGreeterWallpaperPath = %q, want %q", state.ResolvedGreeterWallpaperPath, wantResolvedWallpaper)
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.UsesDynamicWallpaperOverride != tt.wantDynamicOverrideUsed {
|
||||||
|
t.Fatalf("UsesDynamicWallpaperOverride = %v, want %v", state.UsesDynamicWallpaperOverride, tt.wantDynamicOverrideUsed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||||
"github.com/sblinch/kdl-go"
|
|
||||||
"github.com/sblinch/kdl-go/document"
|
"github.com/sblinch/kdl-go/document"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -292,7 +291,7 @@ func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) {
|
|||||||
parser := NewNiriParser(filepath.Dir(overridePath))
|
parser := NewNiriParser(filepath.Dir(overridePath))
|
||||||
parser.currentSource = overridePath
|
parser.currentSource = overridePath
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -341,6 +340,8 @@ func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string {
|
|||||||
val := arg.ValueString()
|
val := arg.ValueString()
|
||||||
if val == "" {
|
if val == "" {
|
||||||
parts = append(parts, `""`)
|
parts = append(parts, `""`)
|
||||||
|
} else if strings.ContainsAny(val, " \t") {
|
||||||
|
parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`)
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, val)
|
parts = append(parts, val)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,103 @@ type NiriParser struct {
|
|||||||
conflictingConfigs map[string]*NiriKeyBinding
|
conflictingConfigs map[string]*NiriKeyBinding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseKDL(data []byte) (*document.Document, error) {
|
||||||
|
return kdl.Parse(strings.NewReader(normalizeKDLBraces(string(data))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeKDLBraces(input string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.Grow(len(input))
|
||||||
|
|
||||||
|
var prev byte
|
||||||
|
n := len(input)
|
||||||
|
for i := 0; i < n; {
|
||||||
|
c := input[i]
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case c == '"':
|
||||||
|
end := findStringEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '"'
|
||||||
|
i = end
|
||||||
|
case c == '/' && i+1 < n && input[i+1] == '/':
|
||||||
|
end := findLineCommentEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '\n'
|
||||||
|
i = end
|
||||||
|
case c == '/' && i+1 < n && input[i+1] == '*':
|
||||||
|
end := findBlockCommentEnd(input, i)
|
||||||
|
sb.WriteString(input[i:end])
|
||||||
|
prev = '/'
|
||||||
|
i = end
|
||||||
|
case c == '{' && prev != 0 && !isBraceAdjacentSpace(prev):
|
||||||
|
sb.WriteByte(' ')
|
||||||
|
sb.WriteByte(c)
|
||||||
|
prev = c
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
sb.WriteByte(c)
|
||||||
|
prev = c
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func findStringEnd(s string, start int) int {
|
||||||
|
n := len(s)
|
||||||
|
for i := start + 1; i < n; {
|
||||||
|
switch s[i] {
|
||||||
|
case '\\':
|
||||||
|
i += 2
|
||||||
|
case '"':
|
||||||
|
return i + 1
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLineCommentEnd(s string, start int) int {
|
||||||
|
for i := start + 2; i < len(s); i++ {
|
||||||
|
if s[i] == '\n' {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBlockCommentEnd(s string, start int) int {
|
||||||
|
n := len(s)
|
||||||
|
depth := 1
|
||||||
|
for i := start + 2; i < n && depth > 0; {
|
||||||
|
switch {
|
||||||
|
case i+1 < n && s[i] == '/' && s[i+1] == '*':
|
||||||
|
depth++
|
||||||
|
i += 2
|
||||||
|
case i+1 < n && s[i] == '*' && s[i+1] == '/':
|
||||||
|
depth--
|
||||||
|
i += 2
|
||||||
|
if depth == 0 {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
func isBraceAdjacentSpace(b byte) bool {
|
||||||
|
switch b {
|
||||||
|
case ' ', '\t', '\n', '\r', '{':
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func NewNiriParser(configDir string) *NiriParser {
|
func NewNiriParser(configDir string) *NiriParser {
|
||||||
return &NiriParser{
|
return &NiriParser{
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
@@ -91,7 +188,7 @@ func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSec
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -159,7 +256,7 @@ func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, erro
|
|||||||
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to read %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
doc, err := kdl.Parse(strings.NewReader(string(data)))
|
doc, err := parseKDL(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,74 @@ package providers
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestNiriParse_NoSpaceBeforeBrace(t *testing.T) {
|
||||||
|
config := `recent-windows {
|
||||||
|
binds {
|
||||||
|
Alt+Tab { next-window scope="output"; }
|
||||||
|
Alt+Shift+Tab { previous-window scope="output"; }
|
||||||
|
Alt+grave { next-window filter="app-id"; }
|
||||||
|
Alt+Shift+grave { previous-window filter="app-id"; }
|
||||||
|
Alt+Escape { next-window scope="all"; }
|
||||||
|
Alt+Shift+Escape{ previous-window scope="all"; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(tmpDir, "config.kdl"), []byte(config), 0o644); err != nil {
|
||||||
|
t.Fatalf("Failed to write test config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := ParseNiriKeys(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ParseNiriKeys failed on valid niri config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var found *NiriKeyBinding
|
||||||
|
for i := range result.Section.Keybinds {
|
||||||
|
kb := &result.Section.Keybinds[i]
|
||||||
|
if kb.Key == "Escape" && slices.Contains(kb.Mods, "Alt") && slices.Contains(kb.Mods, "Shift") {
|
||||||
|
found = kb
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found == nil {
|
||||||
|
t.Fatal("Alt+Shift+Escape bind missing — '{' without preceding space was not handled")
|
||||||
|
}
|
||||||
|
if found.Action != "previous-window" {
|
||||||
|
t.Errorf("Action = %q, want %q", found.Action, "previous-window")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeKDLBraces(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
out string
|
||||||
|
}{
|
||||||
|
{"already spaced", "node { child }\n", "node { child }\n"},
|
||||||
|
{"missing space", "node{ child }\n", "node { child }\n"},
|
||||||
|
{"niri keybind", "Alt+Shift+Escape{ previous-window; }", "Alt+Shift+Escape { previous-window; }"},
|
||||||
|
{"brace inside string", `node "a{b" { child }`, `node "a{b" { child }`},
|
||||||
|
{"brace in line comment", "// foo{bar\nnode { }", "// foo{bar\nnode { }"},
|
||||||
|
{"brace in block comment", "/* foo{bar */ node{ }", "/* foo{bar */ node { }"},
|
||||||
|
{"escaped quote in string", `node "a\"b{c" { }`, `node "a\"b{c" { }`},
|
||||||
|
{"leading brace", "{ child }", "{ child }"},
|
||||||
|
{"nested missing space", "a{b{ c }}", "a {b { c }}"},
|
||||||
|
}
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
got := normalizeKDLBraces(tc.in)
|
||||||
|
if got != tc.out {
|
||||||
|
t.Errorf("normalizeKDLBraces(%q) = %q, want %q", tc.in, got, tc.out)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNiriParseKeyCombo(t *testing.T) {
|
func TestNiriParseKeyCombo(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
combo string
|
combo string
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package matugen
|
package matugen
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
@@ -19,6 +21,8 @@ import (
|
|||||||
"github.com/lucasb-eyer/go-colorful"
|
"github.com/lucasb-eyer/go-colorful"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var ErrNoChanges = errors.New("no color changes")
|
||||||
|
|
||||||
type ColorMode string
|
type ColorMode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -54,7 +58,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
{ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"},
|
||||||
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
||||||
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
||||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
||||||
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
||||||
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
||||||
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
||||||
@@ -67,6 +71,7 @@ var templateRegistry = []TemplateDef{
|
|||||||
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
{ID: "kcolorscheme", ConfigFile: "kcolorscheme.toml", RunUnconditionally: true},
|
||||||
{ID: "vscode", Kind: TemplateKindVSCode},
|
{ID: "vscode", Kind: TemplateKindVSCode},
|
||||||
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
{ID: "emacs", Commands: []string{"emacs"}, ConfigFile: "emacs.toml", Kind: TemplateKindEmacs},
|
||||||
|
{ID: "zed", Commands: []string{"zed", "zeditor", "zedit"}, ConfigFile: "zed.toml"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ColorMode) GTKTheme() string {
|
func (c *ColorMode) GTKTheme() string {
|
||||||
@@ -95,6 +100,7 @@ type Options struct {
|
|||||||
IconTheme string
|
IconTheme string
|
||||||
MatugenType string
|
MatugenType string
|
||||||
RunUserTemplates bool
|
RunUserTemplates bool
|
||||||
|
ColorsOnly bool
|
||||||
StockColors string
|
StockColors string
|
||||||
SyncModeWithPortal bool
|
SyncModeWithPortal bool
|
||||||
TerminalsAlwaysDark bool
|
TerminalsAlwaysDark bool
|
||||||
@@ -160,8 +166,14 @@ func Run(opts Options) error {
|
|||||||
|
|
||||||
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||||
|
|
||||||
if err := buildOnce(&opts); err != nil {
|
changed, buildErr := buildOnce(&opts)
|
||||||
return err
|
if buildErr != nil {
|
||||||
|
return buildErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if !changed {
|
||||||
|
log.Info("No color changes detected, skipping refresh")
|
||||||
|
return ErrNoChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.SyncModeWithPortal {
|
if opts.SyncModeWithPortal {
|
||||||
@@ -172,25 +184,27 @@ func Run(opts Options) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildOnce(opts *Options) error {
|
func buildOnce(opts *Options) (bool, error) {
|
||||||
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp config: %w", err)
|
return false, fmt.Errorf("failed to create temp config: %w", err)
|
||||||
}
|
}
|
||||||
defer os.Remove(cfgFile.Name())
|
defer os.Remove(cfgFile.Name())
|
||||||
defer cfgFile.Close()
|
defer cfgFile.Close()
|
||||||
|
|
||||||
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
return false, fmt.Errorf("failed to create temp dir: %w", err)
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
|
|
||||||
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||||
return fmt.Errorf("failed to build config: %w", err)
|
return false, fmt.Errorf("failed to build config: %w", err)
|
||||||
}
|
}
|
||||||
cfgFile.Close()
|
cfgFile.Close()
|
||||||
|
|
||||||
|
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||||
|
|
||||||
var primaryDark, primaryLight, surface string
|
var primaryDark, primaryLight, surface string
|
||||||
var dank16JSON string
|
var dank16JSON string
|
||||||
var importArgs []string
|
var importArgs []string
|
||||||
@@ -202,7 +216,7 @@ func buildOnce(opts *Options) error {
|
|||||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||||
|
|
||||||
if primaryDark == "" {
|
if primaryDark == "" {
|
||||||
return fmt.Errorf("failed to extract primary dark from stock colors")
|
return false, fmt.Errorf("failed to extract primary dark from stock colors")
|
||||||
}
|
}
|
||||||
if primaryLight == "" {
|
if primaryLight == "" {
|
||||||
primaryLight = primaryDark
|
primaryLight = primaryDark
|
||||||
@@ -216,14 +230,14 @@ func buildOnce(opts *Options) error {
|
|||||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||||
|
|
||||||
matJSON, err := runMatugenDryRun(opts)
|
matJSON, err := runMatugenDryRun(opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("matugen dry-run failed: %w", err)
|
return false, fmt.Errorf("matugen dry-run failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||||
@@ -231,7 +245,7 @@ func buildOnce(opts *Options) error {
|
|||||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||||
|
|
||||||
if primaryDark == "" {
|
if primaryDark == "" {
|
||||||
return fmt.Errorf("failed to extract primary color")
|
return false, fmt.Errorf("failed to extract primary color")
|
||||||
}
|
}
|
||||||
if primaryLight == "" {
|
if primaryLight == "" {
|
||||||
primaryLight = primaryDark
|
primaryLight = primaryDark
|
||||||
@@ -252,10 +266,19 @@ func buildOnce(opts *Options) error {
|
|||||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||||
args = append(args, importArgs...)
|
args = append(args, importArgs...)
|
||||||
if err := runMatugen(args); err != nil {
|
if err := runMatugen(args); err != nil {
|
||||||
return err
|
return false, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||||
|
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
if isDMSGTKActive(opts.ConfigDir) {
|
if isDMSGTKActive(opts.ConfigDir) {
|
||||||
switch opts.Mode {
|
switch opts.Mode {
|
||||||
case ColorModeLight:
|
case ColorModeLight:
|
||||||
@@ -273,7 +296,7 @@ func buildOnce(opts *Options) error {
|
|||||||
|
|
||||||
signalTerminals(opts)
|
signalTerminals(opts)
|
||||||
|
|
||||||
return nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||||
@@ -313,6 +336,10 @@ output_path = '%s'
|
|||||||
|
|
||||||
`, opts.ShellDir, opts.ColorsOutput())
|
`, opts.ShellDir, opts.ColorsOutput())
|
||||||
|
|
||||||
|
if opts.ColorsOnly {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
for _, tmpl := range templateRegistry {
|
for _, tmpl := range templateRegistry {
|
||||||
if opts.ShouldSkipTemplate(tmpl.ID) {
|
if opts.ShouldSkipTemplate(tmpl.ID) {
|
||||||
@@ -579,10 +606,10 @@ func detectMatugenVersionLocked() (matugenFlags, error) {
|
|||||||
matugenVersionOK = true
|
matugenVersionOK = true
|
||||||
|
|
||||||
if matugenSupportsCOE {
|
if matugenSupportsCOE {
|
||||||
log.Infof("Matugen %s supports --continue-on-error", versionStr)
|
log.Debugf("Matugen %s detected: continue-on-error support enabled", versionStr)
|
||||||
}
|
}
|
||||||
if matugenIsV4 {
|
if matugenIsV4 {
|
||||||
log.Infof("Matugen %s: using v4 flags", versionStr)
|
log.Debugf("Matugen %s detected: using v4 compatibility flags", versionStr)
|
||||||
}
|
}
|
||||||
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
return matugenFlags{matugenSupportsCOE, matugenIsV4}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package matugen
|
|||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
mocks_utils "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/utils"
|
||||||
@@ -392,3 +393,51 @@ func TestSubstituteVars(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildMergedConfigColorsOnly(t *testing.T) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
|
||||||
|
shellDir := filepath.Join(tempDir, "shell")
|
||||||
|
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||||
|
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create configs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
baseConfig := "[config]\ncustom_keywords = []\n"
|
||||||
|
if err := os.WriteFile(filepath.Join(configsDir, "base.toml"), []byte(baseConfig), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to write base config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfgFile, err := os.CreateTemp(tempDir, "merged-*.toml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create temp config: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
defer cfgFile.Close()
|
||||||
|
|
||||||
|
opts := &Options{
|
||||||
|
ShellDir: shellDir,
|
||||||
|
ConfigDir: filepath.Join(tempDir, "config"),
|
||||||
|
StateDir: filepath.Join(tempDir, "state"),
|
||||||
|
ColorsOnly: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil {
|
||||||
|
t.Fatalf("buildMergedConfig failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfgFile.Close(); err != nil {
|
||||||
|
t.Fatalf("failed to close merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := os.ReadFile(cfgFile.Name())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read merged config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := string(output)
|
||||||
|
assert.Contains(t, content, "[templates.dank]")
|
||||||
|
assert.Contains(t, content, "output_path = '"+filepath.Join(opts.StateDir, "dms-colors.json")+"'")
|
||||||
|
assert.NotContains(t, content, "[templates.gtk]")
|
||||||
|
assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs")
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package matugen
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -93,10 +94,13 @@ func (q *Queue) runWorker() {
|
|||||||
err := Run(job.Options)
|
err := Run(job.Options)
|
||||||
|
|
||||||
var result Result
|
var result Result
|
||||||
if err != nil {
|
switch {
|
||||||
result = Result{Success: false, Error: err}
|
case err == nil:
|
||||||
} else {
|
|
||||||
result = Result{Success: true}
|
result = Result{Success: true}
|
||||||
|
case errors.Is(err, ErrNoChanges):
|
||||||
|
result = Result{Success: true}
|
||||||
|
default:
|
||||||
|
result = Result{Success: false, Error: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
q.finishJob(result)
|
q.finishJob(result)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -59,7 +60,11 @@ func Send(n Notification) error {
|
|||||||
|
|
||||||
hints := map[string]dbus.Variant{}
|
hints := map[string]dbus.Variant{}
|
||||||
if n.FilePath != "" {
|
if n.FilePath != "" {
|
||||||
hints["image_path"] = dbus.MakeVariant(n.FilePath)
|
imgPath := n.FilePath
|
||||||
|
if !strings.HasPrefix(imgPath, "file://") {
|
||||||
|
imgPath = "file://" + imgPath
|
||||||
|
}
|
||||||
|
hints["image_path"] = dbus.MakeVariant(imgPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
obj := conn.Object(notifyDest, notifyPath)
|
obj := conn.Object(notifyDest, notifyPath)
|
||||||
|
|||||||
385
core/internal/privesc/privesc.go
Normal file
385
core/internal/privesc/privesc.go
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
package privesc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tool identifies a privilege-escalation binary.
|
||||||
|
type Tool string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ToolSudo Tool = "sudo"
|
||||||
|
ToolDoas Tool = "doas"
|
||||||
|
ToolRun0 Tool = "run0"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnvVar selects a specific tool when set to one of: sudo, doas, run0.
|
||||||
|
const EnvVar = "DMS_PRIVESC"
|
||||||
|
|
||||||
|
var detectionOrder = []Tool{ToolSudo, ToolDoas, ToolRun0}
|
||||||
|
|
||||||
|
var (
|
||||||
|
detectOnce sync.Once
|
||||||
|
detected Tool
|
||||||
|
detectErr error
|
||||||
|
userSelected bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// Detect returns the tool that should be used for privilege escalation.
|
||||||
|
// The result is cached after the first call.
|
||||||
|
func Detect() (Tool, error) {
|
||||||
|
detectOnce.Do(func() {
|
||||||
|
detected, detectErr = detectTool()
|
||||||
|
})
|
||||||
|
return detected, detectErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetForTesting clears cached detection state.
|
||||||
|
func ResetForTesting() {
|
||||||
|
detectOnce = sync.Once{}
|
||||||
|
detected = ""
|
||||||
|
detectErr = nil
|
||||||
|
userSelected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// AvailableTools returns the set of supported tools that are installed on
|
||||||
|
// PATH, in detection-precedence order.
|
||||||
|
func AvailableTools() []Tool {
|
||||||
|
var out []Tool
|
||||||
|
for _, t := range detectionOrder {
|
||||||
|
if t.Available() {
|
||||||
|
out = append(out, t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvOverride returns the tool selected by the $DMS_PRIVESC env var (if any)
|
||||||
|
// along with ok=true when the variable is set. An empty or unset variable
|
||||||
|
// returns ok=false.
|
||||||
|
func EnvOverride() (Tool, bool) {
|
||||||
|
v := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar)))
|
||||||
|
if v == "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return Tool(v), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTool forces the detected tool to t, bypassing autodetection. Intended
|
||||||
|
// for use after the caller has prompted the user for a selection.
|
||||||
|
func SetTool(t Tool) error {
|
||||||
|
if !t.Available() {
|
||||||
|
return fmt.Errorf("%q is not installed", t.Name())
|
||||||
|
}
|
||||||
|
detectOnce = sync.Once{}
|
||||||
|
detectOnce.Do(func() {
|
||||||
|
detected = t
|
||||||
|
detectErr = nil
|
||||||
|
})
|
||||||
|
userSelected = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectTool() (Tool, error) {
|
||||||
|
switch override := strings.ToLower(strings.TrimSpace(os.Getenv(EnvVar))); override {
|
||||||
|
case "":
|
||||||
|
// fall through to autodetect
|
||||||
|
case string(ToolSudo), string(ToolDoas), string(ToolRun0):
|
||||||
|
t := Tool(override)
|
||||||
|
if !t.Available() {
|
||||||
|
return "", fmt.Errorf("%s=%s but %q is not installed", EnvVar, override, t.Name())
|
||||||
|
}
|
||||||
|
return t, nil
|
||||||
|
default:
|
||||||
|
return "", fmt.Errorf("invalid %s=%q: must be one of sudo, doas, run0", EnvVar, override)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range detectionOrder {
|
||||||
|
if t.Available() {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("no supported privilege escalation tool found (tried: sudo, doas, run0)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the binary name.
|
||||||
|
func (t Tool) Name() string { return string(t) }
|
||||||
|
|
||||||
|
// Available reports whether this tool's binary is on PATH.
|
||||||
|
func (t Tool) Available() bool {
|
||||||
|
if t == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := exec.LookPath(string(t))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportsStdinPassword reports whether the tool can accept a password via
|
||||||
|
// stdin. Only sudo (-S) supports this.
|
||||||
|
func (t Tool) SupportsStdinPassword() bool {
|
||||||
|
return t == ToolSudo
|
||||||
|
}
|
||||||
|
|
||||||
|
// EscapeSingleQuotes escapes single quotes for safe inclusion inside a
|
||||||
|
// bash single-quoted string.
|
||||||
|
func EscapeSingleQuotes(s string) string {
|
||||||
|
return strings.ReplaceAll(s, "'", "'\\''")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeCommand returns a bash command string that runs `command` with the
|
||||||
|
// detected tool. When the tool supports stdin passwords and password is
|
||||||
|
// non-empty, the password is piped in. Otherwise the tool is invoked with
|
||||||
|
// no non-interactive flag so that an interactive TTY prompt is still
|
||||||
|
// possible for CLI callers.
|
||||||
|
//
|
||||||
|
// If detection fails, the returned shell string exits 1 with an error
|
||||||
|
// message so callers that treat the *exec.Cmd as infallible still fail
|
||||||
|
// deterministically.
|
||||||
|
func MakeCommand(password, command string) string {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return failingShell(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
if password != "" {
|
||||||
|
return fmt.Sprintf("echo '%s' | sudo -S %s", EscapeSingleQuotes(password), command)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("sudo %s", command)
|
||||||
|
case ToolDoas:
|
||||||
|
return fmt.Sprintf("doas sh -c '%s'", EscapeSingleQuotes(command))
|
||||||
|
case ToolRun0:
|
||||||
|
return fmt.Sprintf("run0 sh -c '%s'", EscapeSingleQuotes(command))
|
||||||
|
default:
|
||||||
|
return failingShell(fmt.Errorf("unsupported privilege tool: %q", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecCommand builds an exec.Cmd that runs `command` as root via the
|
||||||
|
// detected tool. Detection errors surface at Run() time as a failing
|
||||||
|
// command writing a clear error to stderr.
|
||||||
|
func ExecCommand(ctx context.Context, password, command string) *exec.Cmd {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", MakeCommand(password, command))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExecArgv builds an exec.Cmd that runs argv as root via the detected tool.
|
||||||
|
// No stdin password is supplied; callers relying on non-interactive success
|
||||||
|
// should ensure cached credentials are present (see CheckCached).
|
||||||
|
func ExecArgv(ctx context.Context, argv ...string) *exec.Cmd {
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("privesc.ExecArgv: argv must not be empty")))
|
||||||
|
}
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case ToolSudo, ToolDoas:
|
||||||
|
return exec.CommandContext(ctx, string(t), argv...)
|
||||||
|
case ToolRun0:
|
||||||
|
return exec.CommandContext(ctx, "run0", argv...)
|
||||||
|
default:
|
||||||
|
return exec.CommandContext(ctx, "bash", "-c", failingShell(fmt.Errorf("unsupported privilege tool: %q", t)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func failingShell(err error) string {
|
||||||
|
return fmt.Sprintf("printf 'privesc: %%s\\n' '%s' >&2; exit 1", EscapeSingleQuotes(err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckCached runs a non-interactive credential probe. Returns nil if the
|
||||||
|
// tool will run commands without prompting (cached credentials, nopass, or
|
||||||
|
// polkit rule).
|
||||||
|
func CheckCached(ctx context.Context) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
cmd = exec.CommandContext(ctx, "sudo", "-n", "true")
|
||||||
|
case ToolDoas:
|
||||||
|
cmd = exec.CommandContext(ctx, "doas", "-n", "true")
|
||||||
|
case ToolRun0:
|
||||||
|
cmd = exec.CommandContext(ctx, "run0", "--no-ask-password", "true")
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported privilege tool: %q", t)
|
||||||
|
}
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCache invalidates any cached credentials. No-op for tools that do
|
||||||
|
// not expose a cache-clear operation.
|
||||||
|
func ClearCache(ctx context.Context) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch t {
|
||||||
|
case ToolSudo:
|
||||||
|
return exec.CommandContext(ctx, "sudo", "-k").Run()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateWithAskpass validates cached credentials using an askpass helper
|
||||||
|
// script. Only sudo supports this mechanism; the TUI uses it to trigger
|
||||||
|
// fingerprint authentication via PAM.
|
||||||
|
func ValidateWithAskpass(ctx context.Context, askpassScript string) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != ToolSudo {
|
||||||
|
return fmt.Errorf("askpass validation requires sudo (detected: %s)", t)
|
||||||
|
}
|
||||||
|
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
||||||
|
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidatePassword validates the given password. Only sudo supports this
|
||||||
|
// (via `sudo -S -v`); for other tools the caller should fall back to
|
||||||
|
// CheckCached.
|
||||||
|
func ValidatePassword(ctx context.Context, password string) error {
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if t != ToolSudo {
|
||||||
|
return fmt.Errorf("password validation requires sudo (detected: %s)", t)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
||||||
|
stdin, err := cmd.StdinPipe()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(stdin, "%s\n", password); err != nil {
|
||||||
|
stdin.Close()
|
||||||
|
_ = cmd.Wait()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stdin.Close()
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuoteArgsForShell wraps each argv element in single quotes so the result
|
||||||
|
// can be safely passed to bash -c.
|
||||||
|
func QuoteArgsForShell(argv []string) string {
|
||||||
|
parts := make([]string, len(argv))
|
||||||
|
for i, a := range argv {
|
||||||
|
parts[i] = "'" + EscapeSingleQuotes(a) + "'"
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run invokes argv with privilege escalation. When the tool supports stdin
|
||||||
|
// passwords and password is non-empty, the password is piped in. Otherwise
|
||||||
|
// argv is invoked directly, which may prompt on a TTY.
|
||||||
|
// Stdout and Stderr are inherited from the current process.
|
||||||
|
func Run(ctx context.Context, password string, argv ...string) error {
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return fmt.Errorf("privesc.Run: argv must not be empty")
|
||||||
|
}
|
||||||
|
t, err := Detect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
switch {
|
||||||
|
case t == ToolSudo && password != "":
|
||||||
|
cmd = ExecCommand(ctx, password, QuoteArgsForShell(argv))
|
||||||
|
default:
|
||||||
|
cmd = ExecArgv(ctx, argv...)
|
||||||
|
}
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stdinIsTTY reports whether stdin is a character device (interactive
|
||||||
|
// terminal) rather than a pipe or file.
|
||||||
|
func stdinIsTTY() bool {
|
||||||
|
fi, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// PromptCLI interactively prompts the user to pick a privilege tool when more
|
||||||
|
// than one is installed and $DMS_PRIVESC is not set. If stdin is not a TTY,
|
||||||
|
// or only one tool is available, or the env var is set, the detected tool is
|
||||||
|
// returned without any prompt.
|
||||||
|
//
|
||||||
|
// The prompt is written to out (typically os.Stdout/os.Stderr) and input is
|
||||||
|
// read from in. EOF or empty input selects the first option.
|
||||||
|
func PromptCLI(out io.Writer, in io.Reader) (Tool, error) {
|
||||||
|
if userSelected {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
if _, envSet := EnvOverride(); envSet {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
|
||||||
|
tools := AvailableTools()
|
||||||
|
switch len(tools) {
|
||||||
|
case 0:
|
||||||
|
return "", fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||||
|
case 1:
|
||||||
|
if err := SetTool(tools[0]); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return tools[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stdinIsTTY() {
|
||||||
|
return Detect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(out, "Multiple privilege escalation tools detected:")
|
||||||
|
for i, t := range tools {
|
||||||
|
fmt.Fprintf(out, " [%d] %s\n", i+1, t.Name())
|
||||||
|
}
|
||||||
|
fmt.Fprintf(out, "Choose one [1-%d] (default 1, or set %s=<tool> to skip): ", len(tools), EnvVar)
|
||||||
|
|
||||||
|
reader := bufio.NewReader(in)
|
||||||
|
line, err := reader.ReadString('\n')
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
return "", fmt.Errorf("failed to read selection: %w", err)
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
idx := 1
|
||||||
|
if line != "" {
|
||||||
|
n, convErr := strconv.Atoi(line)
|
||||||
|
if convErr != nil || n < 1 || n > len(tools) {
|
||||||
|
return "", fmt.Errorf("invalid selection %q", line)
|
||||||
|
}
|
||||||
|
idx = n
|
||||||
|
}
|
||||||
|
|
||||||
|
chosen := tools[idx-1]
|
||||||
|
if err := SetTool(chosen); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return chosen, nil
|
||||||
|
}
|
||||||
@@ -258,7 +258,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|||||||
l := 0
|
l := 0
|
||||||
objectID := client.Uint32(data[l : l+4])
|
objectID := client.Uint32(data[l : l+4])
|
||||||
proxy := i.Context().GetProxy(objectID)
|
proxy := i.Context().GetProxy(objectID)
|
||||||
if proxy != nil {
|
if proxy != nil && !proxy.IsZombie() {
|
||||||
e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1)
|
e.WorkspaceGroup = proxy.(*ExtWorkspaceGroupHandleV1)
|
||||||
} else {
|
} else {
|
||||||
groupHandle := &ExtWorkspaceGroupHandleV1{}
|
groupHandle := &ExtWorkspaceGroupHandleV1{}
|
||||||
@@ -278,7 +278,7 @@ func (i *ExtWorkspaceManagerV1) Dispatch(opcode uint32, fd int, data []byte) {
|
|||||||
l := 0
|
l := 0
|
||||||
objectID := client.Uint32(data[l : l+4])
|
objectID := client.Uint32(data[l : l+4])
|
||||||
proxy := i.Context().GetProxy(objectID)
|
proxy := i.Context().GetProxy(objectID)
|
||||||
if proxy != nil {
|
if proxy != nil && !proxy.IsZombie() {
|
||||||
e.Workspace = proxy.(*ExtWorkspaceHandleV1)
|
e.Workspace = proxy.(*ExtWorkspaceHandleV1)
|
||||||
} else {
|
} else {
|
||||||
wsHandle := &ExtWorkspaceHandleV1{}
|
wsHandle := &ExtWorkspaceHandleV1{}
|
||||||
|
|||||||
@@ -444,20 +444,21 @@ func GetFocusedMonitor() string {
|
|||||||
|
|
||||||
type outputInfo struct {
|
type outputInfo struct {
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
func getAllOutputInfos() map[string]*outputInfo {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
ctx := display.Context()
|
ctx := display.Context()
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
registry, err := display.GetRegistry()
|
registry, err := display.GetRegistry()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
var outputManager *wlr_output_management.ZwlrOutputManagerV1
|
||||||
@@ -476,16 +477,17 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
if err := wlhelpers.Roundtrip(display, ctx); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if outputManager == nil {
|
if outputManager == nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type headState struct {
|
type headState struct {
|
||||||
name string
|
name string
|
||||||
x, y int32
|
x, y int32
|
||||||
|
scale float64
|
||||||
transform int32
|
transform int32
|
||||||
}
|
}
|
||||||
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
heads := make(map[*wlr_output_management.ZwlrOutputHeadV1]*headState)
|
||||||
@@ -501,6 +503,9 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
state.x = pe.X
|
state.x = pe.X
|
||||||
state.y = pe.Y
|
state.y = pe.Y
|
||||||
})
|
})
|
||||||
|
e.Head.SetScaleHandler(func(se wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||||
|
state.scale = se.Scale
|
||||||
|
})
|
||||||
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
e.Head.SetTransformHandler(func(te wlr_output_management.ZwlrOutputHeadV1TransformEvent) {
|
||||||
state.transform = te.Transform
|
state.transform = te.Transform
|
||||||
})
|
})
|
||||||
@@ -511,22 +516,33 @@ func getOutputInfo(outputName string) (*outputInfo, bool) {
|
|||||||
|
|
||||||
for !done {
|
for !done {
|
||||||
if err := ctx.Dispatch(); err != nil {
|
if err := ctx.Dispatch(); err != nil {
|
||||||
return nil, false
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*outputInfo, len(heads))
|
||||||
for _, state := range heads {
|
for _, state := range heads {
|
||||||
if state.name == outputName {
|
if state.name == "" {
|
||||||
return &outputInfo{
|
continue
|
||||||
|
}
|
||||||
|
result[state.name] = &outputInfo{
|
||||||
x: state.x,
|
x: state.x,
|
||||||
y: state.y,
|
y: state.y,
|
||||||
|
scale: state.scale,
|
||||||
transform: state.transform,
|
transform: state.transform,
|
||||||
}, true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOutputInfo(outputName string) (*outputInfo, bool) {
|
||||||
|
infos := getAllOutputInfos()
|
||||||
|
if infos == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
info, ok := infos[outputName]
|
||||||
|
return info, ok
|
||||||
|
}
|
||||||
|
|
||||||
func getDWLActiveWindow() (*WindowGeometry, error) {
|
func getDWLActiveWindow() (*WindowGeometry, error) {
|
||||||
display, err := client.Connect("")
|
display, err := client.Connect("")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package screenshot
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -298,22 +299,20 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
if len(outputs) == 0 {
|
if len(outputs) == 0 {
|
||||||
return nil, fmt.Errorf("no outputs available")
|
return nil, fmt.Errorf("no outputs available")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(outputs) == 1 {
|
if len(outputs) == 1 {
|
||||||
return s.captureWholeOutput(outputs[0])
|
return s.captureWholeOutput(outputs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture all outputs first to get actual buffer sizes
|
wlrInfos := getAllOutputInfos()
|
||||||
type capturedOutput struct {
|
|
||||||
output *WaylandOutput
|
|
||||||
result *CaptureResult
|
|
||||||
physX int
|
|
||||||
physY int
|
|
||||||
}
|
|
||||||
captured := make([]capturedOutput, 0, len(outputs))
|
|
||||||
|
|
||||||
var minX, minY, maxX, maxY int
|
type pendingOutput struct {
|
||||||
first := true
|
result *CaptureResult
|
||||||
|
logX float64
|
||||||
|
logY float64
|
||||||
|
scale float64
|
||||||
|
}
|
||||||
|
var pending []pendingOutput
|
||||||
|
maxScale := 1.0
|
||||||
|
|
||||||
for _, output := range outputs {
|
for _, output := range outputs {
|
||||||
result, err := s.captureWholeOutput(output)
|
result, err := s.captureWholeOutput(output)
|
||||||
@@ -322,50 +321,74 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
outX, outY := output.x, output.y
|
logX, logY := float64(output.x), float64(output.y)
|
||||||
scale := float64(output.scale)
|
scale := float64(output.scale)
|
||||||
|
|
||||||
switch DetectCompositor() {
|
switch DetectCompositor() {
|
||||||
case CompositorHyprland:
|
case CompositorHyprland:
|
||||||
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
if hx, hy, _, _, ok := GetHyprlandMonitorGeometry(output.name); ok {
|
||||||
outX, outY = hx, hy
|
logX, logY = float64(hx), float64(hy)
|
||||||
}
|
}
|
||||||
if s := GetHyprlandMonitorScale(output.name); s > 0 {
|
if hs := GetHyprlandMonitorScale(output.name); hs > 0 {
|
||||||
scale = s
|
scale = hs
|
||||||
}
|
}
|
||||||
case CompositorDWL:
|
default:
|
||||||
if info, ok := getOutputInfo(output.name); ok {
|
if wlrInfos != nil {
|
||||||
outX, outY = info.x, info.y
|
if info, ok := wlrInfos[output.name]; ok {
|
||||||
|
logX, logY = float64(info.x), float64(info.y)
|
||||||
|
if info.scale > 0 {
|
||||||
|
scale = info.scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if scale <= 0 {
|
if scale <= 0 {
|
||||||
scale = 1.0
|
scale = 1.0
|
||||||
}
|
}
|
||||||
|
|
||||||
physX := int(float64(outX) * scale)
|
pending = append(pending, pendingOutput{result: result, logX: logX, logY: logY, scale: scale})
|
||||||
physY := int(float64(outY) * scale)
|
if scale > maxScale {
|
||||||
|
maxScale = scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
captured = append(captured, capturedOutput{
|
if len(pending) == 0 {
|
||||||
output: output,
|
return nil, fmt.Errorf("failed to capture any outputs")
|
||||||
result: result,
|
}
|
||||||
physX: physX,
|
if len(pending) == 1 {
|
||||||
physY: physY,
|
return pending[0].result, nil
|
||||||
})
|
}
|
||||||
|
|
||||||
right := physX + result.Buffer.Width
|
type layoutEntry struct {
|
||||||
bottom := physY + result.Buffer.Height
|
result *CaptureResult
|
||||||
|
canvasX int
|
||||||
|
canvasY int
|
||||||
|
canvasW int
|
||||||
|
canvasH int
|
||||||
|
}
|
||||||
|
entries := make([]layoutEntry, len(pending))
|
||||||
|
var minX, minY, maxX, maxY int
|
||||||
|
|
||||||
if first {
|
for i, p := range pending {
|
||||||
minX, minY = physX, physY
|
cx := int(math.Round(p.logX * maxScale))
|
||||||
maxX, maxY = right, bottom
|
cy := int(math.Round(p.logY * maxScale))
|
||||||
first = false
|
cw := int(math.Round(float64(p.result.Buffer.Width) * maxScale / p.scale))
|
||||||
|
ch := int(math.Round(float64(p.result.Buffer.Height) * maxScale / p.scale))
|
||||||
|
|
||||||
|
entries[i] = layoutEntry{result: p.result, canvasX: cx, canvasY: cy, canvasW: cw, canvasH: ch}
|
||||||
|
|
||||||
|
right := cx + cw
|
||||||
|
bottom := cy + ch
|
||||||
|
if i == 0 {
|
||||||
|
minX, minY, maxX, maxY = cx, cy, right, bottom
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if cx < minX {
|
||||||
if physX < minX {
|
minX = cx
|
||||||
minX = physX
|
|
||||||
}
|
}
|
||||||
if physY < minY {
|
if cy < minY {
|
||||||
minY = physY
|
minY = cy
|
||||||
}
|
}
|
||||||
if right > maxX {
|
if right > maxX {
|
||||||
maxX = right
|
maxX = right
|
||||||
@@ -375,35 +398,26 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(captured) == 0 {
|
|
||||||
return nil, fmt.Errorf("failed to capture any outputs")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(captured) == 1 {
|
|
||||||
return captured[0].result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
totalW := maxX - minX
|
totalW := maxX - minX
|
||||||
totalH := maxY - minY
|
totalH := maxY - minY
|
||||||
|
composite, err := CreateShmBuffer(totalW, totalH, totalW*4)
|
||||||
compositeStride := totalW * 4
|
|
||||||
composite, err := CreateShmBuffer(totalW, totalH, compositeStride)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
c.result.Buffer.Close()
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("create composite buffer: %w", err)
|
return nil, fmt.Errorf("create composite buffer: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
composite.Clear()
|
composite.Clear()
|
||||||
|
|
||||||
var format uint32
|
var format uint32
|
||||||
for _, c := range captured {
|
for _, e := range entries {
|
||||||
if format == 0 {
|
if format == 0 {
|
||||||
format = c.result.Format
|
format = e.result.Format
|
||||||
}
|
}
|
||||||
s.blitBuffer(composite, c.result.Buffer, c.physX-minX, c.physY-minY, c.result.YInverted)
|
s.blitBufferScaled(composite, e.result.Buffer,
|
||||||
c.result.Buffer.Close()
|
e.canvasX-minX, e.canvasY-minY, e.canvasW, e.canvasH,
|
||||||
|
e.result.YInverted)
|
||||||
|
e.result.Buffer.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return &CaptureResult{
|
return &CaptureResult{
|
||||||
@@ -413,32 +427,44 @@ func (s *Screenshoter) captureAllScreens() (*CaptureResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Screenshoter) blitBuffer(dst, src *ShmBuffer, dstX, dstY int, yInverted bool) {
|
func (s *Screenshoter) blitBufferScaled(dst, src *ShmBuffer, dstX, dstY, dstW, dstH int, yInverted bool) {
|
||||||
|
if dstW <= 0 || dstH <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
srcData := src.Data()
|
srcData := src.Data()
|
||||||
dstData := dst.Data()
|
dstData := dst.Data()
|
||||||
|
|
||||||
for srcY := 0; srcY < src.Height; srcY++ {
|
for dy := 0; dy < dstH; dy++ {
|
||||||
actualSrcY := srcY
|
canvasY := dstY + dy
|
||||||
if yInverted {
|
if canvasY < 0 || canvasY >= dst.Height {
|
||||||
actualSrcY = src.Height - 1 - srcY
|
|
||||||
}
|
|
||||||
|
|
||||||
dy := dstY + srcY
|
|
||||||
if dy < 0 || dy >= dst.Height {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
srcRowOff := actualSrcY * src.Stride
|
srcY := dy * src.Height / dstH
|
||||||
dstRowOff := dy * dst.Stride
|
if yInverted {
|
||||||
|
srcY = src.Height - 1 - srcY
|
||||||
|
}
|
||||||
|
if srcY < 0 || srcY >= src.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for srcX := 0; srcX < src.Width; srcX++ {
|
srcRowOff := srcY * src.Stride
|
||||||
dx := dstX + srcX
|
dstRowOff := canvasY * dst.Stride
|
||||||
if dx < 0 || dx >= dst.Width {
|
|
||||||
|
for dx := 0; dx < dstW; dx++ {
|
||||||
|
canvasX := dstX + dx
|
||||||
|
if canvasX < 0 || canvasX >= dst.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
srcX := dx * src.Width / dstW
|
||||||
|
if srcX >= src.Width {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
si := srcRowOff + srcX*4
|
si := srcRowOff + srcX*4
|
||||||
di := dstRowOff + dx*4
|
di := dstRowOff + canvasX*4
|
||||||
|
|
||||||
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
if si+3 >= len(srcData) || di+3 >= len(dstData) {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -311,6 +311,10 @@ func (m *Manager) handleDevicePropertiesChanged(path dbus.ObjectPath, changed ma
|
|||||||
select {
|
select {
|
||||||
case m.eventQueue <- func() {
|
case m.eventQueue <- func() {
|
||||||
time.Sleep(300 * time.Millisecond)
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
log.Infof("[Bluetooth] Auto-trusting newly paired device: %s", devicePath)
|
||||||
|
if err := m.TrustDevice(devicePath, true); err != nil {
|
||||||
|
log.Warnf("[Bluetooth] Auto-trust failed: %v", err)
|
||||||
|
}
|
||||||
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
log.Infof("[Bluetooth] Auto-connecting newly paired device: %s", devicePath)
|
||||||
if err := m.ConnectDevice(devicePath); err != nil {
|
if err := m.ConnectDevice(devicePath); err != nil {
|
||||||
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
log.Warnf("[Bluetooth] Auto-connect failed: %v", err)
|
||||||
|
|||||||
@@ -215,23 +215,27 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
callback: callback,
|
callback: callback,
|
||||||
}
|
}
|
||||||
|
|
||||||
if timer, exists := b.debounceTimers[id]; exists {
|
if existing, exists := b.debounceTimers[id]; exists {
|
||||||
timer.Reset(200 * time.Millisecond)
|
if existing.Stop() {
|
||||||
} else {
|
b.debounceWg.Done()
|
||||||
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
|
||||||
b.debounceMutex.Lock()
|
|
||||||
pending, exists := b.debouncePending[id]
|
|
||||||
if exists {
|
|
||||||
delete(b.debouncePending, id)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.debounceWg.Add(1)
|
||||||
|
b.debounceTimers[id] = time.AfterFunc(200*time.Millisecond, func() {
|
||||||
|
defer b.debounceWg.Done()
|
||||||
|
|
||||||
|
b.debounceMutex.Lock()
|
||||||
|
pending, hasPending := b.debouncePending[id]
|
||||||
|
delete(b.debouncePending, id)
|
||||||
|
delete(b.debounceTimers, id)
|
||||||
b.debounceMutex.Unlock()
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
if !exists {
|
if !hasPending {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.setBrightnessImmediateWithExponent(id, pending.percent)
|
if err := b.setBrightnessImmediateWithExponent(id, pending.percent); err != nil {
|
||||||
if err != nil {
|
|
||||||
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
log.Debugf("Failed to set brightness for %s: %v", id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +243,6 @@ func (b *DDCBackend) SetBrightnessWithExponent(id string, value int, exponential
|
|||||||
pending.callback()
|
pending.callback()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
|
||||||
b.debounceMutex.Unlock()
|
b.debounceMutex.Unlock()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -490,5 +493,19 @@ func (b *DDCBackend) valueToPercent(value int, max int, exponential bool) int {
|
|||||||
return percent
|
return percent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *DDCBackend) WaitPending() {
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
b.debounceWg.Wait()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
log.Debug("WaitPending timed out waiting for DDC writes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *DDCBackend) Close() {
|
func (b *DDCBackend) Close() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ type DDCBackend struct {
|
|||||||
debounceMutex sync.Mutex
|
debounceMutex sync.Mutex
|
||||||
debounceTimers map[string]*time.Timer
|
debounceTimers map[string]*time.Timer
|
||||||
debouncePending map[string]ddcPendingSet
|
debouncePending map[string]ddcPendingSet
|
||||||
|
debounceWg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
type ddcPendingSet struct {
|
type ddcPendingSet struct {
|
||||||
|
|||||||
@@ -212,9 +212,10 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var offer any
|
var offer any
|
||||||
if e.Id != nil {
|
switch {
|
||||||
|
case e.Id != nil:
|
||||||
offer = e.Id
|
offer = e.Id
|
||||||
} else if e.OfferId != 0 {
|
case e.OfferId != 0:
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
offer = m.offerRegistry[e.OfferId]
|
offer = m.offerRegistry[e.OfferId]
|
||||||
m.offerMutex.RUnlock()
|
m.offerMutex.RUnlock()
|
||||||
@@ -224,16 +225,21 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
wasOwner := m.isOwner
|
wasOwner := m.isOwner
|
||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
|
|
||||||
if offer == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if wasOwner {
|
if wasOwner {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prevOffer := m.currentOffer
|
||||||
m.currentOffer = offer
|
m.currentOffer = offer
|
||||||
|
|
||||||
|
if prevOffer != nil && prevOffer != offer {
|
||||||
|
m.releaseOffer(prevOffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if offer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
m.offerMutex.RLock()
|
m.offerMutex.RLock()
|
||||||
mimes := m.offerMimeTypes[offer]
|
mimes := m.offerMimeTypes[offer]
|
||||||
m.offerMutex.RUnlock()
|
m.offerMutex.RUnlock()
|
||||||
@@ -285,6 +291,33 @@ func (m *Manager) setupDataDeviceSync() {
|
|||||||
log.Info("Data device setup complete")
|
log.Info("Data device setup complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) releaseOffer(offer any) {
|
||||||
|
if offer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
typedOffer, ok := offer.(*ext_data_control.ExtDataControlOfferV1)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.offerMutex.Lock()
|
||||||
|
delete(m.offerMimeTypes, offer)
|
||||||
|
delete(m.offerRegistry, typedOffer.ID())
|
||||||
|
m.offerMutex.Unlock()
|
||||||
|
typedOffer.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) releaseCurrentSource() {
|
||||||
|
if m.currentSource == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source, ok := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
||||||
|
m.currentSource = nil
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
source.Destroy()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
func (m *Manager) readAndStore(r *os.File, mimeType string) {
|
||||||
defer r.Close()
|
defer r.Close()
|
||||||
|
|
||||||
@@ -388,7 +421,7 @@ func (m *Manager) deduplicateInTx(b *bolt.Bucket, hash uint64) error {
|
|||||||
if extractHash(v) != hash {
|
if extractHash(v) != hash {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -406,7 +439,7 @@ func (m *Manager) trimLengthInTx(b *bolt.Bucket) error {
|
|||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
var count int
|
var count int
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -449,6 +482,14 @@ func encodeEntry(e Entry) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decodeEntry(data []byte) (Entry, error) {
|
func decodeEntry(data []byte) (Entry, error) {
|
||||||
|
return decodeEntryFields(data, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEntryMeta(data []byte) (Entry, error) {
|
||||||
|
return decodeEntryFields(data, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeEntryFields(data []byte, withData bool) (Entry, error) {
|
||||||
buf := bytes.NewReader(data)
|
buf := bytes.NewReader(data)
|
||||||
var e Entry
|
var e Entry
|
||||||
|
|
||||||
@@ -456,8 +497,15 @@ func decodeEntry(data []byte) (Entry, error) {
|
|||||||
|
|
||||||
var dataLen uint32
|
var dataLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &dataLen)
|
binary.Read(buf, binary.BigEndian, &dataLen)
|
||||||
|
switch {
|
||||||
|
case withData:
|
||||||
e.Data = make([]byte, dataLen)
|
e.Data = make([]byte, dataLen)
|
||||||
buf.Read(e.Data)
|
buf.Read(e.Data)
|
||||||
|
default:
|
||||||
|
if _, err := buf.Seek(int64(dataLen), io.SeekCurrent); err != nil {
|
||||||
|
return e, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var mimeLen uint32
|
var mimeLen uint32
|
||||||
binary.Read(buf, binary.BigEndian, &mimeLen)
|
binary.Read(buf, binary.BigEndian, &mimeLen)
|
||||||
@@ -587,21 +635,27 @@ func (m *Manager) uriListPreview(data []byte) (string, bool) {
|
|||||||
uris = strings.Split(text, "\n")
|
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://") {
|
if len(uris) == 1 && strings.HasPrefix(uris[0], "file://") {
|
||||||
filePath := strings.TrimPrefix(uris[0], "file://")
|
filePath := strings.TrimPrefix(uris[0], "file://")
|
||||||
if info, err := os.Stat(filePath); err == nil && !info.IsDir() {
|
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 imgData, err := os.ReadFile(filePath); err == nil {
|
||||||
if config, imgFmt, err := image.DecodeConfig(bytes.NewReader(imgData)); 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 %s %dx%d ]]", filepath.Base(filePath), imgFmt, config.Width, config.Height), true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
|
return fmt.Sprintf("[[ file %s ]]", filepath.Base(filePath)), false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(uris) > 1 {
|
|
||||||
return fmt.Sprintf("[[ %d files ]]", len(uris)), false
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.textPreview(data), false
|
return m.textPreview(data), false
|
||||||
}
|
}
|
||||||
@@ -623,6 +677,11 @@ func (m *Manager) tryReadImageFromURI(data []byte) ([]byte, string, bool) {
|
|||||||
return nil, "", false
|
return nil, "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg := m.getConfig()
|
||||||
|
if info.Size() > cfg.MaxEntrySize {
|
||||||
|
return nil, "", false
|
||||||
|
}
|
||||||
|
|
||||||
imgData, err := os.ReadFile(filePath)
|
imgData, err := os.ReadFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", false
|
return nil, "", false
|
||||||
@@ -650,14 +709,9 @@ func sizeStr(size int) string {
|
|||||||
func (m *Manager) updateState() {
|
func (m *Manager) updateState() {
|
||||||
history := m.GetHistory()
|
history := m.GetHistory()
|
||||||
|
|
||||||
for i := range history {
|
|
||||||
history[i].Data = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var current *Entry
|
var current *Entry
|
||||||
if len(history) > 0 {
|
if len(history) > 0 {
|
||||||
c := history[0]
|
c := history[0]
|
||||||
c.Data = nil
|
|
||||||
current = &c
|
current = &c
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -732,7 +786,7 @@ func (m *Manager) GetHistory() []Entry {
|
|||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
|
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -917,7 +971,7 @@ func (m *Manager) ClearHistory() {
|
|||||||
var toDelete [][]byte
|
var toDelete [][]byte
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
toDelete = append(toDelete, k)
|
toDelete = append(toDelete, k)
|
||||||
}
|
}
|
||||||
@@ -940,7 +994,7 @@ func (m *Manager) ClearHistory() {
|
|||||||
if b != nil {
|
if b != nil {
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, _ := decodeEntry(v)
|
entry, _ := decodeEntryMeta(v)
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1048,6 +1102,7 @@ func (m *Manager) SetClipboard(data []byte, mimeType string) error {
|
|||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.releaseCurrentSource()
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
m.sourceMutex.Lock()
|
m.sourceMutex.Lock()
|
||||||
m.sourceMimeTypes = []string{mimeType}
|
m.sourceMimeTypes = []string{mimeType}
|
||||||
@@ -1127,9 +1182,11 @@ func (m *Manager) Close() {
|
|||||||
m.subscribers = make(map[string]chan State)
|
m.subscribers = make(map[string]chan State)
|
||||||
m.subMutex.Unlock()
|
m.subMutex.Unlock()
|
||||||
|
|
||||||
if m.currentSource != nil {
|
m.releaseCurrentSource()
|
||||||
source := m.currentSource.(*ext_data_control.ExtDataControlSourceV1)
|
|
||||||
source.Destroy()
|
if m.currentOffer != nil {
|
||||||
|
m.releaseOffer(m.currentOffer)
|
||||||
|
m.currentOffer = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.dataDevice != nil {
|
if m.dataDevice != nil {
|
||||||
@@ -1173,11 +1230,10 @@ func (m *Manager) clearOldEntries(days int) error {
|
|||||||
var toDelete [][]byte
|
var toDelete [][]byte
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Skip pinned entries
|
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1292,7 +1348,7 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1317,7 +1373,6 @@ func (m *Manager) Search(params SearchParams) SearchResult {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.Data = nil
|
|
||||||
all = append(all, entry)
|
all = append(all, entry)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -1492,7 +1547,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil || !entry.Pinned {
|
if err != nil || !entry.Pinned {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1510,7 +1565,6 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check pinned count
|
|
||||||
cfg := m.getConfig()
|
cfg := m.getConfig()
|
||||||
pinnedCount := 0
|
pinnedCount := 0
|
||||||
if err := m.db.View(func(tx *bolt.Tx) error {
|
if err := m.db.View(func(tx *bolt.Tx) error {
|
||||||
@@ -1520,7 +1574,7 @@ func (m *Manager) PinEntry(id uint64) error {
|
|||||||
}
|
}
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
pinnedCount++
|
pinnedCount++
|
||||||
}
|
}
|
||||||
@@ -1611,12 +1665,11 @@ func (m *Manager) GetPinnedEntries() []Entry {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if entry.Pinned {
|
if entry.Pinned {
|
||||||
entry.Data = nil
|
|
||||||
pinned = append(pinned, entry)
|
pinned = append(pinned, entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1642,7 +1695,7 @@ func (m *Manager) GetPinnedCount() int {
|
|||||||
|
|
||||||
c := b.Cursor()
|
c := b.Cursor()
|
||||||
for k, v := c.First(); k != nil; k, v = c.Next() {
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
||||||
entry, err := decodeEntry(v)
|
entry, err := decodeEntryMeta(v)
|
||||||
if err == nil && entry.Pinned {
|
if err == nil && entry.Pinned {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
@@ -1761,6 +1814,7 @@ func (m *Manager) CopyFile(filePath string) error {
|
|||||||
m.ownerLock.Unlock()
|
m.ownerLock.Unlock()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.releaseCurrentSource()
|
||||||
m.currentSource = source
|
m.currentSource = source
|
||||||
|
|
||||||
m.ownerLock.Lock()
|
m.ownerLock.Lock()
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ func (m *Manager) Close() {
|
|||||||
|
|
||||||
func InitializeManager() (*Manager, error) {
|
func InitializeManager() (*Manager, error) {
|
||||||
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
if os.Getuid() != 0 && !hasInputGroupAccess() {
|
||||||
return nil, fmt.Errorf("insufficient permissions to access input devices")
|
return nil, fmt.Errorf("insufficient permissions to access input devices. Add your user to the 'input' group: `sudo usermod -a -G input $USER` or run `dms setup`")
|
||||||
}
|
}
|
||||||
|
|
||||||
return NewManager()
|
return NewManager()
|
||||||
|
|||||||
@@ -16,4 +16,8 @@ const (
|
|||||||
dbusScreensaverPath = "/ScreenSaver"
|
dbusScreensaverPath = "/ScreenSaver"
|
||||||
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
|
dbusScreensaverPath2 = "/org/freedesktop/ScreenSaver"
|
||||||
dbusScreensaverInterface = "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
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
m.screensaverSubscribers.Range(func(key string, ch chan ScreensaverState) bool {
|
||||||
|
close(ch)
|
||||||
|
m.screensaverSubscribers.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
if m.systemConn != nil {
|
if m.systemConn != nil {
|
||||||
m.systemConn.Close()
|
m.systemConn.Close()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package freedesktop
|
package freedesktop
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -15,45 +16,9 @@ type screensaverHandler struct {
|
|||||||
manager *Manager
|
manager *Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) initializeScreensaver() error {
|
func screensaverIntrospectIface(ifaceName string) introspect.Interface {
|
||||||
if m.sessionConn == nil {
|
return introspect.Interface{
|
||||||
m.stateMutex.Lock()
|
Name: ifaceName,
|
||||||
m.state.Screensaver.Available = false
|
|
||||||
m.stateMutex.Unlock()
|
|
||||||
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)
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
screensaverIface := introspect.Interface{
|
|
||||||
Name: dbusScreensaverInterface,
|
|
||||||
Methods: []introspect.Method{
|
Methods: []introspect.Method{
|
||||||
{
|
{
|
||||||
Name: "Inhibit",
|
Name: "Inhibit",
|
||||||
@@ -69,40 +34,106 @@ func (m *Manager) initializeScreensaver() error {
|
|||||||
{Name: "cookie", Type: "u", Direction: "in"},
|
{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"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
introNode := &introspect.Node{
|
func (m *Manager) initializeScreensaver() error {
|
||||||
Name: dbusScreensaverPath,
|
if m.sessionConn == nil {
|
||||||
Interfaces: []introspect.Interface{
|
m.stateMutex.Lock()
|
||||||
introspect.IntrospectData,
|
m.state.Screensaver.Available = false
|
||||||
screensaverIface,
|
m.stateMutex.Unlock()
|
||||||
},
|
return nil
|
||||||
}
|
|
||||||
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{
|
handler := &screensaverHandler{manager: m}
|
||||||
Name: dbusScreensaverPath2,
|
|
||||||
Interfaces: []introspect.Interface{
|
m.screensaverFreedesktopClaimed = m.claimScreensaverName(handler,
|
||||||
introspect.IntrospectData,
|
dbusScreensaverName, dbusScreensaverInterface, dbusScreensaverPath, dbusScreensaverPath2)
|
||||||
screensaverIface,
|
m.screensaverGnomeClaimed = m.claimScreensaverName(handler,
|
||||||
},
|
dbusGnomeScreensaverName, dbusGnomeScreensaverInterface, dbusGnomeScreensaverPath)
|
||||||
}
|
|
||||||
if err := m.sessionConn.Export(introspect.NewIntrospectable(introNode2), dbusScreensaverPath2, "org.freedesktop.DBus.Introspectable"); err != nil {
|
if !m.screensaverFreedesktopClaimed && !m.screensaverGnomeClaimed {
|
||||||
log.Warnf("Failed to export introspectable on %s: %v", dbusScreensaverPath2, err)
|
log.Warn("No screensaver interface could be claimed")
|
||||||
|
m.stateMutex.Lock()
|
||||||
|
m.state.Screensaver.Available = false
|
||||||
|
m.stateMutex.Unlock()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
go m.watchPeerDisconnects()
|
go m.watchPeerDisconnects()
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
m.state.Screensaver.Available = true
|
m.state.Screensaver.Available = true
|
||||||
|
m.state.Screensaver.Active = false
|
||||||
m.state.Screensaver.Inhibited = false
|
m.state.Screensaver.Inhibited = false
|
||||||
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
|
m.state.Screensaver.Inhibitors = []ScreensaverInhibitor{}
|
||||||
m.stateMutex.Unlock()
|
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.Infof("Screensaver name %s already owned by another process (e.g. hypridle/swayidle)", 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,3 +299,51 @@ func (m *Manager) NotifyScreensaverSubscribers() {
|
|||||||
return true
|
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 {
|
type ScreensaverState struct {
|
||||||
Available bool `json:"available"`
|
Available bool `json:"available"`
|
||||||
|
Active bool `json:"active"`
|
||||||
Inhibited bool `json:"inhibited"`
|
Inhibited bool `json:"inhibited"`
|
||||||
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
|
Inhibitors []ScreensaverInhibitor `json:"inhibitors"`
|
||||||
}
|
}
|
||||||
@@ -60,4 +61,6 @@ type Manager struct {
|
|||||||
subscribers syncmap.Map[string, chan FreedeskState]
|
subscribers syncmap.Map[string, chan FreedeskState]
|
||||||
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
screensaverSubscribers syncmap.Map[string, chan ScreensaverState]
|
||||||
screensaverCookieCounter uint32
|
screensaverCookieCounter uint32
|
||||||
|
screensaverFreedesktopClaimed bool
|
||||||
|
screensaverGnomeClaimed bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ const (
|
|||||||
dbusPath = "/org/freedesktop/login1"
|
dbusPath = "/org/freedesktop/login1"
|
||||||
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
||||||
dbusSessionInterface = "org.freedesktop.login1.Session"
|
dbusSessionInterface = "org.freedesktop.login1.Session"
|
||||||
|
dbusUserInterface = "org.freedesktop.login1.User"
|
||||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,15 +17,8 @@ func NewManager() (*Manager, error) {
|
|||||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := os.Getenv("XDG_SESSION_ID")
|
|
||||||
if sessionID == "" {
|
|
||||||
sessionID = "self"
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
state: &SessionState{
|
state: &SessionState{},
|
||||||
SessionID: sessionID,
|
|
||||||
},
|
|
||||||
stateMutex: sync.RWMutex{},
|
stateMutex: sync.RWMutex{},
|
||||||
|
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
@@ -60,12 +53,13 @@ func (m *Manager) initialize() error {
|
|||||||
|
|
||||||
m.initializeFallbackDelay()
|
m.initializeFallbackDelay()
|
||||||
|
|
||||||
sessionPath, err := m.getSession(m.state.SessionID)
|
sessionID, sessionPath, err := m.discoverSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get session path: %w", err)
|
return fmt.Errorf("failed to get session path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
|
m.state.SessionID = sessionID
|
||||||
m.state.SessionPath = string(sessionPath)
|
m.state.SessionPath = string(sessionPath)
|
||||||
m.sessionPath = sessionPath
|
m.sessionPath = sessionPath
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
@@ -79,6 +73,41 @@ func (m *Manager) initialize() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
|
||||||
|
// 1. Explicit XDG_SESSION_ID
|
||||||
|
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
|
||||||
|
if path, err := m.getSession(id); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
|
||||||
|
return id, path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. PID-based lookup (works when caller is inside a session cgroup)
|
||||||
|
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
|
||||||
|
return id, path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. User's primary display session (handles UWSM and similar)
|
||||||
|
if id, path, err := m.getUserDisplaySession(); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
|
||||||
|
return id, path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Score all sessions for current UID
|
||||||
|
if id, path, err := m.findBestSession(); err == nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
|
||||||
|
return id, path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Last resort: "self"
|
||||||
|
path, err := m.getSession("self")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("%w", err)
|
||||||
|
}
|
||||||
|
return "self", path, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
||||||
var out dbus.ObjectPath
|
var out dbus.ObjectPath
|
||||||
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
||||||
@@ -88,6 +117,166 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
|
||||||
|
var path dbus.ObjectPath
|
||||||
|
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionObj := m.conn.Object(dbusDest, path)
|
||||||
|
var id dbus.Variant
|
||||||
|
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return id.Value().(string), path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
|
||||||
|
uid := uint32(os.Getuid())
|
||||||
|
|
||||||
|
var userPath dbus.ObjectPath
|
||||||
|
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
userObj := m.conn.Object(dbusDest, userPath)
|
||||||
|
var display dbus.Variant
|
||||||
|
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pair, ok := display.Value().([]any)
|
||||||
|
if !ok || len(pair) < 2 {
|
||||||
|
return "", "", fmt.Errorf("unexpected Display format")
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionID, _ := pair[0].(string)
|
||||||
|
sessionPath, _ := pair[1].(dbus.ObjectPath)
|
||||||
|
if sessionID == "" || sessionPath == "" {
|
||||||
|
return "", "", fmt.Errorf("empty Display session")
|
||||||
|
}
|
||||||
|
|
||||||
|
return sessionID, sessionPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type sessionCandidate struct {
|
||||||
|
id string
|
||||||
|
path dbus.ObjectPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
|
||||||
|
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
|
||||||
|
var raw [][]any
|
||||||
|
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
uid := uint32(os.Getuid())
|
||||||
|
var candidates []sessionCandidate
|
||||||
|
for _, entry := range raw {
|
||||||
|
if len(entry) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entryUID, _ := entry[1].(uint32)
|
||||||
|
if entryUID != uid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, _ := entry[0].(string)
|
||||||
|
path, _ := entry[4].(dbus.ObjectPath)
|
||||||
|
if id != "" && path != "" {
|
||||||
|
candidates = append(candidates, sessionCandidate{id: id, path: path})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(candidates) == 0 {
|
||||||
|
return "", "", fmt.Errorf("no sessions for uid %d", uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
bestScore := -1
|
||||||
|
var best sessionCandidate
|
||||||
|
for _, c := range candidates {
|
||||||
|
score := m.scoreSession(c.path)
|
||||||
|
if score > bestScore {
|
||||||
|
bestScore = score
|
||||||
|
best = c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if bestScore < 0 {
|
||||||
|
return "", "", fmt.Errorf("no viable session found")
|
||||||
|
}
|
||||||
|
return best.id, best.path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
|
||||||
|
obj := m.conn.Object(dbusDest, path)
|
||||||
|
var props map[string]dbus.Variant
|
||||||
|
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
getStr := func(key string) string {
|
||||||
|
if v, ok := props[key]; ok {
|
||||||
|
if s, ok := v.Value().(string); ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
getBool := func(key string) bool {
|
||||||
|
if v, ok := props[key]; ok {
|
||||||
|
if b, ok := v.Value().(bool); ok {
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
getUint32 := func(key string) uint32 {
|
||||||
|
if v, ok := props[key]; ok {
|
||||||
|
if u, ok := v.Value().(uint32); ok {
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
class := getStr("Class")
|
||||||
|
if class != "user" {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if getBool("Remote") {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
score := 0
|
||||||
|
|
||||||
|
if getBool("Active") {
|
||||||
|
score += 100
|
||||||
|
}
|
||||||
|
|
||||||
|
switch getStr("Type") {
|
||||||
|
case "wayland", "x11":
|
||||||
|
score += 80
|
||||||
|
case "tty":
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := props["Seat"]; ok {
|
||||||
|
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||||
|
if seat, ok := seatArr[0].(string); ok && seat != "" {
|
||||||
|
score += 40
|
||||||
|
if seat == "seat0" {
|
||||||
|
score += 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if getUint32("VTNr") > 0 {
|
||||||
|
score += 20
|
||||||
|
}
|
||||||
|
|
||||||
|
return score
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) refreshSessionBinding() error {
|
func (m *Manager) refreshSessionBinding() error {
|
||||||
if m.managerObj == nil || m.conn == nil {
|
if m.managerObj == nil || m.conn == nil {
|
||||||
return fmt.Errorf("manager not fully initialized")
|
return fmt.Errorf("manager not fully initialized")
|
||||||
|
|||||||
@@ -158,18 +158,26 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := ssid == currentSSID && bssid == currentBSSID
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: ssid == currentSSID && bssid == currentBSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,19 +463,27 @@ func (b *NetworkManagerBackend) updateWiFiNetworks() ([]WiFiNetwork, error) {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := ssid == currentSSID
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := w.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: ssid,
|
SSID: ssid,
|
||||||
BSSID: bssid,
|
BSSID: bssid,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: ssid == currentSSID,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[ssid],
|
Saved: savedSSIDs[ssid],
|
||||||
Autoconnect: autoconnectMap[ssid],
|
Autoconnect: autoconnectMap[ssid],
|
||||||
Hidden: hiddenSSIDs[ssid],
|
Hidden: hiddenSSIDs[ssid],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1003,19 +1019,27 @@ func (b *NetworkManagerBackend) updateAllWiFiDevices() {
|
|||||||
|
|
||||||
channel := frequencyToChannel(freq)
|
channel := frequencyToChannel(freq)
|
||||||
|
|
||||||
|
isConnected := connected && apSSID == ssid
|
||||||
|
rate := maxBitrate / 1000
|
||||||
|
if isConnected {
|
||||||
|
if devBitrate, err := devInfo.wireless.GetPropertyBitrate(); err == nil && devBitrate > 0 {
|
||||||
|
rate = devBitrate / 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
network := WiFiNetwork{
|
network := WiFiNetwork{
|
||||||
SSID: apSSID,
|
SSID: apSSID,
|
||||||
BSSID: apBSSID,
|
BSSID: apBSSID,
|
||||||
Signal: strength,
|
Signal: strength,
|
||||||
Secured: secured,
|
Secured: secured,
|
||||||
Enterprise: enterprise,
|
Enterprise: enterprise,
|
||||||
Connected: connected && apSSID == ssid,
|
Connected: isConnected,
|
||||||
Saved: savedSSIDs[apSSID],
|
Saved: savedSSIDs[apSSID],
|
||||||
Autoconnect: autoconnectMap[apSSID],
|
Autoconnect: autoconnectMap[apSSID],
|
||||||
Hidden: hiddenSSIDs[apSSID],
|
Hidden: hiddenSSIDs[apSSID],
|
||||||
Frequency: freq,
|
Frequency: freq,
|
||||||
Mode: modeStr,
|
Mode: modeStr,
|
||||||
Rate: maxBitrate / 1000,
|
Rate: rate,
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
Device: name,
|
Device: name,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package network
|
package network
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -28,7 +29,13 @@ func TestDetectResult_HasNetworkdField(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectNetworkStack_Integration(t *testing.T) {
|
func TestDetectNetworkStack_Integration(t *testing.T) {
|
||||||
result, err := DetectNetworkStack()
|
result, err := DetectNetworkStack()
|
||||||
|
|
||||||
|
if err != nil && strings.Contains(err.Error(), "connect system bus") {
|
||||||
|
t.Skipf("system D-Bus unavailable: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, result)
|
if assert.NotNil(t, result) {
|
||||||
assert.NotEmpty(t, result.ChosenReason)
|
assert.NotEmpty(t, result.ChosenReason)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1516,7 +1516,11 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
loginctlReady := make(chan struct{})
|
||||||
|
freedesktopReady := make(chan struct{})
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(loginctlReady)
|
||||||
if err := InitializeLoginctlManager(); err != nil {
|
if err := InitializeLoginctlManager(); err != nil {
|
||||||
log.Warnf("Loginctl manager unavailable: %v", err)
|
log.Warnf("Loginctl manager unavailable: %v", err)
|
||||||
} else {
|
} else {
|
||||||
@@ -1525,6 +1529,7 @@ func Start(printDocs bool) error {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
defer close(freedesktopReady)
|
||||||
if err := InitializeFreedeskManager(); err != nil {
|
if err := InitializeFreedeskManager(); err != nil {
|
||||||
log.Warnf("Freedesktop manager unavailable: %v", err)
|
log.Warnf("Freedesktop manager unavailable: %v", err)
|
||||||
} else if freedesktopManager != nil {
|
} else if freedesktopManager != nil {
|
||||||
@@ -1533,6 +1538,31 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Bridge loginctl lock state to the freedesktop/gnome screensaver
|
||||||
|
// ActiveChanged signal so apps like Bitwarden can detect screen lock.
|
||||||
|
go func() {
|
||||||
|
<-loginctlReady
|
||||||
|
<-freedesktopReady
|
||||||
|
|
||||||
|
if loginctlManager == nil || freedesktopManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := loginctlManager.Subscribe("dms-lock-bridge")
|
||||||
|
defer loginctlManager.Unsubscribe("dms-lock-bridge")
|
||||||
|
|
||||||
|
initial := loginctlManager.GetState()
|
||||||
|
lastLocked := initial.Locked
|
||||||
|
freedesktopManager.SetScreenLockActive(lastLocked)
|
||||||
|
|
||||||
|
for state := range ch {
|
||||||
|
if state.Locked != lastLocked {
|
||||||
|
lastLocked = state.Locked
|
||||||
|
freedesktopManager.SetScreenLockActive(lastLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if err := InitializeWaylandManager(); err != nil {
|
if err := InitializeWaylandManager(); err != nil {
|
||||||
log.Warnf("Wayland manager unavailable: %v", err)
|
log.Warnf("Wayland manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
@@ -1569,6 +1599,13 @@ func Start(printDocs bool) error {
|
|||||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||||
} else {
|
} else {
|
||||||
notifyCapabilityChange()
|
notifyCapabilityChange()
|
||||||
|
go func() {
|
||||||
|
<-loginctlReady
|
||||||
|
if loginctlManager == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
themeModeManager.WatchLoginctl(loginctlManager)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
fatalErrChan := make(chan error, 1)
|
fatalErrChan := make(chan error, 1)
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func TestCleanupStaleSockets(t *testing.T) {
|
|||||||
tempDir := t.TempDir()
|
tempDir := t.TempDir()
|
||||||
t.Setenv("XDG_RUNTIME_DIR", tempDir)
|
t.Setenv("XDG_RUNTIME_DIR", tempDir)
|
||||||
|
|
||||||
staleSocket := filepath.Join(tempDir, "danklinux-999999.sock")
|
staleSocket := filepath.Join(tempDir, "danklinux-4194305.sock")
|
||||||
err := os.WriteFile(staleSocket, []byte{}, 0o600)
|
err := os.WriteFile(staleSocket, []byte{}, 0o600)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
)
|
)
|
||||||
@@ -187,6 +188,29 @@ func (m *Manager) Close() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) WatchLoginctl(lm *loginctl.Manager) {
|
||||||
|
ch := lm.Subscribe("thememode")
|
||||||
|
m.wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer m.wg.Done()
|
||||||
|
defer lm.Unsubscribe("thememode")
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.stopChan:
|
||||||
|
return
|
||||||
|
case state, ok := <-ch:
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if state.PreparingForSleep {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.TriggerUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) schedulerLoop() {
|
func (m *Manager) schedulerLoop() {
|
||||||
defer m.wg.Done()
|
defer m.wg.Done()
|
||||||
|
|
||||||
@@ -327,11 +351,13 @@ func statesEqual(a, b *State) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
func (m *Manager) computeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
if config.Mode == "location" {
|
switch config.Mode {
|
||||||
|
case "location":
|
||||||
return m.computeLocationSchedule(now, config)
|
return m.computeLocationSchedule(now, config)
|
||||||
}
|
default:
|
||||||
return computeTimeSchedule(now, config)
|
return computeTimeSchedule(now, config)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
func computeTimeSchedule(now time.Time, config Config) (bool, time.Time) {
|
||||||
startMinutes := config.StartHour*60 + config.StartMinute
|
startMinutes := config.StartHour*60 + config.StartMinute
|
||||||
@@ -381,10 +407,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
|
|||||||
}
|
}
|
||||||
|
|
||||||
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
|
times, cond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, now, config.ElevationTwilight, config.ElevationDaylight)
|
||||||
if cond != wayland.SunNormal {
|
switch cond {
|
||||||
if cond == wayland.SunMidnightSun {
|
case wayland.SunMidnightSun:
|
||||||
return true, startOfNextDay(now)
|
return true, startOfNextDay(now)
|
||||||
}
|
case wayland.SunPolarNight:
|
||||||
return false, startOfNextDay(now)
|
return false, startOfNextDay(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,10 +423,10 @@ func (m *Manager) computeLocationSchedule(now time.Time, config Config) (bool, t
|
|||||||
|
|
||||||
nextDay := startOfNextDay(now)
|
nextDay := startOfNextDay(now)
|
||||||
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
nextTimes, nextCond := wayland.CalculateSunTimesWithTwilight(*lat, *lon, nextDay, config.ElevationTwilight, config.ElevationDaylight)
|
||||||
if nextCond != wayland.SunNormal {
|
switch nextCond {
|
||||||
if nextCond == wayland.SunMidnightSun {
|
case wayland.SunMidnightSun:
|
||||||
return true, startOfNextDay(nextDay)
|
return true, startOfNextDay(nextDay)
|
||||||
}
|
case wayland.SunPolarNight:
|
||||||
return false, startOfNextDay(nextDay)
|
return false, startOfNextDay(nextDay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,13 +439,7 @@ func startOfNextDay(t time.Time) time.Time {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateHourMinute(hour, minute int) bool {
|
func validateHourMinute(hour, minute int) bool {
|
||||||
if hour < 0 || hour > 23 {
|
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59
|
||||||
return false
|
|
||||||
}
|
|
||||||
if minute < 0 || minute > 59 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
|
func (m *Manager) ValidateSchedule(startHour, startMinute, endHour, endMinute int) error {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package wlcontext
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
@@ -123,6 +123,9 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
{Fd: int32(sc.wakeR), Events: unix.POLLIN},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
consecutiveErrors := 0
|
||||||
|
const maxConsecutiveErrors = 20
|
||||||
|
|
||||||
for {
|
for {
|
||||||
sc.drainCmdQueue()
|
sc.drainCmdQueue()
|
||||||
|
|
||||||
@@ -153,9 +156,19 @@ func (sc *SharedContext) eventDispatcher() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
if err := ctx.Dispatch(); err != nil && !os.IsTimeout(err) {
|
||||||
log.Errorf("Wayland connection error: %v", err)
|
consecutiveErrors++
|
||||||
|
log.Warnf("Wayland connection error (%d/%d): %v", consecutiveErrors, maxConsecutiveErrors, err)
|
||||||
|
|
||||||
|
if consecutiveErrors >= maxConsecutiveErrors {
|
||||||
|
log.Errorf("Fatal: Wayland connection unrecoverable after %d attempts. Exiting dispatcher.", maxConsecutiveErrors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
time.Sleep(100 * time.Millisecond * time.Duration(consecutiveErrors))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
consecutiveErrors = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func NewManager(display wlclient.WaylandDisplay) (*Manager, error) {
|
|||||||
m := &Manager{
|
m := &Manager{
|
||||||
display: display,
|
display: display,
|
||||||
ctx: display.Context(),
|
ctx: display.Context(),
|
||||||
cmdq: make(chan cmd, 128),
|
cmdq: make(chan cmd, 512),
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
fatalError: make(chan error, 1),
|
fatalError: make(chan error, 1),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tui
|
|||||||
import (
|
import (
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
"github.com/charmbracelet/bubbles/spinner"
|
"github.com/charmbracelet/bubbles/spinner"
|
||||||
"github.com/charmbracelet/bubbles/textinput"
|
"github.com/charmbracelet/bubbles/textinput"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
@@ -42,6 +43,9 @@ type Model struct {
|
|||||||
sudoPassword string
|
sudoPassword string
|
||||||
existingConfigs []ExistingConfigInfo
|
existingConfigs []ExistingConfigInfo
|
||||||
fingerprintFailed bool
|
fingerprintFailed bool
|
||||||
|
|
||||||
|
availablePrivesc []privesc.Tool
|
||||||
|
selectedPrivesc int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewModel(version string, logFilePath string) Model {
|
func NewModel(version string, logFilePath string) Model {
|
||||||
@@ -147,6 +151,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m.updateGentooUseFlagsState(msg)
|
return m.updateGentooUseFlagsState(msg)
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.updateGentooGCCCheckState(msg)
|
return m.updateGentooGCCCheckState(msg)
|
||||||
|
case StateSelectPrivesc:
|
||||||
|
return m.updateSelectPrivescState(msg)
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.updateAuthMethodChoiceState(msg)
|
return m.updateAuthMethodChoiceState(msg)
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
@@ -189,6 +195,8 @@ func (m Model) View() string {
|
|||||||
return m.viewGentooUseFlags()
|
return m.viewGentooUseFlags()
|
||||||
case StateGentooGCCCheck:
|
case StateGentooGCCCheck:
|
||||||
return m.viewGentooGCCCheck()
|
return m.viewGentooGCCCheck()
|
||||||
|
case StateSelectPrivesc:
|
||||||
|
return m.viewSelectPrivesc()
|
||||||
case StateAuthMethodChoice:
|
case StateAuthMethodChoice:
|
||||||
return m.viewAuthMethodChoice()
|
return m.viewAuthMethodChoice()
|
||||||
case StateFingerprintAuth:
|
case StateFingerprintAuth:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const (
|
|||||||
StateDependencyReview
|
StateDependencyReview
|
||||||
StateGentooUseFlags
|
StateGentooUseFlags
|
||||||
StateGentooGCCCheck
|
StateGentooGCCCheck
|
||||||
|
StateSelectPrivesc
|
||||||
StateAuthMethodChoice
|
StateAuthMethodChoice
|
||||||
StateFingerprintAuth
|
StateFingerprintAuth
|
||||||
StatePasswordPrompt
|
StatePasswordPrompt
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/deps"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/distros"
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/greeter"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,19 +81,24 @@ func (m Model) viewDependencyReview() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
note := ""
|
||||||
|
if dep.Name == "dms-greeter" {
|
||||||
|
note = m.styles.Subtle.Render(" (selection replaces your current display manager)")
|
||||||
|
}
|
||||||
|
|
||||||
var line string
|
var line string
|
||||||
if i == m.selectedDep {
|
if i == m.selectedDep {
|
||||||
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
line = fmt.Sprintf("▶ %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||||
if dep.Version != "" {
|
if dep.Version != "" {
|
||||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||||
}
|
}
|
||||||
line = m.styles.SelectedOption.Render(line)
|
line = m.styles.SelectedOption.Render(line) + note
|
||||||
} else {
|
} else {
|
||||||
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
line = fmt.Sprintf(" %s%s%-25s %s", reinstallMarker, variantMarker, dep.Name, status)
|
||||||
if dep.Version != "" {
|
if dep.Version != "" {
|
||||||
line += fmt.Sprintf(" (%s)", dep.Version)
|
line += fmt.Sprintf(" (%s)", dep.Version)
|
||||||
}
|
}
|
||||||
line = m.styles.Normal.Render(line)
|
line = m.styles.Normal.Render(line) + note
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(line)
|
b.WriteString(line)
|
||||||
@@ -115,6 +121,13 @@ func (m Model) updateDetectingDepsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.state = StateError
|
m.state = StateError
|
||||||
} else {
|
} else {
|
||||||
m.dependencies = depsMsg.deps
|
m.dependencies = depsMsg.deps
|
||||||
|
// dms-greeter is opt-in skipped by default
|
||||||
|
for _, dep := range depsMsg.deps {
|
||||||
|
if dep.Name == "dms-greeter" {
|
||||||
|
m.disabledItems["dms-greeter"] = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
m.state = StateDependencyReview
|
m.state = StateDependencyReview
|
||||||
}
|
}
|
||||||
return m, m.listenForLogs()
|
return m, m.listenForLogs()
|
||||||
@@ -167,16 +180,7 @@ func (m Model) updateDependencyReviewState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if fingerprint is enabled
|
return m.enterAuthPhase()
|
||||||
if checkFingerprintEnabled() {
|
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0 // Default to fingerprint
|
|
||||||
return m, nil
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateSelectWindowManager
|
m.state = StateSelectWindowManager
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -230,6 +234,41 @@ func (m Model) installPackages() tea.Cmd {
|
|||||||
// Convert installer messages to TUI messages
|
// Convert installer messages to TUI messages
|
||||||
go func() {
|
go func() {
|
||||||
for msg := range installerProgressChan {
|
for msg := range installerProgressChan {
|
||||||
|
// Run optional greeter setup
|
||||||
|
if msg.Phase == distros.PhaseComplete && msg.IsComplete && msg.Error == nil {
|
||||||
|
greeterSelected := false
|
||||||
|
for _, dep := range m.dependencies {
|
||||||
|
if dep.Name == "dms-greeter" && !m.disabledItems["dms-greeter"] {
|
||||||
|
greeterSelected = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if greeterSelected {
|
||||||
|
compositorName := "niri"
|
||||||
|
if m.selectedWM == 1 {
|
||||||
|
compositorName = "Hyprland"
|
||||||
|
}
|
||||||
|
m.packageProgressChan <- packageInstallProgressMsg{
|
||||||
|
progress: 0.92,
|
||||||
|
step: "Configuring DMS greeter...",
|
||||||
|
logOutput: "Starting automated greeter setup...",
|
||||||
|
}
|
||||||
|
greeterLogFunc := func(line string) {
|
||||||
|
m.packageProgressChan <- packageInstallProgressMsg{
|
||||||
|
progress: 0.94,
|
||||||
|
step: "Configuring DMS greeter...",
|
||||||
|
logOutput: line,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := greeter.AutoSetupGreeter(compositorName, m.sudoPassword, greeterLogFunc); err != nil {
|
||||||
|
m.packageProgressChan <- packageInstallProgressMsg{
|
||||||
|
progress: 0.96,
|
||||||
|
step: "Greeter setup warning",
|
||||||
|
logOutput: fmt.Sprintf("⚠ Greeter auto-setup warning (non-fatal): %v", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
tuiMsg := packageInstallProgressMsg{
|
tuiMsg := packageInstallProgressMsg{
|
||||||
progress: msg.Progress,
|
progress: msg.Progress,
|
||||||
step: msg.Step,
|
step: msg.Step,
|
||||||
|
|||||||
@@ -56,14 +56,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
m.state = StateGentooGCCCheck
|
m.state = StateGentooGCCCheck
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if checkFingerprintEnabled() {
|
return m.enterAuthPhase()
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||||
@@ -75,14 +68,7 @@ func (m Model) updateGentooUseFlagsState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.selectedWM == 1 {
|
if m.selectedWM == 1 {
|
||||||
return m, m.checkGCCVersion()
|
return m, m.checkGCCVersion()
|
||||||
}
|
}
|
||||||
if checkFingerprintEnabled() {
|
return m.enterAuthPhase()
|
||||||
m.state = StateAuthMethodChoice
|
|
||||||
m.selectedConfig = 0
|
|
||||||
} else {
|
|
||||||
m.state = StatePasswordPrompt
|
|
||||||
m.passwordInput.Focus()
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
case "esc":
|
case "esc":
|
||||||
m.state = StateDependencyReview
|
m.state = StateDependencyReview
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ func dmsPackageName(distroID string, dependencies []deps.Dependency) string {
|
|||||||
if isGit {
|
if isGit {
|
||||||
return "dms-shell-git"
|
return "dms-shell-git"
|
||||||
}
|
}
|
||||||
return "dms-shell-bin"
|
return "dms-shell"
|
||||||
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
case distros.FamilyFedora, distros.FamilyUbuntu, distros.FamilyDebian, distros.FamilySUSE:
|
||||||
if isGit {
|
if isGit {
|
||||||
return "dms-git"
|
return "dms-git"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -274,8 +275,7 @@ func (m Model) delayThenReturn() tea.Cmd {
|
|||||||
|
|
||||||
func (m Model) tryFingerprint() tea.Cmd {
|
func (m Model) tryFingerprint() tea.Cmd {
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
clearCmd := exec.Command("sudo", "-k")
|
_ = privesc.ClearCache(context.Background())
|
||||||
clearCmd.Run()
|
|
||||||
|
|
||||||
tmpDir := os.TempDir()
|
tmpDir := os.TempDir()
|
||||||
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
askpassScript := filepath.Join(tmpDir, fmt.Sprintf("danklinux-fp-%d.sh", time.Now().UnixNano()))
|
||||||
@@ -289,15 +289,9 @@ func (m Model) tryFingerprint() tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-A", "-v")
|
if err := privesc.ValidateWithAskpass(ctx, askpassScript); err != nil {
|
||||||
cmd.Env = append(os.Environ(), fmt.Sprintf("SUDO_ASKPASS=%s", askpassScript))
|
|
||||||
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
return passwordValidMsg{password: "", valid: true}
|
return passwordValidMsg{password: "", valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -307,32 +301,9 @@ func (m Model) validatePassword(password string) tea.Cmd {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "sudo", "-S", "-v")
|
if err := privesc.ValidatePassword(ctx, password); err != nil {
|
||||||
|
|
||||||
stdin, err := cmd.StdinPipe()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
return passwordValidMsg{password: "", valid: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = fmt.Fprintf(stdin, "%s\n", password)
|
|
||||||
stdin.Close()
|
|
||||||
if err != nil {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cmd.Wait()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
return passwordValidMsg{password: "", valid: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
return passwordValidMsg{password: password, valid: true}
|
return passwordValidMsg{password: password, valid: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
133
core/internal/tui/views_privesc.go
Normal file
133
core/internal/tui/views_privesc.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/privesc"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m Model) viewSelectPrivesc() string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
b.WriteString(m.renderBanner())
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.styles.Title.Render("Privilege Escalation Tool"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(m.styles.Normal.Render("Multiple privilege tools are available. Choose one for installation:"))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
|
||||||
|
for i, t := range m.availablePrivesc {
|
||||||
|
label := fmt.Sprintf("%s — %s", t.Name(), privescToolDescription(t))
|
||||||
|
switch i {
|
||||||
|
case m.selectedPrivesc:
|
||||||
|
b.WriteString(m.styles.SelectedOption.Render("▶ " + label))
|
||||||
|
default:
|
||||||
|
b.WriteString(m.styles.Normal.Render(" " + label))
|
||||||
|
}
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
b.WriteString("\n")
|
||||||
|
b.WriteString(m.styles.Subtle.Render(fmt.Sprintf("Set %s=<tool> to skip this prompt in future runs.", privesc.EnvVar)))
|
||||||
|
b.WriteString("\n\n")
|
||||||
|
b.WriteString(m.styles.Subtle.Render("↑/↓: Navigate, Enter: Select, Esc: Back"))
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Model) updateSelectPrivescState(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||||
|
keyMsg, ok := msg.(tea.KeyMsg)
|
||||||
|
if !ok {
|
||||||
|
return m, m.listenForLogs()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keyMsg.String() {
|
||||||
|
case "up":
|
||||||
|
if m.selectedPrivesc > 0 {
|
||||||
|
m.selectedPrivesc--
|
||||||
|
}
|
||||||
|
case "down":
|
||||||
|
if m.selectedPrivesc < len(m.availablePrivesc)-1 {
|
||||||
|
m.selectedPrivesc++
|
||||||
|
}
|
||||||
|
case "enter":
|
||||||
|
chosen := m.availablePrivesc[m.selectedPrivesc]
|
||||||
|
if err := privesc.SetTool(chosen); err != nil {
|
||||||
|
m.err = fmt.Errorf("failed to select %s: %w", chosen.Name(), err)
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m.routeToAuthAfterPrivesc()
|
||||||
|
case "esc":
|
||||||
|
m.state = StateDependencyReview
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func privescToolDescription(t privesc.Tool) string {
|
||||||
|
switch t {
|
||||||
|
case privesc.ToolSudo:
|
||||||
|
return "classic sudo (supports password prompt in this installer)"
|
||||||
|
case privesc.ToolDoas:
|
||||||
|
return "OpenBSD-style doas (requires persist or nopass in /etc/doas.conf)"
|
||||||
|
case privesc.ToolRun0:
|
||||||
|
return "systemd run0 (authenticated via polkit)"
|
||||||
|
default:
|
||||||
|
return string(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// routeToAuthAfterPrivesc advances from the privesc-selection screen to the
|
||||||
|
// right auth flow. Sudo goes through the fingerprint/password path; doas and
|
||||||
|
// run0 skip password entry and proceed to install.
|
||||||
|
func (m Model) routeToAuthAfterPrivesc() (tea.Model, tea.Cmd) {
|
||||||
|
tool, err := privesc.Detect()
|
||||||
|
if err != nil {
|
||||||
|
m.err = err
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tool == privesc.ToolSudo {
|
||||||
|
if checkFingerprintEnabled() {
|
||||||
|
m.state = StateAuthMethodChoice
|
||||||
|
m.selectedConfig = 0
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
m.state = StatePasswordPrompt
|
||||||
|
m.passwordInput.Focus()
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.sudoPassword = ""
|
||||||
|
m.packageProgress = packageInstallProgressMsg{}
|
||||||
|
m.state = StateInstallingPackages
|
||||||
|
m.isLoading = true
|
||||||
|
return m, tea.Batch(m.spinner.Tick, m.installPackages())
|
||||||
|
}
|
||||||
|
|
||||||
|
// enterAuthPhase is called when dependency review (or the Gentoo screens)
|
||||||
|
// finish. It either routes directly to the sudo/fingerprint flow or shows
|
||||||
|
// the privesc-tool selection screen when multiple tools are available and
|
||||||
|
// no $DMS_PRIVESC override is set.
|
||||||
|
func (m Model) enterAuthPhase() (tea.Model, tea.Cmd) {
|
||||||
|
tools := privesc.AvailableTools()
|
||||||
|
_, envSet := privesc.EnvOverride()
|
||||||
|
|
||||||
|
if len(tools) == 0 {
|
||||||
|
m.err = fmt.Errorf("no supported privilege tool (sudo/doas/run0) found on PATH")
|
||||||
|
m.state = StateError
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if envSet || len(tools) == 1 {
|
||||||
|
return m.routeToAuthAfterPrivesc()
|
||||||
|
}
|
||||||
|
|
||||||
|
m.availablePrivesc = tools
|
||||||
|
m.selectedPrivesc = 0
|
||||||
|
m.state = StateSelectPrivesc
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
@@ -864,10 +864,12 @@ func (p *NiriWritableProvider) formatRule(rule windowrules.WindowRule) string {
|
|||||||
|
|
||||||
func formatSizeProperty(name, value string) string {
|
func formatSizeProperty(name, value string) string {
|
||||||
parts := strings.SplitN(value, " ", 2)
|
parts := strings.SplitN(value, " ", 2)
|
||||||
if len(parts) != 2 {
|
if len(parts) == 2 {
|
||||||
|
return fmt.Sprintf(" %s { %s %s; }", name, parts[0], parts[1])
|
||||||
|
}
|
||||||
|
// Bare number without type prefix — default to "fixed"
|
||||||
|
if _, err := strconv.Atoi(value); err == nil {
|
||||||
|
return fmt.Sprintf(" %s { fixed %s; }", name, value)
|
||||||
|
}
|
||||||
return fmt.Sprintf(" %s { }", name)
|
return fmt.Sprintf(" %s { }", name)
|
||||||
}
|
}
|
||||||
sizeType := parts[0]
|
|
||||||
sizeValue := parts[1]
|
|
||||||
return fmt.Sprintf(" %s { %s %s; }", name, sizeType, sizeValue)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct
|
qt6ct
|
||||||
|
Suggests: cups-pk-helper
|
||||||
Provides: dms
|
Provides: dms
|
||||||
Conflicts: dms
|
Conflicts: dms
|
||||||
Replaces: dms
|
Replaces: dms
|
||||||
|
|||||||
9
distro/debian/dms-greeter/_service
Normal file
9
distro/debian/dms-greeter/_service
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<services>
|
||||||
|
<!-- Download dms-qml source tarball from GitHub releases (greeter + quickshell content) -->
|
||||||
|
<service name="download_url">
|
||||||
|
<param name="protocol">https</param>
|
||||||
|
<param name="host">github.com</param>
|
||||||
|
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/dms-qml.tar.gz</param>
|
||||||
|
<param name="filename">dms-qml.tar.gz</param>
|
||||||
|
</service>
|
||||||
|
</services>
|
||||||
6
distro/debian/dms-greeter/debian/changelog
Normal file
6
distro/debian/dms-greeter/debian/changelog
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dms-greeter (1.4.2db8) unstable; urgency=medium
|
||||||
|
|
||||||
|
* Initial Debian OBS package
|
||||||
|
* Port from Ubuntu/Fedora packaging
|
||||||
|
|
||||||
|
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000
|
||||||
23
distro/debian/dms-greeter/debian/control
Normal file
23
distro/debian/dms-greeter/debian/control
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
Source: dms-greeter
|
||||||
|
Section: x11
|
||||||
|
Priority: optional
|
||||||
|
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
|
||||||
|
Build-Depends: debhelper-compat (= 13)
|
||||||
|
Standards-Version: 4.6.2
|
||||||
|
Homepage: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
|
||||||
|
Package: dms-greeter
|
||||||
|
Architecture: any
|
||||||
|
Depends: ${misc:Depends},
|
||||||
|
greetd,
|
||||||
|
quickshell-git | quickshell
|
||||||
|
Suggests: niri | hyprland | sway
|
||||||
|
Description: DankMaterialShell greeter for greetd
|
||||||
|
DankMaterialShell greeter for greetd login manager. A modern, Material Design 3
|
||||||
|
inspired greeter interface built with Quickshell for Wayland compositors.
|
||||||
|
.
|
||||||
|
Supports multiple compositors including Niri, Hyprland, and Sway with automatic
|
||||||
|
compositor detection and configuration. Features session selection, user
|
||||||
|
authentication, and dynamic theming.
|
||||||
27
distro/debian/dms-greeter/debian/copyright
Normal file
27
distro/debian/dms-greeter/debian/copyright
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||||
|
Upstream-Name: dms-greeter
|
||||||
|
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
|
||||||
|
Source: https://github.com/AvengeMedia/DankMaterialShell
|
||||||
|
|
||||||
|
Files: *
|
||||||
|
Copyright: 2026 Avenge Media LLC
|
||||||
|
License: MIT
|
||||||
|
|
||||||
|
License: MIT
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
.
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
.
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
108
distro/debian/dms-greeter/debian/postinst
Normal file
108
distro/debian/dms-greeter/debian/postinst
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
configure)
|
||||||
|
# Create greeter user/group if they don't exist
|
||||||
|
if ! getent group greeter >/dev/null; then
|
||||||
|
addgroup --system greeter
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! getent passwd greeter >/dev/null; then
|
||||||
|
adduser --system --ingroup greeter --home /var/lib/greeter \
|
||||||
|
--shell /bin/bash --gecos "System Greeter" greeter
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /var/cache/dms-greeter ]; then
|
||||||
|
chown -R greeter:greeter /var/cache/dms-greeter 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d /var/lib/greeter ]; then
|
||||||
|
chown -R greeter:greeter /var/lib/greeter 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check and set graphical.target as default
|
||||||
|
CURRENT_TARGET=$(systemctl get-default 2>/dev/null || echo "unknown")
|
||||||
|
if [ "$CURRENT_TARGET" != "graphical.target" ]; then
|
||||||
|
systemctl set-default graphical.target >/dev/null 2>&1 || true
|
||||||
|
TARGET_STATUS="Set to graphical.target (was: $CURRENT_TARGET) ✓"
|
||||||
|
else
|
||||||
|
TARGET_STATUS="Already graphical.target ✓"
|
||||||
|
fi
|
||||||
|
|
||||||
|
GREETD_CONFIG="/etc/greetd/config.toml"
|
||||||
|
CONFIG_STATUS="Not modified (already configured)"
|
||||||
|
|
||||||
|
# Check if niri or hyprland exists
|
||||||
|
COMPOSITOR="niri"
|
||||||
|
if ! command -v niri >/dev/null 2>&1; then
|
||||||
|
if command -v Hyprland >/dev/null 2>&1; then
|
||||||
|
COMPOSITOR="hyprland"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If config doesn't exist, create a default one
|
||||||
|
if [ ! -f "$GREETD_CONFIG" ]; then
|
||||||
|
mkdir -p /etc/greetd
|
||||||
|
cat > "$GREETD_CONFIG" << 'GREETD_EOF'
|
||||||
|
[terminal]
|
||||||
|
vt = 1
|
||||||
|
|
||||||
|
[default_session]
|
||||||
|
user = "greeter"
|
||||||
|
command = "/usr/bin/dms-greeter --command COMPOSITOR_PLACEHOLDER"
|
||||||
|
GREETD_EOF
|
||||||
|
sed -i "s|COMPOSITOR_PLACEHOLDER|$COMPOSITOR|" "$GREETD_CONFIG"
|
||||||
|
CONFIG_STATUS="Created new config with $COMPOSITOR ✓"
|
||||||
|
elif ! grep -q "dms-greeter" "$GREETD_CONFIG"; then
|
||||||
|
# Backup existing config
|
||||||
|
BACKUP_FILE="${GREETD_CONFIG}.backup-$(date +%Y%m%d-%H%M%S)"
|
||||||
|
cp "$GREETD_CONFIG" "$BACKUP_FILE" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Update command in default_session section
|
||||||
|
sed -i "/^\[default_session\]/,/^\[/ s|^command =.*|command = \"/usr/bin/dms-greeter --command $COMPOSITOR\"|" "$GREETD_CONFIG"
|
||||||
|
sed -i '/^\[default_session\]/,/^\[/ s|^user =.*|user = "greeter"|' "$GREETD_CONFIG"
|
||||||
|
CONFIG_STATUS="Updated existing config (backed up) with $COMPOSITOR ✓"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Only show banner on initial install
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
cat << 'EOF'
|
||||||
|
|
||||||
|
=========================================================================
|
||||||
|
DMS Greeter Installation Complete!
|
||||||
|
=========================================================================
|
||||||
|
|
||||||
|
Status:
|
||||||
|
EOF
|
||||||
|
echo " ✓ Greetd config: $CONFIG_STATUS"
|
||||||
|
echo " ✓ Default target: $TARGET_STATUS"
|
||||||
|
cat << 'EOF'
|
||||||
|
✓ Greeter user: Created
|
||||||
|
✓ Greeter directories: /var/cache/dms-greeter, /var/lib/greeter
|
||||||
|
|
||||||
|
Next steps:
|
||||||
|
|
||||||
|
1. Enable the greeter:
|
||||||
|
dms greeter enable
|
||||||
|
(This will automatically disable conflicting display managers,
|
||||||
|
set graphical.target, and enable greetd)
|
||||||
|
|
||||||
|
2. Sync your theme with the greeter (optional):
|
||||||
|
dms greeter sync
|
||||||
|
|
||||||
|
3. Check your setup:
|
||||||
|
dms greeter status
|
||||||
|
|
||||||
|
Ready to test? Run: sudo systemctl start greetd
|
||||||
|
Documentation: https://danklinux.com/docs/dankgreeter/
|
||||||
|
=========================================================================
|
||||||
|
|
||||||
|
EOF
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
||||||
14
distro/debian/dms-greeter/debian/postrm
Normal file
14
distro/debian/dms-greeter/debian/postrm
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
purge)
|
||||||
|
# Remove greeter cache directory on purge
|
||||||
|
rm -rf /var/cache/dms-greeter 2>/dev/null || true
|
||||||
|
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
#DEBHELPER#
|
||||||
|
|
||||||
|
exit 0
|
||||||
48
distro/debian/dms-greeter/debian/rules
Normal file
48
distro/debian/dms-greeter/debian/rules
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/make -f
|
||||||
|
|
||||||
|
export DH_VERBOSE = 1
|
||||||
|
|
||||||
|
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
|
||||||
|
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
|
||||||
|
|
||||||
|
%:
|
||||||
|
dh $@
|
||||||
|
|
||||||
|
override_dh_auto_build:
|
||||||
|
: nothing to build, we use prebuilt tarball content
|
||||||
|
|
||||||
|
override_dh_auto_install:
|
||||||
|
# Same pattern as dms: upstream from combined tarball (native format)
|
||||||
|
# Build root is either . (we're inside dms-qml) or has dms-qml/ subdir
|
||||||
|
SOURCE_DIR=""; \
|
||||||
|
if [ -d dms-qml ]; then SOURCE_DIR="dms-qml"; \
|
||||||
|
elif [ -f Modules/Greetd/assets/dms-greeter ]; then SOURCE_DIR="."; \
|
||||||
|
fi; \
|
||||||
|
if [ -n "$$SOURCE_DIR" ]; then \
|
||||||
|
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
|
||||||
|
( cd $$SOURCE_DIR && tar cf - --exclude=debian . ) | \
|
||||||
|
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
|
||||||
|
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
|
||||||
|
debian/dms-greeter/usr/bin/dms-greeter && \
|
||||||
|
install -Dm644 $$SOURCE_DIR/Modules/Greetd/README.md \
|
||||||
|
debian/dms-greeter/usr/share/doc/dms-greeter/README.md && \
|
||||||
|
install -Dm644 $$SOURCE_DIR/LICENSE \
|
||||||
|
debian/dms-greeter/usr/share/doc/dms-greeter/LICENSE && \
|
||||||
|
install -Dpm0644 $$SOURCE_DIR/systemd/tmpfiles-dms-greeter.conf \
|
||||||
|
debian/dms-greeter/usr/lib/tmpfiles.d/dms-greeter.conf; \
|
||||||
|
else \
|
||||||
|
echo "ERROR: No upstream source (dms-qml or Modules/Greetd/assets/dms-greeter)!" && \
|
||||||
|
echo "Contents of current directory:" && ls -la && exit 1; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Remove build and development files
|
||||||
|
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/core
|
||||||
|
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/distro
|
||||||
|
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.git*
|
||||||
|
rm -f debian/dms-greeter/usr/share/quickshell/dms-greeter/.gitignore
|
||||||
|
rm -rf debian/dms-greeter/usr/share/quickshell/dms-greeter/.github
|
||||||
|
|
||||||
|
override_dh_auto_clean:
|
||||||
|
rm -rf dms-qml
|
||||||
|
# When build root is dms-qml itself, we're inside it - nothing extra to remove
|
||||||
|
dh_auto_clean
|
||||||
1
distro/debian/dms-greeter/debian/source/format
Normal file
1
distro/debian/dms-greeter/debian/source/format
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.0 (native)
|
||||||
1
distro/debian/dms-greeter/debian/source/options
Normal file
1
distro/debian/dms-greeter/debian/source/options
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# OBS _service downloads dms-qml.tar.gz; no extra excludes needed
|
||||||
@@ -9,7 +9,7 @@ Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
|
|||||||
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
|
||||||
|
|
||||||
Package: dms
|
Package: dms
|
||||||
Architecture: amd64
|
Architecture: amd64 arm64
|
||||||
Depends: ${misc:Depends},
|
Depends: ${misc:Depends},
|
||||||
quickshell | quickshell-git,
|
quickshell | quickshell-git,
|
||||||
accountsservice,
|
accountsservice,
|
||||||
@@ -28,6 +28,7 @@ Depends: ${misc:Depends},
|
|||||||
qml6-module-qtquick-templates,
|
qml6-module-qtquick-templates,
|
||||||
qml6-module-qtquick-window,
|
qml6-module-qtquick-window,
|
||||||
qt6ct
|
qt6ct
|
||||||
|
Suggests: cups-pk-helper
|
||||||
Conflicts: dms-git
|
Conflicts: dms-git
|
||||||
Replaces: dms-git
|
Replaces: dms-git
|
||||||
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
Description: DankMaterialShell - Modern Wayland Desktop Shell
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
dms-distropkg-amd64.gz
|
dms-distropkg-amd64.gz
|
||||||
|
dms-distropkg-arm64.gz
|
||||||
dms-source.tar.gz
|
dms-source.tar.gz
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# Include files that are normally excluded by .gitignore
|
# Include files that are normally excluded by .gitignore
|
||||||
# These are needed for the build process on Launchpad
|
# These are needed for the build process on Launchpad
|
||||||
tar-ignore = !dms-distropkg-amd64.gz
|
tar-ignore = !dms-distropkg-amd64.gz
|
||||||
|
tar-ignore = !dms-distropkg-arm64.gz
|
||||||
tar-ignore = !dms-source.tar.gz
|
tar-ignore = !dms-source.tar.gz
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ Recommends: quickshell-git
|
|||||||
# Recommended system packages
|
# Recommended system packages
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
|
Suggests: cups-pk-helper
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ Recommends: danksearch
|
|||||||
Recommends: matugen
|
Recommends: matugen
|
||||||
Recommends: NetworkManager
|
Recommends: NetworkManager
|
||||||
Recommends: qt6-qtmultimedia
|
Recommends: qt6-qtmultimedia
|
||||||
|
Suggests: cups-pk-helper
|
||||||
Suggests: qt6ct
|
Suggests: qt6ct
|
||||||
|
|
||||||
%description
|
%description
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -10,7 +9,7 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = [
|
packages = [
|
||||||
dmsPkgs.dms-shell
|
cfg.package
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||||
++ lib.optionals cfg.enableVPN [
|
++ lib.optionals cfg.enableVPN [
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
let
|
let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
cfg = config.programs.dank-material-shell.greeter;
|
cfg = config.programs.dank-material-shell.greeter;
|
||||||
|
cfgDms = config.programs.dank-material-shell;
|
||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
@@ -23,19 +24,20 @@ let
|
|||||||
lib.makeBinPath [
|
lib.makeBinPath [
|
||||||
cfg.quickshell.package
|
cfg.quickshell.package
|
||||||
compositorPackage
|
compositorPackage
|
||||||
|
pkgs.glib # provides gdbus, used by the fprintd hardware probe in GreeterContent.qml
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
lib.escapeShellArgs (
|
lib.escapeShellArgs (
|
||||||
[
|
[
|
||||||
"sh"
|
"sh"
|
||||||
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
||||||
"--cache-dir"
|
"--cache-dir"
|
||||||
cacheDir
|
cacheDir
|
||||||
"--command"
|
"--command"
|
||||||
cfg.compositor.name
|
cfg.compositor.name
|
||||||
"-p"
|
"-p"
|
||||||
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
"${cfg.package}/share/quickshell/dms"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||||
"-C"
|
"-C"
|
||||||
@@ -65,6 +67,21 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell.greeter = {
|
options.programs.dank-material-shell.greeter = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
||||||
|
package = lib.mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
|
||||||
|
defaultText = lib.literalExpression ''
|
||||||
|
if config.programs.dank-material-shell.enable
|
||||||
|
then config.programs.dank-material-shell.package
|
||||||
|
else built from source;
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
The DankMaterialShell package to use for the greeter.
|
||||||
|
|
||||||
|
Defaults to the package from `programs.dank-material-shell` if it is enabled,
|
||||||
|
otherwise defaults to building from source.
|
||||||
|
'';
|
||||||
|
};
|
||||||
compositor.name = lib.mkOption {
|
compositor.name = lib.mkOption {
|
||||||
type = types.enum [
|
type = types.enum [
|
||||||
"niri"
|
"niri"
|
||||||
@@ -179,7 +196,9 @@ in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f settings.json ]; then
|
if [ -f settings.json ]; then
|
||||||
if cp "$(${jq} -r '.customThemeFile' settings.json)" custom-theme.json; then
|
theme_file="$(${jq} -r '.customThemeFile // empty' settings.json)"
|
||||||
|
if [ -f "$theme_file" ] && [ -r "$theme_file" ]; then
|
||||||
|
cp "$theme_file" custom-theme.json
|
||||||
mv settings.json settings.orig.json
|
mv settings.json settings.orig.json
|
||||||
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
${jq} '.customThemeFile = "${cacheDir}/custom-theme.json"' settings.orig.json > settings.json
|
||||||
fi
|
fi
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user